gitnexus 1.6.10-rc.97 → 1.6.10-rc.98

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.
Files changed (24) hide show
  1. package/dist/_shared/graph/types.d.ts +8 -8
  2. package/dist/core/ingestion/di-extractors/index.d.ts +40 -28
  3. package/dist/core/ingestion/di-extractors/index.js +17 -18
  4. package/dist/core/ingestion/di-extractors/spring.d.ts +27 -5
  5. package/dist/core/ingestion/di-extractors/spring.js +80 -3
  6. package/dist/core/ingestion/frameworks/spring/di-metadata.d.ts +45 -0
  7. package/dist/core/ingestion/frameworks/spring/di-metadata.js +230 -0
  8. package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
  9. package/dist/core/ingestion/languages/java/capture-side-channel.js +19 -1
  10. package/dist/core/ingestion/languages/java/captures.js +12 -1
  11. package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
  12. package/dist/core/ingestion/languages/java/spring-di.d.ts +17 -0
  13. package/dist/core/ingestion/languages/java/spring-di.js +128 -0
  14. package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +9 -1
  15. package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +23 -2
  16. package/dist/core/ingestion/languages/kotlin/captures.js +12 -1
  17. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +5 -1
  18. package/dist/core/ingestion/languages/kotlin/spring-di.d.ts +18 -0
  19. package/dist/core/ingestion/languages/kotlin/spring-di.js +250 -0
  20. package/dist/core/ingestion/languages/kotlin.js +4 -4
  21. package/dist/core/ingestion/pipeline-phases/di.d.ts +8 -33
  22. package/dist/core/ingestion/pipeline-phases/di.js +162 -159
  23. package/dist/storage/parse-cache.js +3 -1
  24. package/package.json +1 -1
@@ -55,14 +55,14 @@ export type NodeProperties = {
55
55
  [key: string]: unknown;
56
56
  };
57
57
  export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'METHOD_OVERRIDES' | 'METHOD_IMPLEMENTS' | 'IMPORTS' | 'USES' | 'DEFINES' | 'DECORATES' | 'IMPLEMENTS' | 'EXTENDS' | 'HAS_METHOD' | 'HAS_PROPERTY' | 'ACCESSES' | 'MEMBER_OF' | 'STEP_IN_PROCESS' | 'HANDLES_ROUTE' | 'FETCHES' | 'HANDLES_TOOL' | 'ENTRY_POINT_OF' | 'WRAPS' | 'QUERIES'
58
- /** Dependency-injection edge: a consumer class receives every implementer
59
- * of interface `T` via a container-injected collection-typed field
60
- * (`List<T>`, `Set<T>`, `Collection<T>`, or `Map<K,T>`). Precondition: the
61
- * field carries an injection annotation recognized by a per-language
62
- * matcher registered in `di-extractors/` (Java/Spring today: `@Autowired`
63
- * or `@Inject`; `@Resource` is excluded — by-name-first semantics).
64
- * Source = the consumer Class node (the one owning the field).
65
- * Target = an implementing Class node.
58
+ /** Dependency-injection edge: a consumer class receives a likely provider
59
+ * through constructor, field, method, or collection injection. A
60
+ * per-language resolver identifies the site and provider metadata; the
61
+ * shared DI phase uses type heritage, qualifier names, and preferred
62
+ * provider markers to resolve it. Ambiguous single injection is represented
63
+ * by multiple lower-confidence edges instead of a fabricated exact target.
64
+ * Source = the consumer Class node (the one owning the injection site).
65
+ * Target = a concrete provider Class node.
66
66
  * Framework specifics live in the `reason` payload (e.g.
67
67
  * `Spring DI: @Autowired List<T>`), not in this type contract.
68
68
  * Lets Cypher queries trace which beans the container injects into a given
@@ -1,47 +1,59 @@
1
1
  /**
2
- * Per-language DI field-matcher registry — the lookup the generic `di`
3
- * pipeline phase uses to decide whether a `Property` node is a
4
- * dependency-injection fan-out candidate.
2
+ * Per-language DI resolver registry — the lookup the generic `di` pipeline
3
+ * phase uses to discover injection sites and provider metadata on graph nodes.
5
4
  *
6
5
  * Mirrors `scope-resolution/pipeline/registry.ts` (`SCOPE_RESOLVERS`): a
7
- * single-valued `ReadonlyMap<SupportedLanguages, DiFieldMatcher>` consumed by
6
+ * single-valued `ReadonlyMap<SupportedLanguages, DiResolver>` consumed by
8
7
  * a framework-neutral phase, so no language or framework names leak into
9
- * shared pipeline code. Adding a framework is two lines: implement a
10
- * `DiFieldMatcher` in `di-extractors/<framework>.ts` and register it here.
8
+ * shared pipeline code. Adding a framework means implementing a `DiResolver`
9
+ * in `di-extractors/<framework>.ts` and registering it here.
11
10
  *
12
- * Scope honesty: matchers are per-language *field-injection* matchers.
13
- * Constructor injection (the dominant modern Spring idiom) lives on
14
- * Method/parameter nodes and would require widening the phase's routing —
15
- * deliberately out of scope (see the plan's Deferred work). The registry is
16
- * single-valued per language, matching the `SCOPE_RESOLVERS` shape; widen the
17
- * value type to arrays only when a second same-language framework actually
18
- * lands (a one-line type change then).
11
+ * The registry is single-valued per language, matching the `SCOPE_RESOLVERS`
12
+ * shape; widen the value type to arrays only when a second same-language
13
+ * framework actually lands. Java and Kotlin share Spring's attached metadata
14
+ * contract while retaining language-specific syntax capture.
19
15
  */
