codegraph-engine 3.0.0__2-py3-none-any.whl
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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +868 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Apache Kafka producer and consumer detector.
|
|
2
|
+
|
|
3
|
+
Detects:
|
|
4
|
+
- Publishers / Producers: producer.send(topic, ...), producer.produce(topic, ...)
|
|
5
|
+
- Consumers / Workers: @consumer(topic), @subscriber(topic), KafkaConsumer(topic), consumer.subscribe([topic])
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from codegraph.async_queue.models import (
|
|
13
|
+
AsyncConsumerRecord,
|
|
14
|
+
AsyncMessagingSystem,
|
|
15
|
+
AsyncProducerRecord,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_KAFKA_CONSUMER_DECORATORS = {
|
|
19
|
+
"consumer",
|
|
20
|
+
"kafka_consumer",
|
|
21
|
+
"subscriber",
|
|
22
|
+
"consume",
|
|
23
|
+
"agent",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect_kafka(
|
|
28
|
+
tree: ast.AST,
|
|
29
|
+
content: str,
|
|
30
|
+
file_path: str,
|
|
31
|
+
module_name: str,
|
|
32
|
+
) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
|
|
33
|
+
producers: list[AsyncProducerRecord] = []
|
|
34
|
+
consumers: list[AsyncConsumerRecord] = []
|
|
35
|
+
lines = content.splitlines()
|
|
36
|
+
|
|
37
|
+
def get_line_snippet(lineno: int) -> str:
|
|
38
|
+
if 1 <= lineno <= len(lines):
|
|
39
|
+
return lines[lineno - 1].strip()
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
# 1. Detect consumers via decorators or subscriber calls
|
|
43
|
+
for node in ast.walk(tree):
|
|
44
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
45
|
+
for dec in node.decorator_list:
|
|
46
|
+
dec_func = dec.func if isinstance(dec, ast.Call) else dec
|
|
47
|
+
dec_name = getattr(dec_func, "id", "") or getattr(dec_func, "attr", "")
|
|
48
|
+
|
|
49
|
+
if dec_name.lower() in _KAFKA_CONSUMER_DECORATORS:
|
|
50
|
+
topic = ""
|
|
51
|
+
if isinstance(dec, ast.Call):
|
|
52
|
+
if dec.args and isinstance(dec.args[0], ast.Constant) and isinstance(dec.args[0].value, str):
|
|
53
|
+
topic = dec.args[0].value
|
|
54
|
+
for kw in dec.keywords:
|
|
55
|
+
if kw.arg in ("topic", "topics") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
56
|
+
topic = kw.value.value
|
|
57
|
+
|
|
58
|
+
if topic:
|
|
59
|
+
consumers.append(
|
|
60
|
+
AsyncConsumerRecord(
|
|
61
|
+
system=AsyncMessagingSystem.KAFKA,
|
|
62
|
+
destination=topic,
|
|
63
|
+
queue_id=f"kafka:{topic}",
|
|
64
|
+
handler_canonical_id=f"{module_name}.{node.name}",
|
|
65
|
+
file_path=file_path,
|
|
66
|
+
line=node.lineno,
|
|
67
|
+
handler_snippet=get_line_snippet(node.lineno),
|
|
68
|
+
confidence="HIGH",
|
|
69
|
+
metadata={"decorator": dec_name},
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# 2. Detect producers & programmatic consumers inside function scopes
|
|
74
|
+
class KafkaCallVisitor(ast.NodeVisitor):
|
|
75
|
+
def __init__(self) -> None:
|
|
76
|
+
self.current_function: str = f"{module_name}.<module>"
|
|
77
|
+
|
|
78
|
+
def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
|
|
79
|
+
prev = self.current_function
|
|
80
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
81
|
+
self.generic_visit(fn)
|
|
82
|
+
self.current_function = prev
|
|
83
|
+
|
|
84
|
+
def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
|
|
85
|
+
prev = self.current_function
|
|
86
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
87
|
+
self.generic_visit(fn)
|
|
88
|
+
self.current_function = prev
|
|
89
|
+
|
|
90
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
91
|
+
# Check for producer.send(topic, ...) or producer.produce(topic, ...)
|
|
92
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr in ("send", "produce"):
|
|
93
|
+
topic = ""
|
|
94
|
+
meta: dict[str, Any] = {"method": node.func.attr}
|
|
95
|
+
if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
96
|
+
topic = node.args[0].value
|
|
97
|
+
for kw in node.keywords:
|
|
98
|
+
if kw.arg == "topic" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
99
|
+
topic = kw.value.value
|
|
100
|
+
elif kw.arg in ("key", "partition", "headers"):
|
|
101
|
+
meta[kw.arg] = True
|
|
102
|
+
|
|
103
|
+
if topic:
|
|
104
|
+
producers.append(
|
|
105
|
+
AsyncProducerRecord(
|
|
106
|
+
system=AsyncMessagingSystem.KAFKA,
|
|
107
|
+
destination=topic,
|
|
108
|
+
queue_id=f"kafka:{topic}",
|
|
109
|
+
caller_canonical_id=self.current_function,
|
|
110
|
+
file_path=file_path,
|
|
111
|
+
line=node.lineno,
|
|
112
|
+
call_snippet=get_line_snippet(node.lineno),
|
|
113
|
+
confidence="HIGH",
|
|
114
|
+
metadata=meta,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Check for consumer.subscribe([topic, ...])
|
|
119
|
+
elif isinstance(node.func, ast.Attribute) and node.func.attr == "subscribe":
|
|
120
|
+
topic_list: list[str] = []
|
|
121
|
+
if node.args and isinstance(node.args[0], (ast.List, ast.Tuple)):
|
|
122
|
+
for elt in node.args[0].elts:
|
|
123
|
+
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
124
|
+
topic_list.append(elt.value)
|
|
125
|
+
for kw in node.keywords:
|
|
126
|
+
if kw.arg == "topics" and isinstance(kw.value, (ast.List, ast.Tuple)):
|
|
127
|
+
for elt in kw.value.elts:
|
|
128
|
+
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
129
|
+
topic_list.append(elt.value)
|
|
130
|
+
|
|
131
|
+
for t in topic_list:
|
|
132
|
+
consumers.append(
|
|
133
|
+
AsyncConsumerRecord(
|
|
134
|
+
system=AsyncMessagingSystem.KAFKA,
|
|
135
|
+
destination=t,
|
|
136
|
+
queue_id=f"kafka:{t}",
|
|
137
|
+
handler_canonical_id=self.current_function,
|
|
138
|
+
file_path=file_path,
|
|
139
|
+
line=node.lineno,
|
|
140
|
+
handler_snippet=get_line_snippet(node.lineno),
|
|
141
|
+
confidence="HIGH",
|
|
142
|
+
metadata={"method": "subscribe"},
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Check for KafkaConsumer(topic, ...)
|
|
147
|
+
elif isinstance(node.func, ast.Name) and node.func.id == "KafkaConsumer":
|
|
148
|
+
if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
149
|
+
t = node.args[0].value
|
|
150
|
+
consumers.append(
|
|
151
|
+
AsyncConsumerRecord(
|
|
152
|
+
system=AsyncMessagingSystem.KAFKA,
|
|
153
|
+
destination=t,
|
|
154
|
+
queue_id=f"kafka:{t}",
|
|
155
|
+
handler_canonical_id=self.current_function,
|
|
156
|
+
file_path=file_path,
|
|
157
|
+
line=node.lineno,
|
|
158
|
+
handler_snippet=get_line_snippet(node.lineno),
|
|
159
|
+
confidence="HIGH",
|
|
160
|
+
metadata={"method": "KafkaConsumer"},
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self.generic_visit(node)
|
|
165
|
+
|
|
166
|
+
KafkaCallVisitor().visit(tree)
|
|
167
|
+
|
|
168
|
+
return producers, consumers
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""RabbitMQ / AMQP / Pika message producer and consumer detector.
|
|
2
|
+
|
|
3
|
+
Detects:
|
|
4
|
+
- Publishers / Producers: channel.basic_publish(exchange=..., routing_key=...)
|
|
5
|
+
- Consumers / Workers: channel.basic_consume(queue=..., on_message_callback=...)
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from codegraph.async_queue.models import (
|
|
13
|
+
AsyncConsumerRecord,
|
|
14
|
+
AsyncMessagingSystem,
|
|
15
|
+
AsyncProducerRecord,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def detect_rabbitmq(
|
|
20
|
+
tree: ast.AST,
|
|
21
|
+
content: str,
|
|
22
|
+
file_path: str,
|
|
23
|
+
module_name: str,
|
|
24
|
+
) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
|
|
25
|
+
producers: list[AsyncProducerRecord] = []
|
|
26
|
+
consumers: list[AsyncConsumerRecord] = []
|
|
27
|
+
lines = content.splitlines()
|
|
28
|
+
|
|
29
|
+
def get_line_snippet(lineno: int) -> str:
|
|
30
|
+
if 1 <= lineno <= len(lines):
|
|
31
|
+
return lines[lineno - 1].strip()
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
class RabbitMQVisitor(ast.NodeVisitor):
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self.current_function: str = f"{module_name}.<module>"
|
|
37
|
+
|
|
38
|
+
def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
|
|
39
|
+
prev = self.current_function
|
|
40
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
41
|
+
self.generic_visit(fn)
|
|
42
|
+
self.current_function = prev
|
|
43
|
+
|
|
44
|
+
def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
|
|
45
|
+
prev = self.current_function
|
|
46
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
47
|
+
self.generic_visit(fn)
|
|
48
|
+
self.current_function = prev
|
|
49
|
+
|
|
50
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
51
|
+
if isinstance(node.func, ast.Attribute):
|
|
52
|
+
attr = node.func.attr
|
|
53
|
+
# Producer: basic_publish
|
|
54
|
+
if attr in ("basic_publish", "publish"):
|
|
55
|
+
routing_key = ""
|
|
56
|
+
exchange = ""
|
|
57
|
+
meta: dict[str, Any] = {"method": attr}
|
|
58
|
+
for kw in node.keywords:
|
|
59
|
+
if kw.arg == "routing_key" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
60
|
+
routing_key = kw.value.value
|
|
61
|
+
elif kw.arg == "exchange" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
62
|
+
exchange = kw.value.value
|
|
63
|
+
|
|
64
|
+
dest = routing_key or exchange
|
|
65
|
+
if dest:
|
|
66
|
+
if exchange:
|
|
67
|
+
meta["exchange"] = exchange
|
|
68
|
+
if routing_key:
|
|
69
|
+
meta["routing_key"] = routing_key
|
|
70
|
+
|
|
71
|
+
producers.append(
|
|
72
|
+
AsyncProducerRecord(
|
|
73
|
+
system=AsyncMessagingSystem.RABBITMQ,
|
|
74
|
+
destination=dest,
|
|
75
|
+
queue_id=f"rabbitmq:{dest}",
|
|
76
|
+
caller_canonical_id=self.current_function,
|
|
77
|
+
file_path=file_path,
|
|
78
|
+
line=node.lineno,
|
|
79
|
+
call_snippet=get_line_snippet(node.lineno),
|
|
80
|
+
confidence="HIGH",
|
|
81
|
+
metadata=meta,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Consumer: basic_consume
|
|
86
|
+
elif attr in ("basic_consume", "consume"):
|
|
87
|
+
queue_name = ""
|
|
88
|
+
callback_fn_name = ""
|
|
89
|
+
for kw in node.keywords:
|
|
90
|
+
if kw.arg == "queue" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
91
|
+
queue_name = kw.value.value
|
|
92
|
+
elif kw.arg == "on_message_callback":
|
|
93
|
+
if isinstance(kw.value, ast.Name):
|
|
94
|
+
callback_fn_name = kw.value.id
|
|
95
|
+
|
|
96
|
+
if queue_name:
|
|
97
|
+
handler_id = f"{module_name}.{callback_fn_name}" if callback_fn_name else self.current_function
|
|
98
|
+
consumers.append(
|
|
99
|
+
AsyncConsumerRecord(
|
|
100
|
+
system=AsyncMessagingSystem.RABBITMQ,
|
|
101
|
+
destination=queue_name,
|
|
102
|
+
queue_id=f"rabbitmq:{queue_name}",
|
|
103
|
+
handler_canonical_id=handler_id,
|
|
104
|
+
file_path=file_path,
|
|
105
|
+
line=node.lineno,
|
|
106
|
+
handler_snippet=get_line_snippet(node.lineno),
|
|
107
|
+
confidence="HIGH",
|
|
108
|
+
metadata={"callback": callback_fn_name} if callback_fn_name else {},
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.generic_visit(node)
|
|
113
|
+
|
|
114
|
+
RabbitMQVisitor().visit(tree)
|
|
115
|
+
|
|
116
|
+
return producers, consumers
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Redis Pub/Sub and Streams async message detector.
|
|
2
|
+
|
|
3
|
+
Detects:
|
|
4
|
+
- Publishers / Producers: r.publish(channel, message), r.xadd(stream, fields)
|
|
5
|
+
- Consumers / Workers: pubsub.subscribe(channel), r.xread({stream: id})
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from codegraph.async_queue.models import (
|
|
13
|
+
AsyncConsumerRecord,
|
|
14
|
+
AsyncMessagingSystem,
|
|
15
|
+
AsyncProducerRecord,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def detect_redis(
|
|
20
|
+
tree: ast.AST,
|
|
21
|
+
content: str,
|
|
22
|
+
file_path: str,
|
|
23
|
+
module_name: str,
|
|
24
|
+
) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
|
|
25
|
+
producers: list[AsyncProducerRecord] = []
|
|
26
|
+
consumers: list[AsyncConsumerRecord] = []
|
|
27
|
+
lines = content.splitlines()
|
|
28
|
+
|
|
29
|
+
def get_line_snippet(lineno: int) -> str:
|
|
30
|
+
if 1 <= lineno <= len(lines):
|
|
31
|
+
return lines[lineno - 1].strip()
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
class RedisVisitor(ast.NodeVisitor):
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self.current_function: str = f"{module_name}.<module>"
|
|
37
|
+
|
|
38
|
+
def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
|
|
39
|
+
prev = self.current_function
|
|
40
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
41
|
+
self.generic_visit(fn)
|
|
42
|
+
self.current_function = prev
|
|
43
|
+
|
|
44
|
+
def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
|
|
45
|
+
prev = self.current_function
|
|
46
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
47
|
+
self.generic_visit(fn)
|
|
48
|
+
self.current_function = prev
|
|
49
|
+
|
|
50
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
51
|
+
if isinstance(node.func, ast.Attribute):
|
|
52
|
+
attr = node.func.attr
|
|
53
|
+
# Producer: r.publish("channel", ...), r.xadd("stream", ...)
|
|
54
|
+
if attr in ("publish", "xadd"):
|
|
55
|
+
channel_or_stream = ""
|
|
56
|
+
if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
|
|
57
|
+
channel_or_stream = node.args[0].value
|
|
58
|
+
for kw in node.keywords:
|
|
59
|
+
if kw.arg in ("channel", "name") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
60
|
+
channel_or_stream = kw.value.value
|
|
61
|
+
|
|
62
|
+
if channel_or_stream:
|
|
63
|
+
meta: dict[str, Any] = {"method": attr, "type": "stream" if attr == "xadd" else "pubsub"}
|
|
64
|
+
producers.append(
|
|
65
|
+
AsyncProducerRecord(
|
|
66
|
+
system=AsyncMessagingSystem.REDIS,
|
|
67
|
+
destination=channel_or_stream,
|
|
68
|
+
queue_id=f"redis:{channel_or_stream}",
|
|
69
|
+
caller_canonical_id=self.current_function,
|
|
70
|
+
file_path=file_path,
|
|
71
|
+
line=node.lineno,
|
|
72
|
+
call_snippet=get_line_snippet(node.lineno),
|
|
73
|
+
confidence="HIGH",
|
|
74
|
+
metadata=meta,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Consumer: pubsub.subscribe("channel"), r.xread({"stream": ...})
|
|
79
|
+
elif attr == "subscribe":
|
|
80
|
+
channels: list[str] = []
|
|
81
|
+
for arg in node.args:
|
|
82
|
+
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
|
83
|
+
channels.append(arg.value)
|
|
84
|
+
elif isinstance(arg, (ast.List, ast.Tuple)):
|
|
85
|
+
for elt in arg.elts:
|
|
86
|
+
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
87
|
+
channels.append(elt.value)
|
|
88
|
+
|
|
89
|
+
for ch in channels:
|
|
90
|
+
consumers.append(
|
|
91
|
+
AsyncConsumerRecord(
|
|
92
|
+
system=AsyncMessagingSystem.REDIS,
|
|
93
|
+
destination=ch,
|
|
94
|
+
queue_id=f"redis:{ch}",
|
|
95
|
+
handler_canonical_id=self.current_function,
|
|
96
|
+
file_path=file_path,
|
|
97
|
+
line=node.lineno,
|
|
98
|
+
handler_snippet=get_line_snippet(node.lineno),
|
|
99
|
+
confidence="HIGH",
|
|
100
|
+
metadata={"type": "pubsub"},
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
elif attr == "xread":
|
|
105
|
+
streams: list[str] = []
|
|
106
|
+
if node.args and isinstance(node.args[0], ast.Dict):
|
|
107
|
+
for k in node.args[0].keys:
|
|
108
|
+
if isinstance(k, ast.Constant) and isinstance(k.value, str):
|
|
109
|
+
streams.append(k.value)
|
|
110
|
+
|
|
111
|
+
for st in streams:
|
|
112
|
+
consumers.append(
|
|
113
|
+
AsyncConsumerRecord(
|
|
114
|
+
system=AsyncMessagingSystem.REDIS,
|
|
115
|
+
destination=st,
|
|
116
|
+
queue_id=f"redis:{st}",
|
|
117
|
+
handler_canonical_id=self.current_function,
|
|
118
|
+
file_path=file_path,
|
|
119
|
+
line=node.lineno,
|
|
120
|
+
handler_snippet=get_line_snippet(node.lineno),
|
|
121
|
+
confidence="HIGH",
|
|
122
|
+
metadata={"type": "stream"},
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self.generic_visit(node)
|
|
127
|
+
|
|
128
|
+
RedisVisitor().visit(tree)
|
|
129
|
+
|
|
130
|
+
return producers, consumers
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""AWS SQS message producer and consumer detector.
|
|
2
|
+
|
|
3
|
+
Detects:
|
|
4
|
+
- Publishers / Producers: sqs.send_message(QueueUrl=...), sqs.send_message_batch()
|
|
5
|
+
- Consumers / Workers: sqs.receive_message(QueueUrl=...), @sqs_handler()
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from codegraph.async_queue.models import (
|
|
13
|
+
AsyncConsumerRecord,
|
|
14
|
+
AsyncMessagingSystem,
|
|
15
|
+
AsyncProducerRecord,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _extract_queue_name(url_or_name: str) -> str:
|
|
20
|
+
"""Extract queue name from SQS URL or name string."""
|
|
21
|
+
clean = url_or_name.strip().rstrip("/")
|
|
22
|
+
if "/" in clean:
|
|
23
|
+
return clean.split("/")[-1]
|
|
24
|
+
return clean
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect_sqs(
|
|
28
|
+
tree: ast.AST,
|
|
29
|
+
content: str,
|
|
30
|
+
file_path: str,
|
|
31
|
+
module_name: str,
|
|
32
|
+
) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
|
|
33
|
+
producers: list[AsyncProducerRecord] = []
|
|
34
|
+
consumers: list[AsyncConsumerRecord] = []
|
|
35
|
+
lines = content.splitlines()
|
|
36
|
+
|
|
37
|
+
def get_line_snippet(lineno: int) -> str:
|
|
38
|
+
if 1 <= lineno <= len(lines):
|
|
39
|
+
return lines[lineno - 1].strip()
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
class SQSVisitor(ast.NodeVisitor):
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
self.current_function: str = f"{module_name}.<module>"
|
|
45
|
+
|
|
46
|
+
def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
|
|
47
|
+
prev = self.current_function
|
|
48
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
49
|
+
# Check for decorator like @sqs_handler
|
|
50
|
+
for dec in fn.decorator_list:
|
|
51
|
+
dec_func = dec.func if isinstance(dec, ast.Call) else dec
|
|
52
|
+
dec_name = getattr(dec_func, "id", "") or getattr(dec_func, "attr", "")
|
|
53
|
+
if "sqs" in dec_name.lower():
|
|
54
|
+
q_name = ""
|
|
55
|
+
if isinstance(dec, ast.Call):
|
|
56
|
+
for kw in dec.keywords:
|
|
57
|
+
if kw.arg in ("queue", "queue_name", "queue_url") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
58
|
+
q_name = _extract_queue_name(kw.value.value)
|
|
59
|
+
if q_name:
|
|
60
|
+
consumers.append(
|
|
61
|
+
AsyncConsumerRecord(
|
|
62
|
+
system=AsyncMessagingSystem.AWS_SQS,
|
|
63
|
+
destination=q_name,
|
|
64
|
+
queue_id=f"aws_sqs:{q_name}",
|
|
65
|
+
handler_canonical_id=f"{module_name}.{fn.name}",
|
|
66
|
+
file_path=file_path,
|
|
67
|
+
line=fn.lineno,
|
|
68
|
+
handler_snippet=get_line_snippet(fn.lineno),
|
|
69
|
+
confidence="HIGH",
|
|
70
|
+
metadata={"decorator": dec_name},
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
self.generic_visit(fn)
|
|
74
|
+
self.current_function = prev
|
|
75
|
+
|
|
76
|
+
def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
|
|
77
|
+
prev = self.current_function
|
|
78
|
+
self.current_function = f"{module_name}.{fn.name}"
|
|
79
|
+
self.generic_visit(fn)
|
|
80
|
+
self.current_function = prev
|
|
81
|
+
|
|
82
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
83
|
+
if isinstance(node.func, ast.Attribute):
|
|
84
|
+
attr = node.func.attr
|
|
85
|
+
# Producer: send_message, send_message_batch
|
|
86
|
+
if attr in ("send_message", "send_message_batch"):
|
|
87
|
+
q_name = ""
|
|
88
|
+
meta: dict[str, Any] = {"method": attr}
|
|
89
|
+
for kw in node.keywords:
|
|
90
|
+
if kw.arg == "QueueUrl" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
91
|
+
q_name = _extract_queue_name(kw.value.value)
|
|
92
|
+
elif kw.arg in ("DelaySeconds", "MessageDeduplicationId", "MessageGroupId"):
|
|
93
|
+
meta[kw.arg] = True
|
|
94
|
+
|
|
95
|
+
if q_name:
|
|
96
|
+
producers.append(
|
|
97
|
+
AsyncProducerRecord(
|
|
98
|
+
system=AsyncMessagingSystem.AWS_SQS,
|
|
99
|
+
destination=q_name,
|
|
100
|
+
queue_id=f"aws_sqs:{q_name}",
|
|
101
|
+
caller_canonical_id=self.current_function,
|
|
102
|
+
file_path=file_path,
|
|
103
|
+
line=node.lineno,
|
|
104
|
+
call_snippet=get_line_snippet(node.lineno),
|
|
105
|
+
confidence="HIGH",
|
|
106
|
+
metadata=meta,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Consumer: receive_message
|
|
111
|
+
elif attr == "receive_message":
|
|
112
|
+
q_name = ""
|
|
113
|
+
for kw in node.keywords:
|
|
114
|
+
if kw.arg == "QueueUrl" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
115
|
+
q_name = _extract_queue_name(kw.value.value)
|
|
116
|
+
if q_name:
|
|
117
|
+
consumers.append(
|
|
118
|
+
AsyncConsumerRecord(
|
|
119
|
+
system=AsyncMessagingSystem.AWS_SQS,
|
|
120
|
+
destination=q_name,
|
|
121
|
+
queue_id=f"aws_sqs:{q_name}",
|
|
122
|
+
handler_canonical_id=self.current_function,
|
|
123
|
+
file_path=file_path,
|
|
124
|
+
line=node.lineno,
|
|
125
|
+
handler_snippet=get_line_snippet(node.lineno),
|
|
126
|
+
confidence="HIGH",
|
|
127
|
+
metadata={"method": "receive_message"},
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self.generic_visit(node)
|
|
132
|
+
|
|
133
|
+
SQSVisitor().visit(tree)
|
|
134
|
+
|
|
135
|
+
return producers, consumers
|