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,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
|