Skip to content

Commit 60aaf07

Browse files
authored
feat(event-topic): SQS event-topic detector — 6 langs (T5-14/15/16/17/18/19) (#310)
* feat(event-topic): SQS Java/Go/Rust detectors (T5-17/18/19) - sqs.rs: shared EventTopicConfigs for SQS_JAVA, SQS_GO, SQS_RUST - java/frameworks.scm: sendMessage/sendMessageBatch/receiveMessage builder patterns - go/frameworks.scm: SendMessage/ReceiveMessage struct-literal patterns with aws.String - rust/frameworks.scm: send_message/receive_message fluent-builder patterns - Wire up extract_event_topics in java/go/rust parsers - python/parser.rs: add StringPool import (compile fix for event_topics block) - Tests: 4 Java, 4 Go, 3 Rust (11 total) * feat(event-topic): SQS Python/TS/JS detectors (T5-14/15/16) - sqs_python.rs: EventTopicConfig for boto3/aioboto3; send_message/receive_message/send_message_batch/delete_message - sqs_ts.rs: EventTopicConfig for @aws-sdk/client-sqs; all four Command types - sqs_js.rs: EventTopicConfig for @aws-sdk/client-sqs (JS variant) - python/frameworks.scm: QueueUrl kwarg literal patterns (sync + async, expression + assignment forms) - typescript/frameworks.scm: new_expression Command constructor patterns (function_declaration + method_definition, sync + await) - javascript/frameworks.scm: same Command patterns as TS (new file) - Wire up extract_event_topics in typescript/parser.rs and javascript/parser.rs - Tests: 6 Python, 6 TypeScript, 6 JavaScript (18 total) - go_events_sqs.rs: fix clippy D-warning (rsplit/next instead of split/last) * chore(sqs): rustfmt parser blank-line cleanup after rebase * fix(sqs): adapt 6 SQS tests to T5-33 Box<str> RawEventTopic migration Drop `&mut StringPool` arg from `extract_event_topics` calls; change `(Vec, StringPool)` return to plain `Vec`; replace `pool.resolve(&lit)` with `lit.as_deref().expect(...)` across all six language test files (Python, TypeScript, JavaScript, Java, Go, Rust).
1 parent 84b6e5f commit 60aaf07

23 files changed

Lines changed: 1772 additions & 0 deletions

crates/ecp-analyzer/src/event_topic/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ pub mod redis_js;
2020
pub mod redis_python;
2121
pub mod redis_rust;
2222
pub mod redis_ts;
23+
pub mod sqs;
24+
pub mod sqs_js;
25+
pub mod sqs_python;
26+
pub mod sqs_ts;
2327

2428
pub use celery_python::CELERY_PYTHON;
2529
pub use config::EventTopicConfig;
@@ -42,3 +46,7 @@ pub use redis_js::REDIS_JS;
4246
pub use redis_python::REDIS_PYTHON;
4347
pub use redis_rust::REDIS_RUST;
4448
pub use redis_ts::REDIS_TS;
49+
pub use sqs::{SQS_GO, SQS_JAVA, SQS_RUST};
50+
pub use sqs_js::SQS_JS;
51+
pub use sqs_python::SQS_PYTHON;
52+
pub use sqs_ts::SQS_TS;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//! `EventTopicConfig` constants for AWS SQS across Java, Go, and Rust.
2+
//!
3+
//! All three share the same topic identifier: the **QueueUrl** string passed
4+
//! to `SendMessage` / `SendMessageBatch` (publish) or `ReceiveMessage` (subscribe).
5+
//!
6+
//! Import gates (per T5-17/18/19 roadmap matrix):
7+
//! - Java: `software.amazon.awssdk.services.sqs`
8+
//! - Go: `github.com/aws/aws-sdk-go-v2/service/sqs`
9+
//! - Rust: `aws-sdk-sqs` (Cargo crate name; as a `use` path: `aws_sdk_sqs`)
10+
//!
11+
//! Direction: `classify_sqs_direction` maps send/publish verbs to `Publish`
12+
//! and receive/poll verbs to `Subscribe`; unknown text defaults to `Publish`.
13+
14+
use super::config::EventTopicConfig;
15+
use ecp_core::analyzer::types::{FrameworkId, PubSub};
16+
17+
/// Maps SQS call-site verb text to `PubSub` direction.
18+
///
19+
/// Producer verbs (`send_message`, `sendMessage`, `SendMessage`, etc.) →
20+
/// `Publish`; consumer verbs (`receive_message`, `receiveMessage`,
21+
/// `ReceiveMessage`) → `Subscribe`. Unrecognised text defaults to `Publish`
22+
/// so the topic is still indexed rather than silently dropped.
23+
pub fn classify_sqs_direction(raw: &str) -> PubSub {
24+
match raw {
25+
"receive_message" | "receiveMessage" | "ReceiveMessage" => PubSub::Subscribe,
26+
_ => PubSub::Publish,
27+
}
28+
}
29+
30+
/// SQS detector for the **AWS SDK for Java v2** (`software.amazon.awssdk`).
31+
///
32+
/// Tree-sitter capture names: `sqs.topic`, `sqs.producer_fn`, `sqs.direction`.
33+
///
34+
/// Fires on `SqsClient.sendMessage(SendMessageRequest.builder().queueUrl("...").build())`
35+
/// and the equivalent `sendMessageBatch` / `receiveMessage` shapes. QueueUrl
36+
/// is captured as a string literal from the `.queueUrl("…")` builder call.
37+
pub const SQS_JAVA: EventTopicConfig = EventTopicConfig {
38+
framework: FrameworkId::Sqs,
39+
topic_capture: "sqs.topic",
40+
producer_capture: "sqs.producer_fn",
41+
direction_capture: "sqs.direction",
42+
import_gate: &["software.amazon.awssdk.services.sqs"],
43+
direction_classifier: classify_sqs_direction,
44+
canonicalize: false,
45+
};
46+
47+
/// SQS detector for the **AWS SDK for Go v2** (`aws-sdk-go-v2`).
48+
///
49+
/// Tree-sitter capture names: `sqs.topic`, `sqs.producer_fn`, `sqs.direction`.
50+
///
51+
/// Fires on `client.SendMessage(ctx, &sqs.SendMessageInput{QueueUrl: aws.String("…")})`.
52+
/// The QueueUrl field value inside the struct literal is captured when it is
53+
/// a string literal passed to `aws.String("…")`.
54+
pub const SQS_GO: EventTopicConfig = EventTopicConfig {
55+
framework: FrameworkId::Sqs,
56+
topic_capture: "sqs.topic",
57+
producer_capture: "sqs.producer_fn",
58+
direction_capture: "sqs.direction",
59+
import_gate: &["github.com/aws/aws-sdk-go-v2/service/sqs"],
60+
direction_classifier: classify_sqs_direction,
61+
canonicalize: false,
62+
};
63+
64+
/// SQS detector for the **AWS SDK for Rust** (`aws-sdk-sqs` crate).
65+
///
66+
/// Tree-sitter capture names: `sqs.topic`, `sqs.producer_fn`, `sqs.direction`.
67+
///
68+
/// Fires on `client.send_message().queue_url("…").send().await` fluent-builder
69+
/// chains. The string literal passed to `.queue_url("…")` is captured.
70+
/// SQS detector for the **AWS SDK for Rust** (`aws-sdk-sqs` crate).
71+
///
72+
/// Tree-sitter capture names: `sqs.topic`, `sqs.direction`.
73+
///
74+
/// Note: `producer_capture` is empty because tree-sitter Rust's named-field
75+
/// `body: (block (...))` does not perform descendant matching, making it
76+
/// impractical to anchor the pattern to a `function_item` while also
77+
/// matching the `queue_url` call deep inside a fluent chain. The `enclosing_fn`
78+
/// field in `RawEventTopic` is left empty; `topic_literal` + `direction` are
79+
/// the primary outputs.
80+
pub const SQS_RUST: EventTopicConfig = EventTopicConfig {
81+
framework: FrameworkId::Sqs,
82+
topic_capture: "sqs.topic",
83+
producer_capture: "",
84+
direction_capture: "sqs.direction",
85+
import_gate: &["aws_sdk_sqs"],
86+
direction_classifier: classify_sqs_direction,
87+
canonicalize: false,
88+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//! `EventTopicConfig` for AWS SQS JavaScript clients.
2+
//!
3+
//! Covers `@aws-sdk/client-sqs` (AWS SDK v3 for JavaScript):
4+
//! - `await client.send(new SendMessageCommand({ QueueUrl: "...", MessageBody: "..." }))`
5+
//! - `await client.send(new ReceiveMessageCommand({ QueueUrl: "...", ... }))`
6+
//! - `await client.send(new SendMessageBatchCommand({ QueueUrl: "...", Entries: [...] }))`
7+
//! - `await client.send(new DeleteMessageCommand({ QueueUrl: "...", ReceiptHandle: "..." }))`
8+
//!
9+
//! Direction: `SendMessageCommand` / `SendMessageBatchCommand` → Publish.
10+
//! `ReceiveMessageCommand` / `DeleteMessageCommand` → Subscribe.
11+
//!
12+
//! Topic literal: the `QueueUrl` property string in the Command constructor object.
13+
//! Non-literal `QueueUrl` (variable) produces no capture → no RawEventTopic.
14+
//!
15+
//! LLM-utility: SQS durable queue semantics differ from Redis pub/sub (ephemeral)
16+
//! and Kafka (log-based replay). LLMs must know all JS producers/consumers of a
17+
//! queue when renaming a QueueUrl — this config surfaces them for `ecp impact`.
18+
19+
use super::config::EventTopicConfig;
20+
use ecp_core::analyzer::types::{FrameworkId, PubSub};
21+
22+
/// Maps the SQS Command constructor name to `PubSub` direction.
23+
///
24+
/// Consumer-side commands (`ReceiveMessageCommand`, `DeleteMessageCommand`)
25+
/// → `Subscribe`. Send variants → `Publish`.
26+
fn classify_sqs_direction(raw: &str) -> PubSub {
27+
match raw {
28+
"ReceiveMessageCommand" | "DeleteMessageCommand" => PubSub::Subscribe,
29+
_ => PubSub::Publish,
30+
}
31+
}
32+
33+
/// SQS JavaScript detector — fires for `@aws-sdk/client-sqs` imports.
34+
///
35+
/// `direction_capture: "sqs.direction"` binds the Command constructor name so
36+
/// `classify_sqs_direction` can resolve `PubSub` direction without fabrication.
37+
///
38+
/// `topic_capture: "sqs.topic"` captures the `QueueUrl` property string literal
39+
/// from the Command constructor's object argument. Non-literal `QueueUrl`
40+
/// produces no capture.
41+
pub const SQS_JS: EventTopicConfig = EventTopicConfig {
42+
framework: FrameworkId::Sqs,
43+
topic_capture: "sqs.topic",
44+
producer_capture: "sqs.fn",
45+
direction_capture: "sqs.direction",
46+
import_gate: &["@aws-sdk/client-sqs"],
47+
direction_classifier: classify_sqs_direction,
48+
canonicalize: false,
49+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//! `EventTopicConfig` for AWS SQS Python clients.
2+
//!
3+
//! Covers boto3 (sync) and aioboto3 (async) under a single config:
4+
//! - boto3: `sqs.send_message(QueueUrl="https://...", MessageBody="...")`
5+
//! `sqs.receive_message(QueueUrl="https://...", ...)`
6+
//! `sqs.send_message_batch(QueueUrl="https://...", Entries=[...])`
7+
//! `sqs.delete_message(QueueUrl="https://...", ReceiptHandle="...")`
8+
//! - aioboto3: same forms under `await`.
9+
//!
10+
//! Direction: `send_message` / `send_message_batch` → Publish.
11+
//! `receive_message` / `delete_message` → Subscribe.
12+
//!
13+
//! Topic literal: the `QueueUrl` keyword argument string. Non-literal QueueUrl
14+
//! produces no capture → no RawEventTopic (no fabrication).
15+
//!
16+
//! LLM-utility: SQS is a durable queue (at-least-once delivery); unlike Redis
17+
//! pub/sub, unprocessed messages accumulate in the queue. An LLM renaming a
18+
//! QueueUrl must know all producers and consumers to avoid losing in-flight
19+
//! messages. This config surfaces both sides so `ecp impact` can trace the full
20+
//! blast radius across boto3 and aioboto3 call sites.
21+
22+
use super::config::EventTopicConfig;
23+
use ecp_core::analyzer::types::{FrameworkId, PubSub};
24+
25+
/// Maps SQS Python method name to `PubSub` direction.
26+
///
27+
/// Consumer-side verbs (`receive_message`, `delete_message`) → `Subscribe`.
28+
/// Everything else (send variants) → `Publish`.
29+
fn classify_sqs_direction(raw: &str) -> PubSub {
30+
match raw {
31+
"receive_message" | "delete_message" => PubSub::Subscribe,
32+
_ => PubSub::Publish,
33+
}
34+
}
35+
36+
/// SQS Python detector — fires for `boto3` and `aioboto3` imports.
37+
///
38+
/// `direction_capture: "sqs.direction"` binds the method name so
39+
/// `classify_sqs_direction` can resolve `PubSub` direction without fabrication.
40+
///
41+
/// `topic_capture: "sqs.topic"` captures the `QueueUrl` keyword argument
42+
/// string literal. Non-literal `QueueUrl` (variable) produces no capture.
43+
pub const SQS_PYTHON: EventTopicConfig = EventTopicConfig {
44+
framework: FrameworkId::Sqs,
45+
topic_capture: "sqs.topic",
46+
producer_capture: "sqs.producer_fn",
47+
direction_capture: "sqs.direction",
48+
import_gate: &["boto3", "aioboto3"],
49+
direction_classifier: classify_sqs_direction,
50+
canonicalize: false,
51+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//! `EventTopicConfig` for AWS SQS TypeScript clients.
2+
//!
3+
//! Covers `@aws-sdk/client-sqs` (AWS SDK v3 for JavaScript/TypeScript):
4+
//! - `await client.send(new SendMessageCommand({ QueueUrl: "...", MessageBody: "..." }))`
5+
//! - `await client.send(new ReceiveMessageCommand({ QueueUrl: "...", ... }))`
6+
//! - `await client.send(new SendMessageBatchCommand({ QueueUrl: "...", Entries: [...] }))`
7+
//! - `await client.send(new DeleteMessageCommand({ QueueUrl: "...", ReceiptHandle: "..." }))`
8+
//!
9+
//! Direction: `SendMessageCommand` / `SendMessageBatchCommand` → Publish.
10+
//! `ReceiveMessageCommand` / `DeleteMessageCommand` → Subscribe.
11+
//!
12+
//! Topic literal: the `QueueUrl` property string in the Command constructor object.
13+
//! Non-literal `QueueUrl` (variable) produces no capture → no RawEventTopic.
14+
//!
15+
//! LLM-utility: SQS is a durable queue (at-least-once delivery); LLMs renaming
16+
//! a QueueUrl must trace all producers and consumers to avoid losing in-flight
17+
//! messages. This config surfaces both send and receive call sites so `ecp impact`
18+
//! can produce a complete blast-radius view across TypeScript AWS SDK code.
19+
20+
use super::config::EventTopicConfig;
21+
use ecp_core::analyzer::types::{FrameworkId, PubSub};
22+
23+
/// Maps the SQS Command constructor name to `PubSub` direction.
24+
///
25+
/// Consumer-side commands (`ReceiveMessageCommand`, `DeleteMessageCommand`)
26+
/// → `Subscribe`. Everything else (send variants) → `Publish`.
27+
fn classify_sqs_direction(raw: &str) -> PubSub {
28+
match raw {
29+
"ReceiveMessageCommand" | "DeleteMessageCommand" => PubSub::Subscribe,
30+
_ => PubSub::Publish,
31+
}
32+
}
33+
34+
/// SQS TypeScript detector — fires for `@aws-sdk/client-sqs` imports.
35+
///
36+
/// `direction_capture: "sqs.direction"` binds the Command constructor name so
37+
/// `classify_sqs_direction` can resolve `PubSub` direction without fabrication.
38+
///
39+
/// `topic_capture: "sqs.topic"` captures the `QueueUrl` property string literal
40+
/// from the Command constructor's object argument. Non-literal `QueueUrl`
41+
/// (variable or expression) produces no capture.
42+
pub const SQS_TS: EventTopicConfig = EventTopicConfig {
43+
framework: FrameworkId::Sqs,
44+
topic_capture: "sqs.topic",
45+
producer_capture: "sqs.fn",
46+
direction_capture: "sqs.direction",
47+
import_gate: &["@aws-sdk/client-sqs"],
48+
direction_classifier: classify_sqs_direction,
49+
canonicalize: false,
50+
};

crates/ecp-analyzer/src/go/frameworks.scm

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
value: (literal_element
151151
(interpreted_string_literal) @kafka.topic))))))))))
152152

