gitnexus 1.6.10-rc.55 → 1.6.10-rc.57
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/analysis-features.d.ts +21 -0
- package/dist/core/analysis-features.js +59 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.d.ts +3 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.js +7 -0
- package/dist/core/ingestion/frameworks/spring/bean-candidates.d.ts +24 -0
- package/dist/core/ingestion/frameworks/spring/bean-candidates.js +178 -0
- package/dist/core/ingestion/frameworks/spring/bean-catalog.d.ts +12 -0
- package/dist/core/ingestion/frameworks/spring/bean-catalog.js +26 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +18 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +42 -0
- package/dist/core/ingestion/languages/java/captures.d.ts +4 -3
- package/dist/core/ingestion/languages/java/captures.js +13 -16
- package/dist/core/ingestion/languages/java/package-facts.d.ts +5 -0
- package/dist/core/ingestion/languages/java/package-facts.js +9 -0
- package/dist/core/ingestion/languages/java/package-siblings.d.ts +3 -16
- package/dist/core/ingestion/languages/java/package-siblings.js +8 -147
- package/dist/core/ingestion/languages/java/query.js +9 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +13 -0
- package/dist/core/ingestion/languages/java/spring-bean-metadata.d.ts +2 -0
- package/dist/core/ingestion/languages/java/spring-bean-metadata.js +8 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/jvm/package-facts.d.ts +35 -0
- package/dist/core/ingestion/languages/jvm/package-facts.js +40 -0
- package/dist/core/ingestion/languages/jvm/package-siblings.d.ts +14 -0
- package/dist/core/ingestion/languages/jvm/package-siblings.js +129 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +17 -3
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +42 -8
- package/dist/core/ingestion/languages/kotlin/captures.js +22 -0
- package/dist/core/ingestion/languages/kotlin/package-facts.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/package-facts.js +9 -0
- package/dist/core/ingestion/languages/kotlin/package-siblings.d.ts +4 -0
- package/dist/core/ingestion/languages/kotlin/package-siblings.js +8 -0
- package/dist/core/ingestion/languages/kotlin/query.js +13 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +9 -1
- package/dist/core/ingestion/languages/kotlin/spring-bean-metadata.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-bean-metadata.js +8 -0
- package/dist/core/ingestion/languages/kotlin.js +5 -6
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +15 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +5 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +15 -16
- package/dist/core/lbug/csv-generator.js +17 -3
- package/dist/core/lbug/lbug-adapter.js +21 -0
- package/dist/core/lbug/schema.d.ts +1 -1
- package/dist/core/lbug/schema.js +1 -0
- package/dist/core/run-analyze.js +93 -0
- package/dist/mcp/local/bean-metadata.d.ts +2 -0
- package/dist/mcp/local/bean-metadata.js +25 -0
- package/dist/mcp/local/local-backend.d.ts +1 -1
- package/dist/mcp/local/local-backend.js +11 -2
- package/dist/storage/parse-cache.js +7 -1
- package/dist/storage/repo-manager.d.ts +6 -0
- package/package.json +1 -1
- package/web/assets/{index-DKqR2tAm.js → index-B4E0OhHQ.js} +2 -2
- package/web/index.html +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A durable statement that one analysis capability was produced by this build. */
|
|
2
|
+
export interface AnalysisFeatureDescriptor {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly version: number;
|
|
5
|
+
readonly appliesTo: (filePaths: readonly string[]) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export type AnalysisFeatureVersions = Readonly<Record<string, number>>;
|
|
8
|
+
/**
|
|
9
|
+
* The Class table shape is global even when a repository contains no JVM code.
|
|
10
|
+
* Existing v8 indexes predate the frameworkAnnotations column and therefore
|
|
11
|
+
* need one full rebuild before any incremental Class write can be safe.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CLASS_FRAMEWORK_ANNOTATIONS_FEATURE: AnalysisFeatureDescriptor;
|
|
14
|
+
/** Resolve the exact feature set this build promises for the supplied files. */
|
|
15
|
+
export declare function resolveAnalysisFeatureVersions(descriptors: readonly AnalysisFeatureDescriptor[], filePaths: readonly string[]): Record<string, number>;
|
|
16
|
+
/**
|
|
17
|
+
* Compare an untrusted metadata value with the exact capabilities produced by
|
|
18
|
+
* this build. Extra keys also mismatch: a rollback must rebuild instead of
|
|
19
|
+
* certifying graph semantics emitted only by a newer binary.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findAnalysisFeatureMismatches(actual: unknown, expected: AnalysisFeatureVersions): readonly string[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Class table shape is global even when a repository contains no JVM code.
|
|
3
|
+
* Existing v8 indexes predate the frameworkAnnotations column and therefore
|
|
4
|
+
* need one full rebuild before any incremental Class write can be safe.
|
|
5
|
+
*/
|
|
6
|
+
export const CLASS_FRAMEWORK_ANNOTATIONS_FEATURE = {
|
|
7
|
+
id: 'graph.class-framework-annotations',
|
|
8
|
+
version: 1,
|
|
9
|
+
appliesTo: () => true,
|
|
10
|
+
};
|
|
11
|
+
/** Resolve the exact feature set this build promises for the supplied files. */
|
|
12
|
+
export function resolveAnalysisFeatureVersions(descriptors, filePaths) {
|
|
13
|
+
const resolved = new Map();
|
|
14
|
+
const seenIds = new Set();
|
|
15
|
+
for (const descriptor of descriptors) {
|
|
16
|
+
if (descriptor.id.trim().length === 0) {
|
|
17
|
+
throw new Error('Analysis feature descriptor id must not be empty');
|
|
18
|
+
}
|
|
19
|
+
if (!Number.isSafeInteger(descriptor.version) || descriptor.version < 1) {
|
|
20
|
+
throw new Error(`Analysis feature "${descriptor.id}" has invalid version ${descriptor.version}`);
|
|
21
|
+
}
|
|
22
|
+
if (seenIds.has(descriptor.id)) {
|
|
23
|
+
throw new Error(`Duplicate analysis feature descriptor: ${descriptor.id}`);
|
|
24
|
+
}
|
|
25
|
+
seenIds.add(descriptor.id);
|
|
26
|
+
if (!descriptor.appliesTo(filePaths))
|
|
27
|
+
continue;
|
|
28
|
+
resolved.set(descriptor.id, descriptor.version);
|
|
29
|
+
}
|
|
30
|
+
return Object.fromEntries([...resolved].sort(([left], [right]) => left.localeCompare(right)));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compare an untrusted metadata value with the exact capabilities produced by
|
|
34
|
+
* this build. Extra keys also mismatch: a rollback must rebuild instead of
|
|
35
|
+
* certifying graph semantics emitted only by a newer binary.
|
|
36
|
+
*/
|
|
37
|
+
export function findAnalysisFeatureMismatches(actual, expected) {
|
|
38
|
+
const expectedKeys = Object.keys(expected);
|
|
39
|
+
if (actual === undefined)
|
|
40
|
+
return expectedKeys.map((id) => `missing:${id}`);
|
|
41
|
+
if (actual === null || typeof actual !== 'object' || Array.isArray(actual)) {
|
|
42
|
+
return ['invalid:analysisFeatures'];
|
|
43
|
+
}
|
|
44
|
+
const stamped = actual;
|
|
45
|
+
const mismatches = [];
|
|
46
|
+
for (const id of expectedKeys) {
|
|
47
|
+
const value = stamped[id];
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
mismatches.push(`missing:${id}`);
|
|
50
|
+
else if (value !== expected[id])
|
|
51
|
+
mismatches.push(`version:${id}`);
|
|
52
|
+
}
|
|
53
|
+
for (const id of Object.keys(stamped)) {
|
|
54
|
+
if (!Object.prototype.hasOwnProperty.call(expected, id)) {
|
|
55
|
+
mismatches.push(`unexpected:${id}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return mismatches.sort();
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isSpringBeanCandidateSourceFile } from './bean-catalog.js';
|
|
2
|
+
/** Durable completeness contract for Java/Kotlin Spring Bean evidence. */
|
|
3
|
+
export const SPRING_BEAN_INVENTORY_FEATURE = {
|
|
4
|
+
id: 'spring.bean-inventory',
|
|
5
|
+
version: 1,
|
|
6
|
+
appliesTo: (filePaths) => filePaths.some(isSpringBeanCandidateSourceFile),
|
|
7
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Capture, ParsedFile, ScopeId } from '../../../../_shared/index.js';
|
|
2
|
+
import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
3
|
+
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
4
|
+
import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
|
5
|
+
export interface ClassAnnotationFact {
|
|
6
|
+
readonly classScopeId: ScopeId;
|
|
7
|
+
readonly annotationNames: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ClassAnnotationFactStore {
|
|
10
|
+
clear(): void;
|
|
11
|
+
set(filePath: string, facts: readonly ClassAnnotationFact[]): void;
|
|
12
|
+
get(filePath: string): readonly ClassAnnotationFact[];
|
|
13
|
+
}
|
|
14
|
+
/** Per-language store for capture facts that cross the worker boundary. */
|
|
15
|
+
export declare function createClassAnnotationFactStore(): ClassAnnotationFactStore;
|
|
16
|
+
/** Record one annotation from the language's existing scope-query traversal. */
|
|
17
|
+
export declare function recordClassAnnotationCapture(facts: Map<ScopeId, Set<string>>, filePath: string, classCapture: Pick<Capture, 'range'>, annotationName: string): void;
|
|
18
|
+
export declare function materializeClassAnnotationFacts(facts: ReadonlyMap<ScopeId, ReadonlySet<string>>): readonly ClassAnnotationFact[];
|
|
19
|
+
export interface SpringBeanCandidateAdapter {
|
|
20
|
+
getClassAnnotationFacts(filePath: string): readonly ClassAnnotationFact[];
|
|
21
|
+
isPackageVisibilityIncomplete(filePath: string): boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Build a language hook that enriches Class nodes after scope resolution. */
|
|
24
|
+
export declare function createSpringBeanCandidateAttacher(adapter: SpringBeanCandidateAdapter): (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, indexes: ScopeResolutionIndexes) => void;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { makeScopeId } from '../../../../_shared/index.js';
|
|
2
|
+
import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
|
|
3
|
+
import { isClassLike, lookupBindingsAt } from '../../scope-resolution/scope/walkers.js';
|
|
4
|
+
import { SPRING_BEAN_STEREOTYPES } from './bean-catalog.js';
|
|
5
|
+
/** Per-language store for capture facts that cross the worker boundary. */
|
|
6
|
+
export function createClassAnnotationFactStore() {
|
|
7
|
+
const factsByFile = new Map();
|
|
8
|
+
return {
|
|
9
|
+
clear: () => factsByFile.clear(),
|
|
10
|
+
set: (filePath, facts) => {
|
|
11
|
+
if (facts.length === 0)
|
|
12
|
+
factsByFile.delete(filePath);
|
|
13
|
+
else
|
|
14
|
+
factsByFile.set(filePath, facts);
|
|
15
|
+
},
|
|
16
|
+
get: (filePath) => factsByFile.get(filePath) ?? [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Record one annotation from the language's existing scope-query traversal. */
|
|
20
|
+
export function recordClassAnnotationCapture(facts, filePath, classCapture, annotationName) {
|
|
21
|
+
const classScopeId = makeScopeId({ filePath, range: classCapture.range, kind: 'Class' });
|
|
22
|
+
const names = facts.get(classScopeId) ?? new Set();
|
|
23
|
+
names.add(annotationName.trim());
|
|
24
|
+
facts.set(classScopeId, names);
|
|
25
|
+
}
|
|
26
|
+
export function materializeClassAnnotationFacts(facts) {
|
|
27
|
+
return [...facts].map(([classScopeId, annotationNames]) => ({
|
|
28
|
+
classScopeId,
|
|
29
|
+
annotationNames: [...annotationNames],
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function simpleNameOf(def) {
|
|
33
|
+
const qualifiedName = def.qualifiedName;
|
|
34
|
+
if (qualifiedName === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const separator = qualifiedName.lastIndexOf('.');
|
|
37
|
+
return separator === -1 ? qualifiedName : qualifiedName.slice(separator + 1);
|
|
38
|
+
}
|
|
39
|
+
function buildOwnedTypeNamesByOwner(indexes) {
|
|
40
|
+
const namesByOwner = new Map();
|
|
41
|
+
for (const def of indexes.defs.byId.values()) {
|
|
42
|
+
if (def.ownerId === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
if (!isClassLike(def.type) && def.type !== 'Annotation')
|
|
45
|
+
continue;
|
|
46
|
+
const simpleName = simpleNameOf(def);
|
|
47
|
+
if (simpleName === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
const names = namesByOwner.get(def.ownerId) ?? new Set();
|
|
50
|
+
names.add(simpleName);
|
|
51
|
+
namesByOwner.set(def.ownerId, names);
|
|
52
|
+
}
|
|
53
|
+
return namesByOwner;
|
|
54
|
+
}
|
|
55
|
+
function hasLexicalTypeDeclaration(startScope, simpleName, indexes) {
|
|
56
|
+
let scopeId = startScope;
|
|
57
|
+
const visited = new Set();
|
|
58
|
+
while (scopeId !== null && !visited.has(scopeId)) {
|
|
59
|
+
visited.add(scopeId);
|
|
60
|
+
const scope = indexes.scopeTree.getScope(scopeId);
|
|
61
|
+
if (scope === undefined)
|
|
62
|
+
return false;
|
|
63
|
+
const locals = scope.bindings.get(simpleName);
|
|
64
|
+
if (locals?.some(({ def }) => isClassLike(def.type) || def.type === 'Annotation'))
|
|
65
|
+
return true;
|
|
66
|
+
scopeId = scope.parent;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function explicitImportTargets(parsed, simpleName) {
|
|
71
|
+
const targets = new Set();
|
|
72
|
+
for (const entry of parsed.parsedImports) {
|
|
73
|
+
if (entry.kind !== 'named' && entry.kind !== 'alias')
|
|
74
|
+
continue;
|
|
75
|
+
if (entry.localName !== simpleName)
|
|
76
|
+
continue;
|
|
77
|
+
targets.add(entry.targetRaw);
|
|
78
|
+
}
|
|
79
|
+
return targets;
|
|
80
|
+
}
|
|
81
|
+
function hasInheritedTypeDeclaration(startScope, simpleName, indexes, ownedTypeNamesByOwner) {
|
|
82
|
+
let scopeId = startScope;
|
|
83
|
+
const visited = new Set();
|
|
84
|
+
while (scopeId !== null && !visited.has(scopeId)) {
|
|
85
|
+
visited.add(scopeId);
|
|
86
|
+
const scope = indexes.scopeTree.getScope(scopeId);
|
|
87
|
+
if (scope === undefined)
|
|
88
|
+
return false;
|
|
89
|
+
if (scope.kind === 'Class') {
|
|
90
|
+
const classDef = scope.ownedDefs.find((def) => isClassLike(def.type));
|
|
91
|
+
if (classDef !== undefined) {
|
|
92
|
+
for (const ancestorId of indexes.methodDispatch.mroFor(classDef.nodeId)) {
|
|
93
|
+
if (ownedTypeNamesByOwner.get(ancestorId)?.has(simpleName) === true)
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
scopeId = scope.parent;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
function hasVisibleTypeBinding(startScope, simpleName, indexes) {
|
|
103
|
+
let scopeId = startScope;
|
|
104
|
+
const visited = new Set();
|
|
105
|
+
while (scopeId !== null && !visited.has(scopeId)) {
|
|
106
|
+
visited.add(scopeId);
|
|
107
|
+
const scope = indexes.scopeTree.getScope(scopeId);
|
|
108
|
+
if (scope === undefined)
|
|
109
|
+
return false;
|
|
110
|
+
const visible = lookupBindingsAt(scopeId, simpleName, indexes);
|
|
111
|
+
if (visible.some(({ def }) => isClassLike(def.type) || def.type === 'Annotation'))
|
|
112
|
+
return true;
|
|
113
|
+
scopeId = scope.parent;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function wildcardImportTarget(parsed, simpleName) {
|
|
118
|
+
const wildcardPackages = new Set(parsed.parsedImports
|
|
119
|
+
.filter((entry) => entry.kind === 'wildcard')
|
|
120
|
+
.map((entry) => entry.targetRaw.replace(/\.\*$/, '')));
|
|
121
|
+
if (wildcardPackages.size !== 1)
|
|
122
|
+
return undefined;
|
|
123
|
+
const [packageName] = wildcardPackages;
|
|
124
|
+
const target = `${packageName}.${simpleName}`;
|
|
125
|
+
return SPRING_BEAN_STEREOTYPES.has(target) ? target : undefined;
|
|
126
|
+
}
|
|
127
|
+
function resolveSpringAnnotation(rawName, parsed, enclosingScope, indexes, ownedTypeNamesByOwner, isPackageVisibilityIncomplete) {
|
|
128
|
+
if (rawName.includes('.')) {
|
|
129
|
+
return SPRING_BEAN_STEREOTYPES.has(rawName) ? rawName : undefined;
|
|
130
|
+
}
|
|
131
|
+
if (hasLexicalTypeDeclaration(enclosingScope, rawName, indexes))
|
|
132
|
+
return undefined;
|
|
133
|
+
if (hasInheritedTypeDeclaration(enclosingScope, rawName, indexes, ownedTypeNamesByOwner)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const explicitImports = explicitImportTargets(parsed, rawName);
|
|
137
|
+
if (explicitImports.size > 0) {
|
|
138
|
+
if (explicitImports.size !== 1)
|
|
139
|
+
return undefined;
|
|
140
|
+
const [imported] = explicitImports;
|
|
141
|
+
return SPRING_BEAN_STEREOTYPES.has(imported) ? imported : undefined;
|
|
142
|
+
}
|
|
143
|
+
const wildcardTarget = wildcardImportTarget(parsed, rawName);
|
|
144
|
+
if (wildcardTarget === undefined || isPackageVisibilityIncomplete)
|
|
145
|
+
return undefined;
|
|
146
|
+
return hasVisibleTypeBinding(enclosingScope, rawName, indexes) ? undefined : wildcardTarget;
|
|
147
|
+
}
|
|
148
|
+
/** Build a language hook that enriches Class nodes after scope resolution. */
|
|
149
|
+
export function createSpringBeanCandidateAttacher(adapter) {
|
|
150
|
+
return (graph, parsedFiles, nodeLookup, indexes) => {
|
|
151
|
+
const ownedTypeNamesByOwner = buildOwnedTypeNamesByOwner(indexes);
|
|
152
|
+
for (const parsed of parsedFiles) {
|
|
153
|
+
for (const fact of adapter.getClassAnnotationFacts(parsed.filePath)) {
|
|
154
|
+
const classScope = indexes.scopeTree.getScope(fact.classScopeId);
|
|
155
|
+
if (classScope === undefined || classScope.kind !== 'Class')
|
|
156
|
+
continue;
|
|
157
|
+
const classDef = classScope.ownedDefs.find((def) => def.type === 'Class');
|
|
158
|
+
if (classDef === undefined)
|
|
159
|
+
continue;
|
|
160
|
+
const graphId = resolveDefGraphId(parsed.filePath, classDef, nodeLookup);
|
|
161
|
+
if (graphId === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
const classNode = graph.getNode(graphId);
|
|
164
|
+
if (classNode === undefined || classNode.label !== 'Class')
|
|
165
|
+
continue;
|
|
166
|
+
const recognized = new Set();
|
|
167
|
+
for (const rawName of fact.annotationNames) {
|
|
168
|
+
const annotation = resolveSpringAnnotation(rawName, parsed, classScope.parent, indexes, ownedTypeNamesByOwner, adapter.isPackageVisibilityIncomplete(parsed.filePath));
|
|
169
|
+
if (annotation !== undefined)
|
|
170
|
+
recognized.add(annotation);
|
|
171
|
+
}
|
|
172
|
+
if (recognized.size === 1) {
|
|
173
|
+
classNode.properties.frameworkAnnotations = [...recognized];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SpringBeanMetadata {
|
|
2
|
+
framework: 'spring';
|
|
3
|
+
role: string;
|
|
4
|
+
annotation: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SpringBeanStereotype {
|
|
7
|
+
role: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const SPRING_BEAN_STEREOTYPES: Map<string, SpringBeanStereotype>;
|
|
10
|
+
export declare function deriveSpringBeanMetadata(frameworkAnnotations: readonly string[]): SpringBeanMetadata | undefined;
|
|
11
|
+
/** Whether a source change can alter Spring Bean candidate metadata. */
|
|
12
|
+
export declare function isSpringBeanCandidateSourceFile(filePath: string): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const SPRING_BEAN_STEREOTYPES = new Map([
|
|
2
|
+
['org.springframework.stereotype.Component', { role: 'component' }],
|
|
3
|
+
['org.springframework.stereotype.Service', { role: 'service' }],
|
|
4
|
+
['org.springframework.stereotype.Repository', { role: 'repository' }],
|
|
5
|
+
['org.springframework.stereotype.Controller', { role: 'controller' }],
|
|
6
|
+
['org.springframework.web.bind.annotation.RestController', { role: 'rest-controller' }],
|
|
7
|
+
['org.springframework.context.annotation.Configuration', { role: 'configuration' }],
|
|
8
|
+
]);
|
|
9
|
+
export function deriveSpringBeanMetadata(frameworkAnnotations) {
|
|
10
|
+
const recognized = [
|
|
11
|
+
...new Set(frameworkAnnotations.filter((annotation) => SPRING_BEAN_STEREOTYPES.has(annotation))),
|
|
12
|
+
];
|
|
13
|
+
if (recognized.length !== 1)
|
|
14
|
+
return undefined;
|
|
15
|
+
const annotation = recognized[0];
|
|
16
|
+
const stereotype = SPRING_BEAN_STEREOTYPES.get(annotation);
|
|
17
|
+
if (!stereotype)
|
|
18
|
+
return undefined;
|
|
19
|
+
return { framework: 'spring', role: stereotype.role, annotation };
|
|
20
|
+
}
|
|
21
|
+
const SPRING_BEAN_SOURCE_EXTENSIONS = ['.java', '.kt', '.kts'];
|
|
22
|
+
/** Whether a source change can alter Spring Bean candidate metadata. */
|
|
23
|
+
export function isSpringBeanCandidateSourceFile(filePath) {
|
|
24
|
+
const normalized = filePath.toLowerCase();
|
|
25
|
+
return SPRING_BEAN_SOURCE_EXTENSIONS.some((extension) => normalized.endsWith(extension));
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
2
|
+
import { type ClassAnnotationFact } from '../../frameworks/spring/bean-candidates.js';
|
|
3
|
+
import { type JvmPackageFact } from '../jvm/package-facts.js';
|
|
4
|
+
export type JavaClassAnnotationFact = ClassAnnotationFact;
|
|
5
|
+
export interface JavaCaptureSideChannel {
|
|
6
|
+
readonly kind: 'java';
|
|
7
|
+
readonly packageFact: JvmPackageFact;
|
|
8
|
+
readonly classAnnotations: readonly JavaClassAnnotationFact[];
|
|
9
|
+
}
|
|
10
|
+
/** Clear facts retained by a prior workspace pass in a long-lived process. */
|
|
11
|
+
export declare function clearJavaClassAnnotationFacts(): void;
|
|
12
|
+
/** Store the annotation syntax collected by Java's existing scope-query traversal. */
|
|
13
|
+
export declare function setJavaClassAnnotationFacts(filePath: string, facts: readonly JavaClassAnnotationFact[]): void;
|
|
14
|
+
/** Snapshot worker-local Java annotation facts for ParsedFile serialization. */
|
|
15
|
+
export declare function collectJavaCaptureSideChannel(filePath: string): JavaCaptureSideChannel | undefined;
|
|
16
|
+
export declare function getJavaClassAnnotationFacts(filePath: string): readonly JavaClassAnnotationFact[];
|
|
17
|
+
/** Restore worker-collected facts before Java's post-resolution hook runs. */
|
|
18
|
+
export declare function applyJavaCaptureSideChannel(parsed: ParsedFile): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createClassAnnotationFactStore, } from '../../frameworks/spring/bean-candidates.js';
|
|
2
|
+
import { isJvmPackageFact, UNKNOWN_JVM_PACKAGE_FACT, } from '../jvm/package-facts.js';
|
|
3
|
+
import { getJavaPackageFact, setJavaPackageFact } from './package-facts.js';
|
|
4
|
+
const classAnnotations = createClassAnnotationFactStore();
|
|
5
|
+
/** Clear facts retained by a prior workspace pass in a long-lived process. */
|
|
6
|
+
export function clearJavaClassAnnotationFacts() {
|
|
7
|
+
classAnnotations.clear();
|
|
8
|
+
}
|
|
9
|
+
/** Store the annotation syntax collected by Java's existing scope-query traversal. */
|
|
10
|
+
export function setJavaClassAnnotationFacts(filePath, facts) {
|
|
11
|
+
classAnnotations.set(filePath, facts);
|
|
12
|
+
}
|
|
13
|
+
/** Snapshot worker-local Java annotation facts for ParsedFile serialization. */
|
|
14
|
+
export function collectJavaCaptureSideChannel(filePath) {
|
|
15
|
+
const facts = classAnnotations.get(filePath);
|
|
16
|
+
const packageFact = getJavaPackageFact(filePath);
|
|
17
|
+
if (facts.length === 0 && packageFact === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
return {
|
|
20
|
+
kind: 'java',
|
|
21
|
+
packageFact: packageFact ?? UNKNOWN_JVM_PACKAGE_FACT,
|
|
22
|
+
classAnnotations: facts,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function getJavaClassAnnotationFacts(filePath) {
|
|
26
|
+
return classAnnotations.get(filePath);
|
|
27
|
+
}
|
|
28
|
+
/** Restore worker-collected facts before Java's post-resolution hook runs. */
|
|
29
|
+
export function applyJavaCaptureSideChannel(parsed) {
|
|
30
|
+
const data = parsed.captureSideChannel;
|
|
31
|
+
if (data === undefined ||
|
|
32
|
+
data === null ||
|
|
33
|
+
typeof data !== 'object' ||
|
|
34
|
+
data.kind !== 'java' ||
|
|
35
|
+
!Array.isArray(data.classAnnotations)) {
|
|
36
|
+
setJavaClassAnnotationFacts(parsed.filePath, []);
|
|
37
|
+
setJavaPackageFact(parsed.filePath, UNKNOWN_JVM_PACKAGE_FACT);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setJavaClassAnnotationFacts(parsed.filePath, data.classAnnotations);
|
|
41
|
+
setJavaPackageFact(parsed.filePath, isJvmPackageFact(data.packageFact) ? data.packageFact : UNKNOWN_JVM_PACKAGE_FACT);
|
|
42
|
+
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* 3. **Arity metadata** on method/constructor declarations.
|
|
12
12
|
* 4. **Reference arity** on call sites.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* The returned captures are deterministic. Class-annotation facts are also
|
|
15
|
+
* recorded for the worker side-channel consumed after scope resolution.
|
|
15
16
|
*/
|
|
16
|
-
import type
|
|
17
|
-
export declare function emitJavaScopeCaptures(sourceText: string,
|
|
17
|
+
import { type CaptureMatch } from '../../../../_shared/index.js';
|
|
18
|
+
export declare function emitJavaScopeCaptures(sourceText: string, filePath: string, cachedTree?: unknown): readonly CaptureMatch[];
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* `emitScopeCaptures` for Java.
|
|
3
|
-
*
|
|
4
|
-
* Drives the Java scope query against tree-sitter-java and groups raw
|
|
5
|
-
* matches into `CaptureMatch[]` for the central extractor. Layers:
|
|
6
|
-
*
|
|
7
|
-
* 1. **Decomposed import declarations** — each `import_declaration`
|
|
8
|
-
* is re-emitted with `@import.kind/source/name` markers.
|
|
9
|
-
* 2. **Receiver binding synthesis** — `this`/`super` type-bindings
|
|
10
|
-
* on instance methods.
|
|
11
|
-
* 3. **Arity metadata** on method/constructor declarations.
|
|
12
|
-
* 4. **Reference arity** on call sites.
|
|
13
|
-
*
|
|
14
|
-
* Pure given the input source text. No I/O, no globals consulted.
|
|
15
|
-
*/
|
|
1
|
+
import { materializeClassAnnotationFacts, recordClassAnnotationCapture, } from '../../frameworks/spring/bean-candidates.js';
|
|
16
2
|
import { nodeIfType, nodeToCapture, synthesizeJavaAnonymousClassName, syntheticCapture, } from '../../utils/ast-helpers.js';
|
|
17
3
|
import { splitImportDeclaration } from './import-decomposer.js';
|
|
18
4
|
import { computeJavaArityMetadata } from './arity-metadata.js';
|
|
@@ -21,6 +7,8 @@ import { getJavaParser, getJavaScopeQuery } from './query.js';
|
|
|
21
7
|
import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
|
|
22
8
|
import { getTreeSitterBufferSize } from '../../constants.js';
|
|
23
9
|
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
10
|
+
import { setJavaClassAnnotationFacts } from './capture-side-channel.js';
|
|
11
|
+
import { captureJavaPackageFact } from './package-facts.js';
|
|
24
12
|
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
|
25
13
|
/** Declaration anchors that carry function-like arity metadata. */
|
|
26
14
|
const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'];
|
|
@@ -58,7 +46,7 @@ function shouldEmitReadMember(memberNode) {
|
|
|
58
46
|
return true;
|
|
59
47
|
}
|
|
60
48
|
}
|
|
61
|
-
export function emitJavaScopeCaptures(sourceText,
|
|
49
|
+
export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
|
|
62
50
|
let tree = cachedTree;
|
|
63
51
|
if (tree === undefined) {
|
|
64
52
|
tree = parseSourceSafe(getJavaParser(), sourceText, undefined, {
|
|
@@ -69,8 +57,10 @@ export function emitJavaScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
|
69
57
|
else {
|
|
70
58
|
recordCacheHit();
|
|
71
59
|
}
|
|
60
|
+
captureJavaPackageFact(filePath, tree.rootNode);
|
|
72
61
|
const rawMatches = getJavaScopeQuery().matches(tree.rootNode);
|
|
73
62
|
const out = [];
|
|
63
|
+
const classAnnotations = new Map();
|
|
74
64
|
for (const m of rawMatches) {
|
|
75
65
|
const grouped = {};
|
|
76
66
|
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
|
|
@@ -89,6 +79,12 @@ export function emitJavaScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
|
89
79
|
}
|
|
90
80
|
if (Object.keys(grouped).length === 0)
|
|
91
81
|
continue;
|
|
82
|
+
const annotatedClass = grouped['@class-annotation.class'];
|
|
83
|
+
const annotationName = grouped['@class-annotation.name'];
|
|
84
|
+
if (annotatedClass !== undefined && annotationName !== undefined) {
|
|
85
|
+
recordClassAnnotationCapture(classAnnotations, filePath, annotatedClass, annotationName.text);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
92
88
|
// Decompose each `import_declaration`. `@import.statement` is captured
|
|
93
89
|
// directly on the `import_declaration` node.
|
|
94
90
|
if (grouped['@import.statement'] !== undefined) {
|
|
@@ -180,6 +176,7 @@ export function emitJavaScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
|
180
176
|
}
|
|
181
177
|
out.push(grouped);
|
|
182
178
|
}
|
|
179
|
+
setJavaClassAnnotationFacts(filePath, materializeClassAnnotationFacts(classAnnotations));
|
|
183
180
|
return [
|
|
184
181
|
...resolveVarTypeBindings(out),
|
|
185
182
|
...synthesizeJavaInheritanceReferences(tree.rootNode),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type JvmPackageFact, type JvmPackageSyntaxNode } from '../jvm/package-facts.js';
|
|
2
|
+
export declare const clearJavaPackageFacts: () => void;
|
|
3
|
+
export declare const captureJavaPackageFact: (filePath: string, root: JvmPackageSyntaxNode) => void;
|
|
4
|
+
export declare const setJavaPackageFact: (filePath: string, fact: JvmPackageFact) => void;
|
|
5
|
+
export declare const getJavaPackageFact: (filePath: string) => JvmPackageFact | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createJvmPackageFactStore, } from '../jvm/package-facts.js';
|
|
2
|
+
const javaPackageFacts = createJvmPackageFactStore({
|
|
3
|
+
packageNodeType: 'package_declaration',
|
|
4
|
+
packageNameNodeTypes: ['scoped_identifier', 'identifier'],
|
|
5
|
+
});
|
|
6
|
+
export const clearJavaPackageFacts = () => javaPackageFacts.clear();
|
|
7
|
+
export const captureJavaPackageFact = (filePath, root) => javaPackageFacts.capture(filePath, root);
|
|
8
|
+
export const setJavaPackageFact = (filePath, fact) => javaPackageFacts.set(filePath, fact);
|
|
9
|
+
export const getJavaPackageFact = (filePath) => javaPackageFacts.get(filePath);
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Java package-scope implicit visibility.
|
|
3
|
-
*
|
|
4
|
-
* Classes in the same Java package see each other without explicit
|
|
5
|
-
* `import` statements. This hook groups files by `package` declaration,
|
|
6
|
-
* then injects cross-file class defs into each file's module-scope
|
|
7
|
-
* `bindingAugmentations` and mirrors type-bindings across same-package
|
|
8
|
-
* files — the Java equivalent of C#'s `populateNamespaceSiblings`.
|
|
9
|
-
*/
|
|
10
|
-
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
11
|
-
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
12
|
-
export declare function populateJavaPackageSiblings(parsedFiles: readonly ParsedFile[], indexes: ScopeResolutionIndexes, ctx: {
|
|
1
|
+
export declare const populateJavaPackageSiblings: (parsedFiles: readonly import('../../../../_shared/index.js').ParsedFile[], indexes: import("../../model/scope-resolution-indexes.js").ScopeResolutionIndexes, ctx: {
|
|
13
2
|
readonly fileContents: ReadonlyMap<string, string>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
17
|
-
}): void;
|
|
3
|
+
}) => void;
|
|
4
|
+
export declare const isJavaPackageSiblingVisibilityIncomplete: (filePath: string) => boolean;
|