From 1152369d17ab1a5967909a3cad95b99d66c0f5ad Mon Sep 17 00:00:00 2001 From: Chris Manghane Date: Wed, 8 Sep 2021 19:38:24 -0600 Subject: [PATCH] feat(45549): track string literal references within call expressions * Enables string literals in call expression to refer to property names. * Enables property names in call expression to refer to string literals. * Changes string literal rename/reference behavior when string literals are in a call expression. Previously, all string references with matching text would reference one another globally and allow renaming even if such an operation would result in a compiler error. Fixes #45549 --- src/services/findAllReferences.ts | 66 ++++++++++++++++--- .../fourslash/renameStringPropertyNames1.ts | 24 +++++++ 2 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 tests/cases/fourslash/renameStringPropertyNames1.ts diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index ded586904aa4b..80806759da2d2 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -1480,6 +1480,25 @@ namespace ts.FindAllReferences { state.addStringOrCommentReference(sourceFile.fileName, createTextSpan(position, search.text.length)); } + const callExpr = findAncestor(referenceLocation, isCallExpression); + if (callExpr && isStringLiteralLike(referenceLocation)) { + // In the case where the reference node is a string literal within a + // call expression, the current symbol we're searching for might be + // a reference to a property name for one of the parameters of the + // call's resolved signature. + const sig = state.checker.getResolvedSignature(callExpr); + if (sig) { + const params = sig.getParameters(); + for (let i = 0; i < params.length; i++) { + const paramType = state.checker.getParameterType(sig, i); + const prop = paramType.getProperty(referenceLocation.text); + if (prop) { + const relatedSymbol = getRelatedSymbol(search, prop, referenceLocation, state); + if (relatedSymbol) addReference(referenceLocation, relatedSymbol, state); + } + } + } + } return; } @@ -2004,20 +2023,51 @@ namespace ts.FindAllReferences { function getReferencesForStringLiteral(node: StringLiteralLike, sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken): SymbolAndEntries[] { const type = getContextualTypeFromParentOrAncestorTypeNode(node, checker); + const callExpr = findAncestor(node, isCallExpression); const references = flatMap(sourceFiles, sourceFile => { cancellationToken.throwIfCancellationRequested(); return mapDefined(getPossibleSymbolReferenceNodes(sourceFile, node.text), ref => { - if (isStringLiteralLike(ref) && ref.text === node.text) { - if (type) { - const refType = getContextualTypeFromParentOrAncestorTypeNode(ref, checker); - if (type !== checker.getStringType() && type === refType) { - return nodeEntry(ref, EntryKind.StringLiteral); - } - } - else { + // Special case: Every string literal refers at least to itself. + if (node === ref) return nodeEntry(ref, EntryKind.StringLiteral); + + // When evaluating references for a string literal, guarantee that the string literal + // node is either global or that any reference shares the same scope. Global literals + // might all refer to each other, but if the string literal being considered is in a + // call expression, any reference should also be from the same call expression. + const refHasSameScope = !callExpr || callExpr === findAncestor(ref, isCallExpression); + const refHasSameText = isStringLiteralLike(ref) || isPropertyNameLiteral(ref) && getTextOfIdentifierOrLiteral(ref) === node.text; + + if (type && isStringLiteralLike(ref) && refHasSameText) { + const refType = getContextualTypeFromParentOrAncestorTypeNode(ref, checker); + if ((refHasSameScope || !type.isStringLiteral() || type.isUnionOrIntersection()) && type === refType) { + // Reference globally or contextually matches the given string literal by referencing + // the same union or intersection type or sharing a similar scope. return nodeEntry(ref, EntryKind.StringLiteral); } } + if (callExpr) { + if (refHasSameScope && isPropertyNameLiteral(ref) && refHasSameText) { + // Reference is the property name of an object literal argument. + return nodeEntry(ref, EntryKind.SearchedLocalFoundProperty); + } + + const sig = checker.getResolvedSignature(callExpr); + if (sig) { + const params = sig.getParameters(); + for (let i = 0; i < params.length; i++) { + const paramType = checker.getParameterType(sig, i); + if (isLiteralTypeNode(ref.parent) && paramType.isStringLiteral() && refHasSameText) { + // Reference is the definition of a string literal parameter. + return nodeEntry(ref, EntryKind.StringLiteral); + } + const prop = paramType.getProperty(node.text); + if (prop && prop === checker.getSymbolAtLocation(ref)) { + // Reference is a property name in one of the parameter types. + return nodeEntry(ref, EntryKind.SearchedLocalFoundProperty); + } + } + } + } }); }); diff --git a/tests/cases/fourslash/renameStringPropertyNames1.ts b/tests/cases/fourslash/renameStringPropertyNames1.ts new file mode 100644 index 0000000000000..85e8cc2bee121 --- /dev/null +++ b/tests/cases/fourslash/renameStringPropertyNames1.ts @@ -0,0 +1,24 @@ +/// + +////interface A { +//// [|[|{| "contextRangeIndex": 0 |}prop|]: string;|] +////} +//// +////declare function f(a: A, k: keyof A): void; +////declare let a: A; +////f(a, "[|prop|]"); +//// +////declare const f2: (a: T, b: keyof T) => void; +////f2({ +//// [|[|{| "contextRangeIndex": 3 |}prop|]: () => {}|] +////}, "[|prop|]"); +//// +////declare const f3: (a: K, b: { [_ in K]: unknown }) => void; +////f3("[|prop|]", { +//// [|[|{| "contextRangeIndex": 7 |}prop|]: () => {}|] +////}); + +const [r0Def, r0, r1, r2Def, r2, r3, r4, r5Def, r5] = test.ranges(); +verify.renameLocations([r0, r1], [r0, r1]); +verify.renameLocations([r2, r3], [r2, r3]); +verify.renameLocations([r4, r5], [r4, r5]);