GitNexus/gitnexus/test/unit/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

179 lines
5.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { getJavaParser } from '../../src/core/ingestion/languages/java/query.js';
import { captureJavaSpringDynamicLookupFacts } from '../../src/core/ingestion/languages/java/spring-dynamic-lookup.js';
import { getKotlinParser } from '../../src/core/ingestion/languages/kotlin/query.js';
import { captureKotlinSpringDynamicLookupFacts } from '../../src/core/ingestion/languages/kotlin/spring-dynamic-lookup.js';
function javaFacts(source: string) {
return captureJavaSpringDynamicLookupFacts(
getJavaParser().parse(source).rootNode,
'src/Example.java',
);
}
function kotlinFacts(source: string) {
return captureKotlinSpringDynamicLookupFacts(
getKotlinParser().parse(source).rootNode,
'src/Example.kt',
);
}
describe('Java Spring dynamic lookup capture', () => {
it('captures collection and singular class-literal calls from known receivers', () => {
const facts = javaFacts(`class Example {
void load() {
SpringContextUtil.getBeans(Port.class);
ApplicationContext.getBeansOfType(com.example.Service.class);
ctx.getBean(Concrete.class);
}
}`);
expect(
facts.map(({ receiverName, methodName, targetTypeName }) => ({
receiverName,
methodName,
targetTypeName,
})),
).toEqual([
{
receiverName: 'SpringContextUtil',
methodName: 'getBeans',
targetTypeName: 'Port',
},
{
receiverName: 'ApplicationContext',
methodName: 'getBeansOfType',
targetTypeName: 'com.example.Service',
},
{ receiverName: 'ctx', methodName: 'getBean', targetTypeName: 'Concrete' },
]);
});
it('assigns adjacent one-line calls only to their actual callable', () => {
const facts = javaFacts(`class Example {
void hit(){ ctx.getBeans(Port.class); }
void miss(){}
}`);
expect(facts).toHaveLength(1);
expect(facts[0]?.ownerRange.startLine).toBe(2);
expect(facts[0]?.ownerRange.endLine).toBe(2);
});
it('assigns an anonymous-class lookup only to the nested method', () => {
const facts = javaFacts(`class Example {
void outer() {
Runnable task = new Runnable() {
public void run() { ctx.getBeans(Port.class); }
};
}
}`);
expect(facts).toHaveLength(1);
expect(facts[0]?.ownerRange.startLine).toBe(4);
expect(facts[0]?.ownerRange.endLine).toBe(4);
});
it('captures constructor lookups using normal callable semantics', () => {
const facts = javaFacts(`class Example {
Example() { beanFactory.getBean(Concrete.class); }
}`);
expect(facts).toHaveLength(1);
expect(facts[0]?.methodName).toBe('getBean');
});
it('ignores comments, Javadoc, strings, text blocks, and unsupported calls', () => {
const facts = javaFacts(`class Example {
/**
* ctx.getBeans(Port.class)
*/
void load() {
// ctx.getBeans(Port.class);
/* applicationContext.getBeansOfType(Port.class); */
String normal = "ctx.getBean(Port.class)";
String block = """
ctx.getBeans(Port.class)
""";
unrelated.getBeans(Port.class);
ctx.getBean("namedBean");
}
}`);
expect(facts).toEqual([]);
});
});
describe('Kotlin Spring dynamic lookup capture', () => {
it('captures Kotlin class literals with and without the Java bridge', () => {
const facts = kotlinFacts(`class Example {
fun load() {
SpringContextUtil.getBeans(Port::class.java)
applicationContext.getBeansOfType(com.example.Service::class.java)
ctx.getBean(Concrete::class)
}
}`);
expect(
facts.map(({ receiverName, methodName, targetTypeName }) => ({
receiverName,
methodName,
targetTypeName,
})),
).toEqual([
{
receiverName: 'SpringContextUtil',
methodName: 'getBeans',
targetTypeName: 'Port',
},
{
receiverName: 'applicationContext',
methodName: 'getBeansOfType',
targetTypeName: 'com.example.Service',
},
{ receiverName: 'ctx', methodName: 'getBean', targetTypeName: 'Concrete' },
]);
});
it('assigns an object-expression lookup only to the nested method', () => {
const facts = kotlinFacts(`class Example {
fun outer() {
val task = object : Runnable {
override fun run() { ctx.getBeans(Port::class.java) }
}
}
}`);
expect(facts).toHaveLength(1);
expect(facts[0]?.ownerRange.startLine).toBe(4);
expect(facts[0]?.ownerRange.endLine).toBe(4);
});
it('captures secondary-constructor lookups and excludes init blocks without graph callables', () => {
const facts = kotlinFacts(`class Example {
init { ctx.getBeans(Ignored::class.java) }
constructor(marker: String) { ctx.getBean(Concrete::class.java) }
}`);
expect(facts).toHaveLength(1);
expect(facts[0]?.targetTypeName).toBe('Concrete');
});
it('ignores comments, KDoc, strings, raw strings, and unsupported calls', () => {
const facts = kotlinFacts(`class Example {
/**
* ctx.getBeans(Port::class.java)
*/
fun load() {
// ctx.getBeans(Port::class.java)
/* applicationContext.getBeansOfType(Port::class.java) */
val normal = "ctx.getBean(Port::class.java)"
val raw = ${'"""'}ctx.getBeans(Port::class.java)${'"""'}
unrelated.getBeans(Port::class.java)
ctx.getBean("namedBean")
}
}`);
expect(facts).toEqual([]);
});
});