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,131 @@
|
|
|
1
|
+
"""Spring Data repository 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
|
+
_REPO_EXTENDS_RE = re.compile(
|
|
18
|
+
r"interface\s+(\w+)\s+extends\s+((?:JpaRepository|CrudRepository|"
|
|
19
|
+
r"PagingAndSortingRepository|ReactiveCrudRepository|"
|
|
20
|
+
r"MongoRepository|ElasticsearchRepository|"
|
|
21
|
+
r"R2dbcRepository|JpaSpecificationExecutor)\w*)"
|
|
22
|
+
r"(?:<\s*(\w+)\s*,\s*[\w<>]+\s*>)?"
|
|
23
|
+
)
|
|
24
|
+
_REPOSITORY_ANNO_RE = re.compile(r"@Repository")
|
|
25
|
+
_INTERFACE_RE = re.compile(r"interface\s+(\w+)")
|
|
26
|
+
_GENERIC_PARAMS_RE = re.compile(r"<\s*(\w+)\s*,")
|
|
27
|
+
_QUERY_RE = re.compile(r'@Query\s*\(\s*(?:value\s*=\s*)?"([^"]+)"')
|
|
28
|
+
_METHOD_RE = re.compile(r"(?:public\s+)?(?:[\w<>\[\],?\s]+)\s+(\w+)\s*\(")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RepositoryDetector:
|
|
32
|
+
"""Detects Spring Data repository interfaces."""
|
|
33
|
+
|
|
34
|
+
name: str = "spring_repository"
|
|
35
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
36
|
+
|
|
37
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
38
|
+
result = DetectorResult()
|
|
39
|
+
text = decode_text(ctx)
|
|
40
|
+
lines = text.split("\n")
|
|
41
|
+
|
|
42
|
+
# Check for repository patterns
|
|
43
|
+
has_repo_annotation = _REPOSITORY_ANNO_RE.search(text) is not None
|
|
44
|
+
extends_match = _REPO_EXTENDS_RE.search(text)
|
|
45
|
+
|
|
46
|
+
if not extends_match and not has_repo_annotation:
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
# Find interface name and entity type
|
|
50
|
+
interface_name: str | None = None
|
|
51
|
+
entity_type: str | None = None
|
|
52
|
+
parent_repo: str | None = None
|
|
53
|
+
interface_line: int = 0
|
|
54
|
+
|
|
55
|
+
if extends_match:
|
|
56
|
+
interface_name = extends_match.group(1)
|
|
57
|
+
parent_repo = extends_match.group(2)
|
|
58
|
+
entity_type = extends_match.group(3)
|
|
59
|
+
# Find line number
|
|
60
|
+
for i, line in enumerate(lines):
|
|
61
|
+
if interface_name and interface_name in line and "interface" in line:
|
|
62
|
+
interface_line = i + 1
|
|
63
|
+
break
|
|
64
|
+
else:
|
|
65
|
+
# Just @Repository on a class/interface
|
|
66
|
+
for i, line in enumerate(lines):
|
|
67
|
+
im = _INTERFACE_RE.search(line)
|
|
68
|
+
if im:
|
|
69
|
+
interface_name = im.group(1)
|
|
70
|
+
interface_line = i + 1
|
|
71
|
+
# Try to extract generic params
|
|
72
|
+
gm = _GENERIC_PARAMS_RE.search(line)
|
|
73
|
+
if gm:
|
|
74
|
+
entity_type = gm.group(1)
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
if not interface_name:
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
repo_id = f"{ctx.file_path}:{interface_name}"
|
|
81
|
+
|
|
82
|
+
properties: dict[str, object] = {}
|
|
83
|
+
if parent_repo:
|
|
84
|
+
properties["extends"] = parent_repo
|
|
85
|
+
if entity_type:
|
|
86
|
+
properties["entity_type"] = entity_type
|
|
87
|
+
|
|
88
|
+
# Extract @Query methods
|
|
89
|
+
custom_queries: list[dict[str, str]] = []
|
|
90
|
+
for i, line in enumerate(lines):
|
|
91
|
+
qm = _QUERY_RE.search(line)
|
|
92
|
+
if qm:
|
|
93
|
+
query_str = qm.group(1)
|
|
94
|
+
# Find method name on next lines
|
|
95
|
+
method_name = None
|
|
96
|
+
for k in range(i + 1, min(i + 4, len(lines))):
|
|
97
|
+
mm = _METHOD_RE.search(lines[k])
|
|
98
|
+
if mm:
|
|
99
|
+
method_name = mm.group(1)
|
|
100
|
+
break
|
|
101
|
+
custom_queries.append({
|
|
102
|
+
"query": query_str,
|
|
103
|
+
"method": method_name or "unknown",
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if custom_queries:
|
|
107
|
+
properties["custom_queries"] = custom_queries
|
|
108
|
+
|
|
109
|
+
node = GraphNode(
|
|
110
|
+
id=repo_id,
|
|
111
|
+
kind=NodeKind.REPOSITORY,
|
|
112
|
+
label=interface_name,
|
|
113
|
+
fqn=interface_name,
|
|
114
|
+
module=ctx.module_name,
|
|
115
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=interface_line),
|
|
116
|
+
annotations=["@Repository"] if has_repo_annotation else [],
|
|
117
|
+
properties=properties,
|
|
118
|
+
)
|
|
119
|
+
result.nodes.append(node)
|
|
120
|
+
|
|
121
|
+
# Edge to entity
|
|
122
|
+
if entity_type:
|
|
123
|
+
edge = GraphEdge(
|
|
124
|
+
source=repo_id,
|
|
125
|
+
target=f"*:{entity_type}",
|
|
126
|
+
kind=EdgeKind.QUERIES,
|
|
127
|
+
label=f"{interface_name} queries {entity_type}",
|
|
128
|
+
)
|
|
129
|
+
result.edges.append(edge)
|
|
130
|
+
|
|
131
|
+
return result
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Java RMI detector for 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
|
+
_REMOTE_INTERFACE_RE = re.compile(
|
|
18
|
+
r"interface\s+(\w+)\s+extends\s+(?:java\.rmi\.)?Remote"
|
|
19
|
+
)
|
|
20
|
+
_UNICAST_RE = re.compile(
|
|
21
|
+
r"class\s+(\w+)\s+extends\s+(?:java\.rmi\.server\.)?UnicastRemoteObject"
|
|
22
|
+
)
|
|
23
|
+
_IMPLEMENTS_RE = re.compile(r"class\s+(\w+)\s+extends\s+\w+\s+implements\s+([\w,\s]+)")
|
|
24
|
+
_REGISTRY_BIND_RE = re.compile(
|
|
25
|
+
r'(?:Registry|Naming)\s*\.(?:bind|rebind)\s*\(\s*"([^"]+)"'
|
|
26
|
+
)
|
|
27
|
+
_REGISTRY_LOOKUP_RE = re.compile(
|
|
28
|
+
r'(?:Registry|Naming)\s*\.lookup\s*\(\s*"([^"]+)"'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RmiDetector:
|
|
33
|
+
"""Detects Java RMI interfaces and remote object exports."""
|
|
34
|
+
|
|
35
|
+
name: str = "rmi"
|
|
36
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
37
|
+
|
|
38
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
39
|
+
result = DetectorResult()
|
|
40
|
+
text = decode_text(ctx)
|
|
41
|
+
lines = text.split("\n")
|
|
42
|
+
|
|
43
|
+
has_remote = "Remote" in text
|
|
44
|
+
has_unicast = "UnicastRemoteObject" in text
|
|
45
|
+
has_naming = "Naming." in text or "Registry." in text
|
|
46
|
+
|
|
47
|
+
if not has_remote and not has_unicast and not has_naming:
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
# Detect Remote interfaces
|
|
51
|
+
for i, line in enumerate(lines):
|
|
52
|
+
m = _REMOTE_INTERFACE_RE.search(line)
|
|
53
|
+
if m:
|
|
54
|
+
iface_name = m.group(1)
|
|
55
|
+
iface_id = f"{ctx.file_path}:{iface_name}"
|
|
56
|
+
result.nodes.append(GraphNode(
|
|
57
|
+
id=iface_id,
|
|
58
|
+
kind=NodeKind.RMI_INTERFACE,
|
|
59
|
+
label=iface_name,
|
|
60
|
+
fqn=iface_name,
|
|
61
|
+
module=ctx.module_name,
|
|
62
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=i + 1),
|
|
63
|
+
properties={"type": "remote_interface"},
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
# Detect UnicastRemoteObject implementations
|
|
67
|
+
for i, line in enumerate(lines):
|
|
68
|
+
m = _UNICAST_RE.search(line)
|
|
69
|
+
if m:
|
|
70
|
+
class_name = m.group(1)
|
|
71
|
+
class_id = f"{ctx.file_path}:{class_name}"
|
|
72
|
+
|
|
73
|
+
# Find which interfaces it implements
|
|
74
|
+
impl_match = _IMPLEMENTS_RE.search(line)
|
|
75
|
+
implemented: list[str] = []
|
|
76
|
+
if impl_match:
|
|
77
|
+
implemented = [s.strip() for s in impl_match.group(2).split(",")]
|
|
78
|
+
|
|
79
|
+
for iface in implemented:
|
|
80
|
+
result.edges.append(GraphEdge(
|
|
81
|
+
source=class_id,
|
|
82
|
+
target=f"*:{iface}",
|
|
83
|
+
kind=EdgeKind.EXPORTS_RMI,
|
|
84
|
+
label=f"{class_name} exports {iface}",
|
|
85
|
+
))
|
|
86
|
+
|
|
87
|
+
# Detect registry bindings
|
|
88
|
+
for i, line in enumerate(lines):
|
|
89
|
+
m = _REGISTRY_BIND_RE.search(line)
|
|
90
|
+
if m:
|
|
91
|
+
binding_name = m.group(1)
|
|
92
|
+
# Find class context
|
|
93
|
+
class_name = self._find_enclosing_class(lines, i)
|
|
94
|
+
if class_name:
|
|
95
|
+
class_id = f"{ctx.file_path}:{class_name}"
|
|
96
|
+
result.edges.append(GraphEdge(
|
|
97
|
+
source=class_id,
|
|
98
|
+
target=f"rmi:binding:{binding_name}",
|
|
99
|
+
kind=EdgeKind.EXPORTS_RMI,
|
|
100
|
+
label=f"{class_name} binds {binding_name}",
|
|
101
|
+
properties={"binding_name": binding_name},
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
# Detect registry lookups
|
|
105
|
+
for i, line in enumerate(lines):
|
|
106
|
+
m = _REGISTRY_LOOKUP_RE.search(line)
|
|
107
|
+
if m:
|
|
108
|
+
binding_name = m.group(1)
|
|
109
|
+
class_name = self._find_enclosing_class(lines, i)
|
|
110
|
+
if class_name:
|
|
111
|
+
class_id = f"{ctx.file_path}:{class_name}"
|
|
112
|
+
result.edges.append(GraphEdge(
|
|
113
|
+
source=class_id,
|
|
114
|
+
target=f"rmi:binding:{binding_name}",
|
|
115
|
+
kind=EdgeKind.INVOKES_RMI,
|
|
116
|
+
label=f"{class_name} invokes {binding_name}",
|
|
117
|
+
properties={"binding_name": binding_name},
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _find_enclosing_class(lines: list[str], line_idx: int) -> str | None:
|
|
124
|
+
class_re = re.compile(r"class\s+(\w+)")
|
|
125
|
+
for i in range(line_idx, -1, -1):
|
|
126
|
+
m = class_re.search(lines[i])
|
|
127
|
+
if m:
|
|
128
|
+
return m.group(1)
|
|
129
|
+
return None
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Spring application events 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
|
+
_EVENT_LISTENER_RE = re.compile(r"@EventListener")
|
|
19
|
+
_TRANSACTIONAL_EVENT_RE = re.compile(r"@TransactionalEventListener")
|
|
20
|
+
_PUBLISH_RE = re.compile(
|
|
21
|
+
r"(?:applicationEventPublisher|eventPublisher|publisher)\s*\.\s*publishEvent\s*\(\s*"
|
|
22
|
+
r"(?:new\s+(\w+)|(\w+))"
|
|
23
|
+
)
|
|
24
|
+
_METHOD_PARAM_RE = re.compile(r"(?:public|protected|private)?\s*\w+\s+(\w+)\s*\(\s*(\w+)\s+\w+\)")
|
|
25
|
+
_EVENT_CLASS_RE = re.compile(r"class\s+(\w+)\s+extends\s+(?:ApplicationEvent|AbstractEvent|\w*Event)")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SpringEventsDetector:
|
|
29
|
+
"""Detects Spring event listeners and publishers."""
|
|
30
|
+
|
|
31
|
+
name: str = "spring_events"
|
|
32
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
33
|
+
|
|
34
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
35
|
+
result = DetectorResult()
|
|
36
|
+
text = decode_text(ctx)
|
|
37
|
+
lines = text.split("\n")
|
|
38
|
+
|
|
39
|
+
has_listener = "@EventListener" in text or "@TransactionalEventListener" in text
|
|
40
|
+
has_publisher = "publishEvent" in text
|
|
41
|
+
has_event_class = _EVENT_CLASS_RE.search(text)
|
|
42
|
+
|
|
43
|
+
if not has_listener and not has_publisher and not has_event_class:
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
# Find class name
|
|
47
|
+
class_name: str | None = None
|
|
48
|
+
class_line: int = 0
|
|
49
|
+
for i, line in enumerate(lines):
|
|
50
|
+
cm = _CLASS_RE.search(line)
|
|
51
|
+
if cm:
|
|
52
|
+
class_name = cm.group(1)
|
|
53
|
+
class_line = i + 1
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if not class_name:
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
class_node_id = f"{ctx.file_path}:{class_name}"
|
|
60
|
+
seen_events: set[str] = set()
|
|
61
|
+
|
|
62
|
+
def _ensure_event_node(event_type: str) -> str:
|
|
63
|
+
event_id = f"event:{event_type}"
|
|
64
|
+
if event_type not in seen_events:
|
|
65
|
+
seen_events.add(event_type)
|
|
66
|
+
result.nodes.append(GraphNode(
|
|
67
|
+
id=event_id,
|
|
68
|
+
kind=NodeKind.EVENT,
|
|
69
|
+
label=event_type,
|
|
70
|
+
properties={"event_class": event_type},
|
|
71
|
+
))
|
|
72
|
+
return event_id
|
|
73
|
+
|
|
74
|
+
# If this file defines an event class, register it
|
|
75
|
+
if has_event_class:
|
|
76
|
+
event_name = has_event_class.group(1)
|
|
77
|
+
_ensure_event_node(event_name)
|
|
78
|
+
|
|
79
|
+
# Detect @EventListener / @TransactionalEventListener
|
|
80
|
+
for i, line in enumerate(lines):
|
|
81
|
+
if not (_EVENT_LISTENER_RE.search(line) or _TRANSACTIONAL_EVENT_RE.search(line)):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Find method and its parameter type (the event type)
|
|
85
|
+
event_type: str | None = None
|
|
86
|
+
for k in range(i + 1, min(i + 5, len(lines))):
|
|
87
|
+
pm = _METHOD_PARAM_RE.search(lines[k])
|
|
88
|
+
if pm:
|
|
89
|
+
event_type = pm.group(2)
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
if event_type:
|
|
93
|
+
event_id = _ensure_event_node(event_type)
|
|
94
|
+
result.edges.append(GraphEdge(
|
|
95
|
+
source=class_node_id,
|
|
96
|
+
target=event_id,
|
|
97
|
+
kind=EdgeKind.LISTENS,
|
|
98
|
+
label=f"{class_name} listens to {event_type}",
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
# Detect publishEvent calls
|
|
102
|
+
for i, line in enumerate(lines):
|
|
103
|
+
m = _PUBLISH_RE.search(line)
|
|
104
|
+
if not m:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
event_type = m.group(1) or m.group(2)
|
|
108
|
+
if event_type:
|
|
109
|
+
event_id = _ensure_event_node(event_type)
|
|
110
|
+
result.edges.append(GraphEdge(
|
|
111
|
+
source=class_node_id,
|
|
112
|
+
target=event_id,
|
|
113
|
+
kind=EdgeKind.PUBLISHES,
|
|
114
|
+
label=f"{class_name} publishes {event_type}",
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
return result
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Spring REST endpoint 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
|
+
# Mapping annotations match patterns
|
|
19
|
+
_MAPPING_ANNOTATIONS = {
|
|
20
|
+
"RequestMapping": None, # method determined from annotation attributes
|
|
21
|
+
"GetMapping": "GET",
|
|
22
|
+
"PostMapping": "POST",
|
|
23
|
+
"PutMapping": "PUT",
|
|
24
|
+
"DeleteMapping": "DELETE",
|
|
25
|
+
"PatchMapping": "PATCH",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_MAPPING_RE = re.compile(
|
|
29
|
+
r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)"
|
|
30
|
+
r"\s*(?:\(([^)]*)\))?"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_CLASS_RE = re.compile(r"(?:public\s+)?class\s+(\w+)")
|
|
34
|
+
_VALUE_RE = re.compile(r'(?:value\s*=\s*|path\s*=\s*)?\{?\s*"([^"]*)"')
|
|
35
|
+
_METHOD_ATTR_RE = re.compile(r'method\s*=\s*RequestMethod\.(\w+)')
|
|
36
|
+
_PRODUCES_RE = re.compile(r'produces\s*=\s*\{?\s*"([^"]*)"')
|
|
37
|
+
_CONSUMES_RE = re.compile(r'consumes\s*=\s*\{?\s*"([^"]*)"')
|
|
38
|
+
_JAVA_METHOD_RE = re.compile(
|
|
39
|
+
r'(?:public|protected|private)?\s*(?:static\s+)?(?:[\w<>\[\],\s]+)\s+(\w+)\s*\('
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _extract_attr(attr_str: str | None, pattern: re.Pattern[str]) -> str | None:
|
|
44
|
+
if attr_str is None:
|
|
45
|
+
return None
|
|
46
|
+
m = pattern.search(attr_str)
|
|
47
|
+
return m.group(1) if m else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SpringRestDetector:
|
|
52
|
+
"""Detects Spring REST endpoints from mapping annotations."""
|
|
53
|
+
|
|
54
|
+
name: str = "spring_rest"
|
|
55
|
+
supported_languages: tuple[str, ...] = ("java",)
|
|
56
|
+
|
|
57
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
58
|
+
result = DetectorResult()
|
|
59
|
+
text = decode_text(ctx)
|
|
60
|
+
lines = text.split("\n")
|
|
61
|
+
|
|
62
|
+
# Find class name
|
|
63
|
+
class_name: str | None = None
|
|
64
|
+
class_base_path = ""
|
|
65
|
+
for i, line in enumerate(lines):
|
|
66
|
+
cm = _CLASS_RE.search(line)
|
|
67
|
+
if cm:
|
|
68
|
+
class_name = cm.group(1)
|
|
69
|
+
# Look backwards for class-level @RequestMapping
|
|
70
|
+
for j in range(max(0, i - 5), i):
|
|
71
|
+
mm = _MAPPING_RE.search(lines[j])
|
|
72
|
+
if mm and mm.group(1) == "RequestMapping":
|
|
73
|
+
path = _extract_attr(mm.group(2), _VALUE_RE)
|
|
74
|
+
if path:
|
|
75
|
+
class_base_path = path.rstrip("/")
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
if not class_name:
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
class_node_id = f"{ctx.file_path}:{class_name}"
|
|
82
|
+
|
|
83
|
+
# Scan for method-level mapping annotations
|
|
84
|
+
for i, line in enumerate(lines):
|
|
85
|
+
m = _MAPPING_RE.search(line)
|
|
86
|
+
if not m:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
annotation_name = m.group(1)
|
|
90
|
+
attr_str = m.group(2)
|
|
91
|
+
|
|
92
|
+
# Skip class-level RequestMapping (already handled)
|
|
93
|
+
# Heuristic: if next non-empty, non-annotation line has 'class ', skip
|
|
94
|
+
is_class_level = False
|
|
95
|
+
for k in range(i + 1, min(i + 5, len(lines))):
|
|
96
|
+
stripped = lines[k].strip()
|
|
97
|
+
if stripped.startswith("@") or not stripped:
|
|
98
|
+
continue
|
|
99
|
+
if "class " in stripped or "interface " in stripped:
|
|
100
|
+
is_class_level = True
|
|
101
|
+
break
|
|
102
|
+
if is_class_level:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Determine HTTP method
|
|
106
|
+
http_method = _MAPPING_ANNOTATIONS[annotation_name]
|
|
107
|
+
if http_method is None:
|
|
108
|
+
extracted = _extract_attr(attr_str, _METHOD_ATTR_RE)
|
|
109
|
+
http_method = extracted if extracted else "GET"
|
|
110
|
+
|
|
111
|
+
# Extract path
|
|
112
|
+
path = _extract_attr(attr_str, _VALUE_RE)
|
|
113
|
+
if path is None and attr_str:
|
|
114
|
+
# bare string value like @GetMapping("/foo")
|
|
115
|
+
bare = re.search(r'"([^"]*)"', attr_str or "")
|
|
116
|
+
if bare:
|
|
117
|
+
path = bare.group(1)
|
|
118
|
+
path = path or ""
|
|
119
|
+
|
|
120
|
+
full_path = f"{class_base_path}/{path.lstrip('/')}" if path else class_base_path or "/"
|
|
121
|
+
if not full_path.startswith("/"):
|
|
122
|
+
full_path = "/" + full_path
|
|
123
|
+
|
|
124
|
+
# Extract produces/consumes
|
|
125
|
+
produces = _extract_attr(attr_str, _PRODUCES_RE)
|
|
126
|
+
consumes = _extract_attr(attr_str, _CONSUMES_RE)
|
|
127
|
+
|
|
128
|
+
# Find the method name on subsequent lines
|
|
129
|
+
method_name = None
|
|
130
|
+
for k in range(i + 1, min(i + 5, len(lines))):
|
|
131
|
+
mm = _JAVA_METHOD_RE.search(lines[k])
|
|
132
|
+
if mm:
|
|
133
|
+
method_name = mm.group(1)
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
endpoint_label = f"{http_method} {full_path}"
|
|
137
|
+
endpoint_id = f"{ctx.file_path}:{class_name}:{method_name or 'unknown'}:{http_method}:{full_path}"
|
|
138
|
+
|
|
139
|
+
properties: dict[str, Any] = {
|
|
140
|
+
"http_method": http_method,
|
|
141
|
+
"path": full_path,
|
|
142
|
+
}
|
|
143
|
+
if produces:
|
|
144
|
+
properties["produces"] = produces
|
|
145
|
+
if consumes:
|
|
146
|
+
properties["consumes"] = consumes
|
|
147
|
+
|
|
148
|
+
node = GraphNode(
|
|
149
|
+
id=endpoint_id,
|
|
150
|
+
kind=NodeKind.ENDPOINT,
|
|
151
|
+
label=endpoint_label,
|
|
152
|
+
fqn=f"{class_name}.{method_name}" if method_name else class_name,
|
|
153
|
+
module=ctx.module_name,
|
|
154
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=i + 1),
|
|
155
|
+
annotations=[f"@{annotation_name}"],
|
|
156
|
+
properties=properties,
|
|
157
|
+
)
|
|
158
|
+
result.nodes.append(node)
|
|
159
|
+
|
|
160
|
+
edge = GraphEdge(
|
|
161
|
+
source=class_node_id,
|
|
162
|
+
target=endpoint_id,
|
|
163
|
+
kind=EdgeKind.EXPOSES,
|
|
164
|
+
label=f"{class_name} exposes {endpoint_label}",
|
|
165
|
+
)
|
|
166
|
+
result.edges.append(edge)
|
|
167
|
+
|
|
168
|
+
return result
|