Skip to content

Commit 36e1c3b

Browse files
Deep FuriyaZee2413
authored andcommitted
fix duplicate logical id issue and made logical id give more information to user
1 parent 474884e commit 36e1c3b

File tree

2 files changed

+99
-45
lines changed

2 files changed

+99
-45
lines changed

src/autocomplete/RelatedResourcesInlineCompletionProvider.ts

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { InlineCompletionItem, InlineCompletionParams } from 'vscode-languageserver-protocol';
22
import { Context } from '../context/Context';
3-
import { DocumentType } from '../document/Document';
3+
import { Document, DocumentType } from '../document/Document';
44
import { DocumentManager } from '../document/DocumentManager';
55
import { ResourceSchema } from '../schema/ResourceSchema';
66
import { SchemaRetriever } from '../schema/SchemaRetriever';
@@ -10,6 +10,8 @@ import { applySnippetIndentation } from '../utils/IndentationUtils';
1010
import { CompletionFormatter } from './CompletionFormatter';
1111
import { InlineCompletionProvider } from './InlineCompletionProvider';
1212

13+
type RelatedResource = { type: string; relatedTo: string };
14+
1315
export class RelatedResourcesInlineCompletionProvider implements InlineCompletionProvider {
1416
private readonly log = LoggerFactory.getLogger(RelatedResourcesInlineCompletionProvider);
1517
private readonly MAX_SUGGESTIONS = 5;
@@ -54,29 +56,36 @@ export class RelatedResourcesInlineCompletionProvider implements InlineCompletio
5456
return undefined;
5557
}
5658

57-
return this.generateInlineCompletionItems(relatedResourceTypes, params, context);
59+
return this.generateInlineCompletionItems(relatedResourceTypes, params);
5860
} catch (error) {
5961
this.log.error({ error: String(error) }, 'Error generating related resources inline completion');
6062
return undefined;
6163
}
6264
}
6365

64-
private getRelatedResourceTypes(existingResourceTypes: string[]): string[] {
66+
private getRelatedResourceTypes(existingResourceTypes: string[]): RelatedResource[] {
6567
const existingRelationships = new Map<string, Set<string>>();
6668
const allRelatedTypes = new Set<string>();
69+
const typeToRelatedMap = new Map<string, string>();
6770

6871
for (const resourceType of existingResourceTypes) {
6972
const relatedTypes = this.relationshipSchemaService.getAllRelatedResourceTypes(resourceType);
7073
existingRelationships.set(resourceType, relatedTypes);
7174
for (const type of relatedTypes) {
7275
allRelatedTypes.add(type);
76+
typeToRelatedMap.set(type, typeToRelatedMap.get(type) ?? resourceType);
7377
}
7478
}
7579

7680
const existingTypesSet = new Set(existingResourceTypes);
7781
const suggestedTypes = [...allRelatedTypes].filter((type) => !existingTypesSet.has(type));
7882

79-
return this.rankSuggestionsByFrequency(suggestedTypes, existingRelationships);
83+
const rankedTypes = this.rankSuggestionsByFrequency(suggestedTypes, existingRelationships);
84+
85+
return rankedTypes.map((type) => ({
86+
type,
87+
relatedTo: typeToRelatedMap.get(type) ?? type,
88+
}));
8089
}
8190

8291
private rankSuggestionsByFrequency(
@@ -108,36 +117,39 @@ export class RelatedResourcesInlineCompletionProvider implements InlineCompletio
108117
}
109118

110119
private generateInlineCompletionItems(
111-
relatedResourceTypes: string[],
120+
relatedResourceTypes: RelatedResource[],
112121
params: InlineCompletionParams,
113-
context: Context,
114122
): InlineCompletionItem[] {
115-
const completionItems: InlineCompletionItem[] = [];
116-
123+
const document = this.documentManager.get(params.textDocument.uri);
117124
const topSuggestions = relatedResourceTypes.slice(0, this.MAX_SUGGESTIONS);
118125

119-
for (const resourceType of topSuggestions) {
120-
const insertText = this.generatePropertySnippet(resourceType, context.documentType, params);
126+
return topSuggestions.map(({ type: resourceType, relatedTo }) => {
127+
const insertText = this.generatePropertySnippet(resourceType, relatedTo, params, document);
121128

122-
completionItems.push({
129+
return {
123130
insertText,
124131
range: {
125132
start: params.position,
126133
end: params.position,
127134
},
128-
filterText: `${resourceType}`,
129-
});
130-
}
131-
132-
return completionItems;
135+
filterText: resourceType,
136+
};
137+
});
133138
}
134139

135140
private generatePropertySnippet(
136141
resourceType: string,
137-
documentType: DocumentType,
142+
relatedToType: string,
138143
params: InlineCompletionParams,
144+
document: Document | undefined,
139145
): string {
140-
const logicalId = 'relatedResourceLogicalId';
146+
const documentType = document?.documentType ?? DocumentType.YAML;
147+
const baseLogicalId = `RelatedTo${relatedToType
148+
.split('::')
149+
.slice(1)
150+
.join('')
151+
.replaceAll(/[^a-zA-Z0-9]/g, '')}`;
152+
const logicalId = this.getUniqueLogicalId(baseLogicalId, document);
141153
const indent0 = CompletionFormatter.getIndentPlaceholder(0);
142154
const indent1 = CompletionFormatter.getIndentPlaceholder(1);
143155

@@ -243,4 +255,24 @@ export class RelatedResourcesInlineCompletionProvider implements InlineCompletio
243255
.replaceAll(/\n\s*{INDENT2}/g, `\n${propertyIndent}`)
244256
.replaceAll(/\n\s*{INDENT3}/g, `\n${' '.repeat(currentIndent + baseIndentSize * 3)}`);
245257
}
258+
259+
private getUniqueLogicalId(baseId: string, document: Document | undefined): string {
260+
const logicalId = `${baseId}LogicalId`;
261+
262+
if (!document) {
263+
return logicalId;
264+
}
265+
266+
const templateText = document.getText();
267+
268+
let counter = 0;
269+
let candidateId = logicalId;
270+
271+
while (templateText.includes(candidateId)) {
272+
counter++;
273+
candidateId = `${baseId}${counter}LogicalId`;
274+
}
275+
276+
return candidateId;
277+
}
246278
}

