mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 06:50:54 +08:00
fix(js,ts): a callee that names nothing records no reference, and a wrapped callee is the plain call (#2742)
A TS/JS call whose callee was no name and no member access (an IIFE, a curried `connect(mapState)(View)`, `handlers[type]()`, `(0, fn)()`, a tagged template on a call) stored the callee's whole source text as the reference's name. Nothing can answer such a name, so each one parked as a failed reference, an IIFE's entire body among them. extractCall now reads the callee past the wrappers that keep it the same callee (parentheses, `!`, `as` / `satisfies`, `await`, type arguments) and records nothing when that is not a name or a dotted chain; the calls inside are visited on their own. Mirrored in the kernel (tsjs/extractors.rs callee_name_of). The reference of a wrapped callee sits at its name, where a plain call's does, and the resolver's bare-call check reads `fn!(`, `(fn)(`, `(fn as F)(` and `(fn satisfies F)(` as it reads `fn(`: SigNoz's `(callback as FrameRequestCallback)(0)` and `(callback)(0)` name the arrow's own parameter, and from the `(` they bound a generated interface's `callback` property. tree-sitter-typescript parses a prefix operator over a generic call as it parses `await f<T>()` (#2586): `!settings.get<boolean>('X')` and `void apiRequest<T>()` are calls whose callee is `!settings.get` / `void apiRequest`. The callee peel that handled `await` now peels a prefix operator too (peelTsPrefixedCallee; the kernel's peel_await_callee), so the call is to `settings.get`, and `void apiRequest<T>().then()` encodes as `apiRequest().then`, not `voidapiRequest().then`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
aa01692620
commit
ae0fc631e0
@@ -268,6 +268,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In React, a component declared through a wrapper with no render function of its own, like styled-components' `styled(ActionButton)` (with `.attrs(…)`, `.withConfig(…)` or type arguments too), emotion's and MUI's `styled(Box)(…)`, `memo(Button)` or `forwardRef(Render)`, now links to the component it wraps, so a render flow in `codegraph_explore`, a component's callers and its impact go on through the wrapper instead of stopping at it. The calls and values in a wrapper's styles, like `${(props) => theme.color(props)}` or a shared `css` block, now belong to the wrapper, as they do to any other constant, so a theme helper used only in styles has callers. A component that wraps its render function in `forwardRef(…)` or `memo(…)` now shows in the impact of the top-level constants it reads, as a function component does. A wrapper around a component that comes from a package, like react-router's `Link` or antd's `Typography.Text`, links to nothing, and the file that declares a wrapper no longer shows as referencing the component it wraps. Re-index React projects after upgrading.
|
||||
- In TypeScript, a type written with a qualifier, like `React.ReactNode`, `Y.Doc`, `TChat.ChatMessage` or `EQueryType.CLICKHOUSE`, now links to the member of what the qualifier names: a type from a package, like React's, no longer links to a class or component of your own that shares its name, and a member of an enum or a namespace your project declares links to that member instead of to another enum's or a same-named component. Re-index TypeScript projects after upgrading.
|
||||
- In JavaScript and TypeScript, an import written from the app's root with a leading slash, like bigbluebutton's `import Icon from '/imports/ui/components/common/icon/component'`, now links to the file it names when the app's `tsconfig.json` or `jsconfig.json` maps it, as `"paths": { "/*": ["*"] }` does. Before, such an import was taken for a package, so the module looked unused and every name it imports linked by name instead, often to the wrong one of several components of the same name, or to nothing. Re-index projects that import this way after upgrading.
|
||||
- In JavaScript and TypeScript, a call whose callee is not a name, like an immediately invoked function `(() => { … })()`, a curried call such as `connect(mapState)(View)`, `handlers[type]()` or `(0, fn)()`, no longer stores the callee's whole source text as the name of a reference that nothing in the project can answer. A single immediately invoked function could leave its entire body in the index that way. The calls inside such a callee, and the inner call of a curried one, are still recorded. A call through parentheses, a cast or a non-null assertion, like `(fn)()`, `(fn as F)()` or `fn!()`, now links to `fn` exactly as `fn()` does, so a parameter named `fn` no longer links to a same-named property elsewhere in the project. In TypeScript, a call with type arguments behind an operator, like `!settings.get<boolean>('X')` or `void apiRequest<Status>()`, now links to what it calls, as `await load<T>()` already did.
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
|
||||
@@ -72,10 +72,87 @@ describe('call-expression receivers (#1683)', () => {
|
||||
it('encodes the receiver as `<inner>().<method>` and drops a receiver with no static callee', () => {
|
||||
const r = extractFromSource('src/x.js', 'function f(d) { d.setdefault("k", []).append(1); make().run(); (0, make)().run(); arr[0]().go(); }');
|
||||
const names = r.unresolvedReferences.filter((u) => u.referenceKind === 'calls').map((u) => u.referenceName).sort();
|
||||
// `(0, make)` and `arr[0]` are the inner calls' own refs, unchanged; their chains are dropped.
|
||||
expect(names).toEqual(['(0, make)', 'arr[0]', 'd.setdefault', 'd.setdefault().append', 'make', 'make().run']);
|
||||
// `(0, make)()` and `arr[0]()` call a value with no name; their chains are dropped too.
|
||||
expect(names).toEqual(['d.setdefault', 'd.setdefault().append', 'make', 'make().run']);
|
||||
const py = extractFromSource('x.py', 'def f(d):\n d.setdefault("k", []).append(1)\n d.items().get(2)\n');
|
||||
expect(py.unresolvedReferences.filter((u) => u.referenceKind === 'calls').map((u) => u.referenceName).sort())
|
||||
.toEqual(['d.items', 'd.items().get', 'd.setdefault', 'd.setdefault().append']);
|
||||
});
|
||||
|
||||
// A callee that names nothing used to give the reference its whole source
|
||||
// text: an IIFE's body, `connect(mapState)`, `handlers[type]`. No symbol has
|
||||
// such a name, so nothing resolved and each one parked as a failed reference.
|
||||
it('records no name for a callee that names nothing, and looks through wrappers that keep one', () => {
|
||||
const js = extractFromSource('src/x.js', [
|
||||
'class A extends B { constructor() { super(1); } }',
|
||||
'function f(handlers, type, fn, ns) {',
|
||||
' (() => { inner(); })();',
|
||||
' (function () { inner2(); })();',
|
||||
' connect(mapState)(View);',
|
||||
' create()(setup);',
|
||||
" breakpoint('tablet')`padding: 0;`;",
|
||||
' handlers[type](1);',
|
||||
' (0, fn)();',
|
||||
' (fn)();',
|
||||
' (ns.fn)();',
|
||||
'}',
|
||||
].join('\n'));
|
||||
expect(js.unresolvedReferences.filter((u) => u.referenceKind === 'calls').map((u) => u.referenceName).sort())
|
||||
.toEqual(['breakpoint', 'connect', 'create', 'fn', 'inner', 'inner2', 'ns.fn', 'super']);
|
||||
// A tag with an object type argument error-recovers into a call on `css<{…}>`.
|
||||
const ts = extractFromSource('src/x.ts', 'function g(fn: () => void) { fn!(); (fn as F)(); css<{ open: boolean }>`color: red;`; }');
|
||||
expect(ts.unresolvedReferences.filter((u) => u.referenceKind === 'calls').map((u) => u.referenceName).sort())
|
||||
.toEqual(['css', 'fn', 'fn']);
|
||||
});
|
||||
|
||||
// tree-sitter-typescript parses a prefix operator over a generic call as the
|
||||
// call's callee, as it does `await f<T>()` (#2586): `!settings.get<boolean>(…)`
|
||||
// was a call named `!settings.get`.
|
||||
it('looks through a prefix operator over a generic call', () => {
|
||||
const ts = extractFromSource('src/x.ts', [
|
||||
'function h() {',
|
||||
" if (!settings.get<boolean>('X')) return;",
|
||||
' void apiRequest<Status>(1);',
|
||||
' return typeof load<T>() === "x" || -count<number>() > 0 || !!useFlag<boolean>();',
|
||||
'}',
|
||||
].join('\n'));
|
||||
const calls = ts.unresolvedReferences.filter((u) => u.referenceKind === 'calls');
|
||||
expect(calls.map((u) => u.referenceName).sort()).toEqual(['apiRequest', 'count', 'load', 'settings.get', 'useFlag']);
|
||||
// Each sits where its callee's name starts, past the operator.
|
||||
expect(calls.find((u) => u.referenceName === 'settings.get')).toMatchObject({ line: 2, column: 7 });
|
||||
expect(calls.find((u) => u.referenceName === 'apiRequest')).toMatchObject({ line: 3, column: 7 });
|
||||
});
|
||||
|
||||
// The reference sits where the name does, and is a bare call as a plain
|
||||
// call's is: SigNoz's `(callback as FrameRequestCallback)(0)` names the
|
||||
// arrow's own parameter, and `(callback)(0)` starting at the `(` bound a
|
||||
// generated interface's `callback` property.
|
||||
it('treats a wrapped callee as the plain call, so a parameter it names shadows a namesake', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cast-callee-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(root, 'schemas.ts'), 'export interface DTO {\n callback?: string[] | null;\n}\n');
|
||||
fs.writeFileSync(path.join(root, 'raf.test.ts'), [
|
||||
"import type { DTO } from './schemas';",
|
||||
"describe('raf', () => {",
|
||||
' beforeEach(() => {',
|
||||
" jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {",
|
||||
' callback(0);',
|
||||
' (callback as FrameRequestCallback)(1);',
|
||||
' callback!(2);',
|
||||
' (callback)(3);',
|
||||
' return 0;',
|
||||
' });',
|
||||
' });',
|
||||
'});',
|
||||
'export const dto: DTO = {};',
|
||||
].join('\n'));
|
||||
const graph = CodeGraph.initSync(root);
|
||||
await graph.indexAll();
|
||||
const property = graph.getNodesByName('callback').find((n) => n.kind === 'property')!;
|
||||
expect(graph.getCallers(property.id)).toEqual([]);
|
||||
graph.destroy();
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,3 +112,14 @@ const Row = styled.div.attrs((props) => ({ role: roleOf(props) }))`
|
||||
${Panel} { margin: 0; }
|
||||
`;
|
||||
const MemoPanel = React.memo(Panel, legacyHelper);
|
||||
// --- callees that name nothing, and wrappers that keep one -----------------------
|
||||
function calleeShapes(handlers, type, fn, ns) {
|
||||
(() => { legacyHelper(1, 2); })();
|
||||
(function () { arrow(3); })();
|
||||
connect(mapState)(Widget);
|
||||
breakpoint('tablet')`padding: 0;`;
|
||||
handlers[type](1);
|
||||
(0, fn)();
|
||||
(fn)();
|
||||
(ns.fn)();
|
||||
}
|
||||
|
||||
@@ -153,6 +153,21 @@ export const MemoCompared = memo(ImportedComponent, helperFn);
|
||||
export const MemoForward = memo(forwardRef(RenderInput));
|
||||
export const RefForwarded = React.forwardRef(RenderInput);
|
||||
|
||||
// Callees that name nothing, and the wrappers that keep one.
|
||||
export async function calleeShapes(handlers: Record<string, () => void>, fn: () => void, load: () => Promise<() => void>) {
|
||||
(() => { helperFn(); })();
|
||||
create<ButtonProps>()(helperFn);
|
||||
handlers['open']();
|
||||
(0, fn)();
|
||||
fn!();
|
||||
(fn as () => void)();
|
||||
(await load())();
|
||||
await helperFn<ButtonProps>();
|
||||
void helperFn<ButtonProps>();
|
||||
if (!NS.helper.deep<boolean>(1)) return;
|
||||
return typeof create<ButtonProps>() === 'function' && !!create<RowProps>();
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const nodes: GraphNode[] = [];
|
||||
return (
|
||||
|
||||
@@ -22,6 +22,8 @@ re!(styled_callee, r"^styled\b");
|
||||
re!(pascal_case, r"^[A-Z]");
|
||||
// extractCall parenthesized-conversion normalization
|
||||
re!(paren_conversion, r"^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$");
|
||||
// tsJsCalleeName's dotted chain (`a.b.c`); ASCII classes, as JS's `\w` is
|
||||
re!(dotted_chain, r"^[A-Za-z_$][0-9A-Za-z_$]*(?:\.[A-Za-z_$][0-9A-Za-z_$]*)+$");
|
||||
// flushFnRefCandidates SIMPLE_NAME
|
||||
re!(simple_name, r"^[A-Za-z_$][A-Za-z0-9_$]*$");
|
||||
// flushFnRefCandidates QUALIFIED_IMPORT
|
||||
|
||||
@@ -1529,7 +1529,7 @@ impl<'t> Walker<'t> {
|
||||
// Where the reference sits: the call, or the callee an `await` was
|
||||
// peeled off (`await f<T>(x)` starts at `await`; the call to `f`
|
||||
// starts where `f` does, as in `await f(x)`). Mirrors refStart.
|
||||
let at = match (raw, func) {
|
||||
let mut at = match (raw, func) {
|
||||
(Some(r), Some(f)) if r.id() != f.id() => f,
|
||||
_ => node,
|
||||
};
|
||||
@@ -1596,7 +1596,15 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callee_name = self.text(func).to_string();
|
||||
// A callee that names nothing (a call, a function, a
|
||||
// subscript) is no reference. Mirrors tsJsCalleeName.
|
||||
match self.callee_name_of(func) {
|
||||
Some((name, name_at)) => {
|
||||
callee_name = name;
|
||||
at = name_at;
|
||||
}
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1612,6 +1620,27 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The name a non-member callee writes, past `(fn)`, `fn!`, `(fn as F)`,
|
||||
/// `await` and a tag's type arguments (`css<Props>`), or None for one that
|
||||
/// names nothing: a call, a function, a subscript, `(0, fn)`. A dotted
|
||||
/// chain keeps its text, `(a.b)()` → `a.b`. The node is where the name
|
||||
/// starts, which is where the reference sits. Mirrors tsJsCalleeName.
|
||||
fn callee_name_of(&self, func: Node<'t>) -> Option<(String, Node<'t>)> {
|
||||
let mut cur = peel_receiver(func);
|
||||
while cur.kind() == "instantiation_expression" {
|
||||
let inner = cur.child_by_field_name("function").or_else(|| cur.named_child(0))?;
|
||||
cur = peel_receiver(inner);
|
||||
}
|
||||
match cur.kind() {
|
||||
"identifier" | "super" | "import" => Some((self.text(cur).to_string(), cur)),
|
||||
"member_expression" => {
|
||||
let chain: String = self.text(cur).chars().filter(|c| !c.is_whitespace()).collect();
|
||||
util::dotted_chain().is_match(&chain).then_some((chain, cur))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// --- extractInstantiation -----------------------------------------------------------
|
||||
|
||||
/// `this.<field>` as a member_expression receiver → Some(field) (#1496, #1987).
|
||||
@@ -1630,17 +1659,27 @@ impl<'t> Walker<'t> {
|
||||
Some(self.text(property).to_string())
|
||||
}
|
||||
|
||||
/// `await f<T>(x)` parses with `await f` as the callee; the call is to
|
||||
/// `f` (#2586). Peels every `await`, skipping a comment written after one.
|
||||
/// TS-family only. Mirrors peelTsAwaitCallee.
|
||||
/// `await f<T>(x)` parses with `await f` as the callee (#2586), and a
|
||||
/// prefix operator over a generic call the same way
|
||||
/// (`!settings.get<boolean>('X')`, `void apiRequest<T>()`); the call is to
|
||||
/// `f`. Peels every `await` and prefix operator, skipping a comment written
|
||||
/// after an `await`. TS-family only. Mirrors peelTsPrefixedCallee.
|
||||
fn peel_await_callee(&self, node: Node<'t>) -> Node<'t> {
|
||||
if !self.variant.is_ts() {
|
||||
return node;
|
||||
}
|
||||
let mut cur = node;
|
||||
while cur.kind() == "await_expression" {
|
||||
let mut walker = cur.walk();
|
||||
let Some(inner) = cur.named_children(&mut walker).find(|c| c.kind() != "comment") else { break };
|
||||
loop {
|
||||
let inner = match cur.kind() {
|
||||
"unary_expression" => cur.child_by_field_name("argument"),
|
||||
"await_expression" => {
|
||||
let mut walker = cur.walk();
|
||||
let found = cur.named_children(&mut walker).find(|c| c.kind() != "comment");
|
||||
found
|
||||
}
|
||||
_ => break,
|
||||
};
|
||||
let Some(inner) = inner else { break };
|
||||
cur = inner;
|
||||
}
|
||||
cur
|
||||
|
||||
@@ -559,13 +559,17 @@ const TS_JS_TRANSPARENT_RECEIVER_TYPES = new Set([
|
||||
|
||||
/**
|
||||
* tree-sitter-typescript parses `await f<T>(x)` as a call whose callee is
|
||||
* `await f` (#2586); the call is to `f`. Peels every `await`, skipping a
|
||||
* comment written after one. Mirrored in the kernel (tsjs/extractors.rs).
|
||||
* `await f` (#2586), and a prefix operator over a generic call the same way:
|
||||
* `!settings.get<boolean>('X')`, `void apiRequest<T>()`, `typeof load<T>()`.
|
||||
* The call is to `f`. Peels every `await` and prefix operator, skipping a
|
||||
* comment written after an `await`. Mirrored in the kernel (tsjs/extractors.rs).
|
||||
*/
|
||||
function peelTsAwaitCallee(node: SyntaxNode): SyntaxNode {
|
||||
function peelTsPrefixedCallee(node: SyntaxNode): SyntaxNode {
|
||||
let cur = node;
|
||||
while (cur.type === 'await_expression') {
|
||||
const inner = cur.namedChildren.find((c: SyntaxNode) => c.type !== 'comment');
|
||||
while (cur.type === 'await_expression' || cur.type === 'unary_expression') {
|
||||
const inner = cur.type === 'unary_expression'
|
||||
? getChildByField(cur, 'argument')
|
||||
: cur.namedChildren.find((c: SyntaxNode) => c.type !== 'comment');
|
||||
if (!inner) break;
|
||||
cur = inner;
|
||||
}
|
||||
@@ -593,6 +597,32 @@ function peelTsJsReceiver(node: SyntaxNode): SyntaxNode {
|
||||
return cur;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name a TS/JS call's callee writes when it is no member access, past the
|
||||
* wrappers that keep it the same callee — `(fn)()`, `fn!()`, `(fn as F)()`,
|
||||
* `await f<T>()`, the type arguments of a tagged template's tag
|
||||
* (`css<Props>\`…\``) — or null when it names nothing: a call (`connect(map)
|
||||
* (View)`, `create<T>()(…)`, `breakpoint('tablet')\`…\``), a function (an
|
||||
* IIFE), a subscript (`handlers[type]()`), `(0, fn)()`. Its text used to be
|
||||
* the reference's name, an IIFE's whole body among them, which no symbol is.
|
||||
* A dotted chain in parentheses keeps its text, `(a.b)()` → `a.b`, as the
|
||||
* parenthesized-callee normalization in extractCall reads it. `at` is where the
|
||||
* name starts, which is where the reference sits, as a plain `fn()` call's does:
|
||||
* scope checks read the name there (a parameter `fn` shadows a namesake).
|
||||
*/
|
||||
function tsJsCalleeName(func: SyntaxNode, source: string): { name: string; at: SyntaxNode } | null {
|
||||
let cur = peelTsJsReceiver(func);
|
||||
while (cur.type === 'instantiation_expression') {
|
||||
const inner = getChildByField(cur, 'function') ?? cur.namedChild(0);
|
||||
if (!inner) return null;
|
||||
cur = peelTsJsReceiver(inner);
|
||||
}
|
||||
if (cur.type === 'identifier' || cur.type === 'super' || cur.type === 'import') return { name: getNodeText(cur, source), at: cur };
|
||||
if (cur.type !== 'member_expression') return null;
|
||||
const chain = getNodeText(cur, source).replace(/\s+/g, '');
|
||||
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(chain) ? { name: chain, at: cur } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a TS/JS receiver still collapses to the bare method name: `this` /
|
||||
* `super` (the resolver reads the owner off the enclosing class), a member
|
||||
@@ -5603,7 +5633,7 @@ export class TreeSitterExtractor {
|
||||
} else {
|
||||
let func = getChildByField(node, 'function') || node.namedChild(0);
|
||||
if (func && (this.language === 'typescript' || this.language === 'tsx' || this.language === 'arkts')) {
|
||||
const peeled = peelTsAwaitCallee(func);
|
||||
const peeled = peelTsPrefixedCallee(func);
|
||||
if (peeled !== func) refStart = peeled.startPosition;
|
||||
func = peeled;
|
||||
}
|
||||
@@ -5909,7 +5939,7 @@ export class TreeSitterExtractor {
|
||||
// wrong edge. The inner call is visited on its own either way.
|
||||
// Mirrored in the kernel (tsjs/extractors.rs, python.rs).
|
||||
let innerFn = getChildByField(receiver, 'function');
|
||||
if (innerFn && (this.language === 'typescript' || this.language === 'tsx')) innerFn = peelTsAwaitCallee(innerFn);
|
||||
if (innerFn && (this.language === 'typescript' || this.language === 'tsx')) innerFn = peelTsPrefixedCallee(innerFn);
|
||||
const innerCallee = innerFn ? getNodeText(innerFn, this.source).replace(/\s+/g, '') : '';
|
||||
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(innerCallee)) return;
|
||||
calleeName = `${innerCallee}().${methodName}`;
|
||||
@@ -5992,6 +6022,15 @@ export class TreeSitterExtractor {
|
||||
} else {
|
||||
calleeName = getNodeText(func, this.source);
|
||||
}
|
||||
} else if (TS_JS_CHAIN_LANGUAGES.has(this.language)) {
|
||||
// A callee that names nothing — a call, a function, a subscript —
|
||||
// is no reference: a silent miss, never a name no symbol has. A
|
||||
// curried call's inner call is visited on its own. Mirrored in the
|
||||
// kernel's extract_call (tsjs/extractors.rs callee_name_of).
|
||||
const named = tsJsCalleeName(func, this.source);
|
||||
if (named === null) return;
|
||||
calleeName = named.name;
|
||||
refStart = named.at.startPosition;
|
||||
} else {
|
||||
calleeName = getNodeText(func, this.source);
|
||||
}
|
||||
|
||||
@@ -3628,10 +3628,12 @@ function isReceiverLessCall(ref: UnresolvedRef, context: ResolutionContext): boo
|
||||
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1]
|
||||
?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
|
||||
if (line === undefined) return false;
|
||||
// `^<name>\s*[(<]` at the call's column, without compiling a pattern per call.
|
||||
// `^<name>\s*[(<]` at the call's column (or a JS/TS wrapper's tail after the
|
||||
// name), without compiling a pattern per call.
|
||||
if (!line.startsWith(ref.referenceName, ref.column)) return false;
|
||||
CALL_OPENER.lastIndex = ref.column + ref.referenceName.length;
|
||||
if (!CALL_OPENER.test(line)) return false;
|
||||
JS_PEELED_CALLEE_TAIL.lastIndex = CALL_OPENER.lastIndex;
|
||||
if (!CALL_OPENER.test(line) && !(JS_FAMILY.has(ref.language) && JS_PEELED_CALLEE_TAIL.test(line))) return false;
|
||||
// Nothing but whitespace, an operator or an opener may precede a bare call:
|
||||
// read the text before the column backwards, past trailing whitespace,
|
||||
// instead of end-anchoring a pattern that scans the prefix from its start.
|
||||
@@ -3649,6 +3651,12 @@ function isReceiverLessCall(ref: UnresolvedRef, context: ResolutionContext): boo
|
||||
|
||||
/** `\s*[(<]` from a given index (sticky) — an optional call's `?.(` too. */
|
||||
const CALL_OPENER = /\s*(?:\?\.\s*)?[(<]/y;
|
||||
/**
|
||||
* What follows a JS/TS callee's name inside a wrapper the extractor sees
|
||||
* through — `fn!()`, `(fn)()`, `(fn as F)()`, `(fn satisfies F)()` — whose
|
||||
* reference sits at the name, as a plain call's does (sticky).
|
||||
*/
|
||||
const JS_PEELED_CALLEE_TAIL = /\s*[!)]|\s+(?:as|satisfies)(?![\w$])/y;
|
||||
const WHITESPACE = /\s/;
|
||||
const WORD_CHAR = /\w/;
|
||||
/** A character that ends a receiver: `.`, a word character, `$`, `]` or `)`. */
|
||||
|
||||
Reference in New Issue
Block a user