mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 20:42:56 +08:00
test(js,ts): pin require().default and abstract classes to the declared default (#2481)
#2432 made a default import read the module's own `export default` statement, and its merge pointed #2433's `require('./x').default` fallback (esmDefaultExport) at the same reading. Nothing in the suite pinned that path with a guessable helper above the default: #2433's fixture keeps its helper unexported. Two cases: - React Native's AnimatedColor shape, an exported helper above `export default class AnimatedColor`, loaded with `require("./AnimatedColor").default` and `const { default: Color } = require("./AnimatedColor")`. With the first-exported guess both `new` calls instantiate the helper. - `export default abstract class Repository` below an exported `connect()`, extended by `class Users extends Repository`. With the guess the `extends` edge is missing. Both fail with e47cb258's resolver (before #2432). On main, putting the guess back on the require path fails only the first, and dropping `abstract` from the declaration pattern fails only the second. The CHANGELOG's default-import bullet now says `require('./x').default` is read the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a9781b0e05
commit
1ce1167e6f
+1
-1
@@ -115,7 +115,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In TypeScript React code, a styled-components or emotion component declared with a type argument, like `const Wrapper = styled.div<WrapperProps>` followed by its CSS, or `styled(Base)<Props>`, is now indexed as a component, as one written without a type argument already was. Before, it was indexed as a plain constant, so `codegraph callers` and impact found none of the code that renders it, and its tags could link to another file's component of the same name instead: in outline/outline, `NudeButton` and `HStack` had no callers, and `<Text>` tags linked to a different `Text` than the one each file imports. Re-index TypeScript React projects after upgrading.
|
||||
- `codegraph sync` and the file watcher now move a React Router route to its page when the router file loads that page lazily into a value of its own, like `const Docs = lazy(() => import('./pages/Docs'))` rendered by `<Route path="/docs" element={<Docs />} />`, and the page's file is added after the router was indexed, comes back after being deleted, or gets or changes its default export in a later edit. Before, the route kept pointing at the `Docs` declaration, or at the page's old component, until the router file itself changed or the project was re-indexed, unless the page's component happened to share the declaration's name. The same now holds for a `lazy: () => import('./pages/Docs')` route whose page switches its default export to another component. Re-index React projects after upgrading to correct routes an earlier version left behind.
|
||||
- `codegraph sync` and the file watcher now link a component to the JSX that renders it, like `<Team />`, when the component is added in a file with no JSX of its own, as in `export default function Team() { return null; }`. Before, the link waited for an unrelated change to a file with JSX, or for a re-index, so the new component looked unused.
|
||||
- In JavaScript and TypeScript, a default import now links to what the module's `export default` statement exports, wherever the statement sits in the file. Before, a module that declares its default, like `export default function Vans() {…}` or `export default class Store {…}`, had its default import linked to the first function it exports instead: in a React Router data-router app, where a page exports its `loader` or `action` above the page component, `import Vans, { loader as vansLoader } from './pages/Vans'` made the route `/vans` and every use of `Vans` lead to the loader. A component file that also exports a styled or memoized component no longer hands its default import to that component, and an anonymous default, like `export default function () {…}` or `export default () => …`, is no longer linked to a function exported beside it or nested inside it. Re-index JavaScript and TypeScript projects after upgrading.
|
||||
- In JavaScript and TypeScript, a default import now links to what the module's `export default` statement exports, wherever the statement sits in the file. Before, a module that declares its default, like `export default function Vans() {…}` or `export default class Store {…}`, had its default import linked to the first function it exports instead: in a React Router data-router app, where a page exports its `loader` or `action` above the page component, `import Vans, { loader as vansLoader } from './pages/Vans'` made the route `/vans` and every use of `Vans` lead to the loader. A component file that also exports a styled or memoized component no longer hands its default import to that component, and an anonymous default, like `export default function () {…}` or `export default () => …`, is no longer linked to a function exported beside it or nested inside it. A module loaded with `require('./x').default` is read the same way. Re-index JavaScript and TypeScript projects after upgrading.
|
||||
- In React and Vue, a tag for a component the file imports from a package, like `<Button>` from antd, `<Typography>` from `@mui/material/Typography`, `<Link>` from react-router-dom or `<Card>` from ant-design-vue, no longer links to a component, function or class in your repository that only shares its name. Before, the tag linked to whatever the repository had by that name, so `codegraph_explore`, callers and impact followed renders that never happen: in SigNoz, antd's `<Button>` tags showed up as callers of a styled link on its 404 page, and in Material UI dashboards like mantis and berry, `<Typography>` and `<Button>` tags showed up as callers of the theme's style overrides. A package here is one that a `package.json` above the file lists and that no file in the repository answers to. A tag imported through a path alias like `components/Modal`, from a workspace package of the repository, or under a name no `package.json` lists still links as before. Re-index React and Vue projects after upgrading.
|
||||
- In React with TypeScript, a component that renders a generic component and passes it type arguments, like outline's `<PaginatedList<Document> items={…} />` or excalidraw's `<DropdownMenuItemContentRadio<"contain" | "overlap"> …>`, now links to the component it renders. Before, a tag followed by type arguments was skipped, so the generic component looked unused by the screens that render it, and `codegraph_explore`'s call paths, callers and impact stopped short of it. Re-index React projects after upgrading.
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
* 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.
|
||||
* one nested inside the default itself. `require('./x').default` of an ES
|
||||
* module reads the same default, through the same lookup.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
@@ -173,6 +174,37 @@ export default () => {
|
||||
}
|
||||
|
||||
export default new Service()
|
||||
`,
|
||||
// An abstract class the statement declares, below an exported helper.
|
||||
'src/repository.ts': `export function connect(): string {
|
||||
return "db"
|
||||
}
|
||||
|
||||
export default abstract class Repository {
|
||||
abstract find(id: string): unknown
|
||||
|
||||
describe(): string {
|
||||
return connect()
|
||||
}
|
||||
}
|
||||
`,
|
||||
// React Native's AnimatedColor shape, loaded with `require(…).default`
|
||||
// (read the way a default import reads it) instead of imported.
|
||||
'src/animated/AnimatedColor.ts': `export function getRgbaValueAndNativeColor(value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
export default class AnimatedColor {
|
||||
constructor(value: string) {
|
||||
getRgbaValueAndNativeColor(value)
|
||||
}
|
||||
}
|
||||
`,
|
||||
'src/animated/load.ts': `export function makeColors(): unknown[] {
|
||||
const AnimatedColor = require("./AnimatedColor").default
|
||||
const { default: Color } = require("./AnimatedColor")
|
||||
return [new AnimatedColor("red"), new Color("blue")]
|
||||
}
|
||||
`,
|
||||
'src/use.tsx': `import handler from "./api/vans"
|
||||
import Store from "./store"
|
||||
@@ -183,6 +215,7 @@ import Banner from "./components/Banner"
|
||||
import runAnonymous from "./anonymous"
|
||||
import Arrow from "./arrow"
|
||||
import service from "./service"
|
||||
import Repository from "./repository"
|
||||
|
||||
export function consume() {
|
||||
handler({}, { json() {} })
|
||||
@@ -195,6 +228,12 @@ export function consume() {
|
||||
Arrow()
|
||||
service.run()
|
||||
}
|
||||
|
||||
export class Users extends Repository {
|
||||
find(id: string) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -253,6 +292,28 @@ describe('a default import of a module that exports something above its default'
|
||||
expect(targets).not.toContain('src/stream.js:emit');
|
||||
});
|
||||
|
||||
it('extends the abstract class the statement declares', () => {
|
||||
const users = cg.getNodesInFile('src/use.tsx').find((n) => n.name === 'Users' && n.kind === 'class')!;
|
||||
const bases = cg
|
||||
.getOutgoingEdges(users.id)
|
||||
.filter((e) => e.kind === 'extends')
|
||||
.map((e) => `${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`);
|
||||
expect(bases).toEqual(['src/repository.ts:Repository']);
|
||||
});
|
||||
|
||||
it('reads `require(…).default` and `{ default: X } = require(…)` the same way', () => {
|
||||
const make = cg.getNodesInFile('src/animated/load.ts').find((n) => n.name === 'makeColors')!;
|
||||
const created = cg
|
||||
.getOutgoingEdges(make.id)
|
||||
.filter((e) => e.kind === 'calls' || e.kind === 'instantiates')
|
||||
.map((e) => `${e.kind} ${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`)
|
||||
.sort();
|
||||
expect(created).toEqual([
|
||||
'instantiates src/animated/AnimatedColor.ts:AnimatedColor',
|
||||
'instantiates src/animated/AnimatedColor.ts:AnimatedColor',
|
||||
]);
|
||||
});
|
||||
|
||||
it('prefers what the statement exports to an exported component above it', () => {
|
||||
const targets = consumed();
|
||||
expect(targets).toContain('src/components/Title.tsx:Title');
|
||||
|
||||
Reference in New Issue
Block a user