Skip to content

Commit b5cc450

Browse files
committed
deleteDeclaration: don't crash on the top node.
A misbehaved client can sometimes cause the server to reach `deleteDeclaration` with the SourceFile, and it will crash due to no `node.parent`. I couldn't find a good way to create a test for it, but I could trigger it manually by having a file with just a `,`, and sending an explicit `getCodeFixes` command to the server with `errorCodes: [6133]`. Do three things to improve this: 1. `textChanges.ts`: if we get here with the root node, delete it instead of failing. 2. `fixUnusedIdentifier.ts`: check that we don't `delete` a node that is the whole source file, so the error is more focused (should have more similar failure stacks). 3. `session.ts`: when there was any failure in `getCodeFixes`, check if the input had a diag code that does not appear in the requested text range, and throw an error saying that the failure is probably a result of a bad request. Closes #33726 (probably not fixing it, but making it easier to find the cause)
1 parent ccdd688 commit b5cc450

File tree

3 files changed

+41
-13
lines changed

3 files changed

+41
-13
lines changed

src/server/session.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,11 @@ namespace ts.server {
712712
typesMapLocation?: string;
713713
}
714714

715+
type IncludePositionDiagnostics<IncludePos extends boolean> =
716+
IncludePos extends true ? readonly protocol.DiagnosticWithLinePosition[]
717+
: IncludePos extends false ? readonly protocol.Diagnostic[]
718+
: readonly (protocol.DiagnosticWithLinePosition | protocol.Diagnostic)[];
719+
715720
export class Session<TMessage = string> implements EventSender {
716721
private readonly gcTimer: GcTimer;
717722
protected projectService: ProjectService;
@@ -1194,18 +1199,19 @@ namespace ts.server {
11941199
});
11951200
}
11961201

