gitnexus 1.6.11-rc.16 → 1.6.11-rc.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/java/captures.js +12 -1
- package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +12 -1
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
- package/dist/core/ingestion/pipeline-phases/di.js +47 -14
- package/dist/storage/parse-cache.js +5 -1
- package/package.json +2 -1
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
3
4
|
import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js';
|
|
4
5
|
import { logger } from '../../logger.js';
|
|
6
|
+
// POMs are static metadata. Parse hierarchy with a real XML parser, but do not
|
|
7
|
+
// invoke Maven or resolve the effective model. Properties, profiles, and remote
|
|
8
|
+
// parent resolution remain outside this extractor's deterministic boundary.
|
|
9
|
+
const pomParser = new XMLParser({
|
|
10
|
+
ignoreAttributes: true,
|
|
11
|
+
removeNSPrefix: true,
|
|
12
|
+
trimValues: true,
|
|
13
|
+
parseTagValue: false,
|
|
14
|
+
processEntities: false,
|
|
15
|
+
ignoreDeclaration: true,
|
|
16
|
+
ignorePiTags: true,
|
|
17
|
+
});
|
|
5
18
|
async function parseJavaManifest(repoPath) {
|
|
6
19
|
const pomPath = path.join(repoPath, 'pom.xml');
|
|
7
20
|
try {
|
|
@@ -9,13 +22,14 @@ async function parseJavaManifest(repoPath) {
|
|
|
9
22
|
return parsePom(content);
|
|
10
23
|
}
|
|
11
24
|
catch {
|
|
12
|
-
// fall through to Gradle
|
|
25
|
+
// Missing pom.xml — fall through to Gradle.
|
|
13
26
|
}
|
|
27
|
+
const gradleSidecars = await readGradleSidecars(repoPath);
|
|
14
28
|
for (const name of ['build.gradle.kts', 'build.gradle']) {
|
|
15
29
|
const gradlePath = path.join(repoPath, name);
|
|
16
30
|
try {
|
|
17
31
|
const content = await fs.readFile(gradlePath, 'utf-8');
|
|
18
|
-
return parseGradle(content, repoPath);
|
|
32
|
+
return parseGradle(content, repoPath, gradleSidecars);
|
|
19
33
|
}
|
|
20
34
|
catch {
|
|
21
35
|
continue;
|
|
@@ -23,45 +37,244 @@ async function parseJavaManifest(repoPath) {
|
|
|
23
37
|
}
|
|
24
38
|
return null;
|
|
25
39
|
}
|
|
40
|
+
async function readIfPresent(filePath) {
|
|
41
|
+
try {
|
|
42
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function readGradleSidecars(repoPath) {
|
|
49
|
+
const [properties, settingsKts, settingsGroovy, catalog] = await Promise.all([
|
|
50
|
+
readIfPresent(path.join(repoPath, 'gradle.properties')),
|
|
51
|
+
readIfPresent(path.join(repoPath, 'settings.gradle.kts')),
|
|
52
|
+
readIfPresent(path.join(repoPath, 'settings.gradle')),
|
|
53
|
+
readIfPresent(path.join(repoPath, 'gradle', 'libs.versions.toml')),
|
|
54
|
+
]);
|
|
55
|
+
const sidecars = {
|
|
56
|
+
catalogLibraries: new Map(),
|
|
57
|
+
catalogBundles: new Map(),
|
|
58
|
+
};
|
|
59
|
+
const groupMatch = properties?.match(/(?:^|\n)\s*group\s*=\s*([^\s#]+)/);
|
|
60
|
+
if (groupMatch)
|
|
61
|
+
sidecars.propertiesGroup = groupMatch[1];
|
|
62
|
+
const settings = settingsKts ?? settingsGroovy;
|
|
63
|
+
const nameMatch = settings?.match(/rootProject\.name\s*=\s*['"]([^'"]+)['"]/);
|
|
64
|
+
if (nameMatch)
|
|
65
|
+
sidecars.rootProjectName = nameMatch[1];
|
|
66
|
+
if (catalog) {
|
|
67
|
+
const parsed = parseGradleVersionCatalog(catalog);
|
|
68
|
+
sidecars.catalogLibraries = parsed.libraries;
|
|
69
|
+
sidecars.catalogBundles = parsed.bundles;
|
|
70
|
+
}
|
|
71
|
+
return sidecars;
|
|
72
|
+
}
|
|
73
|
+
function catalogAccessors(alias) {
|
|
74
|
+
const dotted = alias.replace(/[-_]/g, '.');
|
|
75
|
+
const camel = alias.replace(/[-_]+([A-Za-z0-9])/g, (_, char) => char.toUpperCase());
|
|
76
|
+
return [...new Set([alias, dotted, camel])];
|
|
77
|
+
}
|
|
78
|
+
function projectAccessorToArtifactId(accessor) {
|
|
79
|
+
const last = accessor.split('.').pop();
|
|
80
|
+
return last.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`).replace(/^-/, '');
|
|
81
|
+
}
|
|
82
|
+
function moduleToGa(module) {
|
|
83
|
+
const parts = module.split(':');
|
|
84
|
+
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : undefined;
|
|
85
|
+
}
|
|
86
|
+
function parseInlineTomlTable(rhs) {
|
|
87
|
+
const fields = {};
|
|
88
|
+
for (const match of rhs.matchAll(/([A-Za-z0-9_-]+)\s*=\s*['"]([^'"]+)['"]/g)) {
|
|
89
|
+
fields[match[1]] = match[2];
|
|
90
|
+
}
|
|
91
|
+
return fields;
|
|
92
|
+
}
|
|
93
|
+
/** Default Gradle catalog (`gradle/libs.versions.toml`) — aliases only, no version resolution. */
|
|
94
|
+
function parseGradleVersionCatalog(toml) {
|
|
95
|
+
const libraries = new Map();
|
|
96
|
+
const bundles = new Map();
|
|
97
|
+
let section = 'other';
|
|
98
|
+
const addLibrary = (alias, ga) => {
|
|
99
|
+
for (const accessor of catalogAccessors(alias))
|
|
100
|
+
libraries.set(accessor, ga);
|
|
101
|
+
};
|
|
102
|
+
for (const raw of toml.split(/\r?\n/)) {
|
|
103
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
104
|
+
if (!line)
|
|
105
|
+
continue;
|
|
106
|
+
const header = line.match(/^\[([^\]]+)\]$/);
|
|
107
|
+
if (header) {
|
|
108
|
+
const name = header[1];
|
|
109
|
+
section =
|
|
110
|
+
name === 'libraries' || name.endsWith('.libraries')
|
|
111
|
+
? 'libraries'
|
|
112
|
+
: name === 'bundles' || name.endsWith('.bundles')
|
|
113
|
+
? 'bundles'
|
|
114
|
+
: 'other';
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (section === 'libraries') {
|
|
118
|
+
const dottedModule = line.match(/^([A-Za-z0-9._-]+)\.module\s*=\s*['"]([^'"]+)['"]$/);
|
|
119
|
+
if (dottedModule) {
|
|
120
|
+
const ga = moduleToGa(dottedModule[2]);
|
|
121
|
+
if (ga)
|
|
122
|
+
addLibrary(dottedModule[1], ga);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const assignment = line.match(/^([A-Za-z0-9._-]+)\s*=\s*(.+)$/);
|
|
126
|
+
if (!assignment)
|
|
127
|
+
continue;
|
|
128
|
+
const alias = assignment[1];
|
|
129
|
+
const rhs = assignment[2].trim();
|
|
130
|
+
const quoted = rhs.match(/^['"]([^'"]+)['"]$/);
|
|
131
|
+
if (quoted) {
|
|
132
|
+
const ga = moduleToGa(quoted[1]);
|
|
133
|
+
if (ga)
|
|
134
|
+
addLibrary(alias, ga);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const table = parseInlineTomlTable(rhs);
|
|
138
|
+
const ga = table.module
|
|
139
|
+
? moduleToGa(table.module)
|
|
140
|
+
: table.group && table.name
|
|
141
|
+
? `${table.group}:${table.name}`
|
|
142
|
+
: undefined;
|
|
143
|
+
if (ga)
|
|
144
|
+
addLibrary(alias, ga);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (section === 'bundles') {
|
|
148
|
+
const assignment = line.match(/^([A-Za-z0-9._-]+)\s*=\s*\[([^\]]*)\]$/);
|
|
149
|
+
if (!assignment)
|
|
150
|
+
continue;
|
|
151
|
+
const members = [...assignment[2].matchAll(/['"]([^'"]+)['"]/g)].map((match) => match[1]);
|
|
152
|
+
for (const accessor of catalogAccessors(assignment[1]))
|
|
153
|
+
bundles.set(accessor, members);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { libraries, bundles };
|
|
157
|
+
}
|
|
158
|
+
const GRADLE_GROUP_PATTERNS = [
|
|
159
|
+
/(?:^|[\n{;])\s*(?:rootProject\.)?group\s*=\s*['"]([^'"]+)['"]/,
|
|
160
|
+
/(?:^|[\n{;])\s*group\s+['"]([^'"]+)['"]/,
|
|
161
|
+
];
|
|
162
|
+
const GRADLE_COORD_CONFIGS = 'implementation|api|compileOnly|runtimeOnly|testImplementation|testApi|testCompileOnly|compile|kapt|ksp|commonMainImplementation|commonMainApi';
|
|
163
|
+
const CATALOG_ALIAS = '([A-Za-z0-9_]+(?:\\.[A-Za-z0-9_]+)*)(?:\\.get\\(\\)|\\.asProvider\\(\\))?';
|
|
164
|
+
function gradleDepRe(suffix) {
|
|
165
|
+
return new RegExp(`(?:${GRADLE_COORD_CONFIGS})\\s*${suffix}`, 'g');
|
|
166
|
+
}
|
|
167
|
+
function parseGradleGroup(content) {
|
|
168
|
+
for (const pattern of GRADLE_GROUP_PATTERNS) {
|
|
169
|
+
const match = content.match(pattern);
|
|
170
|
+
if (match?.[1])
|
|
171
|
+
return match[1];
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
function asXmlNode(value) {
|
|
176
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
177
|
+
? value
|
|
178
|
+
: undefined;
|
|
179
|
+
}
|
|
180
|
+
function xmlText(value) {
|
|
181
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
182
|
+
const text = String(value).trim();
|
|
183
|
+
return text || undefined;
|
|
184
|
+
}
|
|
185
|
+
const nested = asXmlNode(value)?.['#text'];
|
|
186
|
+
if (nested === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
return xmlText(nested);
|
|
189
|
+
}
|
|
190
|
+
function xmlChildText(node, name) {
|
|
191
|
+
return node ? xmlText(node[name]) : undefined;
|
|
192
|
+
}
|
|
193
|
+
function asList(value) {
|
|
194
|
+
if (value === undefined || value === null)
|
|
195
|
+
return [];
|
|
196
|
+
return Array.isArray(value) ? value : [value];
|
|
197
|
+
}
|
|
198
|
+
/** Direct project dependencies only — not BOM, profiles, or plugin classpath. */
|
|
199
|
+
function collectProjectDependencies(project, deps) {
|
|
200
|
+
const dependencies = asXmlNode(project.dependencies);
|
|
201
|
+
if (!dependencies)
|
|
202
|
+
return;
|
|
203
|
+
for (const dep of asList(dependencies.dependency)) {
|
|
204
|
+
const depNode = asXmlNode(dep);
|
|
205
|
+
const groupId = xmlChildText(depNode, 'groupId');
|
|
206
|
+
const artifactId = xmlChildText(depNode, 'artifactId');
|
|
207
|
+
if (groupId && artifactId)
|
|
208
|
+
deps.push(`${groupId}:${artifactId}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
26
211
|
function parsePom(content) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
// parseSourceSafe guards tree-sitter's Windows SIGSEGV by switching to a
|
|
215
|
+
// chunked input callback above 16 KB; XMLParser only accepts XML text, so
|
|
216
|
+
// routing POMs through it silently yields an empty document.
|
|
217
|
+
// eslint-disable-next-line gitnexus/require-safe-parse
|
|
218
|
+
parsed = pomParser.parse(content);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
30
221
|
return null;
|
|
31
|
-
const groupId = projectGroupMatch[1].trim();
|
|
32
|
-
const artifactId = projectArtifactMatch[1].trim();
|
|
33
|
-
const deps = [];
|
|
34
|
-
const depBlocks = content.matchAll(/<dependency>\s*([\s\S]*?)<\/dependency>/g);
|
|
35
|
-
for (const block of depBlocks) {
|
|
36
|
-
const gMatch = block[1].match(/<groupId>([^<]+)<\/groupId>/);
|
|
37
|
-
const aMatch = block[1].match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
38
|
-
if (gMatch && aMatch) {
|
|
39
|
-
deps.push(`${gMatch[1].trim()}:${aMatch[1].trim()}`);
|
|
40
|
-
}
|
|
41
222
|
}
|
|
223
|
+
const project = asXmlNode(asXmlNode(parsed)?.project);
|
|
224
|
+
if (!project)
|
|
225
|
+
return null;
|
|
226
|
+
// Maven inherits groupId from <parent>, but artifactId is always the
|
|
227
|
+
// project's own direct child and must never fall back to parent.artifactId.
|
|
228
|
+
const groupId = xmlChildText(project, 'groupId') ?? xmlChildText(asXmlNode(project.parent), 'groupId');
|
|
229
|
+
const artifactId = xmlChildText(project, 'artifactId');
|
|
230
|
+
if (!groupId || !artifactId)
|
|
231
|
+
return null;
|
|
232
|
+
const deps = [];
|
|
233
|
+
collectProjectDependencies(project, deps);
|
|
42
234
|
return { groupId, artifactId, deps: [...new Set(deps)] };
|
|
43
235
|
}
|
|
44
|
-
function parseGradle(content, repoPath) {
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const groupId = groupMatch ? groupMatch[1] : '';
|
|
236
|
+
function parseGradle(content, repoPath, sidecars = { catalogLibraries: new Map(), catalogBundles: new Map() }) {
|
|
237
|
+
// Static text + default catalog file. Do not execute Gradle.
|
|
238
|
+
const groupId = parseGradleGroup(content) ?? sidecars.propertiesGroup ?? '';
|
|
48
239
|
if (!groupId)
|
|
49
240
|
return null;
|
|
50
|
-
const artifactId =
|
|
241
|
+
const artifactId = sidecars.rootProjectName ?? path.basename(repoPath);
|
|
242
|
+
const { catalogLibraries, catalogBundles } = sidecars;
|
|
51
243
|
const deps = [];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
244
|
+
const pushCatalogAlias = (alias) => {
|
|
245
|
+
const ga = catalogLibraries.get(alias);
|
|
246
|
+
if (ga)
|
|
247
|
+
deps.push(ga);
|
|
248
|
+
};
|
|
249
|
+
const namedPattern = gradleDepRe(`(?:\\(\\s*)?(?:group\\s*=\\s*['"](?<group1>[^'"]+)['"]\\s*,\\s*name\\s*=\\s*['"](?<name1>[^'"]+)['"]|name\\s*=\\s*['"](?<name2>[^'"]+)['"]\\s*,\\s*group\\s*=\\s*['"](?<group2>[^'"]+)['"]|group:\\s*['"](?<group3>[^'"]+)['"]\\s*,\\s*name:\\s*['"](?<name3>[^'"]+)['"]|name:\\s*['"](?<name4>[^'"]+)['"]\\s*,\\s*group:\\s*['"](?<group4>[^'"]+)['"])`);
|
|
250
|
+
for (const match of content.matchAll(namedPattern)) {
|
|
251
|
+
const group = match.groups?.group1 ?? match.groups?.group2 ?? match.groups?.group3 ?? match.groups?.group4;
|
|
252
|
+
const name = match.groups?.name1 ?? match.groups?.name2 ?? match.groups?.name3 ?? match.groups?.name4;
|
|
253
|
+
if (group && name)
|
|
254
|
+
deps.push(`${group}:${name}`);
|
|
255
|
+
}
|
|
256
|
+
for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?libs(?:\\.libraries)?\\.(?!bundles\\.|plugins\\.)${CATALOG_ALIAS}`))) {
|
|
257
|
+
pushCatalogAlias(match[1]);
|
|
258
|
+
}
|
|
259
|
+
for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?libs\\.bundles\\.${CATALOG_ALIAS}`))) {
|
|
260
|
+
for (const member of catalogBundles.get(match[1]) ?? []) {
|
|
261
|
+
for (const accessor of catalogAccessors(member))
|
|
262
|
+
pushCatalogAlias(accessor);
|
|
58
263
|
}
|
|
59
264
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
265
|
+
for (const match of content.matchAll(gradleDepRe(`\\(\\s*projects\\.([A-Za-z][A-Za-z0-9.]*)`))) {
|
|
266
|
+
deps.push(`${groupId}:${projectAccessorToArtifactId(match[1])}`);
|
|
267
|
+
}
|
|
268
|
+
for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*['"]([^'"]+)['"]\\s*\\)|['"]([^'"]+)['"])`))) {
|
|
269
|
+
const coord = match[1] ?? match[2];
|
|
270
|
+
if (!coord)
|
|
271
|
+
continue;
|
|
272
|
+
const parts = coord.split(':');
|
|
273
|
+
if (parts.length >= 2)
|
|
274
|
+
deps.push(`${parts[0]}:${parts[1]}`);
|
|
275
|
+
}
|
|
276
|
+
for (const match of content.matchAll(gradleDepRe(`(?:\\(\\s*)?project\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)`))) {
|
|
277
|
+
deps.push(`${groupId}:${match[1].replace(/^:/, '')}`);
|
|
65
278
|
}
|
|
66
279
|
return { groupId, artifactId, deps: [...new Set(deps)] };
|
|
67
280
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ParsedFile, Range, ScopeId } from '../../../../_shared/index.js';
|
|
2
|
+
import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
3
|
+
import type { DiInjectionMatch } from '../../di-extractors/index.js';
|
|
4
|
+
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
5
|
+
import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
|
6
|
+
export interface SpringDynamicLookupFact {
|
|
7
|
+
readonly ownerScopeId: ScopeId;
|
|
8
|
+
readonly ownerRange: Range;
|
|
9
|
+
readonly receiverName: string;
|
|
10
|
+
readonly methodName: string;
|
|
11
|
+
readonly targetTypeName: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function springDynamicLookupCardinality(receiverName: string, methodName: string): DiInjectionMatch['cardinality'] | null;
|
|
14
|
+
export interface SpringDynamicLookupMetadataAdapter {
|
|
15
|
+
getFacts(filePath: string): readonly SpringDynamicLookupFact[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Attach AST-captured programmatic Spring lookups to the framework-neutral DI
|
|
19
|
+
* resolver. Java/Kotlin own syntax capture; this shared JVM/Spring seam owns
|
|
20
|
+
* import-aware type binding and metadata attachment.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createSpringDynamicLookupMetadataAttacher(adapter: SpringDynamicLookupMetadataAdapter): (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, indexes: ScopeResolutionIndexes) => void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { SPRING_DI_INJECTION_SITES_PROPERTY } from '../../di-extractors/spring.js';
|
|
2
|
+
import { resolveCallerGraphId, resolveDefGraphId, } from '../../scope-resolution/graph-bridge/ids.js';
|
|
3
|
+
import { isClassLike, lookupBindingsAt } from '../../scope-resolution/scope/walkers.js';
|
|
4
|
+
const COLLECTION_LOOKUP_METHODS = new Set(['getBeans', 'getBeansOfType']);
|
|
5
|
+
const SINGLE_LOOKUP_METHODS = new Set(['getBean']);
|
|
6
|
+
/**
|
|
7
|
+
* Distinctive utility names plus conventional Spring context variable names.
|
|
8
|
+
* Generic locals remain recall-oriented because repositories often omit the
|
|
9
|
+
* third-party context type from the index; AST call/class-literal gates and
|
|
10
|
+
* import-aware target resolution prevent the raw-text false-positive class.
|
|
11
|
+
*/
|
|
12
|
+
const KNOWN_RECEIVERS = new Set([
|
|
13
|
+
'SpringContextUtil',
|
|
14
|
+
'SpringContextHolder',
|
|
15
|
+
'SpringBeanUtil',
|
|
16
|
+
'ApplicationContextProvider',
|
|
17
|
+
'BeanFactoryProvider',
|
|
18
|
+
'ApplicationContext',
|
|
19
|
+
'BeanFactory',
|
|
20
|
+
'ListableBeanFactory',
|
|
21
|
+
'applicationContext',
|
|
22
|
+
'context',
|
|
23
|
+
'ctx',
|
|
24
|
+
'appContext',
|
|
25
|
+
'beanFactory',
|
|
26
|
+
]);
|
|
27
|
+
export function springDynamicLookupCardinality(receiverName, methodName) {
|
|
28
|
+
const receiverSimpleName = receiverName.slice(receiverName.lastIndexOf('.') + 1);
|
|
29
|
+
if (!KNOWN_RECEIVERS.has(receiverSimpleName))
|
|
30
|
+
return null;
|
|
31
|
+
if (COLLECTION_LOOKUP_METHODS.has(methodName))
|
|
32
|
+
return 'collection';
|
|
33
|
+
if (SINGLE_LOOKUP_METHODS.has(methodName))
|
|
34
|
+
return 'single';
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function visibleTypeDefinitions(fact, indexes) {
|
|
38
|
+
const simpleName = fact.targetTypeName.slice(fact.targetTypeName.lastIndexOf('.') + 1);
|
|
39
|
+
let scopeId = fact.ownerScopeId;
|
|
40
|
+
while (scopeId !== null) {
|
|
41
|
+
const visible = lookupBindingsAt(scopeId, simpleName, indexes)
|
|
42
|
+
.map(({ def }) => def)
|
|
43
|
+
.filter((def) => isClassLike(def.type))
|
|
44
|
+
.filter((def) => !fact.targetTypeName.includes('.') || def.qualifiedName === fact.targetTypeName);
|
|
45
|
+
if (visible.length > 0) {
|
|
46
|
+
const unique = new Map(visible.map((def) => [def.nodeId, def]));
|
|
47
|
+
return [...unique.values()];
|
|
48
|
+
}
|
|
49
|
+
scopeId = indexes.scopeTree.getScope(scopeId)?.parent ?? null;
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function resolveTargetTypeName(graph, fact, callerLanguage, nodeLookup, indexes) {
|
|
54
|
+
const graphIds = new Set();
|
|
55
|
+
for (const definition of visibleTypeDefinitions(fact, indexes)) {
|
|
56
|
+
const graphId = resolveDefGraphId(definition.filePath, definition, nodeLookup);
|
|
57
|
+
if (graphId === undefined)
|
|
58
|
+
continue;
|
|
59
|
+
const node = graph.getNode(graphId);
|
|
60
|
+
if ((node?.label === 'Class' ||
|
|
61
|
+
node?.label === 'Interface' ||
|
|
62
|
+
node?.label === 'Record' ||
|
|
63
|
+
node?.label === 'Enum') &&
|
|
64
|
+
node.properties.language === callerLanguage) {
|
|
65
|
+
graphIds.add(graphId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (graphIds.size !== 1)
|
|
69
|
+
return undefined;
|
|
70
|
+
const targetId = graphIds.values().next().value;
|
|
71
|
+
if (targetId === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const target = graph.getNode(targetId);
|
|
74
|
+
if (target === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const qualifiedName = target.properties.qualifiedName;
|
|
77
|
+
return typeof qualifiedName === 'string' ? qualifiedName : target.properties.name;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Attach AST-captured programmatic Spring lookups to the framework-neutral DI
|
|
81
|
+
* resolver. Java/Kotlin own syntax capture; this shared JVM/Spring seam owns
|
|
82
|
+
* import-aware type binding and metadata attachment.
|
|
83
|
+
*/
|
|
84
|
+
export function createSpringDynamicLookupMetadataAttacher(adapter) {
|
|
85
|
+
return (graph, parsedFiles, nodeLookup, indexes) => {
|
|
86
|
+
for (const parsed of parsedFiles) {
|
|
87
|
+
for (const fact of adapter.getFacts(parsed.filePath)) {
|
|
88
|
+
const cardinality = springDynamicLookupCardinality(fact.receiverName, fact.methodName);
|
|
89
|
+
if (cardinality === null)
|
|
90
|
+
continue;
|
|
91
|
+
const callerId = resolveCallerGraphId(fact.ownerScopeId, indexes, nodeLookup, {
|
|
92
|
+
startLine: fact.ownerRange.startLine,
|
|
93
|
+
startCol: fact.ownerRange.startCol,
|
|
94
|
+
});
|
|
95
|
+
if (callerId === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
const caller = graph.getNode(callerId);
|
|
98
|
+
if (caller === undefined ||
|
|
99
|
+
(caller.label !== 'Function' &&
|
|
100
|
+
caller.label !== 'Method' &&
|
|
101
|
+
caller.label !== 'Constructor')) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const targetTypeName = resolveTargetTypeName(graph, fact, caller.properties.language, nodeLookup, indexes);
|
|
105
|
+
if (targetTypeName === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
const match = {
|
|
108
|
+
targetTypeName,
|
|
109
|
+
cardinality,
|
|
110
|
+
edgeSource: 'site',
|
|
111
|
+
reason: `Spring dynamic lookup: ${fact.receiverName}.${fact.methodName}(${fact.targetTypeName})`,
|
|
112
|
+
};
|
|
113
|
+
// Singular lookups intentionally use the shared DI selection policy:
|
|
114
|
+
// a unique/@Primary candidate wins; unresolved multiplicity is an
|
|
115
|
+
// explicit 0.5-confidence fan-out rather than a guessed runtime winner.
|
|
116
|
+
const existing = caller.properties[SPRING_DI_INJECTION_SITES_PROPERTY];
|
|
117
|
+
caller.properties[SPRING_DI_INJECTION_SITES_PROPERTY] = [
|
|
118
|
+
...(Array.isArray(existing) ? existing : []),
|
|
119
|
+
match,
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { JavaSpringConfigConsumerFact } from './spring-config-bindings.js';
|
|
|
5
5
|
import type { JavaSpringAopFact } from './spring-aop.js';
|
|
6
6
|
import type { JavaSpringConditionalFact } from './spring-conditionals.js';
|
|
7
7
|
import type { JavaSpringDiClassFact } from './spring-di.js';
|
|
8
|
+
import type { SpringDynamicLookupFact } from '../../frameworks/spring/dynamic-lookups.js';
|
|
8
9
|
import type { JavaSpringNonHttpHandlerFact } from './spring-non-http-handlers.js';
|
|
9
10
|
export type JavaClassAnnotationFact = ClassAnnotationFact;
|
|
10
11
|
export interface JavaCaptureSideChannel {
|
|
@@ -15,6 +16,7 @@ export interface JavaCaptureSideChannel {
|
|
|
15
16
|
readonly springConfigConsumers?: readonly JavaSpringConfigConsumerFact[];
|
|
16
17
|
readonly springConditionalFacts?: readonly JavaSpringConditionalFact[];
|
|
17
18
|
readonly springDiFacts?: readonly JavaSpringDiClassFact[];
|
|
19
|
+
readonly springDynamicLookupFacts?: readonly SpringDynamicLookupFact[];
|
|
18
20
|
readonly springNonHttpHandlerFacts?: readonly JavaSpringNonHttpHandlerFact[];
|
|
19
21
|
}
|
|
20
22
|
/** Clear facts retained by a prior workspace pass in a long-lived process. */
|
|
@@ -29,6 +31,8 @@ export declare function setJavaSpringConditionalFacts(filePath: string, facts: r
|
|
|
29
31
|
export declare function getJavaSpringConditionalFacts(filePath: string): readonly JavaSpringConditionalFact[];
|
|
30
32
|
export declare function setJavaSpringDiFacts(filePath: string, facts: readonly JavaSpringDiClassFact[]): void;
|
|
31
33
|
export declare function getJavaSpringDiFacts(filePath: string): readonly JavaSpringDiClassFact[];
|
|
34
|
+
export declare function setJavaSpringDynamicLookupFacts(filePath: string, facts: readonly SpringDynamicLookupFact[]): void;
|
|
35
|
+
export declare function getJavaSpringDynamicLookupFacts(filePath: string): readonly SpringDynamicLookupFact[];
|
|
32
36
|
export declare function setJavaSpringNonHttpHandlerFacts(filePath: string, facts: readonly JavaSpringNonHttpHandlerFact[]): void;
|
|
33
37
|
export declare function getJavaSpringNonHttpHandlerFacts(filePath: string): readonly JavaSpringNonHttpHandlerFact[];
|
|
34
38
|
/** Snapshot worker-local Java annotation facts for ParsedFile serialization. */
|
|
@@ -6,6 +6,7 @@ const springAopFacts = new Map();
|
|
|
6
6
|
const springConfigConsumers = new Map();
|
|
7
7
|
const springConditionalFacts = new Map();
|
|
8
8
|
const springDiFacts = new Map();
|
|
9
|
+
const springDynamicLookupFacts = new Map();
|
|
9
10
|
const springNonHttpHandlerFacts = new Map();
|
|
10
11
|
/** Clear facts retained by a prior workspace pass in a long-lived process. */
|
|
11
12
|
export function clearJavaClassAnnotationFacts() {
|
|
@@ -14,6 +15,7 @@ export function clearJavaClassAnnotationFacts() {
|
|
|
14
15
|
springConfigConsumers.clear();
|
|
15
16
|
springConditionalFacts.clear();
|
|
16
17
|
springDiFacts.clear();
|
|
18
|
+
springDynamicLookupFacts.clear();
|
|
17
19
|
springNonHttpHandlerFacts.clear();
|
|
18
20
|
}
|
|
19
21
|
export function setJavaSpringAopFacts(filePath, facts) {
|
|
@@ -56,6 +58,15 @@ export function setJavaSpringDiFacts(filePath, facts) {
|
|
|
56
58
|
export function getJavaSpringDiFacts(filePath) {
|
|
57
59
|
return springDiFacts.get(filePath) ?? [];
|
|
58
60
|
}
|
|
61
|
+
export function setJavaSpringDynamicLookupFacts(filePath, facts) {
|
|
62
|
+
if (facts.length === 0)
|
|
63
|
+
springDynamicLookupFacts.delete(filePath);
|
|
64
|
+
else
|
|
65
|
+
springDynamicLookupFacts.set(filePath, facts);
|
|
66
|
+
}
|
|
67
|
+
export function getJavaSpringDynamicLookupFacts(filePath) {
|
|
68
|
+
return springDynamicLookupFacts.get(filePath) ?? [];
|
|
69
|
+
}
|
|
59
70
|
export function setJavaSpringNonHttpHandlerFacts(filePath, facts) {
|
|
60
71
|
if (facts.length === 0)
|
|
61
72
|
springNonHttpHandlerFacts.delete(filePath);
|
|
@@ -72,6 +83,7 @@ export function collectJavaCaptureSideChannel(filePath) {
|
|
|
72
83
|
const configConsumers = springConfigConsumers.get(filePath) ?? [];
|
|
73
84
|
const conditionFacts = springConditionalFacts.get(filePath) ?? [];
|
|
74
85
|
const diFacts = springDiFacts.get(filePath) ?? [];
|
|
86
|
+
const dynamicLookupFacts = springDynamicLookupFacts.get(filePath) ?? [];
|
|
75
87
|
const nonHttpHandlerFacts = springNonHttpHandlerFacts.get(filePath) ?? [];
|
|
76
88
|
const packageFact = getJavaPackageFact(filePath);
|
|
77
89
|
if (facts.length === 0 &&
|
|
@@ -79,6 +91,7 @@ export function collectJavaCaptureSideChannel(filePath) {
|
|
|
79
91
|
configConsumers.length === 0 &&
|
|
80
92
|
conditionFacts.length === 0 &&
|
|
81
93
|
diFacts.length === 0 &&
|
|
94
|
+
dynamicLookupFacts.length === 0 &&
|
|
82
95
|
nonHttpHandlerFacts.length === 0 &&
|
|
83
96
|
packageFact === undefined) {
|
|
84
97
|
return undefined;
|
|
@@ -91,6 +104,7 @@ export function collectJavaCaptureSideChannel(filePath) {
|
|
|
91
104
|
...(configConsumers.length > 0 ? { springConfigConsumers: configConsumers } : {}),
|
|
92
105
|
...(conditionFacts.length > 0 ? { springConditionalFacts: conditionFacts } : {}),
|
|
93
106
|
...(diFacts.length > 0 ? { springDiFacts: diFacts } : {}),
|
|
107
|
+
...(dynamicLookupFacts.length > 0 ? { springDynamicLookupFacts: dynamicLookupFacts } : {}),
|
|
94
108
|
...(nonHttpHandlerFacts.length > 0 ? { springNonHttpHandlerFacts: nonHttpHandlerFacts } : {}),
|
|
95
109
|
};
|
|
96
110
|
}
|
|
@@ -110,6 +124,7 @@ export function applyJavaCaptureSideChannel(parsed) {
|
|
|
110
124
|
setJavaSpringConfigConsumerFacts(parsed.filePath, []);
|
|
111
125
|
setJavaSpringConditionalFacts(parsed.filePath, []);
|
|
112
126
|
setJavaSpringDiFacts(parsed.filePath, []);
|
|
127
|
+
setJavaSpringDynamicLookupFacts(parsed.filePath, []);
|
|
113
128
|
setJavaSpringNonHttpHandlerFacts(parsed.filePath, []);
|
|
114
129
|
setJavaPackageFact(parsed.filePath, UNKNOWN_JVM_PACKAGE_FACT);
|
|
115
130
|
return;
|
|
@@ -119,6 +134,7 @@ export function applyJavaCaptureSideChannel(parsed) {
|
|
|
119
134
|
setJavaSpringConfigConsumerFacts(parsed.filePath, Array.isArray(data.springConfigConsumers) ? data.springConfigConsumers : []);
|
|
120
135
|
setJavaSpringConditionalFacts(parsed.filePath, Array.isArray(data.springConditionalFacts) ? data.springConditionalFacts : []);
|
|
121
136
|
setJavaSpringDiFacts(parsed.filePath, Array.isArray(data.springDiFacts) ? data.springDiFacts : []);
|
|
137
|
+
setJavaSpringDynamicLookupFacts(parsed.filePath, Array.isArray(data.springDynamicLookupFacts) ? data.springDynamicLookupFacts : []);
|
|
122
138
|
setJavaSpringNonHttpHandlerFacts(parsed.filePath, Array.isArray(data.springNonHttpHandlerFacts) ? data.springNonHttpHandlerFacts : []);
|
|
123
139
|
setJavaPackageFact(parsed.filePath, isJvmPackageFact(data.packageFact) ? data.packageFact : UNKNOWN_JVM_PACKAGE_FACT);
|
|
124
140
|
}
|
|
@@ -7,11 +7,12 @@ import { getJavaParser, getJavaScopeQuery } from './query.js';
|
|
|
7
7
|
import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
|
|
8
8
|
import { getTreeSitterBufferSize } from '../../constants.js';
|
|
9
9
|
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
10
|
-
import { setJavaClassAnnotationFacts, setJavaSpringAopFacts, setJavaSpringConfigConsumerFacts, setJavaSpringConditionalFacts, setJavaSpringDiFacts, setJavaSpringNonHttpHandlerFacts, } from './capture-side-channel.js';
|
|
10
|
+
import { setJavaClassAnnotationFacts, setJavaSpringAopFacts, setJavaSpringConfigConsumerFacts, setJavaSpringConditionalFacts, setJavaSpringDiFacts, setJavaSpringDynamicLookupFacts, setJavaSpringNonHttpHandlerFacts, } from './capture-side-channel.js';
|
|
11
11
|
import { captureJavaPackageFact } from './package-facts.js';
|
|
12
12
|
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
|
13
13
|
import { captureJavaSpringConfigConsumerFacts } from './spring-config-bindings.js';
|
|
14
14
|
import { captureJavaSpringDiClassFact } from './spring-di.js';
|
|
15
|
+
import { captureJavaSpringDynamicLookupFact } from './spring-dynamic-lookup.js';
|
|
15
16
|
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
|
16
17
|
import { captureJavaSpringAopFacts } from './spring-aop.js';
|
|
17
18
|
import { captureJavaSpringConditionalFacts, } from './spring-conditionals.js';
|
|
@@ -98,6 +99,8 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
98
99
|
const springDiFacts = [];
|
|
99
100
|
const springNonHttpHandlerFacts = [];
|
|
100
101
|
const springDiClassNodeIds = new Set();
|
|
102
|
+
const springDynamicLookupFacts = [];
|
|
103
|
+
const springDynamicLookupNodeIds = new Set();
|
|
101
104
|
for (const m of rawMatches) {
|
|
102
105
|
const grouped = {};
|
|
103
106
|
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
|
|
@@ -116,6 +119,13 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
116
119
|
}
|
|
117
120
|
if (Object.keys(grouped).length === 0)
|
|
118
121
|
continue;
|
|
122
|
+
const dynamicLookupNode = nodeIfType(nodeMap['@reference.call.member'], 'method_invocation');
|
|
123
|
+
if (dynamicLookupNode !== null && !springDynamicLookupNodeIds.has(dynamicLookupNode.id)) {
|
|
124
|
+
springDynamicLookupNodeIds.add(dynamicLookupNode.id);
|
|
125
|
+
const fact = captureJavaSpringDynamicLookupFact(dynamicLookupNode, filePath);
|
|
126
|
+
if (fact !== null)
|
|
127
|
+
springDynamicLookupFacts.push(fact);
|
|
128
|
+
}
|
|
119
129
|
const springAopTypeNode = [
|
|
120
130
|
nodeIfType(nodeMap['@scope.class'], 'class_declaration'),
|
|
121
131
|
nodeIfType(nodeMap['@scope.class'], 'interface_declaration'),
|
|
@@ -287,6 +297,7 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
287
297
|
setJavaSpringAopFacts(filePath, springAopFacts);
|
|
288
298
|
setJavaSpringConditionalFacts(filePath, springConditionalFacts);
|
|
289
299
|
setJavaSpringDiFacts(filePath, springDiFacts);
|
|
300
|
+
setJavaSpringDynamicLookupFacts(filePath, springDynamicLookupFacts);
|
|
290
301
|
setJavaSpringNonHttpHandlerFacts(filePath, springNonHttpHandlerFacts);
|
|
291
302
|
return [
|
|
292
303
|
...resolveVarTypeBindings(out),
|
|
@@ -19,6 +19,7 @@ import { attachJavaSpringConfigBindings } from './spring-config-bindings.js';
|
|
|
19
19
|
import { attachJavaSpringConditionalMetadata } from './spring-conditionals.js';
|
|
20
20
|
import { attachJavaSpringDiMetadata } from './spring-di.js';
|
|
21
21
|
import { attachJavaSpringNonHttpHandlerMetadata } from './spring-non-http-handlers.js';
|
|
22
|
+
import { attachJavaSpringDynamicLookup } from './spring-dynamic-lookup.js';
|
|
22
23
|
import { applyJavaCaptureSideChannel, clearJavaClassAnnotationFacts, } from './capture-side-channel.js';
|
|
23
24
|
import { clearJavaPackageFacts } from './package-facts.js';
|
|
24
25
|
const javaScopeResolver = {
|
|
@@ -65,6 +66,7 @@ const javaScopeResolver = {
|
|
|
65
66
|
attachJavaSpringDiMetadata(graph, parsedFiles, nodeLookup, indexes);
|
|
66
67
|
attachJavaSpringNonHttpHandlerMetadata(graph, parsedFiles, nodeLookup, indexes);
|
|
67
68
|
attachJavaSpringConfigBindings(graph, parsedFiles, nodeLookup, indexes, ctx);
|
|
69
|
+
attachJavaSpringDynamicLookup(graph, parsedFiles, nodeLookup, indexes);
|
|
68
70
|
},
|
|
69
71
|
};
|
|
70
72
|
export { javaScopeResolver };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SpringDynamicLookupFact } from '../../frameworks/spring/dynamic-lookups.js';
|
|
2
|
+
import { type SyntaxNode } from '../../utils/ast-helpers.js';
|
|
3
|
+
/** Capture real Java method invocations; comments and literals are never visited as calls. */
|
|
4
|
+
export declare function captureJavaSpringDynamicLookupFact(node: SyntaxNode, filePath: string): SpringDynamicLookupFact | null;
|
|
5
|
+
/** Standalone extractor for focused tests; production reuses scope-query call nodes. */
|
|
6
|
+
export declare function captureJavaSpringDynamicLookupFacts(rootNode: SyntaxNode, filePath: string): SpringDynamicLookupFact[];
|
|
7
|
+
/** Attach Java lookup facts for later resolution by the shared DI phase. */
|
|
8
|
+
export declare const attachJavaSpringDynamicLookup: (graph: import("../../../graph/types.js").KnowledgeGraph, parsedFiles: readonly import('../../../../_shared/index.js').ParsedFile[], nodeLookup: import("../../scope-resolution/graph-bridge/node-lookup.js").GraphNodeLookup, indexes: import("../../model/scope-resolution-indexes.js").ScopeResolutionIndexes) => void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { makeScopeId } from '../../../../_shared/index.js';
|
|
2
|
+
import { createSpringDynamicLookupMetadataAttacher, springDynamicLookupCardinality, } from '../../frameworks/spring/dynamic-lookups.js';
|
|
3
|
+
import { findAncestorBeforeBoundary, nodeToCapture, } from '../../utils/ast-helpers.js';
|
|
4
|
+
import { getJavaSpringDynamicLookupFacts } from './capture-side-channel.js';
|
|
5
|
+
const CALLABLE_NODE_TYPES = new Set([
|
|
6
|
+
'method_declaration',
|
|
7
|
+
'constructor_declaration',
|
|
8
|
+
'compact_constructor_declaration',
|
|
9
|
+
]);
|
|
10
|
+
const NO_CALLABLE_BOUNDARIES = new Set();
|
|
11
|
+
function classLiteralTypeName(argument) {
|
|
12
|
+
if (argument.type !== 'class_literal' || argument.namedChildCount !== 1)
|
|
13
|
+
return null;
|
|
14
|
+
return argument.namedChild(0)?.text.trim() ?? null;
|
|
15
|
+
}
|
|
16
|
+
/** Capture real Java method invocations; comments and literals are never visited as calls. */
|
|
17
|
+
export function captureJavaSpringDynamicLookupFact(node, filePath) {
|
|
18
|
+
if (node.type !== 'method_invocation')
|
|
19
|
+
return null;
|
|
20
|
+
const receiverName = node.childForFieldName('object')?.text.trim();
|
|
21
|
+
const methodName = node.childForFieldName('name')?.text.trim();
|
|
22
|
+
const argumentsNode = node.childForFieldName('arguments');
|
|
23
|
+
if (receiverName === undefined || methodName === undefined || argumentsNode === null)
|
|
24
|
+
return null;
|
|
25
|
+
if (springDynamicLookupCardinality(receiverName, methodName) === null)
|
|
26
|
+
return null;
|
|
27
|
+
const argumentsWithoutComments = argumentsNode.namedChildren.filter((child) => child.type !== 'line_comment' && child.type !== 'block_comment');
|
|
28
|
+
if (argumentsWithoutComments.length !== 1)
|
|
29
|
+
return null;
|
|
30
|
+
const argument = argumentsWithoutComments[0];
|
|
31
|
+
if (argument === undefined)
|
|
32
|
+
return null;
|
|
33
|
+
const targetTypeName = classLiteralTypeName(argument);
|
|
34
|
+
if (targetTypeName === null)
|
|
35
|
+
return null;
|
|
36
|
+
const owner = findAncestorBeforeBoundary(node, CALLABLE_NODE_TYPES, NO_CALLABLE_BOUNDARIES);
|
|
37
|
+
if (owner === null)
|
|
38
|
+
return null;
|
|
39
|
+
const ownerCapture = nodeToCapture('@spring-dynamic-lookup.owner', owner);
|
|
40
|
+
return {
|
|
41
|
+
ownerScopeId: makeScopeId({
|
|
42
|
+
filePath,
|
|
43
|
+
range: ownerCapture.range,
|
|
44
|
+
kind: 'Function',
|
|
45
|
+
}),
|
|
46
|
+
ownerRange: ownerCapture.range,
|
|
47
|
+
receiverName,
|
|
48
|
+
methodName,
|
|
49
|
+
targetTypeName,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Standalone extractor for focused tests; production reuses scope-query call nodes. */
|
|
53
|
+
export function captureJavaSpringDynamicLookupFacts(rootNode, filePath) {
|
|
54
|
+
return rootNode
|
|
55
|
+
.descendantsOfType('method_invocation')
|
|
56
|
+
.map((node) => captureJavaSpringDynamicLookupFact(node, filePath))
|
|
57
|
+
.filter((fact) => fact !== null);
|
|
58
|
+
}
|
|
59
|
+
/** Attach Java lookup facts for later resolution by the shared DI phase. */
|
|
60
|
+
export const attachJavaSpringDynamicLookup = createSpringDynamicLookupMetadataAttacher({
|
|
61
|
+
getFacts: getJavaSpringDynamicLookupFacts,
|
|
62
|
+
});
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import type { ParsedFile, ScopeId } from '../../../../_shared/index.js';
|
|
40
40
|
import { type ClassAnnotationFact } from '../../frameworks/spring/bean-candidates.js';
|
|
41
41
|
import { type JvmPackageFact } from '../jvm/package-facts.js';
|
|
42
|
+
import type { SpringDynamicLookupFact } from '../../frameworks/spring/dynamic-lookups.js';
|
|
42
43
|
import type { KotlinSpringAopFact } from './spring-aop.js';
|
|
43
44
|
import type { KotlinSpringConditionalFact } from './spring-conditionals.js';
|
|
44
45
|
import type { KotlinSpringDiClassFact } from './spring-di.js';
|
|
@@ -63,6 +64,8 @@ export interface KotlinCaptureSideChannel {
|
|
|
63
64
|
readonly springConditionalFacts?: readonly KotlinSpringConditionalFact[];
|
|
64
65
|
/** Constructor, property, and method injection syntax captured per class. */
|
|
65
66
|
readonly springDiFacts?: readonly KotlinSpringDiClassFact[];
|
|
67
|
+
/** Programmatic Spring bean lookups captured per callable. */
|
|
68
|
+
readonly springDynamicLookupFacts?: readonly SpringDynamicLookupFact[];
|
|
66
69
|
/** Scheduled, event, messaging, and managed-job handler syntax captured per callable. */
|
|
67
70
|
readonly springNonHttpHandlerFacts?: readonly KotlinSpringNonHttpHandlerFact[];
|
|
68
71
|
}
|
|
@@ -75,6 +78,8 @@ export declare function setKotlinSpringConditionalFacts(filePath: string, facts:
|
|
|
75
78
|
export declare function getKotlinSpringConditionalFacts(filePath: string): readonly KotlinSpringConditionalFact[];
|
|
76
79
|
export declare function setKotlinSpringDiFacts(filePath: string, facts: readonly KotlinSpringDiClassFact[]): void;
|
|
77
80
|
export declare function getKotlinSpringDiFacts(filePath: string): readonly KotlinSpringDiClassFact[];
|
|
81
|
+
export declare function setKotlinSpringDynamicLookupFacts(filePath: string, facts: readonly SpringDynamicLookupFact[]): void;
|
|
82
|
+
export declare function getKotlinSpringDynamicLookupFacts(filePath: string): readonly SpringDynamicLookupFact[];
|
|
78
83
|
export declare function setKotlinSpringNonHttpHandlerFacts(filePath: string, facts: readonly KotlinSpringNonHttpHandlerFact[]): void;
|
|
79
84
|
export declare function getKotlinSpringNonHttpHandlerFacts(filePath: string): readonly KotlinSpringNonHttpHandlerFact[];
|
|
80
85
|
/**
|
|
@@ -44,12 +44,14 @@ const classAnnotations = createClassAnnotationFactStore();
|
|
|
44
44
|
const springAopFacts = new Map();
|
|
45
45
|
const springConditionalFacts = new Map();
|
|
46
46
|
const springDiFacts = new Map();
|
|
47
|
+
const springDynamicLookupFacts = new Map();
|
|
47
48
|
const springNonHttpHandlerFacts = new Map();
|
|
48
49
|
export function clearKotlinClassAnnotationFacts() {
|
|
49
50
|
classAnnotations.clear();
|
|
50
51
|
springAopFacts.clear();
|
|
51
52
|
springConditionalFacts.clear();
|
|
52
53
|
springDiFacts.clear();
|
|
54
|
+
springDynamicLookupFacts.clear();
|
|
53
55
|
springNonHttpHandlerFacts.clear();
|
|
54
56
|
}
|
|
55
57
|
export function setKotlinSpringAopFacts(filePath, facts) {
|
|
@@ -85,6 +87,15 @@ export function setKotlinSpringDiFacts(filePath, facts) {
|
|
|
85
87
|
export function getKotlinSpringDiFacts(filePath) {
|
|
86
88
|
return springDiFacts.get(filePath) ?? [];
|
|
87
89
|
}
|
|
90
|
+
export function setKotlinSpringDynamicLookupFacts(filePath, facts) {
|
|
91
|
+
if (facts.length === 0)
|
|
92
|
+
springDynamicLookupFacts.delete(filePath);
|
|
93
|
+
else
|
|
94
|
+
springDynamicLookupFacts.set(filePath, facts);
|
|
95
|
+
}
|
|
96
|
+
export function getKotlinSpringDynamicLookupFacts(filePath) {
|
|
97
|
+
return springDynamicLookupFacts.get(filePath) ?? [];
|
|
98
|
+
}
|
|
88
99
|
export function setKotlinSpringNonHttpHandlerFacts(filePath, facts) {
|
|
89
100
|
if (facts.length === 0)
|
|
90
101
|
springNonHttpHandlerFacts.delete(filePath);
|
|
@@ -105,6 +116,7 @@ export function collectKotlinCaptureSideChannel(filePath) {
|
|
|
105
116
|
const aopFacts = springAopFacts.get(filePath) ?? [];
|
|
106
117
|
const conditionFacts = springConditionalFacts.get(filePath) ?? [];
|
|
107
118
|
const diFacts = springDiFacts.get(filePath) ?? [];
|
|
119
|
+
const dynamicLookupFacts = springDynamicLookupFacts.get(filePath) ?? [];
|
|
108
120
|
const nonHttpHandlerFacts = springNonHttpHandlerFacts.get(filePath) ?? [];
|
|
109
121
|
const packageFact = getKotlinPackageFact(filePath);
|
|
110
122
|
if (companionScopes.length === 0 &&
|
|
@@ -112,6 +124,7 @@ export function collectKotlinCaptureSideChannel(filePath) {
|
|
|
112
124
|
aopFacts.length === 0 &&
|
|
113
125
|
conditionFacts.length === 0 &&
|
|
114
126
|
diFacts.length === 0 &&
|
|
127
|
+
dynamicLookupFacts.length === 0 &&
|
|
115
128
|
nonHttpHandlerFacts.length === 0 &&
|
|
116
129
|
packageFact === undefined) {
|
|
117
130
|
return undefined;
|
|
@@ -124,6 +137,7 @@ export function collectKotlinCaptureSideChannel(filePath) {
|
|
|
124
137
|
...(aopFacts.length > 0 ? { springAopFacts: aopFacts } : {}),
|
|
125
138
|
...(conditionFacts.length > 0 ? { springConditionalFacts: conditionFacts } : {}),
|
|
126
139
|
...(diFacts.length > 0 ? { springDiFacts: diFacts } : {}),
|
|
140
|
+
...(dynamicLookupFacts.length > 0 ? { springDynamicLookupFacts: dynamicLookupFacts } : {}),
|
|
127
141
|
...(nonHttpHandlerFacts.length > 0 ? { springNonHttpHandlerFacts: nonHttpHandlerFacts } : {}),
|
|
128
142
|
};
|
|
129
143
|
}
|
|
@@ -147,6 +161,7 @@ export function applyKotlinCaptureSideChannel(parsed) {
|
|
|
147
161
|
setKotlinSpringAopFacts(parsed.filePath, []);
|
|
148
162
|
setKotlinSpringConditionalFacts(parsed.filePath, []);
|
|
149
163
|
setKotlinSpringDiFacts(parsed.filePath, []);
|
|
164
|
+
setKotlinSpringDynamicLookupFacts(parsed.filePath, []);
|
|
150
165
|
setKotlinSpringNonHttpHandlerFacts(parsed.filePath, []);
|
|
151
166
|
setKotlinPackageFact(parsed.filePath, UNKNOWN_JVM_PACKAGE_FACT);
|
|
152
167
|
return;
|
|
@@ -158,6 +173,7 @@ export function applyKotlinCaptureSideChannel(parsed) {
|
|
|
158
173
|
setKotlinSpringAopFacts(parsed.filePath, Array.isArray(data.springAopFacts) ? data.springAopFacts : []);
|
|
159
174
|
setKotlinSpringConditionalFacts(parsed.filePath, Array.isArray(data.springConditionalFacts) ? data.springConditionalFacts : []);
|
|
160
175
|
setKotlinSpringDiFacts(parsed.filePath, Array.isArray(data.springDiFacts) ? data.springDiFacts : []);
|
|
176
|
+
setKotlinSpringDynamicLookupFacts(parsed.filePath, Array.isArray(data.springDynamicLookupFacts) ? data.springDynamicLookupFacts : []);
|
|
161
177
|
setKotlinSpringNonHttpHandlerFacts(parsed.filePath, Array.isArray(data.springNonHttpHandlerFacts) ? data.springNonHttpHandlerFacts : []);
|
|
162
178
|
setKotlinPackageFact(parsed.filePath, isJvmPackageFact(data.packageFact) ? data.packageFact : UNKNOWN_JVM_PACKAGE_FACT);
|
|
163
179
|
}
|
|
@@ -10,10 +10,11 @@ import { normalizeKotlinType } from './interpret.js';
|
|
|
10
10
|
import { synthesizeKotlinReceiverBinding } from './receiver-binding.js';
|
|
11
11
|
import { getKotlinParser, getKotlinScopeQuery } from './query.js';
|
|
12
12
|
import { markCompanionScope } from './companion-scopes.js';
|
|
13
|
-
import { setKotlinClassAnnotationFacts, setKotlinSpringAopFacts, setKotlinSpringConditionalFacts, setKotlinSpringDiFacts, setKotlinSpringNonHttpHandlerFacts, } from './capture-side-channel.js';
|
|
13
|
+
import { setKotlinClassAnnotationFacts, setKotlinSpringAopFacts, setKotlinSpringConditionalFacts, setKotlinSpringDiFacts, setKotlinSpringDynamicLookupFacts, setKotlinSpringNonHttpHandlerFacts, } from './capture-side-channel.js';
|
|
14
14
|
import { captureKotlinPackageFact } from './package-facts.js';
|
|
15
15
|
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
|
16
16
|
import { captureKotlinSpringDiClassFact } from './spring-di.js';
|
|
17
|
+
import { captureKotlinSpringDynamicLookupFact } from './spring-dynamic-lookup.js';
|
|
17
18
|
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
|
18
19
|
import { captureKotlinSpringAopFacts } from './spring-aop.js';
|
|
19
20
|
import { captureKotlinSpringConditionalFacts, } from './spring-conditionals.js';
|
|
@@ -75,6 +76,8 @@ export function emitKotlinScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
75
76
|
const springNonHttpHandlerFacts = [];
|
|
76
77
|
const springNonHttpHandlerTypeNodeIds = new Set();
|
|
77
78
|
const springDiClassNodeIds = new Set();
|
|
79
|
+
const springDynamicLookupFacts = [];
|
|
80
|
+
const springDynamicLookupNodeIds = new Set();
|
|
78
81
|
const returnTypes = collectKotlinReturnTypeTexts(tree.rootNode);
|
|
79
82
|
out.push(...synthesizeKotlinLocalAssignmentBindings(tree.rootNode, returnTypes));
|
|
80
83
|
out.push(...synthesizeKotlinLoopBindings(tree.rootNode, returnTypes));
|
|
@@ -97,6 +100,13 @@ export function emitKotlinScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
97
100
|
}
|
|
98
101
|
if (Object.keys(grouped).length === 0)
|
|
99
102
|
continue;
|
|
103
|
+
const dynamicLookupNode = nodeIfType(groupedNodes['@reference.call.member'], 'call_expression');
|
|
104
|
+
if (dynamicLookupNode !== null && !springDynamicLookupNodeIds.has(dynamicLookupNode.id)) {
|
|
105
|
+
springDynamicLookupNodeIds.add(dynamicLookupNode.id);
|
|
106
|
+
const fact = captureKotlinSpringDynamicLookupFact(dynamicLookupNode, filePath);
|
|
107
|
+
if (fact !== null)
|
|
108
|
+
springDynamicLookupFacts.push(fact);
|
|
109
|
+
}
|
|
100
110
|
// tree-sitter-kotlin represents both classes and interfaces with
|
|
101
111
|
// `class_declaration`; `object_declaration` is the separate object form.
|
|
102
112
|
const springAopTypeNode = [
|
|
@@ -279,6 +289,7 @@ export function emitKotlinScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
279
289
|
setKotlinSpringAopFacts(filePath, springAopFacts);
|
|
280
290
|
setKotlinSpringConditionalFacts(filePath, springConditionalFacts);
|
|
281
291
|
setKotlinSpringDiFacts(filePath, springDiFacts);
|
|
292
|
+
setKotlinSpringDynamicLookupFacts(filePath, springDynamicLookupFacts);
|
|
282
293
|
setKotlinSpringNonHttpHandlerFacts(filePath, springNonHttpHandlerFacts);
|
|
283
294
|
out.push(...synthesizeCallableFlowCaptures(tree.rootNode, KOTLIN_CALLABLE_CAPTURE_OPTIONS));
|
|
284
295
|
return out;
|
|
@@ -15,6 +15,7 @@ import { clearKotlinPackageFacts } from './package-facts.js';
|
|
|
15
15
|
import { attachKotlinSpringDiMetadata } from './spring-di.js';
|
|
16
16
|
import { attachKotlinSpringConditionalMetadata } from './spring-conditionals.js';
|
|
17
17
|
import { attachKotlinSpringNonHttpHandlerMetadata } from './spring-non-http-handlers.js';
|
|
18
|
+
import { attachKotlinSpringDynamicLookup } from './spring-dynamic-lookup.js';
|
|
18
19
|
/**
|
|
19
20
|
* Kotlin scope resolver for RFC #909 Ring 3.
|
|
20
21
|
*
|
|
@@ -121,6 +122,7 @@ export const kotlinScopeResolver = {
|
|
|
121
122
|
attachKotlinSpringConditionalMetadata(graph, parsedFiles, nodeLookup, indexes);
|
|
122
123
|
attachKotlinSpringDiMetadata(graph, parsedFiles, nodeLookup, indexes);
|
|
123
124
|
attachKotlinSpringNonHttpHandlerMetadata(graph, parsedFiles, nodeLookup, indexes);
|
|
125
|
+
attachKotlinSpringDynamicLookup(graph, parsedFiles, nodeLookup, indexes);
|
|
124
126
|
},
|
|
125
127
|
};
|
|
126
128
|
/**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SpringDynamicLookupFact } from '../../frameworks/spring/dynamic-lookups.js';
|
|
2
|
+
import { type SyntaxNode } from '../../utils/ast-helpers.js';
|
|
3
|
+
/** Capture real Kotlin calls using `Type::class` or `Type::class.java`. */
|
|
4
|
+
export declare function captureKotlinSpringDynamicLookupFact(node: SyntaxNode, filePath: string): SpringDynamicLookupFact | null;
|
|
5
|
+
/** Standalone extractor for focused tests; production reuses scope-query call nodes. */
|
|
6
|
+
export declare function captureKotlinSpringDynamicLookupFacts(rootNode: SyntaxNode, filePath: string): SpringDynamicLookupFact[];
|
|
7
|
+
/** Attach Kotlin lookup facts for later resolution by the shared DI phase. */
|
|
8
|
+
export declare const attachKotlinSpringDynamicLookup: (graph: import("../../../graph/types.js").KnowledgeGraph, parsedFiles: readonly import('../../../../_shared/index.js').ParsedFile[], nodeLookup: import("../../scope-resolution/graph-bridge/node-lookup.js").GraphNodeLookup, indexes: import("../../model/scope-resolution-indexes.js").ScopeResolutionIndexes) => void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { makeScopeId } from '../../../../_shared/index.js';
|
|
2
|
+
import { createSpringDynamicLookupMetadataAttacher, springDynamicLookupCardinality, } from '../../frameworks/spring/dynamic-lookups.js';
|
|
3
|
+
import { findAncestorBeforeBoundary, nodeToCapture, } from '../../utils/ast-helpers.js';
|
|
4
|
+
import { getKotlinSpringDynamicLookupFacts } from './capture-side-channel.js';
|
|
5
|
+
// Kotlin emits graph callables for functions and secondary constructors.
|
|
6
|
+
// `init {}` / primary-constructor bodies have no independent callable node, so
|
|
7
|
+
// attributing their lookups to the enclosing Class would violate graph semantics.
|
|
8
|
+
const CALLABLE_NODE_TYPES = new Set(['function_declaration', 'secondary_constructor']);
|
|
9
|
+
const NO_CALLABLE_BOUNDARIES = new Set();
|
|
10
|
+
const KOTLIN_CLASS_LITERAL = /^([A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*)::class(?:\.java)?$/;
|
|
11
|
+
function navigationParts(node) {
|
|
12
|
+
if (node.type !== 'navigation_expression')
|
|
13
|
+
return null;
|
|
14
|
+
const text = node.text.trim();
|
|
15
|
+
const separator = text.lastIndexOf('.');
|
|
16
|
+
if (separator <= 0 || separator === text.length - 1)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
receiverName: text.slice(0, separator),
|
|
20
|
+
methodName: text.slice(separator + 1),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function singleClassLiteralArgument(node) {
|
|
24
|
+
const suffix = node.namedChildren.find((child) => child.type === 'call_suffix');
|
|
25
|
+
const argumentsNode = suffix?.namedChildren.find((child) => child.type === 'value_arguments');
|
|
26
|
+
if (argumentsNode === undefined)
|
|
27
|
+
return null;
|
|
28
|
+
const argumentsWithoutComments = argumentsNode.namedChildren.filter((child) => child.type !== 'line_comment' && child.type !== 'multiline_comment');
|
|
29
|
+
if (argumentsWithoutComments.length !== 1)
|
|
30
|
+
return null;
|
|
31
|
+
const value = argumentsWithoutComments[0];
|
|
32
|
+
if (value?.type !== 'value_argument' || value.namedChildCount !== 1)
|
|
33
|
+
return null;
|
|
34
|
+
return value.namedChild(0)?.text.trim().match(KOTLIN_CLASS_LITERAL)?.[1] ?? null;
|
|
35
|
+
}
|
|
36
|
+
/** Capture real Kotlin calls using `Type::class` or `Type::class.java`. */
|
|
37
|
+
export function captureKotlinSpringDynamicLookupFact(node, filePath) {
|
|
38
|
+
if (node.type !== 'call_expression')
|
|
39
|
+
return null;
|
|
40
|
+
const callee = node.namedChildren.find((child) => child.type === 'navigation_expression');
|
|
41
|
+
if (callee === undefined)
|
|
42
|
+
return null;
|
|
43
|
+
const parts = navigationParts(callee);
|
|
44
|
+
if (parts === null)
|
|
45
|
+
return null;
|
|
46
|
+
if (springDynamicLookupCardinality(parts.receiverName, parts.methodName) === null)
|
|
47
|
+
return null;
|
|
48
|
+
const targetTypeName = singleClassLiteralArgument(node);
|
|
49
|
+
if (targetTypeName === null)
|
|
50
|
+
return null;
|
|
51
|
+
const owner = findAncestorBeforeBoundary(node, CALLABLE_NODE_TYPES, NO_CALLABLE_BOUNDARIES);
|
|
52
|
+
if (owner === null)
|
|
53
|
+
return null;
|
|
54
|
+
const ownerCapture = nodeToCapture('@spring-dynamic-lookup.owner', owner);
|
|
55
|
+
return {
|
|
56
|
+
ownerScopeId: makeScopeId({
|
|
57
|
+
filePath,
|
|
58
|
+
range: ownerCapture.range,
|
|
59
|
+
kind: 'Function',
|
|
60
|
+
}),
|
|
61
|
+
ownerRange: ownerCapture.range,
|
|
62
|
+
receiverName: parts.receiverName,
|
|
63
|
+
methodName: parts.methodName,
|
|
64
|
+
targetTypeName,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Standalone extractor for focused tests; production reuses scope-query call nodes. */
|
|
68
|
+
export function captureKotlinSpringDynamicLookupFacts(rootNode, filePath) {
|
|
69
|
+
return rootNode
|
|
70
|
+
.descendantsOfType('call_expression')
|
|
71
|
+
.map((node) => captureKotlinSpringDynamicLookupFact(node, filePath))
|
|
72
|
+
.filter((fact) => fact !== null);
|
|
73
|
+
}
|
|
74
|
+
/** Attach Kotlin lookup facts for later resolution by the shared DI phase. */
|
|
75
|
+
export const attachKotlinSpringDynamicLookup = createSpringDynamicLookupMetadataAttacher({
|
|
76
|
+
getFacts: getKotlinSpringDynamicLookupFacts,
|
|
77
|
+
});
|
|
@@ -48,6 +48,9 @@ function providerCandidates(ids, providers) {
|
|
|
48
48
|
// candidates when none are known instead of dropping the injection entirely.
|
|
49
49
|
return recognized.length > 0 ? recognized : all;
|
|
50
50
|
}
|
|
51
|
+
function isConcreteTypeNode(node) {
|
|
52
|
+
return node?.label === 'Class' || node?.label === 'Record' || node?.label === 'Enum';
|
|
53
|
+
}
|
|
51
54
|
export const diPhase = {
|
|
52
55
|
name: 'di',
|
|
53
56
|
deps: ['mro'],
|
|
@@ -103,21 +106,25 @@ export const diPhase = {
|
|
|
103
106
|
ambiguousInjections: 0,
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
|
-
const
|
|
109
|
+
const directSubtypes = new Map();
|
|
107
110
|
const directSupertypes = new Map();
|
|
108
111
|
for (const rel of ctx.graph.iterRelationshipsByType('IMPLEMENTS')) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
const subtypes = directSubtypes.get(rel.targetId) ?? new Set();
|
|
113
|
+
subtypes.add(rel.sourceId);
|
|
114
|
+
directSubtypes.set(rel.targetId, subtypes);
|
|
112
115
|
const supertypes = directSupertypes.get(rel.sourceId) ?? new Set();
|
|
113
116
|
supertypes.add(rel.targetId);
|
|
114
117
|
directSupertypes.set(rel.sourceId, supertypes);
|
|
115
118
|
}
|
|
116
119
|
for (const rel of ctx.graph.iterRelationshipsByType('EXTENDS')) {
|
|
120
|
+
const subtypes = directSubtypes.get(rel.targetId) ?? new Set();
|
|
121
|
+
subtypes.add(rel.sourceId);
|
|
122
|
+
directSubtypes.set(rel.targetId, subtypes);
|
|
117
123
|
const supertypes = directSupertypes.get(rel.sourceId) ?? new Set();
|
|
118
124
|
supertypes.add(rel.targetId);
|
|
119
125
|
directSupertypes.set(rel.sourceId, supertypes);
|
|
120
126
|
}
|
|
127
|
+
const orderedDirectSubtypes = new Map([...directSubtypes].map(([typeId, subtypes]) => [typeId, [...subtypes].sort().reverse()]));
|
|
121
128
|
const memberToClass = new Map();
|
|
122
129
|
for (const relationType of ['HAS_PROPERTY', 'HAS_METHOD']) {
|
|
123
130
|
for (const rel of ctx.graph.iterRelationshipsByType(relationType)) {
|
|
@@ -128,18 +135,45 @@ export const diPhase = {
|
|
|
128
135
|
const interfacesByLanguage = new Map();
|
|
129
136
|
const classesByLanguage = new Map();
|
|
130
137
|
ctx.graph.forEachNode((node) => {
|
|
131
|
-
|
|
138
|
+
const concreteType = isConcreteTypeNode(node);
|
|
139
|
+
if (!concreteType && node.label !== 'Interface')
|
|
132
140
|
return;
|
|
133
141
|
const language = node.properties.language;
|
|
134
142
|
if (typeof language !== 'string' || !candidateLanguages.has(language))
|
|
135
143
|
return;
|
|
136
|
-
const indexes =
|
|
144
|
+
const indexes = concreteType ? classesByLanguage : interfacesByLanguage;
|
|
137
145
|
const index = indexes.get(language) ?? emptyNameIndex();
|
|
138
146
|
addIndexedName(index, node);
|
|
139
147
|
indexes.set(language, index);
|
|
140
|
-
if (
|
|
148
|
+
if (concreteType)
|
|
141
149
|
providerNodes.set(node.id, node);
|
|
142
150
|
});
|
|
151
|
+
const concreteSubtypesByRoot = new Map();
|
|
152
|
+
const concreteSubtypes = (rootTypeId, language) => {
|
|
153
|
+
const cacheKey = `${language}\0${rootTypeId}`;
|
|
154
|
+
const cached = concreteSubtypesByRoot.get(cacheKey);
|
|
155
|
+
if (cached !== undefined)
|
|
156
|
+
return cached;
|
|
157
|
+
const concrete = new Set();
|
|
158
|
+
const queue = [rootTypeId];
|
|
159
|
+
const visited = new Set();
|
|
160
|
+
while (queue.length > 0) {
|
|
161
|
+
const typeId = queue.pop();
|
|
162
|
+
if (typeId === undefined || visited.has(typeId))
|
|
163
|
+
continue;
|
|
164
|
+
visited.add(typeId);
|
|
165
|
+
const typeNode = ctx.graph.getNode(typeId);
|
|
166
|
+
if (typeNode !== undefined &&
|
|
167
|
+
isConcreteTypeNode(typeNode) &&
|
|
168
|
+
typeNode.properties.language === language) {
|
|
169
|
+
concrete.add(typeId);
|
|
170
|
+
}
|
|
171
|
+
const children = orderedDirectSubtypes.get(typeId) ?? [];
|
|
172
|
+
queue.push(...children);
|
|
173
|
+
}
|
|
174
|
+
concreteSubtypesByRoot.set(cacheKey, concrete);
|
|
175
|
+
return concrete;
|
|
176
|
+
};
|
|
143
177
|
// A declaration returning a concrete class is assignable to every class or
|
|
144
178
|
// interface that type extends/implements. Expand once per language+type and
|
|
145
179
|
// register the declaration under those ancestor names. This keeps named
|
|
@@ -227,13 +261,12 @@ export const diPhase = {
|
|
|
227
261
|
ambiguousTypeNames.add(candidate.targetTypeName);
|
|
228
262
|
continue;
|
|
229
263
|
}
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
264
|
+
const rootTypeId = typeof classEntry === 'string'
|
|
265
|
+
? classEntry
|
|
266
|
+
: typeof interfaceEntry === 'string'
|
|
267
|
+
? interfaceEntry
|
|
268
|
+
: undefined;
|
|
269
|
+
const structural = new Set(rootTypeId === undefined ? [] : concreteSubtypes(rootTypeId, candidate.language));
|
|
237
270
|
for (const id of providedTypes.get(candidate.language)?.get(candidate.targetTypeName) ?? []) {
|
|
238
271
|
structural.add(id);
|
|
239
272
|
}
|
|
@@ -663,7 +663,11 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|
|
663
663
|
// each (no JSON/path/generation siblings). A v80 index still names `.json`
|
|
664
664
|
// keys and would skip workers while scope-resolution found nothing — the
|
|
665
665
|
// #1983 main-thread reparse. origin/main at allocation is 80.
|
|
666
|
-
|
|
666
|
+
// 81 -> 82: Java and Kotlin ParsedFile capture side channels now carry
|
|
667
|
+
// programmatic Spring lookup facts. A warm v81 cache has no such facts, so it
|
|
668
|
+
// would skip workers and silently omit the new INJECTS edges. origin/main at
|
|
669
|
+
// allocation is 81.
|
|
670
|
+
const SCHEMA_BUMP = 82;
|
|
667
671
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
668
672
|
try {
|
|
669
673
|
// package.json sits at gitnexus/package.json — two levels up from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.11-rc.
|
|
3
|
+
"version": "1.6.11-rc.18",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"cors": "^2.8.5",
|
|
67
67
|
"express": "^5.2.1",
|
|
68
68
|
"express-rate-limit": "^8.4.1",
|
|
69
|
+
"fast-xml-parser": "^5.11.1",
|
|
69
70
|
"glob": "^13.0.6",
|
|
70
71
|
"graphology": "^0.26.0",
|
|
71
72
|
"graphology-indices": "^0.17.0",
|