osscodeiq 0.0.0__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.
- osscodeiq/__init__.py +0 -0
- osscodeiq/analyzer.py +467 -0
- osscodeiq/cache/__init__.py +0 -0
- osscodeiq/cache/hasher.py +23 -0
- osscodeiq/cache/store.py +300 -0
- osscodeiq/classifiers/__init__.py +0 -0
- osscodeiq/classifiers/layer_classifier.py +69 -0
- osscodeiq/cli.py +721 -0
- osscodeiq/config.py +113 -0
- osscodeiq/detectors/__init__.py +0 -0
- osscodeiq/detectors/auth/__init__.py +0 -0
- osscodeiq/detectors/auth/certificate_auth.py +139 -0
- osscodeiq/detectors/auth/ldap_auth.py +89 -0
- osscodeiq/detectors/auth/session_header_auth.py +120 -0
- osscodeiq/detectors/base.py +41 -0
- osscodeiq/detectors/config/__init__.py +0 -0
- osscodeiq/detectors/config/batch_structure.py +128 -0
- osscodeiq/detectors/config/cloudformation.py +183 -0
- osscodeiq/detectors/config/docker_compose.py +179 -0
- osscodeiq/detectors/config/github_actions.py +150 -0
- osscodeiq/detectors/config/gitlab_ci.py +216 -0
- osscodeiq/detectors/config/helm_chart.py +187 -0
- osscodeiq/detectors/config/ini_structure.py +101 -0
- osscodeiq/detectors/config/json_structure.py +72 -0
- osscodeiq/detectors/config/kubernetes.py +305 -0
- osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
- osscodeiq/detectors/config/openapi.py +194 -0
- osscodeiq/detectors/config/package_json.py +99 -0
- osscodeiq/detectors/config/properties_detector.py +108 -0
- osscodeiq/detectors/config/pyproject_toml.py +169 -0
- osscodeiq/detectors/config/sql_structure.py +155 -0
- osscodeiq/detectors/config/toml_structure.py +93 -0
- osscodeiq/detectors/config/tsconfig_json.py +105 -0
- osscodeiq/detectors/config/yaml_structure.py +82 -0
- osscodeiq/detectors/cpp/__init__.py +0 -0
- osscodeiq/detectors/cpp/cpp_structures.py +192 -0
- osscodeiq/detectors/csharp/__init__.py +0 -0
- osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
- osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
- osscodeiq/detectors/csharp/csharp_structures.py +317 -0
- osscodeiq/detectors/docs/__init__.py +0 -0
- osscodeiq/detectors/docs/markdown_structure.py +117 -0
- osscodeiq/detectors/frontend/__init__.py +0 -0
- osscodeiq/detectors/frontend/angular_components.py +177 -0
- osscodeiq/detectors/frontend/frontend_routes.py +259 -0
- osscodeiq/detectors/frontend/react_components.py +148 -0
- osscodeiq/detectors/frontend/svelte_components.py +84 -0
- osscodeiq/detectors/frontend/vue_components.py +150 -0
- osscodeiq/detectors/generic/__init__.py +1 -0
- osscodeiq/detectors/generic/imports_detector.py +413 -0
- osscodeiq/detectors/go/__init__.py +0 -0
- osscodeiq/detectors/go/go_orm.py +202 -0
- osscodeiq/detectors/go/go_structures.py +162 -0
- osscodeiq/detectors/go/go_web.py +157 -0
- osscodeiq/detectors/iac/__init__.py +0 -0
- osscodeiq/detectors/iac/bicep.py +135 -0
- osscodeiq/detectors/iac/dockerfile.py +182 -0
- osscodeiq/detectors/iac/terraform.py +188 -0
- osscodeiq/detectors/java/__init__.py +0 -0
- osscodeiq/detectors/java/azure_functions.py +424 -0
- osscodeiq/detectors/java/azure_messaging.py +350 -0
- osscodeiq/detectors/java/class_hierarchy.py +349 -0
- osscodeiq/detectors/java/config_def.py +82 -0
- osscodeiq/detectors/java/cosmos_db.py +105 -0
- osscodeiq/detectors/java/graphql_resolver.py +188 -0
- osscodeiq/detectors/java/grpc_service.py +142 -0
- osscodeiq/detectors/java/ibm_mq.py +178 -0
- osscodeiq/detectors/java/jaxrs.py +160 -0
- osscodeiq/detectors/java/jdbc.py +196 -0
- osscodeiq/detectors/java/jms.py +116 -0
- osscodeiq/detectors/java/jpa_entity.py +143 -0
- osscodeiq/detectors/java/kafka.py +113 -0
- osscodeiq/detectors/java/kafka_protocol.py +70 -0
- osscodeiq/detectors/java/micronaut.py +248 -0
- osscodeiq/detectors/java/module_deps.py +191 -0
- osscodeiq/detectors/java/public_api.py +206 -0
- osscodeiq/detectors/java/quarkus.py +176 -0
- osscodeiq/detectors/java/rabbitmq.py +150 -0
- osscodeiq/detectors/java/raw_sql.py +136 -0
- osscodeiq/detectors/java/repository.py +131 -0
- osscodeiq/detectors/java/rmi.py +129 -0
- osscodeiq/detectors/java/spring_events.py +117 -0
- osscodeiq/detectors/java/spring_rest.py +168 -0
- osscodeiq/detectors/java/spring_security.py +212 -0
- osscodeiq/detectors/java/tibco_ems.py +193 -0
- osscodeiq/detectors/java/websocket.py +188 -0
- osscodeiq/detectors/kotlin/__init__.py +0 -0
- osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
- osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
- osscodeiq/detectors/proto/__init__.py +0 -0
- osscodeiq/detectors/proto/proto_structure.py +153 -0
- osscodeiq/detectors/python/__init__.py +0 -0
- osscodeiq/detectors/python/celery_tasks.py +88 -0
- osscodeiq/detectors/python/django_auth.py +132 -0
- osscodeiq/detectors/python/django_models.py +157 -0
- osscodeiq/detectors/python/django_views.py +74 -0
- osscodeiq/detectors/python/fastapi_auth.py +143 -0
- osscodeiq/detectors/python/fastapi_routes.py +68 -0
- osscodeiq/detectors/python/flask_routes.py +67 -0
- osscodeiq/detectors/python/kafka_python.py +175 -0
- osscodeiq/detectors/python/pydantic_models.py +115 -0
- osscodeiq/detectors/python/python_structures.py +234 -0
- osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
- osscodeiq/detectors/registry.py +100 -0
- osscodeiq/detectors/rust/__init__.py +0 -0
- osscodeiq/detectors/rust/actix_web.py +234 -0
- osscodeiq/detectors/rust/rust_structures.py +174 -0
- osscodeiq/detectors/scala/__init__.py +0 -0
- osscodeiq/detectors/scala/scala_structures.py +128 -0
- osscodeiq/detectors/shell/__init__.py +0 -0
- osscodeiq/detectors/shell/bash_detector.py +127 -0
- osscodeiq/detectors/shell/powershell_detector.py +118 -0
- osscodeiq/detectors/typescript/__init__.py +0 -0
- osscodeiq/detectors/typescript/express_routes.py +55 -0
- osscodeiq/detectors/typescript/fastify_routes.py +156 -0
- osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
- osscodeiq/detectors/typescript/kafka_js.py +164 -0
- osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
- osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
- osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
- osscodeiq/detectors/typescript/passport_jwt.py +133 -0
- osscodeiq/detectors/typescript/prisma_orm.py +96 -0
- osscodeiq/detectors/typescript/remix_routes.py +160 -0
- osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
- osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
- osscodeiq/detectors/typescript/typescript_structures.py +185 -0
- osscodeiq/detectors/utils.py +49 -0
- osscodeiq/discovery/__init__.py +11 -0
- osscodeiq/discovery/change_detector.py +97 -0
- osscodeiq/discovery/file_discovery.py +342 -0
- osscodeiq/flow/__init__.py +0 -0
- osscodeiq/flow/engine.py +78 -0
- osscodeiq/flow/models.py +72 -0
- osscodeiq/flow/renderer.py +127 -0
- osscodeiq/flow/templates/interactive.html +252 -0
- osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
- osscodeiq/flow/vendor/cytoscape.min.js +32 -0
- osscodeiq/flow/vendor/dagre.min.js +3809 -0
- osscodeiq/flow/views.py +357 -0
- osscodeiq/graph/__init__.py +0 -0
- osscodeiq/graph/backend.py +52 -0
- osscodeiq/graph/backends/__init__.py +23 -0
- osscodeiq/graph/backends/kuzu.py +576 -0
- osscodeiq/graph/backends/networkx.py +135 -0
- osscodeiq/graph/backends/sqlite_backend.py +406 -0
- osscodeiq/graph/builder.py +297 -0
- osscodeiq/graph/query.py +228 -0
- osscodeiq/graph/store.py +183 -0
- osscodeiq/graph/views.py +231 -0
- osscodeiq/models/__init__.py +17 -0
- osscodeiq/models/graph.py +116 -0
- osscodeiq/output/__init__.py +0 -0
- osscodeiq/output/dot.py +171 -0
- osscodeiq/output/mermaid.py +160 -0
- osscodeiq/output/safety.py +58 -0
- osscodeiq/output/serializers.py +42 -0
- osscodeiq/parsing/__init__.py +5 -0
- osscodeiq/parsing/languages/__init__.py +0 -0
- osscodeiq/parsing/languages/base.py +23 -0
- osscodeiq/parsing/languages/java.py +68 -0
- osscodeiq/parsing/languages/python.py +57 -0
- osscodeiq/parsing/languages/typescript.py +95 -0
- osscodeiq/parsing/parser_manager.py +125 -0
- osscodeiq/parsing/structured/__init__.py +0 -0
- osscodeiq/parsing/structured/gradle_parser.py +78 -0
- osscodeiq/parsing/structured/json_parser.py +24 -0
- osscodeiq/parsing/structured/properties_parser.py +56 -0
- osscodeiq/parsing/structured/sql_parser.py +54 -0
- osscodeiq/parsing/structured/xml_parser.py +148 -0
- osscodeiq/parsing/structured/yaml_parser.py +38 -0
- osscodeiq/server/__init__.py +7 -0
- osscodeiq/server/app.py +53 -0
- osscodeiq/server/mcp_server.py +174 -0
- osscodeiq/server/middleware.py +16 -0
- osscodeiq/server/routes.py +184 -0
- osscodeiq/server/service.py +445 -0
- osscodeiq/server/templates/welcome.html +56 -0
- osscodeiq-0.0.0.dist-info/METADATA +30 -0
- osscodeiq-0.0.0.dist-info/RECORD +183 -0
- osscodeiq-0.0.0.dist-info/WHEEL +5 -0
- osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
- osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
- osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Azure Service Bus and Event Hub detector for Java and TypeScript source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.detectors.utils import decode_text
|
|
9
|
+
from osscodeiq.models.graph import (
|
|
10
|
+
EdgeKind,
|
|
11
|
+
GraphEdge,
|
|
12
|
+
GraphNode,
|
|
13
|
+
NodeKind,
|
|
14
|
+
SourceLocation,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_CLASS_RE = re.compile(r"(?:public\s+)?class\s+(\w+)")
|
|
18
|
+
|
|
19
|
+
# TypeScript/JavaScript class or const pattern
|
|
20
|
+
_TS_CLASS_RE = re.compile(r"(?:export\s+)?class\s+(\w+)")
|
|
21
|
+
_TS_CONST_RE = re.compile(r"(?:export\s+)?(?:const|let|var)\s+(\w+)")
|
|
22
|
+
|
|
23
|
+
# --- Java Azure Service Bus SDK ---
|
|
24
|
+
_SB_SENDER_CLIENT_RE = re.compile(r'\bServiceBusSenderClient\b')
|
|
25
|
+
_SB_RECEIVER_CLIENT_RE = re.compile(r'\bServiceBusReceiverClient\b')
|
|
26
|
+
_SB_PROCESSOR_CLIENT_RE = re.compile(r'\bServiceBusProcessorClient\b')
|
|
27
|
+
_SB_CLIENT_RE = re.compile(r'\bServiceBusClient\b')
|
|
28
|
+
_SB_CLIENT_BUILDER_RE = re.compile(r'\bServiceBusClientBuilder\b')
|
|
29
|
+
|
|
30
|
+
# --- JS/TS Azure Service Bus SDK ---
|
|
31
|
+
_SB_SENDER_JS_RE = re.compile(r'\bServiceBusSender\b')
|
|
32
|
+
_SB_RECEIVER_JS_RE = re.compile(r'\bServiceBusReceiver\b')
|
|
33
|
+
|
|
34
|
+
# --- Azure Event Hub (both Java and JS/TS) ---
|
|
35
|
+
_EH_PRODUCER_RE = re.compile(r'\bEventHubProducerClient\b')
|
|
36
|
+
_EH_CONSUMER_RE = re.compile(r'\bEventHubConsumerClient\b')
|
|
37
|
+
_EH_PROCESSOR_RE = re.compile(r'\bEventProcessorClient\b')
|
|
38
|
+
|
|
39
|
+
# --- Azure Functions trigger annotations ---
|
|
40
|
+
_SB_QUEUE_TRIGGER_RE = re.compile(r'@ServiceBusQueueTrigger\s*\([^)]*name\s*=\s*"([^"]*)"')
|
|
41
|
+
_SB_TOPIC_TRIGGER_RE = re.compile(r'@ServiceBusTopicTrigger\s*\([^)]*name\s*=\s*"([^"]*)"')
|
|
42
|
+
_EH_TRIGGER_RE = re.compile(r'@EventHubTrigger\s*\([^)]*name\s*=\s*"([^"]*)"')
|
|
43
|
+
|
|
44
|
+
# Generic trigger annotations (looser match)
|
|
45
|
+
_SB_QUEUE_TRIGGER_LOOSE_RE = re.compile(r'@ServiceBusQueueTrigger')
|
|
46
|
+
_SB_TOPIC_TRIGGER_LOOSE_RE = re.compile(r'@ServiceBusTopicTrigger')
|
|
47
|
+
_EH_TRIGGER_LOOSE_RE = re.compile(r'@EventHubTrigger')
|
|
48
|
+
|
|
49
|
+
# Queue/topic name extraction from builder patterns
|
|
50
|
+
_QUEUE_NAME_RE = re.compile(r'(?:queueName|queue)\s*\(\s*"([^"]+)"')
|
|
51
|
+
_TOPIC_NAME_RE = re.compile(r'(?:topicName|topic)\s*\(\s*"([^"]+)"')
|
|
52
|
+
_EH_NAME_RE = re.compile(r'(?:eventHubName|eventHub)\s*\(\s*"([^"]+)"')
|
|
53
|
+
|
|
54
|
+
# String literal near ServiceBus/EventHub patterns
|
|
55
|
+
_STRING_LITERAL_RE = re.compile(r'"([^"]+)"')
|
|
56
|
+
|
|
57
|
+
# JS/TS createSender/createReceiver with queue/topic name
|
|
58
|
+
_JS_CREATE_SENDER_RE = re.compile(r'createSender\s*\(\s*"([^"]+)"')
|
|
59
|
+
_JS_CREATE_RECEIVER_RE = re.compile(r'createReceiver\s*\(\s*"([^"]+)"')
|
|
60
|
+
|
|
61
|
+
# JS/TS Service Bus subscription
|
|
62
|
+
_JS_SUBSCRIBE_RE = re.compile(r'subscribe\s*\(')
|
|
63
|
+
|
|
64
|
+
# JS/TS EventHub
|
|
65
|
+
_JS_EH_SEND_RE = re.compile(r'sendBatch\s*\(')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AzureMessagingDetector:
|
|
69
|
+
"""Detects Azure Service Bus and Event Hub usage in Java and TypeScript source files."""
|
|
70
|
+
|
|
71
|
+
name: str = "azure_messaging"
|
|
72
|
+
supported_languages: tuple[str, ...] = ("java", "typescript", "javascript")
|
|
73
|
+
|
|
74
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
75
|
+
result = DetectorResult()
|
|
76
|
+
text = decode_text(ctx)
|
|
77
|
+
lines = text.split("\n")
|
|
78
|
+
|
|
79
|
+
if "ServiceBus" not in text and "EventHub" not in text and "azure-messaging" not in text and "@azure/service-bus" not in text and "@azure/event-hubs" not in text:
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
# Find class/module name depending on language
|
|
83
|
+
class_name: str | None = None
|
|
84
|
+
if ctx.language in ("typescript", "javascript"):
|
|
85
|
+
for line in lines:
|
|
86
|
+
cm = _TS_CLASS_RE.search(line)
|
|
87
|
+
if cm:
|
|
88
|
+
class_name = cm.group(1)
|
|
89
|
+
break
|
|
90
|
+
if not class_name:
|
|
91
|
+
# Fall back to file name
|
|
92
|
+
class_name = ctx.file_path.rsplit("/", 1)[-1].rsplit(".", 1)[0]
|
|
93
|
+
else:
|
|
94
|
+
for line in lines:
|
|
95
|
+
cm = _CLASS_RE.search(line)
|
|
96
|
+
if cm:
|
|
97
|
+
class_name = cm.group(1)
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
if not class_name:
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
class_node_id = f"{ctx.file_path}:{class_name}"
|
|
104
|
+
seen_sb_queues: set[str] = set()
|
|
105
|
+
seen_sb_topics: set[str] = set()
|
|
106
|
+
seen_event_hubs: set[str] = set()
|
|
107
|
+
|
|
108
|
+
def _ensure_sb_queue_node(name: str) -> str:
|
|
109
|
+
node_id = f"azure:servicebus:{name}"
|
|
110
|
+
if name not in seen_sb_queues:
|
|
111
|
+
seen_sb_queues.add(name)
|
|
112
|
+
result.nodes.append(GraphNode(
|
|
113
|
+
id=node_id,
|
|
114
|
+
kind=NodeKind.QUEUE,
|
|
115
|
+
label=f"azure:servicebus:{name}",
|
|
116
|
+
properties={"broker": "azure_servicebus", "queue": name},
|
|
117
|
+
))
|
|
118
|
+
return node_id
|
|
119
|
+
|
|
120
|
+
def _ensure_sb_topic_node(name: str) -> str:
|
|
121
|
+
node_id = f"azure:servicebus:{name}"
|
|
122
|
+
if name not in seen_sb_topics:
|
|
123
|
+
seen_sb_topics.add(name)
|
|
124
|
+
result.nodes.append(GraphNode(
|
|
125
|
+
id=node_id,
|
|
126
|
+
kind=NodeKind.TOPIC,
|
|
127
|
+
label=f"azure:servicebus:{name}",
|
|
128
|
+
properties={"broker": "azure_servicebus", "topic": name},
|
|
129
|
+
))
|
|
130
|
+
return node_id
|
|
131
|
+
|
|
132
|
+
def _ensure_eventhub_node(name: str) -> str:
|
|
133
|
+
node_id = f"azure:eventhub:{name}"
|
|
134
|
+
if name not in seen_event_hubs:
|
|
135
|
+
seen_event_hubs.add(name)
|
|
136
|
+
result.nodes.append(GraphNode(
|
|
137
|
+
id=node_id,
|
|
138
|
+
kind=NodeKind.TOPIC,
|
|
139
|
+
label=f"azure:eventhub:{name}",
|
|
140
|
+
properties={"broker": "azure_eventhub", "event_hub": name},
|
|
141
|
+
))
|
|
142
|
+
return node_id
|
|
143
|
+
|
|
144
|
+
# Determine producer/consumer role from class patterns
|
|
145
|
+
is_sb_sender = bool(
|
|
146
|
+
_SB_SENDER_CLIENT_RE.search(text)
|
|
147
|
+
or _SB_SENDER_JS_RE.search(text)
|
|
148
|
+
)
|
|
149
|
+
is_sb_receiver = bool(
|
|
150
|
+
_SB_RECEIVER_CLIENT_RE.search(text)
|
|
151
|
+
or _SB_PROCESSOR_CLIENT_RE.search(text)
|
|
152
|
+
or _SB_RECEIVER_JS_RE.search(text)
|
|
153
|
+
)
|
|
154
|
+
is_eh_producer = bool(_EH_PRODUCER_RE.search(text))
|
|
155
|
+
is_eh_consumer = bool(
|
|
156
|
+
_EH_CONSUMER_RE.search(text)
|
|
157
|
+
or _EH_PROCESSOR_RE.search(text)
|
|
158
|
+
)
|
|
159
|
+
has_sb_client = bool(_SB_CLIENT_RE.search(text) or _SB_CLIENT_BUILDER_RE.search(text))
|
|
160
|
+
|
|
161
|
+
# Extract queue names from builder patterns
|
|
162
|
+
queue_names: list[str] = []
|
|
163
|
+
topic_names: list[str] = []
|
|
164
|
+
eh_names: list[str] = []
|
|
165
|
+
|
|
166
|
+
for line in lines:
|
|
167
|
+
m = _QUEUE_NAME_RE.search(line)
|
|
168
|
+
if m:
|
|
169
|
+
queue_names.append(m.group(1))
|
|
170
|
+
|
|
171
|
+
m = _TOPIC_NAME_RE.search(line)
|
|
172
|
+
if m:
|
|
173
|
+
topic_names.append(m.group(1))
|
|
174
|
+
|
|
175
|
+
m = _EH_NAME_RE.search(line)
|
|
176
|
+
if m:
|
|
177
|
+
eh_names.append(m.group(1))
|
|
178
|
+
|
|
179
|
+
# JS/TS createSender / createReceiver with queue/topic name
|
|
180
|
+
for line in lines:
|
|
181
|
+
m = _JS_CREATE_SENDER_RE.search(line)
|
|
182
|
+
if m:
|
|
183
|
+
queue_names.append(m.group(1))
|
|
184
|
+
is_sb_sender = True
|
|
185
|
+
|
|
186
|
+
m = _JS_CREATE_RECEIVER_RE.search(line)
|
|
187
|
+
if m:
|
|
188
|
+
queue_names.append(m.group(1))
|
|
189
|
+
is_sb_receiver = True
|
|
190
|
+
|
|
191
|
+
# Azure Functions trigger annotations
|
|
192
|
+
for line in lines:
|
|
193
|
+
m = _SB_QUEUE_TRIGGER_RE.search(line)
|
|
194
|
+
if m:
|
|
195
|
+
queue_names.append(m.group(1))
|
|
196
|
+
is_sb_receiver = True
|
|
197
|
+
|
|
198
|
+
m = _SB_TOPIC_TRIGGER_RE.search(line)
|
|
199
|
+
if m:
|
|
200
|
+
topic_names.append(m.group(1))
|
|
201
|
+
is_sb_receiver = True
|
|
202
|
+
|
|
203
|
+
m = _EH_TRIGGER_RE.search(line)
|
|
204
|
+
if m:
|
|
205
|
+
eh_names.append(m.group(1))
|
|
206
|
+
is_eh_consumer = True
|
|
207
|
+
|
|
208
|
+
# Loose trigger detection (without name extraction)
|
|
209
|
+
if _SB_QUEUE_TRIGGER_LOOSE_RE.search(line) and not _SB_QUEUE_TRIGGER_RE.search(line):
|
|
210
|
+
is_sb_receiver = True
|
|
211
|
+
if _SB_TOPIC_TRIGGER_LOOSE_RE.search(line) and not _SB_TOPIC_TRIGGER_RE.search(line):
|
|
212
|
+
is_sb_receiver = True
|
|
213
|
+
if _EH_TRIGGER_LOOSE_RE.search(line) and not _EH_TRIGGER_RE.search(line):
|
|
214
|
+
is_eh_consumer = True
|
|
215
|
+
|
|
216
|
+
# Create Service Bus queue nodes and edges
|
|
217
|
+
for qname in queue_names:
|
|
218
|
+
queue_id = _ensure_sb_queue_node(qname)
|
|
219
|
+
if is_sb_sender:
|
|
220
|
+
result.edges.append(GraphEdge(
|
|
221
|
+
source=class_node_id,
|
|
222
|
+
target=queue_id,
|
|
223
|
+
kind=EdgeKind.SENDS_TO,
|
|
224
|
+
label=f"{class_name} sends to {qname}",
|
|
225
|
+
properties={"queue": qname},
|
|
226
|
+
))
|
|
227
|
+
if is_sb_receiver:
|
|
228
|
+
result.edges.append(GraphEdge(
|
|
229
|
+
source=class_node_id,
|
|
230
|
+
target=queue_id,
|
|
231
|
+
kind=EdgeKind.RECEIVES_FROM,
|
|
232
|
+
label=f"{class_name} receives from {qname}",
|
|
233
|
+
properties={"queue": qname},
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
# Create Service Bus topic nodes and edges
|
|
237
|
+
for tname in topic_names:
|
|
238
|
+
topic_id = _ensure_sb_topic_node(tname)
|
|
239
|
+
if is_sb_sender:
|
|
240
|
+
result.edges.append(GraphEdge(
|
|
241
|
+
source=class_node_id,
|
|
242
|
+
target=topic_id,
|
|
243
|
+
kind=EdgeKind.SENDS_TO,
|
|
244
|
+
label=f"{class_name} sends to {tname}",
|
|
245
|
+
properties={"topic": tname},
|
|
246
|
+
))
|
|
247
|
+
if is_sb_receiver:
|
|
248
|
+
result.edges.append(GraphEdge(
|
|
249
|
+
source=class_node_id,
|
|
250
|
+
target=topic_id,
|
|
251
|
+
kind=EdgeKind.RECEIVES_FROM,
|
|
252
|
+
label=f"{class_name} receives from {tname}",
|
|
253
|
+
properties={"topic": tname},
|
|
254
|
+
))
|
|
255
|
+
|
|
256
|
+
# Create Event Hub nodes and edges
|
|
257
|
+
for ehname in eh_names:
|
|
258
|
+
eh_id = _ensure_eventhub_node(ehname)
|
|
259
|
+
if is_eh_producer:
|
|
260
|
+
result.edges.append(GraphEdge(
|
|
261
|
+
source=class_node_id,
|
|
262
|
+
target=eh_id,
|
|
263
|
+
kind=EdgeKind.SENDS_TO,
|
|
264
|
+
label=f"{class_name} sends to {ehname}",
|
|
265
|
+
properties={"event_hub": ehname},
|
|
266
|
+
))
|
|
267
|
+
if is_eh_consumer:
|
|
268
|
+
result.edges.append(GraphEdge(
|
|
269
|
+
source=class_node_id,
|
|
270
|
+
target=eh_id,
|
|
271
|
+
kind=EdgeKind.RECEIVES_FROM,
|
|
272
|
+
label=f"{class_name} receives from {ehname}",
|
|
273
|
+
properties={"event_hub": ehname},
|
|
274
|
+
))
|
|
275
|
+
|
|
276
|
+
# If we detected SDK usage but no explicit names, create generic nodes
|
|
277
|
+
# to at least show the dependency
|
|
278
|
+
if is_sb_sender and not queue_names and not topic_names:
|
|
279
|
+
result.nodes.append(GraphNode(
|
|
280
|
+
id="azure:servicebus:__sender__",
|
|
281
|
+
kind=NodeKind.QUEUE,
|
|
282
|
+
label="azure:servicebus:sender",
|
|
283
|
+
properties={"broker": "azure_servicebus", "role": "sender"},
|
|
284
|
+
))
|
|
285
|
+
result.edges.append(GraphEdge(
|
|
286
|
+
source=class_node_id,
|
|
287
|
+
target="azure:servicebus:__sender__",
|
|
288
|
+
kind=EdgeKind.SENDS_TO,
|
|
289
|
+
label=f"{class_name} sends to Azure Service Bus",
|
|
290
|
+
properties={},
|
|
291
|
+
))
|
|
292
|
+
elif is_sb_receiver and not queue_names and not topic_names:
|
|
293
|
+
result.nodes.append(GraphNode(
|
|
294
|
+
id="azure:servicebus:__receiver__",
|
|
295
|
+
kind=NodeKind.QUEUE,
|
|
296
|
+
label="azure:servicebus:receiver",
|
|
297
|
+
properties={"broker": "azure_servicebus", "role": "receiver"},
|
|
298
|
+
))
|
|
299
|
+
result.edges.append(GraphEdge(
|
|
300
|
+
source=class_node_id,
|
|
301
|
+
target="azure:servicebus:__receiver__",
|
|
302
|
+
kind=EdgeKind.RECEIVES_FROM,
|
|
303
|
+
label=f"{class_name} receives from Azure Service Bus",
|
|
304
|
+
properties={},
|
|
305
|
+
))
|
|
306
|
+
elif has_sb_client and not queue_names and not topic_names and not is_sb_sender and not is_sb_receiver:
|
|
307
|
+
result.nodes.append(GraphNode(
|
|
308
|
+
id="azure:servicebus:__client__",
|
|
309
|
+
kind=NodeKind.QUEUE,
|
|
310
|
+
label="azure:servicebus:client",
|
|
311
|
+
properties={"broker": "azure_servicebus", "role": "client"},
|
|
312
|
+
))
|
|
313
|
+
result.edges.append(GraphEdge(
|
|
314
|
+
source=class_node_id,
|
|
315
|
+
target="azure:servicebus:__client__",
|
|
316
|
+
kind=EdgeKind.CONNECTS_TO,
|
|
317
|
+
label=f"{class_name} connects to Azure Service Bus",
|
|
318
|
+
properties={},
|
|
319
|
+
))
|
|
320
|
+
|
|
321
|
+
if is_eh_producer and not eh_names:
|
|
322
|
+
result.nodes.append(GraphNode(
|
|
323
|
+
id="azure:eventhub:__producer__",
|
|
324
|
+
kind=NodeKind.TOPIC,
|
|
325
|
+
label="azure:eventhub:producer",
|
|
326
|
+
properties={"broker": "azure_eventhub", "role": "producer"},
|
|
327
|
+
))
|
|
328
|
+
result.edges.append(GraphEdge(
|
|
329
|
+
source=class_node_id,
|
|
330
|
+
target="azure:eventhub:__producer__",
|
|
331
|
+
kind=EdgeKind.SENDS_TO,
|
|
332
|
+
label=f"{class_name} sends to Azure Event Hub",
|
|
333
|
+
properties={},
|
|
334
|
+
))
|
|
335
|
+
elif is_eh_consumer and not eh_names:
|
|
336
|
+
result.nodes.append(GraphNode(
|
|
337
|
+
id="azure:eventhub:__consumer__",
|
|
338
|
+
kind=NodeKind.TOPIC,
|
|
339
|
+
label="azure:eventhub:consumer",
|
|
340
|
+
properties={"broker": "azure_eventhub", "role": "consumer"},
|
|
341
|
+
))
|
|
342
|
+
result.edges.append(GraphEdge(
|
|
343
|
+
source=class_node_id,
|
|
344
|
+
target="azure:eventhub:__consumer__",
|
|
345
|
+
kind=EdgeKind.RECEIVES_FROM,
|
|
346
|
+
label=f"{class_name} receives from Azure Event Hub",
|
|
347
|
+
properties={},
|
|
348
|
+
))
|
|
349
|
+
|
|
350
|
+
return result
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Tree-sitter-based Java class hierarchy detector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.models.graph import (
|
|
9
|
+
EdgeKind,
|
|
10
|
+
GraphEdge,
|
|
11
|
+
GraphNode,
|
|
12
|
+
NodeKind,
|
|
13
|
+
SourceLocation,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _extract_type_name(node) -> str | None:
|
|
18
|
+
"""Extract a simple type name from a tree-sitter type node.
|
|
19
|
+
|
|
20
|
+
Handles plain identifiers, scoped_type_identifier, and generic_type nodes.
|
|
21
|
+
"""
|
|
22
|
+
if node is None:
|
|
23
|
+
return None
|
|
24
|
+
if node.type == "type_identifier":
|
|
25
|
+
return node.text.decode()
|
|
26
|
+
if node.type == "scoped_type_identifier":
|
|
27
|
+
return node.text.decode()
|
|
28
|
+
if node.type == "generic_type":
|
|
29
|
+
# The first child is the raw type identifier
|
|
30
|
+
for child in node.children:
|
|
31
|
+
if child.type in ("type_identifier", "scoped_type_identifier"):
|
|
32
|
+
return child.text.decode()
|
|
33
|
+
# Fallback: walk children for a type_identifier
|
|
34
|
+
for child in node.children:
|
|
35
|
+
name = _extract_type_name(child)
|
|
36
|
+
if name is not None:
|
|
37
|
+
return name
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _collect_type_names_from_type_list(node) -> list[str]:
|
|
42
|
+
"""Collect all type names from a type_list node."""
|
|
43
|
+
names: list[str] = []
|
|
44
|
+
if node is None:
|
|
45
|
+
return names
|
|
46
|
+
for child in node.children:
|
|
47
|
+
if child.type in ("type_identifier", "generic_type", "scoped_type_identifier"):
|
|
48
|
+
name = _extract_type_name(child)
|
|
49
|
+
if name:
|
|
50
|
+
names.append(name)
|
|
51
|
+
return names
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _collect_type_names(container_node) -> list[str]:
|
|
55
|
+
"""Collect type names from a superclass, super_interfaces, or extends_interfaces node.
|
|
56
|
+
|
|
57
|
+
These container nodes hold a ``type_list`` child which in turn holds the
|
|
58
|
+
actual type identifier children.
|
|
59
|
+
"""
|
|
60
|
+
if container_node is None:
|
|
61
|
+
return []
|
|
62
|
+
for child in container_node.children:
|
|
63
|
+
if child.type == "type_list":
|
|
64
|
+
return _collect_type_names_from_type_list(child)
|
|
65
|
+
# Fallback: try the container itself (e.g. if it *is* the type_list)
|
|
66
|
+
return _collect_type_names_from_type_list(container_node)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _find_child_by_type(node, type_name: str):
|
|
70
|
+
"""Find the first direct child with the given node type."""
|
|
71
|
+
for child in node.children:
|
|
72
|
+
if child.type == type_name:
|
|
73
|
+
return child
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _has_modifier(modifiers_node, modifier_type: str) -> bool:
|
|
78
|
+
"""Check if a modifiers node contains a specific modifier keyword."""
|
|
79
|
+
if modifiers_node is None:
|
|
80
|
+
return False
|
|
81
|
+
for child in modifiers_node.children:
|
|
82
|
+
if child.type == modifier_type:
|
|
83
|
+
return True
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_visibility(modifiers_node) -> str:
|
|
88
|
+
"""Extract visibility from modifiers (public, protected, private, or package-private)."""
|
|
89
|
+
if modifiers_node is None:
|
|
90
|
+
return "package-private"
|
|
91
|
+
for child in modifiers_node.children:
|
|
92
|
+
if child.type in ("public", "protected", "private"):
|
|
93
|
+
return child.type
|
|
94
|
+
return "package-private"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ClassHierarchyDetector:
|
|
98
|
+
"""Detects Java class hierarchies using tree-sitter AST."""
|
|
99
|
+
|
|
100
|
+
name: str = "java.class_hierarchy"
|
|
101
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
102
|
+
|
|
103
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
104
|
+
result = DetectorResult()
|
|
105
|
+
if ctx.tree is None:
|
|
106
|
+
return result
|
|
107
|
+
self._walk(ctx.tree.root_node, ctx, result, prefix="")
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def _walk(self, node, ctx: DetectorContext, result: DetectorResult, prefix: str) -> None:
|
|
111
|
+
"""Recursively walk AST nodes looking for type declarations."""
|
|
112
|
+
for child in node.children:
|
|
113
|
+
if child.type == "class_declaration":
|
|
114
|
+
self._process_class(child, ctx, result, prefix)
|
|
115
|
+
elif child.type == "interface_declaration":
|
|
116
|
+
self._process_interface(child, ctx, result, prefix)
|
|
117
|
+
elif child.type == "enum_declaration":
|
|
118
|
+
self._process_enum(child, ctx, result, prefix)
|
|
119
|
+
elif child.type == "annotation_type_declaration":
|
|
120
|
+
self._process_annotation_type(child, ctx, result, prefix)
|
|
121
|
+
|
|
122
|
+
def _process_class(
|
|
123
|
+
self, node, ctx: DetectorContext, result: DetectorResult, prefix: str
|
|
124
|
+
) -> None:
|
|
125
|
+
name_node = node.child_by_field_name("name")
|
|
126
|
+
if name_node is None:
|
|
127
|
+
return
|
|
128
|
+
simple_name = name_node.text.decode()
|
|
129
|
+
qualified_name = f"{prefix}{simple_name}" if not prefix else f"{prefix}.{simple_name}"
|
|
130
|
+
|
|
131
|
+
modifiers = _find_child_by_type(node, "modifiers")
|
|
132
|
+
is_abstract = _has_modifier(modifiers, "abstract")
|
|
133
|
+
is_final = _has_modifier(modifiers, "final")
|
|
134
|
+
visibility = _get_visibility(modifiers)
|
|
135
|
+
|
|
136
|
+
kind = NodeKind.ABSTRACT_CLASS if is_abstract else NodeKind.CLASS
|
|
137
|
+
node_id = f"{ctx.file_path}:{qualified_name}"
|
|
138
|
+
|
|
139
|
+
# Superclass (child_by_field_name works for "superclass")
|
|
140
|
+
superclass_name: str | None = None
|
|
141
|
+
superclass_node = node.child_by_field_name("superclass")
|
|
142
|
+
if superclass_node is not None:
|
|
143
|
+
superclass_name = _extract_type_name(superclass_node)
|
|
144
|
+
|
|
145
|
+
# Interfaces (field "interfaces" maps to super_interfaces node)
|
|
146
|
+
interfaces_node = _find_child_by_type(node, "super_interfaces")
|
|
147
|
+
interface_names = _collect_type_names(interfaces_node)
|
|
148
|
+
|
|
149
|
+
properties: dict[str, Any] = {
|
|
150
|
+
"visibility": visibility,
|
|
151
|
+
"is_abstract": is_abstract,
|
|
152
|
+
"is_final": is_final,
|
|
153
|
+
}
|
|
154
|
+
if superclass_name:
|
|
155
|
+
properties["superclass"] = superclass_name
|
|
156
|
+
if interface_names:
|
|
157
|
+
properties["interfaces"] = interface_names
|
|
158
|
+
|
|
159
|
+
graph_node = GraphNode(
|
|
160
|
+
id=node_id,
|
|
161
|
+
kind=kind,
|
|
162
|
+
label=qualified_name,
|
|
163
|
+
fqn=qualified_name,
|
|
164
|
+
module=ctx.module_name,
|
|
165
|
+
location=SourceLocation(
|
|
166
|
+
file_path=ctx.file_path,
|
|
167
|
+
line_start=name_node.start_point[0] + 1,
|
|
168
|
+
),
|
|
169
|
+
properties=properties,
|
|
170
|
+
)
|
|
171
|
+
result.nodes.append(graph_node)
|
|
172
|
+
|
|
173
|
+
# Edges for extends
|
|
174
|
+
if superclass_name:
|
|
175
|
+
result.edges.append(
|
|
176
|
+
GraphEdge(
|
|
177
|
+
source=node_id,
|
|
178
|
+
target=f"*:{superclass_name}",
|
|
179
|
+
kind=EdgeKind.EXTENDS,
|
|
180
|
+
label=f"{qualified_name} extends {superclass_name}",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Edges for implements
|
|
185
|
+
for iface in interface_names:
|
|
186
|
+
result.edges.append(
|
|
187
|
+
GraphEdge(
|
|
188
|
+
source=node_id,
|
|
189
|
+
target=f"*:{iface}",
|
|
190
|
+
kind=EdgeKind.IMPLEMENTS,
|
|
191
|
+
label=f"{qualified_name} implements {iface}",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Recurse into class body for nested types
|
|
196
|
+
body = node.child_by_field_name("body")
|
|
197
|
+
if body is not None:
|
|
198
|
+
self._walk(body, ctx, result, prefix=qualified_name)
|
|
199
|
+
|
|
200
|
+
def _process_interface(
|
|
201
|
+
self, node, ctx: DetectorContext, result: DetectorResult, prefix: str
|
|
202
|
+
) -> None:
|
|
203
|
+
name_node = node.child_by_field_name("name")
|
|
204
|
+
if name_node is None:
|
|
205
|
+
return
|
|
206
|
+
simple_name = name_node.text.decode()
|
|
207
|
+
qualified_name = f"{prefix}{simple_name}" if not prefix else f"{prefix}.{simple_name}"
|
|
208
|
+
|
|
209
|
+
modifiers = _find_child_by_type(node, "modifiers")
|
|
210
|
+
visibility = _get_visibility(modifiers)
|
|
211
|
+
|
|
212
|
+
node_id = f"{ctx.file_path}:{qualified_name}"
|
|
213
|
+
|
|
214
|
+
# Extended interfaces (not a field; must walk children for the node type)
|
|
215
|
+
extends_node = _find_child_by_type(node, "extends_interfaces")
|
|
216
|
+
extended_names = _collect_type_names(extends_node)
|
|
217
|
+
|
|
218
|
+
properties: dict[str, Any] = {
|
|
219
|
+
"visibility": visibility,
|
|
220
|
+
"is_abstract": False,
|
|
221
|
+
"is_final": False,
|
|
222
|
+
}
|
|
223
|
+
if extended_names:
|
|
224
|
+
properties["interfaces"] = extended_names
|
|
225
|
+
|
|
226
|
+
graph_node = GraphNode(
|
|
227
|
+
id=node_id,
|
|
228
|
+
kind=NodeKind.INTERFACE,
|
|
229
|
+
label=qualified_name,
|
|
230
|
+
fqn=qualified_name,
|
|
231
|
+
module=ctx.module_name,
|
|
232
|
+
location=SourceLocation(
|
|
233
|
+
file_path=ctx.file_path,
|
|
234
|
+
line_start=name_node.start_point[0] + 1,
|
|
235
|
+
),
|
|
236
|
+
properties=properties,
|
|
237
|
+
)
|
|
238
|
+
result.nodes.append(graph_node)
|
|
239
|
+
|
|
240
|
+
# Edges for extended interfaces
|
|
241
|
+
for ext in extended_names:
|
|
242
|
+
result.edges.append(
|
|
243
|
+
GraphEdge(
|
|
244
|
+
source=node_id,
|
|
245
|
+
target=f"*:{ext}",
|
|
246
|
+
kind=EdgeKind.EXTENDS,
|
|
247
|
+
label=f"{qualified_name} extends {ext}",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Recurse into interface body for nested types
|
|
252
|
+
body = node.child_by_field_name("body")
|
|
253
|
+
if body is not None:
|
|
254
|
+
self._walk(body, ctx, result, prefix=qualified_name)
|
|
255
|
+
|
|
256
|
+
def _process_enum(
|
|
257
|
+
self, node, ctx: DetectorContext, result: DetectorResult, prefix: str
|
|
258
|
+
) -> None:
|
|
259
|
+
name_node = node.child_by_field_name("name")
|
|
260
|
+
if name_node is None:
|
|
261
|
+
return
|
|
262
|
+
simple_name = name_node.text.decode()
|
|
263
|
+
qualified_name = f"{prefix}{simple_name}" if not prefix else f"{prefix}.{simple_name}"
|
|
264
|
+
|
|
265
|
+
modifiers = _find_child_by_type(node, "modifiers")
|
|
266
|
+
visibility = _get_visibility(modifiers)
|
|
267
|
+
|
|
268
|
+
node_id = f"{ctx.file_path}:{qualified_name}"
|
|
269
|
+
|
|
270
|
+
# Interfaces
|
|
271
|
+
interfaces_node = _find_child_by_type(node, "super_interfaces")
|
|
272
|
+
interface_names = _collect_type_names(interfaces_node)
|
|
273
|
+
|
|
274
|
+
properties: dict[str, Any] = {
|
|
275
|
+
"visibility": visibility,
|
|
276
|
+
"is_abstract": False,
|
|
277
|
+
"is_final": False,
|
|
278
|
+
}
|
|
279
|
+
if interface_names:
|
|
280
|
+
properties["interfaces"] = interface_names
|
|
281
|
+
|
|
282
|
+
graph_node = GraphNode(
|
|
283
|
+
id=node_id,
|
|
284
|
+
kind=NodeKind.ENUM,
|
|
285
|
+
label=qualified_name,
|
|
286
|
+
fqn=qualified_name,
|
|
287
|
+
module=ctx.module_name,
|
|
288
|
+
location=SourceLocation(
|
|
289
|
+
file_path=ctx.file_path,
|
|
290
|
+
line_start=name_node.start_point[0] + 1,
|
|
291
|
+
),
|
|
292
|
+
properties=properties,
|
|
293
|
+
)
|
|
294
|
+
result.nodes.append(graph_node)
|
|
295
|
+
|
|
296
|
+
# Edges for implements
|
|
297
|
+
for iface in interface_names:
|
|
298
|
+
result.edges.append(
|
|
299
|
+
GraphEdge(
|
|
300
|
+
source=node_id,
|
|
301
|
+
target=f"*:{iface}",
|
|
302
|
+
kind=EdgeKind.IMPLEMENTS,
|
|
303
|
+
label=f"{qualified_name} implements {iface}",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Recurse into enum body for nested types
|
|
308
|
+
body = node.child_by_field_name("body")
|
|
309
|
+
if body is not None:
|
|
310
|
+
self._walk(body, ctx, result, prefix=qualified_name)
|
|
311
|
+
|
|
312
|
+
def _process_annotation_type(
|
|
313
|
+
self, node, ctx: DetectorContext, result: DetectorResult, prefix: str
|
|
314
|
+
) -> None:
|
|
315
|
+
name_node = node.child_by_field_name("name")
|
|
316
|
+
if name_node is None:
|
|
317
|
+
return
|
|
318
|
+
simple_name = name_node.text.decode()
|
|
319
|
+
qualified_name = f"{prefix}{simple_name}" if not prefix else f"{prefix}.{simple_name}"
|
|
320
|
+
|
|
321
|
+
modifiers = _find_child_by_type(node, "modifiers")
|
|
322
|
+
visibility = _get_visibility(modifiers)
|
|
323
|
+
|
|
324
|
+
node_id = f"{ctx.file_path}:{qualified_name}"
|
|
325
|
+
|
|
326
|
+
properties: dict[str, Any] = {
|
|
327
|
+
"visibility": visibility,
|
|
328
|
+
"is_abstract": False,
|
|
329
|
+
"is_final": False,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
graph_node = GraphNode(
|
|
333
|
+
id=node_id,
|
|
334
|
+
kind=NodeKind.ANNOTATION_TYPE,
|
|
335
|
+
label=qualified_name,
|
|
336
|
+
fqn=qualified_name,
|
|
337
|
+
module=ctx.module_name,
|
|
338
|
+
location=SourceLocation(
|
|
339
|
+
file_path=ctx.file_path,
|
|
340
|
+
line_start=name_node.start_point[0] + 1,
|
|
341
|
+
),
|
|
342
|
+
properties=properties,
|
|
343
|
+
)
|
|
344
|
+
result.nodes.append(graph_node)
|
|
345
|
+
|
|
346
|
+
# Recurse into annotation body for nested types
|
|
347
|
+
body = node.child_by_field_name("body")
|
|
348
|
+
if body is not None:
|
|
349
|
+
self._walk(body, ctx, result, prefix=qualified_name)
|