diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 966e6f4e4c7..b6a40258f69 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -570,9 +570,12 @@ namespace ts { isArrayLikeType, isTypeInvalidDueToUnionDiscriminant, getAllPossiblePropertiesOfTypes, - getSuggestionForNonexistentProperty: (node, type) => getSuggestionForNonexistentProperty(node, type), + getSuggestedSymbolForNonexistentProperty, + getSuggestionForNonexistentProperty, + getSuggestedSymbolForNonexistentSymbol: (location, name, meaning) => getSuggestedSymbolForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), - getSuggestionForNonexistentExport: (node, target) => getSuggestionForNonexistentExport(node, target), + getSuggestedSymbolForNonexistentModule, + getSuggestionForNonexistentExport, getBaseConstraintOfType, getDefaultFromTypeParameter: type => type && type.flags & TypeFlags.TypeParameter ? getDefaultFromTypeParameter(type as TypeParameter) : undefined, resolveName(name, location, meaning, excludeGlobals) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6102205a4d0..1009e75fa89 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3484,8 +3484,11 @@ namespace ts { */ /* @internal */ tryGetMemberInModuleExportsAndProperties(memberName: string, moduleSymbol: Symbol): Symbol | undefined; getApparentType(type: Type): Type; + /* @internal */ getSuggestedSymbolForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined; /* @internal */ getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): string | undefined; + /* @internal */ getSuggestedSymbolForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): Symbol | undefined; /* @internal */ getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string | undefined; + /* @internal */ getSuggestedSymbolForNonexistentModule(node: Identifier, target: Symbol): Symbol | undefined; /* @internal */ getSuggestionForNonexistentExport(node: Identifier, target: Symbol): string | undefined; getBaseConstraintOfType(type: Type): Type | undefined; getDefaultFromTypeParameter(type: Type): Type | undefined; diff --git a/src/services/codefixes/fixSpelling.ts b/src/services/codefixes/fixSpelling.ts index 725237dd99e..037f00462c0 100644 --- a/src/services/codefixes/fixSpelling.ts +++ b/src/services/codefixes/fixSpelling.ts @@ -14,27 +14,27 @@ namespace ts.codefix { const { sourceFile } = context; const info = getInfo(sourceFile, context.span.start, context); if (!info) return undefined; - const { node, suggestion } = info; + const { node, suggestedSymbol } = info; const { target } = context.host.getCompilationSettings(); - const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, node, suggestion, target!)); - return [createCodeFixAction("spelling", changes, [Diagnostics.Change_spelling_to_0, suggestion], fixId, Diagnostics.Fix_all_detected_spelling_errors)]; + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, node, suggestedSymbol, target!)); + return [createCodeFixAction("spelling", changes, [Diagnostics.Change_spelling_to_0, symbolName(suggestedSymbol)], fixId, Diagnostics.Fix_all_detected_spelling_errors)]; }, fixIds: [fixId], getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => { const info = getInfo(diag.file, diag.start, context); const { target } = context.host.getCompilationSettings(); - if (info) doChange(changes, context.sourceFile, info.node, info.suggestion, target!); + if (info) doChange(changes, context.sourceFile, info.node, info.suggestedSymbol, target!); }), }); - function getInfo(sourceFile: SourceFile, pos: number, context: CodeFixContextBase): { node: Node, suggestion: string } | undefined { + function getInfo(sourceFile: SourceFile, pos: number, context: CodeFixContextBase): { node: Node, suggestedSymbol: Symbol } | undefined { // This is the identifier of the misspelled word. eg: // this.speling = 1; // ^^^^^^^ const node = getTokenAtPosition(sourceFile, pos); const checker = context.program.getTypeChecker(); - let suggestion: string | undefined; + let suggestedSymbol: Symbol | undefined; if (isPropertyAccessExpression(node.parent) && node.parent.name === node) { Debug.assert(isIdentifierOrPrivateIdentifier(node), "Expected an identifier for spelling (property access)"); let containingType = checker.getTypeAtLocation(node.parent.expression); @@ -42,29 +42,36 @@ namespace ts.codefix { containingType = checker.getNonNullableType(containingType); } const name = node as Identifier | PrivateIdentifier; - suggestion = checker.getSuggestionForNonexistentProperty(name, containingType); + suggestedSymbol = checker.getSuggestedSymbolForNonexistentProperty(name, containingType); } else if (isImportSpecifier(node.parent) && node.parent.name === node) { Debug.assert(node.kind === SyntaxKind.Identifier, "Expected an identifier for spelling (import)"); const importDeclaration = findAncestor(node, isImportDeclaration)!; const resolvedSourceFile = getResolvedSourceFileFromImportDeclaration(sourceFile, context, importDeclaration); if (resolvedSourceFile && resolvedSourceFile.symbol) { - suggestion = checker.getSuggestionForNonexistentExport(node as Identifier, resolvedSourceFile.symbol); + suggestedSymbol = checker.getSuggestedSymbolForNonexistentModule(node as Identifier, resolvedSourceFile.symbol); } } else { const meaning = getMeaningFromLocation(node); const name = getTextOfNode(node); Debug.assert(name !== undefined, "name should be defined"); - suggestion = checker.getSuggestionForNonexistentSymbol(node, name, convertSemanticMeaningToSymbolFlags(meaning)); + suggestedSymbol = checker.getSuggestedSymbolForNonexistentSymbol(node, name, convertSemanticMeaningToSymbolFlags(meaning)); } - return suggestion === undefined ? undefined : { node, suggestion }; + return suggestedSymbol === undefined ? undefined : { node, suggestedSymbol }; } - function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: Node, suggestion: string, target: ScriptTarget) { + function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: Node, suggestedSymbol: Symbol, target: ScriptTarget) { + const suggestion = symbolName(suggestedSymbol); if (!isIdentifierText(suggestion, target) && isPropertyAccessExpression(node.parent)) { - changes.replaceNode(sourceFile, node.parent, createElementAccess(node.parent.expression, createLiteral(suggestion))); + const valDecl = suggestedSymbol.valueDeclaration; + if (isNamedDeclaration(valDecl) && isPrivateIdentifier(valDecl.name)) { + changes.replaceNode(sourceFile, node, createIdentifier(suggestion)); + } + else { + changes.replaceNode(sourceFile, node.parent, createElementAccess(node.parent.expression, createLiteral(suggestion))); + } } else { changes.replaceNode(sourceFile, node, createIdentifier(suggestion)); diff --git a/tests/cases/fourslash/codeFixSpellingPrivatePropertyName.ts b/tests/cases/fourslash/codeFixSpellingPrivatePropertyName.ts new file mode 100644 index 00000000000..7952f45ab5f --- /dev/null +++ b/tests/cases/fourslash/codeFixSpellingPrivatePropertyName.ts @@ -0,0 +1,21 @@ +/// + +////class A { +//// #foo: number; +//// constructor() { +//// [|this.foo = 1;|] +//// } +////} + +verify.codeFixAvailable([ + { description: "Change spelling to '#foo'" }, + { description: "Declare property 'foo'" }, + { description: "Add index signature for property 'foo'" }, + { description: "Remove declaration for: '#foo'" }, +]); + +verify.codeFix({ + index: 0, + description: "Change spelling to '#foo'", + newRangeContent: "this.#foo = 1;" +}); diff --git a/tests/cases/fourslash/codeFixSpellingPropertyNameStartsWithHash.ts b/tests/cases/fourslash/codeFixSpellingPropertyNameStartsWithHash.ts new file mode 100644 index 00000000000..0bc7eff06cd --- /dev/null +++ b/tests/cases/fourslash/codeFixSpellingPropertyNameStartsWithHash.ts @@ -0,0 +1,20 @@ +/// + +////class A { +//// "#foo": number = 100; +//// constructor() { +//// [|this.foo = 1;|] +//// } +////} + +verify.codeFixAvailable([ + { description: "Change spelling to '#foo'" }, + { description: "Declare property 'foo'" }, + { description: "Add index signature for property 'foo'" } +]); + +verify.codeFix({ + index: 0, + description: "Change spelling to '#foo'", + newRangeContent: 'this["#foo"] = 1;' +});