Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
continue;
}

// Skip Fn::ForEach resources
if (resourceName.startsWith(IntrinsicFunction.ForEach)) {
continue;
}

const resource = resourceContext.entity;

completionItems.push(
Expand Down Expand Up @@ -690,7 +695,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
}

const items = [...resourceEntities.keys()]
.filter((logicalId) => logicalId !== context.logicalId)
.filter((logicalId) => logicalId !== context.logicalId && !logicalId.startsWith(IntrinsicFunction.ForEach))
.map((logicalId) => createCompletionItem(logicalId, CompletionItemKind.Reference, { context }));

return context.text.length > 0 ? this.fuzzySearch(items, context.text) : items;
Expand Down
41 changes: 41 additions & 0 deletions tst/e2e/Completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1921,6 +1921,47 @@ Resources:

await client.closeDocument({ textDocument: { uri } });
});

it('should exclude Fn::ForEach resources from !Ref completions', async () => {
const template = getSimpleYamlTemplateText();
const updatedTemplate = `AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::LanguageExtensions
Resources:
FirstBucket:
Type: AWS::S3::Bucket
Fn::ForEach::LoopBuckets:
- BucketName
- - Alpha
- Beta
- Bucket\${BucketName}:
Type: AWS::S3::Bucket
AnotherResource:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref F`;
const uri = await client.openYamlTemplate(template);

await client.changeDocument({
textDocument: { uri, version: 2 },
contentChanges: [{ text: updatedTemplate }],
});

const completions: any = await client.completion({
textDocument: { uri },
position: { line: 14, character: 23 },
});

expect(completions).toBeDefined();
expect(completions?.items).toBeDefined();

const labels = completions.items.map((item: any) => item.label);
// Should include regular resources
expect(labels).toContain('FirstBucket');
// Should NOT include Fn::ForEach resources
expect(labels).not.toContain('Fn::ForEach::LoopBuckets');

await client.closeDocument({ textDocument: { uri } });
});
});

describe('Value Completions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,30 @@ describe('IntrinsicFunctionArgumentCompletionProvider - GetAtt Function', () =>
expect(labels).toContain('LambdaRole');
expect(labels).toContain('DatabaseInstance');
});

it('should exclude Fn::ForEach resources from completions', () => {
const resourceDataWithForEach = {
...mockResourceData,
'Fn::ForEach::Buckets': { Type: 'AWS::S3::Bucket' },
'Fn::ForEach::Instances': { Type: 'AWS::EC2::Instance' },
};
setupResourceEntities(resourceDataWithForEach);

const mockContext = createMockGetAttContext('', []);

const result = provider.getCompletions(mockContext, createTestParams());

expect(result).toBeDefined();
expect(result!.length).toBe(4); // Should only include the 4 regular resources

const labels = result!.map((item) => item.label);
expect(labels).toContain('MyVPC');
expect(labels).toContain('MyS3Bucket');
expect(labels).toContain('LambdaRole');
expect(labels).toContain('DatabaseInstance');
expect(labels).not.toContain('Fn::ForEach::Buckets');
expect(labels).not.toContain('Fn::ForEach::Instances');
});
});

describe('Invalid Arguments', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,45 @@ describe('IntrinsicFunctionArgumentCompletionProvider - Ref Function', () => {
const resourceCompletions = result!.filter((item) => item.detail?.includes('Resource ('));
expect(resourceCompletions.length).toBe(2); // Should include all resources in Outputs section
});

it('should exclude Fn::ForEach resources from completions', () => {
const context = createMockContext(TopLevelSection.Resources, 'MyResource', { text: '' });
Object.defineProperty(context, 'intrinsicContext', {
value: createMockIntrinsicContext(IntrinsicFunction.Ref, ''),
});

const mockResourcesMap = new Map([
['MyResource', { entity: { Type: 'AWS::S3::Bucket' } }],
['OtherResource', { entity: { Type: 'AWS::EC2::Instance' } }],
['Fn::ForEach::Buckets', { entity: { Type: 'AWS::S3::Bucket' } }],
['Fn::ForEach::Instances', { entity: { Type: 'AWS::EC2::Instance' } }],
]);

const mockSyntaxTree = stubInterface<SyntaxTree>();
mockSyntaxTree.findTopLevelSections.returns(new Map([[TopLevelSection.Resources, {} as SyntaxNode]]));
(mockSyntaxTree as any).type = DocumentType.YAML;
mockSyntaxTreeManager.getSyntaxTree.returns(mockSyntaxTree);

(getEntityMap as any).mockImplementation((syntaxTree: any, section: TopLevelSection) => {
if (section === TopLevelSection.Resources) {
return mockResourcesMap;
}
return new Map();
});

const result = provider.getCompletions(context, createTestParams());

expect(result).toBeDefined();
const resourceCompletions = result!.filter((item) => item.detail?.includes('Resource ('));
// Should only include OtherResource (MyResource is excluded as current, Fn::ForEach:: resources are filtered)
expect(resourceCompletions.length).toBe(1);
expect(resourceCompletions[0].label).toBe('OtherResource');

// Verify Fn::ForEach resources are not in the results
const labels = result!.map((item) => item.label);
expect(labels).not.toContain('Fn::ForEach::Buckets');
expect(labels).not.toContain('Fn::ForEach::Instances');
});
});

describe('filtering and fuzzy search', () => {
Expand Down
Loading