fix(schema): persist Spring constructor-to-bean injection edges (#3239)

Co-authored-by: Jayson Albert <momeijw@gamil.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
JaysonAlbert 2026-09-10 23:03:53 +08:00 committed by GitHub
parent 3236e2fbcd
commit e7141ab0c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 72 additions and 1 deletions

View file

@ -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\`,

View file

@ -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;
}
}

View file

@ -0,0 +1,3 @@
package example;
public interface Handler {}

View file

@ -0,0 +1,9 @@
package example;
import java.util.Map;
public class SpringContextUtil {
public static <T> Map<String, T> getBeans(Class<T> type) {
return Map.of();
}
}

View file

@ -0,0 +1,7 @@
package example;
public class Worker {
public Worker() {
SpringContextUtil.getBeans(Handler.class);
}
}

View file

@ -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);

View file

@ -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.