mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
fix(react): a type argument or a name the component binds itself renders no JSX child (#2442)
The jsx-render pass reads tag names off a parent's source with a pattern,
and two kinds of name it read that way linked a same-named class or
component elsewhere:
- A name written only in a type argument or type parameter list:
outline's `<PaginatedList<Document> …>` and `useState<User>()`, or
bulletproof-react's generic `<Entry extends BaseEntity>(…) =>`. A `<Name`
right after an identifier opens a type argument list (the rule #2420
applies to the values a file declares, here for every name), and a
`<Name extends …` opens a type parameter list.
- A name the parent binds itself by its first tag, a `const`/`let`/`var` or
a parameter (name-matcher's `jsCodeBindsName`, now exported): outline's
`const Content = variant === "dropdown" ? DropdownMenu.SubContent :
ContextMenu.SubContent`, `(Widget, index) => <Widget />`. Such a tag
renders only a component the parent declares inside itself, else
nothing. The check runs only for names the parent also writes outside a
tag. Destructured names still link by name, as for a destructured call.
Fresh before/after indexes on 13 repos (main ed199e60): 66 jsx-render
edges removed and none added; no other edge, node, ref or file changes.
Every removal checks out against the TypeScript compiler (38 names with
no JSX tag in the parent, 28 tags bound to a local); 64 of the 66 targets
were wrong, 2 right by luck. mantis, proshop_mern, next-saas-starter and
create-t3-turbo are byte-identical, and Screens is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Go, a call or type written through an import whose path ends in a version, or in something other than the package's name, is now known to go through that import: `yaml.Unmarshal(…)` after `import "go.yaml.in/yaml/v3"` or `"gopkg.in/yaml.v3"`, `sqlite3.Error` after `import "github.com/mattn/go-sqlite3"`, `klog.V(2)` after `import "k8s.io/klog/v2"`. Before, only the last part of the path named such an import (`v3`, `yaml.v3`, `go-sqlite3`), so the name was matched on its own and could link to any project function, method or type that shared it: kubernetes' `klog.V(…)` calls were linked to a logging wrapper's `V` method, and etcd's `semver.Version` parameters to an unrelated `Version` function. A name from another module now links to nothing, and one through a package of your own project, like `kit.New()` after `import "example.com/kit/v2"`, links to that package's symbol. A comment in an import block is also no longer taken for the name of the import after it. Re-index Go projects after upgrading. Thanks @danusha2345 for the comment fix. (#2374)
|
||||
- React Router routes written as JSX now read `<Route index>` the way route objects do: inside `<Route path="/" element={<Layout />}>`, `<Route index element={<Home />} />` is the page at `/`, and `Layout` counts as the layout of every route nested in it, as does a guard with no path of its own like `<Route element={<RequireAuth />}>`. Before, the index route was never read, so `/` led to the layout itself and a `<Route path="/dashboard">` holding only an index route led to nothing. A route's `element` written on lines of its own, the way Prettier formats a long one, or wrapped in `<Suspense fallback={<Loader />}>`, now links to the page it shows rather than to nothing or to the fallback, and a path written in braces, like `path={"agents"}`, is read like any other. Re-index React projects after upgrading.
|
||||
- In JavaScript and TypeScript, an import written `import { default as AppRoot } from './routes/app/root'`, as in bulletproof-react's router, now counts as the module's default import, the same as `import AppRoot from './routes/app/root'`. Before, it was read as an import of an export named `default`, which no module has, so a call, a route or a JSX attribute that used `AppRoot` was matched by its name alone: it could link to another file's `AppRoot`, or to nothing when the default export has a name of its own. Svelte, Vue and Astro script blocks are read the same way.
|
||||
- In React, a component's JSX no longer links it to a class or component that only shares a name with one of its type arguments or with a variable of its own. A type in angle brackets, like `Document` in `<PaginatedList<Document> items={…} />`, `User` in `useState<User>()` or `Entry` in a generic `<Entry extends BaseEntity>(…) =>` component, is no longer read as a tag. A tag naming a variable or parameter the component sets itself, like `<Content>` after `const Content = isDropdown ? DropdownMenu.SubContent : ContextMenu.SubContent` or `<Widget>` in `widgets.map((Widget) => <Widget />)`, now links to nothing, unless the component declares a component of that name inside itself. Before, these linked to an unrelated class or component elsewhere in the repository: outline's document lists showed up among the callers of its `Document` model class, and its menus among the callers of the command bar's `Content`, so `codegraph_explore`, callers and impact followed renders that never happen. Re-index React projects after upgrading.
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
|
||||
@@ -129,3 +129,212 @@ export function Header() { return <h1><Badge /></h1>; }
|
||||
expect(rendersFrom(db, 'Header')).toEqual(['only/badge.tsx']);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The pass reads tag names off a parent's source with a pattern, and two
|
||||
* kinds of name it reads that way are no other file's component.
|
||||
*
|
||||
* - A type argument: `<PaginatedList<Document> …>` or `useState<User>()`
|
||||
* writes a type's name right after another name. outline's document list
|
||||
* rendered its `Document` model class, its settings tables the `User` and
|
||||
* `Group` classes.
|
||||
* - A name the parent binds itself, as a `const` or a parameter, is that local
|
||||
* wherever its tags sit. outline's menus pick a Radix part into `const
|
||||
* Content = variant === 'dropdown' ? DropdownMenu.SubContent :
|
||||
* ContextMenu.SubContent` and rendered the command bar's styled `Content`
|
||||
* instead. A local is no node, so it renders nothing, unless the parent
|
||||
* declares a component of that name inside itself.
|
||||
*/
|
||||
describe('JSX child: a name the parent writes only as a type, or binds itself', () => {
|
||||
let dir: string;
|
||||
let cg: any;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsx-local-'));
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), '{"dependencies":{"react":"^18.0.0"}}');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cg?.close?.();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const write = (rel: string, body: string) => {
|
||||
const p = path.join(dir, rel);
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.writeFileSync(p, body);
|
||||
};
|
||||
|
||||
async function index(): Promise<void> {
|
||||
cg = await CodeGraph.init(dir, { silent: true });
|
||||
await cg.indexAll();
|
||||
}
|
||||
|
||||
/** `name file:line` of each node a jsx-render edge out of `parent` points at. */
|
||||
const renders = (parent: string): string[] =>
|
||||
cg.db.db
|
||||
.prepare(
|
||||
`SELECT t.name || ' ' || t.file_path || ':' || t.start_line AS r FROM edges e
|
||||
JOIN nodes s ON s.id = e.source
|
||||
JOIN nodes t ON t.id = e.target
|
||||
WHERE s.name = ? AND json_extract(e.metadata, '$.synthesizedBy') = 'jsx-render'
|
||||
ORDER BY r`
|
||||
)
|
||||
.all(parent)
|
||||
.map((row: any) => row.r);
|
||||
|
||||
it('renders nothing for a name written only as a type argument or parameter', async () => {
|
||||
write('app/models/Document.ts', 'export default class Document {\n title = "";\n}\n');
|
||||
write('app/models/User.ts', 'export default class User {\n name = "";\n}\n');
|
||||
write(
|
||||
'app/components/PaginatedList.tsx',
|
||||
'export function PaginatedList<T>({ items }: { items: T[] }) {\n return <ul>{items.length}</ul>;\n}\n'
|
||||
);
|
||||
write(
|
||||
'app/components/Badge.tsx',
|
||||
'import * as React from "react";\nexport class Badge extends React.Component {\n render() {\n return <b />;\n }\n}\n'
|
||||
);
|
||||
write(
|
||||
'app/components/DocumentList.tsx',
|
||||
`import * as React from 'react';
|
||||
import Document from '../models/Document';
|
||||
import User from '../models/User';
|
||||
import { Badge } from './Badge';
|
||||
import { PaginatedList } from './PaginatedList';
|
||||
|
||||
export function DocumentList({ items }: { items: Document[] }) {
|
||||
const [owner] = React.useState<User | null>(null);
|
||||
const badge = React.useRef<Badge>(null);
|
||||
return (
|
||||
<div>
|
||||
<PaginatedList<Document> items={items} />
|
||||
<Badge ref={badge} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`
|
||||
);
|
||||
// bulletproof-react's table: a generic arrow function's type parameter.
|
||||
write('app/profile/entry.tsx', 'export function Entry() {\n return <div />;\n}\n');
|
||||
write(
|
||||
'app/components/Table.tsx',
|
||||
`type BaseEntity = { id: string };
|
||||
|
||||
export const Table = <Entry extends BaseEntity>({ data }: { data: Entry[] }) => {
|
||||
return <table>{data.length}</table>;
|
||||
};
|
||||
`
|
||||
);
|
||||
await index();
|
||||
// `Badge` is written as a type argument too, but a tag of it is enough.
|
||||
// A generic tag's own name is not read at all: no tag ends after
|
||||
// `<PaginatedList` (its type arguments follow).
|
||||
expect(renders('DocumentList')).toEqual(['Badge app/components/Badge.tsx:2']);
|
||||
expect(renders('Table')).toEqual([]);
|
||||
});
|
||||
|
||||
it('renders nothing for a name the parent binds itself as a const or a parameter', async () => {
|
||||
write(
|
||||
'app/components/CommandBarItem.tsx',
|
||||
'export function Content() {\n return <div />;\n}\n\nexport function Icon() {\n return <svg />;\n}\n'
|
||||
);
|
||||
write('shared/editor/Widget.tsx', 'export function Widget() {\n return <div />;\n}\n');
|
||||
write('app/components/Portal.tsx', 'export function Portal({ children }) {\n return <div>{children}</div>;\n}\n');
|
||||
write(
|
||||
'app/components/Menu.tsx',
|
||||
`import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import * as ContextMenu from '@radix-ui/react-context-menu';
|
||||
|
||||
const Portal = DropdownMenu.Portal;
|
||||
|
||||
export function SubMenuContent({ variant, children }) {
|
||||
const Portal =
|
||||
variant === 'dropdown' ? DropdownMenu.Portal : ContextMenu.Portal;
|
||||
const Content =
|
||||
variant === 'dropdown'
|
||||
? DropdownMenu.SubContent
|
||||
: ContextMenu.SubContent;
|
||||
return (
|
||||
<Portal>
|
||||
<Content collisionPadding={6}>{children}</Content>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsLinks({ config }) {
|
||||
return (
|
||||
<ul>
|
||||
{config.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return <li key={item.path}><Icon /></li>;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function Widgets({ widgets }) {
|
||||
return <div>{Object.values(widgets).map((Widget, index) => <Widget key={index} />)}</div>;
|
||||
}
|
||||
`
|
||||
);
|
||||
await index();
|
||||
// The file's own `Portal` is shadowed too: the tag means the local.
|
||||
expect(renders('SubMenuContent')).toEqual([]);
|
||||
expect(renders('SettingsLinks')).toEqual([]);
|
||||
expect(renders('Widgets')).toEqual([]);
|
||||
});
|
||||
|
||||
it('renders a component the parent declares inside itself, and the outer one for a tag above the local', async () => {
|
||||
write('a-decoy/row.tsx', 'export function Row() {\n return <tr />;\n}\n');
|
||||
write('app/icon.tsx', 'export function Icon() {\n return <svg />;\n}\n');
|
||||
write(
|
||||
'app/table.tsx',
|
||||
`import { Icon } from './icon';
|
||||
|
||||
function Row() {
|
||||
return <tr className="outer" />;
|
||||
}
|
||||
|
||||
export function Table({ rows }) {
|
||||
const Row = ({ row }) => <tr>{row.name}</tr>;
|
||||
return <table>{rows.map((row) => <Row key={row.id} row={row} />)}</table>;
|
||||
}
|
||||
|
||||
export function Toolbar({ actions }) {
|
||||
return (
|
||||
<div>
|
||||
<Icon />
|
||||
{actions.map((action) => {
|
||||
const Icon = action.icon;
|
||||
return <Icon key={action.id} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`
|
||||
);
|
||||
await index();
|
||||
expect(renders('Table')).toEqual(['Row app/table.tsx:8']);
|
||||
expect(renders('Toolbar')).toEqual(['Icon app/icon.tsx:1']);
|
||||
});
|
||||
|
||||
it('keeps a destructured name on the same-named component, as a destructured call does', async () => {
|
||||
// excalidraw's example app: `const { Sidebar, Footer, … } = excalidrawLib`.
|
||||
// Destructuring is not the parent's own binding here, as it is not for a
|
||||
// bare call (`const { t } = useI18n()`), so the name links by name.
|
||||
write(
|
||||
'packages/excalidraw/components/WelcomeScreen.tsx',
|
||||
'export default function WelcomeScreen() {\n return <div />;\n}\n'
|
||||
);
|
||||
write(
|
||||
'examples/app/ExampleApp.tsx',
|
||||
`export default function ExampleApp({ excalidrawLib }) {
|
||||
const { WelcomeScreen, exportToSvg } = excalidrawLib;
|
||||
return <div><WelcomeScreen /></div>;
|
||||
}
|
||||
`
|
||||
);
|
||||
await index();
|
||||
expect(renders('ExampleApp')).toEqual(['WelcomeScreen packages/excalidraw/components/WelcomeScreen.tsx:1']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ import { createYielder, type MaybeYield } from './cooperative-yield';
|
||||
import { crossTierEdges, hasCrossTierPattern, hasTestRequestPattern, testRequestEdges } from './tier-synthesizer';
|
||||
import { enclosingFn, makeLineAt } from './synth-utils';
|
||||
import { resolveImportPath } from './import-resolver';
|
||||
import { crossesCodeBoundary } from './name-matcher';
|
||||
import { crossesCodeBoundary, jsCodeBindsName } from './name-matcher';
|
||||
|
||||
const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
|
||||
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
|
||||
@@ -1584,14 +1584,62 @@ function jsxChild(
|
||||
return candidates.find((n) => JSX_CHILD_LANGUAGES.includes(n.language)) ?? candidates[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the `<Name` at `at` opens a JSX tag. A type argument list follows
|
||||
* its type's name directly — `useState<User>()`, `Array<Item>`, the
|
||||
* `<Document>` of `<PaginatedList<Document>` — and a tag never does, short of
|
||||
* a `return` written up against it. A generic arrow function's type
|
||||
* parameters read like a tag but constrain theirs: `<Entry extends
|
||||
* BaseEntity>({ data }) =>`.
|
||||
*/
|
||||
function opensTag(src: string, at: number, name: string): boolean {
|
||||
const end = at + 1 + name.length;
|
||||
if (/^\s+extends\s/.test(src.slice(end, end + 40))) return false;
|
||||
return !/[\w$]/.test(src[at - 1] ?? '') || /\breturn$/.test(src.slice(Math.max(0, at - 7), at));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `src` writes `name` other than in a tag (`<Name`, `</Name`): a
|
||||
* parent binds a name only where it writes it bare (`const Content =`,
|
||||
* `(Widget) =>`), so the check for that runs only on these.
|
||||
*/
|
||||
function writtenBare(src: string, name: string): boolean {
|
||||
for (let at = src.indexOf(name); at !== -1; at = src.indexOf(name, at + name.length)) {
|
||||
if (/[\w$]/.test(src[at - 1] ?? '') || /[\w$]/.test(src[at + name.length] ?? '')) continue;
|
||||
if (src[at - 1] === '<' || (src[at - 1] === '/' && src[at - 2] === '<')) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a tag renders when its parent binds the name itself: a component the
|
||||
* parent declares in its own lines (`const Row = ({ row }) => <tr />` in a
|
||||
* table's body), else nothing. Any other local is no node, and a same-named
|
||||
* component elsewhere is only a guess at what it holds.
|
||||
*/
|
||||
function declaredInside(ctx: ResolutionContext, name: string, parent: Node, child: Node): Node | undefined {
|
||||
const inside = (n: Node) => n.id !== parent.id && n.filePath === parent.filePath &&
|
||||
n.startLine >= parent.startLine && n.startLine <= parent.endLine;
|
||||
if (inside(child)) return child;
|
||||
return ctx.getNodesByName(name).find((n) => JSX_CHILD_KINDS.has(n.kind) && inside(n));
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 5: React JSX child rendering. A component that returns `<Child .../>`
|
||||
* mounts Child — React calls it — but JSX instantiation isn't a static call edge,
|
||||
* so a render tree (App.render → StaticCanvas → renderStaticScene) breaks at the
|
||||
* JSX hop. Link parent → each capitalized JSX child it renders. File-oriented
|
||||
* (read each JSX file once). Precision gate: the child name must resolve to a
|
||||
* component/function/class node — TS generics like `Array<Foo>` resolve to a type
|
||||
* (or nothing) and are dropped.
|
||||
* (read each JSX file once). Precision gates: the parent must write the name as
|
||||
* a tag, not only in a type argument or parameter list (`useState<User>()`,
|
||||
* `<Entry extends BaseEntity>(…) =>`; outline's `<PaginatedList<Document>`
|
||||
* read its `Document` model class as a child), and the name must resolve to a
|
||||
* component/function/class node. A name the parent binds itself by its first
|
||||
* tag — a parameter, or a `const` like outline's `const Content = variant ===
|
||||
* "dropdown" ? DropdownMenu.SubContent : ContextMenu.SubContent` — is that
|
||||
* local, and renders only a component declared inside the parent
|
||||
* (`declaredInside`). A destructured name still links by name, as a
|
||||
* destructured function call does.
|
||||
*/
|
||||
async function reactJsxChildEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
|
||||
let scannedFiles = 0;
|
||||
@@ -1620,26 +1668,41 @@ async function reactJsxChildEdges(ctx: ResolutionContext, onYield: MaybeYield):
|
||||
// over go-ethereum's graphiql.min.js (2,234 functions on 980 KB). The
|
||||
// file is split once, not once per function.
|
||||
let lines: string[] | null = null;
|
||||
const tagsBySpan = new Map<string, Set<string>>();
|
||||
// Each name a span holds, and the line of its first tag: 0 while only type
|
||||
// argument or parameter lists name it (`opensTag`). `bare` holds the
|
||||
// names it also writes outside a tag (`writtenBare`).
|
||||
const tagsBySpan = new Map<string, { tags: Map<string, number>; bare: Set<string> }>();
|
||||
for (const parent of parents) {
|
||||
if (!parent.startLine || !parent.endLine) continue;
|
||||
const span = `${parent.startLine}:${parent.endLine}`;
|
||||
let names = tagsBySpan.get(span);
|
||||
if (!names) {
|
||||
names = new Set<string>();
|
||||
names = { tags: new Map<string, number>(), bare: new Set<string>() };
|
||||
lines ??= content.split('\n');
|
||||
const src = lines.slice(parent.startLine - 1, parent.endLine).join('\n');
|
||||
if (src.includes('</') || src.includes('/>')) {
|
||||
let line = parent.startLine;
|
||||
let counted = 0;
|
||||
JSX_TAG_RE.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = JSX_TAG_RE.exec(src))) names.add(m[1]!);
|
||||
while ((m = JSX_TAG_RE.exec(src))) {
|
||||
for (let nl = src.indexOf('\n', counted); nl !== -1 && nl < m.index; nl = src.indexOf('\n', nl + 1)) line++;
|
||||
counted = m.index;
|
||||
if (!names.tags.get(m[1]!)) names.tags.set(m[1]!, opensTag(src, m.index, m[1]!) ? line : 0);
|
||||
}
|
||||
for (const [name, first] of names.tags) if (first && writtenBare(src, name)) names.bare.add(name);
|
||||
}
|
||||
tagsBySpan.set(span, names);
|
||||
}
|
||||
let added = 0;
|
||||
for (const name of names) {
|
||||
for (const [name, line] of names.tags) {
|
||||
if (added >= MAX_JSX_CHILDREN) break;
|
||||
const child = jsxChild(ctx, name, file, importsOf);
|
||||
if (!line) continue;
|
||||
let child = jsxChild(ctx, name, file, importsOf);
|
||||
// A name the parent binds itself by its first tag is that local.
|
||||
if (child && names.bare.has(name) && jsCodeBindsName(name, parent, file, line, ctx)) {
|
||||
child = declaredInside(ctx, name, parent, child);
|
||||
}
|
||||
if (!child || child.id === parent.id || crossesCodeBoundary(parent.language, child.language)) continue;
|
||||
const key = `${parent.id}>${child.id}`;
|
||||
if (seen.has(key)) continue;
|
||||
|
||||
@@ -6857,7 +6857,7 @@ function jsFunctionLocalScope(name: string, ref: UnresolvedRef, context: Resolut
|
||||
let scope: { start: number; end: number } | null = null;
|
||||
const fn = context.getNodeById?.(ref.fromNodeId);
|
||||
if (fn && (fn.kind === 'function' || fn.kind === 'method') && fn.startLine <= ref.line && fn.endLine >= ref.line &&
|
||||
jsCodeBindsName(name, fn, ref, context)) {
|
||||
jsCodeBindsName(name, fn, ref.filePath, ref.line, context)) {
|
||||
scope = { start: fn.startLine, end: fn.endLine };
|
||||
}
|
||||
memo.set(key, scope);
|
||||
@@ -6872,14 +6872,18 @@ function jsFunctionLocalScope(name: string, ref: UnresolvedRef, context: Resolut
|
||||
*/
|
||||
const JS_CODE_BINDS = new WeakMap<ResolutionContext, Map<string, boolean>>();
|
||||
|
||||
/** Whether the function's code through the reference's line declares `name`, or names it in a parameter list. */
|
||||
function jsCodeBindsName(name: string, fn: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
/**
|
||||
* Whether the function's code in `filePath`, from its first line through
|
||||
* `line` (a reference's, or a JSX tag's), declares `name`, or names it in a
|
||||
* parameter list.
|
||||
*/
|
||||
export function jsCodeBindsName(name: string, fn: Node, filePath: string, line: number, context: ResolutionContext): boolean {
|
||||
let memo = JS_CODE_BINDS.get(context);
|
||||
if (!memo) JS_CODE_BINDS.set(context, (memo = new Map()));
|
||||
const key = `${ref.filePath}\0${fn.startLine}\0${ref.line}\0${name}`;
|
||||
const key = `${filePath}\0${fn.startLine}\0${line}\0${name}`;
|
||||
let binds = memo.get(key);
|
||||
if (binds === undefined) {
|
||||
const text = jsFunctionCodeThrough(fn, ref, context);
|
||||
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
|
||||
@@ -6904,38 +6908,38 @@ function jsCodeBindsName(name: string, fn: Node, ref: UnresolvedRef, context: Re
|
||||
}
|
||||
|
||||
/**
|
||||
* Per context: the comment-stripped lines of the functions jsFunctionLocalScope
|
||||
* Per context: the comment-stripped lines of the functions jsCodeBindsName
|
||||
* read last, and where each line ends in that text, most recent last.
|
||||
*/
|
||||
const JS_FN_CODE = new WeakMap<ResolutionContext, Map<string, { code: string; lineEnds: number[] }>>();
|
||||
const JS_FN_CODE_KEEP = 64;
|
||||
|
||||
/**
|
||||
* The function's lines from its first through the reference's, comments
|
||||
* blanked. Stripping looks at most one character ahead — past a line's end,
|
||||
* a newline, which completes no comment marker — so this is the whole
|
||||
* The function's lines from its first through `line`, comments blanked.
|
||||
* Stripping looks at most one character ahead — past a line's end, a
|
||||
* newline, which completes no comment marker — so this is the whole
|
||||
* function's stripped text cut at that line's end. Stripping the lines again
|
||||
* for every reference took time in the square of a large function's length;
|
||||
* in a minified script, every function's text runs to the end of its one
|
||||
* line (#2334).
|
||||
*/
|
||||
function jsFunctionCodeThrough(fn: Node, ref: UnresolvedRef, context: ResolutionContext): string {
|
||||
function jsFunctionCodeThrough(fn: Node, filePath: string, line: number, context: ResolutionContext): string {
|
||||
let fns = JS_FN_CODE.get(context);
|
||||
if (!fns) JS_FN_CODE.set(context, (fns = new Map()));
|
||||
const key = `${ref.filePath}\0${fn.startLine}\0${fn.endLine}`;
|
||||
const key = `${filePath}\0${fn.startLine}\0${fn.endLine}`;
|
||||
let own = fns.get(key);
|
||||
if (own) {
|
||||
fns.delete(key);
|
||||
} else {
|
||||
const lines = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [])
|
||||
const lines = (context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? [])
|
||||
.slice(fn.startLine - 1, fn.endLine);
|
||||
let end = -1;
|
||||
own = { code: stripCommentsForRegex(lines.join('\n'), 'javascript'), lineEnds: lines.map((line) => (end += line.length + 1)) };
|
||||
own = { code: stripCommentsForRegex(lines.join('\n'), 'javascript'), lineEnds: lines.map((text) => (end += text.length + 1)) };
|
||||
if (fns.size >= JS_FN_CODE_KEEP) fns.delete(fns.keys().next().value!);
|
||||
}
|
||||
fns.set(key, own);
|
||||
// Through the reference's line, or the file's last when it ends sooner.
|
||||
const last = Math.min(ref.line, fn.startLine - 1 + own.lineEnds.length) - fn.startLine;
|
||||
// Through `line`, or the file's last when it ends sooner.
|
||||
const last = Math.min(line, fn.startLine - 1 + own.lineEnds.length) - fn.startLine;
|
||||
return last < 0 ? '' : own.code.slice(0, own.lineEnds[last]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user