From 133117545c8fb0c3e48a53c0c1f15d996478a495 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 05:38:40 -0500 Subject: [PATCH] fix(jsx): a .jsx component links to the file-scope values it reads, as in .js and .tsx 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 (`` 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 --- CHANGELOG.md | 1 + __tests__/kernel-tsjs-parity.test.ts | 24 +++++++++++++++++ __tests__/value-reference-edges.test.ts | 27 +++++++++++++++++++ codegraph-kernel/src/tsjs/mod.rs | 9 +------ docs/design/value-reference-edges-playbook.md | 4 +-- docs/design/value-reference-edges.md | 15 +++++++++-- src/extraction/tree-sitter.ts | 2 +- 7 files changed, 69 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa4c1d081..a5bb981db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, working out which structs implement an interface now counts the methods of the interfaces it embeds, and the methods a struct gets from the types it embeds. Before, only each type's own methods counted: etcd's `AuthReadTx`, which embeds `UnsafeAuthReader` and adds `RLock` and `RUnlock`, was listed as implemented by every read-write lock in the project, an interface that only embeds others had no implementations at all, and a struct that gets its methods from an embedded base, like prometheus's service discoveries embedding `refresh.Discovery` or gin's `Engine` embedding `RouterGroup`, was missing. `codegraph_explore`, impact and the type hierarchy now list the right implementations, and a call through the interface reaches a method the embedding struct overrides, like gin's `Engine.Use`. 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 ``, 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 `` links led nowhere. Each route links to the page its `element` renders, past a guard like `` or ``, 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 ``, 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) ## [1.6.2] - 2026-10-03 diff --git a/__tests__/kernel-tsjs-parity.test.ts b/__tests__/kernel-tsjs-parity.test.ts index 57fec8649d..c4eba022bd 100644 --- a/__tests__/kernel-tsjs-parity.test.ts +++ b/__tests__/kernel-tsjs-parity.test.ts @@ -314,6 +314,30 @@ export function second() { return probe(); } ])); }); + 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 ; } +export function Nav() { + return
    {ROUTE_TABLE.map((r) =>
  • {r.label}
  • )}
; +} +export const Box = () =>
; +`, 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'); diff --git a/__tests__/value-reference-edges.test.ts b/__tests__/value-reference-edges.test.ts index 485a00bd9e..3751546eb7 100644 --- a/__tests__/value-reference-edges.test.ts +++ b/__tests__/value-reference-edges.test.ts @@ -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 ;', + '}', + 'export function Nav() {', + ' return
    {NAV_ITEMS.map((item) =>
  • {item.label}
  • )}
;', + '}', + ].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'), diff --git a/codegraph-kernel/src/tsjs/mod.rs b/codegraph-kernel/src/tsjs/mod.rs index 2658226bab..abf1ebe8fc 100644 --- a/codegraph-kernel/src/tsjs/mod.rs +++ b/codegraph-kernel/src/tsjs/mod.rs @@ -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) { diff --git a/docs/design/value-reference-edges-playbook.md b/docs/design/value-reference-edges-playbook.md index 17bb78bb61..363d662045 100644 --- a/docs/design/value-reference-edges-playbook.md +++ b/docs/design/value-reference-edges-playbook.md @@ -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` | diff --git a/docs/design/value-reference-edges.md b/docs/design/value-reference-edges.md index 8ef442362f..25484d44a2 100644 --- a/docs/design/value-reference-edges.md +++ b/docs/design/value-reference-edges.md @@ -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 (``) — 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 (`` 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 `