Skip to content

Commit ac7561c

Browse files
authored
Handle non-string parameter values in hover (#536)
1 parent e405e5d commit ac7561c

6 files changed

Lines changed: 112 additions & 9 deletions

File tree

src/context/semantic/Entity.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { stringToBoolean } from '../../utils/String';
1+
import { stringToBoolean, toStringOrUndefined } from '../../utils/String';
22
import { toNumber } from '../../utils/TypeConverters';
33
import { EntityType } from '../CloudFormationEnums';
44
import { CfnIntrinsicFunction, CfnValue, MappingValueType } from './CloudFormationTypes';
5-
import { coerceParameterToTypedValues, ParameterType, ParameterValueType } from './ParameterType';
5+
import { coerceParameterToTypedValues, ParameterType, ParameterValueType, PARAMETER_TYPES } from './ParameterType';
66

77
export abstract class Entity {
88
private _keys!: ReadonlyArray<string>;
@@ -114,12 +114,14 @@ export class Parameter extends Entity {
114114

115115
return new Parameter(
116116
logicalId,
117-
object['Type'] as ParameterType | undefined,
117+
typeof object['Type'] === 'string' && PARAMETER_TYPES.includes(object['Type'] as ParameterType)
118+
? (object['Type'] as ParameterType)
119+
: undefined,
118120
Default,
119-
object['AllowedPattern'] as string | undefined,
121+
toStringOrUndefined(object['AllowedPattern']),
120122
AllowedValues,
121-
object['ConstraintDescription'] as string | undefined,
122-
object['Description'] as string | undefined,
123+
toStringOrUndefined(object['ConstraintDescription']),
124+
toStringOrUndefined(object['Description']),
123125
object['MaxLength'] === undefined ? undefined : toNumber(object['MaxLength']),
124126
object['MaxValue'] === undefined ? undefined : toNumber(object['MaxValue']),
125127
object['MinLength'] === undefined ? undefined : toNumber(object['MinLength']),

src/utils/String.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,9 @@ export function byteSize(str: string) {
3838
export function formatNumber(value: number, decimals: number = 2) {
3939
return value.toFixed(decimals);
4040
}
41+
42+
export function toStringOrUndefined(value: unknown): string | undefined {
43+
if (typeof value === 'string') return value;
44+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
45+
return undefined;
46+
}

tst/unit/autocomplete/EntityFieldCompletionProvider.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('EntityFieldCompletionProvider', () => {
4242
const mockContext = createParameterContext('MyParameter', {
4343
text: 'e',
4444
data: {
45-
Type: 'string',
45+
Type: 'String',
4646
Description: 'some description',
4747
},
4848
propertyPath: ['Parameters', 'MyParameter', 'e'],

tst/unit/context/semantic/Entity.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,58 @@ describe('Entity', () => {
100100
expect(parameter.name).toBe('TestParam');
101101
expect(parameter.NoEcho).toBe(true);
102102
});
103+
104+
it('should handle intrinsic functions in string fields by setting them to undefined', () => {
105+
const data = {
106+
Type: { 'Fn::Sub': 'String' },
107+
Description: { 'Fn::Sub': 'Repository for ${AWS::StackName}' },
108+
ConstraintDescription: { 'Fn::If': ['Condition', 'Valid', 'Invalid'] },
109+
AllowedPattern: { 'Fn::Sub': '^${AWS::StackName}.*' },
110+
Default: 'valid-default',
111+
} as any;
112+
113+
const parameter = Parameter.from('TestParam', data);
114+
115+
expect(parameter.name).toBe('TestParam');
116+
expect(parameter.Type).toBeUndefined();
117+
expect(parameter.Default).toBe('valid-default');
118+
expect(parameter.Description).toBeUndefined();
119+
expect(parameter.ConstraintDescription).toBeUndefined();
120+
expect(parameter.AllowedPattern).toBeUndefined();
121+
});
122+
123+
it('should handle invalid string values in Type field', () => {
124+
const data = {
125+
Type: 'InvalidParameterType',
126+
Description: 'Valid description',
127+
Default: 'valid-default',
128+
} as any;
129+
130+
const parameter = Parameter.from('TestParam', data);
131+
132+
expect(parameter.name).toBe('TestParam');
133+
expect(parameter.Type).toBeUndefined();
134+
expect(parameter.Description).toBe('Valid description');
135+
expect(parameter.Default).toBe('valid-default');
136+
});
137+
138+
it('should coerce numbers and booleans to strings', () => {
139+
const data = {
140+
Type: ParameterType.String,
141+
Description: 123,
142+
ConstraintDescription: true,
143+
AllowedPattern: false,
144+
Default: 'valid-default',
145+
} as any;
146+
147+
const parameter = Parameter.from('TestParam', data);
148+
149+
expect(parameter.name).toBe('TestParam');
150+
expect(parameter.Type).toBe(ParameterType.String);
151+
expect(parameter.Description).toBe('123');
152+
expect(parameter.ConstraintDescription).toBe('true');
153+
expect(parameter.AllowedPattern).toBe('false');
154+
expect(parameter.Default).toBe('valid-default');
155+
});
103156
});
104157
});

tst/unit/hover/ParameterHoverProvider.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2+
import { ParameterType } from '../../../src/context/semantic/ParameterType';
23
import { ParameterHoverProvider } from '../../../src/hover/ParameterHoverProvider';
34
import { createParameterContext } from '../../utils/MockContext';
45

@@ -8,7 +9,7 @@ describe('ParameterHoverProvider', () => {
89
it('should return parameter information from template', () => {
910
const mockContext = createParameterContext('EnvironmentType', {
1011
data: {
11-
Type: 'String' as any,
12+
Type: ParameterType.String,
1213
Default: 'dev',
1314
Description: 'Environment type',
1415
AllowedValues: ['dev', 'test', 'prod'],
@@ -27,5 +28,24 @@ describe('ParameterHoverProvider', () => {
2728
expect(result).toContain('- prod');
2829
expect(result).toContain('**Constraint Description:** Must be dev, test, or prod');
2930
});
31+
32+
it('should handle parameter with intrinsic function in Description without crashing', () => {
33+
const mockContext = createParameterContext('EcrRepoName', {
34+
data: {
35+
Type: 'String' as any,
36+
Default: 'my-repo',
37+
Description: { 'Fn::Sub': 'Repository for ${AWS::StackName}' },
38+
},
39+
});
40+
41+
// Should not throw an error
42+
const result = parameterHoverProvider.getInformation(mockContext);
43+
44+
expect(result).toContain('(parameter) EcrRepoName: string');
45+
expect(result).toContain('**Type:** String');
46+
expect(result).toContain('**Default Value:** "my-repo"');
47+
expect(result).not.toContain('Description');
48+
expect(result).not.toContain('Fn::Sub');
49+
});
3050
});
3151
});

tst/unit/utils/String.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { dashesToUnderscores } from '../../../src/utils/String';
2+
import { dashesToUnderscores, toStringOrUndefined } from '../../../src/utils/String';
33

44
describe('String', () => {
55
describe('dashesToUnderscores', () => {
@@ -12,4 +12,26 @@ describe('String', () => {
1212
expect(dashesToUnderscores('test-123_abc-xyz')).toBe('test_123_abc_xyz');
1313
});
1414
});
15+
16+
describe('toStringOrUndefined', () => {
17+
it('should return string values unchanged', () => {
18+
expect(toStringOrUndefined('hello')).toBe('hello');
19+
expect(toStringOrUndefined('')).toBe('');
20+
expect(toStringOrUndefined('123')).toBe('123');
21+
});
22+
23+
it('should convert numbers and booleans to strings', () => {
24+
expect(toStringOrUndefined(123)).toBe('123');
25+
expect(toStringOrUndefined(true)).toBe('true');
26+
expect(toStringOrUndefined(false)).toBe('false');
27+
});
28+
29+
it('should return undefined for objects, null, undefined, arrays', () => {
30+
expect(toStringOrUndefined(null)).toBeUndefined();
31+
expect(toStringOrUndefined(undefined)).toBeUndefined();
32+
expect(toStringOrUndefined({})).toBeUndefined();
33+
expect(toStringOrUndefined([])).toBeUndefined();
34+
expect(toStringOrUndefined({ 'Fn::Sub': 'test' })).toBeUndefined();
35+
});
36+
});
1537
});

0 commit comments

Comments
 (0)