mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
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:
@@ -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>`.
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user