Skip to content

Commit e6659a5

Browse files
zgliczclaude
andauthored
JS-1336 Fix infinite loop in getFullyQualifiedNameTS when import is shadowed (#6402)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3435d42 commit e6659a5

3 files changed

Lines changed: 28 additions & 3 deletions

File tree

packages/jsts/src/rules/S2077/unit.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,13 @@ describe('S2077', () => {
257257
it('S2077 with type information', () => {
258258
const ruleTester = new RuleTester();
259259
ruleTester.run('Formatting SQL queries is security-sensitive [TS]', rule, {
260-
valid: [],
260+
valid: [
261+
// No infinite loop when local variable shadows imported name
262+
{
263+
code: `import { geolocation as geo } from "@vercel/functions";
264+
const geo = geo(request);`,
265+
},
266+
],
261267
invalid: [
262268
// mysql: ESM namespace import with concatenation
263269
{

packages/jsts/src/rules/helpers/ancestor.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import type { TSESTree } from '@typescript-eslint/utils';
1818
import { Rule, SourceCode } from 'eslint';
1919
import type { Node } from 'estree';
20+
import ts from 'typescript';
2021
import { functionLike } from './ast.js';
2122

2223
export function findFirstMatchingLocalAncestor(
@@ -99,3 +100,14 @@ export function childrenOf(node: Node, visitorKeys: SourceCode.VisitorKeys): Nod
99100
}
100101
return children.filter(Boolean);
101102
}
103+
104+
export function isTsAncestor(candidate: ts.Node, node: ts.Node): boolean {
105+
let current: ts.Node | undefined = node.parent;
106+
while (current) {
107+
if (current === candidate) {
108+
return true;
109+
}
110+
current = current.parent;
111+
}
112+
return false;
113+
}

packages/jsts/src/rules/helpers/module-ts.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
import ts from 'typescript';
1818
import type { ParserServicesWithTypeInformation } from '@typescript-eslint/utils';
19+
import { isTsAncestor } from './ancestor.js';
1920
import { removeNodePrefixIfExists } from './module.js';
2021

2122
export function getFullyQualifiedNameTS(
@@ -104,8 +105,14 @@ export function getFullyQualifiedNameTS(
104105
case ts.SyntaxKind.Identifier: {
105106
const identifierSymbol = services.program.getTypeChecker().getSymbolAtLocation(node);
106107
const declaration = identifierSymbol?.declarations?.at(0);
107-
// Handle: no symbol info, compiler module, or self-referential declaration (e.g., `module` in CommonJS)
108-
if (isCompilerModule(identifierSymbol) || !declaration || declaration === node) {
108+
// Handle: no symbol info, compiler module, self-referential declaration (e.g., `module` in CommonJS),
109+
// or declaration that contains the root node (e.g., `const geo = geo(request)` where import is shadowed)
110+
if (
111+
isCompilerModule(identifierSymbol) ||
112+
!declaration ||
113+
declaration === node ||
114+
isTsAncestor(declaration, rootNode)
115+
) {
109116
result.push((node as ts.Identifier).text);
110117
return returnResult();
111118
} else {

0 commit comments

Comments
 (0)