mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-02 09:45:39 +08:00
fix(spring): name heuristics start from the reference's own file and package (#2222)
The Spring resolver's entity/service/controller name heuristics took the first same-named class under a conventional folder; they now prefer the ref's own file, then its package, and never reach another file's nested class by its bare name. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ff190b30d8
commit
d6f68648cb
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- In Spring projects, a class name now resolves to the one in the reference's own file or package before any guess by folder convention. A class nested inside another file's class is never reached by its bare name. Before, every MyBatis `Example` class's `new Criteria()` in mall linked to the first `Example`'s nested `Criteria`, and halo's Lombok `@Builder` linked to a nested `Builder` class elsewhere.
|
||||
- A method that hands its call on to another object is no longer linked to itself. In TypeScript and JavaScript, a call through `this.<field>` or `window.<x>` counts as recursion only when the field is declared as the method's own class. A guess from a receiver's name alone never lands on the calling method. Before, BookStack's `toggle()` doing `this.container.classList.toggle('open')`, `listen()` doing `window.$events.listen(…)`, and `FileStorage::delete` doing `$storage->delete($path)` each pointed at themselves.
|
||||
- C# names now follow the language's scopes. A block `namespace X { … }` qualifies only the types inside it, so a type declared after the block, or in a file's second namespace, is no longer filed under the first. A namespace written inside another is `Outer.Inner`. A `using` links to the project's namespace instead of another file's `using` of the same name. A `global using` applies only within its own project. A nested type is reachable by bare name only from inside its owner or a type deriving from it, including through another partial part. Before, serilog's `Guard` was filed under `JetBrains.Annotations`, its tests' `Some.InformationEvent()` went to the performance tests' `Some`, and AutoMapper's same-file `new Source()` could reach another test class's nested `Source`.
|
||||
- A function or name passed as a value now resolves to what is in scope where it's written. A function nested inside another function is only reachable from inside it. In Python, a parameter or local of the same name is that local. A pytest fixture is a test's parameter only in its own module or under its `conftest.py`. Before, httpx's `self._build_auth(auth)` linked to an `auth` a test defines inside another function, and `auth_flow(self, request)` handing `request` on linked to the package's `request()` function.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* The Spring resolver's name heuristics (an entity under `/model/`, a
|
||||
* `…Service`, a `…Controller`) start from what the reference's own scope
|
||||
* declares — its file, then its package — and never reach a class nested in
|
||||
* another file's class by its bare name: every MyBatis `XExample` declares its
|
||||
* own nested `Criteria`, and mall's `new Criteria()` all went to the first.
|
||||
*/
|
||||
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;
|
||||
|
||||
const example = (name: string) => `package com.macro.mall.model;
|
||||
|
||||
public class ${name} {
|
||||
public Criteria createCriteria() {
|
||||
Criteria criteria = new Criteria();
|
||||
return criteria;
|
||||
}
|
||||
|
||||
public static class Criteria {
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-spring-model-'));
|
||||
const files: Record<string, string> = {
|
||||
'pom.xml': '<project><dependency>spring-boot-starter</dependency></project>\n',
|
||||
'mall-mbg/src/main/java/com/macro/mall/model/CmsHelpCategoryExample.java': example('CmsHelpCategoryExample'),
|
||||
'mall-mbg/src/main/java/com/macro/mall/model/OmsOrderExample.java': example('OmsOrderExample'),
|
||||
'mall-admin/src/main/java/com/macro/mall/service/OrderService.java': `package com.macro.mall.service;
|
||||
|
||||
@Service
|
||||
public class OrderService {
|
||||
}
|
||||
`,
|
||||
};
|
||||
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('Spring name heuristics', () => {
|
||||
it('resolve a name to its own file’s nested class, not another file’s', () => {
|
||||
const file = 'mall-mbg/src/main/java/com/macro/mall/model/OmsOrderExample.java';
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'instantiates' || e.kind === 'references')
|
||||
.map((e) => cg.getNode(e.target)!).filter((t) => t.name === 'Criteria').map((t) => t.filePath);
|
||||
expect(targets.length).toBeGreaterThan(0);
|
||||
expect(new Set(targets)).toEqual(new Set([file]));
|
||||
});
|
||||
});
|
||||
@@ -137,7 +137,7 @@ export const springResolver: FrameworkResolver = {
|
||||
|
||||
// Pattern 1: Service references (dependency injection)
|
||||
if (ref.referenceName.endsWith('Service')) {
|
||||
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
|
||||
const result = resolveByNameAndKind(ref, SERVICE_KINDS, SERVICE_DIRS, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -150,7 +150,7 @@ export const springResolver: FrameworkResolver = {
|
||||
|
||||
// Pattern 2: Repository references
|
||||
if (ref.referenceName.endsWith('Repository')) {
|
||||
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, REPO_DIRS, context);
|
||||
const result = resolveByNameAndKind(ref, SERVICE_KINDS, REPO_DIRS, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -163,7 +163,7 @@ export const springResolver: FrameworkResolver = {
|
||||
|
||||
// Pattern 3: Controller references
|
||||
if (ref.referenceName.endsWith('Controller')) {
|
||||
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, CONTROLLER_DIRS, context);
|
||||
const result = resolveByNameAndKind(ref, CLASS_KINDS, CONTROLLER_DIRS, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -176,7 +176,7 @@ export const springResolver: FrameworkResolver = {
|
||||
|
||||
// Pattern 4: Entity/Model references
|
||||
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
||||
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, ENTITY_DIRS, context);
|
||||
const result = resolveByNameAndKind(ref, CLASS_KINDS, ENTITY_DIRS, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -189,7 +189,7 @@ export const springResolver: FrameworkResolver = {
|
||||
|
||||
// Pattern 5: Component references
|
||||
if (ref.referenceName.endsWith('Component') || ref.referenceName.endsWith('Config')) {
|
||||
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, COMPONENT_DIRS, context);
|
||||
const result = resolveByNameAndKind(ref, CLASS_KINDS, COMPONENT_DIRS, context);
|
||||
if (result) {
|
||||
return {
|
||||
original: ref,
|
||||
@@ -555,19 +555,30 @@ function joinPath(prefix: string, sub: string): string {
|
||||
|
||||
/**
|
||||
* Resolve a symbol by name using indexed queries instead of scanning all files.
|
||||
* What the reference's own scope declares comes first — its file, then its
|
||||
* package (directory) — and a class nested in another file's class is out of
|
||||
* reach by its bare name: every MyBatis `XExample` declares its own nested
|
||||
* `Criteria`, and mall's 8,747 `new Criteria()` went to the first one.
|
||||
*/
|
||||
function resolveByNameAndKind(
|
||||
name: string,
|
||||
ref: UnresolvedRef,
|
||||
kinds: Set<string>,
|
||||
preferredDirPatterns: string[],
|
||||
context: ResolutionContext,
|
||||
): string | null {
|
||||
const candidates = context.getNodesByName(name);
|
||||
const candidates = context.getNodesByName(ref.referenceName);
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
|
||||
const kindFiltered = candidates.filter((n) => kinds.has(n.kind) &&
|
||||
(n.filePath === ref.filePath || !isNestedType(n, context)));
|
||||
if (kindFiltered.length === 0) return null;
|
||||
|
||||
const sameFile = kindFiltered.find((n) => n.filePath === ref.filePath);
|
||||
if (sameFile) return sameFile.id;
|
||||
const dir = ref.filePath.slice(0, ref.filePath.lastIndexOf('/') + 1);
|
||||
const samePackage = kindFiltered.find((n) => n.filePath.startsWith(dir) && !n.filePath.slice(dir.length).includes('/'));
|
||||
if (samePackage) return samePackage.id;
|
||||
|
||||
// Prefer candidates in framework-conventional directories
|
||||
const preferred = kindFiltered.filter((n) =>
|
||||
preferredDirPatterns.some((d) => n.filePath.includes(d))
|
||||
@@ -578,3 +589,12 @@ function resolveByNameAndKind(
|
||||
// Fall back to any match
|
||||
return kindFiltered[0]!.id;
|
||||
}
|
||||
|
||||
/** Whether a JVM type is declared inside another type of its file. */
|
||||
function isNestedType(n: Node, context: ResolutionContext): boolean {
|
||||
const cut = n.qualifiedName.lastIndexOf('::');
|
||||
if (cut < 0) return false;
|
||||
const owner = n.qualifiedName.slice(0, cut);
|
||||
return context.getNodesInFile(n.filePath).some((p) =>
|
||||
p.qualifiedName === owner && (p.kind === 'class' || p.kind === 'interface' || p.kind === 'enum' || p.kind === 'struct' || p.kind === 'trait'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user