Skip to content

Commit ce5e03e

Browse files
committed
Handle white spaces in yaml and json
1 parent 339e0f3 commit ce5e03e

File tree

3 files changed

+137
-33
lines changed

3 files changed

+137
-33
lines changed

src/context/semantic/LogicalIdReferenceFinder.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { SyntaxNode } from 'tree-sitter';
22
import { DocumentType } from '../../document/Document';
33
import { PseudoParametersSet, ResourceAttributes } from '../ContextType';
44

5+
/* eslint-disable no-restricted-syntax, security/detect-unsafe-regex */
56
export function selectText(specificNode: SyntaxNode, fullEntitySearch: boolean, rootNode?: SyntaxNode): string {
67
let text: string | undefined;
78
if (fullEntitySearch) {
@@ -81,23 +82,23 @@ function findYamlIntrinsicReferences(text: string, logicalIds: Set<string>): voi
8182
if (text.includes('!Condition')) {
8283
extractMatches(text, YamlConditionShort, logicalIds);
8384
}
84-
if (text.includes('Ref:')) {
85+
if (text.includes('Ref')) {
8586
extractMatches(text, YamlRefColon, logicalIds);
8687
}
87-
if (text.includes('Fn::GetAtt:')) {
88+
if (text.includes('Fn::GetAtt')) {
8889
extractMatches(text, YamlGetAttColon, logicalIds);
8990
extractMatches(text, YamlGetAttColonString, logicalIds);
9091
}
91-
if (text.includes('Fn::FindInMap:')) {
92+
if (text.includes('Fn::FindInMap')) {
9293
extractMatches(text, YamlFindInMapColon, logicalIds);
9394
}
94-
if (text.includes('Fn::If:')) {
95+
if (text.includes('Fn::If')) {
9596
extractMatches(text, YamlIfColon, logicalIds);
9697
}
97-
if (text.includes('Condition:')) {
98+
if (text.includes('Condition')) {
9899
extractMatches(text, YamlCondition, logicalIds);
99100
}
100-
if (text.includes('Fn::ValueOf:')) {
101+
if (text.includes('Fn::ValueOf')) {
101102
extractMatches(text, YamlValueOf, logicalIds);
102103
}
103104
// Extract all ${} variables in one pass - covers !Sub, Fn::Sub:, and standalone
@@ -108,7 +109,7 @@ function findYamlIntrinsicReferences(text: string, logicalIds: Set<string>): voi
108109
if (text.includes('- ')) {
109110
extractMatches(text, YamlListItem, logicalIds);
110111
}
111-
if (text.includes('DependsOn:')) {
112+
if (text.includes('DependsOn')) {
112113
extractYamlDependsOnReferences(text, logicalIds);
113114
}
114115
}
@@ -186,7 +187,7 @@ const CommonProperties = new Set(
186187
].flatMap((word) => [word, word.toUpperCase(), word.toLowerCase()]),
187188
);
188189

189-
// Pre-compiled for performance - exported for testing/analysis
190+
// Pre-compiled for performance
190191
const JsonRef = /"Ref"\s*:\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Ref": "LogicalId"} - references to parameters, resources, etc.
191192
const JsonGetAtt = /"Fn::GetAtt"\s*:\s*\[\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Fn::GetAtt": ["LogicalId", "Attribute"]} - gets attributes from resources
192193
const JsonGetAttString = /"Fn::GetAtt"\s*:\s*"([A-Za-z][A-Za-z0-9]*)\./g; // Matches {"Fn::GetAtt": "LogicalId.Attribute"} - string syntax
@@ -199,31 +200,31 @@ const JsonArrayItem = /"([A-Za-z][A-Za-z0-9]*)"/g; // Matches "LogicalId" within
199200
const JsonValueOf = /"Fn::ValueOf"\s*:\s*\[\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Fn::ValueOf": ["ParamName", "Attr"]} - gets parameter attribute
200201

201202
const YamlRef = /!Ref\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !Ref LogicalId - YAML short form reference
202-
const YamlGetAtt = /!GetAtt\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt LogicalId.Attribute - YAML short form get attribute
203-
const YamlGetAttArray = /!GetAtt\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt [LogicalId, Attribute] - YAML short form get attribute with array syntax
203+
const YamlGetAtt = /!GetAtt\s+['"]?([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt LogicalId.Attribute - YAML short form get attribute with optional quotes
204+
const YamlGetAttArray = /!GetAtt\s+\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches !GetAtt [LogicalId, Attribute] - YAML short form get attribute with array syntax
204205
const YamlFindInMap = /!FindInMap\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !FindInMap [MappingName, Key1, Key2] - YAML short form mapping lookup
205206
const YamlIf = /!If\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !If [ConditionName, TrueValue, FalseValue] - YAML short form conditional
206207
const YamlConditionShort = /!Condition\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !Condition ConditionName - YAML short form condition reference
207-
const YamlRefColon = /Ref:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Ref: LogicalId - YAML long form reference
208-
const YamlGetAttColon = /Fn::GetAtt:\s*\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Fn::GetAtt: [LogicalId, Attribute] - YAML long form get attribute
209-
const YamlGetAttColonString = /Fn::GetAtt:\s*([A-Za-z][A-Za-z0-9]*)\./g; // Matches Fn::GetAtt: LogicalId.Attribute - YAML long form string syntax
210-
const YamlFindInMapColon = /Fn::FindInMap:\s*\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Fn::FindInMap: [MappingName, Key1, Key2] - YAML long form mapping lookup
211-
const YamlIfColon = /Fn::If:\s*\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Fn::If: [ConditionName, TrueValue, FalseValue] - YAML long form conditional
212-
const YamlCondition = /Condition:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Condition: ConditionName - resource condition property in YAML
213-
const YamlSingleDep = /DependsOn:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches DependsOn: LogicalId - single resource dependency in YAML
214-
const YamlInlineDeps = /DependsOn:\s*\[([^\]]+)]/g; // Matches DependsOn: [Id1, Id2] - inline array format in YAML
208+
const YamlRefColon = /(?<![A-Za-z])['"]?Ref['"]?\s*:\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Ref:, 'Ref':, "Ref": LogicalId with optional quoted values
209+
const YamlGetAttColon = /['"]?Fn::GetAtt['"]?\s*:\s*\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Fn::GetAtt:, 'Fn::GetAtt':, "Fn::GetAtt": [LogicalId, Attribute]
210+
const YamlGetAttColonString = /['"]?Fn::GetAtt['"]?\s*:\s*['"]?([A-Za-z][A-Za-z0-9]*)\./g; // Matches Fn::GetAtt: LogicalId.Attribute with optional quotes
211+
const YamlFindInMapColon = /['"]?Fn::FindInMap['"]?\s*:\s*\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Fn::FindInMap: [MappingName, ...] with optional quotes
212+
const YamlIfColon = /['"]?Fn::If['"]?\s*:\s*\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Fn::If: [ConditionName, ...] with optional quotes
213+
const YamlCondition = /(?<![A-Za-z])['"]?Condition['"]?\s*:\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Condition:, 'Condition':, "Condition": ConditionName with optional quoted values
214+
const YamlSingleDep = /(?<![A-Za-z])['"]?DependsOn['"]?\s*:\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches DependsOn: LogicalId with optional quotes
215+
const YamlInlineDeps = /(?<![A-Za-z])['"]?DependsOn['"]?\s*:\s*\[([^\]]+)]/g; // Matches DependsOn: [Id1, Id2] with optional quotes
215216
const YamlListItem = /-\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches - LogicalId in YAML list format
216217
const YamlInlineItemPattern = /([A-Za-z][A-Za-z0-9]*)/g; // Matches LogicalId within the inline array
217-
const YamlValueOf = /Fn::ValueOf:\s*\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Fn::ValueOf: [ParamName, Attr] - gets parameter attribute
218+
const YamlValueOf = /['"]?Fn::ValueOf['"]?\s*:\s*\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Fn::ValueOf: [ParamName, Attr] with optional quotes
218219

219220
// Shared pattern for ${} variables - used by both JSON and YAML
220221
const SubVariables = /\$\{([A-Za-z][A-Za-z0-9]*)(?:[.:]|(?=\}))/g; // Matches ${LogicalId} or ${Resource.Attr} or ${AWS::Region} - captures first segment only
221222

222223
const ValidLogicalId = /^[A-Za-z][A-Za-z0-9.]+$/;
223224

224225
// Validated these regex, they will fail fast with ?= lookahead
225-
// eslint-disable-next-line security/detect-unsafe-regex
226-
const YamlListDep = /DependsOn:\s*\n(\s*-\s*[A-Za-z][A-Za-z0-9]*(?:\s+-\s*[A-Za-z][A-Za-z0-9]*)*)/g; // Matches DependsOn: followed by YAML list items
226+
const YamlListDep =
227+
/(?<![A-Za-z])['"]?DependsOn['"]?\s*:\s*\n(\s*-\s*[A-Za-z][A-Za-z0-9]*(?:\s+-\s*[A-Za-z][A-Za-z0-9]*)*)/g; // Matches DependsOn: followed by YAML list items
227228

228229
export function isLogicalIdCandidate(str: unknown): boolean {
229230
if (!str || typeof str !== 'string' || str.length < 2) return false;

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ describe('Intrinsic Function Coverage', () => {
3838
const result = referencedLogicalIds('Fn::GetAtt: MyResource.Arn', '', DocumentType.YAML);
3939
expect(result).toEqual(new Set(['MyResource']));
4040
});
41+
42+
it('!GetAtt quoted dot notation', () => {
43+
const result = referencedLogicalIds('!GetAtt "MyResource.Arn"', '', DocumentType.YAML);
44+
expect(result).toEqual(new Set(['MyResource']));
45+
});
46+
47+
it('!GetAtt quoted array notation', () => {
48+
const result = referencedLogicalIds('!GetAtt ["MyResource", "Arn"]', '', DocumentType.YAML);
49+
expect(result).toEqual(new Set(['MyResource']));
50+
});
4151
});
4252

4353
// Fn::Sub - substitutes variables in strings
@@ -91,6 +101,11 @@ describe('Intrinsic Function Coverage', () => {
91101
const result = referencedLogicalIds('Fn::If: [MyCondition, yes, no]', '', DocumentType.YAML);
92102
expect(result).toEqual(new Set(['MyCondition']));
93103
});
104+
105+
it('Fn::If with space before colon', () => {
106+
const result = referencedLogicalIds('Fn::If : [MyCondition, yes, no]', '', DocumentType.YAML);
107+
expect(result).toEqual(new Set(['MyCondition']));
108+
});
94109
});
95110

96111
// Condition - resource condition attribute
@@ -779,4 +794,72 @@ describe('Intrinsic Function Coverage', () => {
779794
});
780795
});
781796
});
797+
798+
describe('Quoted YAML keys', () => {
799+
it('single-quoted Ref', () => {
800+
const result = referencedLogicalIds("'Ref': MyResource", '', DocumentType.YAML);
801+
expect(result).toEqual(new Set(['MyResource']));
802+
});
803+
804+
it('double-quoted Ref', () => {
805+
const result = referencedLogicalIds('"Ref": MyResource', '', DocumentType.YAML);
806+
expect(result).toEqual(new Set(['MyResource']));
807+
});
808+
809+
it('single-quoted Condition', () => {
810+
const result = referencedLogicalIds("'Condition': MyCondition", '', DocumentType.YAML);
811+
expect(result).toEqual(new Set(['MyCondition']));
812+
});
813+
814+
it('double-quoted Fn::If', () => {
815+
const result = referencedLogicalIds('"Fn::If": [MyCondition, yes, no]', '', DocumentType.YAML);
816+
expect(result).toEqual(new Set(['MyCondition']));
817+
});
818+
819+
it('single-quoted Fn::GetAtt', () => {
820+
const result = referencedLogicalIds("'Fn::GetAtt': [MyResource, Arn]", '', DocumentType.YAML);
821+
expect(result).toEqual(new Set(['MyResource']));
822+
});
823+
824+
it('double-quoted DependsOn', () => {
825+
const result = referencedLogicalIds('"DependsOn": MyResource', '', DocumentType.YAML);
826+
expect(result).toEqual(new Set(['MyResource']));
827+
});
828+
});
829+
830+
describe('Whitespace handling', () => {
831+
describe('YAML', () => {
832+
it('space before colon', () => {
833+
const result = referencedLogicalIds('Ref : MyResource', '', DocumentType.YAML);
834+
expect(result).toEqual(new Set(['MyResource']));
835+
});
836+
837+
it('multiple spaces around colon', () => {
838+
const result = referencedLogicalIds('Condition : MyCondition', '', DocumentType.YAML);
839+
expect(result).toEqual(new Set(['MyCondition']));
840+
});
841+
842+
it('no space after colon', () => {
843+
const result = referencedLogicalIds('Ref:MyResource', '', DocumentType.YAML);
844+
expect(result).toEqual(new Set(['MyResource']));
845+
});
846+
});
847+
848+
describe('JSON', () => {
849+
it('space before colon', () => {
850+
const result = referencedLogicalIds('{"Ref" : "MyResource"}', '', DocumentType.JSON);
851+
expect(result).toEqual(new Set(['MyResource']));
852+
});
853+
854+
it('multiple spaces around colon', () => {
855+
const result = referencedLogicalIds('{"Ref" : "MyResource"}', '', DocumentType.JSON);
856+
expect(result).toEqual(new Set(['MyResource']));
857+
});
858+
859+
it('no space after colon', () => {
860+
const result = referencedLogicalIds('{"Ref":"MyResource"}', '', DocumentType.JSON);
861+
expect(result).toEqual(new Set(['MyResource']));
862+
});
863+
});
864+
});
782865
});

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,6 @@ describe('LogicalIdReferenceFinder', () => {
9898
const result = referencedLogicalIds(text, '', DocumentType.JSON);
9999
expect(result).toEqual(new Set(['Resource1', 'Resource2']));
100100
});
101-
102-
it('should handle Ref with whitespace', () => {
103-
const text = '{ "Ref" : "MyResource" }';
104-
const result = referencedLogicalIds(text, '', DocumentType.JSON);
105-
expect(result).toEqual(new Set(['MyResource']));
106-
});
107101
});
108102

109103
describe('Fn::GetAtt pattern', () => {
@@ -112,12 +106,6 @@ describe('LogicalIdReferenceFinder', () => {
112106
const result = referencedLogicalIds(text, '', DocumentType.JSON);
113107
expect(result).toEqual(new Set(['MyResource']));
114108
});
115-
116-
it('should handle GetAtt with whitespace', () => {
117-
const text = '{ "Fn::GetAtt" : [ "MyResource" , "Arn" ] }';
118-
const result = referencedLogicalIds(text, '', DocumentType.JSON);
119-
expect(result).toEqual(new Set(['MyResource']));
120-
});
121109
});
122110

123111
describe('Fn::FindInMap pattern', () => {
@@ -378,6 +366,38 @@ describe('LogicalIdReferenceFinder', () => {
378366
});
379367
});
380368

369+
describe('False positive prevention', () => {
370+
it('should not match SomeRef as Ref', () => {
371+
const text = 'SomeRef: MyValue';
372+
const result = referencedLogicalIds(text, '', DocumentType.YAML);
373+
expect(result).toEqual(new Set());
374+
});
375+
376+
it('should not match MyCondition as Condition', () => {
377+
const text = 'MyCondition: SomeValue';
378+
const result = referencedLogicalIds(text, '', DocumentType.YAML);
379+
expect(result).toEqual(new Set());
380+
});
381+
382+
it('should not match PreDependsOn as DependsOn', () => {
383+
const text = 'PreDependsOn: MyResource';
384+
const result = referencedLogicalIds(text, '', DocumentType.YAML);
385+
expect(result).toEqual(new Set());
386+
});
387+
388+
it('should match standalone Ref', () => {
389+
const text = 'Ref: MyResource';
390+
const result = referencedLogicalIds(text, '', DocumentType.YAML);
391+
expect(result).toEqual(new Set(['MyResource']));
392+
});
393+
394+
it('should match Condition at start of line', () => {
395+
const text = ' Condition: MyCondition';
396+
const result = referencedLogicalIds(text, '', DocumentType.YAML);
397+
expect(result).toEqual(new Set(['MyCondition']));
398+
});
399+
});
400+
381401
describe('isLogicalIdCandidate', () => {
382402
describe('valid logical IDs', () => {
383403
it('should accept simple alphanumeric IDs', () => {

0 commit comments

Comments
 (0)