20
16
  import { SupportedLanguages } from '../../../_shared/index.js';
21
17
  import type { GraphNode } from '../../../_shared/index.js';
22
- /** A successful DI field match, produced by a per-language matcher. */
23
- export interface DiFieldMatch {
24
- /** The element type name `T` — the injected bean interface. */
25
- elementTypeName: string;
18
+ /** A successful injection-site match, produced by a per-language resolver. */
19
+ export interface DiInjectionMatch {
20
+ /** The requested dependency type name. */
21
+ targetTypeName: string;
22
+ /** A collection receives every matching provider; a single site may need
23
+ * framework-specific named/preferred-provider disambiguation. */
24
+ cardinality: 'single' | 'collection';
25
+ /** Statically known provider name requested at the injection site. The
26
+ * resolver owns the human-readable explanation of that selection. */
27
+ namedSelection?: {
28
+ name: string;
29
+ reason: string;
30
+ };
26
31
  /** Human-readable edge reason. Framework specifics (names, idioms,
27
32
  * collection wrapper, gating annotation) live in this payload so the
28
33
  * shared `di` phase stays framework-neutral. */
29
34
  reason: string;
30
35
  }
31
- /**
32
- * A per-language field-injection matcher: given a `Property` node, return the
33
- * parsed DI match or `null` when the field is not container-injected. The
34
- * matcher receives the whole node (not pre-plucked fields) so the shared
35
- * phase stays ignorant of which properties matter.
36
- */
37
- export type DiFieldMatcher = (node: GraphNode) => DiFieldMatch | null;
36
+ /** Provider metadata used by the shared resolver without naming a framework. */
37
+ export interface DiProviderMatch {
38
+ /** Provider names and aliases that can satisfy a named injection. */
39
+ names: readonly string[];
40
+ /** Present when the framework marks this as its preferred candidate. The
41
+ * value is appended to the emitted edge reason when it disambiguates. */
42
+ preferenceReason?: string;
43
+ }
44
+ /** Per-language DI behavior. Matchers receive whole nodes so the shared phase
45
+ * remains ignorant of language/framework-specific property shapes. */
46
+ export interface DiResolver {
47
+ matchInjectionSites(node: GraphNode): readonly DiInjectionMatch[];
48
+ matchProvider(node: GraphNode): DiProviderMatch | null;
49
+ }
38
50
  /**
39
51
  * Type guard narrowing an arbitrary graph `language` string to
40
- * `SupportedLanguages`, so `DI_MATCHERS.get()` needs no cast.
52
+ * `SupportedLanguages`, so `DI_RESOLVERS.get()` needs no cast.
41
53
  */
42
54
  export declare function isSupportedLanguage(value: string): value is SupportedLanguages;
43
- /** Map of `SupportedLanguages` → `DiFieldMatcher`. The `di` phase routes each
44
- * `Property` node here by `node.properties.language`; no entry ⇒ the node is
55
+ /** Map of `SupportedLanguages` → `DiResolver`. The `di` phase routes each
56
+ * graph node here by `node.properties.language`; no entry ⇒ the node is
45
57
  * skipped. This is the single source of truth for which languages (and,
46
58
  * transitively, frameworks) produce INJECTS edges. */
47
- export declare const DI_MATCHERS: ReadonlyMap<SupportedLanguages, DiFieldMatcher>;
59
+ export declare const DI_RESOLVERS: ReadonlyMap<SupportedLanguages, DiResolver>;
@@ -1,35 +1,34 @@
1
1
  /**
2
- * Per-language DI field-matcher registry — the lookup the generic `di`
3
- * pipeline phase uses to decide whether a `Property` node is a
4
- * dependency-injection fan-out candidate.
2
+ * Per-language DI resolver registry — the lookup the generic `di` pipeline
3
+ * phase uses to discover injection sites and provider metadata on graph nodes.
5
4
  *
6
5
  * Mirrors `scope-resolution/pipeline/registry.ts` (`SCOPE_RESOLVERS`): a
7
- * single-valued `ReadonlyMap<SupportedLanguages, DiFieldMatcher>` consumed by
6
+ * single-valued `ReadonlyMap<SupportedLanguages, DiResolver>` consumed by
8
7
  * a framework-neutral phase, so no language or framework names leak into
9
- * shared pipeline code. Adding a framework is two lines: implement a
10
- * `DiFieldMatcher` in `di-extractors/<framework>.ts` and register it here.
8
+ * shared pipeline code. Adding a framework means implementing a `DiResolver`
9
+ * in `di-extractors/<framework>.ts` and registering it here.
11
10
  *
12
- * Scope honesty: matchers are per-language *field-injection* matchers.
13
- * Constructor injection (the dominant modern Spring idiom) lives on
14
- * Method/parameter nodes and would require widening the phase's routing —
15
- * deliberately out of scope (see the plan's Deferred work). The registry is
16
- * single-valued per language, matching the `SCOPE_RESOLVERS` shape; widen the
17
- * value type to arrays only when a second same-language framework actually
18
- * lands (a one-line type change then).
11
+ * The registry is single-valued per language, matching the `SCOPE_RESOLVERS`
12
+ * shape; widen the value type to arrays only when a second same-language
13
+ * framework actually lands. Java and Kotlin share Spring's attached metadata
14
+ * contract while retaining language-specific syntax capture.
19
15
  */
20
16
  import { SupportedLanguages } from '../../../_shared/index.js';
21
- import { springDiFieldMatcher } from './spring.js';
17
+ import { springDiResolver } from './spring.js';
22
18
  /** All `SupportedLanguages` string values, for narrowing raw graph strings. */
23
19
  const SUPPORTED_LANGUAGE_VALUES = new Set(Object.values(SupportedLanguages));
24
20
  /**
25
21
  * Type guard narrowing an arbitrary graph `language` string to
26
- * `SupportedLanguages`, so `DI_MATCHERS.get()` needs no cast.
22
+ * `SupportedLanguages`, so `DI_RESOLVERS.get()` needs no cast.
27
23
  */
28
24
  export function isSupportedLanguage(value) {
29
25
  return SUPPORTED_LANGUAGE_VALUES.has(value);
30
26
  }
31
- /** Map of `SupportedLanguages` → `DiFieldMatcher`. The `di` phase routes each
32
- * `Property` node here by `node.properties.language`; no entry ⇒ the node is
27
+ /** Map of `SupportedLanguages` → `DiResolver`. The `di` phase routes each
28
+ * graph node here by `node.properties.language`; no entry ⇒ the node is
33
29
  * skipped. This is the single source of truth for which languages (and,
34
30
  * transitively, frameworks) produce INJECTS edges. */
35
- export const DI_MATCHERS = new Map([[SupportedLanguages.Java, springDiFieldMatcher]]);
31
+ export const DI_RESOLVERS = new Map([
32
+ [SupportedLanguages.Java, springDiResolver],
33
+ [SupportedLanguages.Kotlin, springDiResolver],
34
+ ]);
@@ -51,11 +51,22 @@
51
51
  * between `<` and the element) are NOT stripped and fail closed —
