Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Python, `super().get_ip()` called in a method, and `super(Adapter, self).get_ip()` written on one line, now link to the method the class inherits, except where Python would raise or only runtime could tell which method runs; re-index to pick up the new links.
- In C#, a `global using static` brings its type's members into reach only in the project that declares it, the files under the nearest `.csproj`. A bare name in another project of the same repository no longer links to them as a trusted call.
- In `codegraph_explore`, an override that hands off to the method it overrides no longer leads the Flow when the question names only the bare method (`pre_sql_setup`), so the Flow starts at the base method instead.
- 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.

## [1.6.2] - 2026-10-03

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.

## About this fork

This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`c0f45c7d`](https://github.com/colbymchenry/codegraph/commit/c0f45c7d) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.
This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`09511055`](https://github.com/colbymchenry/codegraph/commit/09511055) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.

The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below).

Expand Down Expand Up @@ -80,7 +80,7 @@ Session search uses `.codegraph/sessions-v2.db`. The first session search import

### What the fork adds

Compared with upstream `main` at `c0f45c7d` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit.
Compared with upstream `main` at `09511055` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit.

| Feature | Upstream | Fork | What it does |
|---|:-:|:-:|---|
Expand Down
169 changes: 166 additions & 3 deletions __tests__/jsx-child-disambiguation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,12 @@ export const Table = <Entry extends BaseEntity>({ data }: { data: Entry[] }) =>
);
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']);
// `<PaginatedList<Document>` is a tag that passes a type argument: it
// renders PaginatedList, and its `Document` stays a type.
expect(renders('DocumentList')).toEqual([
'Badge app/components/Badge.tsx:2',
'PaginatedList app/components/PaginatedList.tsx:1',
]);
expect(renders('Table')).toEqual([]);
});

Expand Down Expand Up @@ -346,3 +349,163 @@ export function Toolbar({ actions }) {
expect(renders('ExampleApp')).toEqual(['WelcomeScreen packages/excalidraw/components/WelcomeScreen.tsx:1']);
});
});

/**
* A generic component's tag passes its type arguments right after its name:
* outline's lists render `<PaginatedList<Document> items={…} />`, excalidraw's
* property panels `<RadioSelection<TextAlign | false> …>`. The pass read a
* tag's name only where whitespace, `/` or `>` ended it, so a tag that `<`
* ends was never read, and the parent had no edge to the component it renders.
* The type arguments stay types.
*/
describe('JSX child: a tag that passes type arguments', () => {
let dir: string;
let cg: any;

beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsx-generic-'));
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 the component a generic tag names, however its type arguments are written', async () => {
write('app/models/Document.ts', 'export default class Document {\n title = "";\n}\n');
write('app/models/Mode.ts', 'export class Mode {\n name = "";\n}\n');
write(
'app/components/PaginatedList.tsx',
`type PaginatedItem = { id: string };

const PaginatedList = <T extends PaginatedItem>({ items }: { items: T[] }) => {
return <ul>{items.length}</ul>;
};

export default PaginatedList;
`
);
write(
'app/components/RadioSelection.tsx',
'export const RadioSelection = <T extends Object>({ value }: { value: T }) => {\n return <div>{String(value)}</div>;\n};\n'
);
write(
'app/components/DataTable.tsx',
'export function DataTable<Row, Column>({ children }: { rows: Row[]; columns: Column[]; children?: unknown }) {\n return <table>{children}</table>;\n}\n'
);
write(
'app/components/DocumentList.tsx',
`import PaginatedList from './PaginatedList';
import { RadioSelection } from './RadioSelection';
import { DataTable } from './DataTable';
import Document from '../models/Document';
import { Mode } from '../models/Mode';

export function DocumentList({ documents, mode }: { documents: Document[]; mode: Mode }) {
return (
<section>
<PaginatedList<Document>
items={documents}
/>
<RadioSelection<Exclude<Mode, "auto"> | false> value={mode} />
<DataTable<Document, string> rows={documents} columns={[]}>
<p>{documents.length}</p>
</DataTable>
</section>
);
}
`
);
await index();
// `Document` and `Mode` are classes the tags pass as types, not children.
expect(renders('DocumentList')).toEqual([
'DataTable app/components/DataTable.tsx:1',
'PaginatedList app/components/PaginatedList.tsx:3',
'RadioSelection app/components/RadioSelection.tsx:1',
]);
});

it('still renders nothing for a generic type written as a type argument', async () => {
write(
'app/components/Grid.tsx',
`import * as React from 'react';

export class Grid<Row> extends React.Component<{ rows: Row[] }> {
render() {
return <table>{this.props.rows.length}</table>;
}
}
`
);
write(
'app/components/Report.tsx',
`import * as React from 'react';
import { Grid } from './Grid';

export function Report({ rows }: { rows: string[] }) {
const grid = React.useRef<Grid<string>>(null);
const [grids] = React.useState<Array<Grid<string>>>([]);
return <section>{rows.length + grids.length}</section>;
}
`
);
await index();
expect(renders('Report')).toEqual([]);
});

it('renders nothing for a generic tag the file imports from a package', async () => {
// refine's examples: react95's `<Select<number>>` and antd's
// `<Table<IPost>>` are the packages' own components, not another app's
// Select or Table that shares the name.
fs.writeFileSync(
path.join(dir, 'package.json'),
'{"dependencies":{"react":"^18.0.0","react95":"^4.0.0","antd":"^5.0.0"}}'
);
write('apps/shadcn/components/ui/select.tsx', 'export function Select() {\n return <select />;\n}\n');
write('apps/shadcn/components/ui/table.tsx', 'export function Table() {\n return <table />;\n}\n');
write(
'apps/win95/pages/posts.tsx',
`import { Select } from 'react95';
import { Table } from 'antd';

type IPost = { id: number };

export function PostList({ posts }: { posts: IPost[] }) {
return (
<div>
<Select<number> options={[]} />
<Table<IPost> dataSource={posts} />
</div>
);
}
`
);
await index();
expect(renders('PostList')).toEqual([]);
});
});
Loading