1197-
private getDiagnosticsWorker(
1198-
args: protocol.FileRequestArgs, isSemantic: boolean, selector: (project: Project, file: string) => readonly Diagnostic[], includeLinePosition: boolean
1199-
): readonly protocol.DiagnosticWithLinePosition[] | readonly protocol.Diagnostic[] {
1202+
private getDiagnosticsWorker<IncludePos extends boolean>(
1203+
args: protocol.FileRequestArgs, isSemantic: boolean, selector: (project: Project, file: string) => readonly Diagnostic[], includeLinePosition: IncludePos
1204+
): IncludePositionDiagnostics<IncludePos> {
12001205
const { project, file } = this.getFileAndProject(args);
12011206
if (isSemantic && isDeclarationFileInJSOnlyNonConfiguredProject(project, file)) {
1202-
return emptyArray;
1207+
return emptyArray as unknown as IncludePositionDiagnostics<IncludePos>;
12031208
}
12041209
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
12051210
const diagnostics = selector(project, file);
1206-
return includeLinePosition
1211+
return (includeLinePosition
12071212
? this.convertToDiagnosticsWithLinePosition(diagnostics, scriptInfo)
1208-
: diagnostics.map(d => formatDiag(file, project, d));
1213+
: diagnostics.map(d => formatDiag(file, project, d))
1214+
) as unknown as IncludePositionDiagnostics<IncludePos>;
12091215
}
12101216

12111217
private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): readonly protocol.FileSpanWithContext[] | readonly DefinitionInfo[] {
@@ -1367,7 +1373,7 @@ namespace ts.server {
13671373
emptyArray;
13681374
}
13691375

1370-
private getSyntacticDiagnosticsSync(args: protocol.SyntacticDiagnosticsSyncRequestArgs): readonly protocol.Diagnostic[] | readonly protocol.DiagnosticWithLinePosition[] {
1376+
private getSyntacticDiagnosticsSync(args: protocol.SyntacticDiagnosticsSyncRequestArgs) {
13711377
const { configFile } = this.getConfigFileAndProject(args);
13721378
if (configFile) {
13731379
// all the config file errors are reported as part of semantic check so nothing to report here
@@ -1377,15 +1383,15 @@ namespace ts.server {
13771383
return this.getDiagnosticsWorker(args, /*isSemantic*/ false, (project, file) => project.getLanguageService().getSyntacticDiagnostics(file), !!args.includeLinePosition);
13781384
}
13791385

1380-
private getSemanticDiagnosticsSync(args: protocol.SemanticDiagnosticsSyncRequestArgs): readonly protocol.Diagnostic[] | readonly protocol.DiagnosticWithLinePosition[] {
1386+
private getSemanticDiagnosticsSync(args: protocol.SemanticDiagnosticsSyncRequestArgs) {
13811387
const { configFile, project } = this.getConfigFileAndProject(args);
13821388
if (configFile) {
13831389
return this.getConfigFileDiagnostics(configFile, project!, !!args.includeLinePosition); // TODO: GH#18217
13841390
}
13851391
return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSemanticDiagnostics(file).filter(d => !!d.file), !!args.includeLinePosition);
13861392
}
13871393

1388-
private getSuggestionDiagnosticsSync(args: protocol.SuggestionDiagnosticsSyncRequestArgs): readonly protocol.Diagnostic[] | readonly protocol.DiagnosticWithLinePosition[] {
1394+
private getSuggestionDiagnosticsSync(args: protocol.SuggestionDiagnosticsSyncRequestArgs) {
13891395
const { configFile } = this.getConfigFileAndProject(args);
13901396
if (configFile) {
13911397
// Currently there are no info diagnostics for config files.
@@ -2198,7 +2204,23 @@ namespace ts.server {
21982204
const scriptInfo = project.getScriptInfoForNormalizedPath(file)!;
21992205
const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo);
22002206

2201-
const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, this.getFormatOptions(file), this.getPreferences(file));
2207+
let codeActions: readonly CodeFixAction[];
2208+
try {
2209+
codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, this.getFormatOptions(file), this.getPreferences(file));
2210+
}
2211+
catch(e) {
2212+
const existingDiagCodes = flatten((["getSyntacticDiagnostics", "getSemanticDiagnostics", "getSuggestionDiagnostics"] as const).map(getter =>
2213+
this.getDiagnosticsWorker(args, /*isSemantic*/ getter !== "getSyntacticDiagnostics",
2214+
(project, file) => project.getLanguageService()[getter](file),
2215+
/*includeLinePosition*/ true)
2216+
.filter(d => decodedTextSpanIntersectsWith(startPosition, endPosition - startPosition, d.start, d.length))
2217+
.map(d => d.code)));
2218+
const badCode = args.errorCodes.find(c => !existingDiagCodes.includes(c));
2219+
if (badCode !== undefined) {
2220+
e.message = `Bad error code: ${badCode} not found in range ${startPosition}..${endPosition} (found: ${existingDiagCodes.join(", ")}); could have caused this error:\n${e.message}`;
2221+
}
2222+
throw e;
2223+
}
22022224
return simplifiedResult ? codeActions.map(codeAction => this.mapCodeFixAction(codeAction)) : codeActions;
22032225
}
22042226

src/services/codefixes/fixUnusedIdentifier.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,10 @@ namespace ts.codefix {
237237
if (isParameter(parent)) {
238238
tryDeleteParameter(changes, sourceFile, parent, checker, sourceFiles, program, cancellationToken, isFixAll);
239239
}
240-
else if (!isFixAll || !(isIdentifier(token) && FindAllReferences.Core.isSymbolReferencedInFile(token, checker, sourceFile))) {
241-
changes.delete(sourceFile, isImportClause(parent) ? token : isComputedPropertyName(parent) ? parent.parent : parent);
240+
else if (!(isFixAll && isIdentifier(token) && FindAllReferences.Core.isSymbolReferencedInFile(token, checker, sourceFile))) {
241+
const node = isImportClause(parent) ? token : isComputedPropertyName(parent) ? parent.parent : parent;
242+
Debug.assert(node !== sourceFile, "should not delete whole source file");
243+
changes.delete(sourceFile, node);
242244
}
243245
}
244246

src/services/textChanges.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1361,7 +1361,11 @@ namespace ts.textChanges {
13611361
break;
13621362

13631363
default:
1364-
if (isImportClause(node.parent) && node.parent.name === node) {
1364+
if (!node.parent) {
1365+
// a misbehaving client can reach here with the SourceFile node
1366+
deleteNode(changes, sourceFile, node);
1367+
}
1368+
else if (isImportClause(node.parent) && node.parent.name === node) {
13651369
deleteDefaultImport(changes, sourceFile, node.parent);
13661370
}
13671371
else if (isCallExpression(node.parent) && contains(node.parent.arguments, node)) {

0 commit comments

Comments
 (0)