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,113 @@
1
+ """Kafka producer/consumer detector for Java 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
+ _KAFKA_LISTENER_RE = re.compile(
19
+ r'@KafkaListener\s*\(\s*(?:.*?topics?\s*=\s*)?[\{"]?\s*"([^"]+)"'
20
+ )
21
+ _KAFKA_SEND_RE = re.compile(
22
+ r'(?:kafkaTemplate|KafkaTemplate)\s*\.send\s*\(\s*"([^"]+)"'
23
+ )
24
+ _KAFKA_SEND_CONST_RE = re.compile(
25
+ r'(?:kafkaTemplate|KafkaTemplate)\s*\.send\s*\(\s*(\w+)'
26
+ )
27
+ _GROUP_ID_RE = re.compile(r'groupId\s*=\s*"([^"]+)"')
28
+
29
+
30
+ class KafkaDetector:
31
+ """Detects Kafka consumers (@KafkaListener) and producers (KafkaTemplate.send)."""
32
+
33
+ name: str = "kafka"
34
+ supported_languages: tuple[str, ...] = ("java",)
35
+
36
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
37
+ result = DetectorResult()
38
+ text = decode_text(ctx)
39
+ lines = text.split("\n")
40
+
41
+ if "KafkaListener" not in text and "KafkaTemplate" not in text and "kafkaTemplate" not in text:
42
+ return result
43
+
44
+ # Find class name
45
+ class_name: str | None = None
46
+ for line in lines:
47
+ cm = _CLASS_RE.search(line)
48
+ if cm:
49
+ class_name = cm.group(1)
50
+ break
51
+
52
+ if not class_name:
53
+ return result
54
+
55
+ class_node_id = f"{ctx.file_path}:{class_name}"
56
+ seen_topics: set[str] = set()
57
+
58
+ def _ensure_topic_node(topic: str) -> str:
59
+ topic_id = f"kafka:topic:{topic}"
60
+ if topic not in seen_topics:
61
+ seen_topics.add(topic)
62
+ result.nodes.append(GraphNode(
63
+ id=topic_id,
64
+ kind=NodeKind.TOPIC,
65
+ label=f"kafka:{topic}",
66
+ properties={"broker": "kafka", "topic": topic},
67
+ ))
68
+ return topic_id
69
+
70
+ # Detect @KafkaListener consumers
71
+ for i, line in enumerate(lines):
72
+ m = _KAFKA_LISTENER_RE.search(line)
73
+ if not m:
74
+ # Multi-line annotation — check if previous line has @KafkaListener
75
+ if i > 0 and "@KafkaListener" in lines[i - 1]:
76
+ m = re.search(r'"([^"]+)"', line)
77
+ if not m:
78
+ continue
79
+
80
+ topic = m.group(1)
81
+ topic_id = _ensure_topic_node(topic)
82
+
83
+ group_id = _GROUP_ID_RE.search(line)
84
+ props: dict[str, str] = {"topic": topic}
85
+ if group_id:
86
+ props["group_id"] = group_id.group(1)
87
+
88
+ result.edges.append(GraphEdge(
89
+ source=class_node_id,
90
+ target=topic_id,
91
+ kind=EdgeKind.CONSUMES,
92
+ label=f"{class_name} consumes {topic}",
93
+ properties=props,
94
+ ))
95
+
96
+ # Detect KafkaTemplate.send producers
97
+ for i, line in enumerate(lines):
98
+ m = _KAFKA_SEND_RE.search(line)
99
+ if not m:
100
+ continue
101
+
102
+ topic = m.group(1)
103
+ topic_id = _ensure_topic_node(topic)
104
+
105
+ result.edges.append(GraphEdge(
106
+ source=class_node_id,
107
+ target=topic_id,
108
+ kind=EdgeKind.PRODUCES,
109
+ label=f"{class_name} produces to {topic}",
110
+ properties={"topic": topic},
111
+ ))
112
+
113
+ return result
@@ -0,0 +1,70 @@
1
+ """Kafka protocol message detector for Java 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
+ _PROTOCOL_MSG_RE = re.compile(
18
+ r"class\s+(\w+)\s+extends\s+(AbstractRequest|AbstractResponse)(?!\.)\b"
19
+ )
20
+
21
+
22
+ class KafkaProtocolDetector:
23
+ """Detects classes extending AbstractRequest or AbstractResponse — Kafka's binary protocol message pattern."""
24
+
25
+ name: str = "kafka_protocol"
26
+ supported_languages: tuple[str, ...] = ("java",)
27
+
28
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
29
+ result = DetectorResult()
30
+ text = decode_text(ctx)
31
+
32
+ if "AbstractRequest" not in text and "AbstractResponse" not in text:
33
+ return result
34
+
35
+ lines = text.split("\n")
36
+
37
+ for i, line in enumerate(lines):
38
+ m = _PROTOCOL_MSG_RE.search(line)
39
+ if not m:
40
+ continue
41
+
42
+ class_name = m.group(1)
43
+ parent_class = m.group(2)
44
+ protocol_type = "request" if parent_class == "AbstractRequest" else "response"
45
+
46
+ node_id = f"{ctx.file_path}:{class_name}"
47
+
48
+ result.nodes.append(
49
+ GraphNode(
50
+ id=node_id,
51
+ kind=NodeKind.PROTOCOL_MESSAGE,
52
+ label=class_name,
53
+ location=SourceLocation(
54
+ file_path=ctx.file_path,
55
+ line_start=i + 1,
56
+ ),
57
+ properties={"protocol_type": protocol_type},
58
+ )
59
+ )
60
+
61
+ result.edges.append(
62
+ GraphEdge(
63
+ source=node_id,
64
+ target=f"*:{parent_class}",
65
+ kind=EdgeKind.EXTENDS,
66
+ label=f"{class_name} extends {parent_class}",
67
+ )
68
+ )
69
+
70
+ return result
@@ -0,0 +1,248 @@
1
+ """Micronaut framework detector for Java source files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
9
+ from osscodeiq.detectors.utils import decode_text
10
+ from osscodeiq.models.graph import (
11
+ EdgeKind,
12
+ GraphEdge,
13
+ GraphNode,
14
+ NodeKind,
15
+ SourceLocation,
16
+ )
17
+
18
+ _CONTROLLER_RE = re.compile(r'@Controller\s*\(\s*"([^"]*)"')
19
+ _HTTP_METHOD_RE = re.compile(r'@(Get|Post|Put|Delete)(?!Mapping)\s*(?:\(\s*"([^"]*)")?\s*\)?')
20
+ _BEAN_SCOPE_RE = re.compile(r"@(Singleton|Prototype|Infrastructure)\b")
21
+ _CLIENT_RE = re.compile(r'@Client\s*\(\s*"([^"]*)"')
22
+ _INJECT_RE = re.compile(r"@Inject\b")
23
+ _SCHEDULED_RE = re.compile(r'@Scheduled\s*\(\s*fixedRate\s*=\s*"([^"]+)"')
24
+ _EVENT_LISTENER_RE = re.compile(r"@EventListener\b")
25
+ _INJECT = "@Inject"
26
+ _EVENT_LISTENER = "@EventListener"
27
+
28
+ _CLASS_RE = re.compile(r"(?:public\s+)?class\s+(\w+)")
29
+ _JAVA_METHOD_RE = re.compile(
30
+ r"(?:public|protected|private)?\s*(?:static\s+)?(?:[\w<>\[\],\s]+)\s+(\w+)\s*\("
31
+ )
32
+
33
+
34
+ class MicronautDetector:
35
+ """Detects Micronaut-specific patterns in Java source files."""
36
+
37
+ name: str = "micronaut"
38
+ supported_languages: tuple[str, ...] = ("java",)
39
+
40
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
41
+ result = DetectorResult()
42
+ text = decode_text(ctx)
43
+
44
+ # Fast bail
45
+ if not any(
46
+ marker in text
47
+ for marker in (
48
+ "@Controller",
49
+ "@Get",
50
+ "@Post",
51
+ "@Put",
52
+ "@Delete",
53
+ "@Singleton",
54
+ "@Prototype",
55
+ "@Infrastructure",
56
+ "@Client",
57
+ _INJECT,
58
+ "@Scheduled",
59
+ _EVENT_LISTENER,
60
+ "io.micronaut",
61
+ )
62
+ ):
63
+ return result
64
+
65
+ lines = text.split("\n")
66
+
67
+ # Find class name and controller base path
68
+ class_name: str | None = None
69
+ controller_path: str | None = None
70
+ for i, line in enumerate(lines):
71
+ cm = _CLASS_RE.search(line)
72
+ if cm:
73
+ class_name = cm.group(1)
74
+ # Look backwards for @Controller
75
+ for j in range(max(0, i - 5), i):
76
+ pm = _CONTROLLER_RE.search(lines[j])
77
+ if pm:
78
+ controller_path = pm.group(1).rstrip("/")
79
+ break
80
+ break
81
+
82
+ class_node_id = f"{ctx.file_path}:{class_name}" if class_name else ctx.file_path
83
+
84
+ # If we found a @Controller, emit a CLASS node
85
+ if controller_path is not None and class_name:
86
+ ctrl_id = f"micronaut:{ctx.file_path}:controller:{class_name}"
87
+ result.nodes.append(
88
+ GraphNode(
89
+ id=ctrl_id,
90
+ kind=NodeKind.CLASS,
91
+ label=f"@Controller({controller_path}) {class_name}",
92
+ fqn=class_name,
93
+ module=ctx.module_name,
94
+ location=SourceLocation(file_path=ctx.file_path, line_start=1),
95
+ annotations=["@Controller"],
96
+ properties={"framework": "micronaut", "path": controller_path},
97
+ )
98
+ )
99
+
100
+ for i, line in enumerate(lines):
101
+ lineno = i + 1
102
+
103
+ # HTTP method annotations -> ENDPOINT
104
+ m = _HTTP_METHOD_RE.search(line)
105
+ if m:
106
+ http_method = m.group(1).upper()
107
+ method_path = m.group(2) or ""
108
+
109
+ # Build full path
110
+ if controller_path is not None:
111
+ full_path = f"{controller_path}/{method_path.lstrip('/')}" if method_path else controller_path
112
+ else:
113
+ full_path = f"/{method_path.lstrip('/')}" if method_path else "/"
114
+ if not full_path.startswith("/"):
115
+ full_path = "/" + full_path
116
+
117
+ # Find method name
118
+ method_name: str | None = None
119
+ for k in range(i + 1, min(i + 5, len(lines))):
120
+ mm = _JAVA_METHOD_RE.search(lines[k])
121
+ if mm:
122
+ method_name = mm.group(1)
123
+ break
124
+
125
+ node_id = f"micronaut:{ctx.file_path}:endpoint:{http_method}:{full_path}:{lineno}"
126
+ endpoint_label = f"{http_method} {full_path}"
127
+ result.nodes.append(
128
+ GraphNode(
129
+ id=node_id,
130
+ kind=NodeKind.ENDPOINT,
131
+ label=endpoint_label,
132
+ fqn=f"{class_name}.{method_name}" if class_name and method_name else class_name,
133
+ module=ctx.module_name,
134
+ location=SourceLocation(file_path=ctx.file_path, line_start=lineno),
135
+ annotations=[f"@{m.group(1)}"],
136
+ properties={
137
+ "framework": "micronaut",
138
+ "http_method": http_method,
139
+ "path": full_path,
140
+ },
141
+ )
142
+ )
143
+ result.edges.append(
144
+ GraphEdge(
145
+ source=class_node_id,
146
+ target=node_id,
147
+ kind=EdgeKind.EXPOSES,
148
+ label=f"{class_name or 'class'} exposes {endpoint_label}",
149
+ )
150
+ )
151
+
152
+ # Bean scope annotations -> MIDDLEWARE
153
+ m = _BEAN_SCOPE_RE.search(line)
154
+ if m:
155
+ scope = m.group(1)
156
+ node_id = f"micronaut:{ctx.file_path}:scope_{scope.lower()}:{lineno}"
157
+ result.nodes.append(
158
+ GraphNode(
159
+ id=node_id,
160
+ kind=NodeKind.MIDDLEWARE,
161
+ label=f"@{scope} (bean scope)",
162
+ fqn=f"{class_name}.{scope}" if class_name else scope,
163
+ module=ctx.module_name,
164
+ location=SourceLocation(file_path=ctx.file_path, line_start=lineno),
165
+ annotations=[f"@{scope}"],
166
+ properties={"framework": "micronaut", "bean_scope": scope},
167
+ )
168
+ )
169
+
170
+ # @Client -> DEPENDS_ON edge
171
+ m = _CLIENT_RE.search(line)
172
+ if m:
173
+ client_target = m.group(1)
174
+ client_id = f"micronaut:{ctx.file_path}:client:{lineno}"
175
+ result.nodes.append(
176
+ GraphNode(
177
+ id=client_id,
178
+ kind=NodeKind.CLASS,
179
+ label=f"@Client({client_target})",
180
+ fqn=client_target,
181
+ module=ctx.module_name,
182
+ location=SourceLocation(file_path=ctx.file_path, line_start=lineno),
183
+ annotations=["@Client"],
184
+ properties={"framework": "micronaut", "client_target": client_target},
185
+ )
186
+ )
187
+ result.edges.append(
188
+ GraphEdge(
189
+ source=class_node_id,
190
+ target=client_id,
191
+ kind=EdgeKind.DEPENDS_ON,
192
+ label=f"{class_name or 'class'} depends on {client_target}",
193
+ )
194
+ )
195
+
196
+ # @Inject -> annotation node
197
+ m = _INJECT_RE.search(line)
198
+ if m:
199
+ node_id = f"micronaut:{ctx.file_path}:inject:{lineno}"
200
+ result.nodes.append(
201
+ GraphNode(
202
+ id=node_id,
203
+ kind=NodeKind.MIDDLEWARE,
204
+ label=_INJECT,
205
+ fqn=f"{class_name}.inject" if class_name else "inject",
206
+ module=ctx.module_name,
207
+ location=SourceLocation(file_path=ctx.file_path, line_start=lineno),
208
+ annotations=[_INJECT],
209
+ properties={"framework": "micronaut"},
210
+ )
211
+ )
212
+
213
+ # @Scheduled -> EVENT
214
+ m = _SCHEDULED_RE.search(line)
215
+ if m:
216
+ rate = m.group(1)
217
+ node_id = f"micronaut:{ctx.file_path}:scheduled:{lineno}"
218
+ result.nodes.append(
219
+ GraphNode(
220
+ id=node_id,
221
+ kind=NodeKind.EVENT,
222
+ label=f"@Scheduled(fixedRate={rate})",
223
+ fqn=f"{class_name}.scheduled" if class_name else "scheduled",
224
+ module=ctx.module_name,
225
+ location=SourceLocation(file_path=ctx.file_path, line_start=lineno),
226
+ annotations=["@Scheduled"],
227
+ properties={"framework": "micronaut", "fixed_rate": rate},
228
+ )
229
+ )
230
+
231
+ # @EventListener -> EVENT
232
+ m = _EVENT_LISTENER_RE.search(line)
233
+ if m:
234
+ node_id = f"micronaut:{ctx.file_path}:event_listener:{lineno}"
235
+ result.nodes.append(
236
+ GraphNode(
237
+ id=node_id,
238
+ kind=NodeKind.EVENT,
239
+ label=_EVENT_LISTENER,
240
+ fqn=f"{class_name}.eventListener" if class_name else "eventListener",
241
+ module=ctx.module_name,
242
+ location=SourceLocation(file_path=ctx.file_path, line_start=lineno),
243
+ annotations=[_EVENT_LISTENER],
244
+ properties={"framework": "micronaut"},
245
+ )
246
+ )
247
+
248
+ return result
@@ -0,0 +1,191 @@
1
+ """Module dependency detector for Maven (pom.xml) and Gradle build files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+ from xml.etree import ElementTree
8
+
9
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
10
+ from osscodeiq.detectors.utils import decode_text
11
+ from osscodeiq.models.graph import (
12
+ EdgeKind,
13
+ GraphEdge,
14
+ GraphNode,
15
+ NodeKind,
16
+ SourceLocation,
17
+ )
18
+
19
+ _POM_NS = {"m": "http://maven.apache.org/POM/4.0.0"}
20
+
21
+ # Gradle patterns
22
+ _GRADLE_DEPENDENCY_RE = re.compile(
23
+ r"(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation)\s+"
24
+ r"(?:project\s*\(\s*['\"]([^'\"]+)['\"]\s*\)"
25
+ r"|['\"]([^'\"]+)['\"])"
26
+ )
27
+ _GRADLE_SETTINGS_MODULE_RE = re.compile(r"include\s+['\"]([^'\"]+)['\"]")
28
+
29
+
30
+ class ModuleDepsDetector:
31
+ """Detects Maven/Gradle module declarations and inter-module dependencies."""
32
+
33
+ name: str = "module_deps"
34
+ supported_languages: tuple[str, ...] = ("java", "xml", "gradle")
35
+
36
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
37
+ if ctx.file_path.endswith("pom.xml"):
38
+ return self._detect_maven(ctx)
39
+ elif ctx.file_path.endswith((".gradle", ".gradle.kts")):
40
+ return self._detect_gradle(ctx)
41
+ elif ctx.file_path.endswith("settings.gradle") or ctx.file_path.endswith("settings.gradle.kts"):
42
+ return self._detect_gradle_settings(ctx)
43
+ return DetectorResult()
44
+
45
+ def _detect_maven(self, ctx: DetectorContext) -> DetectorResult:
46
+ result = DetectorResult()
47
+ text = decode_text(ctx)
48
+
49
+ try:
50
+ root = ElementTree.fromstring(text)
51
+ except ElementTree.ParseError:
52
+ return result
53
+
54
+ # Determine this module's coordinates
55
+ # Try with namespace first, then without
56
+ group_id_el = root.find("m:groupId", _POM_NS)
57
+ if group_id_el is None:
58
+ group_id_el = root.find("groupId")
59
+ artifact_id_el = root.find("m:artifactId", _POM_NS)
60
+ if artifact_id_el is None:
61
+ artifact_id_el = root.find("artifactId")
62
+
63
+ if artifact_id_el is None:
64
+ return result
65
+
66
+ group_id = group_id_el.text if group_id_el is not None else "unknown"
67
+ artifact_id = artifact_id_el.text or "unknown"
68
+ module_id = f"module:{group_id}:{artifact_id}"
69
+
70
+ result.nodes.append(GraphNode(
71
+ id=module_id,
72
+ kind=NodeKind.MODULE,
73
+ label=artifact_id,
74
+ fqn=f"{group_id}:{artifact_id}",
75
+ module=ctx.module_name,
76
+ location=SourceLocation(file_path=ctx.file_path, line_start=1),
77
+ properties={"group_id": group_id, "artifact_id": artifact_id, "build_tool": "maven"},
78
+ ))
79
+
80
+ # Detect sub-modules
81
+ for modules_el in (root.findall("m:modules/m:module", _POM_NS) + root.findall("modules/module")):
82
+ if modules_el.text:
83
+ sub_module = modules_el.text
84
+ sub_id = f"module:{group_id}:{sub_module}"
85
+ result.nodes.append(GraphNode(
86
+ id=sub_id,
87
+ kind=NodeKind.MODULE,
88
+ label=sub_module,
89
+ fqn=f"{group_id}:{sub_module}",
90
+ properties={"build_tool": "maven", "parent": artifact_id},
91
+ ))
92
+ result.edges.append(GraphEdge(
93
+ source=module_id,
94
+ target=sub_id,
95
+ kind=EdgeKind.CONTAINS,
96
+ label=f"{artifact_id} contains {sub_module}",
97
+ ))
98
+
99
+ # Detect dependencies
100
+ dep_paths = (
101
+ root.findall("m:dependencies/m:dependency", _POM_NS)
102
+ + root.findall("dependencies/dependency")
103
+ )
104
+ for dep in dep_paths:
105
+ dep_group_el = dep.find("m:groupId", _POM_NS)
106
+ if dep_group_el is None:
107
+ dep_group_el = dep.find("groupId")
108
+ dep_artifact_el = dep.find("m:artifactId", _POM_NS)
109
+ if dep_artifact_el is None:
110
+ dep_artifact_el = dep.find("artifactId")
111
+ if dep_artifact_el is not None:
112
+ dep_group = dep_group_el.text if dep_group_el is not None else "unknown"
113
+ dep_artifact = dep_artifact_el.text or "unknown"
114
+ dep_id = f"module:{dep_group}:{dep_artifact}"
115
+ result.edges.append(GraphEdge(
116
+ source=module_id,
117
+ target=dep_id,
118
+ kind=EdgeKind.DEPENDS_ON,
119
+ label=f"{artifact_id} depends on {dep_artifact}",
120
+ properties={"group_id": dep_group, "artifact_id": dep_artifact},
121
+ ))
122
+
123
+ return result
124
+
125
+ def _detect_gradle(self, ctx: DetectorContext) -> DetectorResult:
126
+ result = DetectorResult()
127
+ text = decode_text(ctx)
128
+ lines = text.split("\n")
129
+
130
+ # Try to infer module name from file path
131
+ module_name = ctx.module_name or ctx.file_path.rsplit("/", 1)[0].rsplit("/", 1)[-1]
132
+ module_id = f"module:{module_name}"
133
+
134
+ result.nodes.append(GraphNode(
135
+ id=module_id,
136
+ kind=NodeKind.MODULE,
137
+ label=module_name,
138
+ fqn=module_name,
139
+ module=ctx.module_name,
140
+ location=SourceLocation(file_path=ctx.file_path, line_start=1),
141
+ properties={"build_tool": "gradle"},
142
+ ))
143
+
144
+ for i, line in enumerate(lines):
145
+ m = _GRADLE_DEPENDENCY_RE.search(line)
146
+ if not m:
147
+ continue
148
+
149
+ project_dep = m.group(1) # project(':submodule')
150
+ external_dep = m.group(2) # 'group:artifact:version'
151
+
152
+ if project_dep:
153
+ dep_name = project_dep.lstrip(":")
154
+ dep_id = f"module:{dep_name}"
155
+ result.edges.append(GraphEdge(
156
+ source=module_id,
157
+ target=dep_id,
158
+ kind=EdgeKind.DEPENDS_ON,
159
+ label=f"{module_name} depends on {dep_name}",
160
+ properties={"type": "project"},
161
+ ))
162
+ elif external_dep and ":" in external_dep:
163
+ parts = external_dep.split(":")
164
+ dep_id = f"module:{parts[0]}:{parts[1]}" if len(parts) >= 2 else f"module:{external_dep}"
165
+ result.edges.append(GraphEdge(
166
+ source=module_id,
167
+ target=dep_id,
168
+ kind=EdgeKind.DEPENDS_ON,
169
+ label=f"{module_name} depends on {external_dep}",
170
+ properties={"coordinate": external_dep, "type": "external"},
171
+ ))
172
+
173
+ return result
174
+
175
+ def _detect_gradle_settings(self, ctx: DetectorContext) -> DetectorResult:
176
+ result = DetectorResult()
177
+ text = decode_text(ctx)
178
+
179
+ for m in _GRADLE_SETTINGS_MODULE_RE.finditer(text):
180
+ module_path = m.group(1).lstrip(":")
181
+ module_id = f"module:{module_path}"
182
+ result.nodes.append(GraphNode(
183
+ id=module_id,
184
+ kind=NodeKind.MODULE,
185
+ label=module_path,
186
+ fqn=module_path,
187
+ location=SourceLocation(file_path=ctx.file_path),
188
+ properties={"build_tool": "gradle"},
189
+ ))
190
+
191
+ return result