Skip to content

Commit 7fda1f1

Browse files
authored
Template detection updates (#259)
* Template detection updates
1 parent 260cd2a commit 7fda1f1

File tree

10 files changed

+127
-32
lines changed

10 files changed

+127
-32
lines changed

src/context/syntaxtree/SyntaxTreeManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class SyntaxTreeManager {
2323
}
2424

2525
public addWithTypes(uri: string, content: string, type: DocumentType, cfnFileType: CloudFormationFileType) {
26-
if (cfnFileType !== CloudFormationFileType.Template) {
26+
if (cfnFileType !== CloudFormationFileType.Template && cfnFileType !== CloudFormationFileType.Empty) {
2727
return;
2828
}
2929

@@ -36,7 +36,7 @@ export class SyntaxTreeManager {
3636

3737
@Measure({ name: 'createTree' })
3838
private createTree(uri: string, content: string, type: DocumentType, cfnFileType: CloudFormationFileType) {
39-
if (cfnFileType !== CloudFormationFileType.Template) {
39+
if (cfnFileType !== CloudFormationFileType.Template && cfnFileType !== CloudFormationFileType.Empty) {
4040
throw new Error('Syntax tree can only be created for CloudFormation templates');
4141
}
4242

src/document/Document.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class Document {
4343
public updateCfnFileType(): void {
4444
const content = this.textDocument.getText();
4545
if (!content.trim()) {
46-
this._cfnFileType = CloudFormationFileType.Unknown;
46+
this._cfnFileType = CloudFormationFileType.Empty;
4747
this.cachedParsedContent = undefined;
4848
return;
4949
}
@@ -74,8 +74,12 @@ export class Document {
7474
return CloudFormationFileType.Template;
7575
}
7676

77+
if (typeof this.cachedParsedContent === 'string' && this.documentType === DocumentType.YAML) {
78+
return CloudFormationFileType.Empty;
79+
}
80+
7781
if (!this.cachedParsedContent || typeof this.cachedParsedContent !== 'object') {
78-
return CloudFormationFileType.Unknown;
82+
return CloudFormationFileType.Other;
7983
}
8084

8185
const parsed = this.cachedParsedContent as Record<string, unknown>;
@@ -96,7 +100,7 @@ export class Document {
96100
return CloudFormationFileType.Template;
97101
}
98102

99-
return CloudFormationFileType.Unknown;
103+
return CloudFormationFileType.Other;
100104
}
101105

102106
public getParsedDocumentContent(): unknown {
@@ -202,5 +206,7 @@ export enum DocumentType {
202206
export enum CloudFormationFileType {
203207
Template = 'template',
204208
GitSyncDeployment = 'gitsync-deployment',
205-
Unknown = 'unknown',
209+
Unknown = 'unknown', // Unanalyzed files
210+
Other = 'other', // For files we know aren't CloudFormation
211+
Empty = 'empty', // For nearly empty files that we can't determine yet
206212
}

src/handlers/DocumentHandler.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TextDocumentChangeEvent } from 'vscode-languageserver/lib/common/textDo
44
import { NotificationHandler } from 'vscode-languageserver-protocol';
55
import { TextDocument } from 'vscode-languageserver-textdocument';
66
import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager';
7-
import { Document } from '../document/Document';
7+
import { CloudFormationFileType, Document } from '../document/Document';
88
import { createEdit } from '../document/DocumentUtils';
99
import { LspDocuments } from '../protocol/LspDocuments';
1010
import { ServerComponents } from '../server/ServerComponents';
@@ -27,7 +27,7 @@ export function didOpenHandler(components: ServerComponents): (event: TextDocume
2727

2828
const content = document.contents();
2929

30-
if (document.isTemplate()) {
30+
if (document.isTemplate() || document.cfnFileType === CloudFormationFileType.Empty) {
3131
try {
3232
components.syntaxTreeManager.addWithTypes(uri, content, document.documentType, document.cfnFileType);
3333
} catch (error) {
@@ -199,7 +199,11 @@ function updateSyntaxTree(syntaxTreeManager: SyntaxTreeManager, textDocument: Te
199199
const uri = textDocument.uri;
200200
const document = new Document(textDocument);
201201
if (syntaxTreeManager.getSyntaxTree(uri)) {
202-
syntaxTreeManager.updateWithEdit(uri, document.contents(), edit);
202+
if (document.cfnFileType === CloudFormationFileType.Other) {
203+
syntaxTreeManager.deleteSyntaxTree(uri);
204+
} else {
205+
syntaxTreeManager.updateWithEdit(uri, document.contents(), edit);
206+
}
203207
} else {
204208
syntaxTreeManager.addWithTypes(uri, document.contents(), document.documentType, document.cfnFileType);
205209
}

src/services/cfnLint/CfnLintService.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,8 +413,13 @@ export class CfnLintService implements SettingsConfigurable, Closeable {
413413
// Check if this file should be processed by cfn-lint
414414
const fileType = this.documentManager.get(uri)?.cfnFileType;
415415

416-
if (!fileType || fileType === CloudFormationFileType.Unknown) {
417-
this.telemetry.count(`lint.file.${CloudFormationFileType.Unknown}`, 1);
416+
if (
417+
!fileType ||
418+
fileType === CloudFormationFileType.Other ||
419+
fileType === CloudFormationFileType.Unknown ||
420+
fileType === CloudFormationFileType.Empty
421+
) {
422+
this.telemetry.count(`lint.file.skipped`, 1);
418423
this.publishDiagnostics(uri, []);
419424
return;
420425
}

src/services/guard/GuardService.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,13 @@ export class GuardService implements SettingsConfigurable, Closeable {
172172
async validate(content: string, uri: string, _forceUseContent?: boolean): Promise<void> {
173173
const fileType = this.documentManager.get(uri)?.cfnFileType;
174174

175-
if (!fileType || fileType === CloudFormationFileType.Unknown) {
176-
this.telemetry.count(`validate.file.${CloudFormationFileType.Unknown}`, 1);
175+
if (
176+
!fileType ||
177+
fileType === CloudFormationFileType.Other ||
178+
fileType === CloudFormationFileType.Unknown ||
179+
fileType === CloudFormationFileType.Empty
180+
) {
181+
this.telemetry.count(`validate.file.skipped`, 1);
177182
// Not a CloudFormation file, publish empty diagnostics to clear any previous issues
178183
this.publishDiagnostics(uri, []);
179184
return;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it } from 'vitest';
2+
import { DocumentType } from '../../../src/document/Document';
3+
import { CompletionExpectationBuilder, TemplateBuilder, TemplateScenario } from '../../utils/TemplateBuilder';
4+
5+
describe('Empty File Autocompletion', () => {
6+
describe('YAML Empty File', () => {
7+
const template = new TemplateBuilder(DocumentType.YAML);
8+
9+
it('should provide CloudFormation keys on empty file', async () => {
10+
const scenario: TemplateScenario = {
11+
name: 'Empty YAML file completion',
12+
steps: [
13+
{
14+
action: 'initialize',
15+
content: '',
16+
verification: {
17+
position: { line: 0, character: 0 },
18+
expectation: CompletionExpectationBuilder.create()
19+
.expectContainsItems(['Resources'])
20+
.expectMinItems(1)
21+
.build(),
22+
},
23+
},
24+
],
25+
};
26+
await template.executeScenario(scenario);
27+
});
28+
29+
it('should NOT provide completions for non-CloudFormation files', async () => {
30+
const scenario: TemplateScenario = {
31+
name: 'Non-CloudFormation YAML file',
32+
steps: [
33+
{
34+
action: 'initialize',
35+
content: 'name: my-app\nversion: 1.0.0\nscripts:\n build: npm run build',
36+
verification: {
37+
position: { line: 4, character: 0 },
38+
expectation: CompletionExpectationBuilder.create().expectMaxItems(0).build(),
39+
},
40+
},
41+
],
42+
};
43+
await template.executeScenario(scenario);
44+
});
45+
});
46+
});

tst/unit/context/syntaxtree/SyntaxTreeManager.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, afterEach, vi, Mocked, MockedClass }
33
import { JsonSyntaxTree } from '../../../../src/context/syntaxtree/JsonSyntaxTree';
44
import { SyntaxTreeManager } from '../../../../src/context/syntaxtree/SyntaxTreeManager';
55
import { YamlSyntaxTree } from '../../../../src/context/syntaxtree/YamlSyntaxTree';
6-
import { DocumentType } from '../../../../src/document/Document';
6+
import { DocumentType, CloudFormationFileType } from '../../../../src/document/Document';
77
import { point } from '../../../utils/TemplateUtils';
88

99
vi.mock('../../../../src/context/syntaxtree/JsonSyntaxTree');
@@ -184,4 +184,26 @@ describe('SyntaxTreeManager', () => {
184184
expect(MockedYamlSyntaxTree).toHaveBeenCalled();
185185
});
186186
});
187+
188+
describe('addWithTypes', () => {
189+
it('should create syntax tree for empty files', () => {
190+
const uri = 'file:///empty.yaml';
191+
const content = '';
192+
193+
syntaxTreeManager.addWithTypes(uri, content, DocumentType.YAML, CloudFormationFileType.Empty);
194+
195+
expect(MockedYamlSyntaxTree).toHaveBeenCalledWith(content);
196+
expect(syntaxTreeManager.getSyntaxTree(uri)).toBe(mockYamlTree);
197+
});
198+
199+
it('should not create syntax tree for other file types', () => {
200+
const uri = 'file:///other.yaml';
201+
const content = 'name: my-app\nversion: 1.0.0';
202+
203+
syntaxTreeManager.addWithTypes(uri, content, DocumentType.YAML, CloudFormationFileType.Other);
204+
205+
expect(MockedYamlSyntaxTree).not.toHaveBeenCalled();
206+
expect(syntaxTreeManager.getSyntaxTree(uri)).toBeUndefined();
207+
});
208+
});
187209
});

tst/unit/document/Document.test.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,15 @@ describe('Document', () => {
247247
const textDocument = TextDocument.create('file:///test.json', 'json', 1, content);
248248
const doc = new Document(textDocument);
249249

250-
expect(doc.cfnFileType).toBe(CloudFormationFileType.Unknown);
250+
expect(doc.cfnFileType).toBe(CloudFormationFileType.Other);
251251
});
252252

253253
it('package.json with CloudFormation-like keys', () => {
254254
const content = '{"name": "my-package", "Parameters": {"env": "prod"}, "Outputs": {"build": "dist"}}';
255255
const textDocument = TextDocument.create('file:///test.json', 'json', 1, content);
256256
const doc = new Document(textDocument);
257257

258-
expect(doc.cfnFileType).toBe(CloudFormationFileType.Unknown);
258+
expect(doc.cfnFileType).toBe(CloudFormationFileType.Other);
259259
});
260260

261261
it('nested Resources key', () => {
@@ -270,25 +270,33 @@ describe('Document', () => {
270270
const textDocument = TextDocument.create('file:///test.json', 'json', 1, content);
271271
const doc = new Document(textDocument);
272272

273-
expect(doc.cfnFileType).toBe(CloudFormationFileType.Unknown);
273+
expect(doc.cfnFileType).toBe(CloudFormationFileType.Other);
274274
});
275275
});
276276

277277
describe('should handle empty content', () => {
278-
it('empty file should be Unknown', () => {
278+
it('empty file should be Empy', () => {
279279
const content = '';
280280
const textDocument = TextDocument.create('file:///test.json', 'json', 1, content);
281281
const doc = new Document(textDocument);
282282

283-
expect(doc.cfnFileType).toBe(CloudFormationFileType.Unknown);
283+
expect(doc.cfnFileType).toBe(CloudFormationFileType.Empty);
284284
});
285285

286-
it('whitespace-only file should be Unknown', () => {
286+
it('whitespace-only file should be Empy', () => {
287287
const content = ' \n\n \t ';
288288
const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content);
289289
const doc = new Document(textDocument);
290290

291-
expect(doc.cfnFileType).toBe(CloudFormationFileType.Unknown);
291+
expect(doc.cfnFileType).toBe(CloudFormationFileType.Empty);
292+
});
293+
294+
it('string only should be Empty', () => {
295+
const content = '\nRe\n';
296+
const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content);
297+
const doc = new Document(textDocument);
298+
299+
expect(doc.cfnFileType).toBe(CloudFormationFileType.Empty);
292300
});
293301
});
294302

tst/unit/document/DocumentManager.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ describe('DocumentManager', () => {
2626
expect(documentManager.isTemplate(uri)).toBe(true);
2727
});
2828

29-
it('should return Unknown for non-CloudFormation files', () => {
29+
it('should return Other for non-CloudFormation files', () => {
3030
const uri = 'file:///config.yaml';
3131
const content = 'name: my-app\nversion: 1.0.0';
3232
const textDocument = TextDocument.create(uri, 'yaml', 1, content);
3333
mockDocuments.get.returns(textDocument);
3434

35-
expect(documentManager.get(uri)?.cfnFileType).toBe(CloudFormationFileType.Unknown);
35+
expect(documentManager.get(uri)?.cfnFileType).toBe(CloudFormationFileType.Other);
3636
expect(documentManager.isTemplate(uri)).toBe(false);
3737
});
3838

tst/utils/TemplateBuilder.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ContextManager } from '../../src/context/ContextManager';
1111
import { SectionType, TopLevelSection } from '../../src/context/ContextType';
1212
import { SyntaxTreeManager } from '../../src/context/syntaxtree/SyntaxTreeManager';
1313
import { DefinitionProvider } from '../../src/definition/DefinitionProvider';
14-
import { DocumentType } from '../../src/document/Document';
14+
import { DocumentType, Document } from '../../src/document/Document';
1515
import { DocumentManager } from '../../src/document/DocumentManager';
1616
import { HoverRouter } from '../../src/hover/HoverRouter';
1717
import { SchemaRetriever } from '../../src/schema/SchemaRetriever';
@@ -188,21 +188,20 @@ export class TemplateBuilder {
188188
initialize(content: string = ''): void {
189189
this.version = 1;
190190

191+
// Clear any existing state
192+
this.syntaxTreeManager.deleteSyntaxTree(this.uri);
193+
(this.textDocuments as any)._syncedDocuments.delete(this.uri);
194+
191195
// Create a TextDocument and add it to the TextDocuments collection
192196
const textDocument = TextDocument.create(this.uri, 'yaml', this.version, content);
193197

194198
// Manually add the document to the TextDocuments collection
195199
// This simulates what happens when LSP receives a didOpen notification
196200
(this.textDocuments as any)._syncedDocuments.set(this.uri, textDocument);
197201

198-
// Create syntax tree if content is provided (simulating didOpenHandler behavior)
199-
if (content) {
200-
try {
201-
this.syntaxTreeManager.add(this.uri, content);
202-
} catch {
203-
// Ignore syntax tree creation errors in tests
204-
}
205-
}
202+
// Create syntax tree using proper document detection (like real LSP)
203+
const document = new Document(textDocument);
204+
this.syntaxTreeManager.addWithTypes(this.uri, document.contents(), document.documentType, document.cfnFileType);
206205
}
207206

208207
typeAt(position: Position, text: string): void {

0 commit comments

Comments
 (0)