gitnexus 1.6.11-rc.29 → 1.6.11-rc.30

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 (27) hide show
  1. package/dist/core/ingestion/frameworks/spring/argument-facts.d.ts +80 -0
  2. package/dist/core/ingestion/frameworks/spring/argument-facts.js +85 -0
  3. package/dist/core/ingestion/frameworks/spring/message-producers.d.ts +78 -0
  4. package/dist/core/ingestion/frameworks/spring/message-producers.js +90 -0
  5. package/dist/core/ingestion/frameworks/spring/non-http-handlers.d.ts +29 -0
  6. package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
  7. package/dist/core/ingestion/languages/java/capture-side-channel.js +18 -0
  8. package/dist/core/ingestion/languages/java/captures.js +16 -8
  9. package/dist/core/ingestion/languages/java/spring-di.d.ts +17 -1
  10. package/dist/core/ingestion/languages/java/spring-di.js +44 -2
  11. package/dist/core/ingestion/languages/java/spring-message-producers.d.ts +20 -0
  12. package/dist/core/ingestion/languages/java/spring-message-producers.js +83 -0
  13. package/dist/core/ingestion/languages/java/spring-non-http-handlers.d.ts +11 -1
  14. package/dist/core/ingestion/languages/java/spring-non-http-handlers.js +13 -3
  15. package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
  16. package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +18 -0
  17. package/dist/core/ingestion/languages/kotlin/captures.js +16 -8
  18. package/dist/core/ingestion/languages/kotlin/spring-di.d.ts +51 -1
  19. package/dist/core/ingestion/languages/kotlin/spring-di.js +86 -6
  20. package/dist/core/ingestion/languages/kotlin/spring-message-producers.d.ts +17 -0
  21. package/dist/core/ingestion/languages/kotlin/spring-message-producers.js +119 -0
  22. package/dist/core/ingestion/languages/kotlin/spring-non-http-handlers.d.ts +16 -0
  23. package/dist/core/ingestion/languages/kotlin/spring-non-http-handlers.js +77 -2
  24. package/dist/core/ingestion/utils/ast-helpers.d.ts +16 -0
  25. package/dist/core/ingestion/utils/ast-helpers.js +18 -0
  26. package/dist/storage/parse-cache.js +14 -1
  27. package/package.json +1 -1
