fix(ts): a call to a parameter of any type links nothing elsewhere (#2732)

A JS/TS name the calling function binds as a parameter is that parameter,
never a same-named function in another file, as the file's own were made
in #2226. The parameter reader took a type or default to be anything up to
a comma or a parenthesis, so a parameter typed as a function, defaulted to
one, or after one was never seen: bitwarden's isAvailable(f) on a callback
typed (f: string) => boolean linked to the napi isAvailable, and vscode's
merge editor's compare(a, b) on a comparator parameter to strings.ts's.

A type or default may now hold balanced parentheses, braces, brackets and
generics, a parameter property's modifiers are read, and an earlier
destructured parameter is passed over; each alternative still has one
parse. A name ends where no identifier character follows, so a const
syncState$ no longer declares syncState.

The parameter also shadows the file's own import of the name, which the
import resolver answered before the check ran, but only where it is in
scope: in the body of the function, arrow or catch clause that binds it,
or the block or for statement of a const, let, var, function or class,
read from the function's text up to the reference. A lone arrow
parameter counts, and an object type a function returns is no body. A
Promise executor's resolve is the calls inside it, never a resolve(file)
after it, and a const X = require('./x').default is the import itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 07:08:54 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 5994edb34e
commit 73f68e188c
4 changed files with 444 additions and 30 deletions
+2
View File
@@ -259,6 +259,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In TypeScript and JavaScript, a type, class, constant or other declaration that a `.d.ts` file declares inside `declare module '…'` now links only from files that use that module: files that import it, the module's other declarations, and files that import it through a file of yours that re-exports it. Before, any file using the same name linked to it: Node's `import { readFileSync } from 'fs'` linked to the `fs` that VS Code's `vscode.d.ts` declares, and a `ProcessEnv`, or an LSP `TextDocument` imported through a barrel, to the `vscode` module's types of those names. Re-index TypeScript projects after upgrading.
- In C++, a call written through a class the index doesn't hold, like protocolbuffers/protobuf's `FeatureSet::descriptor()` or `OneofOptions::default_instance()` (both classes are declared in `descriptor.pb.h`, a file too large to index), no longer links to another class's method of that name whose class only shares a word with it, like `JavaFeatures_NestInFileClassFeature::descriptor` or `CppFileOptions::default_instance`. A class named like the call's would have been found first, so one that only shares a word is another class. A call through a template parameter, through an alias, including one the calling function or a macro it expands declares, or through a namespace the file opens, aliases or uses links as before, and so does a call of a method only one class in the project declares. Re-index C++ projects after upgrading.
- In C++, a call written through a namespace, like protocolbuffers/protobuf's `absl::StripPrefix(…)` or fmtlib/fmt's `fmt::ptr(p)`, no longer links to a class's method that only shares the function's name, like protobuf's `MultiCasePrefixStripper::StripPrefix` or a test scanner's `ptr()`: a namespace holds functions and types, not methods. A call through a namespace a macro opens now links to what that namespace declares, so fmt's `fmt::ptr(p)` reaches fmt's own `ptr` function and `fmt::day(…)` its `day` type, while a call into a library outside your project, like abseil, links to nothing. A call through a lowercase class, or an alias or typedef of one, links as before. Re-index C++ projects after upgrading.
- In TypeScript and JavaScript, a call to a function's own parameter, like `isAvailable(f)` on a parameter typed `(f: string) => boolean` or `compare(a, b)` on a comparator parameter, no longer links to a function of the same name in another file, or to the file's own import of that name. Before, a parameter whose type or default value contained parentheses, as a function type does, wasn't recognized as one. A parameter or local stands in for an import of its name only where it is in scope, so a Promise executor's `resolve(…)` no longer links to an imported `path.resolve`, while a `resolve(file)` after the executor still does. Re-index TypeScript and JavaScript projects after upgrading.
- In TypeScript and JavaScript, a name ending in `$`, like `syncState$`, is no longer read as a declaration of `syncState`, so a call to an imported `syncState(…)` in the same function keeps its link, and a file's own `function $()` or `const $ = …` now counts as declaring `$`.
## [1.6.2] - 2026-10-03
+225
View File
@@ -79,3 +79,228 @@ describe('JS names the calling function binds', () => {
expect(targetsFrom('runInContext::run::walk')).toContain('runInContext::run::walk');
});
});
/**
* The same holds for a TypeScript parameter whose type or default holds
* parentheses, generics or braces, and across files: bitwarden's
* `isAvailable(f)` on a callback parameter typed `(f: string) => boolean`
* reached the napi `isAvailable` another file exports, and vscode's merge
* editor's `compare(a, b)` on a comparator parameter a `strings.ts`
* `compare`. A parameter shadows the file's own import of the name too, but
* only where it is in scope: inside its function's or arrow's body, never
* after it. Neither does `const syncState$` declare `syncState`, nor `const
* X = require('./x').default` hide the import it is.
*/
describe('TS parameters of any type are the calling function’s, in any file', () => {
let tsRoot = '';
let ts: CodeGraph;
beforeAll(async () => {
tsRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-js-local-ts-'));
const files: Record<string, string> = {
'src/sdk.ts': 'export function isAvailable(format: string): boolean { return format.length > 0; }\n',
'src/strings.ts': 'export function compare(a: number, b: number): number { return a - b; }\n',
'src/use.ts': `import { isAvailable } from './sdk';
export function check(format: string): boolean {
return isAvailable(format);
}
export function pick(formats: string[], isAvailable: (f: string) => boolean): string[] {
return formats.filter((f) => isAvailable(f));
}
export function pickDefault(formats: string[], isAvailable = (f: string) => f.length > 0): string[] {
return formats.filter((f) => isAvailable(f));
}
`,
'src/sort.ts': `export const compareAll = (items: number[], compare: (a: number, b: number) => number) =>
items.sort((a, b) => compare(a, b));
export function later(cb: () => void, options: Map<string, number>, compare: unknown): void {
cb();
(compare as (a: number, b: number) => number)(options.size, 0);
}
export class Sorter {
constructor(private readonly compare: (a: number, b: number) => number) {
compare(1, 2);
}
}
`,
'src/other.ts': `export function sortAll(items: number[]): number[] {
return items.sort((a, b) => compare(a, b));
}
`,
'src/state.ts': 'export function syncState(obs: object): object { return obs; }\n',
'src/sync.ts': `import { syncState } from './state';
export function syncObservable(obs$: object): object {
const syncState$ = syncState(obs$);
return syncState$;
}
export function syncLater(items: object[]): object {
items.forEach((syncState) => console.log(syncState));
return syncState(items);
}
export function syncInside(items: object[]): void {
items.forEach((syncState: (o: object) => object) => {
syncState(items);
});
}
`,
'src/paths.ts': 'export function resolve(...parts: string[]): string { return parts.join("/"); }\n',
// No semicolons after the arrow in fmtAll, as a project formatted without them writes it.
'src/run.ts': `import { resolve } from './paths';
export function run(file: string): Promise<void> {
return new Promise((resolve) => {
resolve(file);
});
}
export function runThen(file: string): string {
new Promise<void>((resolve) => setTimeout(resolve, 0));
return resolve(file, 'out');
}
export function fmtAll(items: string[]): string {
const fmt = (resolve: string) => resolve.trim()
return resolve(fmt(items[0]))
}
export function eachInLoop(files: string[]): void {
for (const resolve of [String]) resolve(files[0]);
}
export function eachAfterLoop(files: string[]): string {
for (const resolve of [String]) String(resolve);
return resolve(files[1]);
}
export function bare(): Promise<void> {
return new Promise<void>(resolve => {
resolve();
});
}
export function bareThen(file: string): string {
new Promise<void>(resolve => setTimeout(resolve, 0));
return resolve(file);
}
export function nested(file: string): string {
function resolve(p: string): string { return p; }
return resolve(file);
}
`,
'src/widget.ts': 'export class Widget {}\nexport type Size = { width: number };\n',
// A return type's name is no lone arrow parameter, and a value never hides a type.
'src/build.ts': `import { Widget, type Size } from './widget';
export const buildWidget = (name: string): Widget => {
return new Widget();
};
export function measure(Size: number): number {
const size: Size = { width: Size };
return size.width;
}
`,
// The object type a function returns is no body: withShape's own one still holds its parameter.
'src/shape.ts': `import { compare } from './strings';
export function withShape(compare: (a: number, b: number) => number): { sorted: number[]; size: number } {
return { sorted: [compare(1, 2)], size: 1 };
}
export function plain(a: number, b: number): number {
return compare(a, b);
}
`,
'src/biometrics.ts': `export default class OsBiometrics {
constructor(readonly name: string) {}
}
`,
// describeBiometrics binds the name too, so the file's bindings alone don't settle init's.
'src/service.ts': `export class BiometricsService {
private service: object | undefined;
init(platform: string): void {
if (platform === 'darwin') {
const OsBiometrics = require('./biometrics').default;
this.service = new OsBiometrics('mac');
}
}
}
export function describeBiometrics(OsBiometrics: unknown): string {
return String(OsBiometrics);
}
`,
// An anonymous IIFE's calls are the file's own: only the file's view of
// its bindings sees the parameter, after a function-typed one.
'src/iife.ts': `((done: () => void, compare: (a: number, b: number) => number) => {
compare(1, 2);
done();
})(() => {}, (a, b) => a - b);
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(tsRoot, rel)), { recursive: true });
fs.writeFileSync(path.join(tsRoot, rel), content);
}
ts = await CodeGraph.init(tsRoot, { index: true });
});
afterAll(() => {
ts?.close();
if (tsRoot) fs.rmSync(tsRoot, { recursive: true, force: true });
});
/** `<file>:<qualified name>` of every call (or edge of `kind`) the named symbol in `file` makes. */
const callsFrom = (file: string, name: string, kind = 'calls'): string[] => {
const node = ts.getNodesInFile(file).find((n) => n.name === name && n.kind !== 'file')!;
expect(node, `${file} ${name}`).toBeDefined();
return ts.getOutgoingEdges(node.id).filter((e) => e.kind === kind).map((e) => {
const target = ts.getNode(e.target)!;
return `${target.filePath}:${target.qualifiedName}`;
});
};
it('a parameter typed as a function, or defaulted to one, calls what it is given', () => {
expect(callsFrom('src/use.ts', 'pick')).toEqual([]);
expect(callsFrom('src/use.ts', 'pickDefault')).toEqual([]);
expect(callsFrom('src/sort.ts', 'compareAll')).toEqual([]);
});
it('after earlier parameters with function and generic types, and as a parameter property', () => {
expect(callsFrom('src/sort.ts', 'later')).toEqual([]);
expect(callsFrom('src/sort.ts', 'constructor')).toEqual([]);
});
it('shadows the file’s import of the name, which a function binding nothing still calls', () => {
expect(callsFrom('src/use.ts', 'check')).toEqual(['src/sdk.ts:isAvailable']);
});
it('in code no function node holds, the parameter list the call sits in', () => {
const file = ts.getNodesInFile('src/iife.ts').find((n) => n.kind === 'file')!;
const calls = ts.getOutgoingEdges(file.id).filter((e) => e.kind === 'calls').map((e) => ts.getNode(e.target)!.filePath);
expect(calls).not.toContain('src/strings.ts');
});
it('leaves a name no function binds to the matcher', () => {
expect(callsFrom('src/other.ts', 'sortAll')).toEqual(['src/strings.ts:compare']);
});
it('shadows an import only inside the body that binds the name', () => {
expect(callsFrom('src/sync.ts', 'syncInside')).toEqual([]);
expect(callsFrom('src/run.ts', 'run')).toEqual([]);
expect(callsFrom('src/run.ts', 'eachInLoop')).toEqual([]);
expect(callsFrom('src/sync.ts', 'syncLater')).toEqual(['src/state.ts:syncState']);
expect(callsFrom('src/run.ts', 'runThen')).toEqual(['src/paths.ts:resolve']);
expect(callsFrom('src/run.ts', 'eachAfterLoop')).toEqual(['src/paths.ts:resolve']);
});
it('ends an arrow’s expression body at a line that starts the next statement', () => {
expect(callsFrom('src/run.ts', 'fmtAll')).toContain('src/paths.ts:resolve');
});
it('reads a lone arrow parameter, a nested function and a body after an object return type', () => {
expect(callsFrom('src/run.ts', 'bare')).toEqual([]);
expect(callsFrom('src/run.ts', 'bareThen')).toEqual(['src/paths.ts:resolve']);
expect(callsFrom('src/run.ts', 'nested')).not.toContain('src/paths.ts:resolve');
expect(callsFrom('src/shape.ts', 'withShape')).toEqual([]);
expect(callsFrom('src/shape.ts', 'plain')).toEqual(['src/strings.ts:compare']);
});
it('takes no return type for a parameter, and lets no value hide a type', () => {
expect(callsFrom('src/build.ts', 'buildWidget', 'instantiates')).toEqual(['src/widget.ts:Widget']);
expect(callsFrom('src/build.ts', 'measure', 'references')).toContain('src/widget.ts:Size');
});
it('reads `syncState$` as no declaration of `syncState`, and a required local as the import', () => {
expect(callsFrom('src/sync.ts', 'syncObservable')).toEqual(['src/state.ts:syncState']);
expect(callsFrom('src/service.ts', 'init', 'instantiates')).toEqual(['src/biometrics.ts:OsBiometrics']);
});
});
+5 -1
View File
@@ -21,7 +21,7 @@ import {
isImportableKind,
CPP_DEFINE_SIGNATURE,
} from './types';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves } from './name-matcher';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, isImportShadowedByJsBinding } from './name-matcher';
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
@@ -1356,6 +1356,10 @@ export class ReferenceResolver {
const module = this.nodeById(resolved.targetNodeId);
if (module && module.kind !== 'file' && module.kind !== 'module') return null;
}
// A name the calling JS/TS function binds itself (a parameter, a local) in
// scope at the reference is that binding even when the file imports the
// name, which the import resolver answers before the matcher's own check.
if (resolved && resolved.resolvedBy === 'import' && isImportShadowedByJsBinding(resolved, ref, this.context)) return null;
// A declaration of a declaration file's `declare module 'x'` is reached
// through an import of `x`, never by its bare name from a file that
// reaches no `x` (see reachesAmbientModule).
+212 -29
View File
@@ -11136,29 +11136,171 @@ export function jsCodeBindsName(name: string, fn: Node, filePath: string, line:
let binds = memo.get(key);
if (binds === undefined) {
const text = jsFunctionCodeThrough(fn, filePath, line, context);
const { param } = localBindingPatterns(name, 'g');
const n = name.replace(/\$/g, '\\$');
// A plain declaration. Destructuring re-binds what a call returns under
// the same name — `const { t } = useI18n()`, `const { getLabel } =
// useProps(props)` — which is the same-named function more often than not.
const declared = new RegExp(`\\b(?:const|let|var)\\s+${n}\\b(?!\\s*[,\\]}])`).test(text);
// A parameter list — never a control-flow head (`if (openMarkerClose) {`).
// A return type stays on its line, never a ternary's `: data.slice()` below `filter(canRowExpand)`.
// The leading `(?=\()` (every match opens a list) keeps the lookbehind to
// where one opens. Node 22's V8 stops optimizing the regexes a process
// compiles once it has generated about a megabyte of regex code, which a
// resolver pool worker soon has, and then tried the lookbehind at every
// position, each time reading back through the run of blanks before it:
// go-ethereum's graphiql.min.js, a 980 KB line whose last 962 KB the
// stripper blanks (it reads the `//` closing `/Trident\//` as a comment),
// never resolved.
const parameter = new RegExp(`(?=\\()(?<!\\b(?:if|while|for|switch|with)\\s*)${param.source.replace('(?::[^=;{]*)?', '(?::[^=;{}()\\n]*)?')}`);
binds = declared || parameter.test(text);
const declared = new RegExp(`\\b(?:const|let|var)\\s+${n}(?![\\w$])(?!\\s*[,\\]}])`).test(text);
binds = declared || jsParameterListPattern(name, '').test(text);
memo.set(key, binds);
}
return binds;
}
/**
* A parameter list that names `name` (localBindingPatterns' `param`), never
* a control-flow head (`if (openMarkerClose) {`). A return type stays on its
* line, never a ternary's `: data.slice()` below `filter(canRowExpand)`, and
* may hold an object type, so the match ends at the body's `{`, not at the
* type's: `delta(…): { removed: T[]; added: T[] } {`.
* The leading `(?=\()` (every match opens a list) keeps the lookbehind to
* where one opens. Node 22's V8 stops optimizing the regexes a process
* compiles once it has generated about a megabyte of regex code, which a
* resolver pool worker soon has, and then tried the lookbehind at every
* position, each time reading back through the run of blanks before it:
* go-ethereum's graphiql.min.js, a 980 KB line whose last 962 KB the
* stripper blanks (it reads the `//` closing `/Trident\//` as a comment),
* never resolved.
*/
function jsParameterListPattern(name: string, flags: string): RegExp {
const { param } = localBindingPatterns(name, flags);
const returnType = `(?::(?:[^=;{}()\\n]|${PARAM_BRACE})*)?`;
return new RegExp(`(?=\\()(?<!\\b(?:if|while|for|switch|with)\\s*)${param.source.replace('(?::[^=;{]*)?', () => returnType)}`, flags);
}
/**
* Whether the JS/TS function a reference sits in binds its name in a scope
* that holds at the reference, not just somewhere in its text: a parameter
* list or a lone arrow parameter whose body is still open there (a
* function's or a `catch` clause's block, an arrow's block or expression),
* or a `var`/`let`/`const`, `function` or `class` whose block or `for`
* statement is. A `new Promise((resolve) => …)` executor's `resolve` is the
* calls inside it, not a `resolve(file)` after it, and a `const X =
* require('./x')` binds the import itself. A function too long to read
* again keeps the import.
*/
function jsBindingEnclosesReference(name: string, fn: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
const through = jsFunctionCodeThrough(fn, ref.filePath, ref.line, context);
const cut = through.slice(0, through.lastIndexOf('\n') + 1 + ref.column);
if (cut.length > 100_000) return false;
// Brackets in strings, template literals and regex literals open no scope.
const regexes: number[] = [];
const chars = blankStringContents(cut, regexes).split('');
for (let r = 0; r + 1 < regexes.length; r += 2) chars.fill(' ', regexes[r]! + 1, regexes[r + 1]!);
const code = chars.join('');
const n = name.replace(/\$/g, '\\$');
const declaration = new RegExp(
`\\b(?:(?:const|let|var)\\s+${n}(?![\\w$])(?!\\s*[,\\]}])(\\s*=\\s*(?:await\\s+)?(?:require|import)\\s*\\()?|(?:function|class)\\s+${n}(?![\\w$]))`,
'g'
);
for (const m of code.matchAll(declaration)) {
if (m[1]) continue;
const inForHead = /\bfor\s*(?:await\s*)?\(\s*$/.test(code.slice(Math.max(0, m.index! - 16), m.index));
if (inForHead ? jsForStatementOpenAtEnd(code, m.index!) : jsBlockOpenAtEnd(code, m.index!)) return true;
}
for (const m of code.matchAll(jsParameterListPattern(name, 'g'))) {
const end = m.index! + m[0].length;
if (code[end - 1] === '{' ? jsBlockOpenAtEnd(code, end) : jsBodyOpenAtEnd(code, end)) return true;
}
// `new Promise<void>(resolve => { … })`: a lone parameter where an
// expression starts, never the name a return type ends with (`(): Bounds =>`).
const loneParameter = new RegExp(`(^|[(,=\\[{?:]|=>|\\b(?:return|async|default|yield|await))\\s*${n}\\s*=>`, 'g');
for (const m of code.matchAll(loneParameter)) {
if (m[1] === ':' && /\)\s*$/.test(code.slice(Math.max(0, m.index! - 200), m.index))) continue;
if (jsBodyOpenAtEnd(code, m.index! + m[0].length)) return true;
}
return false;
}
/**
* Whether the block that holds `at` (just inside a body's `{`, or where a
* declaration starts) is still open at the end of `code`.
*/
function jsBlockOpenAtEnd(code: string, at: number): boolean {
let depth = 0;
for (let i = at; i < code.length; i++) {
if (code[i] === '{') depth++;
else if (code[i] === '}' && --depth < 0) return false;
}
return true;
}
const JS_OPERAND_END = /[\w$)\]}'"`]/;
const JS_OPERAND_START = /[\w$'"`]/;
/**
* Whether the body that starts at `at` (an arrow's, after its `=>`, or a
* `for` statement's, after its head) is still open at the end of `code`: a
* `{…}` block, or an expression or statement, which a comma or a semicolon
* ends, or the bracket closing around it, or a line that starts another
* statement where no semicolon ends the last.
*/
function jsBodyOpenAtEnd(code: string, at: number): boolean {
let i = at;
while (i < code.length && WHITESPACE.test(code[i]!)) i++;
if (code[i] === '{') return jsBlockOpenAtEnd(code, i + 1);
let depth = 0;
for (; i < code.length; i++) {
const c = code[i]!;
if (c === '(' || c === '[' || c === '{') {
depth++;
} else if (c === ')' || c === ']' || c === '}') {
if (--depth < 0) return false;
} else if (depth === 0 && (c === ',' || c === ';')) {
return false;
} else if (depth === 0 && c === '\n') {
// `const f = (x) => x + 1` with no semicolon, above the reference: the
// line before ends an operand (a name, a closing bracket, a quote) and
// the next starts one (the reference itself, when the code ends there)
// rather than an operator or a `.` continuing the expression.
let before = i - 1;
while (before >= 0 && WHITESPACE.test(code[before]!)) before--;
let after = i + 1;
while (after < code.length && WHITESPACE.test(code[after]!)) after++;
if (JS_OPERAND_END.test(code[before] ?? '') && (after === code.length || JS_OPERAND_START.test(code[after]!))) return false;
i = after - 1;
}
}
return true;
}
/**
* Whether a declaration in a `for` head, at `at`, is still in scope at the
* end of `code`: in the head, or in the statement after it.
*/
function jsForStatementOpenAtEnd(code: string, at: number): boolean {
let depth = 0;
for (let i = at; i < code.length; i++) {
const c = code[i];
if (c === '(' || c === '[' || c === '{') depth++;
else if ((c === ')' || c === ']' || c === '}') && --depth < 0) return c === ')' && jsBodyOpenAtEnd(code, i + 1);
}
return true;
}
/**
* Whether an import-resolved `result` is the file's import of the reference's
* name while the calling JS/TS function binds that name itself, in a scope
* that holds at the reference (jsBindingEnclosesReference): bitwarden's
* `pick(…, isAvailable: (f: string) => boolean)` calling `isAvailable(f)`
* means its parameter, not the file's `import { isAvailable } from './sdk'`.
* The import resolver answers before matchReference's check, which reads the
* function's whole text — too coarse for an import, whose link is usually right.
*/
export function isImportShadowedByJsBinding(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (!JS_FAMILY.has(ref.language) || !JS_LOCAL_REF_KINDS.has(ref.referenceKind) || !/^[A-Za-z_$][\w$]*$/.test(ref.referenceName)) return false;
const target = context.getNodeById?.(result.targetNodeId);
// (A parameter or local is a value: it hides no interface or type alias, `let coords: Bounds`.)
if (!target || target.name !== ref.referenceName || target.kind === 'interface' || target.kind === 'type_alias') return false;
const fn = context.getNodeById?.(ref.fromNodeId);
if (!fn || (fn.kind !== 'function' && fn.kind !== 'method') || fn.startLine > ref.line || fn.endLine < ref.line) return false;
if (target.filePath === ref.filePath && target.startLine >= fn.startLine && target.startLine <= fn.endLine) return false;
// No function binds a name its file never does (read once per file and name).
if (!isLocallyBoundJsName(ref.referenceName, ref.filePath, context)) return false;
if (ref.referenceKind === 'calls' && bareCallReceiver(ref, context) !== null) return false;
return jsBindingEnclosesReference(ref.referenceName, fn, ref, context);
}
/**
* Per context: the comment-stripped lines of the functions jsCodeBindsName
* read last, and where each line ends in that text, most recent last.
@@ -11241,28 +11383,50 @@ const JS_BINDING_NAME = /^[\w$]+$/;
const ARROW_HEAD_CHAR = /[\w$.]/;
const IMPORT_BINDING_VALUE = /^\s*(?:await\s+)?(?:require|import)\s*\(/;
// The balanced groups a parameter's type or default can hold, two levels deep.
const PARAM_PAREN = '\\((?:[^()]|\\([^()]*\\))*\\)';
const PARAM_BRACE = '\\{(?:[^{}]|\\{[^{}]*\\})*\\}';
const PARAM_BRACKET = '\\[(?:[^\\[\\]]|\\[[^\\[\\]]*\\])*\\]';
const PARAM_ANGLE = '<(?!\\s)(?:=>|=(?!>)|[^<>=]|<(?:=>|=(?!>)|[^<>=])*>)*>';
/**
* One piece of a parameter's type or default: a character other than a comma,
* or a balanced group, which may hold commas (`Map<K, V>`, `{ a: A, b: B }`,
* `(a: number, b: number) => number`). Every alternative starts with a
* character of its own — `=>` is the arrow of a function type, and a `<` or
* `>` spaced like an operator (`f.length > 0`) is no bracket — so a type has
* one parse.
*/
const PARAM_PART = `(?:=>|=(?!>)|[^,(){}\\[\\]<>=]|<(?=\\s)|>(?=[\\s=])|${PARAM_PAREN}|${PARAM_BRACE}|${PARAM_BRACKET}|${PARAM_ANGLE})`;
/** A TypeScript parameter property's modifiers: `constructor(private readonly store: Store)`. */
const PARAM_MODIFIERS = '(?:(?:public|private|protected|readonly|override)\\s+)*';
function localBindingPatterns(name: string, flags: string): LocalBindingPatterns {
const n = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return {
// `const { name } = require('./m')` / `= await import('./m')` binds an IMPORT,
// not a shadow: the symbol lives in the other file and the call means it.
decl: new RegExp(
'\\b(?:const|let|var)\\s+(?:' + n + '\\b|[{\\[][^;=]*?\\b' + n + '\\b[^;=]*?[}\\]])\\s*(?:=\\s*([^;\\n]*))?',
'\\b(?:const|let|var)\\s+(?:' + n + '(?![\\w$])|[{\\[][^;=]*?\\b' + n + '(?![\\w$])[^;=]*?[}\\]])\\s*(?:=\\s*([^;\\n]*))?',
flags
),
fn: new RegExp('\\b(?:function|class)\\s+' + n + '\\b', flags),
fn: new RegExp('\\b(?:function|class)\\s+' + n + '(?![\\w$])', flags),
// a parameter: every token before the name in the list is itself a
// parameter (identifier, optional type, optional default) — so a string
// argument containing the word cannot match.
// parameter (identifier or destructuring, optional type, optional
// default) — so a string argument containing the word cannot match.
// Each earlier parameter has exactly one parse: its first non-space
// character after the identifier picks the type (`?`/`:`), default (`=`)
// or bare alternative. Written as `(type)?(default)?\s*`, the same
// strings split several ways per parameter, and a failing search
// backtracked through every combination — 30-40s per name on a vscode
// test file whose helper takes nine `name: T = value` parameters.
// or bare alternative, and a type or default runs to the comma. Written
// as `(type)?(default)?\s*`, the same strings split several ways per
// parameter, and a failing search backtracked through every combination
// — 30-40s per name on a vscode test file whose helper takes nine `name:
// T = value` parameters. A type or default may hold balanced groups
// (PARAM_PART): `isAvailable: (f: string) => boolean`, `cb: () => void`
// before the name, `compare = (a, b) => a - b`.
param: new RegExp(
'\\(\\s*(?:(?:\\.\\.\\.)?[\\w$]+(?:\\s*(?:\\?\\s*)?:[^,()]+|\\s*=[^,()]+|\\s*),\\s*)*' +
n + '\\b(?:\\s*\\??\\s*:[^,()]*)?(?:\\s*=[^,()]*)?(?:\\s*,\\s*[^()]*)?\\)\\s*(?::[^=;{]*)?(?:=>|\\{)',
'\\(\\s*(?:(?:' + PARAM_MODIFIERS + '(?:\\.\\.\\.)?[\\w$]+|' + PARAM_BRACE + '|' + PARAM_BRACKET + ')' +
'(?:\\s*(?:\\?\\s*)?:' + PARAM_PART + '+|\\s*=' + PARAM_PART + '+|\\s*),\\s*)*' +
PARAM_MODIFIERS + n + '(?![\\w$])(?:\\s*\\??\\s*:' + PARAM_PART + '*)?(?:\\s*=' + PARAM_PART + '*)?' +
'(?:\\s*,(?:[^()]|' + PARAM_PAREN + ')*)?\\)\\s*(?::[^=;{]*)?(?:=>|\\{)',
flags
),
};
@@ -11308,12 +11472,31 @@ function bindsAnywhere(source: string, name: string): boolean {
return fn.test(source) || param.test(source) || new RegExp('(?:^|[^\\w$.])' + n + '\\s*=>').test(source);
}
/**
* The `(` of the parenthesized list `source[at]` sits in, read back over the
* balanced groups before it (an earlier parameter's `cb: () => void` or `{ a,
* b }`), or -1 when the name sits in a block, an object or an array, after a
* statement's end, or too far back to be in a parameter list.
*/
function enclosingOpenParen(source: string, at: number): number {
let depth = 0;
for (let i = at - 1; i >= 0 && at - i <= 2000; i--) {
const c = source[i];
if (c === ')' || c === ']' || c === '}') depth++;
else if (c === '(' || c === '[' || c === '{') {
if (depth === 0) return c === '(' ? i : -1;
depth--;
} else if (c === ';' && depth === 0) return -1;
}
return -1;
}
/**
* bindsAnywhere for a plain identifier, tried only where a match can start:
* a declaration at its keyword (in order, resuming past each match exactly as
* the global search does), a parameter at the `(` its list opens with — the
* last `(` before an occurrence of the name, with no `)` between, since no
* earlier parameter can hold a parenthesis — and `name =>` at each arrow.
* the global search does), a parameter at the `(` its list opens with
* (enclosingOpenParen, from an occurrence of the name), and `name =>` at each
* arrow.
*/
function bindsAtSites(source: string, name: string, sites: LocalBindingSites): boolean {
let patterns = LOCAL_BINDING_PATTERNS.get(name);
@@ -11340,8 +11523,8 @@ function bindsAtSites(source: string, name: string, sites: LocalBindingSites): b
}
let tried = -1;
for (let at = source.indexOf(name); at !== -1; at = source.indexOf(name, at + 1)) {
const open = at > 0 ? source.lastIndexOf('(', at - 1) : -1;
if (open < 0 || open === tried || source.lastIndexOf(')', at - 1) > open) continue;
const open = enclosingOpenParen(source, at);
if (open < 0 || open === tried) continue;
tried = open;
param.lastIndex = open;
if (param.test(source)) return true;