Skip to content

Commit 1152369

Browse files
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
1 parent 617251f commit 1152369

File tree

2 files changed

+82
-8
lines changed

2 files changed

+82
-8
lines changed

src/services/findAllReferences.ts

+58-8
Original file line numberDiff line numberDiff line change
@@ -1480,6 +1480,25 @@ namespace ts.FindAllReferences {
14801480
state.addStringOrCommentReference(sourceFile.fileName, createTextSpan(position, search.text.length));
14811481
}
14821482

1483+
const callExpr = findAncestor(referenceLocation, isCallExpression);
1484+
if (callExpr && isStringLiteralLike(referenceLocation)) {
1485+
// In the case where the reference node is a string literal within a
1486+
// call expression, the current symbol we're searching for might be
1487+
// a reference to a property name for one of the parameters of the
1488+
// call's resolved signature.
1489+
const sig = state.checker.getResolvedSignature(callExpr);
1490+
if (sig) {
1491+
const params = sig.getParameters();
1492+
for (let i = 0; i < params.length; i++) {
1493+
const paramType = state.checker.getParameterType(sig, i);
1494+
const prop = paramType.getProperty(referenceLocation.text);
1495+
if (prop) {
1496+
const relatedSymbol = getRelatedSymbol(search, prop, referenceLocation, state);
1497+
if (relatedSymbol) addReference(referenceLocation, relatedSymbol, state);
1498+
}
1499+
}
1500+
}
1501+
}
14831502
return;
14841503
}
14851504

@@ -2004,20 +2023,51 @@ namespace ts.FindAllReferences {
20042023

20052024
function getReferencesForStringLiteral(node: StringLiteralLike, sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken): SymbolAndEntries[] {
20062025
const type = getContextualTypeFromParentOrAncestorTypeNode(node, checker);
2026+
const callExpr = findAncestor(node, isCallExpression);
20072027
const references = flatMap(sourceFiles, sourceFile => {
20082028
cancellationToken.throwIfCancellationRequested();
20092029
return mapDefined(getPossibleSymbolReferenceNodes(sourceFile, node.text), ref => {
2010-
if (isStringLiteralLike(ref) && ref.text === node.text) {
2011-
if (type) {
2012-
const refType = getContextualTypeFromParentOrAncestorTypeNode(ref, checker);
2013-
if (type !== checker.getStringType() && type === refType) {
2014-
return nodeEntry(ref, EntryKind.StringLiteral);
2015-
}
2016-
}
2017-
else {
2030+
// Special case: Every string literal refers at least to itself.
2031+
if (node === ref) return nodeEntry(ref, EntryKind.StringLiteral);
2032+
2033+
// When evaluating references for a string literal, guarantee that the string literal
2034+
// node is either global or that any reference shares the same scope. Global literals
2035+
// might all refer to each other, but if the string literal being considered is in a
2036+
// call expression, any reference should also be from the same call expression.
2037+
const refHasSameScope = !callExpr || callExpr === findAncestor(ref, isCallExpression);
2038+
const refHasSameText = isStringLiteralLike(ref) || isPropertyNameLiteral(ref) && getTextOfIdentifierOrLiteral(ref) === node.text;
2039+
2040+
if (type && isStringLiteralLike(ref) && refHasSameText) {
2041+
const refType = getContextualTypeFromParentOrAncestorTypeNode(ref, checker);
2042+
if ((refHasSameScope || !type.isStringLiteral() || type.isUnionOrIntersection()) && type === refType) {
2043+
// Reference globally or contextually matches the given string literal by referencing
2044+
// the same union or intersection type or sharing a similar scope.
20182045
return nodeEntry(ref, EntryKind.StringLiteral);
20192046
}
20202047
}
2048+
if (callExpr) {
2049+
if (refHasSameScope && isPropertyNameLiteral(ref) && refHasSameText) {
2050+
// Reference is the property name of an object literal argument.
2051+
return nodeEntry(ref, EntryKind.SearchedLocalFoundProperty);
2052+
}
2053+
2054+
const sig = checker.getResolvedSignature(callExpr);
2055+
if (sig) {
2056+
const params = sig.getParameters();
2057+
for (let i = 0; i < params.length; i++) {
2058+
const paramType = checker.getParameterType(sig, i);
2059+
if (isLiteralTypeNode(ref.parent) && paramType.isStringLiteral() && refHasSameText) {
2060+
// Reference is the definition of a string literal parameter.
2061+
return nodeEntry(ref, EntryKind.StringLiteral);
2062+
}
2063+
const prop = paramType.getProperty(node.text);
2064+
if (prop && prop === checker.getSymbolAtLocation(ref)) {
2065+
// Reference is a property name in one of the parameter types.
2066+
return nodeEntry(ref, EntryKind.SearchedLocalFoundProperty);
2067+
}
2068+
}
2069+
}
2070+
}
20212071
});
20222072
});
20232073

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface A {
4+
//// [|[|{| "contextRangeIndex": 0 |}prop|]: string;|]
5+
////}
6+
////
7+
////declare function f(a: A, k: keyof A): void;
8+
////declare let a: A;
9+
////f(a, "[|prop|]");
10+
////
11+
////declare const f2: <T>(a: T, b: keyof T) => void;
12+
////f2({
13+
//// [|[|{| "contextRangeIndex": 3 |}prop|]: () => {}|]
14+
////}, "[|prop|]");
15+
////
16+
////declare const f3: <K extends string>(a: K, b: { [_ in K]: unknown }) => void;
17+
////f3("[|prop|]", {
18+
//// [|[|{| "contextRangeIndex": 7 |}prop|]: () => {}|]
19+
////});
20+
21+
const [r0Def, r0, r1, r2Def, r2, r3, r4, r5Def, r5] = test.ranges();
22+
verify.renameLocations([r0, r1], [r0, r1]);
23+
verify.renameLocations([r2, r3], [r2, r3]);
24+
verify.renameLocations([r4, r5], [r4, r5]);

0 commit comments

Comments
 (0)