52
52
  * acceptable.
53
53
  *
54
- * Registered under `SupportedLanguages.Java` in `./index.ts` (`DI_MATCHERS`);
55
- * language routing is the registry's job, so the matcher itself never reads
56
- * `node.properties.language`.
54
+ * Registered for Java and Kotlin in `./index.ts` (`DI_RESOLVERS`); language
55
+ * routing is the registry's job, so the matcher itself never reads
56
+ * `node.properties.language`. Kotlin's AST-backed class metadata is the
57
+ * primary path because Kotlin Property extraction intentionally exposes less
58
+ * annotation/type syntax than Java's legacy field contract.
57
59
  */
58
- import type { DiFieldMatcher } from './index.js';
60
+ import type { GraphNode } from '../../../_shared/index.js';
61
+ import type { DiResolver } from './index.js';
62
+ /** Ephemeral Class-node property populated by Java's post-resolution Spring
63
+ * metadata hook. It is consumed in the same pipeline run before persistence. */
64
+ export declare const SPRING_DI_INJECTION_SITES_PROPERTY = "springDiInjectionSites";
65
+ /** Ephemeral Class-node property carrying Spring bean names / @Primary. */
66
+ export declare const SPRING_DI_PROVIDER_PROPERTY = "springDiProvider";
67
+ /** Marker placed on Property nodes whose richer AST-backed field fact was
68
+ * attached to the owning Class, suppressing the legacy collection fallback. */
69
+ export declare const SPRING_DI_CAPTURED_FIELD_PROPERTY = "springDiCapturedField";
59
70
  /**
60
71
  * Parse a Spring DI collection field's raw declared type (verbatim source
61
72
  * text, generics preserved) and return the injected bean type name.
@@ -73,10 +84,21 @@ export declare function parseSpringCollectionType(rawDeclaredType: string): {
73
84
  collectionType: string;
74
85
  elementTypeName: string;
75
86
  } | null;
87
+ /** Parse either a supported collect-all type or a standard single bean type. */
88
+ export declare function parseSpringInjectionType(rawDeclaredType: string): {
89
+ targetTypeName: string;
90
+ cardinality: 'single' | 'collection';
91
+ displayType: string;
92
+ } | null;
76
93
  /**
77
94
  * Match a `Property` node against Spring's collection-injection shape.
78
95
  *
79
96
  * Returns the parsed match (with a Spring-specific human-readable `reason`
80
97
  * payload) or `null` when the field is not container-injected.
81
98
  */
