mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
fix(resolution): calls on the browser's own objects link no project method (#3026)
`window.open('/help')`, `localStorage.getItem('token')`,
`location.reload()` and `history.back()` linked to a project class's
method of the same name, like an app's `WindowService::open` or
`StorageService::getItem`, so callers and impact on such a wrapper
listed every direct browser call in the app. Three readings did it:
- an untyped receiver went to matchMethodCall's guesses, by the
receiver's word (`window` ~ WindowService) or a lone method name;
- a chain from `window`, which the extractor records by its bare method
name so a project object hung on `window` can be found (#1707), fell to
the exact-name match when the chain names no project object:
`window.localStorage.getItem()`;
- a JS `self.close()` was read as `this.close()` by bareCallReceiver and
callReceiverKind, though a `self` the file doesn't bind is the global
object (a worker's scope, or `window.self`).
Now, in the JS family:
- a call on one of JS_BROWSER_HOST_GLOBALS (window, self, globalThis,
document, localStorage, sessionStorage, location, history, navigator,
screen, performance) gets no edge when the file writes that name only
as a receiver (`document.x`, `history.back()`, `self?.close()`) or after
`typeof`, decided before the receiver-type reader, whose backward scan
has no scope. Any other mention (a parameter, destructured ones
included, a declaration, an import, an argument) keeps the name the
file's own: outline passes its Document model as `({ document }:
Props)`, and those `document.save()` calls keep their links;
- matchCollapsedObjectCall declines a `window`/`globalThis` chain whose
first link is one of them, after its project-object lookup; a chain
through a project object (`window.App.init()`) is unchanged;
- an unbound `self` reaches matchCollapsedObjectCall as
`globalThis.self`, and callReceiverKind no longer counts it as the
instance. A `self` the file binds (`var self = this`) reads as before.
Validation: new browser-host-globals tests (4) pass with the fix. On
main the first fails with 8 wrong edges and the third (outline's
destructured `document`, a `typeof window` guard) with 2; the two that
pin what must keep working (a bound, imported or parameter name; `this`,
`var self = this`, `window.App`) pass on both. A first version that
asked only whether the file binds the name dropped outline's
destructured `document` calls; the third test fails against it.
A/B against main: only removals. excalidraw 12 (`window.open` ->
Portal::open, `document.createElement` -> DraggableTool::createElement,
a worker's `self.postMessage` -> WorkerPool::postMessage), outline 21
(test and app `sessionStorage`/`localStorage` calls -> MemoryStorage,
`window.document.body.*`, `window.history.back()` -> a Props property),
bitwarden apps/browser + apps/web 7 (`window.postMessage`,
`window.location.reload()`, `window.navigator.permissions.query()`), and
express unchanged. 39 of the 40 linked a project method by name only;
the 40th linked outline's own `declare global` typing of a browser API
(Document.modelContext.registerTool).
Reimplements PR #2915 by @KissFeng.
Fixes #2907
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
38dc3741aa
commit
f1da755a91
@@ -23,6 +23,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In JavaScript and TypeScript, a call or `new` in a parameter's default value, like `function renderWithNote(note = buildNote())`, a destructured `{ size = defaultSize() }` or `constructor(private svc = new Service())`, now counts as the function's own call. Before, it was dropped, so `buildNote` was missing that caller and impact on it came back incomplete. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and the pull request. (#2588)
|
||||
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
|
||||
- In JavaScript and TypeScript, react-dom's `root.render(<Popup />)` no longer links to your own class component's `render()`, such as an error boundary's, when `root` is declared outside the function making the call, like a module's `let root: Root | null`, or holds what `createRoot(el)` returned. A call on a variable declared outside the calling function now goes by the type that declaration gives it, so one declared with your own class, like `const store = new Store()` at the top of a file, links to that class's method, and a class component's `render()` and lifecycle methods, which only React calls, are no longer a guess for an untyped value. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report. (#2589)
|
||||
- In JavaScript and TypeScript, a call on one of the browser's own objects, like `window.open('/help')`, `localStorage.getItem('token')`, `location.reload()`, `history.back()`, `window.localStorage.getItem('token')` or a worker's `self.postMessage(…)`, no longer links to a method of your own that only shares its name, like a `WindowService.open` or a `StorageService.getItem` an app wraps the browser in, so `codegraph callers` and impact on such a wrapper no longer list every direct browser call in the app. A name the file declares, imports or takes as a parameter, like `const history = createRouter()`, still calls what it holds, `this` still means the class or object it is written in, and so does a `self` the file sets to it, like `var self = this`. Re-index JavaScript and TypeScript projects after upgrading. Thanks @Gobakabana for the report and @KissFeng. (#2907)
|
||||
- In JavaScript and TypeScript, a method called on a freshly constructed object, like `new RegExp(p).exec(s)`, `new URL(u).toString()` or `new Runner().run()`, now links only to that class's own or inherited method, on the class the file imports when several share its name, and no longer to an unrelated project method of the same name; a class from outside your project links nothing. Re-index JavaScript and TypeScript projects after upgrading. Thanks @danusha2345. (#1945)
|
||||
- In Python, `import a.b as X` written inside a function now links its calls like `X.f()`, as the same import at module level already did. Thanks @Egrn for the report. (#2600)
|
||||
- In Python, `from app import email` no longer links the importing file to a project variable named `app`, such as a FastAPI or Flask `app = FastAPI()`. (#2584)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Calls on the browser's own objects link to no project method (#2907).
|
||||
*
|
||||
* `window.open('/help')`, `localStorage.getItem('token')`, `location.reload()`
|
||||
* and `history.back()` are DOM calls, yet each linked to a project class's
|
||||
* method of the same name — an app's `WindowService.open`, `StorageService.
|
||||
* getItem`, `PageService.back` — so `callers` and impact on such a wrapper
|
||||
* listed every direct DOM call in the app. Three readings did it: a receiver
|
||||
* with no type was guessed by its word or a lone method name; a chain from
|
||||
* `window` (`window.localStorage.getItem()`), recorded by its bare method name
|
||||
* so a project global hung on `window` can be found (#1707), fell to the
|
||||
* exact-name match; and a JS `self.close()` was read as `this.close()`,
|
||||
* though an unbound `self` is the global object.
|
||||
*
|
||||
* A name the file binds or imports is still its own, and `this` is still the
|
||||
* instance.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let dirs: string[] = [];
|
||||
let graphs: CodeGraph[] = [];
|
||||
afterEach(() => {
|
||||
for (const graph of graphs) graph.close();
|
||||
for (const dir of dirs) fs.rmSync(dir, { recursive: true, force: true });
|
||||
graphs = [];
|
||||
dirs = [];
|
||||
});
|
||||
|
||||
async function index(files: Record<string, string>): Promise<CodeGraph> {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-host-globals-'));
|
||||
dirs.push(dir);
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, rel), content);
|
||||
}
|
||||
const cg = await CodeGraph.init(dir, { index: true });
|
||||
graphs.push(cg);
|
||||
return cg;
|
||||
}
|
||||
|
||||
/** `caller -> target` for every call edge out of `fromFile`, by qualified name. */
|
||||
function callsFrom(cg: CodeGraph, fromFile: string): string[] {
|
||||
return cg
|
||||
.getNodesInFile(fromFile)
|
||||
.flatMap((n) =>
|
||||
cg
|
||||
.getOutgoingEdges(n.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => `${n.qualifiedName} -> ${cg.getNode(e.target)?.qualifiedName}`)
|
||||
)
|
||||
.sort();
|
||||
}
|
||||
|
||||
const SERVICES = [
|
||||
'export class WindowService {',
|
||||
' open(url: string): void {}',
|
||||
' close(): void {}',
|
||||
' postMessage(m: unknown): void {}',
|
||||
'}',
|
||||
'export class StorageService {',
|
||||
' getItem(key: string): string | null { return null; }',
|
||||
'}',
|
||||
'export class PageService {',
|
||||
' reload(): void {}',
|
||||
' back(): void {}',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
describe('calls on browser host objects', () => {
|
||||
it('link no project method, written directly or through window or self', async () => {
|
||||
const cg = await index({
|
||||
'src/services.ts': SERVICES,
|
||||
'src/browser.ts': [
|
||||
"export function openHelp(): void { window.open('/help'); }",
|
||||
"export function readToken(): string | null { return localStorage.getItem('token'); }",
|
||||
'export function refresh(): void { location.reload(); }',
|
||||
'export function goBack(): void { history.back(); }',
|
||||
"export function readTokenChained(): string | null { return window.localStorage.getItem('token'); }",
|
||||
'export function goBackChained(): void { window.history.back(); }',
|
||||
'export function closeSelf(): void { self.close(); }',
|
||||
'export class Popup {',
|
||||
' dismiss(): void { self.close(); }',
|
||||
' close(): void {}',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'src/worker.js': "self.postMessage('ready');\nexport function reply(x) { self.postMessage(x); }\n",
|
||||
});
|
||||
expect(callsFrom(cg, 'src/browser.ts')).toEqual([]);
|
||||
expect(callsFrom(cg, 'src/worker.js')).toEqual([]);
|
||||
});
|
||||
|
||||
it('still link a name the file binds, imports or takes as a parameter', async () => {
|
||||
const cg = await index({
|
||||
'src/router.ts': [
|
||||
'export class Router { forward(): void {} }',
|
||||
'export function createRouter(): Router { return new Router(); }',
|
||||
'',
|
||||
].join('\n'),
|
||||
'src/nav.ts': 'export const history = { push(to: string): void {} };\n',
|
||||
'src/loc.ts': 'export class Loc { reload(): void {} }\n',
|
||||
'src/uses.ts': [
|
||||
"import { createRouter } from './router';",
|
||||
"import { history } from './nav';",
|
||||
"import { Loc } from './loc';",
|
||||
'export function local(): void {',
|
||||
' const navigator = createRouter();',
|
||||
' navigator.forward();',
|
||||
'}',
|
||||
"export function imported(): void { history.push('/x'); }",
|
||||
'export function param(location: Loc): void { location.reload(); }',
|
||||
'',
|
||||
].join('\n'),
|
||||
});
|
||||
expect(callsFrom(cg, 'src/uses.ts')).toEqual([
|
||||
'imported -> history::push',
|
||||
'local -> Router::forward',
|
||||
'local -> createRouter',
|
||||
'param -> Loc::reload',
|
||||
]);
|
||||
});
|
||||
|
||||
it('still link a destructured parameter named like one, and decline after a `typeof` guard', async () => {
|
||||
// outline's commands and scenes take their Document model as `({ document }: Props)`.
|
||||
const cg = await index({
|
||||
'src/models/Model.ts': 'export class Model {\n save(): void {}\n}\n',
|
||||
'src/models/Document.ts': "import Model from './Model';\nexport default class Document extends Model {\n save(): void {}\n archive(): void {}\n}\n",
|
||||
'src/services.ts': SERVICES,
|
||||
'src/DocumentPublish.tsx': [
|
||||
"import type Document from './models/Document';",
|
||||
'',
|
||||
'type Props = {',
|
||||
' /** The document to publish. */',
|
||||
' document: Document;',
|
||||
'};',
|
||||
'',
|
||||
'export function DocumentPublish({ document }: Props) {',
|
||||
' const publish = async () => {',
|
||||
' await document.save();',
|
||||
' };',
|
||||
' return publish;',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'src/help.ts': [
|
||||
'export function openHelp(): void {',
|
||||
" if (typeof window !== 'undefined') window.open('/help');",
|
||||
'}',
|
||||
'export function goBack(nav: { canGoBack: boolean }): void {',
|
||||
' if (nav.canGoBack && window.history.length > 1) history.back();',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
});
|
||||
expect(callsFrom(cg, 'src/DocumentPublish.tsx')).toEqual(['DocumentPublish::publish -> Document::save']);
|
||||
expect(callsFrom(cg, 'src/help.ts')).toEqual([]);
|
||||
});
|
||||
|
||||
it('still read `this`, a `self` the file binds to it, and a project object hung on window', async () => {
|
||||
const cg = await index({
|
||||
'src/dialog.js': [
|
||||
'export class Dialog {',
|
||||
' close() {}',
|
||||
' open() { this.close(); }',
|
||||
' later() { var self = this; setTimeout(function () { self.close(); }); }',
|
||||
'}',
|
||||
'export const store = {',
|
||||
' close() {},',
|
||||
' open() { this.close(); },',
|
||||
'};',
|
||||
'',
|
||||
].join('\n'),
|
||||
'src/app.js': 'window.App = {\n init() {},\n};\nexport function boot() { window.App.init(); }\n',
|
||||
});
|
||||
expect(callsFrom(cg, 'src/dialog.js')).toEqual([
|
||||
'Dialog::later -> Dialog::close',
|
||||
'Dialog::open -> Dialog::close',
|
||||
'store::open -> store::close',
|
||||
]);
|
||||
expect(callsFrom(cg, 'src/app.js')).toEqual(['boot -> window.App::init']);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,18 @@ export const JS_BUILT_INS = new Set([
|
||||
'fetch', 'require', 'module', 'exports', '__dirname', '__filename',
|
||||
]);
|
||||
|
||||
/**
|
||||
* The browser's host objects a script reaches by name: the global object under
|
||||
* its names (`window`, `self`, `globalThis`) and the members of it that code
|
||||
* calls straight on. A call on one of them — `window.open()`,
|
||||
* `localStorage.getItem()`, `history.back()` — is the browser's, unless the
|
||||
* file binds or imports the name itself.
|
||||
*/
|
||||
export const JS_BROWSER_HOST_GLOBALS = new Set([
|
||||
'window', 'self', 'globalThis', 'document', 'localStorage', 'sessionStorage',
|
||||
'location', 'history', 'navigator', 'screen', 'performance',
|
||||
]);
|
||||
|
||||
/** Method names that require receiver evidence before linking to project code. */
|
||||
export const JS_BUILTIN_METHODS = new Set([
|
||||
// Array / typed arrays and collections.
|
||||
|
||||
@@ -11,7 +11,7 @@ import { escapeRegExp } from '../utils';
|
||||
import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types';
|
||||
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
|
||||
import { isJsSuperCallSite } from './js-super-calls';
|
||||
import { JS_ARRAY_METHODS, JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
||||
import { JS_ARRAY_METHODS, JS_BROWSER_HOST_GLOBALS, JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
||||
import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility';
|
||||
import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, dartPrivateVisible, inSameDartLibrary } from './dart-libraries';
|
||||
import { isDartLocallyBound } from './dart-local-scope';
|
||||
@@ -3632,8 +3632,10 @@ export const CASE_INSENSITIVE_LANGUAGES = new Set<string>(['php', 'pascal', 'cfm
|
||||
* read from the source: TS/JS keeps `this.container.classList.toggle()` and
|
||||
* `window.$events.listen()` bare, Scala `requestToArmeria(request).execute()`
|
||||
* and `_.get.whenRequestMatchesPartial(…)`. `'self'` for `this.m()` /
|
||||
* `self.m()` / `super.m()` / `super().m()`; null for a call written bare (or
|
||||
* not found). `links` are the member names between `this` and the method.
|
||||
* `self.m()` / `super.m()` / `super().m()` — in JS, `self.m()` only when the
|
||||
* file binds `self` (`globalThis.self` otherwise); null for a call written
|
||||
* bare (or not found). `links` are the member names between `this` and the
|
||||
* method.
|
||||
*/
|
||||
function bareCallReceiver(ref: UnresolvedRef, context: ResolutionContext): { receiver: string; links: string[] } | null {
|
||||
if (ref.referenceKind !== 'calls' || !/^[A-Za-z_$][\w$]*$/.test(ref.referenceName)) return null;
|
||||
@@ -3659,12 +3661,70 @@ function bareCallReceiver(ref: UnresolvedRef, context: ResolutionContext): { rec
|
||||
const before = text.slice(0, at.index).replace(/\s+$/, '');
|
||||
if (!/\??\.$/.test(before)) return null;
|
||||
const head = before.replace(/\??\.$/, '').replace(/\s+$/, '');
|
||||
// A JS `self` the file doesn't bind is the global object's own `self` — a
|
||||
// worker's scope, or the page's `window` — never `this`, and is read as the
|
||||
// path it is (`'self'` alone would read as `this`).
|
||||
if (isJsGlobalSelf(head, ref, context)) return { receiver: 'globalThis.self', links: [] };
|
||||
if (/(?:^|[^\w$.])(?:this|self|super|Self)$/.test(head) || /(?:^|[^\w$.])super\s*\([^()]*\)$/.test(head)) return { receiver: 'self', links: [] };
|
||||
const chain = /(?:^|[^\w$.#])((?:this|super)(?:\s*\??\.\s*#?[\w$]+)+)$/.exec(head);
|
||||
const links = chain ? chain[1]!.split('.').slice(1).map((l) => l.replace(/[\s?]/g, '')) : [];
|
||||
return { receiver: head.slice(-40), links };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a JS/TS name is one of the browser's own objects where the
|
||||
* reference is written (JS_BROWSER_HOST_GLOBALS: `window`, `self`,
|
||||
* `localStorage`, `history`, …) because the file uses the name only that way
|
||||
* (usesNameOnlyAsReceiver). A call on one is the browser's, never a project
|
||||
* method: `window.open()` is no `WindowService::open`, `history.back()` no
|
||||
* router's `back` (#2907). A file that binds the name — `const history =
|
||||
* createRouter()`, `var self = this`, a parameter, plain or destructured like
|
||||
* outline's `({ document }: Props)` — or imports it calls what that holds.
|
||||
*/
|
||||
function isBrowserHostGlobal(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
return JS_FAMILY.has(ref.language) && JS_BROWSER_HOST_GLOBALS.has(name) &&
|
||||
usesNameOnlyAsReceiver(name, ref.filePath, context);
|
||||
}
|
||||
|
||||
/** Per context, by `file\0name`: whether the file uses the name only as a receiver. */
|
||||
const RECEIVER_ONLY_NAMES = new WeakMap<ResolutionContext, Map<string, boolean>>();
|
||||
|
||||
/**
|
||||
* Whether a JS/TS file writes `name` only as a receiver — `document.x`,
|
||||
* `history.back()`, `self?.close()`, `localStorage['k']` — or after
|
||||
* `typeof`, which is how code uses one of the browser's own objects. Any
|
||||
* other mention may bind or hand over a value of the file's own: a parameter
|
||||
* (destructured ones included), a declaration, an import, an argument, an
|
||||
* object key. Read with comments and strings blanked; a property of the same
|
||||
* name (`x.document`) is no mention. An indexOf scan, not a look-behind
|
||||
* regex, so a minified line costs one pass.
|
||||
*/
|
||||
function usesNameOnlyAsReceiver(name: string, filePath: string, context: ResolutionContext): boolean {
|
||||
let memo = RECEIVER_ONLY_NAMES.get(context);
|
||||
if (!memo) RECEIVER_ONLY_NAMES.set(context, (memo = new Map()));
|
||||
const key = `${filePath}\0${name}`;
|
||||
const known = memo.get(key);
|
||||
if (known !== undefined) return known;
|
||||
const code = blankStringContents(stripCommentsForRegex(context.readFile(filePath) ?? '', 'typescript'));
|
||||
let only = true;
|
||||
for (let at = code.indexOf(name); at !== -1 && only; at = code.indexOf(name, at + 1)) {
|
||||
const end = at + name.length;
|
||||
if ((at > 0 && /[\w$.#]/.test(code[at - 1]!)) || /[\w$]/.test(code[end] ?? '')) continue;
|
||||
let next = end;
|
||||
while (next < code.length && /\s/.test(code[next]!)) next++;
|
||||
const receiver = (code[next] === '.' && code[next + 1] !== '.') || code[next] === '[' ||
|
||||
(code[next] === '?' && code[next + 1] === '.');
|
||||
if (!receiver && !/\btypeof\s+$/.test(code.slice(Math.max(0, at - 16), at))) only = false;
|
||||
}
|
||||
memo.set(key, only);
|
||||
return only;
|
||||
}
|
||||
|
||||
/** Whether `head`, the code before a call's `.`, ends with a JS `self` that names the global object. */
|
||||
function isJsGlobalSelf(head: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
return /(?:^|[^\w$.])self$/.test(head) && isBrowserHostGlobal('self', ref, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a Python call recorded by its bare name was written on the instance,
|
||||
* `self.get_ip(request)`. The extractor drops the `self.`, so a same-named name
|
||||
@@ -12382,7 +12442,9 @@ function callReceiverKind(ref: UnresolvedRef, context: ResolutionContext): 'bare
|
||||
// `cls.` receiver too, and there it is a variable (protobuf's
|
||||
// `parent.getFullName()` on the EnumDescriptor an enum value belongs to).
|
||||
if (/(?:^|[^\w$.])super(?:\s*\([^()]*\)|<[^<>]*>)?\s*\.\s*$/.test(before)) return 'super';
|
||||
if (/(?:^|[^\w$.])(?:this|self)\s*\??\.\s*$/.test(before)) return 'self';
|
||||
if (/(?:^|[^\w$.])this\s*\??\.\s*$/.test(before)) return 'self';
|
||||
// A JS `self` the file doesn't bind is the global object, not the instance.
|
||||
if (/(?:^|[^\w$.])self\s*\??\.\s*$/.test(before)) return isBrowserHostGlobal('self', ref, context) ? null : 'self';
|
||||
if (ref.language === 'python' && /(?:^|[^\w$.])cls\s*\.\s*$/.test(before)) return 'self';
|
||||
if (/(?:\.|->|::)\s*$/.test(before)) return null;
|
||||
return 'bare';
|
||||
@@ -13819,9 +13881,12 @@ const HOST_GLOBAL_RECEIVER =/(?:^|[^\w$.])(window|globalThis)\s*\??\.\s*([A-Za-z
|
||||
* name as the project-global escape (#1707) — and `this.render()` inside an
|
||||
* object literal's member. Both name the owner of the member they call, which
|
||||
* is tried first. Undefined when the call is neither, or no such owner has the
|
||||
* member (the bare-name strategies then run as before).
|
||||
* member (the bare-name strategies then run as before). Null when the object
|
||||
* is one of the browser's own: a global `self.close()`, or a `window` chain
|
||||
* that starts at `localStorage`, `history` or another host object, like
|
||||
* `window.localStorage.getItem()` — no project code is that object (#2907).
|
||||
*/
|
||||
export function matchCollapsedObjectCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | undefined {
|
||||
export function matchCollapsedObjectCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
|
||||
if (ref.referenceKind !== 'calls' || !JS_FAMILY.has(ref.language) || !/^[A-Za-z_$][\w$]*$/.test(ref.referenceName)) return undefined;
|
||||
// A call written bare starts with its own name and `(`: no receiver to read.
|
||||
const line = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1];
|
||||
@@ -13840,9 +13905,13 @@ export function matchCollapsedObjectCall(ref: UnresolvedRef, context: Resolution
|
||||
const hit = owner ? resolveObjectLiteralMember(owner, ref.referenceName, ref, context, 0.85, 'instance-method') : null;
|
||||
return hit ?? undefined;
|
||||
}
|
||||
// A global `self` arrives as `globalThis.self` (bareCallReceiver).
|
||||
const host = HOST_GLOBAL_RECEIVER.exec(written.receiver);
|
||||
if (!host) return undefined;
|
||||
return resolveObjectPathMember(host[2]!.replace(/[\s?]/g, ''), ref.referenceName, ref, context, host[1]!) ?? undefined;
|
||||
const path = host[2]!.replace(/[\s?]/g, '');
|
||||
const hit = resolveObjectPathMember(path, ref.referenceName, ref, context, host[1]!);
|
||||
if (hit) return hit;
|
||||
return JS_BROWSER_HOST_GLOBALS.has(path.split('.')[0]!) ? null : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21496,6 +21565,14 @@ export function matchMethodCall(
|
||||
|
||||
const [, objectOrClass, methodName] = match;
|
||||
|
||||
// A call on one of the browser's own objects — `window.open()`,
|
||||
// `localStorage.getItem()`, `location.reload()`, `history.back()` — that
|
||||
// the file neither binds nor imports is the browser's: no project method is
|
||||
// it, whatever the method's name or a class's resemblance to the receiver
|
||||
// (#2907). Ahead of the receiver-type reader, whose backward scan has no
|
||||
// scope and would take a `history: X` member line for this name.
|
||||
if (ref.referenceKind === 'calls' && dotMatch && isBrowserHostGlobal(objectOrClass!, ref, context)) return null;
|
||||
|
||||
// A JS/TS call of a member no node in the index is named — `list.push()`,
|
||||
// `fs.writeFileSync()`, a test library's matcher — is one only the
|
||||
// object-literal reader below can link (#2829). It follows a member to the
|
||||
@@ -29445,9 +29522,10 @@ function matchReferenceInner(
|
||||
if (isUnresolvedJsMemberCall(ref)) return matchObjectPathCall(ref, context);
|
||||
|
||||
// `window.App.init()` / a sibling's `this.init()`, recorded by the bare
|
||||
// name: the object they are written on comes first (#2300).
|
||||
// name: the object they are written on comes first (#2300). One of the
|
||||
// browser's own objects is the last word (#2907).
|
||||
const collapsed = matchCollapsedObjectCall(ref, context);
|
||||
if (collapsed) return collapsed;
|
||||
if (collapsed !== undefined) return collapsed;
|
||||
|
||||
// A Swift call through a type path (`API.PackageController.GetRoute.query`)
|
||||
// resolves on the type the path names, or not at all: the strategies below
|
||||
|
||||
Reference in New Issue
Block a user