153+
153154
;; segmentio/kafka-go: WriteMessages inside a method_declaration.
154155
(method_declaration
155156
name: (field_identifier) @kafka.go.fn
@@ -170,6 +171,7 @@
170171
value: (literal_element
171172
(interpreted_string_literal) @kafka.topic))))))))))
172173

174+
173175
;; Shopify/sarama: msg := &sarama.ProducerMessage{Topic: "topic", ...}
174176
;; Captures Topic string literal directly from the struct literal.
175177
;; The `&` unary operator wraps the composite_literal; the struct type's
@@ -191,3 +193,93 @@
191193
(#eq? @_stopic_key "Topic"))
192194
value: (literal_element
193195
(interpreted_string_literal) @kafka.topic))))))))))
196+
197+
;; ---- AWS SQS Go SDK v2 (T5-18) ----
198+
;; Pattern: client.SendMessage(ctx, &sqs.SendMessageInput{QueueUrl: aws.String("url")})
199+
;; client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{QueueUrl: aws.String("url")})
200+
;;
201+
;; Anchored to function_declaration to capture the enclosing function name
202+
;; alongside the topic literal in a single match.
203+
;; Import gate (github.com/aws/aws-sdk-go-v2/service/sqs) enforced at runtime.
204+
;;
205+
;; Go AST structure (tree-sitter):
206+
;; block > statement_list > expression_statement > call_expression
207+
;; arguments > unary_expression > composite_literal
208+
;; body > literal_value > keyed_element
209+
;; key: literal_element > identifier("QueueUrl")
210+
;; value: literal_element > call_expression(aws.String)
211+
;; arguments > interpreted_string_literal("url")
212+
;;
213+
;; Dynamic QueueUrl (variable identifier, not string literal inside aws.String)
214+
;; does not produce an interpreted_string_literal → no match → no fabrication.
215+
216+
;; Publish — SendMessage / SendMessageBatch with literal QueueUrl.
217+
(function_declaration
218+
name: (identifier) @sqs.producer_fn
219+
body: (block
220+
(statement_list
221+
(expression_statement
222+
(call_expression
223+
function: (selector_expression
224+
field: (field_identifier) @sqs.direction)
225+
arguments: (argument_list
226+
(unary_expression
227+
operand: (composite_literal
228+
body: (literal_value
229+
(keyed_element
230+
key: (literal_element
231+
(identifier) @_qk)
232+
value: (literal_element
233+
(call_expression
234+
arguments: (argument_list
235+
(interpreted_string_literal) @sqs.topic))))))))
236+
(#match? @sqs.direction "^(SendMessage|SendMessageBatch)$")
237+
(#eq? @_qk "QueueUrl"))))))
238+
239+
;; Subscribe — ReceiveMessage with literal QueueUrl.
240+
(function_declaration
241+
name: (identifier) @sqs.producer_fn
242+
body: (block
243+
(statement_list
244+
(expression_statement
245+
(call_expression
246+
function: (selector_expression
247+
field: (field_identifier) @sqs.direction)
248+
arguments: (argument_list
249+
(unary_expression
250+
operand: (composite_literal
251+
body: (literal_value
252+
(keyed_element
253+
key: (literal_element
254+
(identifier) @_qk)
255+
value: (literal_element
256+
(call_expression
257+
arguments: (argument_list
258+
(interpreted_string_literal) @sqs.topic))))))))
259+
(#eq? @sqs.direction "ReceiveMessage")
260+
(#eq? @_qk "QueueUrl")))))
261+
)
262+
263+
;; Method receiver variant — SendMessage / ReceiveMessage inside a method body.
264+
(method_declaration
265+
name: (field_identifier) @sqs.producer_fn
266+
body: (block
267+
(statement_list
268+
(expression_statement
269+
(call_expression
270+
function: (selector_expression
271+
field: (field_identifier) @sqs.direction)
272+
arguments: (argument_list
273+
(unary_expression
274+
operand: (composite_literal
275+
body: (literal_value
276+
(keyed_element
277+
key: (literal_element
278+
(identifier) @_qk)
279+
value: (literal_element
280+
(call_expression
281+
arguments: (argument_list
282+
(interpreted_string_literal) @sqs.topic))))))))
283+
(#match? @sqs.direction "^(SendMessage|SendMessageBatch|ReceiveMessage)$")
284+
(#eq? @_qk "QueueUrl")))))
285+
)

crates/ecp-analyzer/src/go/parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ impl LanguageProvider for GoProvider {
627627
crate::event_topic::REDIS_GO,
628628
crate::event_topic::KAFKA_GO,
629629
crate::event_topic::RABBITMQ_GO,
630+
crate::event_topic::SQS_GO,
630631
],
631632
&imports,
632633
);

0 commit comments

Comments
 (0)