From 2668d5cc7e33d6a81393e801c8d63db6c938d9b7 Mon Sep 17 00:00:00 2001 From: Hubert Tarnacki Date: Mon, 31 Aug 2026 09:22:16 +0200 Subject: [PATCH] fix(spring): gate DI convention patterns to Java/Kotlin non-inheritance refs 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. --- CHANGELOG.md | 2 + __tests__/frameworks.test.ts | 71 +++++++++++++++++++++++++++++++ src/resolution/frameworks/java.ts | 9 ++++ 3 files changed, 82 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b68b091..1f0a9a6e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -265,6 +265,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) diff --git a/__tests__/frameworks.test.ts b/__tests__/frameworks.test.ts index e467ab612..27919c3d1 100644 --- a/__tests__/frameworks.test.ts +++ b/__tests__/frameworks.test.ts @@ -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'; diff --git a/src/resolution/frameworks/java.ts b/src/resolution/frameworks/java.ts index ff31c846b..bd36b1e4e 100644 --- a/src/resolution/frameworks/java.ts +++ b/src/resolution/frameworks/java.ts @@ -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);