diff --git a/CHANGELOG.md b/CHANGELOG.md index 77cd625e1..58a270a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` 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`. diff --git a/__tests__/react-resolver-language.test.ts b/__tests__/react-resolver-language.test.ts new file mode 100644 index 000000000..50bbf3bf3 --- /dev/null +++ b/__tests__/react-resolver-language.test.ts @@ -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 = { + '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'); + }); +}); diff --git a/src/resolution/frameworks/react.ts b/src/resolution/frameworks/react.ts index f9180fa0b..c4fa6946c 100644 --- a/src/resolution/frameworks/react.ts +++ b/src/resolution/frameworks/react.ts @@ -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 = 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;