82
- export declare const springDiFieldMatcher: DiFieldMatcher;
99
+ export declare const springDiFieldMatcher: (node: GraphNode) => {
100
+ elementTypeName: string;
101
+ reason: string;
102
+ } | null;
103
+ /** JVM/Spring resolver registered behind the framework-neutral DI seam. */
104
+ export declare const springDiResolver: DiResolver;
@@ -51,9 +51,11 @@
51
51
  * between `<` and the element) are NOT stripped and fail closed —
52
52
  * acceptable.
53
53
  *
54
- * Registered under `SupportedLanguages.Java` in `./index.ts` (`DI_MATCHERS`);
55
- * language routing is the registry's job, so the matcher itself never reads
56
- * `node.properties.language`.
54
+ * Registered for Java and Kotlin in `./index.ts` (`DI_RESOLVERS`); language
55
+ * routing is the registry's job, so the matcher itself never reads
56
+ * `node.properties.language`. Kotlin's AST-backed class metadata is the
57
+ * primary path because Kotlin Property extraction intentionally exposes less
58
+ * annotation/type syntax than Java's legacy field contract.
57
59
  */
58
60
  import { isDev } from '../utils/env.js';
59
61
  import { logger } from '../../logger.js';
@@ -76,6 +78,14 @@ const WILDCARD_SUPER_PREFIX = '? super ';
76
78
  * parser accepts. Everything else (wildcards, arrays, comments, stray
77
79
  * punctuation) fails closed. */
78
80
  const JAVA_TYPE_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
