GitNexus/gitnexus/test/integration/spring-dynamic-lookup.test.ts
ChunxueLi 19f6731c34
feat(java): resolve SpringContextUtil.getBeans(X.class) dynamic lookups (#2886)
* feat(java): resolve SpringContextUtil.getBeans(X.class) dynamic lookups

* fix(ingestion): make Spring dynamic lookups graph-correct

Capture Java and Kotlin lookups from ASTs and resolve them through scoped type bindings and transitive JVM assignability so emitted INJECTS edges are attributable, cache-safe, and production-tested.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(ingestion): keep Spring lookup capture linear

Reuse Java and Kotlin scope-query call nodes instead of rewalking each AST, cache DI subtype closures, and enforce linear scaling with production-path benchmarks in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 23:09:21 +00:00

220 lines
8.2 KiB
TypeScript

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
getRelationships,
runPipelineFromRepo,
writeFixtureRepo,
type PipelineResult,
} from './resolvers/helpers.js';
const temporaryRepositories: string[] = [];
function temporaryRepository(prefix: string): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
temporaryRepositories.push(root);
return root;
}
function injectionEdges(result: PipelineResult) {
return getRelationships(result, 'INJECTS').map((edge) => ({
source: edge.source,
target: edge.target,
type: edge.rel.type,
confidence: edge.rel.confidence,
reason: edge.rel.reason,
}));
}
afterEach(() => {
for (const root of temporaryRepositories.splice(0)) {
fs.rmSync(root, { recursive: true, force: true });
}
});
describe('Spring dynamic lookup production integration', () => {
it('resolves Java imports, assignability, concrete targets, ranges, and nested callables', async () => {
const root = temporaryRepository('gitnexus-java-spring-dynamic-');
writeFixtureRepo(root, {
'src/a/Parent.java': `package a;
interface Parent {}
interface Child extends Parent {}
class Impl implements Child {}
class SecondImpl implements Child {}`,
'src/a/Base.java': 'package a; public class Base {}',
'src/a/Concrete.java': 'package a; public class Concrete extends Base {}',
'src/a/RecordBean.java': 'package a; public record RecordBean(String value) {}',
'src/b/Parent.java': 'package b; public interface Parent {}',
'src/b/OtherImpl.java': 'package b; public class OtherImpl implements Parent {}',
'src/a/SamePackageCaller.java': `package a;
class SamePackageCaller {
void samePackageLookup() { ctx.getBeans(Parent.class); }
}`,
'src/c/JavaCaller.java': `package c;
import a.Parent;
import a.Base;
import a.Concrete;
import a.RecordBean;
class JavaCaller {
JavaCaller() { beanFactory.getBean(Concrete.class); }
void javaCollection(){ SpringContextUtil.getBeans(Parent.class); }
void adjacentMiss(){}
void javaBase(){ applicationContext.getBeansOfType(Base.class); }
void javaSingle(){ ctx.getBean(Concrete.class); }
void javaRecord(){ ctx.getBean(RecordBean.class); }
void javaAmbiguousSingle(){ ctx.getBean(Parent.class); }
void javaOuter() {
Runnable task = new Runnable() {
public void run() { ctx.getBeans(Parent.class); }
};
}
void javaFalsePositives() {
// ctx.getBeans(Parent.class);
/* applicationContext.getBeansOfType(Parent.class); */
String normal = "ctx.getBean(Concrete.class)";
String block = """
ctx.getBeans(Parent.class)
""";
}
}`,
'src/c/AmbiguousCaller.java': `package c;
import a.*;
import b.*;
class AmbiguousCaller {
void ambiguousLookup() { ctx.getBeans(Parent.class); }
}`,
'src/foreign.ts': 'interface Parent {}',
});
const result = await runPipelineFromRepo(root, () => {});
const edges = injectionEdges(result);
expect(edges).toEqual(
expect.arrayContaining([
{
source: 'javaCollection',
target: 'Impl',
type: 'INJECTS',
confidence: 0.8,
reason: 'Spring dynamic lookup: SpringContextUtil.getBeans(Parent)',
},
{
source: 'javaCollection',
target: 'SecondImpl',
type: 'INJECTS',
confidence: 0.8,
reason: 'Spring dynamic lookup: SpringContextUtil.getBeans(Parent)',
},
expect.objectContaining({ source: 'javaBase', target: 'Concrete', confidence: 0.8 }),
{
source: 'javaSingle',
target: 'Concrete',
type: 'INJECTS',
confidence: 0.9,
reason: 'Spring dynamic lookup: ctx.getBean(Concrete)',
},
expect.objectContaining({
source: 'javaRecord',
target: 'RecordBean',
confidence: 0.9,
}),
expect.objectContaining({
source: 'javaAmbiguousSingle',
target: 'Impl',
confidence: 0.5,
}),
expect.objectContaining({
source: 'javaAmbiguousSingle',
target: 'SecondImpl',
confidence: 0.5,
}),
expect.objectContaining({ source: 'run', target: 'Impl', confidence: 0.8 }),
expect.objectContaining({ source: 'samePackageLookup', target: 'Impl', confidence: 0.8 }),
]),
);
expect(edges.some((edge) => edge.source === 'adjacentMiss')).toBe(false);
expect(edges.some((edge) => edge.source === 'javaOuter')).toBe(false);
expect(edges.some((edge) => edge.source === 'javaFalsePositives')).toBe(false);
expect(edges.some((edge) => edge.source === 'ambiguousLookup')).toBe(false);
expect(
edges
.filter((edge) => edge.source === 'javaAmbiguousSingle')
.every((edge) => edge.reason.includes('ambiguous candidates: Impl, SecondImpl')),
).toBe(true);
expect(
edges.some(
(edge) =>
edge.source === 'javaCollection' &&
(edge.target === 'Child' || edge.target === 'OtherImpl'),
),
).toBe(false);
expect(edges.some((edge) => edge.target === 'Concrete' && edge.confidence === 0.5)).toBe(false);
expect(
edges.filter((edge) => edge.source === 'JavaCaller' && edge.target === 'Concrete'),
).toHaveLength(1);
}, 60000);
it('resolves Kotlin class literals through the same graph semantics', async () => {
const root = temporaryRepository('gitnexus-kotlin-spring-dynamic-');
writeFixtureRepo(root, {
'src/a/Parent.kt': 'package a\ninterface Parent',
'src/a/Child.kt': 'package a\ninterface Child : Parent',
'src/a/Impl.kt': 'package a\nclass Impl : Child',
'src/a/Concrete.kt': 'package a\nopen class Base\nclass Concrete : Base()',
'src/c/KotlinCaller.kt': `package c
import a.Parent
import a.Base
import a.Concrete
class KotlinCaller {
constructor(marker: String) { beanFactory.getBean(Concrete::class.java) }
fun kotlinCollection(){ SpringContextUtil.getBeans(Parent::class.java) }
fun adjacentMiss(){}
fun kotlinBase(){ applicationContext.getBeansOfType(Base::class.java) }
fun kotlinSingle(){ ctx.getBean(Concrete::class) }
fun kotlinOuter() {
val task = object : Runnable {
override fun run() { ctx.getBeans(Parent::class.java) }
}
}
fun kotlinFalsePositives() {
// ctx.getBeans(Parent::class.java)
/* applicationContext.getBeansOfType(Parent::class.java) */
val normal = "ctx.getBean(Concrete::class.java)"
val raw = ${'"""'}ctx.getBeans(Parent::class.java)${'"""'}
}
}`,
'src/foreign.ts': 'interface Parent {}',
});
const result = await runPipelineFromRepo(root, () => {});
const edges = injectionEdges(result);
expect(edges).toEqual(
expect.arrayContaining([
{
source: 'kotlinCollection',
target: 'Impl',
type: 'INJECTS',
confidence: 0.8,
reason: 'Spring dynamic lookup: SpringContextUtil.getBeans(Parent)',
},
expect.objectContaining({ source: 'kotlinBase', target: 'Concrete', confidence: 0.8 }),
{
source: 'kotlinSingle',
target: 'Concrete',
type: 'INJECTS',
confidence: 0.9,
reason: 'Spring dynamic lookup: ctx.getBean(Concrete)',
},
expect.objectContaining({ source: 'run', target: 'Impl', confidence: 0.8 }),
]),
);
expect(edges.some((edge) => edge.source === 'adjacentMiss')).toBe(false);
expect(edges.some((edge) => edge.source === 'kotlinOuter')).toBe(false);
expect(edges.some((edge) => edge.source === 'kotlinFalsePositives')).toBe(false);
expect(
edges.filter((edge) => edge.source === 'constructor' && edge.target === 'Concrete'),
).toHaveLength(1);
}, 60000);
});