Skip to content

Commit 69c083c

Browse files
committed
FindInMap bug fix, removed .todo from a now passing test
1 parent ca68e6f commit 69c083c

File tree

3 files changed

+86
-10
lines changed

3 files changed

+86
-10
lines changed

src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,19 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
5353
getCompletions(context: Context, params: CompletionParams): CompletionItem[] | undefined {
5454
const syntaxTree = this.syntaxTreeManager.getSyntaxTree(params.textDocument.uri);
5555
if (!syntaxTree) {
56+
log.debug('No syntax tree found');
5657
return;
5758
}
5859

5960
// Only handle contexts that are inside intrinsic functions
6061
if (!context?.intrinsicContext?.inIntrinsic()) {
62+
log.debug('Not in intrinsic context');
6163
return undefined;
6264
}
6365

6466
const intrinsicFunction = context.intrinsicContext.intrinsicFunction();
6567
if (!intrinsicFunction) {
68+
log.debug('No intrinsic function found');
6669
return undefined;
6770
}
6871

@@ -71,6 +74,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
7174
provider: 'IntrinsicFunctionArgument',
7275
context: context.record(),
7376
intrinsicFunction: intrinsicFunction.type,
77+
args: intrinsicFunction.args,
7478
},
7579
'Processing intrinsic function argument completion request',
7680
);
@@ -515,38 +519,98 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
515519
context: Context,
516520
): CompletionItem[] | undefined {
517521
// Validate arguments structure for second-level keys
518-
if (!Array.isArray(args) || args.length < 2 || typeof args[0] !== 'string' || typeof args[1] !== 'string') {
522+
if (!this.isValidSecondLevelKeyArgs(args)) {
519523
log.debug('Invalid arguments for second-level key completions');
520524
return undefined;
521525
}
522526

523527
try {
524528
const mappingName = args[0];
525-
const topLevelKey = args[1];
529+
const topLevelKey = args[1] as string | { Ref: unknown } | { '!Ref': unknown };
526530

527531
const mappingEntity = this.getMappingEntity(mappingsEntities, mappingName);
528532
if (!mappingEntity) {
529533
log.debug(`Mapping entity not found: ${mappingName}`);
530534
return undefined;
531535
}
532536

533-
const secondLevelKeys = mappingEntity.getSecondLevelKeys(topLevelKey);
537+
const secondLevelKeys = this.getSecondLevelKeysForTopLevelKey(mappingEntity, topLevelKey);
534538
if (secondLevelKeys.length === 0) {
535-
log.debug(`No second-level keys found for mapping: ${mappingName}, top-level key: ${topLevelKey}`);
539+
log.debug(`No second-level keys found for mapping: ${mappingName}`);
536540
return undefined;
537541
}
538542

539543
const items = secondLevelKeys.map((key) =>
540544
createCompletionItem(key, CompletionItemKind.EnumMember, { context }),
541545
);
542546

543-
return context.text.length > 0 ? this.fuzzySearch(items, context.text) : items;
547+
return this.filterSecondLevelKeyItems(items, context, topLevelKey);
544548
} catch (error) {
545-
log.error({ error }, 'Error creating second-level key completions');
549+
log.debug({ error }, 'Error creating second-level key completions');
546550
return undefined;
547551
}
548552
}
549553

554+
private isValidSecondLevelKeyArgs(args: unknown): args is [string, string | object] {
555+
if (!Array.isArray(args) || args.length < 2 || typeof args[0] !== 'string') {
556+
return false;
557+
}
558+
559+
// Second argument valid if it is a string i.e. 'us-east-1' or object '{Ref: AWS::Region}' or '{!Ref: AWS::Region}'
560+
return typeof args[1] === 'string' || this.isRefObject(args[1]);
561+
}
562+
563+
private getSecondLevelKeysForTopLevelKey(
564+
mappingEntity: Mapping,
565+
topLevelKey: string | { Ref: unknown } | { '!Ref': unknown },
566+
): string[] {
567+
if (typeof topLevelKey === 'string') {
568+
return mappingEntity.getSecondLevelKeys(topLevelKey);
569+
} else {
570+
// For dynamic references, get all possible keys
571+
return mappingEntity.getSecondLevelKeysDynamic(mappingEntity);
572+
}
573+
}
574+
575+
private filterSecondLevelKeyItems(
576+
items: CompletionItem[],
577+
context: Context,
578+
topLevelKey: string | { Ref: unknown } | { '!Ref': unknown },
579+
): CompletionItem[] {
580+
// Check if context.text contains the full FindInMap syntax (empty third argument case)
581+
if (context.text.startsWith('[') && context.text.endsWith(']')) {
582+
return items;
583+
}
584+
585+
// If no text typed, return all items
586+
if (context.text.length === 0) {
587+
return items;
588+
}
589+
590+
return this.applySecondLevelKeyFiltering(items, context, topLevelKey);
591+
}
592+
593+
private applySecondLevelKeyFiltering(
594+
items: CompletionItem[],
595+
context: Context,
596+
topLevelKey: string | { Ref: unknown } | { '!Ref': unknown },
597+
): CompletionItem[] {
598+
// For dynamic keys, try prefix matching first, then fall back to fuzzy search
599+
if (typeof topLevelKey === 'object') {
600+
const prefixMatches = items.filter((item) =>
601+
item.label.toLowerCase().startsWith(context.text.toLowerCase()),
602+
);
603+
604+
if (prefixMatches.length === 0) {
605+
return this.fuzzySearch(items, context.text);
606+
}
607+
608+
return prefixMatches;
609+
}
610+
611+
return this.fuzzySearch(items, context.text);
612+
}
613+
550614
private getMappingEntity(mappingsEntities: Map<string, Context>, mappingName: string): Mapping | undefined {
551615
try {
552616
const mappingContext = mappingsEntities.get(mappingName);
@@ -702,4 +766,8 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
702766

703767
return undefined;
704768
}
769+
770+
private isRefObject(value: unknown): value is { Ref: unknown } | { '!Ref': unknown } {
771+
return typeof value === 'object' && value !== null && ('Ref' in value || '!Ref' in value);
772+
}
705773
}

src/context/semantic/Entity.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ export class Mapping extends Entity {
147147
return [];
148148
}
149149

150+
public getSecondLevelKeysDynamic(mappingEntity: Mapping): string[] {
151+
const allKeys = new Set<string>();
152+
const topLevelKeys = mappingEntity.getTopLevelKeys();
153+
154+
for (const tlKey of topLevelKeys) {
155+
const keys = mappingEntity.getSecondLevelKeys(tlKey);
156+
for (const key of keys) allKeys.add(key);
157+
}
158+
return [...allKeys];
159+
}
160+
150161
public getValue(topLevelKey: string, secondLevelKey: string): MappingValueType | undefined {
151162
return this.value[topLevelKey]?.[secondLevelKey];
152163
}

tst/e2e/autocomplete/Autocomplete.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,6 @@ Rules:
406406
position: { line: 114, character: 54 },
407407
expectation: CompletionExpectationBuilder.create()
408408
.expectContainsItems(['InstanceType'])
409-
// todo: second level of Mapping not being suggested when using !Ref for first level key
410-
// works using 'us-east-1'
411-
.todo()
412409
.build(),
413410
},
414411
},
@@ -1383,7 +1380,7 @@ O`,
13831380
position: { line: 471, character: 61 },
13841381
// todo: fix bug in FindInMap completion where using intrinsic in second arg breaks
13851382
// suggestion for third arg
1386-
expectation: CompletionExpectationBuilder.create().expectItems(['AMI']).todo().build(),
1383+
expectation: CompletionExpectationBuilder.create().expectItems(['AMI']).build(),
13871384
},
13881385
},
13891386
{

0 commit comments

Comments
 (0)