81
+ /** Ephemeral Class-node property populated by Java's post-resolution Spring
82
+ * metadata hook. It is consumed in the same pipeline run before persistence. */
83
+ export const SPRING_DI_INJECTION_SITES_PROPERTY = 'springDiInjectionSites';
84
+ /** Ephemeral Class-node property carrying Spring bean names / @Primary. */
85
+ export const SPRING_DI_PROVIDER_PROPERTY = 'springDiProvider';
86
+ /** Marker placed on Property nodes whose richer AST-backed field fact was
87
+ * attached to the owning Class, suppressing the legacy collection fallback. */
88
+ export const SPRING_DI_CAPTURED_FIELD_PROPERTY = 'springDiCapturedField';
79
89
  /**
80
90
  * Split a generic-argument list on TOP-LEVEL commas only, tracking `<`/`>`
81
91
  * bracket depth so nested generics (e.g. the `Pair<A,B>` key in
@@ -179,6 +189,21 @@ export function parseSpringCollectionType(rawDeclaredType) {
179
189
  return null;
180
190
  return { collectionType: wrapper, elementTypeName };
181
191
  }
192
+ /** Parse either a supported collect-all type or a standard single bean type. */
193
+ export function parseSpringInjectionType(rawDeclaredType) {
194
+ const collection = parseSpringCollectionType(rawDeclaredType);
195
+ if (collection !== null) {
196
+ return {
197
+ targetTypeName: collection.elementTypeName,
198
+ cardinality: 'collection',
199
+ displayType: `${collection.collectionType}<${collection.elementTypeName}>`,
200
+ };
201
+ }
202
+ const normalized = rawDeclaredType.replace(/\s+/g, '').trim();
203
+ if (!JAVA_TYPE_NAME_PATTERN.test(normalized))
204
+ return null;
205
+ return { targetTypeName: normalized, cardinality: 'single', displayType: normalized };
206
+ }
182
207
  /**
183
208
  * Match a `Property` node against Spring's collection-injection shape.
184
209
  *
@@ -218,3 +243,55 @@ export const springDiFieldMatcher = (node) => {
218
243
  reason: `Spring DI: ${matchedAnnotation} ${parsed.collectionType}<${parsed.elementTypeName}>`,
219
244
  };
220
245
  };
246
+ function isInjectionMatch(value) {
247
+ if (value === null || typeof value !== 'object')
248
+ return false;
249
+ const match = value;
250
+ const namedSelection = match.namedSelection;
251
+ return (typeof match.targetTypeName === 'string' &&
252
+ (match.cardinality === 'single' || match.cardinality === 'collection') &&
253
+ typeof match.reason === 'string' &&
254
+ (namedSelection === undefined ||
255
+ (typeof namedSelection === 'object' &&
256
+ namedSelection !== null &&
257
+ typeof namedSelection.name === 'string' &&
258
+ typeof namedSelection.reason === 'string')));
259
+ }
260
+ function isProviderMatch(value) {
261
+ if (value === null || typeof value !== 'object')
262
+ return false;
263
+ const provider = value;
264
+ return (Array.isArray(provider.names) &&
265
+ provider.names.every((name) => typeof name === 'string') &&
266
+ (provider.preferenceReason === undefined || typeof provider.preferenceReason === 'string'));
267
+ }
268
+ /** JVM/Spring resolver registered behind the framework-neutral DI seam. */
269
+ export const springDiResolver = {
270
+ matchInjectionSites(node) {
271
+ const matches = [];
272
+ // Preserve the existing Property-node collection contract for hand-built
273
+ // graphs and for compatibility with pre-#2414 extraction fixtures.
274
+ if (node.label === 'Property' && node.properties[SPRING_DI_CAPTURED_FIELD_PROPERTY] !== true) {
275
+ const field = springDiFieldMatcher(node);
276
+ if (field !== null) {
277
+ matches.push({
278
+ targetTypeName: field.elementTypeName,
279
+ cardinality: 'collection',
280
+ reason: field.reason,
281
+ });
282
+ }
283
+ }
284
+ const attached = node.properties[SPRING_DI_INJECTION_SITES_PROPERTY];
285
+ if (Array.isArray(attached)) {
286
+ for (const candidate of attached) {
287
+ if (isInjectionMatch(candidate))
288
+ matches.push(candidate);
289
+ }
290
+ }
291
+ return matches;
292
+ },
293
+ matchProvider(node) {
294
+ const attached = node.properties[SPRING_DI_PROVIDER_PROPERTY];
295
+ return isProviderMatch(attached) ? attached : null;
296
+ },
297
+ };
@@ -0,0 +1,45 @@
1
+ import type { ParsedFile, ScopeId } from '../../../../_shared/index.js';
2
+ import type { KnowledgeGraph } from '../../../graph/types.js';
3
+ import { parseSpringInjectionType } from '../../di-extractors/spring.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 SpringDiAnnotationFact {
7
+ readonly name: string;
8
+ readonly text: string;
9
+ }
10
+ export interface SpringDiDependencyFact<Annotation extends SpringDiAnnotationFact> {
11
+ readonly name: string;
12
+ readonly rawType: string;
13
+ readonly annotations: readonly Annotation[];
14
+ }
15
+ export interface SpringDiInjectionSiteFact<Annotation extends SpringDiAnnotationFact, SiteKind extends string> {
16
+ readonly kind: SiteKind;
17
+ readonly memberName: string;
18
+ readonly implicitConstructor: boolean;
19
+ readonly annotations: readonly Annotation[];
20
+ readonly dependencies: readonly SpringDiDependencyFact<Annotation>[];
21
+ }
22
+ export interface SpringDiClassFact<Annotation extends SpringDiAnnotationFact, SiteKind extends string> {
23
+ readonly classScopeId: ScopeId;
24
+ readonly classAnnotations: readonly Annotation[];
25
+ readonly injectionSites: readonly SpringDiInjectionSiteFact<Annotation, SiteKind>[];
26
+ }
27
+ export declare function springAnnotationSimpleName(name: string): string;
28
+ export declare function hasSpringDiRelevantAnnotation(annotations: readonly SpringDiAnnotationFact[]): boolean;
29
+ export declare function hasSpringStereotypeSyntax(annotations: readonly SpringDiAnnotationFact[]): boolean;
30
+ type ParsedSpringInjectionType = NonNullable<ReturnType<typeof parseSpringInjectionType>>;
31
+ export interface SpringDiMetadataAdapter<Annotation extends SpringDiAnnotationFact, SiteKind extends string> {
32
+ getFacts(filePath: string): readonly SpringDiClassFact<Annotation, SiteKind>[];
33
+ isPackageVisibilityIncomplete(filePath: string): boolean;
34
+ parseInjectionType(rawType: string): ParsedSpringInjectionType | null;
35
+ capturedMemberKind: SiteKind;
36
+ isInjectionAnnotationApplicable?(annotation: Annotation, site: SpringDiInjectionSiteFact<Annotation, SiteKind>): boolean;
37
+ isQualifierAnnotationApplicable?(annotation: Annotation, site: SpringDiInjectionSiteFact<Annotation, SiteKind>): boolean;
38
+ }
39
+ /**
40
+ * Build the post-resolution Spring DI metadata hook shared by language adapters.
41
+ * Language adapters retain syntax capture, type normalization, use-site rules,
42
+ * and side-channel ownership; this function owns framework semantics only.
43
+ */
44
+ export declare function createSpringDiMetadataAttacher<Annotation extends SpringDiAnnotationFact, SiteKind extends string>(adapter: SpringDiMetadataAdapter<Annotation, SiteKind>): (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, indexes: ScopeResolutionIndexes) => void;
45
+ export {};
@@ -0,0 +1,230 @@
1
+ import { SPRING_DI_CAPTURED_FIELD_PROPERTY, SPRING_DI_INJECTION_SITES_PROPERTY, SPRING_DI_PROVIDER_PROPERTY, } from '../../di-extractors/spring.js';
2
+ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
3
+ import { createSpringAnnotationNameResolver } from './bean-candidates.js';
4
+ import { SPRING_BEAN_STEREOTYPES } from './bean-catalog.js';
5
+ const INJECTION_ANNOTATIONS = new Set([
6
+ 'org.springframework.beans.factory.annotation.Autowired',
7
+ 'jakarta.inject.Inject',
8
+ 'javax.inject.Inject',
9
+ ]);
10
+ const QUALIFIER_ANNOTATIONS = new Set([
11
+ 'org.springframework.beans.factory.annotation.Qualifier',
12
+ 'jakarta.inject.Named',
13
+ 'javax.inject.Named',
14
+ ]);
15
+ const PRIMARY_ANNOTATIONS = new Set(['org.springframework.context.annotation.Primary']);
16
+ const RESOLVABLE_DI_ANNOTATIONS = new Set([
17
+ ...SPRING_BEAN_STEREOTYPES.keys(),
18
+ ...INJECTION_ANNOTATIONS,
19
+ ...QUALIFIER_ANNOTATIONS,
20
+ ...PRIMARY_ANNOTATIONS,
21
+ ]);
22
+ const CAPTURE_RELEVANT_ANNOTATIONS = new Set([
23
+ 'Autowired',
24
+ 'Inject',
25
+ 'Qualifier',
26
+ 'Named',
27
+ 'Primary',
28
+ 'Component',
29
+ 'Service',
30
+ 'Repository',
31
+ 'Controller',
32
+ 'RestController',
33
+ 'Configuration',
34
+ ]);
35
+ const STEREOTYPE_SIMPLE_NAMES = new Set([...SPRING_BEAN_STEREOTYPES.keys()].map((name) => springAnnotationSimpleName(name)));
36
+ export function springAnnotationSimpleName(name) {
37
+ const separator = name.lastIndexOf('.');
38
+ return separator === -1 ? name : name.slice(separator + 1);
39
+ }
40
+ export function hasSpringDiRelevantAnnotation(annotations) {
41
+ return annotations.some((annotation) => CAPTURE_RELEVANT_ANNOTATIONS.has(springAnnotationSimpleName(annotation.name)));
42
+ }
43
+ export function hasSpringStereotypeSyntax(annotations) {
44
+ return annotations.some((annotation) => STEREOTYPE_SIMPLE_NAMES.has(springAnnotationSimpleName(annotation.name)));
45
+ }
46
+ function staticStringArgument(annotationText) {
47
+ const args = annotationText.match(/\((.*)\)$/s)?.[1]?.trim();
48
+ if (args === undefined)
49
+ return undefined;
50
+ const value = args.replace(/^value\s*=\s*/, '').trim();
51
+ const literal = value.match(/^"((?:\\.|[^"\\])*)"$/s);
52
+ if (literal === null)
53
+ return undefined;
54
+ try {
55
+ return JSON.parse(`"${literal[1]}"`);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ function defaultBeanName(className) {
62
+ if (className.length === 0)
63
+ return className;
64
+ if (className.length > 1 &&
65
+ className[0] !== className[0].toLowerCase() &&
66
+ className[1] !== className[1].toLowerCase()) {
67
+ return className;
68
+ }
69
+ return className[0].toLowerCase() + className.slice(1);
70
+ }
71
+ /**
72
+ * Build the post-resolution Spring DI metadata hook shared by language adapters.
73
+ * Language adapters retain syntax capture, type normalization, use-site rules,
74
+ * and side-channel ownership; this function owns framework semantics only.
75
+ */
76
+ export function createSpringDiMetadataAttacher(adapter) {
77
+ return (graph, parsedFiles, nodeLookup, indexes) => {
78
+ const resolveAnnotation = createSpringAnnotationNameResolver(indexes);
79
+ for (const parsed of parsedFiles) {
80
+ const incomplete = adapter.isPackageVisibilityIncomplete(parsed.filePath);
81
+ for (const fact of adapter.getFacts(parsed.filePath)) {
82
+ const classScope = indexes.scopeTree.getScope(fact.classScopeId);
83
+ if (classScope === undefined || classScope.kind !== 'Class')
84
+ continue;
85
+ const classDef = classScope.ownedDefs.find((definition) => definition.type === 'Class');
86
+ if (classDef === undefined)
87
+ continue;
88
+ const graphId = resolveDefGraphId(parsed.filePath, classDef, nodeLookup);
89
+ if (graphId === undefined)
90
+ continue;
91
+ const classNode = graph.getNode(graphId);
92
+ if (classNode === undefined || classNode.label !== 'Class')
93
+ continue;
94
+ const resolvedAnnotations = new Map();
95
+ const resolveFact = (annotation, enclosingScope = classScope.parent) => {
96
+ const cacheKey = `${enclosingScope ?? '<root>'}\0${annotation.name}`;
97
+ if (resolvedAnnotations.has(cacheKey))
98
+ return resolvedAnnotations.get(cacheKey);
99
+ const resolved = resolveAnnotation(annotation.name, parsed, enclosingScope, RESOLVABLE_DI_ANNOTATIONS, incomplete);
100
+ resolvedAnnotations.set(cacheKey, resolved);
101
+ return resolved;
102
+ };
103
+ const frameworkAnnotations = Array.isArray(classNode.properties.frameworkAnnotations)
104
+ ? classNode.properties.frameworkAnnotations.filter((annotation) => typeof annotation === 'string')
105
+ : [];
106
+ if (frameworkAnnotations.length > 0) {
107
+ const names = new Set();
108
+ let explicitBeanName;
109
+ let hasDynamicBeanName = false;
110
+ let primary = false;
111
+ for (const annotation of fact.classAnnotations) {
112
+ const resolved = resolveFact(annotation);
113
+ if (resolved === undefined)
114
+ continue;
115
+ if (SPRING_BEAN_STEREOTYPES.has(resolved)) {
116
+ const argumentText = annotation.text.match(/\((.*)\)$/s)?.[1]?.trim();
117
+ if (argumentText !== undefined && argumentText.length > 0) {
118
+ const staticName = staticStringArgument(annotation.text);
119
+ if (staticName === undefined)
120
+ hasDynamicBeanName = true;
121
+ else if (staticName.length > 0)
122
+ explicitBeanName = staticName;
123
+ }
124
+ }
125
+ if (QUALIFIER_ANNOTATIONS.has(resolved)) {
126
+ const qualifier = staticStringArgument(annotation.text);
127
+ if (qualifier !== undefined)
128
+ names.add(qualifier);
129
+ }
130
+ if (PRIMARY_ANNOTATIONS.has(resolved))
131
+ primary = true;
132
+ }
133
+ if (explicitBeanName !== undefined)
134
+ names.add(explicitBeanName);
135
+ else if (!hasDynamicBeanName)
136
+ names.add(defaultBeanName(classNode.properties.name));
137
+ const provider = {
138
+ names: [...names],
139
+ ...(primary ? { preferenceReason: 'selected @Primary' } : {}),
140
+ };
141
+ classNode.properties[SPRING_DI_PROVIDER_PROPERTY] = provider;
142
+ }
143
+ const matches = [];
144
+ const semanticallyOwnedMemberNames = new Set();
145
+ for (const site of fact.injectionSites) {
146
+ let injectionAnnotation;
147
+ for (const annotation of site.annotations) {
148
+ if (adapter.isInjectionAnnotationApplicable?.(annotation, site) === false)
149
+ continue;
150
+ const resolved = resolveFact(annotation, classScope.id);
151
+ if (resolved !== undefined && INJECTION_ANNOTATIONS.has(resolved)) {
152
+ injectionAnnotation = annotation;
153
+ break;
154
+ }
155
+ }
156
+ if (injectionAnnotation === undefined) {
157
+ if (!site.implicitConstructor || frameworkAnnotations.length === 0)
158
+ continue;
159
+ }
160
+ else if (site.kind === adapter.capturedMemberKind) {
161
+ // Claim the member only after its injection annotation resolves to
162
+ // a recognized FQN. Ambiguous wildcard imports stay unclaimed so
163
+ // the legacy collection matcher can fall back. A dynamic qualifier
164
+ // later fails closed, but this path still owns the member and must
165
+ // suppress that legacy fallback.
166
+ semanticallyOwnedMemberNames.add(site.memberName);
167
+ }
168
+ for (const dependency of site.dependencies) {
169
+ const parsedType = adapter.parseInjectionType(dependency.rawType);
170
+ if (parsedType === null)
171
+ continue;
172
+ let qualifierAnnotation;
173
+ for (const annotation of dependency.annotations) {
174
+ if (adapter.isQualifierAnnotationApplicable?.(annotation, site) === false)
175
+ continue;
176
+ const resolved = resolveFact(annotation, classScope.id);
177
+ if (resolved !== undefined && QUALIFIER_ANNOTATIONS.has(resolved)) {
178
+ qualifierAnnotation = annotation;
179
+ break;
180
+ }
181
+ }
182
+ const qualifier = qualifierAnnotation === undefined
183
+ ? undefined
184
+ : staticStringArgument(qualifierAnnotation.text);
185
+ // A present-but-dynamic qualifier is not the same as no qualifier.
186
+ // Without its value we cannot choose a provider honestly, so fail
187
+ // closed instead of emitting the unqualified candidate set.
188
+ if (qualifierAnnotation !== undefined && qualifier === undefined)
189
+ continue;
190
+ const trigger = injectionAnnotation === undefined
191
+ ? 'constructor'
192
+ : `@${springAnnotationSimpleName(injectionAnnotation.name)} ${site.kind}`;
193
+ const location = site.kind === adapter.capturedMemberKind
194
+ ? site.memberName
195
+ : `${site.memberName} parameter ${dependency.name}`;
196
+ matches.push({
197
+ targetTypeName: parsedType.targetTypeName,
198
+ cardinality: parsedType.cardinality,
199
+ ...(qualifier === undefined
200
+ ? {}
201
+ : {
202
+ namedSelection: {
203
+ name: qualifier,
204
+ reason: `qualifier "${qualifier}"`,
205
+ },
206
+ }),
207
+ reason: `Spring DI: ${trigger} ${location}: ${parsedType.displayType}`,
208
+ });
209
+ }
210
+ }
211
+ if (matches.length > 0) {
212
+ classNode.properties[SPRING_DI_INJECTION_SITES_PROPERTY] = matches;
213
+ }
214
+ for (const memberName of semanticallyOwnedMemberNames) {
215
+ for (const { def } of classScope.bindings.get(memberName) ?? []) {
216
+ if (def.ownerId !== classDef.nodeId)
217
+ continue;
218
+ const propertyId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
219
+ if (propertyId === undefined)
220
+ continue;
221
+ const property = graph.getNode(propertyId);
222
+ if (property?.label === 'Property') {
223
+ property.properties[SPRING_DI_CAPTURED_FIELD_PROPERTY] = true;
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ };
230
+ }
@@ -2,12 +2,14 @@ import type { ParsedFile } from '../../../../_shared/index.js';
2
2
  import { type ClassAnnotationFact } from '../../frameworks/spring/bean-candidates.js';
3
3
  import { type JvmPackageFact } from '../jvm/package-facts.js';
4
4
  import type { JavaSpringConfigConsumerFact } from './spring-config-bindings.js';
5
+ import type { JavaSpringDiClassFact } from './spring-di.js';
5
6
  export type JavaClassAnnotationFact = ClassAnnotationFact;
6
7
  export interface JavaCaptureSideChannel {
7
8
  readonly kind: 'java';
8
9
  readonly packageFact: JvmPackageFact;
9
10
  readonly classAnnotations: readonly JavaClassAnnotationFact[];
10
11
  readonly springConfigConsumers?: readonly JavaSpringConfigConsumerFact[];
12
+ readonly springDiFacts?: readonly JavaSpringDiClassFact[];
11
13
  }
12
14
  /** Clear facts retained by a prior workspace pass in a long-lived process. */
13
15
  export declare function clearJavaClassAnnotationFacts(): void;
@@ -15,6 +17,8 @@ export declare function clearJavaClassAnnotationFacts(): void;
15
17
  export declare function setJavaClassAnnotationFacts(filePath: string, facts: readonly JavaClassAnnotationFact[]): void;
16
18
  export declare function setJavaSpringConfigConsumerFacts(filePath: string, facts: readonly JavaSpringConfigConsumerFact[]): void;
17
19
  export declare function getJavaSpringConfigConsumerFacts(filePath: string): readonly JavaSpringConfigConsumerFact[];
20
+ export declare function setJavaSpringDiFacts(filePath: string, facts: readonly JavaSpringDiClassFact[]): void;
21
+ export declare function getJavaSpringDiFacts(filePath: string): readonly JavaSpringDiClassFact[];
18
22
  /** Snapshot worker-local Java annotation facts for ParsedFile serialization. */
19
23
  export declare function collectJavaCaptureSideChannel(filePath: string): JavaCaptureSideChannel | undefined;
20
24
  export declare function getJavaClassAnnotationFacts(filePath: string): readonly JavaClassAnnotationFact[];