tst/unit/autocomplete/RelatedResourcesInlineCompletionProvider.test.ts

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ Resources:
125125
// Verify that suggested resources are from the expected set
126126
for (const item of result) {
127127
expect(item.insertText).toBeDefined();
128-
expect(item.insertText).toContain('relatedResourceLogicalId:');
128+
expect(item.insertText).toContain('RelatedToS3BucketLogicalId:');
129129
expect(item.insertText).toContain('Type:');
130130
expect(item.range).toBeDefined();
131131
expect(item.range.start).toEqual(mockParams.position);
@@ -164,7 +164,7 @@ Resources:
164164
expect(result).toBeDefined();
165165
expect(Array.isArray(result)).toBe(true);
166166
// Should only include AWS::IAM::Role (not the existing S3 and Lambda)
167-
expect(result).toEqual(['AWS::IAM::Role']);
167+
expect(result).toEqual([{ type: 'AWS::IAM::Role', relatedTo: 'AWS::S3::Bucket' }]);
168168
});
169169

170170
test('should rank suggestions by frequency', () => {
@@ -181,21 +181,21 @@ Resources:
181181
expect(result).toBeDefined();
182182
expect(Array.isArray(result)).toBe(true);
183183
// AWS::IAM::Role should be first (appears in all 3), others alphabetically
184-
expect(result[0]).toBe('AWS::IAM::Role');
184+
expect(result[0].type).toBe('AWS::IAM::Role');
185185
});
186186
});
187187

