diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index 158b16d00c7f1..953fafe3c91a7 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -11,7 +11,7 @@ namespace ts.FindAllReferences { | { readonly type: DefinitionKind.Label; readonly node: Identifier } | { readonly type: DefinitionKind.Keyword; readonly node: Node } | { readonly type: DefinitionKind.This; readonly node: Node } - | { readonly type: DefinitionKind.String; readonly node: StringLiteral }; + | { readonly type: DefinitionKind.String; readonly node: StringLiteralLike }; export const enum EntryKind { Span, Node, StringLiteral, SearchedLocalFoundProperty, SearchedPropertyFoundLocal } export type NodeEntryKind = EntryKind.Node | EntryKind.StringLiteral | EntryKind.SearchedLocalFoundProperty | EntryKind.SearchedPropertyFoundLocal; @@ -621,7 +621,7 @@ namespace ts.FindAllReferences { // Could not find a symbol e.g. unknown identifier if (!symbol) { // String literal might be a property (and thus have a symbol), so do this here rather than in getReferencedSymbolsSpecial. - return !options.implementations && isStringLiteral(node) ? getReferencesForStringLiteral(node, sourceFiles, cancellationToken) : undefined; + return !options.implementations && isStringLiteralLike(node) ? getReferencesForStringLiteral(node, sourceFiles, checker, cancellationToken) : undefined; } if (symbol.escapedName === InternalSymbolName.ExportEquals) { @@ -1889,11 +1889,23 @@ namespace ts.FindAllReferences { }]; } - function getReferencesForStringLiteral(node: StringLiteral, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken): SymbolAndEntries[] { + function getReferencesForStringLiteral(node: StringLiteralLike, sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken): SymbolAndEntries[] { + const type = getContextualTypeOrAncestorTypeNodeType(node, checker); const references = flatMap(sourceFiles, sourceFile => { cancellationToken.throwIfCancellationRequested(); - return mapDefined(getPossibleSymbolReferenceNodes(sourceFile, node.text), ref => - isStringLiteral(ref) && ref.text === node.text ? nodeEntry(ref, EntryKind.StringLiteral) : undefined); + return mapDefined(getPossibleSymbolReferenceNodes(sourceFile, node.text), ref => { + if (isStringLiteralLike(ref) && ref.text === node.text) { + if (type) { + const refType = getContextualTypeOrAncestorTypeNodeType(ref, checker); + if (type !== checker.getStringType() && type === refType) { + return nodeEntry(ref, EntryKind.StringLiteral); + } + } + else { + return nodeEntry(ref, EntryKind.StringLiteral); + } + } + }); }); return [{ diff --git a/src/services/rename.ts b/src/services/rename.ts index c3392a0ee6471..59efe2f7a209c 100644 --- a/src/services/rename.ts +++ b/src/services/rename.ts @@ -14,7 +14,15 @@ namespace ts.Rename { function getRenameInfoForNode(node: Node, typeChecker: TypeChecker, sourceFile: SourceFile, isDefinedInLibraryFile: (declaration: Node) => boolean, options?: RenameInfoOptions): RenameInfo | undefined { const symbol = typeChecker.getSymbolAtLocation(node); if (!symbol) { - if (isLabelName(node)) { + if (isStringLiteralLike(node)) { + const type = getContextualTypeOrAncestorTypeNodeType(node, typeChecker); + if (type && ((type.flags & TypeFlags.StringLiteral) || ( + (type.flags & TypeFlags.Union) && every((type as UnionType).types, type => !!(type.flags & TypeFlags.StringLiteral)) + ))) { + return getRenameInfoSuccess(node.text, node.text, ScriptElementKind.string, "", node, sourceFile); + } + } + else if (isLabelName(node)) { const name = getTextOfNode(node); return getRenameInfoSuccess(name, name, ScriptElementKind.label, ScriptElementKindModifier.none, node, sourceFile); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index d63e21d476f79..bececaaa4f49c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -774,6 +774,27 @@ namespace ts { } } + function getAncestorTypeNode(node: Node) { + let lastTypeNode: TypeNode | undefined; + findAncestor(node, a => { + if (isTypeNode(a)) { + lastTypeNode = a; + } + return !isQualifiedName(a.parent) && !isTypeNode(a.parent) && !isTypeElement(a.parent); + }); + return lastTypeNode; + } + + export function getContextualTypeOrAncestorTypeNodeType(node: Expression, checker: TypeChecker) { + const contextualType = checker.getContextualType(node); + if (contextualType) { + return contextualType; + } + + const ancestorTypeNode = getAncestorTypeNode(node); + return ancestorTypeNode && checker.getTypeAtLocation(ancestorTypeNode); + } + function getAdjustedLocationForDeclaration(node: Node, forRename: boolean) { if (!forRename) { switch (node.kind) { diff --git a/tests/cases/fourslash/renameStringLiteralOk.ts b/tests/cases/fourslash/renameStringLiteralOk.ts new file mode 100644 index 0000000000000..7b8bdc5a18f15 --- /dev/null +++ b/tests/cases/fourslash/renameStringLiteralOk.ts @@ -0,0 +1,11 @@ +/// + +//// interface Foo { +//// f: '[|foo|]' | 'bar' +//// } +//// const d: 'foo' = 'foo' +//// declare const f: Foo +//// f.f = '[|foo|]' +//// f.f = `[|foo|]` + +verify.rangesWithSameTextAreRenameLocations("foo"); diff --git a/tests/cases/fourslash/renameStringLiteralOk1.ts b/tests/cases/fourslash/renameStringLiteralOk1.ts new file mode 100644 index 0000000000000..a6b0047d72d1e --- /dev/null +++ b/tests/cases/fourslash/renameStringLiteralOk1.ts @@ -0,0 +1,18 @@ +/// + +//// declare function f(): '[|foo|]' | 'bar' +//// class Foo { +//// f = f() +//// } +//// const d: 'foo' = 'foo' +//// declare const ff: Foo +//// ff.f = '[|foo|]' + +verify.rangesWithSameTextAreRenameLocations("foo"); + +interface Foo { + f: 'foo' | 'bar' +} +const d: 'foo' = 'foo' +declare const f: Foo +f.f = 'foo' \ No newline at end of file diff --git a/tests/cases/fourslash/renameStringLiteralTypes.ts b/tests/cases/fourslash/renameStringLiteralTypes.ts index ef2b7bb76adc9..294d0fc260893 100644 --- a/tests/cases/fourslash/renameStringLiteralTypes.ts +++ b/tests/cases/fourslash/renameStringLiteralTypes.ts @@ -11,4 +11,4 @@ //// ////animate({ deltaX: 100, deltaY: 100, easing: "[|ease-in-out|]" }); -goTo.eachRange(() => { verify.renameInfoFailed(); }); +verify.rangesWithSameTextAreRenameLocations("ease-in-out");