fix(jsx): a .jsx component links to the file-scope values it reads, as in .js and .tsx (#2561)

Value-reference edges (a reader symbol -> the file-scope const/var it
reads, which impact follows) ran for typescript, tsx and javascript but
not jsx. A .jsx file is its own language, parsed with the JavaScript
grammar, so the same component had no edge to the lazy page it renders
(`<LoginPage />` under `const LoginPage = Loadable(lazy(...))`), the route
table or the config it maps over as .jsx, and all of them as .js or .tsx.
Every other JS-family gate in the extractor lists jsx, and nothing in the
history says this one was left out on purpose: the kernel mirrored the gap
with an explicit "NOT jsx" check for parity.

Add jsx to VALUE_REF_LANGS and drop the kernel's jsx exclusion.

On mantis, matx, berry, mastodon and bigbluebutton-html5 the index only
gains edges (35, 56, 15, 28 and 266): no node, ref or file changes and no
edge removed. Every added edge is a same-file read of a file-scope value
that is the only binding of its name in the file. A repo with no .jsx
files indexes byte-identically, and kernel-parity.mjs shows 0 diffs over
those repos with the same deferrals as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-14 23:23:21 +00:00
committed by GitHub
parent 8206c8c271
commit 3f714bcc48
7 changed files with 69 additions and 13 deletions
+1
View File
@@ -130,6 +130,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Go, a struct or defined type now counts as implementing an interface only when its methods take as many parameters and return as many results as the interface's. Before, matching method names was enough, so etcd's watch cache, whose `Get` takes a context, a key and options, was listed as a `peerGetter`, whose `Get` takes a peer ID, and a call through an interface could be shown reaching methods it can never reach. Look-alikes could also crowd out the real implementations, so `codegraph_explore` stopped at grpc-go's xDS `TransportBuilder` instead of reaching the transport it builds. A server still counts for the gRPC client interface of the service it serves, like etcd's `kvServer` for `KVClient`, so a client's call keeps leading to the handler that serves it. Re-index Go projects after upgrading.
- In Angular templates, a property binding, an interpolation, a structural directive or a control-flow block that calls one of the component's own members, like `[name]="icon()"`, `{{ label() }}`, `*ngIf="isOpen()"` or `@if (loading()) {`, now links the component to that member, and so does reading a getter, like `[disabled]="!canSave"`, or handing a method to a child component, like `[displayWith]="displayFn"` or `trackBy: trackById`. Before, only event bindings such as `(click)="save()"` were read, so a signal, a `computed` value, a getter or a method used only from a template had no callers and looked unused. A pipe, a template variable, a call on another object such as `form.reset()`, and a plain field read like `[value]="title"` still link nothing. Re-index Angular projects after upgrading.
- React Router routes kept in a table of their own are now indexed: the ASP.NET Core React template's `AppRoutes` array that `App.js` maps into `<Route {...rest}>`, a `routes` array imported into `useRoutes(routes)` or `createBrowserRouter(routes)` or returned by a function as in `useRoutes(routes(isLoggedIn))`, route objects kept one per file and listed in `createBrowserRouter([MainRoutes, LoginRoutes])`, and routes written in place in `useRoutes([…])`. Before, route objects were read only from a file that itself creates a data router, so these apps had no routes, and their `navigate('/login')` calls and `<Link to="/counter">` links led nowhere. Each route links to the page its `element` renders, past a guard like `<ProtectedRoute>` or `<AdminWrapper>`, and through `lazy(() => import(…))` to the page it loads rather than a same-named page elsewhere in the repository. An `index: true` route is the page at its parent's address, and a parent route around others counts as their layout. A `{ path, element }` list that nothing hands to the router, such as a menu, still makes no routes. Re-index React projects after upgrading.
- In `.jsx` files, the components and functions that read a constant or variable declared at the top of the file, like a page loaded with `const LoginPage = Loadable(lazy(…))` and rendered as `<LoginPage />`, a route table, or a messages or style object, now show up in its impact, as they already did in `.js` and `.tsx` files. Before, `.jsx` files were left out, so `codegraph impact` and the impact trail in `codegraph_explore` and `codegraph_node` showed nothing depending on such a value. Re-index projects with `.jsx` files after upgrading.
- 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)
- In Go, a call is now matched to the package or variable it is actually made on. Before, a call through a package, like `klog.Infof("Log using Infof")` or `digest.Digest(dig)` inside `&distribution.Descriptor{Digest: …}`, could link to a project method of the same name when the name appeared again on the line, and in `klog.Error(err.Error())` the `err.Error()` call was taken for a call into `klog`. A parameter, receiver or local variable named like an import, like etcd's `jwt, err := newTokenProviderJWT(…)` or the `suite` receiver in a testify test suite, now counts as that variable, so calls on it link to the methods of the type it is declared as, and to nothing when that type comes from outside your project. Re-index Go projects after upgrading.
- In Go, a call through a parameter or local variable named like a standard-library package, such as `context`, `log`, `trace`, `printer` or `token`, now links to the method it calls. Before, every call written `context.…` or `printer.…` was taken for a call into the standard library and linked to nothing: gin's `context.AbortWithError(…)` inside `func(context *Context)`, harbor's `context.IsAuthenticated()` after `context := NewSecurityContext(…)` and `log.Errorf(…)` after `log := log.G(ctx)`, etcd's `printer.DBHashKV(…)` and `trace.Step(…)`. The variable's declaration decides the method: the type it is declared as, or the package of the project function its value comes from. A variable holding a value from outside your project, like `scanner := bufio.NewScanner(r)` or `url, err := url.Parse(raw)`, still links to nothing, and one whose type can't be told links only to a method no other type in the project has, never to one picked because it matches the variable's name. Re-index Go projects after upgrading.
+24
View File
@@ -341,6 +341,30 @@ const lowerCase = styled.div<Props>\`color: red;\`;
for (const name of ['Mixin', 'Compared', 'lowerCase']) expect(kind(name), name).toEqual(['constant']);
});
it.each([
['tsx', 'tsx'], ['js', 'javascript'], ['jsx', 'jsx'],
] as const)('edges the file-scope values a component reads or renders: %s', (ext, language) => {
// `.jsx` was left out of the value-reference languages on both paths, so
// this file had none of these edges as .jsx and all of them as .js or .tsx.
const result = assertParity(`routes.${ext}`, `
import { lazy } from 'react';
import Loadable from './Loadable';
const LoginPage = Loadable(lazy(() => import('./pages/Login')));
const ROUTE_TABLE = [{ path: '/login', label: 'Login' }];
const THEME_TOKENS = { color: 'red', size: 12 };
export function AuthRoutes() { return <LoginPage />; }
export function Nav() {
return <ul style={{ color: THEME_TOKENS.color }}>{ROUTE_TABLE.map((r) => <li key={r.path}>{r.label}</li>)}</ul>;
}
export const Box = () => <div data-size={THEME_TOKENS.size} />;
`, language);
const name = (id: string) => result.nodes.find((n) => n.id === id)?.name;
expect(result.edges
.filter((e) => e.kind === 'references' && e.metadata?.valueRef === true)
.map((e) => `${name(e.source)} -> ${name(e.target)}`)
.sort()).toEqual(['AuthRoutes -> LoginPage', 'Box -> THEME_TOKENS', 'Nav -> ROUTE_TABLE', 'Nav -> THEME_TOKENS']);
});
it('torture fixture (tsx): components, stores, RTK, fn-refs, value-refs, decorators', () => {
const file = path.join(FIXTURE_DIR, 'torture.tsx');
assertParity('fixtures/torture.tsx', fs.readFileSync(file, 'utf8'), 'tsx');
+27
View File
@@ -124,6 +124,33 @@ describe('value-reference edges', () => {
expect(valueRefReaders(cg, 'THEME_TOKENS')).toEqual(expect.arrayContaining(['Label', 'Box']));
});
it('edges readers in a .jsx file, as it does the same code in .js or .tsx', async () => {
// `.jsx` is a language of its own, parsed with the JavaScript grammar. It
// used to be missing from the value-reference languages, so a component in
// a .jsx file had no edge to the lazy page it renders or the table it maps
// over, while the same code in a .js or .tsx file had both.
fs.writeFileSync(
path.join(dir, 'routes.jsx'),
[
"import { lazy } from 'react';",
"import Loadable from './Loadable';",
"const LoginPage = Loadable(lazy(() => import('./pages/Login')));",
"const NAV_ITEMS = [{ path: '/login', label: 'Login' }];",
'export function AuthRoutes() {',
' return <LoginPage />;',
'}',
'export function Nav() {',
' return <ul>{NAV_ITEMS.map((item) => <li key={item.path}>{item.label}</li>)}</ul>;',
'}',
].join('\n'),
);
cg = index();
await cg.indexAll();
expect(valueRefReaders(cg, 'LoginPage')).toEqual(['AuthRoutes']);
expect(valueRefReaders(cg, 'NAV_ITEMS')).toEqual(['Nav']);
});
it('edges same-file readers to a module-level const/static (Rust)', async () => {
fs.writeFileSync(
path.join(dir, 'lib.rs'),
+1 -8
View File
@@ -46,10 +46,6 @@ impl Variant {
fn is_ts(self) -> bool {
matches!(self, Variant::Typescript | Variant::Tsx)
}
/// VALUE_REF_LANGS includes typescript/tsx/javascript but NOT jsx.
fn value_refs(self) -> bool {
!matches!(self, Variant::Jsx)
}
}
/// typescriptExtractor.methodTypes / javascriptExtractor.methodTypes.
@@ -478,9 +474,6 @@ impl<'t> Walker<'t> {
// --- value references (captureValueRefScope / flushValueRefs) --------------
fn capture_value_ref_scope(&mut self, kind: &'static str, name: &str, row: u32, node: Node<'t>, value_target: bool) {
if !self.variant.value_refs() {
return;
}
let target_kind_ok = kind == "constant" || kind == "variable";
if value_target
&& target_kind_ok
@@ -506,7 +499,7 @@ impl<'t> Walker<'t> {
let scopes = std::mem::take(&mut self.value_scopes);
let mut targets = std::mem::take(&mut self.fs_values);
let counts = std::mem::take(&mut self.fs_value_counts);
if !self.variant.value_refs() || std::env::var("CODEGRAPH_VALUE_REFS").as_deref() == Ok("0") {
if std::env::var("CODEGRAPH_VALUE_REFS").as_deref() == Ok("0") {
return;
}
if targets.is_empty() || scopes.is_empty() || util::is_generated_file(self.file_path) {
@@ -45,7 +45,7 @@ agent read-reduction (see §4.3).
| Symbol | Role |
|---|---|
| `VALUE_REF_LANGS` (static Set) | languages the feature runs for. Currently `typescript`, `javascript`, `tsx`, `go`, `python`, `rust`, `ruby`, `c`, `java`, `csharp`, `php`, `scala`, `kotlin`, `swift`, `dart`, `pascal`. **Add the new language here.** |
| `VALUE_REF_LANGS` (static Set) | languages the feature runs for. Currently `typescript`, `javascript`, `tsx`, `jsx`, `arkts`, `go`, `python`, `rust`, `ruby`, `c`, `java`, `csharp`, `php`, `scala`, `kotlin`, `swift`, `dart`, `pascal`. **Add the new language here.** |
| `valueRefsEnabled` | `process.env.CODEGRAPH_VALUE_REFS !== '0'` — default ON, env opts out. |
| `MAX_VALUE_REF_NODES` (20_000) | per-scope traversal cap (and the shadow-scan cap). |
| `captureValueRefScope(kind, name, id, node)` | called from `createNode` on every node. Records **targets** (file-scope `const`/`var`) and **reader scopes** (`function`/`method`/`const`/`var`). |
@@ -169,7 +169,7 @@ the bottom of this section).
| Language | How |
|---|---|
| TypeScript, JavaScript, tsx | file-scope `const`/`var`; the original languages |
| TypeScript, JavaScript, tsx, jsx | file-scope `const`/`var`; the original languages (`jsx` was left out until 2026-10 — see the design doc's tsx note) |
| Python | module-level `NAME =` |
| Go | package `const`/`var` |
| Rust | module + impl `const`/`static` |
+13 -2
View File
@@ -1,6 +1,6 @@
# Design + status: same-file value-reference edges
**Status:** SHIPPED (default-on for TS/JS/tsx + Go + Python + Rust + Ruby + C + Java + C# + PHP + Scala + Kotlin + Swift + Dart + Pascal; `CODEGRAPH_VALUE_REFS=0` disables). The
**Status:** SHIPPED (default-on for TS/JS/tsx/jsx + Go + Python + Rust + Ruby + C + Java + C# + PHP + Scala + Kotlin + Swift + Dart + Pascal; `CODEGRAPH_VALUE_REFS=0` disables). The
emitter lives in `TreeSitterExtractor.flushValueRefs` (`src/extraction/tree-sitter.ts`).
**Motivation:** close the impact-analysis hole for *value consumers*. Static
extraction edges calls, imports, and inheritance, but never edges a constant to the
@@ -13,7 +13,7 @@ readers" class of change (the ReScript-PR false positive that motivated the work
## TL;DR for a new session
We emit a `references` edge (`metadata: { valueRef: true }`) from a reader symbol to
the **file/package-scope `const`/`var` it reads**, same-file only, for TS/JS/tsx + Go + Python + Rust + Ruby + C + Java + C# + PHP + Scala + Kotlin + Swift + Dart + Pascal. Those edges
the **file/package-scope `const`/`var` it reads**, same-file only, for TS/JS/tsx/jsx + Go + Python + Rust + Ruby + C + Java + C# + PHP + Scala + Kotlin + Swift + Dart + Pascal. Those edges
flow straight into `getImpactRadius` / `codegraph impact` and the impact trail in
`codegraph_explore` / `codegraph_node` — no agent-behaviour change required.
@@ -419,6 +419,17 @@ tsx-specific path — a const read *only* inside JSX (`<Foo x={CONST}/>`) — re
reader-scan descending into the JSX subtree; it's locked by a unit test
(`value-reference-edges.test.ts`), so no separate tsx repo sweep is needed.
**`jsx` was missing until 2026-10.** A `.jsx` file is its own language (`jsx`), parsed
with the JavaScript grammar, and `VALUE_REF_LANGS` — and the kernel's mirror in
`codegraph-kernel/src/tsjs/` — listed `javascript` and `tsx` but not `jsx`. So a `.jsx`
component had no edge to the lazy page it renders (`<LoginPage />` under
`const LoginPage = Loadable(lazy(…))`) or the table it maps over, while the same file as
`.js` or `.tsx` had both. Adding it changed nothing else: on mantis, matx, berry,
mastodon and bigbluebutton-html5 the index only gained edges (400 in all, every one a
same-file read of a file-scope value that is the only binding of its name in the file),
and a repo with no `.jsx` files indexed byte-identically. Locked by the `.jsx` cases in
`value-reference-edges.test.ts` and `kernel-tsjs-parity.test.ts`.
**Svelte / Vue / Astro are covered for free** — their extractors re-parse the `<script>` /
frontmatter block as `typescript` / `javascript`, which are in `VALUE_REF_LANGS`, so a `const`
in a `.svelte`/`.vue`/`.astro` script edges its readers without any extra work (verified on a
+1 -1
View File
@@ -557,7 +557,7 @@ export class TreeSitterExtractor {
// Value-reference edges (default ON; set CODEGRAPH_VALUE_REFS=0 to disable; see flushValueRefs).
// Same-file reads of file-scope const/var symbols → `references` edges so impact analysis catches
// value consumers ("change this constant/table, affect its readers").
private static readonly VALUE_REF_LANGS = new Set<string>(['typescript', 'javascript', 'tsx', 'arkts', 'go', 'python', 'rust', 'ruby', 'c', 'java', 'csharp', 'php', 'scala', 'kotlin', 'swift', 'dart', 'pascal']);
private static readonly VALUE_REF_LANGS = new Set<string>(['typescript', 'javascript', 'tsx', 'jsx', 'arkts', 'go', 'python', 'rust', 'ruby', 'c', 'java', 'csharp', 'php', 'scala', 'kotlin', 'swift', 'dart', 'pascal']);
private static readonly MAX_VALUE_REF_NODES = 20_000;
private readonly valueRefsEnabled = process.env.CODEGRAPH_VALUE_REFS !== '0';
private fileScopeValues = new Map<string, string>();