diff --git a/src/ai/CfnAI.ts b/src/ai/CfnAI.ts index b476a66e..cf70b4a9 100644 --- a/src/ai/CfnAI.ts +++ b/src/ai/CfnAI.ts @@ -58,10 +58,11 @@ export class CfnAI implements SettingsConfigurable, Closeable { throw new Error(`Template not found ${toString(templateFile)}`); } - return await agent.execute( - await Prompts.describeTemplate(document.contents()), - await this.getToolsWithFallback(), - ); + const content = document.contents(); + if (!content) { + throw new Error('Document content is undefined'); + } + return await agent.execute(await Prompts.describeTemplate(content), await this.getToolsWithFallback()); }); } @@ -72,10 +73,11 @@ export class CfnAI implements SettingsConfigurable, Closeable { throw new Error(`Template not found ${toString(templateFile)}`); } - return await agent.execute( - await Prompts.optimizeTemplate(document.contents()), - await this.getToolsWithFallback(), - ); + const content = document.contents(); + if (!content) { + throw new Error('Document content is undefined'); + } + return await agent.execute(await Prompts.optimizeTemplate(content), await this.getToolsWithFallback()); }); } @@ -86,8 +88,12 @@ export class CfnAI implements SettingsConfigurable, Closeable { return; } + const content = document.contents(); + if (!content) { + throw new Error('Document content is undefined'); + } return await agent.execute( - await Prompts.analyzeDiagnostic(document.contents(), diagnostics), + await Prompts.analyzeDiagnostic(content, diagnostics), await this.getToolsWithFallback(), ); }); @@ -107,6 +113,9 @@ export class CfnAI implements SettingsConfigurable, Closeable { } const templateContent = document.contents(); + if (!templateContent) { + throw new Error('Document content is undefined'); + } const resourceTypes = this.relationshipSchemaService.extractResourceTypesFromTemplate(templateContent); const relationshipContext = this.relationshipSchemaService.getRelationshipContext(resourceTypes); diff --git a/src/codeLens/ManagedResourceCodeLens.ts b/src/codeLens/ManagedResourceCodeLens.ts index 71eb9bd9..d6da65cb 100644 --- a/src/codeLens/ManagedResourceCodeLens.ts +++ b/src/codeLens/ManagedResourceCodeLens.ts @@ -28,6 +28,9 @@ export class ManagedResourceCodeLens { } const text = document.getText(); + if (!text) { + return []; + } const lines = text.split('\n'); for (const [, resourceContext] of resourcesMap) { diff --git a/src/codeLens/StackActionsCodeLens.ts b/src/codeLens/StackActionsCodeLens.ts index abfafa93..af57457f 100644 --- a/src/codeLens/StackActionsCodeLens.ts +++ b/src/codeLens/StackActionsCodeLens.ts @@ -36,6 +36,9 @@ export function getStackActionsCodeLenses( let codeLensLine = 0; const lines = document.getLines(); + if (!lines) { + return []; + } for (const [i, line] of lines.entries()) { const lineContents = line.trim(); if (lineContents.length > 0 && !lineContents.startsWith('#')) { diff --git a/src/context/FileContextManager.ts b/src/context/FileContextManager.ts index 64d71369..5616409d 100644 --- a/src/context/FileContextManager.ts +++ b/src/context/FileContextManager.ts @@ -22,8 +22,13 @@ export class FileContextManager { return undefined; } + const content = document.contents(); + if (!content) { + return undefined; + } + try { - return new FileContext(uri, document.documentType, document.contents()); + return new FileContext(uri, document.documentType, content); } catch (error) { this.log.error(error, `Failed to create file context ${uri}`); return undefined; diff --git a/src/document/Document.ts b/src/document/Document.ts index c06bb73e..0a62b751 100644 --- a/src/document/Document.ts +++ b/src/document/Document.ts @@ -16,15 +16,15 @@ export class Document { private cachedParsedContent: unknown; constructor( - private readonly textDocument: TextDocument, + public readonly uri: DocumentUri, + private readonly textDocument: (uri: string) => TextDocument | undefined, detectIndentation: boolean = true, fallbackTabSize: number = DefaultSettings.editor.tabSize, - public readonly uri: DocumentUri = textDocument.uri, - public readonly languageId: string = textDocument.languageId, - public readonly version: number = textDocument.version, - public readonly lineCount: number = textDocument.lineCount, ) { - const { extension, type } = detectDocumentType(textDocument.uri, textDocument.getText()); + const doc = this.getTextDocument(); + const { extension, type } = doc + ? detectDocumentType(doc.uri, doc.getText()) + : { extension: '', type: DocumentType.YAML }; this.extension = extension; this.documentType = type; @@ -36,12 +36,28 @@ export class Document { this.processIndentation(detectIndentation, fallbackTabSize); } + private getTextDocument(): TextDocument | undefined { + return this.textDocument(this.uri); + } + + public get languageId(): string | undefined { + return this.getTextDocument()?.languageId; + } + + public get version(): number | undefined { + return this.getTextDocument()?.version; + } + + public get lineCount(): number | undefined { + return this.getTextDocument()?.lineCount; + } + public get cfnFileType(): CloudFormationFileType { return this._cfnFileType; } public updateCfnFileType(): void { - const content = this.textDocument.getText(); + const content = this.getTextDocument()?.getText() ?? ''; if (!content.trim()) { this._cfnFileType = CloudFormationFileType.Empty; this.cachedParsedContent = undefined; @@ -54,14 +70,12 @@ export class Document { } catch { // If parsing fails, leave cfnFileType unchanged and clear cache this.cachedParsedContent = undefined; - this.log.debug( - `Failed to parse document ${this.textDocument.uri}, keeping cfnFileType as ${this._cfnFileType}`, - ); + this.log.debug(`Failed to parse document ${this.uri}, keeping cfnFileType as ${this._cfnFileType}`); } } private parseContent(): unknown { - const content = this.textDocument.getText(); + const content = this.getTextDocument()?.getText() ?? ''; if (this.documentType === DocumentType.JSON) { return JSON.parse(content); } @@ -124,19 +138,19 @@ export class Document { } public getText(range?: Range) { - return this.textDocument.getText(range); + return this.getTextDocument()?.getText(range); } - public getLines(): string[] { - return this.getText().split('\n'); + public getLines(): string[] | undefined { + return this.getText()?.split('\n'); } public positionAt(offset: number) { - return this.textDocument.positionAt(offset); + return this.getTextDocument()?.positionAt(offset); } public offsetAt(position: Position) { - return this.textDocument.offsetAt(position); + return this.getTextDocument()?.offsetAt(position); } public isTemplate() { @@ -144,7 +158,7 @@ export class Document { } public contents() { - return this.textDocument.getText(); + return this.getTextDocument()?.getText(); } public metadata(): DocumentMetadata { @@ -154,9 +168,9 @@ export class Document { ext: this.extension, type: this.documentType, cfnType: this.cfnFileType, - languageId: this.languageId, - version: this.version, - lineCount: this.lineCount, + languageId: this.languageId ?? '', + version: this.version ?? 0, + lineCount: this.lineCount ?? 0, }; } @@ -176,6 +190,9 @@ export class Document { private detectIndentationFromContent(): number | undefined { const content = this.contents(); + if (!content) { + return undefined; + } const lines = content.split('\n'); const maxLinesToAnalyze = Math.min(lines.length, 30); diff --git a/src/document/DocumentManager.ts b/src/document/DocumentManager.ts index 4d3255ec..82d50879 100644 --- a/src/document/DocumentManager.ts +++ b/src/document/DocumentManager.ts @@ -46,17 +46,22 @@ export class DocumentManager implements SettingsConfigurable, Closeable { } get(uri: string) { - let document = this.documentMap.get(uri); - if (document) { - return document; - } - const textDocument = this.documents.get(uri); if (!textDocument) { return; } - document = new Document(textDocument, this.editorSettings.detectIndentation, this.editorSettings.tabSize); + let document = this.documentMap.get(uri); + if (document) { + return document; + } + + document = new Document( + uri, + (u) => this.documents.get(u), + this.editorSettings.detectIndentation, + this.editorSettings.tabSize, + ); this.documentMap.set(uri, document); return document; } @@ -73,7 +78,12 @@ export class DocumentManager implements SettingsConfigurable, Closeable { for (const textDoc of this.documents.all()) { let document = this.documentMap.get(textDoc.uri); if (!document) { - document = new Document(textDoc, this.editorSettings.detectIndentation, this.editorSettings.tabSize); + document = new Document( + textDoc.uri, + (u) => this.documents.get(u), + this.editorSettings.detectIndentation, + this.editorSettings.tabSize, + ); this.documentMap.set(textDoc.uri, document); } allDocs.push(document); @@ -166,7 +176,10 @@ export class DocumentManager implements SettingsConfigurable, Closeable { private emitDocSizeMetrics() { for (const doc of this.documentMap.values()) { if (doc.isTemplate()) { - this.telemetry.histogram('documents.template.size.bytes', byteSize(doc.contents()), { unit: 'By' }); + const content = doc.contents(); + if (content) { + this.telemetry.histogram('documents.template.size.bytes', byteSize(content), { unit: 'By' }); + } } } } diff --git a/src/handlers/DocumentHandler.ts b/src/handlers/DocumentHandler.ts index 3991c396..c22ac58a 100644 --- a/src/handlers/DocumentHandler.ts +++ b/src/handlers/DocumentHandler.ts @@ -25,6 +25,10 @@ export function didOpenHandler(components: ServerComponents): (event: TextDocume } const content = document.contents(); + if (content === undefined) { + log.error(`No content found for document ${uri}`); + return; + } if (document.isTemplate() || document.cfnFileType === CloudFormationFileType.Empty) { try { @@ -55,8 +59,12 @@ export function didChangeHandler( } // This is the document AFTER changes - const document = new Document(textDocument); + const document = new Document(textDocument.uri, () => textDocument); const finalContent = document.getText(); + if (finalContent === undefined) { + log.error(`No content found for document ${documentUri}`); + return; + } const tree = components.syntaxTreeManager.getSyntaxTree(documentUri); diff --git a/src/handlers/StackHandler.ts b/src/handlers/StackHandler.ts index 330342af..828ae0a2 100644 --- a/src/handlers/StackHandler.ts +++ b/src/handlers/StackHandler.ts @@ -96,12 +96,12 @@ export function getTemplateArtifactsHandler( throw new Error(`Cannot retrieve file with uri: ${params}`); } - const template = new ArtifactExporter( - components.s3Service, - document.documentType, - document.uri, - document.contents(), - ); + const content = document.contents(); + if (content === undefined) { + throw new Error(`Cannot retrieve content for file: ${params}`); + } + + const template = new ArtifactExporter(components.s3Service, document.documentType, document.uri, content); const artifacts = template.getTemplateArtifacts(); return { artifacts }; } catch (error) { diff --git a/src/resourceState/ResourceStateImporter.ts b/src/resourceState/ResourceStateImporter.ts index 4008633b..61a1a1ee 100644 --- a/src/resourceState/ResourceStateImporter.ts +++ b/src/resourceState/ResourceStateImporter.ts @@ -114,6 +114,9 @@ export class ResourceStateImporter { if (insertPosition.replaceEntireFile) { // Replace entire file with properly formatted JSON snippetText = docFormattedText; + if (document.lineCount === undefined) { + return this.getFailureResponse('Import failed. Document is no longer available.'); + } const endPosition = { line: document.lineCount, character: 0 }; textEdit = TextEdit.replace(Range.create({ line: 0, character: 0 }, endPosition), snippetText); } else { @@ -420,7 +423,7 @@ export class ResourceStateImporter { : { line: resourcesSection.endPosition.row + 1, character: 0 }; } else { // Find the last non-empty line - let lastNonEmptyLine = document.lineCount - 1; + let lastNonEmptyLine = document.lineCount ? document.lineCount - 1 : 0; while (lastNonEmptyLine >= 0 && document.getLine(lastNonEmptyLine)?.trim().length === 0) { lastNonEmptyLine--; } @@ -434,12 +437,25 @@ export class ResourceStateImporter { }; } - let line = resourcesSection ? resourcesSection.endPosition.row : document.lineCount - 1; + let line = resourcesSection + ? resourcesSection.endPosition.row + : document.lineCount + ? document.lineCount - 1 + : 0; // For JSON without Resources section, check if file is essentially empty if (!resourcesSection) { try { - const parsed = JSON.parse(document.getText()) as Record; + const text = document.getText(); + if (!text) { + return { + position: { line: 0, character: 0 }, + commaPrefixNeeded: false, + newLineSuffixNeeded: false, + replaceEntireFile: true, + }; + } + const parsed = JSON.parse(text) as Record; const hasContent = Object.keys(parsed).length > 0; // If no content, replace entire file @@ -495,7 +511,7 @@ export class ResourceStateImporter { } // malformed case, allow import to end of document return { - position: { line: document.lineCount, character: 0 }, + position: { line: document.lineCount ?? 0, character: 0 }, commaPrefixNeeded: false, newLineSuffixNeeded: false, replaceEntireFile: false, diff --git a/src/stacks/actions/StackActionOperations.ts b/src/stacks/actions/StackActionOperations.ts index fa74902e..06a7e111 100644 --- a/src/stacks/actions/StackActionOperations.ts +++ b/src/stacks/actions/StackActionOperations.ts @@ -97,6 +97,9 @@ export async function processChangeSet( throw new ResponseError(ErrorCodes.InvalidParams, `Document not found: ${params.uri}`); } let templateBody = document.contents(); + if (!templateBody) { + throw new ResponseError(ErrorCodes.InvalidParams, `Document content is undefined: ${params.uri}`); + } let templateS3Url: string | undefined; let expectedETag: string | undefined; try { @@ -104,7 +107,7 @@ export async function processChangeSet( const s3KeyPrefix = params.s3Key?.includes('/') ? params.s3Key.slice(0, params.s3Key.lastIndexOf('/')) : undefined; - const template = new ArtifactExporter(s3Service, document.documentType, document.uri, document.contents()); + const template = new ArtifactExporter(s3Service, document.documentType, document.uri, templateBody); const exportedTemplate = await template.export(params.s3Bucket, s3KeyPrefix); diff --git a/src/utils/ResourceInsertionUtils.ts b/src/utils/ResourceInsertionUtils.ts index 74ccc813..a1fdf018 100644 --- a/src/utils/ResourceInsertionUtils.ts +++ b/src/utils/ResourceInsertionUtils.ts @@ -29,16 +29,17 @@ export function getInsertPosition(resourcesSection: SyntaxNode | undefined, docu ? { line: resourcesSection.endPosition.row, character: 0 } : { line: resourcesSection.endPosition.row + 1, character: 0 }; } else { + const lineCount = document.lineCount ?? 1; position = - document.getLine(document.lineCount - 1)?.trim().length === 0 - ? { line: document.lineCount - 1, character: 0 } - : { line: document.lineCount, character: 0 }; + document.getLine(lineCount - 1)?.trim().length === 0 + ? { line: lineCount - 1, character: 0 } + : { line: lineCount, character: 0 }; } return { position, commaPrefixNeeded: false, newLineSuffixNeeded: false }; } // JSON handling - let line = resourcesSection ? resourcesSection.endPosition.row : document.lineCount - 1; + let line = resourcesSection ? resourcesSection.endPosition.row : (document.lineCount ?? 1) - 1; while (line > 0) { const previousLine = document.getLine(line - 1); if (previousLine === undefined) { @@ -61,7 +62,7 @@ export function getInsertPosition(resourcesSection: SyntaxNode | undefined, docu } // malformed case, allow import to end of document return { - position: { line: document.lineCount, character: 0 }, + position: { line: document.lineCount ?? 0, character: 0 }, commaPrefixNeeded: false, newLineSuffixNeeded: false, }; diff --git a/tools/utils.ts b/tools/utils.ts index 2b9ead8e..cdd18640 100644 --- a/tools/utils.ts +++ b/tools/utils.ts @@ -174,7 +174,7 @@ export function discoverTemplateFiles(paths: string[]): TemplateFile[] { const content = readFileSync(path, 'utf8'); const { extension, type } = detectDocumentType(path, content); const textDocument = TextDocument.create(path, type === DocumentType.JSON ? 'json' : 'yaml', 1, content); - const document = new Document(textDocument); + const document = new Document(textDocument.uri, () => textDocument); return { name: uriToPath(path).base, diff --git a/tst/unit/codeLens/ManagedResourceCodeLens.test.ts b/tst/unit/codeLens/ManagedResourceCodeLens.test.ts index ea60075f..6b02aedc 100644 --- a/tst/unit/codeLens/ManagedResourceCodeLens.test.ts +++ b/tst/unit/codeLens/ManagedResourceCodeLens.test.ts @@ -1,9 +1,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { TextDocument } from 'vscode-languageserver-textdocument'; import { ManagedResourceCodeLens } from '../../../src/codeLens/ManagedResourceCodeLens'; import { getEntityMap } from '../../../src/context/SectionContextBuilder'; -import { Document } from '../../../src/document/Document'; import { createMockSyntaxTreeManager } from '../../utils/MockServerComponents'; +import { createTestDocument } from '../../utils/Utils'; // Mock the SectionContextBuilder module vi.mock('../../../src/context/SectionContextBuilder', () => ({ @@ -25,8 +24,11 @@ describe('ManagedResourceCodeLens', () => { it('should return empty array when syntax tree is not found', () => { mockSyntaxTreeManager.getSyntaxTree.returns({} as any); - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'Resources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = codeLens.getCodeLenses('file:///test.yaml', document); @@ -37,9 +39,7 @@ describe('ManagedResourceCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({} as any); mockGetEntityMap.mockReturnValue(undefined); - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'AWSTemplateFormatVersion: "2010-09-09"'), - ); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, 'AWSTemplateFormatVersion: "2010-09-09"'); const result = codeLens.getCodeLenses('file:///test.yaml', document); expect(result).toEqual([]); @@ -54,7 +54,7 @@ describe('ManagedResourceCodeLens', () => { StackName: test-stack PrimaryIdentifier: bucket-id`; - const document = new Document(TextDocument.create('file:///test.yaml', 'yaml', 1, yamlContent)); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, yamlContent); const mockResourceContext = { entity: { @@ -88,7 +88,7 @@ describe('ManagedResourceCodeLens', () => { StackName: test-stack PrimaryIdentifier: bucket-id`; - const document = new Document(TextDocument.create('file:///test.yaml', 'yaml', 1, yamlContent)); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, yamlContent); const mockResourceContext = { entity: { @@ -118,7 +118,7 @@ describe('ManagedResourceCodeLens', () => { ManagedByStack: true StackName: test-stack`; - const document = new Document(TextDocument.create('file:///test.yaml', 'yaml', 1, yamlContent)); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, yamlContent); const mockResourceContext = { entity: { diff --git a/tst/unit/codeLens/StackActionsCodeLens.test.ts b/tst/unit/codeLens/StackActionsCodeLens.test.ts index 899d142f..e20b3785 100644 --- a/tst/unit/codeLens/StackActionsCodeLens.test.ts +++ b/tst/unit/codeLens/StackActionsCodeLens.test.ts @@ -1,9 +1,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { TextDocument } from 'vscode-languageserver-textdocument'; import { getStackActionsCodeLenses } from '../../../src/codeLens/StackActionsCodeLens'; import { getEntityMap } from '../../../src/context/SectionContextBuilder'; -import { Document } from '../../../src/document/Document'; import { createMockSyntaxTreeManager } from '../../utils/MockServerComponents'; +import { createTestDocument } from '../../utils/Utils'; vi.mock('../../../src/context/SectionContextBuilder', () => ({ getEntityMap: vi.fn(), @@ -22,8 +21,11 @@ describe('StackActionsCodeLens', () => { it('should return empty array when syntax tree is not found', () => { mockSyntaxTreeManager.getSyntaxTree.returns(undefined); - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'Resources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -35,9 +37,7 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(undefined); - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'AWSTemplateFormatVersion: "2010-09-09"'), - ); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, 'AWSTemplateFormatVersion: "2010-09-09"'); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -48,7 +48,7 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map()); - const document = new Document(TextDocument.create('file:///test.yaml', 'yaml', 1, 'Resources: {}')); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, 'Resources: {}'); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -59,9 +59,7 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'some: random\nyaml: content'), - ); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, 'some: random\nyaml: content'); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -72,8 +70,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'Resources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -88,8 +89,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'Resources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -102,8 +106,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, '\n\nResources:\n Bucket:\n Type: AWS::S3::Bucket'), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + '\n\nResources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -115,13 +122,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create( - 'file:///test.yaml', - 'yaml', - 1, - '# This is a comment\n# Another comment\nResources:\n Bucket:\n Type: AWS::S3::Bucket', - ), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + '# This is a comment\n# Another comment\nResources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -133,13 +138,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create( - 'file:///test.yaml', - 'yaml', - 1, - '\n# Comment\n\n# Another comment\n\nResources:\n Bucket:\n Type: AWS::S3::Bucket', - ), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + '\n# Comment\n\n# Another comment\n\nResources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -151,13 +154,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create( - 'file:///test.yaml', - 'yaml', - 1, - 'AWSTemplateFormatVersion: "2010-09-09"\nResources:\n Bucket:\n Type: AWS::S3::Bucket', - ), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'AWSTemplateFormatVersion: "2010-09-09"\nResources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -170,13 +171,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Function', {}]])); - const document = new Document( - TextDocument.create( - 'file:///test.yaml', - 'yaml', - 1, - 'Transform: AWS::Serverless-2016-10-31\nResources:\n Function:\n Type: AWS::Serverless::Function', - ), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'Transform: AWS::Serverless-2016-10-31\nResources:\n Function:\n Type: AWS::Serverless::Function', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -188,8 +187,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create('file:///test.json', 'json', 1, '{"Resources":{"Bucket":{"Type":"AWS::S3::Bucket"}}}'), + const document = createTestDocument( + 'file:///test.json', + 'json', + 1, + '{"Resources":{"Bucket":{"Type":"AWS::S3::Bucket"}}}', ); const result = getStackActionsCodeLenses('file:///test.json', document, mockSyntaxTreeManager); @@ -207,13 +209,11 @@ describe('StackActionsCodeLens', () => { ]), ); - const document = new Document( - TextDocument.create( - 'file:///test.yaml', - 'yaml', - 1, - 'Resources:\n Bucket:\n Type: AWS::S3::Bucket\n Table:\n Type: AWS::DynamoDB::Table', - ), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'Resources:\n Bucket:\n Type: AWS::S3::Bucket\n Table:\n Type: AWS::DynamoDB::Table', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); @@ -225,13 +225,11 @@ describe('StackActionsCodeLens', () => { mockSyntaxTreeManager.getSyntaxTree.returns({ rootNode: {} } as any); mockGetEntityMap.mockReturnValue(new Map([['Bucket', {}]])); - const document = new Document( - TextDocument.create( - 'file:///test.yaml', - 'yaml', - 1, - ' \n\t\n \nResources:\n Bucket:\n Type: AWS::S3::Bucket', - ), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + ' \n\t\n \nResources:\n Bucket:\n Type: AWS::S3::Bucket', ); const result = getStackActionsCodeLenses('file:///test.yaml', document, mockSyntaxTreeManager); diff --git a/tst/unit/context/FileContextManager.test.ts b/tst/unit/context/FileContextManager.test.ts index 8a0010cf..81e88bd9 100644 --- a/tst/unit/context/FileContextManager.test.ts +++ b/tst/unit/context/FileContextManager.test.ts @@ -168,16 +168,14 @@ describe('FileContextManager', () => { documentType: DocumentType.YAML, contents: vi.fn().mockReturnValue(''), }; - const mockFileContext = {} as FileContext; mockDocumentManager.get.returns(mockDocument as any); mockDocumentManager.isTemplate.returns(true); - MockedFileContext.mockImplementation(() => mockFileContext); const result = fileContextManager.getFileContext(testUri); - expect(MockedFileContext).toHaveBeenCalledWith(testUri, DocumentType.YAML, ''); - expect(result).toBe(mockFileContext); + expect(MockedFileContext).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); }); it('should handle complex CloudFormation templates', () => { @@ -263,12 +261,11 @@ Outputs: mockDocumentManager.get.returns(mockDocument as any); mockDocumentManager.isTemplate.returns(true); - MockedFileContext.mockImplementation(() => ({}) as FileContext); const result = fileContextManager.getFileContext(testUri); - expect(MockedFileContext).toHaveBeenCalledWith(testUri, DocumentType.YAML, undefined); - expect(result).toBeDefined(); + expect(MockedFileContext).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); }); it('should handle DocumentManager throwing errors', () => { diff --git a/tst/unit/document/Document.test.ts b/tst/unit/document/Document.test.ts index 461e6345..1b40850c 100644 --- a/tst/unit/document/Document.test.ts +++ b/tst/unit/document/Document.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { CloudFormationFileType, Document, DocumentType } from '../../../src/document/Document'; +import { CloudFormationFileType, DocumentType } from '../../../src/document/Document'; +import { createDocument } from '../../utils/Utils'; describe('Document', () => { describe('constructor', () => { @@ -8,7 +9,7 @@ describe('Document', () => { const content = 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.extension).toBe('yaml'); expect(doc.documentType).toBe(DocumentType.YAML); @@ -19,7 +20,7 @@ describe('Document', () => { const content = '{"Resources": {"Bucket": {"Type": "AWS::S3::Bucket"}}}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.extension).toBe('json'); expect(doc.documentType).toBe(DocumentType.JSON); @@ -30,7 +31,7 @@ describe('Document', () => { const jsonContent = '{"Resources": {}}'; const textDocument = TextDocument.create('file:///test.template', 'template', 1, jsonContent); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.documentType).toBe(DocumentType.JSON); }); @@ -40,14 +41,14 @@ describe('Document', () => { it('should return current content', () => { const content = 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.contents()).toBe(content); }); it('should return updated content after changes', () => { const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, 'old'); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); TextDocument.update(textDocument, [{ text: 'new content' }], 2); @@ -63,14 +64,14 @@ describe('Document', () => { 1, 'Resources:\n Bucket:\n Type: AWS::S3::Bucket', ); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Template); }); it('should handle detection errors gracefully', () => { const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, 'invalid: [unclosed'); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(() => doc.cfnFileType).not.toThrow(); expect(doc.cfnFileType).toBeDefined(); @@ -81,7 +82,7 @@ describe('Document', () => { it('should return correct line by number', () => { const content = 'line 0\nline 1\nline 2'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.getLine(0)).toBe('line 0\n'); expect(doc.getLine(1)).toBe('line 1\n'); @@ -91,7 +92,7 @@ describe('Document', () => { it('should return empty string for negative line number', () => { const content = 'line 0\nline 1'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.getLine(-1)).toBe(''); }); @@ -99,7 +100,7 @@ describe('Document', () => { it('should return empty string for line number beyond content', () => { const content = 'line 0\nline 1'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.getLine(2)).toBe(''); expect(doc.getLine(5)).toBe(''); @@ -107,7 +108,7 @@ describe('Document', () => { it('should handle empty content', () => { const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, ''); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.getLine(0)).toBe(''); expect(doc.getLine(1)).toBe(''); @@ -118,7 +119,7 @@ describe('Document', () => { it('should parse JSON document content', () => { const content = '{"Resources": {"Bucket": {"Type": "AWS::S3::Bucket"}}}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); const parsed = doc.getParsedDocumentContent(); @@ -134,7 +135,7 @@ describe('Document', () => { it('should parse YAML document content', () => { const content = 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); const parsed = doc.getParsedDocumentContent(); @@ -150,7 +151,7 @@ describe('Document', () => { it('should return undefined for invalid JSON', () => { const content = '{"invalid": json}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.getParsedDocumentContent()).toBeUndefined(); }); @@ -158,7 +159,7 @@ describe('Document', () => { it('should return undefined for invalid YAML', () => { const content = 'key: value\n invalid: indentation'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.getParsedDocumentContent()).toBeUndefined(); }); @@ -169,7 +170,7 @@ describe('Document', () => { it('with languageId cloudformation', () => { const content = '{}'; // Empty content const textDocument = TextDocument.create('file:///test.json', 'cloudformation', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Template); }); @@ -177,7 +178,7 @@ describe('Document', () => { it('with AWSTemplateFormatVersion', () => { const content = '{"AWSTemplateFormatVersion": "2010-09-09"}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Template); }); @@ -185,7 +186,7 @@ describe('Document', () => { it('with Resources', () => { const content = '{"Resources": {"Bucket": {"Type": "AWS::S3::Bucket"}}}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Template); }); @@ -193,7 +194,7 @@ describe('Document', () => { it('with Transform', () => { const content = '{"Transform": "AWS::Serverless-2016-10-31"}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Template); }); @@ -201,7 +202,7 @@ describe('Document', () => { it('YAML template with Resources', () => { const content = 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Template); }); @@ -211,7 +212,7 @@ describe('Document', () => { it('with template-file-path', () => { const content = '{"template-file-path": "./template.yaml"}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.GitSyncDeployment); }); @@ -219,7 +220,7 @@ describe('Document', () => { it('with templateFilePath', () => { const content = 'templateFilePath: ./template.yaml'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.GitSyncDeployment); }); @@ -245,7 +246,7 @@ describe('Document', () => { ] }`; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Other); }); @@ -253,7 +254,7 @@ describe('Document', () => { it('package.json with CloudFormation-like keys', () => { const content = '{"name": "my-package", "Parameters": {"env": "prod"}, "Outputs": {"build": "dist"}}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Other); }); @@ -268,7 +269,7 @@ describe('Document', () => { } }`; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Other); }); @@ -278,7 +279,7 @@ describe('Document', () => { it('empty file should be Empy', () => { const content = ''; const textDocument = TextDocument.create('file:///test.json', 'json', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Empty); }); @@ -286,7 +287,7 @@ describe('Document', () => { it('whitespace-only file should be Empy', () => { const content = ' \n\n \t '; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Empty); }); @@ -294,7 +295,7 @@ describe('Document', () => { it('string only should be Empty', () => { const content = '\nRe\n'; const textDocument = TextDocument.create('file:///test.yaml', 'yaml', 1, content); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Empty); }); @@ -305,7 +306,7 @@ describe('Document', () => { // Start with valid CloudFormation template const validContent = '{"AWSTemplateFormatVersion": "2010-09-09", "Resources": {}}'; const textDocument = TextDocument.create('file:///test.json', 'json', 1, validContent); - const doc = new Document(textDocument); + const doc = createDocument(textDocument); expect(doc.cfnFileType).toBe(CloudFormationFileType.Template); diff --git a/tst/unit/document/DocumentManager.test.ts b/tst/unit/document/DocumentManager.test.ts index 37f2773a..8cbac686 100644 --- a/tst/unit/document/DocumentManager.test.ts +++ b/tst/unit/document/DocumentManager.test.ts @@ -63,4 +63,94 @@ describe('DocumentManager', () => { expect(documentManager.getLine('file:///nonexistent.yaml', 0)).toBeUndefined(); }); }); + + describe('document cache updates', () => { + it('should cache document on first access', () => { + const uri = 'file:///test.yaml'; + const content = 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'; + const textDocument = TextDocument.create(uri, 'yaml', 1, content); + + mockDocuments.get.returns(textDocument); + + const doc1 = documentManager.get(uri); + const doc2 = documentManager.get(uri); + + expect(doc1).toBe(doc2); + }); + + it('should return updated content when TextDocument is mutated', () => { + const uri = 'file:///test.yaml'; + const textDocument = TextDocument.create(uri, 'yaml', 1, 'old content'); + + mockDocuments.get.returns(textDocument); + + const doc = documentManager.get(uri); + expect(doc?.contents()).toBe('old content'); + + TextDocument.update(textDocument, [{ text: 'new content' }], 2); + + expect(doc?.contents()).toBe('new content'); + }); + + it('should return updated version when TextDocument is mutated', () => { + const uri = 'file:///test.yaml'; + const textDocument = TextDocument.create(uri, 'yaml', 1, 'content'); + + mockDocuments.get.returns(textDocument); + + const doc = documentManager.get(uri); + expect(doc?.version).toBe(1); + + TextDocument.update(textDocument, [{ text: 'updated' }], 2); + + expect(doc?.version).toBe(2); + }); + + it('should return updated lineCount when TextDocument is mutated', () => { + const uri = 'file:///test.yaml'; + const textDocument = TextDocument.create(uri, 'yaml', 1, 'line1'); + + mockDocuments.get.returns(textDocument); + + const doc = documentManager.get(uri); + expect(doc?.lineCount).toBe(1); + + TextDocument.update(textDocument, [{ text: 'line1\nline2\nline3' }], 2); + + expect(doc?.lineCount).toBe(3); + }); + + it('should remove document from cache', () => { + const uri = 'file:///test.yaml'; + const textDocument = TextDocument.create(uri, 'yaml', 1, 'content'); + + mockDocuments.get.returns(textDocument); + + const doc1 = documentManager.get(uri); + expect(doc1).toBeDefined(); + + documentManager.removeDocument(uri); + + const doc2 = documentManager.get(uri); + expect(doc2).not.toBe(doc1); + }); + + it('should recreate document after cache invalidation', () => { + const uri = 'file:///test.yaml'; + const textDocument = TextDocument.create(uri, 'yaml', 1, 'Resources: {}'); + + mockDocuments.get.returns(textDocument); + + const doc1 = documentManager.get(uri); + expect(doc1?.cfnFileType).toBe(CloudFormationFileType.Template); + + documentManager.removeDocument(uri); + + TextDocument.update(textDocument, [{ text: 'name: app' }], 2); + + const doc2 = documentManager.get(uri); + expect(doc2).not.toBe(doc1); + expect(doc2?.cfnFileType).toBe(CloudFormationFileType.Other); + }); + }); }); diff --git a/tst/unit/handlers/CodeLensHandler.test.ts b/tst/unit/handlers/CodeLensHandler.test.ts index 90f582f9..c576ae05 100644 --- a/tst/unit/handlers/CodeLensHandler.test.ts +++ b/tst/unit/handlers/CodeLensHandler.test.ts @@ -1,9 +1,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { CodeLensParams, CancellationToken } from 'vscode-languageserver-protocol'; -import { TextDocument } from 'vscode-languageserver-textdocument'; import { CodeLensProvider } from '../../../src/codeLens/CodeLensProvider'; import { getEntityMap } from '../../../src/context/SectionContextBuilder'; -import { Document } from '../../../src/document/Document'; import { codeLensHandler } from '../../../src/handlers/CodeLensHandler'; import { createMockComponents, @@ -11,6 +9,7 @@ import { createMockManagedResourceCodeLens, createMockSyntaxTreeManager, } from '../../utils/MockServerComponents'; +import { createTestDocument } from '../../utils/Utils'; vi.mock('../../../src/context/SectionContextBuilder', () => ({ getEntityMap: vi.fn(), @@ -49,8 +48,11 @@ describe('CodeLensHandler', () => { }); it('should return stack actions and managed resource code lenses for valid CFN template', async () => { - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'Resources:\n Bucket:\n Type: AWS::S3::Bucket', ); docManager.get.returns(document); @@ -84,7 +86,7 @@ describe('CodeLensHandler', () => { }); it('should not return stack actions for empty files', async () => { - const document = new Document(TextDocument.create('file:///test.yaml', 'yaml', 1, '')); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, ''); docManager.get.returns(document); managedCodeLens.getCodeLenses.returns([]); @@ -99,9 +101,7 @@ describe('CodeLensHandler', () => { }); it('should not return stack actions for non-CFN files', async () => { - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'some: random\nyaml: content'), - ); + const document = createTestDocument('file:///test.yaml', 'yaml', 1, 'some: random\nyaml: content'); docManager.get.returns(document); managedCodeLens.getCodeLenses.returns([]); @@ -116,13 +116,11 @@ describe('CodeLensHandler', () => { }); it('should not return stack actions when Resources section is missing', async () => { - const document = new Document( - TextDocument.create( - 'file:///test.yaml', - 'yaml', - 1, - 'AWSTemplateFormatVersion: "2010-09-09"\nDescription: Test', - ), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'AWSTemplateFormatVersion: "2010-09-09"\nDescription: Test', ); docManager.get.returns(document); @@ -140,8 +138,11 @@ describe('CodeLensHandler', () => { }); it('should pass correct arguments to stack action commands', async () => { - const document = new Document( - TextDocument.create('file:///test.yaml', 'yaml', 1, 'Resources:\n Bucket:\n Type: AWS::S3::Bucket'), + const document = createTestDocument( + 'file:///test.yaml', + 'yaml', + 1, + 'Resources:\n Bucket:\n Type: AWS::S3::Bucket', ); docManager.get.returns(document); diff --git a/tst/unit/handlers/DocumentHandler.test.ts b/tst/unit/handlers/DocumentHandler.test.ts index 5a1a3fb1..dac27876 100644 --- a/tst/unit/handlers/DocumentHandler.test.ts +++ b/tst/unit/handlers/DocumentHandler.test.ts @@ -30,7 +30,8 @@ describe('DocumentHandler', () => { } function createMockDocument(cfnFileType = CloudFormationFileType.Template) { - const doc = new Document(createTextDocument()); + const textDoc = createTextDocument(); + const doc = new Document(textDoc.uri, () => textDoc); (doc as any)._cfnFileType = cfnFileType; return doc; } diff --git a/tst/unit/relatedResources/RelatedResourcesSnippetProvider.test.ts b/tst/unit/relatedResources/RelatedResourcesSnippetProvider.test.ts index 92c07732..a5344242 100644 --- a/tst/unit/relatedResources/RelatedResourcesSnippetProvider.test.ts +++ b/tst/unit/relatedResources/RelatedResourcesSnippetProvider.test.ts @@ -1,8 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { CodeActionKind } from 'vscode-languageserver-protocol'; -import { TextDocument } from 'vscode-languageserver-textdocument'; import { getEntityMap } from '../../../src/context/SectionContextBuilder'; -import { Document } from '../../../src/document/Document'; import { RelatedResourcesSnippetProvider } from '../../../src/relatedResources/RelatedResourcesSnippetProvider'; import { createMockComponents, @@ -12,6 +10,7 @@ import { createMockSyntaxTreeManager, } from '../../utils/MockServerComponents'; import { combinedSchemas } from '../../utils/SchemaUtils'; +import { createTestDocument } from '../../utils/Utils'; // Mock the SectionContextBuilder module vi.mock('../../../src/context/SectionContextBuilder', () => ({ @@ -58,7 +57,7 @@ describe('RelatedResourcesSnippetProvider', () => { it('should generate code action for YAML document without Resources section', () => { const templateUri = 'file:///test/template.yaml'; const yamlContent = 'AWSTemplateFormatVersion: "2010-09-09"\n'; - const document = new Document(TextDocument.create(templateUri, 'yaml', 1, yamlContent)); + const document = createTestDocument(templateUri, 'yaml', 1, yamlContent); documentManager.get.withArgs(templateUri).returns(document); syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(undefined); @@ -86,7 +85,7 @@ Resources: MyBucket: Type: AWS::S3::Bucket `; - const document = new Document(TextDocument.create(templateUri, 'yaml', 1, yamlContent)); + const document = createTestDocument(templateUri, 'yaml', 1, yamlContent); const mockSyntaxTree = { findTopLevelSections: vi.fn().mockReturnValue( @@ -118,7 +117,7 @@ Resources: it('should generate code action for JSON document without Resources section', () => { const templateUri = 'file:///test/template.json'; const jsonContent = '{\n "AWSTemplateFormatVersion": "2010-09-09"\n}'; - const document = new Document(TextDocument.create(templateUri, 'json', 1, jsonContent)); + const document = createTestDocument(templateUri, 'json', 1, jsonContent); documentManager.get.withArgs(templateUri).returns(document); syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(undefined); @@ -142,7 +141,7 @@ Resources: } } }`; - const document = new Document(TextDocument.create(templateUri, 'json', 1, jsonContent)); + const document = createTestDocument(templateUri, 'json', 1, jsonContent); const mockSyntaxTree = { findTopLevelSections: vi.fn().mockReturnValue( @@ -176,7 +175,7 @@ Resources: MyBucket: Type: AWS::S3::Bucket `; - const document = new Document(TextDocument.create(templateUri, 'yaml', 1, yamlContent)); + const document = createTestDocument(templateUri, 'yaml', 1, yamlContent); const mockSyntaxTree = { findTopLevelSections: vi.fn().mockReturnValue( @@ -215,7 +214,7 @@ Resources: MyBucket: Type: AWS::S3::Bucket `; - const document = new Document(TextDocument.create(templateUri, 'yaml', 1, yamlContent)); + const document = createTestDocument(templateUri, 'yaml', 1, yamlContent); const mockSyntaxTree = { findTopLevelSections: vi.fn().mockReturnValue( @@ -261,7 +260,7 @@ Resources: LambdaFunctionRelatedToS3Bucket: Type: AWS::Lambda::Function `; - const document = new Document(TextDocument.create(templateUri, 'yaml', 1, yamlContent)); + const document = createTestDocument(templateUri, 'yaml', 1, yamlContent); const mockSyntaxTree = { findTopLevelSections: vi.fn().mockReturnValue( @@ -300,7 +299,7 @@ Resources: MyBucket: Type: AWS::S3::Bucket `; - const document = new Document(TextDocument.create(templateUri, 'yaml', 1, yamlContent)); + const document = createTestDocument(templateUri, 'yaml', 1, yamlContent); const mockSyntaxTree = { findTopLevelSections: vi.fn().mockReturnValue( diff --git a/tst/utils/TemplateBuilder.ts b/tst/utils/TemplateBuilder.ts index dcad3880..7dfab326 100644 --- a/tst/utils/TemplateBuilder.ts +++ b/tst/utils/TemplateBuilder.ts @@ -11,7 +11,7 @@ import { ContextManager } from '../../src/context/ContextManager'; import { SectionType, TopLevelSection } from '../../src/context/ContextType'; import { SyntaxTreeManager } from '../../src/context/syntaxtree/SyntaxTreeManager'; import { DefinitionProvider } from '../../src/definition/DefinitionProvider'; -import { DocumentType, Document } from '../../src/document/Document'; +import { DocumentType } from '../../src/document/Document'; import { DocumentManager } from '../../src/document/DocumentManager'; import { HoverRouter } from '../../src/hover/HoverRouter'; import { SchemaRetriever } from '../../src/schema/SchemaRetriever'; @@ -24,6 +24,7 @@ import { createMockSchemaRetriever, } from './MockServerComponents'; import { combinedSchemas } from './SchemaUtils'; +import { createDocument } from './Utils'; function expectAt(actual: any, position: Position, description?: string) { const positionStr = `${position.line}:${position.character}`; @@ -202,8 +203,13 @@ export class TemplateBuilder { (this.textDocuments as any)._syncedDocuments.set(this.uri, textDocument); // Create syntax tree using proper document detection (like real LSP) - const document = new Document(textDocument); - this.syntaxTreeManager.addWithTypes(this.uri, document.contents(), document.documentType, document.cfnFileType); + const document = createDocument(textDocument); + this.syntaxTreeManager.addWithTypes( + this.uri, + document.contents() ?? '', + document.documentType, + document.cfnFileType, + ); } typeAt(position: Position, text: string): void { diff --git a/tst/utils/Utils.ts b/tst/utils/Utils.ts index 74a55bf0..cd18207a 100644 --- a/tst/utils/Utils.ts +++ b/tst/utils/Utils.ts @@ -1,4 +1,6 @@ import { setImmediate } from 'node:timers/promises'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { Document } from '../../src/document/Document'; export async function flushAllPromises() { await setImmediate(); @@ -52,3 +54,13 @@ export async function waitFor( ): Promise { await WaitFor.waitFor(code, timeoutMs, intervalMs); } + +export function createTestDocument(uri: string, languageId: string, version: number, content: string): Document { + const td = TextDocument.create(uri, languageId, version, content); + return new Document(td.uri, () => td); +} + +// Helper to create Document from TextDocument +export function createDocument(textDocument: TextDocument): Document { + return new Document(textDocument.uri, () => textDocument); +}