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:
Colby Mchenry
2026-10-10 09:04:33 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent a9781b0e05
commit 1ce1167e6f
2 changed files with 63 additions and 2 deletions
+1 -1
View File
@@ -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.
+62 -1
View File
@@ -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');