mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(js,ts): of several export default lines, the one that names a node is the module's
A template can hold statement-shaped text: react.dev's SandpackWithHTMLOutput.tsx writes two sandbox files' `export default function` lines above its own `export default memo(function ...)`, and reading only the first line left the module's default import with nothing. Every line-initial statement outside a block comment is now read, the first that names a node wins, and a function or class a statement declares binds only the node that starts there, so a template's `export default function formatHTML` can't reach the file's real formatHTML. The block-comment check reads the file once for all of its statements. Carries #2432's default-export tests unchanged (all pass), and adapts its multi-statement and declaration-position rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* A default import is the declaration the module's `export default` statement
|
||||
* names, wherever it sits in the file — not the first exported function.
|
||||
*
|
||||
* A React Router 6.4+ data-router page exports its loader or action above the
|
||||
* page component (`export function loader() {…}` then `export default function
|
||||
* Vans() {…}`), and `import Vans, { loader as vansLoader } from './Vans'` bound
|
||||
* `Vans` to `loader`: the route `/vans` rendered the loader, and every call
|
||||
* through the default import went there too. A component file that exports a
|
||||
* styled or memo component lost its default function or binding to that
|
||||
* component, and an anonymous default (`export default function () {…}`,
|
||||
* `export default () => …`) took whatever exported function came first — even
|
||||
* one nested inside the default itself.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
const files: Record<string, string> = {
|
||||
'package.json': JSON.stringify({ name: 'vanlife', private: true, dependencies: { react: '^18', 'react-router-dom': '^6.4' } }),
|
||||
'src/index.jsx': `import { RouterProvider, createBrowserRouter, createRoutesFromElements, Route } from "react-router-dom"
|
||||
import Vans, { loader as vansLoader } from "./pages/Vans/Vans"
|
||||
import HostVans, { loader as hostVansLoader } from "./pages/Host/HostVans"
|
||||
import Login, { action as loginAction } from "./pages/Login"
|
||||
|
||||
const router = createBrowserRouter(createRoutesFromElements(
|
||||
<Route path="/">
|
||||
<Route path="login" element={<Login />} action={loginAction} />
|
||||
<Route path="vans" element={<Vans />} loader={vansLoader} />
|
||||
<Route path="host/vans" element={<HostVans />} loader={hostVansLoader} />
|
||||
</Route>
|
||||
))
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
`,
|
||||
'src/api.js': `export function getVans() {
|
||||
return fetch("/api/vans")
|
||||
}
|
||||
`,
|
||||
'src/pages/Vans/Vans.jsx': `import { useLoaderData, defer } from "react-router-dom"
|
||||
import { getVans } from "../../api"
|
||||
|
||||
export function loader() {
|
||||
return defer({ vans: getVans() })
|
||||
}
|
||||
|
||||
export default function Vans() {
|
||||
const dataPromise = useLoaderData()
|
||||
function renderVanElements(vans) {
|
||||
return vans.map((van) => <h3 key={van.id}>{van.name}</h3>)
|
||||
}
|
||||
return <div>{renderVanElements(dataPromise.vans)}</div>
|
||||
}
|
||||
`,
|
||||
'src/pages/Host/HostVans.jsx': `import { useLoaderData } from "react-router-dom"
|
||||
import { getVans } from "../../api"
|
||||
|
||||
export const loader = async () => getVans()
|
||||
|
||||
export default function HostVans() {
|
||||
return <ul>{useLoaderData().length}</ul>
|
||||
}
|
||||
`,
|
||||
'src/pages/Login.jsx': `import { useActionData } from "react-router-dom"
|
||||
|
||||
export async function action({ request }) {
|
||||
return request.formData()
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const data = useActionData()
|
||||
return <form>{data}</form>
|
||||
}
|
||||
`,
|
||||
// A Next.js API route: a helper exported above the default handler.
|
||||
'src/api/vans.ts': `export function listVans() {
|
||||
return []
|
||||
}
|
||||
|
||||
export default async function handler(req: unknown, res: { json(v: unknown): void }) {
|
||||
res.json(listVans())
|
||||
}
|
||||
`,
|
||||
'src/store.ts': `export function createStore() {
|
||||
return {}
|
||||
}
|
||||
|
||||
export default class Store {
|
||||
read() {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
`,
|
||||
// A regex with a backtick inside a template literal's interpolation
|
||||
// (outline's headingToSlug): a string masker loses step there and blanked
|
||||
// the statement below it.
|
||||
'src/slug.ts': `export function escapeHtml(text: string) {
|
||||
return text
|
||||
}
|
||||
|
||||
function safeSlugify(text: string) {
|
||||
return \`h-\${escapeHtml(text).replace(/[\`]/g, "")}\`
|
||||
}
|
||||
|
||||
export default function headingToSlug(text: string, index = 0) {
|
||||
return index === 0 ? safeSlugify(text) : \`\${safeSlugify(text)}-\${index}\`
|
||||
}
|
||||
`,
|
||||
// A line break between the statement and the declaration it writes.
|
||||
'src/stream.js': `export function emit() {
|
||||
return 1
|
||||
}
|
||||
|
||||
export default
|
||||
function* stream() {
|
||||
yield emit()
|
||||
}
|
||||
`,
|
||||
// An exported styled component above the default function, or the binding.
|
||||
'src/components/Title.tsx': `import styled from "styled-components"
|
||||
|
||||
export const Heading = styled.h1\`
|
||||
font-size: 2rem;
|
||||
\`
|
||||
|
||||
export default function Title() {
|
||||
return <Heading>Vans</Heading>
|
||||
}
|
||||
`,
|
||||
'src/components/Banner.tsx': `import styled from "styled-components"
|
||||
|
||||
export const Strip = styled.div\`
|
||||
display: flex;
|
||||
\`
|
||||
|
||||
const Banner = () => <Strip />
|
||||
|
||||
export default Banner
|
||||
`,
|
||||
// Anonymous defaults name no declaration of their own.
|
||||
'src/anonymous.js': `export function helper() {
|
||||
return 1
|
||||
}
|
||||
|
||||
export default function () {
|
||||
function inner() {
|
||||
return helper()
|
||||
}
|
||||
return inner()
|
||||
}
|
||||
`,
|
||||
'src/arrow.jsx': `export function useThing() {
|
||||
return 1
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const handle = () => useThing()
|
||||
return <button onClick={handle} />
|
||||
}
|
||||
`,
|
||||
// An expression default keeps the class it instantiates.
|
||||
'src/service.ts': `export class Service {
|
||||
run() {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service()
|
||||
`,
|
||||
'src/use.tsx': `import handler from "./api/vans"
|
||||
import Store from "./store"
|
||||
import headingToSlug from "./slug"
|
||||
import stream from "./stream"
|
||||
import Title from "./components/Title"
|
||||
import Banner from "./components/Banner"
|
||||
import runAnonymous from "./anonymous"
|
||||
import Arrow from "./arrow"
|
||||
import service from "./service"
|
||||
|
||||
export function consume() {
|
||||
handler({}, { json() {} })
|
||||
new Store().read()
|
||||
headingToSlug("Vans")
|
||||
stream()
|
||||
Title()
|
||||
Banner()
|
||||
runAnonymous()
|
||||
Arrow()
|
||||
service.run()
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-default-export-'));
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** `file:name` of every node `consume` calls or instantiates. */
|
||||
function consumed(): string[] {
|
||||
const consume = cg.getNodesInFile('src/use.tsx').find((n) => n.name === 'consume')!;
|
||||
return cg
|
||||
.getOutgoingEdges(consume.id)
|
||||
.filter((e) => e.kind === 'calls' || e.kind === 'instantiates')
|
||||
.map((e) => cg.getNode(e.target)!)
|
||||
.map((n) => `${n.filePath}:${n.name}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('a default import of a module that exports something above its default', () => {
|
||||
it('binds each data-router route to the page component, not its loader or action', () => {
|
||||
const bindings = cg
|
||||
.getNodesByKind('route')
|
||||
.flatMap((r) =>
|
||||
cg
|
||||
.getOutgoingEdges(r.id)
|
||||
.filter((e) => e.kind === 'references')
|
||||
.map((e) => `${r.name} -> ${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`)
|
||||
)
|
||||
.sort();
|
||||
expect(bindings).toEqual([
|
||||
'/host/vans -> src/pages/Host/HostVans.jsx:HostVans',
|
||||
'/login -> src/pages/Login.jsx:Login',
|
||||
'/vans -> src/pages/Vans/Vans.jsx:Vans',
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls the default-exported function or class the statement declares', () => {
|
||||
const targets = consumed();
|
||||
expect(targets).toContain('src/api/vans.ts:handler');
|
||||
expect(targets).toContain('src/store.ts:Store');
|
||||
expect(targets).toContain('src/slug.ts:headingToSlug');
|
||||
expect(targets).toContain('src/stream.js:stream');
|
||||
expect(targets).not.toContain('src/api/vans.ts:listVans');
|
||||
expect(targets).not.toContain('src/store.ts:createStore');
|
||||
expect(targets).not.toContain('src/slug.ts:escapeHtml');
|
||||
expect(targets).not.toContain('src/stream.js:emit');
|
||||
});
|
||||
|
||||
it('prefers what the statement exports to an exported component above it', () => {
|
||||
const targets = consumed();
|
||||
expect(targets).toContain('src/components/Title.tsx:Title');
|
||||
expect(targets).toContain('src/components/Banner.tsx:Banner');
|
||||
expect(targets).not.toContain('src/components/Title.tsx:Heading');
|
||||
expect(targets).not.toContain('src/components/Banner.tsx:Strip');
|
||||
});
|
||||
|
||||
it('binds an anonymous default to nothing, not to an exported function beside or inside it', () => {
|
||||
const targets = consumed();
|
||||
for (const wrong of ['src/anonymous.js:helper', 'src/anonymous.js:inner', 'src/arrow.jsx:useThing', 'src/arrow.jsx:handle']) {
|
||||
expect(targets).not.toContain(wrong);
|
||||
}
|
||||
});
|
||||
|
||||
it('still finds the class behind an expression default', () => {
|
||||
expect(consumed()).toContain('src/service.ts:run');
|
||||
});
|
||||
});
|
||||
@@ -152,6 +152,34 @@ export function App() { return <div><VansPage /><PriceCard /></div>; }
|
||||
expect(rendersFrom(db, 'App')).toEqual(['pages/card.tsx:Card', 'pages/vans.tsx:Vans']);
|
||||
});
|
||||
|
||||
it('reads the module’s own `export default` past ones a template writes as text', async () => {
|
||||
// react.dev's SandpackWithHTMLOutput.tsx: sandbox files' sources sit in
|
||||
// templates above the real statement, and one names a function the file
|
||||
// also declares, which only the statement that declares it may bind.
|
||||
write(
|
||||
'components/sandpack.tsx',
|
||||
`import { memo } from 'react';
|
||||
export function formatHTML(markup: string) { return markup.trim(); }
|
||||
const files = {
|
||||
'/App.js': \`
|
||||
export default function ShowRenderedHTML({children}) {
|
||||
return children;
|
||||
}
|
||||
\`,
|
||||
'/formatHTML.js': \`
|
||||
export default function formatHTML(markup) {
|
||||
return markup;
|
||||
}
|
||||
\`,
|
||||
};
|
||||
export default memo(function SandpackWithHTMLOutput() { return <pre>{Object.keys(files).join()}</pre>; });
|
||||
`
|
||||
);
|
||||
write('mdx.tsx', `import Sandpack from './components/sandpack';\nexport function MDX() { return <Sandpack />; }\n`);
|
||||
const db = await index();
|
||||
expect(rendersFrom(db, 'MDX')).toEqual(['components/sandpack.tsx:SandpackWithHTMLOutput']);
|
||||
});
|
||||
|
||||
it('renders nothing for a default export with no declaration of its own, not a same-named symbol elsewhere', async () => {
|
||||
write('app/header.tsx', `export function Header() { return <nav>app header</nav>; }\n`);
|
||||
write('emails/components/header.tsx', `export default () => <header>email header</header>;\n`);
|
||||
|
||||
@@ -95,11 +95,11 @@ interface FileExportIndex {
|
||||
/** First exported function or class: the guess for a module that never says. */
|
||||
defaultFnClass: Node | undefined;
|
||||
/**
|
||||
* What the module's `export default` statement says its default export is
|
||||
* (see {@link defaultStatement}). `undefined` until first read; `null` when
|
||||
* What the module's `export default` statements say its default export is
|
||||
* (see {@link defaultStatements}). `undefined` until first read; empty when
|
||||
* the module has no such statement or is not a JS-family file.
|
||||
*/
|
||||
defaultStatement?: DefaultStatement | null;
|
||||
defaultStatements?: DefaultStatement[];
|
||||
/**
|
||||
* Names a local export clause (`export { impl as alias }`) binds to a
|
||||
* declaration the extractor never flagged isExported, for names not in
|
||||
@@ -122,6 +122,10 @@ const NOT_A_BINDING = new Set(['new', 'await', 'typeof', 'void', 'delete', 'yiel
|
||||
interface NamedBinding {
|
||||
name: string;
|
||||
wrapped: boolean;
|
||||
/** For a function or class the expression declares itself: where its name is written in the code read. */
|
||||
nameAt?: number;
|
||||
/** For such a declaration in an `export default` statement: the lines its node starts on, the statement's to its name's. */
|
||||
declaredLines?: { from: number; to: number };
|
||||
}
|
||||
|
||||
/** A default export written as an expression with no name of its own, and where it starts. */
|
||||
@@ -168,7 +172,7 @@ function namedBindingAt(code: string, at: number, wrapped = false, depth = 0): N
|
||||
const declared = /^(?:async\s+)?function\b\s*\*?\s*([A-Za-z_$][\w$]*)?|^(?:abstract\s+)?class\b(?:\s+(?!extends\b|implements\b)([A-Za-z_$][\w$]*))?/.exec(head);
|
||||
if (declared) {
|
||||
const name = declared[1] ?? declared[2];
|
||||
return name ? { name, wrapped } : null;
|
||||
return name ? { name, wrapped, nameAt: at + declared[0].length - name.length } : null;
|
||||
}
|
||||
const constructed = /^new\s+([A-Za-z_$][\w$]*)\b(?!\s*\.)/.exec(head);
|
||||
if (constructed) return { name: constructed[1]!, wrapped: true };
|
||||
@@ -207,84 +211,106 @@ function namedBindingAt(code: string, at: number, wrapped = false, depth = 0): N
|
||||
}
|
||||
|
||||
/**
|
||||
* What a JS-family module's default export is, read once from the statement
|
||||
* that makes it: the binding it names — `export default Page`, `export
|
||||
* What a JS-family module's default export is, read once from the statements
|
||||
* that make it: the binding one names — `export default Page`, `export
|
||||
* default function Vans()`, `export default class Baz`, `export { Qux as
|
||||
* default }`, or what a wrapper call hands on (`export default memo(Foo)`) —
|
||||
* or, for an expression that names nothing (`export default () => …`), where
|
||||
* that expression starts. Null when the module has no such statement.
|
||||
* that expression starts. Every line-initial `export default` outside a block
|
||||
* comment is read, in order: a template's text can hold one too — react.dev's
|
||||
* `SandpackWithHTMLOutput.tsx` writes two sandbox files' `export default
|
||||
* function` lines above its own `export default memo(function …)` — and the
|
||||
* first that names a node is the module's (see the default branch of
|
||||
* {@link findExportedSymbolWalk}). Empty when the module has none.
|
||||
*/
|
||||
function defaultStatement(filePath: string, idx: FileExportIndex, context: ResolutionContext): DefaultStatement | null {
|
||||
if (idx.defaultStatement !== undefined) return idx.defaultStatement;
|
||||
let found: DefaultStatement | null = null;
|
||||
function defaultStatements(filePath: string, idx: FileExportIndex, context: ResolutionContext): DefaultStatement[] {
|
||||
if (idx.defaultStatements !== undefined) return idx.defaultStatements;
|
||||
const found: DefaultStatement[] = [];
|
||||
const source = JS_FAMILY_FILE.test(filePath) ? context.readFile(filePath) : null;
|
||||
if (source && source.includes('default')) {
|
||||
const at = exportDefaultAt(source);
|
||||
if (at >= 0) {
|
||||
let lineAt = 0;
|
||||
let line = 1;
|
||||
for (const at of exportDefaultStarts(source)) {
|
||||
for (let i = source.indexOf('\n', lineAt); i >= 0 && i < at; i = source.indexOf('\n', i + 1)) line++;
|
||||
lineAt = at;
|
||||
// The statement's own text is all it takes: what a wrapper call wraps
|
||||
// is its first argument, near the call's start.
|
||||
const code = stripCommentsForRegex(source.slice(at, at + DEFAULT_STATEMENT_WINDOW), 'typescript');
|
||||
const before = source.slice(0, at);
|
||||
found = namedBindingAt(code, 0) ?? {
|
||||
line: before.split('\n').length,
|
||||
column: at - before.lastIndexOf('\n') - 1,
|
||||
};
|
||||
} else {
|
||||
const binding = namedBindingAt(code, 0);
|
||||
if (binding?.nameAt !== undefined) {
|
||||
// A declaration's node starts on the statement's line, or on its
|
||||
// name's when the statement breaks before it.
|
||||
let to = line;
|
||||
for (let i = code.indexOf('\n'); i >= 0 && i < binding.nameAt; i = code.indexOf('\n', i + 1)) to++;
|
||||
binding.declaredLines = { from: line, to };
|
||||
}
|
||||
found.push(binding ?? { line, column: at - source.lastIndexOf('\n', at - 1) - 1 });
|
||||
}
|
||||
if (found.length === 0) {
|
||||
const clause = extractLocalExportAliases(source).find((a) => a.exportedName === 'default');
|
||||
if (clause) found = { name: clause.localName, wrapped: false };
|
||||
if (clause) found.push({ name: clause.localName, wrapped: false });
|
||||
}
|
||||
}
|
||||
idx.defaultStatement = found;
|
||||
idx.defaultStatements = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
/** How much of an `export default` statement is read: enough for any wrapper call's head. */
|
||||
const DEFAULT_STATEMENT_WINDOW = 4000;
|
||||
|
||||
/** Where the expression of the first `export default` statement outside a block comment starts, or -1. */
|
||||
function exportDefaultAt(source: string): number {
|
||||
/** Where the expression of each `export default` statement outside a block comment starts, in order. */
|
||||
function exportDefaultStarts(source: string): number[] {
|
||||
const starts: number[] = [];
|
||||
const inBlockComment = blockCommentReader(source);
|
||||
EXPORT_DEFAULT_RE.lastIndex = 0;
|
||||
for (let m = EXPORT_DEFAULT_RE.exec(source); m; m = EXPORT_DEFAULT_RE.exec(source)) {
|
||||
if (!insideBlockComment(source, m.index)) return m.index + m[0].length;
|
||||
if (!inBlockComment(m.index)) starts.push(m.index + m[0].length);
|
||||
}
|
||||
return -1;
|
||||
return starts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `offset` sits inside a block comment of a JS source, read from the
|
||||
* start past strings, template literals and line comments the way
|
||||
* `stripCommentsForRegex` reads them — without copying the file.
|
||||
* Whether each of a rising run of offsets in a JS source sits inside a block
|
||||
* comment, read in one pass past strings, template literals and line comments
|
||||
* the way `stripCommentsForRegex` reads them — without copying the file.
|
||||
*/
|
||||
function insideBlockComment(source: string, offset: number): boolean {
|
||||
for (let i = 0; i < offset; i++) {
|
||||
const ch = source[i];
|
||||
if (ch === '/' && source[i + 1] === '*') {
|
||||
const end = source.indexOf('*/', i + 2);
|
||||
if (end < 0 || end >= offset) return true;
|
||||
i = end + 1;
|
||||
} else if (ch === '/' && source[i + 1] === '/') {
|
||||
const end = source.indexOf('\n', i + 2);
|
||||
if (end < 0) return false;
|
||||
i = end;
|
||||
} else if (ch === '"' || ch === "'" || ch === '`') {
|
||||
// A quote ends at its match; one that isn't a template's, at its line's end.
|
||||
for (i++; i < offset && source[i] !== ch; i++) {
|
||||
if (source[i] === '\\') i++;
|
||||
else if (ch !== '`' && source[i] === '\n') break;
|
||||
}
|
||||
function blockCommentReader(source: string): (offset: number) => boolean {
|
||||
let i = 0;
|
||||
return (offset) => {
|
||||
while (i < offset) {
|
||||
const ch = source[i];
|
||||
if (ch === '/' && source[i + 1] === '*') {
|
||||
const end = source.indexOf('*/', i + 2);
|
||||
if (end < 0 || end >= offset) return true;
|
||||
i = end + 2;
|
||||
} else if (ch === '/' && source[i + 1] === '/') {
|
||||
const end = source.indexOf('\n', i + 2);
|
||||
i = end < 0 ? source.length : end;
|
||||
} else if (ch === '"' || ch === "'" || ch === '`') {
|
||||
// A quote ends at its match; one that isn't a template's, at its line's end.
|
||||
let j = i + 1;
|
||||
for (; j < source.length && source[j] !== ch; j++) {
|
||||
if (source[j] === '\\') j++;
|
||||
else if (ch !== '`' && source[j] === '\n') break;
|
||||
}
|
||||
if (j >= offset) return false;
|
||||
i = j + 1;
|
||||
} else i++;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The node a default export statement makes the export. A named binding is
|
||||
* the file's own declaration of it, else what the file imports under that
|
||||
* name (`import Login from './Login'; export default Login` in an `index`);
|
||||
* one a wrapper call hands on must run — `export default createRouter(routes)`
|
||||
* exports a router, not `routes`. An anonymous one is a node only when one
|
||||
* stands at the expression itself: React Native's
|
||||
* `export default codegenNativeComponent('MyView')`.
|
||||
* The node a default export statement makes the export. A function or class
|
||||
* the statement declares is the node that starts there — never a namesake
|
||||
* elsewhere in the file, which a template's text would otherwise reach. A
|
||||
* named binding is the file's own declaration of it, else what the file
|
||||
* imports under that name (`import Login from './Login'; export default
|
||||
* Login` in an `index`); one a wrapper call hands on must run — `export
|
||||
* default createRouter(routes)` exports a router, not `routes`. An anonymous
|
||||
* one is a node only when one stands at the expression itself: React
|
||||
* Native's `export default codegenNativeComponent('MyView')`.
|
||||
*/
|
||||
function defaultStatementNode(
|
||||
filePath: string,
|
||||
@@ -298,6 +324,11 @@ function defaultStatementNode(
|
||||
return context.getNodesInFile(filePath).find((n) =>
|
||||
WRAPPED_BINDING_KINDS.has(n.kind) && n.startLine === statement.line && n.startColumn === statement.column);
|
||||
}
|
||||
const lines = statement.declaredLines;
|
||||
if (lines) {
|
||||
return nodesInFileNamed(filePath, statement.name, context)
|
||||
.find((n) => WRAPPED_BINDING_KINDS.has(n.kind) && n.startLine >= lines.from && n.startLine <= lines.to);
|
||||
}
|
||||
return bindingNode(filePath, statement, language, context, visited, depth);
|
||||
}
|
||||
|
||||
@@ -338,10 +369,9 @@ function bindingNode(
|
||||
* a same-named symbol elsewhere is not it either.
|
||||
*/
|
||||
export function hasAnonymousDefaultExport(filePath: string, context: ResolutionContext): boolean {
|
||||
const idx = getFileExportIndex(filePath, context);
|
||||
const statement = defaultStatement(filePath, idx, context);
|
||||
return statement !== null && !('name' in statement) &&
|
||||
defaultStatementNode(filePath, statement, 'typescript', context, new Set(), 0) === undefined;
|
||||
const statements = defaultStatements(filePath, getFileExportIndex(filePath, context), context);
|
||||
return statements.length > 0 && statements.every((s) =>
|
||||
!('name' in s) && defaultStatementNode(filePath, s, 'typescript', context, new Set(), 0) === undefined);
|
||||
}
|
||||
|
||||
const valueBindingMemos = new WeakMap<ResolutionContext, Map<string, Node | null>>();
|
||||
@@ -376,13 +406,13 @@ export function valueBinding(value: Node, context: ResolutionContext): Node | un
|
||||
const init = /^[A-Za-z_$][\w$]*\s*(?::(?:[^=]|=>)*?)?=(?![=>])\s*/.exec(code);
|
||||
const at = init ? init[0].length : -1;
|
||||
const binding = init ? namedBindingAt(code, at) : null;
|
||||
if (binding && binding.name !== value.name) {
|
||||
found = bindingNode(value.filePath, { name: binding.name, wrapped: true }, value.language, context, new Set([value.filePath]), 0);
|
||||
} else if (binding) {
|
||||
if (binding?.nameAt !== undefined) {
|
||||
// `const Suggestions = observer(function Suggestions() {…})`: the function the value holds.
|
||||
found = nodesInFileNamed(value.filePath, binding.name, context)
|
||||
.find((n) => WRAPPED_BINDING_KINDS.has(n.kind) && n.startLine >= value.startLine && n.endLine <= value.endLine);
|
||||
} else if (init) {
|
||||
} else if (binding && binding.name !== value.name) {
|
||||
found = bindingNode(value.filePath, { name: binding.name, wrapped: true }, value.language, context, new Set([value.filePath]), 0);
|
||||
} else if (!binding && init) {
|
||||
// `const MyView = codegenNativeComponent('MyView')`: a node standing at the initializer itself.
|
||||
const before = code.slice(0, at);
|
||||
const newline = before.lastIndexOf('\n');
|
||||
@@ -2963,9 +2993,17 @@ function findExportedSymbolWalk(
|
||||
// default` names, or an expression with no node of its own — so it is
|
||||
// never guessed: the first exported function or component stood in for
|
||||
// it, and `export function loader` above `export default function Vans`
|
||||
// made every `import Vans from './Vans'` the loader.
|
||||
const statement = defaultStatement(filePath, exportIndex, context);
|
||||
if (statement) return defaultStatementNode(filePath, statement, language, context, visited, depth);
|
||||
// made every `import Vans from './Vans'` the loader. Of several
|
||||
// statements, the first that names a node is the module's own; one a
|
||||
// template only writes as text names none.
|
||||
const statements = defaultStatements(filePath, exportIndex, context);
|
||||
if (statements.length > 0) {
|
||||
for (const statement of statements) {
|
||||
const node = defaultStatementNode(filePath, statement, language, context, visited, depth);
|
||||
if (node) return node;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
// Svelte/Vue single-file components ARE the module's default export,
|
||||
// but are extracted as kind 'component' (not function/class). Without
|
||||
// this, an `export { default as X } from './X.svelte'` barrel never
|
||||
|
||||
Reference in New Issue
Block a user