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.
- package/dist/core/ingestion/frameworks/spring/argument-facts.d.ts +80 -0
- package/dist/core/ingestion/frameworks/spring/argument-facts.js +85 -0
- package/dist/core/ingestion/frameworks/spring/message-producers.d.ts +78 -0
- package/dist/core/ingestion/frameworks/spring/message-producers.js +90 -0
- package/dist/core/ingestion/frameworks/spring/non-http-handlers.d.ts +29 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +18 -0
- package/dist/core/ingestion/languages/java/captures.js +16 -8
- package/dist/core/ingestion/languages/java/spring-di.d.ts +17 -1
- package/dist/core/ingestion/languages/java/spring-di.js +44 -2
- package/dist/core/ingestion/languages/java/spring-message-producers.d.ts +20 -0
- package/dist/core/ingestion/languages/java/spring-message-producers.js +83 -0
- package/dist/core/ingestion/languages/java/spring-non-http-handlers.d.ts +11 -1
- package/dist/core/ingestion/languages/java/spring-non-http-handlers.js +13 -3
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +18 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +16 -8
- package/dist/core/ingestion/languages/kotlin/spring-di.d.ts +51 -1
- package/dist/core/ingestion/languages/kotlin/spring-di.js +86 -6
- package/dist/core/ingestion/languages/kotlin/spring-message-producers.d.ts +17 -0
- package/dist/core/ingestion/languages/kotlin/spring-message-producers.js +119 -0
- package/dist/core/ingestion/languages/kotlin/spring-non-http-handlers.d.ts +16 -0
- package/dist/core/ingestion/languages/kotlin/spring-non-http-handlers.js +77 -2
- package/dist/core/ingestion/utils/ast-helpers.d.ts +16 -0
- package/dist/core/ingestion/utils/ast-helpers.js +18 -0
- package/dist/storage/parse-cache.js +14 -1
- 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
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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[];
|