mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(spring): gate DI convention patterns to Java/Kotlin non-inheritance refs (#1827)
In a polyglot repo the Spring resolver is detected globally (a sibling
Java module with spring-boot in its pom is enough), and its resolve()
then ran on references from every language. Pattern 4's bare-name
fallback ('any [A-Z][a-zA-Z]+ name, prefer /model/ dirs, else ANY
same-named class') could therefore hijack a Scala 'extends ExtCustomer'
to an unrelated same-named test class, corrupting the inheritance graph
that impact analysis walks.
Two gates in front of the DI/convention patterns (1-5):
- skip refs whose language is not java/kotlin - Spring DI conventions
say nothing about other languages
- skip extends/implements refs entirely - inheriting from a class is
never a DI injection point; those must resolve via imports/name
matching
The Spring config-key resolution above the gates is untouched (it
already gates itself). Regression tests cover the hijack case and the
still-working Java DI path.
Co-authored-by: Hubert Tarnacki <hubert.tarnacki@comarch.com>
This commit is contained in:
@@ -283,6 +283,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
- Inheritance relationships no longer attach external Rust or npm supertypes to unrelated local symbols with the same name, including in Svelte, Vue and Astro components; re-index after upgrading to clear existing false relationships. Thanks @ctype-lab. (#1536)
|
||||
|
||||
- Spring's dependency-injection resolution patterns no longer capture inheritance references or references from other languages. In a polyglot repository where Spring is detected (a sibling Java module is enough), a Scala or Kotlin `extends`/`implements` reference could be resolved by Spring's directory-convention heuristics to an unrelated same-named class — for example a test fixture — corrupting the inheritance graph that `codegraph_impact` and `codegraph_explore` walk. Inheritance now always resolves through imports and name matching, and the Spring DI patterns only apply to Java/Kotlin references. Re-index after upgrading. (#1825)
|
||||
|
||||
- PHP static calls through imported class aliases now reach the correct class when services and repositories share method names, so callers and impact analysis show the right dependencies after re-indexing. (#1545)
|
||||
- TypeScript/JavaScript: a call through a field of the enclosing class — `this.mailer.send()` — now resolves on the field's declared type, so a delegating wrapper that shares the method's name no longer records itself as its own callee and `callers`, `impact` and trace stop lying on that shape. A field whose type is external or a builtin stays unresolved rather than guessed. Re-index after upgrading. (#1496)
|
||||
- TypeScript and JavaScript collection calls through local variables and their nested properties no longer link to unrelated project methods; re-index after upgrading. (#1566)
|
||||
|
||||
@@ -992,6 +992,77 @@ class OwnerController {
|
||||
});
|
||||
});
|
||||
|
||||
describe('springResolver.resolve — DI heuristics are gated to Java/Kotlin non-inheritance refs', () => {
|
||||
// A polyglot repo (Scala + a sibling Java module) detects Spring globally.
|
||||
// The DI/convention patterns (bare-name Pattern 4 especially) must then not
|
||||
// hijack a Scala `extends X` to a same-named class found via directory
|
||||
// heuristics — inheritance must resolve through imports/name matching.
|
||||
const decoyNode: Node = {
|
||||
id: 'class:src/test/model/ExtCustomer.java:ExtCustomer:3',
|
||||
kind: 'class',
|
||||
name: 'ExtCustomer',
|
||||
qualifiedName: 'src/test/model/ExtCustomer.java::ExtCustomer',
|
||||
filePath: 'src/test/model/ExtCustomer.java',
|
||||
language: 'java',
|
||||
startLine: 3,
|
||||
endLine: 10,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const context = {
|
||||
getNodesInFile: () => [],
|
||||
getNodesByName: (n: string) => (n === 'ExtCustomer' ? [decoyNode] : []),
|
||||
getNodesByQualifiedName: () => [],
|
||||
getNodesByKind: () => [],
|
||||
fileExists: () => false,
|
||||
readFile: () => null,
|
||||
getProjectRoot: () => '/test',
|
||||
getAllFiles: () => [],
|
||||
getNodesByLowerName: () => [],
|
||||
getImportMappings: () => [],
|
||||
};
|
||||
const baseRef = {
|
||||
fromNodeId: 'class:A.scala:MExtCustomer:5',
|
||||
referenceName: 'ExtCustomer',
|
||||
line: 5,
|
||||
column: 10,
|
||||
filePath: 'A.scala',
|
||||
};
|
||||
|
||||
it('does NOT resolve a Scala extends reference (Pattern 4 bare-name fallback)', () => {
|
||||
const ref = { ...baseRef, referenceKind: 'extends' as const, language: 'scala' as const };
|
||||
expect(springResolver.resolve(ref, context as any)).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT resolve a non-Java/Kotlin plain reference either', () => {
|
||||
const ref = { ...baseRef, referenceKind: 'references' as const, language: 'scala' as const };
|
||||
expect(springResolver.resolve(ref, context as any)).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT resolve a Java extends reference — inheritance is never a DI pattern', () => {
|
||||
const ref = {
|
||||
...baseRef,
|
||||
filePath: 'B.java',
|
||||
referenceKind: 'extends' as const,
|
||||
language: 'java' as const,
|
||||
};
|
||||
expect(springResolver.resolve(ref, context as any)).toBeNull();
|
||||
});
|
||||
|
||||
it('still resolves a Java DI reference through the entity pattern', () => {
|
||||
const ref = {
|
||||
...baseRef,
|
||||
filePath: 'B.java',
|
||||
referenceKind: 'references' as const,
|
||||
language: 'java' as const,
|
||||
};
|
||||
const result = springResolver.resolve(ref, context as any);
|
||||
expect(result?.targetNodeId).toBe(decoyNode.id);
|
||||
expect(result?.resolvedBy).toBe('framework');
|
||||
});
|
||||
});
|
||||
|
||||
import { playResolver } from '../src/resolution/frameworks/play';
|
||||
import { isSourceFile, isPlayRoutesFile } from '../src/extraction/grammars';
|
||||
|
||||
|
||||
@@ -126,6 +126,15 @@ export const springResolver: FrameworkResolver = {
|
||||
}
|
||||
}
|
||||
|
||||
// The DI/convention patterns below (1–5) are Spring-specific heuristics:
|
||||
// they must only fire on Java/Kotlin refs, and never on inheritance refs.
|
||||
// Without these gates a Scala `extends ExtCustomer` (Spring detected via a
|
||||
// sibling Java module) was hijacked by Pattern 4's bare-name fallback to an
|
||||
// unrelated same-named test class — `extends`/`implements` must resolve via
|
||||
// imports/name matching, not directory heuristics.
|
||||
if (ref.language !== 'java' && ref.language !== 'kotlin') return null;
|
||||
if (ref.referenceKind === 'extends' || ref.referenceKind === 'implements') return null;
|
||||
|
||||
// Pattern 1: Service references (dependency injection)
|
||||
if (ref.referenceName.endsWith('Service')) {
|
||||
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
|
||||
|
||||
Reference in New Issue
Block a user