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