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.
Files changed (183) hide show
  1. osscodeiq/__init__.py +0 -0
  2. osscodeiq/analyzer.py +467 -0
  3. osscodeiq/cache/__init__.py +0 -0
  4. osscodeiq/cache/hasher.py +23 -0
  5. osscodeiq/cache/store.py +300 -0
  6. osscodeiq/classifiers/__init__.py +0 -0
  7. osscodeiq/classifiers/layer_classifier.py +69 -0
  8. osscodeiq/cli.py +721 -0
  9. osscodeiq/config.py +113 -0
  10. osscodeiq/detectors/__init__.py +0 -0
  11. osscodeiq/detectors/auth/__init__.py +0 -0
  12. osscodeiq/detectors/auth/certificate_auth.py +139 -0
  13. osscodeiq/detectors/auth/ldap_auth.py +89 -0
  14. osscodeiq/detectors/auth/session_header_auth.py +120 -0
  15. osscodeiq/detectors/base.py +41 -0
  16. osscodeiq/detectors/config/__init__.py +0 -0
  17. osscodeiq/detectors/config/batch_structure.py +128 -0
  18. osscodeiq/detectors/config/cloudformation.py +183 -0
  19. osscodeiq/detectors/config/docker_compose.py +179 -0
  20. osscodeiq/detectors/config/github_actions.py +150 -0
  21. osscodeiq/detectors/config/gitlab_ci.py +216 -0
  22. osscodeiq/detectors/config/helm_chart.py +187 -0
  23. osscodeiq/detectors/config/ini_structure.py +101 -0
  24. osscodeiq/detectors/config/json_structure.py +72 -0
  25. osscodeiq/detectors/config/kubernetes.py +305 -0
  26. osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
  27. osscodeiq/detectors/config/openapi.py +194 -0
  28. osscodeiq/detectors/config/package_json.py +99 -0
  29. osscodeiq/detectors/config/properties_detector.py +108 -0
  30. osscodeiq/detectors/config/pyproject_toml.py +169 -0
  31. osscodeiq/detectors/config/sql_structure.py +155 -0
  32. osscodeiq/detectors/config/toml_structure.py +93 -0
  33. osscodeiq/detectors/config/tsconfig_json.py +105 -0
  34. osscodeiq/detectors/config/yaml_structure.py +82 -0
  35. osscodeiq/detectors/cpp/__init__.py +0 -0
  36. osscodeiq/detectors/cpp/cpp_structures.py +192 -0
  37. osscodeiq/detectors/csharp/__init__.py +0 -0
  38. osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
  39. osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
  40. osscodeiq/detectors/csharp/csharp_structures.py +317 -0
  41. osscodeiq/detectors/docs/__init__.py +0 -0
  42. osscodeiq/detectors/docs/markdown_structure.py +117 -0
  43. osscodeiq/detectors/frontend/__init__.py +0 -0
  44. osscodeiq/detectors/frontend/angular_components.py +177 -0
  45. osscodeiq/detectors/frontend/frontend_routes.py +259 -0
  46. osscodeiq/detectors/frontend/react_components.py +148 -0
  47. osscodeiq/detectors/frontend/svelte_components.py +84 -0
  48. osscodeiq/detectors/frontend/vue_components.py +150 -0
  49. osscodeiq/detectors/generic/__init__.py +1 -0
  50. osscodeiq/detectors/generic/imports_detector.py +413 -0
  51. osscodeiq/detectors/go/__init__.py +0 -0
  52. osscodeiq/detectors/go/go_orm.py +202 -0
  53. osscodeiq/detectors/go/go_structures.py +162 -0
  54. osscodeiq/detectors/go/go_web.py +157 -0
  55. osscodeiq/detectors/iac/__init__.py +0 -0
  56. osscodeiq/detectors/iac/bicep.py +135 -0
  57. osscodeiq/detectors/iac/dockerfile.py +182 -0
  58. osscodeiq/detectors/iac/terraform.py +188 -0
  59. osscodeiq/detectors/java/__init__.py +0 -0
  60. osscodeiq/detectors/java/azure_functions.py +424 -0
  61. osscodeiq/detectors/java/azure_messaging.py +350 -0
  62. osscodeiq/detectors/java/class_hierarchy.py +349 -0
  63. osscodeiq/detectors/java/config_def.py +82 -0
  64. osscodeiq/detectors/java/cosmos_db.py +105 -0
  65. osscodeiq/detectors/java/graphql_resolver.py +188 -0
  66. osscodeiq/detectors/java/grpc_service.py +142 -0
  67. osscodeiq/detectors/java/ibm_mq.py +178 -0
  68. osscodeiq/detectors/java/jaxrs.py +160 -0
  69. osscodeiq/detectors/java/jdbc.py +196 -0
  70. osscodeiq/detectors/java/jms.py +116 -0
  71. osscodeiq/detectors/java/jpa_entity.py +143 -0
  72. osscodeiq/detectors/java/kafka.py +113 -0
  73. osscodeiq/detectors/java/kafka_protocol.py +70 -0
  74. osscodeiq/detectors/java/micronaut.py +248 -0
  75. osscodeiq/detectors/java/module_deps.py +191 -0
  76. osscodeiq/detectors/java/public_api.py +206 -0
  77. osscodeiq/detectors/java/quarkus.py +176 -0
  78. osscodeiq/detectors/java/rabbitmq.py +150 -0
  79. osscodeiq/detectors/java/raw_sql.py +136 -0
  80. osscodeiq/detectors/java/repository.py +131 -0
  81. osscodeiq/detectors/java/rmi.py +129 -0
  82. osscodeiq/detectors/java/spring_events.py +117 -0
  83. osscodeiq/detectors/java/spring_rest.py +168 -0
  84. osscodeiq/detectors/java/spring_security.py +212 -0
  85. osscodeiq/detectors/java/tibco_ems.py +193 -0
  86. osscodeiq/detectors/java/websocket.py +188 -0
  87. osscodeiq/detectors/kotlin/__init__.py +0 -0
  88. osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
  89. osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
  90. osscodeiq/detectors/proto/__init__.py +0 -0
  91. osscodeiq/detectors/proto/proto_structure.py +153 -0
  92. osscodeiq/detectors/python/__init__.py +0 -0
  93. osscodeiq/detectors/python/celery_tasks.py +88 -0
  94. osscodeiq/detectors/python/django_auth.py +132 -0
  95. osscodeiq/detectors/python/django_models.py +157 -0
  96. osscodeiq/detectors/python/django_views.py +74 -0
  97. osscodeiq/detectors/python/fastapi_auth.py +143 -0
  98. osscodeiq/detectors/python/fastapi_routes.py +68 -0
  99. osscodeiq/detectors/python/flask_routes.py +67 -0
  100. osscodeiq/detectors/python/kafka_python.py +175 -0
  101. osscodeiq/detectors/python/pydantic_models.py +115 -0
  102. osscodeiq/detectors/python/python_structures.py +234 -0
  103. osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
  104. osscodeiq/detectors/registry.py +100 -0
  105. osscodeiq/detectors/rust/__init__.py +0 -0
  106. osscodeiq/detectors/rust/actix_web.py +234 -0
  107. osscodeiq/detectors/rust/rust_structures.py +174 -0
  108. osscodeiq/detectors/scala/__init__.py +0 -0
  109. osscodeiq/detectors/scala/scala_structures.py +128 -0
  110. osscodeiq/detectors/shell/__init__.py +0 -0
  111. osscodeiq/detectors/shell/bash_detector.py +127 -0
  112. osscodeiq/detectors/shell/powershell_detector.py +118 -0
  113. osscodeiq/detectors/typescript/__init__.py +0 -0
  114. osscodeiq/detectors/typescript/express_routes.py +55 -0
  115. osscodeiq/detectors/typescript/fastify_routes.py +156 -0
  116. osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
  117. osscodeiq/detectors/typescript/kafka_js.py +164 -0
  118. osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
  119. osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
  120. osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
  121. osscodeiq/detectors/typescript/passport_jwt.py +133 -0
  122. osscodeiq/detectors/typescript/prisma_orm.py +96 -0
  123. osscodeiq/detectors/typescript/remix_routes.py +160 -0
  124. osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
  125. osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
  126. osscodeiq/detectors/typescript/typescript_structures.py +185 -0
  127. osscodeiq/detectors/utils.py +49 -0
  128. osscodeiq/discovery/__init__.py +11 -0
  129. osscodeiq/discovery/change_detector.py +97 -0
  130. osscodeiq/discovery/file_discovery.py +342 -0
  131. osscodeiq/flow/__init__.py +0 -0
  132. osscodeiq/flow/engine.py +78 -0
  133. osscodeiq/flow/models.py +72 -0
  134. osscodeiq/flow/renderer.py +127 -0
  135. osscodeiq/flow/templates/interactive.html +252 -0
  136. osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
  137. osscodeiq/flow/vendor/cytoscape.min.js +32 -0
  138. osscodeiq/flow/vendor/dagre.min.js +3809 -0
  139. osscodeiq/flow/views.py +357 -0
  140. osscodeiq/graph/__init__.py +0 -0
  141. osscodeiq/graph/backend.py +52 -0
  142. osscodeiq/graph/backends/__init__.py +23 -0
  143. osscodeiq/graph/backends/kuzu.py +576 -0
  144. osscodeiq/graph/backends/networkx.py +135 -0
  145. osscodeiq/graph/backends/sqlite_backend.py +406 -0
  146. osscodeiq/graph/builder.py +297 -0
  147. osscodeiq/graph/query.py +228 -0
  148. osscodeiq/graph/store.py +183 -0
  149. osscodeiq/graph/views.py +231 -0
  150. osscodeiq/models/__init__.py +17 -0
  151. osscodeiq/models/graph.py +116 -0
  152. osscodeiq/output/__init__.py +0 -0
  153. osscodeiq/output/dot.py +171 -0
  154. osscodeiq/output/mermaid.py +160 -0
  155. osscodeiq/output/safety.py +58 -0
  156. osscodeiq/output/serializers.py +42 -0
  157. osscodeiq/parsing/__init__.py +5 -0
  158. osscodeiq/parsing/languages/__init__.py +0 -0
  159. osscodeiq/parsing/languages/base.py +23 -0
  160. osscodeiq/parsing/languages/java.py +68 -0
  161. osscodeiq/parsing/languages/python.py +57 -0
  162. osscodeiq/parsing/languages/typescript.py +95 -0
  163. osscodeiq/parsing/parser_manager.py +125 -0
  164. osscodeiq/parsing/structured/__init__.py +0 -0
  165. osscodeiq/parsing/structured/gradle_parser.py +78 -0
  166. osscodeiq/parsing/structured/json_parser.py +24 -0
  167. osscodeiq/parsing/structured/properties_parser.py +56 -0
  168. osscodeiq/parsing/structured/sql_parser.py +54 -0
  169. osscodeiq/parsing/structured/xml_parser.py +148 -0
  170. osscodeiq/parsing/structured/yaml_parser.py +38 -0
  171. osscodeiq/server/__init__.py +7 -0
  172. osscodeiq/server/app.py +53 -0
  173. osscodeiq/server/mcp_server.py +174 -0
  174. osscodeiq/server/middleware.py +16 -0
  175. osscodeiq/server/routes.py +184 -0
  176. osscodeiq/server/service.py +445 -0
  177. osscodeiq/server/templates/welcome.html +56 -0
  178. osscodeiq-0.0.0.dist-info/METADATA +30 -0
  179. osscodeiq-0.0.0.dist-info/RECORD +183 -0
  180. osscodeiq-0.0.0.dist-info/WHEEL +5 -0
  181. osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
  182. osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
  183. 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)