Skip to content

Commit 0982708

Browse files
authored
Improve intrinsic functions and add json fallback parsing (#364)
1 parent 8b8a7ef commit 0982708

File tree

8 files changed

+1727
-198
lines changed

8 files changed

+1727
-198
lines changed

src/context/semantic/LogicalIdReferenceFinder.ts

Lines changed: 61 additions & 67 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) {
@@ -35,44 +36,33 @@ export function referencedLogicalIds(
3536
}
3637

3738
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) {
39+
// Early exit checks - only run regex if marker exists
40+
if (text.includes('"Ref"')) {
4941
extractMatches(text, JsonRef, logicalIds);
5042
}
51-
if (getAttIndex !== -1) {
43+
if (text.includes('"Fn::GetAtt"')) {
5244
extractMatches(text, JsonGetAtt, logicalIds);
45+
extractMatches(text, JsonGetAttString, logicalIds);
5346
}
54-
if (findMapIndex !== -1) {
47+
if (text.includes('"Fn::FindInMap"')) {
5548
extractMatches(text, JsonFindInMap, logicalIds);
5649
}
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) {
50+
if (text.includes('"Fn::If"')) {
6551
extractMatches(text, JsonIf, logicalIds);
6652
}
67-
if (conditionIndex !== -1) {
53+
if (text.includes('"Condition"')) {
6854
extractMatches(text, JsonCondition, logicalIds);
6955
}
70-
if (subVarIndex !== -1) {
71-
extractMatches(text, JsonSubVariables, logicalIds);
72-
}
73-
if (dependsIndex !== -1) {
56+
if (text.includes('"DependsOn"')) {
7457
extractJsonDependsOnReferences(text, logicalIds);
7558
}
59+
if (text.includes('"Fn::ValueOf"')) {
60+
extractMatches(text, JsonValueOf, logicalIds);
61+
}
62+
// Extract all ${} variables in one pass - covers Fn::Sub and standalone
63+
if (text.includes('${')) {
64+
extractMatches(text, SubVariables, logicalIds);
65+
}
7666
}
7767

7868
function findYamlIntrinsicReferences(text: string, logicalIds: Set<string>): void {
@@ -86,44 +76,43 @@ function findYamlIntrinsicReferences(text: string, logicalIds: Set<string>): voi
8676
if (text.includes('!FindInMap')) {
8777
extractMatches(text, YamlFindInMap, logicalIds);
8878
}
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-
}
79+
if (text.includes('!If')) {
80+
extractMatches(text, YamlIf, logicalIds);
9781
}
98-
if (text.includes('Ref:')) {
82+
if (text.includes('!Condition')) {
83+
extractMatches(text, YamlConditionShort, logicalIds);
84+
}
85+
if (text.includes('Ref')) {
9986
extractMatches(text, YamlRefColon, logicalIds);
10087
}
101-
if (text.includes('Fn::GetAtt:')) {
88+
if (text.includes('Fn::GetAtt')) {
10289
extractMatches(text, YamlGetAttColon, logicalIds);
90+
extractMatches(text, YamlGetAttColonString, logicalIds);
10391
}
104-
if (text.includes('Fn::FindInMap:')) {
92+
if (text.includes('Fn::FindInMap')) {
10593
extractMatches(text, YamlFindInMapColon, logicalIds);
10694
}
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-
}
95+
if (text.includes('Fn::If')) {
96+
extractMatches(text, YamlIfColon, logicalIds);
11597
}
116-
if (text.includes('Condition:')) {
98+
if (text.includes('Condition')) {
11799
extractMatches(text, YamlCondition, logicalIds);
118100
}
101+
if (text.includes('!ValueOf')) {
102+
extractMatches(text, YamlValueOfShort, logicalIds);
103+
}
104+
if (text.includes('Fn::ValueOf')) {
105+
extractMatches(text, YamlValueOf, logicalIds);
106+
}
107+
// Extract all ${} variables in one pass - covers !Sub, Fn::Sub:, and standalone
119108
if (text.includes('${')) {
120-
extractMatches(text, YamlSubVariables, logicalIds);
109+
extractMatches(text, SubVariables, logicalIds);
121110
}
111+
// Handle YAML list items (for Fn::GetAtt list syntax, DependsOn lists, etc.)
122112
if (text.includes('- ')) {
123-
extractMatches(text, YamlInlineListItem, logicalIds);
113+
extractMatches(text, YamlListItem, logicalIds);
124114
}
125-
126-
if (text.includes('DependsOn:')) {
115+
if (text.includes('DependsOn')) {
127116
extractYamlDependsOnReferences(text, logicalIds);
128117
}
129118
}
@@ -173,6 +162,7 @@ function extractYamlDependsOnReferences(text: string, logicalIds: Set<string>):
173162

174163
const CommonProperties = new Set(
175164
[
165+
'AWS',
176166
'Type',
177167
'Properties',
178168
...ResourceAttributes,
@@ -203,38 +193,42 @@ const CommonProperties = new Set(
203193
// Pre-compiled for performance
204194
const JsonRef = /"Ref"\s*:\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Ref": "LogicalId"} - references to parameters, resources, etc.
205195
const JsonGetAtt = /"Fn::GetAtt"\s*:\s*\[\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Fn::GetAtt": ["LogicalId", "Attribute"]} - gets attributes from resources
196+
const JsonGetAttString = /"Fn::GetAtt"\s*:\s*"([A-Za-z][A-Za-z0-9]*)\./g; // Matches {"Fn::GetAtt": "LogicalId.Attribute"} - string syntax
206197
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
208198
const JsonIf = /"Fn::If"\s*:\s*\[\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Fn::If": ["ConditionName", "TrueValue", "FalseValue"]} - conditional logic
209199
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
211200
const JsonSingleDep = /"DependsOn"\s*:\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches "DependsOn": "LogicalId" - single resource dependency
212201
const JsonArrayDep = /"DependsOn"\s*:\s*\[([^\]]+)]/g; // Matches "DependsOn": ["Id1", "Id2"] - array of resource dependencies
213202
const JsonArrayItem = /"([A-Za-z][A-Za-z0-9]*)"/g; // Matches "LogicalId" within the DependsOn array
203+
const JsonValueOf = /"Fn::ValueOf"\s*:\s*\[\s*"([A-Za-z][A-Za-z0-9]*)"/g; // Matches {"Fn::ValueOf": ["ParamName", "Attr"]} - gets parameter attribute
214204

215205
const YamlRef = /!Ref\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !Ref LogicalId - YAML short form reference
216-
const YamlGetAtt = /!GetAtt\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt LogicalId.Attribute - YAML short form get attribute
217-
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
206+
const YamlGetAtt = /!GetAtt\s+['"]?([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt LogicalId.Attribute - YAML short form get attribute with optional quotes
207+
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
218208
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
220-
const YamlRefColon = /Ref:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Ref: LogicalId - YAML long form reference
221-
const YamlGetAttColon = /Fn::GetAtt:\s*\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Fn::GetAtt: [LogicalId, Attribute] - YAML long form get attribute
222-
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
224-
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
226-
const YamlSingleDep = /DependsOn:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches DependsOn: LogicalId - single resource dependency in YAML
227-
const YamlInlineDeps = /DependsOn:\s*\[([^\]]+)]/g; // Matches DependsOn: [Id1, Id2] - inline array format in YAML
209+
const YamlIf = /!If\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !If [ConditionName, TrueValue, FalseValue] - YAML short form conditional
210+
const YamlConditionShort = /!Condition\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !Condition ConditionName - YAML short form condition reference
211+
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
212+
const YamlGetAttColon = /['"]?Fn::GetAtt['"]?\s*:\s*\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Fn::GetAtt:, 'Fn::GetAtt':, "Fn::GetAtt": [LogicalId, Attribute]
213+
const YamlGetAttColonString = /['"]?Fn::GetAtt['"]?\s*:\s*['"]?([A-Za-z][A-Za-z0-9]*)\./g; // Matches Fn::GetAtt: LogicalId.Attribute with optional quotes
214+
const YamlFindInMapColon = /['"]?Fn::FindInMap['"]?\s*:\s*\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Fn::FindInMap: [MappingName, ...] with optional quotes
215+
const YamlIfColon = /['"]?Fn::If['"]?\s*:\s*\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Fn::If: [ConditionName, ...] with optional quotes
216+
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
217+
const YamlSingleDep = /(?<![A-Za-z])['"]?DependsOn['"]?\s*:\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches DependsOn: LogicalId with optional quotes
218+
const YamlInlineDeps = /(?<![A-Za-z])['"]?DependsOn['"]?\s*:\s*\[([^\]]+)]/g; // Matches DependsOn: [Id1, Id2] with optional quotes
228219
const YamlListItem = /-\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches - LogicalId in YAML list format
229220
const YamlInlineItemPattern = /([A-Za-z][A-Za-z0-9]*)/g; // Matches LogicalId within the inline array
221+
const YamlValueOfShort = /!ValueOf\s+\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches !ValueOf [ParamName, Attr] - YAML short form
222+
const YamlValueOf = /['"]?Fn::ValueOf['"]?\s*:\s*\[\s*['"]?([A-Za-z][A-Za-z0-9]*)['"]?/g; // Matches Fn::ValueOf: [ParamName, Attr] with optional quotes
223+
224+
// Shared pattern for ${} variables - used by both JSON and YAML
225+
const SubVariables = /\$\{([A-Za-z][A-Za-z0-9]*)(?:[.:]|(?=\}))/g; // Matches ${LogicalId} or ${Resource.Attr} or ${AWS::Region} - captures first segment only
230226

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

233229
// Validated these regex, they will fail fast with ?= lookahead
234-
// eslint-disable-next-line security/detect-unsafe-regex
235-
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
236-
237-
const YamlInlineListItem = /^(?=\s*-)\s*-\s+([A-Za-z][A-Za-z0-9]*)/gm; // Matches - LogicalId - standalone list items (for DependsOn arrays)
230+
const YamlListDep =
231+
/(?<![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
238232

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

0 commit comments

Comments
 (0)