fix(react): the React resolver only resolves a script's references (#2211)

The React resolver's `languages` gates its route extraction, not its
resolve(): framework resolvers see every language's references. Its
component rule was already JSX-only, but its hook (`use...`) and context
(`...Context` / `...Provider`) rules were not: in halo - a Spring app whose
UI package makes React detected - 209 Java import edges (`SecurityContext`,
`ApplicationContext`, `ObjectProvider`, `DirtiesContext`...) were resolved by
it onto import nodes. It now answers only JavaScript / TypeScript / TSX /
JSX references.

halo -209 +3 (a Java test's `responseContext()` helper now resolves to
itself); trpc, excalidraw, react-redux-realworld, bulletproof-react,
react-native-reusables unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 22:10:14 +00:00
committed by GitHub
parent d7cdb0c09b
commit 5c01f017cf
3 changed files with 72 additions and 0 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- In a project with a React front end, the React rules for hooks (`use…`) and contexts (`…Context`, `…Provider`) no longer apply to backend code. In halo, a Spring app with a React UI, Java imports such as `SecurityContext` and `ObjectProvider` had been handled as React contexts.
- In a project with a Vue front end, the backend's calls no longer land on Vue components. mealie's Python `QueryFilterBuilder(…)` had been linking to the `QueryFilterBuilder.vue` component. The Vue rules (compiler macros, Nuxt auto-imports, components by name) now apply only to the app's scripts.
- Production code no longer links to a same-named symbol inside a test suite, since tests aren't built into the program. Before, typeorm's `Record<string, …>` types reached a test entity called `Record`, and okhttp's sample `@Override` annotations reached a test's nested `Override` class. Test-support code a project ships, such as a `testing/` folder or a `*-test` module, stays in reach.
- A Dart call like `ext.endsWith(".avi")` or `map.putIfAbsent(…)` on a value whose type isn't known now counts as Dart's own String, List or Map method. It no longer lands on a project method of the same name. A Dart extension is also matched by the type it extends, not by its own name, so getx's `ext.endsWith` on a String stopped reaching `extension RxStringExt on Rx<String>`.
+65
View File
@@ -0,0 +1,65 @@
/**
* The React resolver's hook (`use…`) and context (`…Context` / `…Provider`)
* rules are a script's, never another language's: in halo — a Spring app
* with a React-detected UI package — Java imports of `SecurityContext` or
* `ObjectProvider` were taken by the context rule, and a Java test's own
* `responseContext()` helper went unresolved.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-react-lang-'));
const files: Record<string, string> = {
'package.json': JSON.stringify({ name: 'app', dependencies: { react: '^18' } }),
'ui/src/ThemeContext.tsx': `import { createContext } from 'react';
export const responseContext = createContext(null);
`,
'src/main/java/app/Filter.java': `package app;
import org.springframework.security.core.context.SecurityContext;
public class Filter {
Object responseContext() {
return null;
}
void filter() {
responseContext();
}
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
describe('the React resolver', () => {
it('does not take a Java import of a …Context class', () => {
const ids = cg.getNodesInFile('src/main/java/app/Filter.java').map((n) => n.id);
const byReact = cg.getOutgoingEdgesFrom(ids).filter((e) => (e.metadata as { framework?: string } | undefined)?.framework === 'react');
expect(byReact).toEqual([]);
});
it('leaves a Java call named like a context to Java', () => {
const filter = cg.getNodesInFile('src/main/java/app/Filter.java').find((n) => n.name === 'filter')!;
const targets = cg.getOutgoingEdges(filter.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!);
expect(targets.map((t) => t.filePath)).not.toContain('ui/src/ThemeContext.tsx');
expect(targets.map((t) => t.qualifiedName)).toContain('app::Filter::responseContext');
});
});
+6
View File
@@ -10,6 +10,9 @@ import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from
import { dependsOn } from './package-deps';
import { resolveImportPath } from '../import-resolver';
/** The languages React components, hooks and contexts are written and used in. */
const REACT_SCRIPT_LANGUAGES: ReadonlySet<string> = new Set(['typescript', 'javascript', 'tsx', 'jsx']);
export const reactResolver: FrameworkResolver = {
name: 'react',
// Includes 'tsx'/'jsx' so route extraction runs on JSX files (where
@@ -34,6 +37,9 @@ export const reactResolver: FrameworkResolver = {
},
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
// Components, hooks and contexts are a script's: halo's Java
// `import org.springframework…SecurityContext` is no React context.
if (!REACT_SCRIPT_LANGUAGES.has(ref.language)) return null;
if (ref.referenceName.startsWith(LAZY_ROUTE_PREFIX)) {
const target = lazyRouteComponent(ref.referenceName.slice(LAZY_ROUTE_PREFIX.length), ref.filePath, context);
return target ? { original: ref, targetNodeId: target, confidence: 0.9, resolvedBy: 'framework' } : null;