@@ -0,0 +1,80 @@
1
+ /**
2
+ * One argument of a Spring annotation or of a messaging-template call, captured
3
+ * exactly as it is written in source.
4
+ *
5
+ * Capture-time facts are deliberately UNRESOLVED. When these facts are produced
6
+ * the file's imports are not finalized, constants declared in sibling files do
7
+ * not exist yet, and no configuration source has been read — so a captured
8
+ * `text` may be a string literal, a constant reference (`Destinations.ORDERS`),
9
+ * a property placeholder (`"${app.orders.topic}"`), or an arbitrary expression.
10
+ * Turning any of those into an address is a separate, later phase; nothing here
11
+ * may call a resolver.
12
+ *
13
+ * NOT the same thing as `SpringAnnotationArgument` in `annotation-arguments.ts`,
14
+ * and the two are deliberately not merged:
15
+ *
16
+ * - Source. This fact is built from AST nodes while the tree is in hand;
17
+ * `parseSpringAnnotationArguments` re-parses an annotation's `text` much
18
+ * later, from a string, with a hand-written delimiter scanner.
19
+ * - Failure. The text parser returns `null` when its scanner cannot balance
20
+ * the input, and a caller must decide what that means. There is no such
21
+ * state here: the grammar has already decided where each argument begins
22
+ * and ends.
23
+ * - Absence. The text parser answers `[]` both for `@Scheduled` and for
24
+ * `@Scheduled()`, because a string cannot tell "no list" from "empty list"
25
+ * without re-deriving it. Capture keeps the two apart — absent versus `[]` —
26
+ * so downstream code can rely on the distinction wherever arguments were
27
+ * read at all. A capture that reads them for only some of its facts says so
28
+ * on its own `args` field.
29
+ * - Scope. This fact also describes CALL arguments (`template.send(topic, p)`),
30
+ * which the annotation parser has no notion of.
31
+ *
32
+ * Collapsing them would mean giving the text parser a failure mode it cannot
33
+ * produce, or taking the three-state distinction away from capture.
34
+ */
35
+ export interface SpringArgumentFact {
36
+ /**
37
+ * Argument name for a named argument, absent for a positional one.
38
+ *
39
+ * Both forms occur, and where the destination sits differs by construct. An
40
+ * annotation names it (`@KafkaListener(topics = ...)` versus
41
+ * `@RabbitListener(queues = ...)`). A call normally gives it by position
42
+ * (`kafkaTemplate.send(topic, payload)`) — always so in Java, which has no
43
+ * named arguments — but a Kotlin call may name its arguments whenever the
44
+ * callee is itself declared in Kotlin, and then the key is captured too.
45
+ */
46
+ readonly name?: string;
47
+ /**
48
+ * Argument value in its source spelling — quotes, braces and casts intact,
49
+ * nothing resolved — after `normalizeSpringFactText`. That pass trims the
50
+ * text and collapses whitespace around the dots of a multi-line expression,
51
+ * so one destination written two ways yields one fact. It is the only
52
+ * rewrite; see the function for why formatting must not reach the data.
53
+ */
54
+ readonly text: string;
55
+ }
56
+ /**
57
+ * Join an expression that the source wrapped across lines, so that one
58
+ * expression has one spelling no matter where it was written.
59
+ *
60
+ * A receiver chain written as `outer\n .inner\n .kafkaTemplate`, and an
61
+ * argument written as `Destinations\n .ORDERS`, are the same expressions as
62
+ * their single-line spellings. Raw node text would carry the newline and the
63
+ * ENCLOSING BLOCK's indentation across the worker boundary, so the same
64
+ * expression at two nesting depths — or in a CRLF checkout — would not compare
65
+ * equal downstream. Receivers and arguments get the identical treatment on
66
+ * purpose: an inconsistent rule inside one fact is a trap for the phase that
67
+ * has to match a publish against a subscription.
68
+ *
69
+ * Only a run of whitespace that CONTAINS A NEWLINE and sits next to a dot is
70
+ * removed, and only OUTSIDE a string literal. Single-line spacing is left
71
+ * alone, so `registry.get("a . b").template` keeps its argument exactly as
72
+ * written; literal-awareness extends that to Java text blocks and Kotlin raw
73
+ * strings, whose embedded newlines are part of the value and must survive
74
+ * (`"""line-a\n.line-b"""` is not the same string as `"""line-a.line-b"""`).
75
+ *
76
+ * Wraps that are not adjacent to a dot (`"a" +\n "b"`) are left as written:
77
+ * normalizing them would have to reason about operators, and the same
78
+ * conservatism already applies to receivers.
79
+ */
80
+ export declare function normalizeSpringFactText(text: string): string;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Join an expression that the source wrapped across lines, so that one
3
+ * expression has one spelling no matter where it was written.
4
+ *
5
+ * A receiver chain written as `outer\n .inner\n .kafkaTemplate`, and an
6
+ * argument written as `Destinations\n .ORDERS`, are the same expressions as
7
+ * their single-line spellings. Raw node text would carry the newline and the
8
+ * ENCLOSING BLOCK's indentation across the worker boundary, so the same
9
+ * expression at two nesting depths — or in a CRLF checkout — would not compare
10
+ * equal downstream. Receivers and arguments get the identical treatment on
11
+ * purpose: an inconsistent rule inside one fact is a trap for the phase that
12
+ * has to match a publish against a subscription.
13
+ *
14
+ * Only a run of whitespace that CONTAINS A NEWLINE and sits next to a dot is
15
+ * removed, and only OUTSIDE a string literal. Single-line spacing is left
16
+ * alone, so `registry.get("a . b").template` keeps its argument exactly as
17
+ * written; literal-awareness extends that to Java text blocks and Kotlin raw
18
+ * strings, whose embedded newlines are part of the value and must survive
19
+ * (`"""line-a\n.line-b"""` is not the same string as `"""line-a.line-b"""`).
20
+ *
21
+ * Wraps that are not adjacent to a dot (`"a" +\n "b"`) are left as written:
22
+ * normalizing them would have to reason about operators, and the same
23
+ * conservatism already applies to receivers.
24
+ */
25
+ export function normalizeSpringFactText(text) {
26
+ const trimmed = text.trim();
27
+ // Fast path: the overwhelming majority of captured text is single-line.
28
+ if (!trimmed.includes('\n') && !trimmed.includes('\r'))
29
+ return trimmed;
30
+ let out = '';
31
+ let index = 0;
32
+ let quote = null;
33
+ while (index < trimmed.length) {
34
+ const char = trimmed[index];
35
+ if (quote === '"""') {
36
+ if (trimmed.startsWith('"""', index)) {
37
+ out += '"""';
38
+ index += 3;
39
+ quote = null;
40
+ continue;
41
+ }
42
+ out += char;
43
+ index += 1;
44
+ continue;
45
+ }
46
+ if (quote !== null) {
47
+ // A backslash escape is copied whole so that `"\\"` ends the literal and
48
+ // `"\""` does not.
49
+ if (char === '\\' && index + 1 < trimmed.length) {
50
+ out += trimmed.slice(index, index + 2);
51
+ index += 2;
52
+ continue;
53
+ }
54
+ if (char === quote)
55
+ quote = null;
56
+ out += char;
57
+ index += 1;
58
+ continue;
59
+ }
60
+ if (trimmed.startsWith('"""', index)) {
61
+ quote = '"""';
62
+ out += '"""';
63
+ index += 3;
64
+ continue;
65
+ }
66
+ if (char === '"' || char === "'") {
67
+ quote = char;
68
+ out += char;
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (char === '.' || /\s/.test(char)) {
73
+ const separator = /^\s*\.\s*/.exec(trimmed.slice(index));
74
+ if (separator !== null) {
75
+ const matched = separator[0];
76
+ out += matched.includes('\n') ? '.' : matched;
77
+ index += matched.length;
78
+ continue;
79
+ }
80
+ }
81
+ out += char;
82
+ index += 1;
83
+ }
84
+ return out;
85
+ }
@@ -0,0 +1,78 @@
1
+ import type { Range, ScopeId } from '../../../../_shared/index.js';
2
+ import type { SpringArgumentFact } from './argument-facts.js';
3
+ /**
4
+ * Outbound side of Spring messaging: the template calls that publish to a
5
+ * broker destination, mirroring the inbound `@KafkaListener` / `@RabbitListener`
6
+ * family already recognized in `non-http-handlers.ts`.
7
+ *
8
+ * Recognition is purely syntactic and happens while the language's own scope
9
+ * query already has the call node in hand. The receiver's declared type is NOT
10
+ * consulted: at capture time the field may be inherited, injected from another
11
+ * file, or typed through an import that is not finalized yet. Matching on the
12
+ * receiver's simple name instead keeps the capture cheap and resolver-free; a
13
+ * later phase that owns type information can refine or discard a fact.
14
+ */
15
+ export type SpringMessageProducerTemplate = 'kafka' | 'rabbit' | 'jms' | 'stream-bridge';
16
+ /**
17
+ * Cheap pre-filter usable before any receiver text is materialized. Both
18
+ * languages visit every member call, so the common case must cost one set
19
+ * lookup on the method name.
20
+ */
21
+ export declare function isSpringMessageProducerMethod(methodName: string): boolean;
22
+ /**
23
+ * Classify a `receiver.method(...)` call as a messaging producer, or `null`.
24
+ *
25
+ * `receiverName` is the receiver expression as written; only its last
26
+ * dot-separated segment participates, so `this.kafkaTemplate` and
27
+ * `outer.inner.kafkaTemplate` match while `templates.get("k")` does not.
28
+ *
29
+ * The PLAIN_IDENTIFIER gate runs BEFORE the fold and is load-bearing, because
30
+ * the last-dot split is textual: in `config.get("a.kafkaTemplate")` it yields
31
+ * `kafkaTemplate")`, which folds to something a name match would accept. Only
32
+ * an identifier survives the gate, which is also what rejects `templates["k"]`,
33
+ * `getTemplate()`, and a receiver whose dot is separated by a comment.
34
+ *
35
+ * The folded segment then matches case-insensitively when it CONTAINS the
36
+ * template type name, so every convention a template bean is really declared
37
+ * with is recognized — decorated by prefix (`orderKafkaTemplate`), by suffix
38
+ * (`kafkaTemplateDlq`, `kafkaTemplateV2`, `rabbitTemplate1`), or written as a
39
+ * constant (`KAFKA_TEMPLATE`, `STREAM_BRIDGE`). A suffix-only rule accepted
40
+ * one of those and silently dropped the rest, which are exactly the publishes
41
+ * this capture exists to find. A receiver named only `template` still does not
42
+ * match: without type information that would attribute any `send` in the
43
+ * repository to Kafka.
44
+ *
45
+ * The bare type name (`KafkaTemplate.send(...)`) contains itself and so is
46
+ * accepted. That is left as it is: the match is by NAME, a name equal to the
47
+ * type is the strongest evidence the rule has, and a later phase that owns type
48
+ * information can discard a static-looking receiver.
49
+ *
50
+ * A substring rule also lets ONE receiver satisfy TWO signatures, which a
51
+ * suffix rule could not: `KafkaTemplate` and `StreamBridge` both publish
52
+ * through `send`, and `RabbitTemplate` and `JmsTemplate` both through
53
+ * `convertAndSend`, so `streamBridgeKafkaTemplate.send(...)` matches two
54
+ * templates at once. Such a receiver yields NO fact. Nothing here can break the
55
+ * tie honestly: the receiver's TYPE is deliberately not resolved, and the name
56
+ * is not ranked evidence — neither the longest match, nor the last one, nor the
57
+ * order of this list says whether that bean is a KafkaTemplate fronted by a
58
+ * stream binding or a StreamBridge named after the broker behind it. Returning
59
+ * the first match published an arbitrary choice as a definite broker
60
+ * attribution, the one outcome a consumer cannot tell from a fact. Silence
61
+ * costs a rare publish and stays recoverable by a phase that owns types.
62
+ */
63
+ export declare function springMessageProducerTemplateOf(receiverName: string, methodName: string): SpringMessageProducerTemplate | null;
64
+ export interface SpringMessageProducerFact {
65
+ /** Callable that performs the publish; the enclosing method or function. */
66
+ readonly ownerScopeId: ScopeId;
67
+ readonly ownerRange: Range;
68
+ readonly template: SpringMessageProducerTemplate;
69
+ /** Receiver expression as written, for example `this.orderKafkaTemplate`. */
70
+ readonly receiverName: string;
71
+ readonly methodName: string;
72
+ /**
73
+ * Call arguments in source order, or absent when the call site has no
74
+ * argument list at all (a Kotlin trailing-lambda call). An empty array means
75
+ * an empty argument list was written — a different fact from no list.
76
+ */
77
+ readonly args?: readonly SpringArgumentFact[];
78
+ }
@@ -0,0 +1,90 @@
1
+ const PRODUCER_SIGNATURES = [
2
+ { template: 'kafka', typeName: 'KafkaTemplate', methodName: 'send' },
3
+ { template: 'rabbit', typeName: 'RabbitTemplate', methodName: 'convertAndSend' },
4
+ { template: 'jms', typeName: 'JmsTemplate', methodName: 'convertAndSend' },
5
+ { template: 'stream-bridge', typeName: 'StreamBridge', methodName: 'send' },
6
+ ];
7
+ const PRODUCER_METHOD_NAMES = new Set(PRODUCER_SIGNATURES.map((signature) => signature.methodName));
8
+ /** A receiver we can attribute; `templates["k"]` or `getTemplate()` cannot be. */
9
+ const PLAIN_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
10
+ /**
11
+ * Fold a receiver's simple name to the form the type-name match runs against.
12
+ *
13
+ * `_` and `$` are word separators in the spellings this has to accept, not part
14
+ * of the words: `KAFKA_TEMPLATE` and `kafka_template` are the same bean name as
15
+ * `kafkaTemplate`, written to the constant and snake conventions. Digits stay,
16
+ * because they are part of a name (`kafkaTemplate2`), never a separator.
17
+ */
18
+ function foldReceiverName(receiverSimpleName) {
19
+ return receiverSimpleName.replace(/[_$]/g, '').toLowerCase();
20
+ }
21
+ /**
22
+ * Cheap pre-filter usable before any receiver text is materialized. Both
23
+ * languages visit every member call, so the common case must cost one set
24
+ * lookup on the method name.
25
+ */
26
+ export function isSpringMessageProducerMethod(methodName) {
27
+ return PRODUCER_METHOD_NAMES.has(methodName);
28
+ }
29
+ /**
30
+ * Classify a `receiver.method(...)` call as a messaging producer, or `null`.
31
+ *
32
+ * `receiverName` is the receiver expression as written; only its last
33
+ * dot-separated segment participates, so `this.kafkaTemplate` and
34
+ * `outer.inner.kafkaTemplate` match while `templates.get("k")` does not.
35
+ *
36
+ * The PLAIN_IDENTIFIER gate runs BEFORE the fold and is load-bearing, because
37
+ * the last-dot split is textual: in `config.get("a.kafkaTemplate")` it yields
38
+ * `kafkaTemplate")`, which folds to something a name match would accept. Only
39
+ * an identifier survives the gate, which is also what rejects `templates["k"]`,
40
+ * `getTemplate()`, and a receiver whose dot is separated by a comment.
41
+ *
42
+ * The folded segment then matches case-insensitively when it CONTAINS the
43
+ * template type name, so every convention a template bean is really declared
44
+ * with is recognized — decorated by prefix (`orderKafkaTemplate`), by suffix
45
+ * (`kafkaTemplateDlq`, `kafkaTemplateV2`, `rabbitTemplate1`), or written as a
46
+ * constant (`KAFKA_TEMPLATE`, `STREAM_BRIDGE`). A suffix-only rule accepted
47
+ * one of those and silently dropped the rest, which are exactly the publishes
48
+ * this capture exists to find. A receiver named only `template` still does not
49
+ * match: without type information that would attribute any `send` in the
50
+ * repository to Kafka.
51
+ *
52
+ * The bare type name (`KafkaTemplate.send(...)`) contains itself and so is
53
+ * accepted. That is left as it is: the match is by NAME, a name equal to the
54
+ * type is the strongest evidence the rule has, and a later phase that owns type
55
+ * information can discard a static-looking receiver.
56
+ *
57
+ * A substring rule also lets ONE receiver satisfy TWO signatures, which a
58
+ * suffix rule could not: `KafkaTemplate` and `StreamBridge` both publish
59
+ * through `send`, and `RabbitTemplate` and `JmsTemplate` both through
60
+ * `convertAndSend`, so `streamBridgeKafkaTemplate.send(...)` matches two
61
+ * templates at once. Such a receiver yields NO fact. Nothing here can break the
62
+ * tie honestly: the receiver's TYPE is deliberately not resolved, and the name
63
+ * is not ranked evidence — neither the longest match, nor the last one, nor the
64
+ * order of this list says whether that bean is a KafkaTemplate fronted by a
65
+ * stream binding or a StreamBridge named after the broker behind it. Returning
66
+ * the first match published an arbitrary choice as a definite broker
67
+ * attribution, the one outcome a consumer cannot tell from a fact. Silence
68
+ * costs a rare publish and stays recoverable by a phase that owns types.
69
+ */
70
+ export function springMessageProducerTemplateOf(receiverName, methodName) {
71
+ if (!isSpringMessageProducerMethod(methodName))
72
+ return null;
73
+ const receiverSimpleName = receiverName.slice(receiverName.lastIndexOf('.') + 1).trim();
74
+ if (!PLAIN_IDENTIFIER.test(receiverSimpleName))
75
+ return null;
76
+ const folded = foldReceiverName(receiverSimpleName);
77
+ let matched = null;
78
+ for (const signature of PRODUCER_SIGNATURES) {
79
+ if (signature.methodName !== methodName)
80
+ continue;
81
+ if (!folded.includes(signature.typeName.toLowerCase()))
82
+ continue;
83
+ // A second match makes the receiver ambiguous; see above for why it is not
84
+ // resolved by preferring one of them.
85
+ if (matched !== null)
86
+ return null;
87
+ matched = signature.template;
88
+ }
89
+ return matched;
90
+ }
@@ -2,12 +2,41 @@ import type { ParsedFile, Range, ScopeId } from '../../../../_shared/index.js';
2
2
  import type { KnowledgeGraph } from '../../../graph/types.js';
3
3
  import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
4
4
  import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
5
+ import type { SpringArgumentFact } from './argument-facts.js';
5
6
  export declare const SPRING_NON_HTTP_HANDLER_ENTRY_POINT_MULTIPLIER = 3;
6
7
  export type SpringNonHttpHandlerKind = 'scheduled' | 'event' | 'message' | 'xxl-job';
7
8
  export interface SpringNonHttpHandlerAnnotationFact {
8
9
  readonly name: string;
9
10
  /** Kotlin use-site targets describe generated/property elements, not the callable. */
10
11
  readonly useSiteTarget?: string;
12
+ /**
13
+ * Annotation arguments in source order. An empty array always means an empty
14
+ * list was written (`@Scheduled()`), which is a different fact from absence —
15
+ * but absence has TWO causes, and only one of them is a statement about the
16
+ * source. Either the annotation was written without an argument list
17
+ * (`@Scheduled`), or arguments were never read for this callable.
18
+ *
19
+ * They are read only for a callable that carries a handler annotation. Java
20
+ * produces facts for no other callable, so there absence does mean "no list
21
+ * was written". Kotlin also produces a fact for a merely annotated function —
22
+ * it captures those without a name prefilter so an import alias cannot hide a
23
+ * handler — and on those facts arguments are absent however the annotation
24
+ * was written.
25
+ *
26
+ * The values keep their source spelling, with one deliberate exception:
27
+ * `normalizeSpringFactText` trims them and collapses whitespace around the
28
+ * dots of a multi-line expression, so `Destinations.ORDERS` and the same
29
+ * reference wrapped across lines produce equal facts. Without that, source
30
+ * formatting — including the enclosing block's indentation, which is not a
31
+ * property of the expression at all — would leak into the data and make two
32
+ * spellings of one destination compare unequal downstream.
33
+ *
34
+ * Nothing else is touched. `@KafkaListener(topics = ...)` and
35
+ * `@RabbitListener(queues = ...)` name the destination differently, and a
36
+ * destination may be a literal, a constant reference, or a `${...}`
37
+ * placeholder; resolving any of those belongs to a later phase.
38
+ */
39
+ readonly args?: readonly SpringArgumentFact[];
11
40
  }
12
41
  export interface SpringNonHttpHandlerFact<Annotation extends SpringNonHttpHandlerAnnotationFact = SpringNonHttpHandlerAnnotationFact> {
13
42
  readonly ownerScopeId: ScopeId;
@@ -6,6 +6,7 @@ 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
8
  import type { SpringDynamicLookupFact } from '../../frameworks/spring/dynamic-lookups.js';
9
+ import type { SpringMessageProducerFact } from '../../frameworks/spring/message-producers.js';
9
10
  import type { JavaSpringNonHttpHandlerFact } from './spring-non-http-handlers.js';
10
11
  export type JavaClassAnnotationFact = ClassAnnotationFact;
11
12
  export interface JavaCaptureSideChannel {
@@ -18,6 +19,7 @@ export interface JavaCaptureSideChannel {
18
19
  readonly springDiFacts?: readonly JavaSpringDiClassFact[];
19
20
  readonly springDynamicLookupFacts?: readonly SpringDynamicLookupFact[];
20
21
  readonly springNonHttpHandlerFacts?: readonly JavaSpringNonHttpHandlerFact[];
22
+ readonly springMessageProducerFacts?: readonly SpringMessageProducerFact[];
21
23
  }
22
24
  /** Clear facts retained by a prior workspace pass in a long-lived process. */
23
25
  export declare function clearJavaClassAnnotationFacts(): void;
@@ -35,6 +37,8 @@ export declare function setJavaSpringDynamicLookupFacts(filePath: string, facts:
35
37
  export declare function getJavaSpringDynamicLookupFacts(filePath: string): readonly SpringDynamicLookupFact[];
36
38
  export declare function setJavaSpringNonHttpHandlerFacts(filePath: string, facts: readonly JavaSpringNonHttpHandlerFact[]): void;
37
39
  export declare function getJavaSpringNonHttpHandlerFacts(filePath: string): readonly JavaSpringNonHttpHandlerFact[];
40
+ export declare function setJavaSpringMessageProducerFacts(filePath: string, facts: readonly SpringMessageProducerFact[]): void;
41
+ export declare function getJavaSpringMessageProducerFacts(filePath: string): readonly SpringMessageProducerFact[];
38
42
  /** Snapshot worker-local Java annotation facts for ParsedFile serialization. */
39
43
  export declare function collectJavaCaptureSideChannel(filePath: string): JavaCaptureSideChannel | undefined;
40
44
  export declare function getJavaClassAnnotationFacts(filePath: string): readonly JavaClassAnnotationFact[];
@@ -8,6 +8,7 @@ const springConditionalFacts = new Map();
8
8
  const springDiFacts = new Map();
9
9
  const springDynamicLookupFacts = new Map();
10
10
  const springNonHttpHandlerFacts = new Map();
11
+ const springMessageProducerFacts = new Map();
11
12
  /** Clear facts retained by a prior workspace pass in a long-lived process. */
12
13
  export function clearJavaClassAnnotationFacts() {
13
14
  classAnnotations.clear();
@@ -17,6 +18,7 @@ export function clearJavaClassAnnotationFacts() {
17
18
  springDiFacts.clear();
18
19
  springDynamicLookupFacts.clear();
19
20
  springNonHttpHandlerFacts.clear();
21
+ springMessageProducerFacts.clear();
20
22
  }
21
23
  export function setJavaSpringAopFacts(filePath, facts) {
22
24
  if (facts.length === 0)
@@ -76,6 +78,15 @@ export function setJavaSpringNonHttpHandlerFacts(filePath, facts) {
76
78
  export function getJavaSpringNonHttpHandlerFacts(filePath) {
77
79
  return springNonHttpHandlerFacts.get(filePath) ?? [];
78
80
  }
81
+ export function setJavaSpringMessageProducerFacts(filePath, facts) {
82
+ if (facts.length === 0)
83
+ springMessageProducerFacts.delete(filePath);
84
+ else
85
+ springMessageProducerFacts.set(filePath, facts);
86
+ }
87
+ export function getJavaSpringMessageProducerFacts(filePath) {
88
+ return springMessageProducerFacts.get(filePath) ?? [];
89
+ }
79
90
  /** Snapshot worker-local Java annotation facts for ParsedFile serialization. */
80
91
  export function collectJavaCaptureSideChannel(filePath) {
81
92
  const facts = classAnnotations.get(filePath);
@@ -85,6 +96,7 @@ export function collectJavaCaptureSideChannel(filePath) {
85
96
  const diFacts = springDiFacts.get(filePath) ?? [];
86
97
  const dynamicLookupFacts = springDynamicLookupFacts.get(filePath) ?? [];
87
98
  const nonHttpHandlerFacts = springNonHttpHandlerFacts.get(filePath) ?? [];
99
+ const messageProducerFacts = springMessageProducerFacts.get(filePath) ?? [];
88
100
  const packageFact = getJavaPackageFact(filePath);
89
101
  if (facts.length === 0 &&
90
102
  aopFacts.length === 0 &&
@@ -93,6 +105,7 @@ export function collectJavaCaptureSideChannel(filePath) {
93
105
  diFacts.length === 0 &&
94
106
  dynamicLookupFacts.length === 0 &&
95
107
  nonHttpHandlerFacts.length === 0 &&
108
+ messageProducerFacts.length === 0 &&
96
109
  packageFact === undefined) {
97
110
  return undefined;
98
111
  }
@@ -106,6 +119,9 @@ export function collectJavaCaptureSideChannel(filePath) {
106
119
  ...(diFacts.length > 0 ? { springDiFacts: diFacts } : {}),
107
120
  ...(dynamicLookupFacts.length > 0 ? { springDynamicLookupFacts: dynamicLookupFacts } : {}),
108
121
  ...(nonHttpHandlerFacts.length > 0 ? { springNonHttpHandlerFacts: nonHttpHandlerFacts } : {}),
122
+ ...(messageProducerFacts.length > 0
123
+ ? { springMessageProducerFacts: messageProducerFacts }
124
+ : {}),
109
125
  };
110
126
  }
111
127
  export function getJavaClassAnnotationFacts(filePath) {
@@ -126,6 +142,7 @@ export function applyJavaCaptureSideChannel(parsed) {
126
142
  setJavaSpringDiFacts(parsed.filePath, []);
127
143
  setJavaSpringDynamicLookupFacts(parsed.filePath, []);
128
144
  setJavaSpringNonHttpHandlerFacts(parsed.filePath, []);
145
+ setJavaSpringMessageProducerFacts(parsed.filePath, []);
129
146
  setJavaPackageFact(parsed.filePath, UNKNOWN_JVM_PACKAGE_FACT);
130
147
  return;
131
148
  }
@@ -136,5 +153,6 @@ export function applyJavaCaptureSideChannel(parsed) {
136
153
  setJavaSpringDiFacts(parsed.filePath, Array.isArray(data.springDiFacts) ? data.springDiFacts : []);
137
154
  setJavaSpringDynamicLookupFacts(parsed.filePath, Array.isArray(data.springDynamicLookupFacts) ? data.springDynamicLookupFacts : []);
138
155
  setJavaSpringNonHttpHandlerFacts(parsed.filePath, Array.isArray(data.springNonHttpHandlerFacts) ? data.springNonHttpHandlerFacts : []);
156
+ setJavaSpringMessageProducerFacts(parsed.filePath, Array.isArray(data.springMessageProducerFacts) ? data.springMessageProducerFacts : []);
139
157
  setJavaPackageFact(parsed.filePath, isJvmPackageFact(data.packageFact) ? data.packageFact : UNKNOWN_JVM_PACKAGE_FACT);
140
158
  }
@@ -7,12 +7,13 @@ 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, setJavaSpringDynamicLookupFacts, setJavaSpringNonHttpHandlerFacts, } from './capture-side-channel.js';
10
+ import { setJavaClassAnnotationFacts, setJavaSpringAopFacts, setJavaSpringConfigConsumerFacts, setJavaSpringConditionalFacts, setJavaSpringDiFacts, setJavaSpringDynamicLookupFacts, setJavaSpringMessageProducerFacts, 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
15
  import { captureJavaSpringDynamicLookupFact } from './spring-dynamic-lookup.js';
16
+ import { captureJavaSpringMessageProducerFact } from './spring-message-producers.js';
16
17
  import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
17
18
  import { captureJavaSpringAopFacts } from './spring-aop.js';
18
19
  import { captureJavaSpringConditionalFacts, } from './spring-conditionals.js';
@@ -101,7 +102,8 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
101
102
  const springNonHttpHandlerFacts = [];
102
103
  const springDiClassNodeIds = new Set();
103
104
  const springDynamicLookupFacts = [];
104
- const springDynamicLookupNodeIds = new Set();
105
+ const springMessageProducerFacts = [];
106
+ const springMemberCallNodeIds = new Set();
105
107
  for (const m of rawMatches) {
106
108
  const grouped = {};
107
109
  // Parallel tag -> captured SyntaxNode map. The tree-sitter query already
@@ -120,12 +122,17 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
120
122
  }
121
123
  if (Object.keys(grouped).length === 0)
122
124
  continue;
123
- const dynamicLookupNode = nodeIfType(nodeMap['@reference.call.member'], 'method_invocation');
124
- if (dynamicLookupNode !== null && !springDynamicLookupNodeIds.has(dynamicLookupNode.id)) {
125
- springDynamicLookupNodeIds.add(dynamicLookupNode.id);
126
- const fact = captureJavaSpringDynamicLookupFact(dynamicLookupNode, filePath);
127
- if (fact !== null)
128
- springDynamicLookupFacts.push(fact);
125
+ // One visit per member call node: the same invocation can back several
126
+ // query matches, and both Spring call-shape captures must see it once.
127
+ const memberCallNode = nodeIfType(nodeMap['@reference.call.member'], 'method_invocation');
128
+ if (memberCallNode !== null && !springMemberCallNodeIds.has(memberCallNode.id)) {
129
+ springMemberCallNodeIds.add(memberCallNode.id);
130
+ const lookupFact = captureJavaSpringDynamicLookupFact(memberCallNode, filePath);
131
+ if (lookupFact !== null)
132
+ springDynamicLookupFacts.push(lookupFact);
133
+ const producerFact = captureJavaSpringMessageProducerFact(memberCallNode, filePath);
134
+ if (producerFact !== null)
135
+ springMessageProducerFacts.push(producerFact);
129
136
  }
130
137
  const springAopTypeNode = [
131
138
  nodeIfType(nodeMap['@scope.class'], 'class_declaration'),
@@ -300,6 +307,7 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
300
307
  setJavaSpringDiFacts(filePath, springDiFacts);
301
308
  setJavaSpringDynamicLookupFacts(filePath, springDynamicLookupFacts);
302
309
  setJavaSpringNonHttpHandlerFacts(filePath, springNonHttpHandlerFacts);
310
+ setJavaSpringMessageProducerFacts(filePath, springMessageProducerFacts);
303
311
  return [
304
312
  ...resolveVarTypeBindings(out),
305
313
  ...synthesizeJavaInheritanceReferences(tree.rootNode),
@@ -1,13 +1,29 @@
1
1
  import { type SpringDiAnnotationFact, type SpringDiClassFact, type SpringDiDependencyFact, type SpringDiInjectionSiteFact } from '../../frameworks/spring/di-metadata.js';
2
+ import { type SpringArgumentFact } from '../../frameworks/spring/argument-facts.js';
2
3
  import { type SyntaxNode } from '../../utils/ast-helpers.js';
3
4
  export interface JavaAnnotationSyntaxFact extends SpringDiAnnotationFact {
4
5
  readonly line: number;
6
+ /** Present only for callers that opt in via `javaSpringAnnotationFacts`. */
7
+ readonly args?: readonly SpringArgumentFact[];
8
+ }
9
+ /**
10
+ * Options for `javaSpringAnnotationFacts`.
11
+ *
12
+ * The STRUCTURED arguments are opt-in because DI captures every annotated
13
+ * field, constructor, and method in the repository, and none of its consumers
14
+ * reads them. Note what this does and does not save: every fact already carries
15
+ * `text`, the annotation's full source, so the argument TEXT crosses the worker
16
+ * boundary either way. What the opt-in avoids is a second, parsed copy of that
17
+ * same text on facts that would never look at it.
18
+ */
19
+ export interface JavaSpringAnnotationFactOptions {
20
+ readonly includeArguments?: boolean;
5
21
  }
6
22
  export type JavaSpringDependencyFact = SpringDiDependencyFact<JavaAnnotationSyntaxFact>;
7
23
  type JavaSpringInjectionSiteKind = 'field' | 'constructor' | 'method';
8
24
  export type JavaSpringInjectionSiteFact = SpringDiInjectionSiteFact<JavaAnnotationSyntaxFact, JavaSpringInjectionSiteKind>;
9
25
  export type JavaSpringDiClassFact = SpringDiClassFact<JavaAnnotationSyntaxFact, JavaSpringInjectionSiteKind>;
10
- export declare function javaSpringAnnotationFacts(node: SyntaxNode): JavaAnnotationSyntaxFact[];
26
+ export declare function javaSpringAnnotationFacts(node: SyntaxNode, options?: JavaSpringAnnotationFactOptions): JavaAnnotationSyntaxFact[];
11
27
  /**
12
28
  * Capture one class already surfaced by Java's scope query.
13
29
  *
@@ -1,11 +1,51 @@
1
1
  import { makeScopeId } from '../../../../_shared/index.js';
2
2
  import { createSpringDiMetadataAttacher, hasSpringDiRelevantAnnotation, hasSpringStereotypeSyntax, } from '../../frameworks/spring/di-metadata.js';
3
3
  import { hasSpringBeanFactorySyntax, } from '../../frameworks/spring/bean-factories.js';
4
+ import { normalizeSpringFactText, } from '../../frameworks/spring/argument-facts.js';
4
5
  import { parseSpringInjectionType } from '../../di-extractors/spring.js';
5
- import { nodeToCapture } from '../../utils/ast-helpers.js';
6
+ import { hasRecoveredSyntax, nodeToCapture } from '../../utils/ast-helpers.js';
6
7
  import { isJavaPackageSiblingVisibilityIncomplete } from './package-siblings.js';
7
8
  import { getJavaSpringDiFacts } from './capture-side-channel.js';
8
- export function javaSpringAnnotationFacts(node) {
9
+ const JAVA_COMMENT_NODE_TYPES = new Set(['line_comment', 'block_comment']);
10
+ /**
11
+ * Annotation arguments as written, or `undefined` for a marker annotation.
12
+ *
13
+ * `@Scheduled` yields `undefined` (no argument list in the syntax) while
14
+ * `@Scheduled()` yields `[]` (an empty list was written). Named arguments keep
15
+ * their key, single-element ones stay positional, and array initializers are
16
+ * kept as one raw `{...}` text — splitting or dereferencing them would be
17
+ * resolution, which does not belong at capture time.
18
+ *
19
+ * An argument list that did not parse also yields `undefined`. Error recovery
20
+ * fills gaps with invented nodes — `@KafkaListener(topics = "orders", groupId =`
21
+ * hands back a `groupId` whose value is a `{}` that nobody wrote — and there is
22
+ * no fourth state here for "unreadable". Collapsing it into the marker case is
23
+ * deliberate: both tell a consumer there is nothing here to resolve, which is
24
+ * true, whereas a fabricated value would send it somewhere real and wrong.
25
+ */
26
+ function javaAnnotationArgumentFacts(annotation) {
27
+ const argumentList = annotation.childForFieldName('arguments');
28
+ if (argumentList === null || hasRecoveredSyntax(argumentList))
29
+ return undefined;
30
+ const args = [];
31
+ for (const child of argumentList.namedChildren) {
32
+ if (JAVA_COMMENT_NODE_TYPES.has(child.type))
33
+ continue;
34
+ if (child.type === 'element_value_pair') {
35
+ const key = child.childForFieldName('key');
36
+ const value = child.childForFieldName('value');
37
+ if (key === null || value === null) {
38
+ args.push({ text: normalizeSpringFactText(child.text) });
39
+ continue;
40
+ }
41
+ args.push({ name: key.text.trim(), text: normalizeSpringFactText(value.text) });
42
+ continue;
43
+ }
44
+ args.push({ text: normalizeSpringFactText(child.text) });
45
+ }
46
+ return args;
47
+ }
48
+ export function javaSpringAnnotationFacts(node, options = {}) {
9
49
  const facts = [];
10
50
  for (const child of node.namedChildren) {
11
51
  if (child.type !== 'modifiers')
@@ -16,10 +56,12 @@ export function javaSpringAnnotationFacts(node) {
16
56
  const nameNode = modifier.childForFieldName('name') ?? modifier.firstNamedChild;
17
57
  if (nameNode === null)
18
58
  continue;
59
+ const args = options.includeArguments === true ? javaAnnotationArgumentFacts(modifier) : undefined;
19
60
  facts.push({
20
61
  name: nameNode.text.trim(),
21
62
  text: modifier.text.trim(),
22
63
  line: modifier.startPosition.row + 1,
64
+ ...(args === undefined ? {} : { args }),
23
65
  });
24
66
  }
25
67
  }
@@ -0,0 +1,20 @@
1
+ import { type SpringMessageProducerFact } from '../../frameworks/spring/message-producers.js';
2
+ import { type SyntaxNode } from '../../utils/ast-helpers.js';
3
+ /**
4
+ * Capture one messaging-template publish from a Java call already surfaced by
5
+ * the scope query, without resolving the destination it names.
6
+ *
7
+ * The destination argument may be a literal, a reference to a constant that
8
+ * lives in another file, or a `${...}` placeholder resolved from configuration;
9
+ * all three are recorded as written and left to a later phase.
10
+ *
11
+ * A call whose argument list did not parse yields NO fact. The fact exists to
12
+ * carry a destination, and error recovery invents argument boundaries — an
13
+ * unterminated `send(TOPIC,` absorbs the next declaration's source and offers
14
+ * it as an argument. There is no state on this fact that means "published
15
+ * somewhere unreadable", so the choice is between silence and a plausible lie,
16
+ * and silence is recoverable: the file is re-captured when it parses.
17
+ */
18
+ export declare function captureJavaSpringMessageProducerFact(node: SyntaxNode, filePath: string): SpringMessageProducerFact | null;
19
+ /** Standalone extractor for focused tests; production reuses scope-query call nodes. */
20
+ export declare function captureJavaSpringMessageProducerFacts(rootNode: SyntaxNode, filePath: string): SpringMessageProducerFact[];