Skip to content

Commit 5beb01f

Browse files
authored
Updated diagnostic to only highlight the first line, not entire node (#125)
* Updated diagnostic to only highlight the first line, not entire node * fix test failure * Update to use getKeyRangeFromPath method to get line
1 parent 799aacb commit 5beb01f

File tree

8 files changed

+258
-142
lines changed

8 files changed

+258
-142
lines changed

src/server/CfnInfraCore.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ export class CfnInfraCore implements Configurables, Closeable {
5454
overrides.awsCredentials ?? new AwsCredentials(lspComponents.authHandlers, this.settingsManager);
5555

5656
this.diagnosticCoordinator =
57-
overrides.diagnosticCoordinator ?? new DiagnosticCoordinator(lspComponents.diagnostics);
57+
overrides.diagnosticCoordinator ??
58+
new DiagnosticCoordinator(lspComponents.diagnostics, this.syntaxTreeManager);
5859
}
5960

6061
configurables(): Configurable[] {

src/services/DiagnosticCoordinator.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { Diagnostic, PublishDiagnosticsParams } from 'vscode-languageserver';
1+
import { Diagnostic, PublishDiagnosticsParams, Range } from 'vscode-languageserver';
2+
import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager';
3+
import { NodeType } from '../context/syntaxtree/utils/NodeType';
4+
import { FieldNames } from '../context/syntaxtree/utils/TreeSitterTypes';
25
import { LspDiagnostics } from '../protocol/LspDiagnostics';
36
import { CFN_VALIDATION_SOURCE } from '../stacks/actions/ValidationWorkflow';
47
import { LoggerFactory } from '../telemetry/LoggerFactory';
@@ -16,7 +19,10 @@ export class DiagnosticCoordinator {
1619
private readonly urisToDiagnostics = new Map<string, SourceToDiagnostics>();
1720
private readonly log = LoggerFactory.getLogger(DiagnosticCoordinator);
1821

19-
constructor(private readonly lspDiagnostics: LspDiagnostics) {}
22+
constructor(
23+
private readonly lspDiagnostics: LspDiagnostics,
24+
private readonly syntaxTreeManager: SyntaxTreeManager,
25+
) {}
2026

2127
/**
2228
* Publish diagnostics from a specific source for a document.
@@ -164,4 +170,44 @@ export class DiagnosticCoordinator {
164170

165171
return allDiagnostics;
166172
}
173+
174+
/**
175+
* Extract the key range from a path using syntax tree directly
176+
*/
177+
getKeyRangeFromPath(uri: string, path: string): Range | undefined {
178+
// Parse paths like "/Resources/User/Properties/Policies"
179+
// Remove leading slash if present and split by '/'
180+
const pathSegments = path.startsWith('/') ? path.slice(1).split('/') : path.split('/');
181+
182+
const syntaxTree = this.syntaxTreeManager.getSyntaxTree(uri);
183+
if (!syntaxTree) {
184+
return undefined;
185+
}
186+
187+
// Get the node at the path
188+
const result = syntaxTree.getNodeByPath(pathSegments);
189+
if (!result.node) {
190+
return undefined;
191+
}
192+
193+
// Check if this is a pair node (key/value pair)
194+
if (NodeType.isPairNode(result.node, syntaxTree.type)) {
195+
// Get the key node from the pair
196+
const keyNode = result.node.childForFieldName(FieldNames.KEY);
197+
if (keyNode) {
198+
return {
199+
start: {
200+
line: keyNode.startPosition.row,
201+
character: keyNode.startPosition.column,
202+
},
203+
end: {
204+
line: keyNode.endPosition.row,
205+
character: keyNode.endPosition.column,
206+
},
207+
};
208+
}
209+
}
210+
211+
return undefined;
212+
}
167213
}

src/services/guard/GuardService.ts

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { readFile } from 'fs/promises';
22
import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver';
33
import { SyntaxTreeManager } from '../../context/syntaxtree/SyntaxTreeManager';
4-
import { NodeType } from '../../context/syntaxtree/utils/NodeType';
5-
import { FieldNames } from '../../context/syntaxtree/utils/TreeSitterTypes';
64
import { CloudFormationFileType } from '../../document/Document';
75
import { DocumentManager } from '../../document/DocumentManager';
86
import { ServerComponents } from '../../server/ServerComponents';
@@ -259,14 +257,8 @@ export class GuardService implements SettingsConfigurable, Closeable {
259257
private getViolationRange(uri: string, violation: GuardViolation): Range {
260258
// If we have a CloudFormation path, try to resolve it to precise location
261259
if (violation.location.path && uri) {
262-
// Guard provides paths like "/Resources/User/Properties/Policies"
263-
// Remove leading slash if present and split by '/'
264-
const pathSegments = violation.location.path.startsWith('/')
265-
? violation.location.path.slice(1).split('/')
266-
: violation.location.path.split('/');
267-
268260
// Try to get just the key part of the key/value pair using syntax tree directly
269-
const keyRange = this.getKeyRangeFromPath(uri, pathSegments);
261+
const keyRange = this.diagnosticCoordinator.getKeyRangeFromPath(uri, violation.location.path);
270262
if (keyRange) {
271263
return keyRange;
272264
}
@@ -283,42 +275,6 @@ export class GuardService implements SettingsConfigurable, Closeable {
283275
};
284276
}
285277

286-
/**
287-
* Extract the key range from a path using syntax tree directly
288-
*/
289-
private getKeyRangeFromPath(uri: string, pathSegments: ReadonlyArray<string>): Range | undefined {
290-
const syntaxTree = this.syntaxTreeManager.getSyntaxTree(uri);
291-
if (!syntaxTree) {
292-
return undefined;
293-
}
294-
295-
// Get the node at the path
296-
const result = syntaxTree.getNodeByPath(pathSegments);
297-
if (!result.node) {
298-
return undefined;
299-
}
300-
301-
// Check if this is a pair node (key/value pair)
302-
if (NodeType.isPairNode(result.node, syntaxTree.type)) {
303-
// Get the key node from the pair
304-
const keyNode = result.node.childForFieldName(FieldNames.KEY);
305-
if (keyNode) {
306-
return {
307-
start: {
308-
line: keyNode.startPosition.row,
309-
character: keyNode.startPosition.column,
310-
},
311-
end: {
312-
line: keyNode.endPosition.row,
313-
character: keyNode.endPosition.column,
314-
},
315-
};
316-
}
317-
}
318-
319-
return undefined;
320-
}
321-
322278
/**
323279
* Validate a document with debouncing and proper trigger handling
324280
*

src/stacks/actions/StackActionOperations.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Change, ChangeSetType, StackStatus } from '@aws-sdk/client-cloudformation';
22
import { WaiterState } from '@smithy/util-waiter';
33
import { DateTime } from 'luxon';
4-
import Parser from 'tree-sitter';
54
import { v4 as uuidv4 } from 'uuid';
6-
import { ResponseError, ErrorCodes, Diagnostic, DiagnosticSeverity } from 'vscode-languageserver';
5+
import { ResponseError, ErrorCodes, Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver';
76
import { TopLevelSection } from '../../context/ContextType';
87
import { getEntityMap } from '../../context/SectionContextBuilder';
98
import { SyntaxTreeManager } from '../../context/syntaxtree/SyntaxTreeManager';
@@ -265,35 +264,31 @@ export async function publishValidationDiagnostics(
265264

266265
const diagnostics: Diagnostic[] = [];
267266
for (const event of events) {
268-
let startPosition: Parser.Point | undefined;
269-
let endPosition: Parser.Point | undefined;
267+
let range: Range | undefined;
270268

271269
if (event.ResourcePropertyPath) {
272270
logger.debug({ event }, 'Getting property-specific start and end positions');
273271

274-
// Parse ValidationPath like "/Resources/S3Bucket" or "/Resources/S3Bucket/Properties/BucketName"
275-
const pathSegments = event.ResourcePropertyPath.split('/').filter(Boolean);
276-
277-
const nodeByPath = syntaxTree.getNodeByPath(pathSegments);
278-
279-
startPosition = nodeByPath.node?.startPosition;
280-
endPosition = nodeByPath.node?.endPosition;
272+
range = diagnosticCoordinator.getKeyRangeFromPath(uri, event.ResourcePropertyPath);
281273
} else if (event.LogicalId) {
282274
// fall back to using LogicalId and underlining entire resource
283275
logger.debug({ event }, 'No ResourcePropertyPath found, falling back to using LogicalId');
284276
const resourcesMap = getEntityMap(syntaxTree, TopLevelSection.Resources);
285277

286-
startPosition = resourcesMap?.get(event.LogicalId)?.startPosition;
287-
endPosition = resourcesMap?.get(event.LogicalId)?.endPosition;
278+
const startPosition = resourcesMap?.get(event.LogicalId)?.startPosition;
279+
const endPosition = resourcesMap?.get(event.LogicalId)?.endPosition;
280+
if (startPosition && endPosition) {
281+
range = {
282+
start: pointToPosition(startPosition),
283+
end: pointToPosition(endPosition),
284+
};
285+
}
288286
}
289287

290-
if (startPosition && endPosition) {
288+
if (range) {
291289
diagnostics.push({
292290
severity: event.Severity === 'ERROR' ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning,
293-
range: {
294-
start: pointToPosition(startPosition),
295-
end: pointToPosition(endPosition),
296-
},
291+
range: range,
297292
message: event.Message,
298293
source: CFN_VALIDATION_SOURCE,
299294
data: uuidv4(),

tst/integration/DocumentHandlerCoordinator.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument';
55
import { DocumentUri } from 'vscode-languageserver-textdocument/lib/esm/main';
66
import { didCloseHandler } from '../../src/handlers/DocumentHandler';
77
import { DiagnosticCoordinator } from '../../src/services/DiagnosticCoordinator';
8-
import { createMockComponents } from '../utils/MockServerComponents';
8+
import { createMockComponents, createMockSyntaxTreeManager } from '../utils/MockServerComponents';
99

1010
function mockDocumentEvent(uri: DocumentUri, content: string): TextDocumentChangeEvent<TextDocument> {
1111
return {
@@ -28,7 +28,8 @@ describe('DocumentHandler Integration with DiagnosticCoordinator', () => {
2828
mockLspDiagnostics = {
2929
publishDiagnostics: vi.fn().mockResolvedValue(undefined),
3030
};
31-
realCoordinator = new DiagnosticCoordinator(mockLspDiagnostics);
31+
const mockSyntaxTreeManager = createMockSyntaxTreeManager();
32+
realCoordinator = new DiagnosticCoordinator(mockLspDiagnostics, mockSyntaxTreeManager);
3233

3334
// Create mock services with the real coordinator
3435
mockServices = createMockComponents({

0 commit comments

Comments
 (0)