From e7141ab0c1f9c2cb1554ea239c1499ee4c0c85d8 Mon Sep 17 00:00:00 2001 From: JaysonAlbert Date: Thu, 10 Sep 2026 23:03:53 +0800 Subject: [PATCH] fix(schema): persist Spring constructor-to-bean injection edges (#3239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jayson Albert Co-authored-by: Gergő Magyar --- gitnexus/src/core/lbug/schema.ts | 5 +++- .../src/example/AppConfig.java | 12 ++++++++ .../src/example/Handler.java | 3 ++ .../src/example/SpringContextUtil.java | 9 ++++++ .../src/example/Worker.java | 7 +++++ .../spring-constructor-bean-roundtrip.test.ts | 30 +++++++++++++++++++ .../structural-pair-coverage.test.ts | 7 +++++ 7 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/AppConfig.java create mode 100644 gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/Handler.java create mode 100644 gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/SpringContextUtil.java create mode 100644 gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/Worker.java create mode 100644 gitnexus/test/integration/spring-constructor-bean-roundtrip.test.ts diff --git a/gitnexus/src/core/lbug/schema.ts b/gitnexus/src/core/lbug/schema.ts index 5abc7bc46..3d3edd8ac 100644 --- a/gitnexus/src/core/lbug/schema.ts +++ b/gitnexus/src/core/lbug/schema.ts @@ -518,7 +518,7 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [ ]; /** - * The 66 pairs NEITHER rule above generates — everything left after the two + * The pairs NEITHER rule above generates — everything left after the two * cross products are subtracted. Carried by CONTAINMENT, inheritance, imports * and DI: a container label crossed with a contained label. No predicate * describes that surface (any container can hold any definition). @@ -531,6 +531,8 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [ * templates), the two `Route|Process` / `Tool|Process` entry points whose * emitter names both labels as literals, and `BasicBlock|BasicBlock`, the PDG * substrate. + * Spring dynamic lookup in a constructor also emits INJECTS to a synthetic + * @Bean CodeElement (#3238), so Constructor|CodeElement belongs here. * * NOTHING A RULE ALREADY COVERS BELONGS HERE. `generatedRelationPairs` skips * any pair present in this block, so a redundant line does not merely duplicate @@ -624,6 +626,7 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder, FROM \`Constructor\` TO \`Impl\`, FROM \`Constructor\` TO \`Namespace\`, FROM \`Constructor\` TO \`Typedef\`, + FROM \`Constructor\` TO CodeElement, FROM Route TO Process, FROM Tool TO Process, FROM Destination TO \`Property\`, diff --git a/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/AppConfig.java b/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/AppConfig.java new file mode 100644 index 000000000..2f06e41d4 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/AppConfig.java @@ -0,0 +1,12 @@ +package example; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + @Bean + public Handler handler() { + return null; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/Handler.java b/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/Handler.java new file mode 100644 index 000000000..2e2c8f480 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/Handler.java @@ -0,0 +1,3 @@ +package example; + +public interface Handler {} diff --git a/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/SpringContextUtil.java b/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/SpringContextUtil.java new file mode 100644 index 000000000..045e01021 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/SpringContextUtil.java @@ -0,0 +1,9 @@ +package example; + +import java.util.Map; + +public class SpringContextUtil { + public static Map getBeans(Class type) { + return Map.of(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/Worker.java b/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/Worker.java new file mode 100644 index 000000000..eda02aca2 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/spring-constructor-bean-lookup/src/example/Worker.java @@ -0,0 +1,7 @@ +package example; + +public class Worker { + public Worker() { + SpringContextUtil.getBeans(Handler.class); + } +} diff --git a/gitnexus/test/integration/spring-constructor-bean-roundtrip.test.ts b/gitnexus/test/integration/spring-constructor-bean-roundtrip.test.ts new file mode 100644 index 000000000..4f6cad103 --- /dev/null +++ b/gitnexus/test/integration/spring-constructor-bean-roundtrip.test.ts @@ -0,0 +1,30 @@ +/** Regression for #3238: a constructor's dynamic @Bean lookup must survive COPY. */ +import { expect, it } from 'vitest'; +import path from 'path'; +import { FIXTURES, runPipelineFromRepo } from './resolvers/helpers.js'; +import { createTempDir } from '../helpers/test-db.js'; +import * as adapter from '../../src/core/lbug/lbug-adapter.js'; + +it('persists the constructor-to-factory-bean INJECTS edge from the real pipeline', async () => { + const fixture = path.join(FIXTURES, 'spring-constructor-bean-lookup'); + const result = await runPipelineFromRepo(fixture, () => {}, { workerPoolSize: 1 }); + const temp = await createTempDir(); + try { + await adapter.initLbug(path.join(temp.dbPath, 'lbug')); + await adapter.loadGraphToLbug(result.graph, fixture, temp.dbPath); + const rows = await adapter.executeQuery( + "MATCH (c:`Constructor`)-[r:CodeRelation]->(b:CodeElement) WHERE r.type = 'INJECTS' " + + 'RETURN c.filePath AS sourceFile, b.filePath AS targetFile, b.name AS beanName', + ); + expect(rows).toEqual([ + { + sourceFile: 'src/example/Worker.java', + targetFile: 'src/example/AppConfig.java', + beanName: 'handler', + }, + ]); + } finally { + await adapter.closeLbug(); + await temp.cleanup(); + } +}, 120_000); diff --git a/gitnexus/test/integration/structural-pair-coverage.test.ts b/gitnexus/test/integration/structural-pair-coverage.test.ts index 0c9775d36..8691349b1 100644 --- a/gitnexus/test/integration/structural-pair-coverage.test.ts +++ b/gitnexus/test/integration/structural-pair-coverage.test.ts @@ -71,6 +71,13 @@ type CorpusEntry = { * exists to catch. */ const NON_BRIDGE_CORPUS = [ + { + // Dynamic Spring lookup inside a constructor targets a synthetic @Bean + // CodeElement, not the bean's declared return-type Class/Interface (#3238). + fixture: 'spring-constructor-bean-lookup', + emitter: 'spring constructor INJECTS', + sentinels: ['Constructor|CodeElement'], + }, { // `cobol-processor.ts`: CONTAINS/CALLS/ACCESSES over Module / Namespace / // Record / Property / CodeElement.