mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-12 23:02:45 +00:00
* 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>
220 lines
8.2 KiB
TypeScript
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);
|
|
});
|