Skip to content

Commit 964ce8e

Browse files
committed
Improve intrinsic functions and add json fallback parsing
1 parent 205779c commit 964ce8e

File tree

6 files changed

+916
-288
lines changed

6 files changed

+916
-288
lines changed

src/context/semantic/LogicalIdReferenceFinder.ts

Lines changed: 28 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -35,44 +35,29 @@ export function referencedLogicalIds(
3535
}
3636

3737
function findJsonIntrinsicReferences(text: string, logicalIds: Set<string>): void {
38-
// Single pass through text with combined regex for better performance
39-
const refIndex = text.indexOf('"Ref"');
40-
const getAttIndex = text.indexOf('"Fn::GetAtt"');
41-
const findMapIndex = text.indexOf('"Fn::FindInMap"');
42-
const subIndex = text.indexOf('"Fn::Sub"');
43-
const ifIndex = text.indexOf('"Fn::If"');
44-
const conditionIndex = text.indexOf('"Condition"');
45-
const dependsIndex = text.indexOf('"DependsOn"');
46-
const subVarIndex = text.indexOf('${');
47-
48-
if (refIndex !== -1) {
38+
// Early exit checks - only run regex if marker exists
39+
if (text.includes('"Ref"')) {
4940
extractMatches(text, JsonRef, logicalIds);
5041
}
51-
if (getAttIndex !== -1) {
42+
if (text.includes('"Fn::GetAtt"')) {
5243
extractMatches(text, JsonGetAtt, logicalIds);
5344
}
54-
if (findMapIndex !== -1) {
45+
if (text.includes('"Fn::FindInMap"')) {
5546
extractMatches(text, JsonFindInMap, logicalIds);
5647
}
57-
if (subIndex !== -1) {
58-
let subMatch: RegExpExecArray | null;
59-
while ((subMatch = JsonSub.exec(text)) !== null) {
60-
const templateString = subMatch[1];
61-
extractMatches(templateString, JsonSubVariables, logicalIds);
62-
}
63-
}
64-
if (ifIndex !== -1) {
48+
if (text.includes('"Fn::If"')) {
6549
extractMatches(text, JsonIf, logicalIds);
6650
}
67-
if (conditionIndex !== -1) {
51+
if (text.includes('"Condition"')) {
6852
extractMatches(text, JsonCondition, logicalIds);
6953
}
70-
if (subVarIndex !== -1) {
71-
extractMatches(text, JsonSubVariables, logicalIds);
72-
}
73-
if (dependsIndex !== -1) {
54+
if (text.includes('"DependsOn"')) {
7455
extractJsonDependsOnReferences(text, logicalIds);
7556
}
57+
// Extract all ${} variables in one pass - covers Fn::Sub and standalone
58+
if (text.includes('${')) {
59+
extractMatches(text, SubVariables, logicalIds);
60+
}
7661
}
7762

7863
function findYamlIntrinsicReferences(text: string, logicalIds: Set<string>): void {
@@ -86,14 +71,8 @@ function findYamlIntrinsicReferences(text: string, logicalIds: Set<string>): voi
8671
if (text.includes('!FindInMap')) {
8772
extractMatches(text, YamlFindInMap, logicalIds);
8873
}
89-
90-
// Extract template strings from !Sub and find variables within them
91-
if (text.includes('!Sub')) {
92-
let subMatch: RegExpExecArray | null;
93-
while ((subMatch = YamlSub.exec(text)) !== null) {
94-
const templateString = subMatch[1];
95-
extractMatches(templateString, YamlSubVariables, logicalIds);
96-
}
74+
if (text.includes('!If')) {
75+
extractMatches(text, YamlIf, logicalIds);
9776
}
9877
if (text.includes('Ref:')) {
9978
extractMatches(text, YamlRefColon, logicalIds);
@@ -104,27 +83,22 @@ function findYamlIntrinsicReferences(text: string, logicalIds: Set<string>): voi
10483
if (text.includes('Fn::FindInMap:')) {
10584
extractMatches(text, YamlFindInMapColon, logicalIds);
10685
}
107-
108-
// Extract template strings from Fn::Sub and find variables within them
109-
if (text.includes('Fn::Sub:')) {
110-
let subMatch: RegExpExecArray | null;
111-
while ((subMatch = YamlSubColon.exec(text)) !== null) {
112-
const templateString = subMatch[1];
113-
extractMatches(templateString, YamlSubVariables, logicalIds);
114-
}
86+
if (text.includes('Fn::If:')) {
87+
extractMatches(text, YamlIfColon, logicalIds);
11588
}
11689
if (text.includes('Condition:')) {
11790
extractMatches(text, YamlCondition, logicalIds);
11891
}
92+
if (text.includes('DependsOn:')) {
93+
extractYamlDependsOnReferences(text, logicalIds);
94+
}
95+
// Extract all ${} variables in one pass - covers !Sub, Fn::Sub:, and standalone
11996
if (text.includes('${')) {
120-
extractMatches(text, YamlSubVariables, logicalIds);
97+
extractMatches(text, SubVariables, logicalIds);
12198
}
99+
// Handle YAML list items (for Fn::GetAtt list syntax, DependsOn lists, etc.)
122100
if (text.includes('- ')) {
123-
extractMatches(text, YamlInlineListItem, logicalIds);
124-
}
125-
126-
if (text.includes('DependsOn:')) {
127-
extractYamlDependsOnReferences(text, logicalIds);
101+
extractMatches(text, YamlListItem, logicalIds);
128102
}
129103
}
130104

@@ -173,6 +147,7 @@ function extractYamlDependsOnReferences(text: string, logicalIds: Set<string>):
173147

174148
const CommonProperties = new Set(
175149
[
150+
'AWS',
176151
'Type',
177152
'Properties',
178153
...ResourceAttributes,
@@ -204,10 +179,8 @@ const CommonProperties = new Set(
204179
const JsonRef = /"Ref"\s*:\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Ref": "LogicalId"} - references to parameters, resources, etc.
205180
const JsonGetAtt = /"Fn::GetAtt"\s*:\s*\[\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Fn::GetAtt": ["LogicalId", "Attribute"]} - gets attributes from resources
206181
const JsonFindInMap = /"Fn::FindInMap"\s*:\s*\[\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Fn::FindInMap": ["MappingName", "Key1", "Key2"]} - lookups in mappings
207-
const JsonSub = /"Fn::Sub"\s*:\s*"([^"]+)"/g; // Matches {"Fn::Sub": "template string"} - string substitution with variables
208182
const JsonIf = /"Fn::If"\s*:\s*\[\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Fn::If": ["ConditionName", "TrueValue", "FalseValue"]} - conditional logic
209183
const JsonCondition = /"Condition"\s*:\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches "Condition": "ConditionName" - resource condition property
210-
const JsonSubVariables = /\$\{([A-Za-z][A-Za-z0-9:]*)\}/g; // Matches ${LogicalId} or ${AWS::Region} - variables in Fn::Sub templates
211184
const JsonSingleDep = /"DependsOn"\s*:\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches "DependsOn": "LogicalId" - single resource dependency
212185
const JsonArrayDep = /"DependsOn"\s*:\s*\[([^\]]+)]/g; // Matches "DependsOn": ["Id1", "Id2"] - array of resource dependencies
213186
const JsonArrayItem = /"([A-Za-z][A-Za-z0-9]*)"/g; // Matches "LogicalId" within the DependsOn array
@@ -216,26 +189,26 @@ const YamlRef = /!Ref\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !Ref LogicalId - YA
216189
const YamlGetAtt = /!GetAtt\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt LogicalId.Attribute - YAML short form get attribute
217190
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
218191
const YamlFindInMap = /!FindInMap\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !FindInMap [MappingName, Key1, Key2] - YAML short form mapping lookup
219-
const YamlSub = /!Sub\s+["']?([^"'\n]+)["']?/g; // Matches !Sub "template string" - YAML short form string substitution
192+
const YamlIf = /!If\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !If [ConditionName, TrueValue, FalseValue] - YAML short form conditional
220193
const YamlRefColon = /Ref:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Ref: LogicalId - YAML long form reference
221194
const YamlGetAttColon = /Fn::GetAtt:\s*\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Fn::GetAtt: [LogicalId, Attribute] - YAML long form get attribute
222195
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
223-
const YamlSubColon = /Fn::Sub:\s*["']?([^"'\n]+)["']?/g; // Matches Fn::Sub: "template string" - YAML long form string substitution
196+
const YamlIfColon = /Fn::If:\s*\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Fn::If: [ConditionName, TrueValue, FalseValue] - YAML long form conditional
224197
const YamlCondition = /Condition:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Condition: ConditionName - resource condition property in YAML
225-
const YamlSubVariables = /\$\{([A-Za-z][A-Za-z0-9:]*)\}/g; // Matches ${LogicalId} or ${AWS::Region} - variables in Fn::Sub templates
226198
const YamlSingleDep = /DependsOn:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches DependsOn: LogicalId - single resource dependency in YAML
227199
const YamlInlineDeps = /DependsOn:\s*\[([^\]]+)]/g; // Matches DependsOn: [Id1, Id2] - inline array format in YAML
228200
const YamlListItem = /-\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches - LogicalId in YAML list format
229201
const YamlInlineItemPattern = /([A-Za-z][A-Za-z0-9]*)/g; // Matches LogicalId within the inline array
230202

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

233208
// Validated these regex, they will fail fast with ?= lookahead
234209
// eslint-disable-next-line security/detect-unsafe-regex
235210
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
236211

237-
const YamlInlineListItem = /^(?=\s*-)\s*-\s+([A-Za-z][A-Za-z0-9]*)/gm; // Matches - LogicalId - standalone list items (for DependsOn arrays)
238-
239212
export function isLogicalIdCandidate(str: unknown): boolean {
240213
if (!str || typeof str !== 'string' || str.length < 2) return false;
241214

0 commit comments

Comments
 (0)