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)
This commit is contained in:
Eli Barzilay 2021-02-25 16:57:48 -05:00
parent fb60c9f46e
commit 6ce82ab02e
3 changed files with 31 additions and 7 deletions

View File

@ -1393,7 +1393,7 @@ namespace ts.server {
emptyArray;
}
private getSyntacticDiagnosticsSync(args: protocol.SyntacticDiagnosticsSyncRequestArgs): readonly protocol.Diagnostic[] | readonly protocol.DiagnosticWithLinePosition[] {
private getSyntacticDiagnosticsSync(args: protocol.SyntacticDiagnosticsSyncRequestArgs) {
const { configFile } = this.getConfigFileAndProject(args);
if (configFile) {
// all the config file errors are reported as part of semantic check so nothing to report here
@ -1403,7 +1403,7 @@ namespace ts.server {
return this.getDiagnosticsWorker(args, /*isSemantic*/ false, (project, file) => project.getLanguageService().getSyntacticDiagnostics(file), !!args.includeLinePosition);
}
private getSemanticDiagnosticsSync(args: protocol.SemanticDiagnosticsSyncRequestArgs): readonly protocol.Diagnostic[] | readonly protocol.DiagnosticWithLinePosition[] {
private getSemanticDiagnosticsSync(args: protocol.SemanticDiagnosticsSyncRequestArgs) {
const { configFile, project } = this.getConfigFileAndProject(args);
if (configFile) {
return this.getConfigFileDiagnostics(configFile, project!, !!args.includeLinePosition); // TODO: GH#18217
@ -1411,7 +1411,7 @@ namespace ts.server {
return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSemanticDiagnostics(file).filter(d => !!d.file), !!args.includeLinePosition);
}
private getSuggestionDiagnosticsSync(args: protocol.SuggestionDiagnosticsSyncRequestArgs): readonly protocol.Diagnostic[] | readonly protocol.DiagnosticWithLinePosition[] {
private getSuggestionDiagnosticsSync(args: protocol.SuggestionDiagnosticsSyncRequestArgs) {
const { configFile } = this.getConfigFileAndProject(args);
if (configFile) {
// Currently there are no info diagnostics for config files.
@ -2233,7 +2233,25 @@ namespace ts.server {
const scriptInfo = project.getScriptInfoForNormalizedPath(file)!;
const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo);
const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, this.getFormatOptions(file), this.getPreferences(file));
let codeActions: readonly CodeFixAction[];
try {
codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, this.getFormatOptions(file), this.getPreferences(file));
}
catch(e) {
const ls = project.getLanguageService();
const existingDiagCodes = [
...ls.getSyntacticDiagnostics(file),
...ls.getSemanticDiagnostics(file),
...ls.getSuggestionDiagnostics(file)
].map(d =>
decodedTextSpanIntersectsWith(startPosition, endPosition - startPosition, d.start!, d.length!)
&& d.code);
const badCode = args.errorCodes.find(c => !existingDiagCodes.includes(c));
if (badCode !== undefined) {
e.message = `BADCLIENT: Bad error code, ${badCode} not found in range ${startPosition}..${endPosition} (found: ${existingDiagCodes.join(", ")}); could have caused this error:\n${e.message}`;
}
throw e;
}
return simplifiedResult ? codeActions.map(codeAction => this.mapCodeFixAction(codeAction)) : codeActions;
}

View File

@ -237,8 +237,10 @@ namespace ts.codefix {
if (isParameter(parent)) {
tryDeleteParameter(changes, sourceFile, parent, checker, sourceFiles, program, cancellationToken, isFixAll);
}
else if (!isFixAll || !(isIdentifier(token) && FindAllReferences.Core.isSymbolReferencedInFile(token, checker, sourceFile))) {
changes.delete(sourceFile, isImportClause(parent) ? token : isComputedPropertyName(parent) ? parent.parent : parent);
else if (!(isFixAll && isIdentifier(token) && FindAllReferences.Core.isSymbolReferencedInFile(token, checker, sourceFile))) {
const node = isImportClause(parent) ? token : isComputedPropertyName(parent) ? parent.parent : parent;
Debug.assert(node !== sourceFile, "should not delete whole source file");
changes.delete(sourceFile, node);
}
}

View File

@ -1363,7 +1363,11 @@ namespace ts.textChanges {
break;
default:
if (isImportClause(node.parent) && node.parent.name === node) {
if (!node.parent) {
// a misbehaving client can reach here with the SourceFile node
deleteNode(changes, sourceFile, node);
}
else if (isImportClause(node.parent) && node.parent.name === node) {
deleteDefaultImport(changes, sourceFile, node.parent);
}
else if (isCallExpression(node.parent) && contains(node.parent.arguments, node)) {