188188
describe('generateInlineCompletionItems', () => {
189189
test('should limit suggestions to top 5', () => {
190190
const manyResourceTypes = [
191-
'AWS::IAM::Role',
192-
'AWS::CloudFront::Distribution',
193-
'AWS::API::Gateway',
194-
'AWS::EC2::SecurityGroup',
195-
'AWS::RDS::DBInstance',
196-
'AWS::DynamoDB::Table',
197-
'AWS::SNS::Topic',
198-
'AWS::SQS::Queue',
191+
{ type: 'AWS::IAM::Role', relatedTo: 'AWS::S3::Bucket' },
192+
{ type: 'AWS::CloudFront::Distribution', relatedTo: 'AWS::S3::Bucket' },
193+
{ type: 'AWS::API::Gateway', relatedTo: 'AWS::Lambda::Function' },
194+
{ type: 'AWS::EC2::SecurityGroup', relatedTo: 'AWS::EC2::Instance' },
195+
{ type: 'AWS::RDS::DBInstance', relatedTo: 'AWS::EC2::Instance' },
196+
{ type: 'AWS::DynamoDB::Table', relatedTo: 'AWS::Lambda::Function' },
197+
{ type: 'AWS::SNS::Topic', relatedTo: 'AWS::Lambda::Function' },
198+
{ type: 'AWS::SQS::Queue', relatedTo: 'AWS::Lambda::Function' },
199199
];
200200

201201
const result = (provider as any).generateInlineCompletionItems(manyResourceTypes, mockParams, mockContext);
@@ -206,7 +206,10 @@ Resources:
206206
});
207207

208208
test('should create proper completion items structure', () => {
209-
const resourceTypes = ['AWS::IAM::Role', 'AWS::CloudFront::Distribution'];
209+
const resourceTypes = [
210+
{ type: 'AWS::IAM::Role', relatedTo: 'AWS::S3::Bucket' },
211+
{ type: 'AWS::CloudFront::Distribution', relatedTo: 'AWS::S3::Bucket' },
212+
];
210213

211214
const result = (provider as any).generateInlineCompletionItems(resourceTypes, mockParams, mockContext);
212215

@@ -215,7 +218,7 @@ Resources:
215218

216219
for (const item of result) {
217220
expect(item.insertText).toBeDefined();
218-
expect(item.insertText).toContain('relatedResourceLogicalId:');
221+
expect(item.insertText).toContain('RelatedToS3BucketLogicalId:');
219222
expect(item.range).toBeDefined();
220223
expect(item.range.start).toEqual(mockParams.position);
221224
expect(item.range.end).toEqual(mockParams.position);
@@ -235,34 +238,48 @@ Resources:
235238
describe('generatePropertySnippet', () => {
236239
test('should generate basic property snippet', () => {
237240
const resourceType = 'AWS::S3::Bucket';
241+
const relatedToType = 'AWS::Lambda::Function';
238242

239243
const result = (provider as any).generatePropertySnippet(
240244
resourceType,
241-
mockContext.documentType,
245+
relatedToType,
242246
mockParams,
247+
undefined,
243248
);
244249

245250
expect(result).toBeDefined();
246-
expect(result).toContain('relatedResourceLogicalId:');
251+
expect(result).toContain('RelatedToLambdaFunctionLogicalId:');
247252
expect(result).toContain('Type: AWS::S3::Bucket');
248253
});
249254

250255
test('should handle different resource types', () => {
251-
const testCases = ['AWS::Lambda::Function', 'AWS::IAM::Role', 'AWS::EC2::Instance', 'AWS::RDS::DBInstance'];
256+
const testCases = [
257+
{ resourceType: 'AWS::Lambda::Function', relatedTo: 'AWS::S3::Bucket' },
258+
{ resourceType: 'AWS::IAM::Role', relatedTo: 'AWS::Lambda::Function' },
259+
{ resourceType: 'AWS::EC2::Instance', relatedTo: 'AWS::IAM::Role' },
260+
{ resourceType: 'AWS::RDS::DBInstance', relatedTo: 'AWS::EC2::Instance' },
261+
];
252262

253-
for (const resourceType of testCases) {
263+
for (const { resourceType, relatedTo } of testCases) {
254264
const result = (provider as any).generatePropertySnippet(
255265
resourceType,
256-
mockContext.documentType,
266+
relatedTo,
257267
mockParams,
268+
undefined,
258269
);
259-
expect(result).toContain('relatedResourceLogicalId:');
270+
const expectedLogicalId = `RelatedTo${relatedTo
271+
.split('::')
272+
.slice(1)
273+
.join('')
274+
.replaceAll(/[^a-zA-Z0-9]/g, '')}LogicalId`;
275+
expect(result).toContain(`${expectedLogicalId}:`);
260276
expect(result).toContain(`Type: ${resourceType}`);
261277
}
262278
});
263279

264280
test('should include required properties when schema has required fields', () => {
265281
const resourceType = 'AWS::S3::Bucket';
282+
const relatedToType = 'AWS::Lambda::Function';
266283

267284
// Mock schema with required properties
268285
const mockSchema = {
@@ -275,11 +292,12 @@ Resources:
275292

276293
const result = (provider as any).generatePropertySnippet(
277294
resourceType,
278-
mockContext.documentType,
295+
relatedToType,
279296
mockParams,
297+
undefined,
280298
);
281299

282-
expect(result).toContain('relatedResourceLogicalId:');
300+
expect(result).toContain('RelatedToLambdaFunctionLogicalId:');
283301
expect(result).toContain(`Type: ${resourceType}`);
284302
expect(result).toContain('Properties:');
285303
expect(result).toContain('BucketName: ');
@@ -290,6 +308,7 @@ Resources:
290308

291309
test('should generate properly indented YAML snippets with cursor context', () => {
292310
const resourceType = 'AWS::IAM::Role';
311+
const relatedToType = 'AWS::S3::Bucket';
293312
const mockSchema = {
294313
required: ['AssumeRolePolicyDocument'],
295314
};
@@ -316,25 +335,28 @@ Resources:
316335
' BucketName: test',
317336
' ',
318337
],
338+
documentType: 'YAML',
319339
};
320340
mockDocumentManager.get.returns(mockDocument as any);
321341
mockDocumentManager.getEditorSettingsForDocument.returns({ tabSize: 2, insertSpaces: true } as any);
322342

323343
const result = (provider as any).generatePropertySnippet(
324344
resourceType,
325-
mockContext.documentType,
345+
relatedToType,
326346
mockParams,
347+
mockDocument,
327348
);
328349

329350
// Verify proper YAML structure with cursor context indentation
330-
expect(result).toContain('relatedResourceLogicalId:');
351+
expect(result).toContain('RelatedToS3BucketLogicalId:');
331352
expect(result).toContain(' Type: AWS::IAM::Role'); // 4 spaces (2 existing + 2 more)
332353
expect(result).toContain(' Properties:'); // 4 spaces (2 existing + 2 more)
333354
expect(result).toContain(' AssumeRolePolicyDocument: '); // 6 spaces (2 existing + 4 more)
334355
});
335356

336357
test('should generate properly indented JSON snippets with cursor context', () => {
337358
const resourceType = 'AWS::IAM::Role';
359+
const relatedToType = 'AWS::S3::Bucket';
338360
const mockSchema = {
339361
required: ['AssumeRolePolicyDocument'],
340362
};
@@ -361,20 +383,20 @@ Resources:
361383
' },',
362384
' ',
363385
],
386+
documentType: 'JSON',
364387
};
365388
mockDocumentManager.get.returns(mockDocument as any);
366389
mockDocumentManager.getEditorSettingsForDocument.returns({ tabSize: 2, insertSpaces: true } as any);
367390

368-
const jsonContext = { ...mockContext, documentType: 'JSON' };
369-
370391
const result = (provider as any).generatePropertySnippet(
371392
resourceType,
372-
jsonContext.documentType,
393+
relatedToType,
373394
mockParams,
395+
mockDocument,
374396
);
375397

376398
// Verify proper JSON structure with cursor context indentation
377-
expect(result).toContain('"relatedResourceLogicalId": {');
399+
expect(result).toContain('"RelatedToS3BucketLogicalId": {');
378400
expect(result).toContain(' "Type": "AWS::IAM::Role"'); // 4 spaces (2 existing + 2 more)
379401
expect(result).toContain(' "Properties": {'); // 4 spaces (2 existing + 2 more)
380402
expect(result).toContain(' "AssumeRolePolicyDocument": ""'); // 6 spaces (2 existing + 4 more)

0 commit comments

Comments
 (0)