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,117 @@
1
+ """Markdown structure detector for headings and internal links."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
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
+ _HEADING_RE = re.compile(r'^(#{1,6})\s+(.+)$', re.MULTILINE)
19
+ _LINK_RE = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
20
+ _EXTERNAL_RE = re.compile(r'^https?://')
21
+
22
+
23
+ class MarkdownStructureDetector:
24
+ """Detects Markdown headings and internal file links."""
25
+
26
+ name: str = "markdown_structure"
27
+ supported_languages: tuple[str, ...] = ("markdown",)
28
+
29
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
30
+ result = DetectorResult()
31
+
32
+ try:
33
+ text = decode_text(ctx)
34
+ except Exception:
35
+ return result
36
+
37
+ filepath = ctx.file_path
38
+ lines = text.split("\n")
39
+
40
+ # Find first H1 for module label
41
+ first_h1: str | None = None
42
+ for line in lines:
43
+ m = _HEADING_RE.match(line)
44
+ if m and len(m.group(1)) == 1:
45
+ first_h1 = m.group(2).strip()
46
+ break
47
+
48
+ module_label = first_h1 or os.path.basename(filepath)
49
+ module_id = f"md:{filepath}"
50
+
51
+ # MODULE node for the file
52
+ result.nodes.append(GraphNode(
53
+ id=module_id,
54
+ kind=NodeKind.MODULE,
55
+ label=module_label,
56
+ fqn=filepath,
57
+ module=ctx.module_name,
58
+ location=SourceLocation(
59
+ file_path=filepath,
60
+ line_start=1,
61
+ ),
62
+ ))
63
+
64
+ # CONFIG_KEY nodes for each heading
65
+ for i, line in enumerate(lines):
66
+ m = _HEADING_RE.match(line)
67
+ if not m:
68
+ continue
69
+ level = len(m.group(1))
70
+ heading_text = m.group(2).strip()
71
+ line_num = i + 1
72
+
73
+ heading_id = f"md:{filepath}:heading:{line_num}"
74
+ result.nodes.append(GraphNode(
75
+ id=heading_id,
76
+ kind=NodeKind.CONFIG_KEY,
77
+ label=heading_text,
78
+ fqn=f"{filepath}:heading:{heading_text}",
79
+ module=ctx.module_name,
80
+ location=SourceLocation(
81
+ file_path=filepath,
82
+ line_start=line_num,
83
+ ),
84
+ properties={"level": level, "text": heading_text},
85
+ ))
86
+
87
+ result.edges.append(GraphEdge(
88
+ source=module_id,
89
+ target=heading_id,
90
+ kind=EdgeKind.CONTAINS,
91
+ label=f"{filepath} contains heading {heading_text}",
92
+ ))
93
+
94
+ # DEPENDS_ON edges for internal links
95
+ for i, line in enumerate(lines):
96
+ for m in _LINK_RE.finditer(line):
97
+ link_text = m.group(1)
98
+ link_target = m.group(2)
99
+
100
+ # Skip external URLs
101
+ if _EXTERNAL_RE.match(link_target):
102
+ continue
103
+
104
+ # Strip anchor fragments
105
+ link_path = link_target.split("#")[0]
106
+ if not link_path:
107
+ continue
108
+
109
+ result.edges.append(GraphEdge(
110
+ source=module_id,
111
+ target=link_path,
112
+ kind=EdgeKind.DEPENDS_ON,
113
+ label=f"{filepath} links to {link_path}",
114
+ properties={"link_text": link_text},
115
+ ))
116
+
117
+ return result
File without changes
@@ -0,0 +1,177 @@
1
+ """Angular component, service, directive, pipe, and module detector."""
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 GraphNode, NodeKind, SourceLocation
10
+
11
+
12
+ class AngularComponentDetector:
13
+ """Detects Angular @Component, @Injectable, @Directive, @Pipe, and @NgModule decorators."""
14
+
15
+ name: str = "frontend.angular_components"
16
+ supported_languages: tuple[str, ...] = ("typescript",)
17
+
18
+ # @Component({ selector: 'app-name' }) followed by class Name
19
+ _COMPONENT_DECORATOR = re.compile(
20
+ r"@Component\s*\(\s*\{.*?selector\s*:\s*['\"]([^'\"]+)['\"].*?\}\s*\)\s*\n?\s*(?:export\s+)?class\s+(\w+)",
21
+ re.DOTALL,
22
+ )
23
+
24
+ # @Injectable({ providedIn: 'root' }) followed by class Name
25
+ _INJECTABLE_DECORATOR = re.compile(
26
+ r"@Injectable\s*\(\s*\{.*?providedIn\s*:\s*['\"](\w+)['\"].*?\}\s*\)\s*\n?\s*(?:export\s+)?class\s+(\w+)",
27
+ re.DOTALL,
28
+ )
29
+
30
+ # @Directive({ selector: '[appHighlight]' }) followed by class Name
31
+ _DIRECTIVE_DECORATOR = re.compile(
32
+ r"@Directive\s*\(\s*\{.*?selector\s*:\s*['\"]([^'\"]+)['\"].*?\}\s*\)\s*\n?\s*(?:export\s+)?class\s+(\w+)",
33
+ re.DOTALL,
34
+ )
35
+
36
+ # @Pipe({ name: 'pipeName' }) followed by class Name
37
+ _PIPE_DECORATOR = re.compile(
38
+ r"@Pipe\s*\(\s*\{.*?name\s*:\s*['\"](\w+)['\"].*?\}\s*\)\s*\n?\s*(?:export\s+)?class\s+(\w+)",
39
+ re.DOTALL,
40
+ )
41
+
42
+ # @NgModule({ declarations: [...] }) followed by class Name
43
+ _NGMODULE_DECORATOR = re.compile(
44
+ r"@NgModule\s*\(\s*\{.*?\}\s*\)\s*\n?\s*(?:export\s+)?class\s+(\w+)",
45
+ re.DOTALL,
46
+ )
47
+
48
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
49
+ result = DetectorResult()
50
+ text = decode_text(ctx)
51
+
52
+ seen: set[str] = set()
53
+
54
+ # --- @Component ---
55
+ for match in self._COMPONENT_DECORATOR.finditer(text):
56
+ selector = match.group(1)
57
+ class_name = match.group(2)
58
+ if class_name in seen:
59
+ continue
60
+ seen.add(class_name)
61
+ line = text[: match.start()].count("\n") + 1
62
+ node_id = f"angular:{ctx.file_path}:component:{class_name}"
63
+ result.nodes.append(
64
+ GraphNode(
65
+ id=node_id,
66
+ kind=NodeKind.COMPONENT,
67
+ label=class_name,
68
+ fqn=f"{ctx.file_path}::{class_name}",
69
+ module=ctx.module_name,
70
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
71
+ properties={
72
+ "framework": "angular",
73
+ "selector": selector,
74
+ "decorator": "Component",
75
+ },
76
+ )
77
+ )
78
+
79
+ # --- @Injectable (services) ---
80
+ for match in self._INJECTABLE_DECORATOR.finditer(text):
81
+ provided_in = match.group(1)
82
+ class_name = match.group(2)
83
+ if class_name in seen:
84
+ continue
85
+ seen.add(class_name)
86
+ line = text[: match.start()].count("\n") + 1
87
+ node_id = f"angular:{ctx.file_path}:service:{class_name}"
88
+ result.nodes.append(
89
+ GraphNode(
90
+ id=node_id,
91
+ kind=NodeKind.MIDDLEWARE,
92
+ label=class_name,
93
+ fqn=f"{ctx.file_path}::{class_name}",
94
+ module=ctx.module_name,
95
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
96
+ properties={
97
+ "framework": "angular",
98
+ "provided_in": provided_in,
99
+ "decorator": "Injectable",
100
+ },
101
+ )
102
+ )
103
+
104
+ # --- @Directive ---
105
+ for match in self._DIRECTIVE_DECORATOR.finditer(text):
106
+ selector = match.group(1)
107
+ class_name = match.group(2)
108
+ if class_name in seen:
109
+ continue
110
+ seen.add(class_name)
111
+ line = text[: match.start()].count("\n") + 1
112
+ node_id = f"angular:{ctx.file_path}:component:{class_name}"
113
+ result.nodes.append(
114
+ GraphNode(
115
+ id=node_id,
116
+ kind=NodeKind.COMPONENT,
117
+ label=class_name,
118
+ fqn=f"{ctx.file_path}::{class_name}",
119
+ module=ctx.module_name,
120
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
121
+ properties={
122
+ "framework": "angular",
123
+ "selector": selector,
124
+ "decorator": "Directive",
125
+ },
126
+ )
127
+ )
128
+
129
+ # --- @Pipe ---
130
+ for match in self._PIPE_DECORATOR.finditer(text):
131
+ pipe_name = match.group(1)
132
+ class_name = match.group(2)
133
+ if class_name in seen:
134
+ continue
135
+ seen.add(class_name)
136
+ line = text[: match.start()].count("\n") + 1
137
+ node_id = f"angular:{ctx.file_path}:component:{class_name}"
138
+ result.nodes.append(
139
+ GraphNode(
140
+ id=node_id,
141
+ kind=NodeKind.COMPONENT,
142
+ label=class_name,
143
+ fqn=f"{ctx.file_path}::{class_name}",
144
+ module=ctx.module_name,
145
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
146
+ properties={
147
+ "framework": "angular",
148
+ "pipe_name": pipe_name,
149
+ "decorator": "Pipe",
150
+ },
151
+ )
152
+ )
153
+
154
+ # --- @NgModule ---
155
+ for match in self._NGMODULE_DECORATOR.finditer(text):
156
+ class_name = match.group(1)
157
+ if class_name in seen:
158
+ continue
159
+ seen.add(class_name)
160
+ line = text[: match.start()].count("\n") + 1
161
+ node_id = f"angular:{ctx.file_path}:component:{class_name}"
162
+ result.nodes.append(
163
+ GraphNode(
164
+ id=node_id,
165
+ kind=NodeKind.COMPONENT,
166
+ label=class_name,
167
+ fqn=f"{ctx.file_path}::{class_name}",
168
+ module=ctx.module_name,
169
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
170
+ properties={
171
+ "framework": "angular",
172
+ "decorator": "NgModule",
173
+ },
174
+ )
175
+ )
176
+
177
+ return result
@@ -0,0 +1,259 @@
1
+ """Frontend route detector for React Router, Vue Router, Next.js, and Angular."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import PurePosixPath
7
+
8
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
9
+ from osscodeiq.detectors.utils import decode_text
10
+ from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
11
+
12
+
13
+ class FrontendRouteDetector:
14
+ """Detects frontend routing definitions across major frameworks."""
15
+
16
+ name: str = "frontend.frontend_routes"
17
+ supported_languages: tuple[str, ...] = ("typescript", "javascript", "vue", "svelte")
18
+
19
+ # --- React Router patterns ---
20
+ # <Route path="/foo" component={Bar}> or <Route path="/foo" element={<Bar />}>
21
+ _REACT_ROUTE_COMPONENT = re.compile(
22
+ r"<Route\s+[^>]*?path\s*=\s*[\"']([^\"']+)[\"'][^>]*?"
23
+ r"component\s*=\s*\{(\w+)\}",
24
+ )
25
+ _REACT_ROUTE_ELEMENT = re.compile(
26
+ r"<Route\s+[^>]*?path\s*=\s*[\"']([^\"']+)[\"'][^>]*?"
27
+ r"element\s*=\s*\{<(\w+)",
28
+ )
29
+ # <Route path="/foo"> (no component/element yet, or nested)
30
+ _REACT_ROUTE_BARE = re.compile(
31
+ r"<Route\s+[^>]*?path\s*=\s*[\"']([^\"']+)[\"']",
32
+ )
33
+
34
+ # --- Vue Router patterns ---
35
+ # { path: '/foo', component: Bar } or { path: '/foo', component: () => import(...) }
36
+ _VUE_ROUTE = re.compile(
37
+ r"\{\s*path\s*:\s*['\"]([^'\"]+)['\"]"
38
+ r"(?:.*?component\s*:\s*(\w+))?"
39
+ )
40
+ _VUE_CREATE_ROUTER = re.compile(r"createRouter\s*\(")
41
+ _VUE_ROUTES_ARRAY = re.compile(r"\broutes\s*:\s*\[")
42
+
43
+ # --- Angular patterns ---
44
+ _ANGULAR_ROUTE = re.compile(
45
+ r"\{\s*path\s*:\s*['\"]([^'\"]+)['\"]"
46
+ r"(?:.*?component\s*:\s*(\w+))?"
47
+ )
48
+ _ANGULAR_ROUTER_MODULE = re.compile(
49
+ r"RouterModule\.for(?:Root|Child)\s*\("
50
+ )
51
+
52
+ # --- Next.js file-based routing ---
53
+ # pages/index.tsx, pages/about.tsx, pages/users/[id].tsx
54
+ _NEXTJS_PAGES = re.compile(r"^pages/(.+)\.(tsx|ts|jsx|js)$")
55
+ # app/**/page.tsx (App Router)
56
+ _NEXTJS_APP = re.compile(r"^app/(.+)/page\.(tsx|ts|jsx|js)$")
57
+
58
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
59
+ result = DetectorResult()
60
+ text = decode_text(ctx)
61
+
62
+ self._detect_nextjs_file_routes(ctx, result)
63
+ self._detect_react_router(ctx, text, result)
64
+ self._detect_vue_router(ctx, text, result)
65
+ self._detect_angular_router(ctx, text, result)
66
+
67
+ return result
68
+
69
+ def _detect_nextjs_file_routes(
70
+ self, ctx: DetectorContext, result: DetectorResult
71
+ ) -> None:
72
+ """Detect Next.js file-based routes from file path alone."""
73
+ fp = ctx.file_path
74
+
75
+ match = self._NEXTJS_PAGES.match(fp)
76
+ if match:
77
+ raw = match.group(1)
78
+ route_path = self._nextjs_pages_path(raw)
79
+ node_id = f"route:{fp}:nextjs:{route_path}"
80
+ result.nodes.append(
81
+ GraphNode(
82
+ id=node_id,
83
+ kind=NodeKind.ENDPOINT,
84
+ label=f"page {route_path}",
85
+ fqn=f"{fp}::page",
86
+ module=ctx.module_name,
87
+ location=SourceLocation(file_path=fp, line_start=1),
88
+ properties={
89
+ "protocol": "frontend_route",
90
+ "framework": "nextjs",
91
+ "route_path": route_path,
92
+ },
93
+ )
94
+ )
95
+ return
96
+
97
+ match = self._NEXTJS_APP.match(fp)
98
+ if match:
99
+ raw = match.group(1)
100
+ route_path = "/" + raw.replace("\\", "/")
101
+ node_id = f"route:{fp}:nextjs:{route_path}"
102
+ result.nodes.append(
103
+ GraphNode(
104
+ id=node_id,
105
+ kind=NodeKind.ENDPOINT,
106
+ label=f"page {route_path}",
107
+ fqn=f"{fp}::page",
108
+ module=ctx.module_name,
109
+ location=SourceLocation(file_path=fp, line_start=1),
110
+ properties={
111
+ "protocol": "frontend_route",
112
+ "framework": "nextjs",
113
+ "route_path": route_path,
114
+ },
115
+ )
116
+ )
117
+
118
+ @staticmethod
119
+ def _nextjs_pages_path(raw: str) -> str:
120
+ """Convert a pages-directory relative path to a route path."""
121
+ # pages/index -> /
122
+ # pages/about -> /about
123
+ # pages/users/[id] -> /users/[id]
124
+ parts = raw.replace("\\", "/").split("/")
125
+ # Remove trailing 'index'
126
+ if parts and parts[-1] == "index":
127
+ parts = parts[:-1]
128
+ route = "/" + "/".join(parts) if parts else "/"
129
+ return route
130
+
131
+ def _detect_react_router(
132
+ self, ctx: DetectorContext, text: str, result: DetectorResult
133
+ ) -> None:
134
+ """Detect React Router route definitions."""
135
+ seen_paths: set[str] = set()
136
+
137
+ # <Route path="..." component={Comp}>
138
+ for match in self._REACT_ROUTE_COMPONENT.finditer(text):
139
+ path = match.group(1)
140
+ component = match.group(2)
141
+ if path in seen_paths:
142
+ continue
143
+ seen_paths.add(path)
144
+ line = text[: match.start()].count("\n") + 1
145
+ node_id = f"route:{ctx.file_path}:react:{path}"
146
+ result.nodes.append(self._route_node(
147
+ node_id, path, "react", ctx, line,
148
+ ))
149
+ # RENDERS edge to component
150
+ result.edges.append(GraphEdge(
151
+ source=node_id,
152
+ target=component,
153
+ kind=EdgeKind.RENDERS,
154
+ label=f"renders {component}",
155
+ ))
156
+
157
+ # <Route path="..." element={<Comp />}>
158
+ for match in self._REACT_ROUTE_ELEMENT.finditer(text):
159
+ path = match.group(1)
160
+ component = match.group(2)
161
+ if path in seen_paths:
162
+ continue
163
+ seen_paths.add(path)
164
+ line = text[: match.start()].count("\n") + 1
165
+ node_id = f"route:{ctx.file_path}:react:{path}"
166
+ result.nodes.append(self._route_node(
167
+ node_id, path, "react", ctx, line,
168
+ ))
169
+ result.edges.append(GraphEdge(
170
+ source=node_id,
171
+ target=component,
172
+ kind=EdgeKind.RENDERS,
173
+ label=f"renders {component}",
174
+ ))
175
+
176
+ # Bare <Route path="..."> (no component/element captured above)
177
+ for match in self._REACT_ROUTE_BARE.finditer(text):
178
+ path = match.group(1)
179
+ if path in seen_paths:
180
+ continue
181
+ seen_paths.add(path)
182
+ line = text[: match.start()].count("\n") + 1
183
+ node_id = f"route:{ctx.file_path}:react:{path}"
184
+ result.nodes.append(self._route_node(
185
+ node_id, path, "react", ctx, line,
186
+ ))
187
+
188
+ def _detect_vue_router(
189
+ self, ctx: DetectorContext, text: str, result: DetectorResult
190
+ ) -> None:
191
+ """Detect Vue Router route definitions."""
192
+ has_create_router = bool(self._VUE_CREATE_ROUTER.search(text))
193
+ has_routes_array = bool(self._VUE_ROUTES_ARRAY.search(text))
194
+
195
+ if not (has_create_router or has_routes_array):
196
+ return
197
+
198
+ for match in self._VUE_ROUTE.finditer(text):
199
+ path = match.group(1)
200
+ component = match.group(2)
201
+ line = text[: match.start()].count("\n") + 1
202
+ node_id = f"route:{ctx.file_path}:vue:{path}"
203
+ result.nodes.append(self._route_node(
204
+ node_id, path, "vue", ctx, line,
205
+ ))
206
+ if component:
207
+ result.edges.append(GraphEdge(
208
+ source=node_id,
209
+ target=component,
210
+ kind=EdgeKind.RENDERS,
211
+ label=f"renders {component}",
212
+ ))
213
+
214
+ def _detect_angular_router(
215
+ self, ctx: DetectorContext, text: str, result: DetectorResult
216
+ ) -> None:
217
+ """Detect Angular Router route definitions."""
218
+ has_router_module = bool(self._ANGULAR_ROUTER_MODULE.search(text))
219
+
220
+ if not has_router_module:
221
+ return
222
+
223
+ for match in self._ANGULAR_ROUTE.finditer(text):
224
+ path = match.group(1)
225
+ component = match.group(2)
226
+ line = text[: match.start()].count("\n") + 1
227
+ node_id = f"route:{ctx.file_path}:angular:{path}"
228
+ result.nodes.append(self._route_node(
229
+ node_id, path, "angular", ctx, line,
230
+ ))
231
+ if component:
232
+ result.edges.append(GraphEdge(
233
+ source=node_id,
234
+ target=component,
235
+ kind=EdgeKind.RENDERS,
236
+ label=f"renders {component}",
237
+ ))
238
+
239
+ @staticmethod
240
+ def _route_node(
241
+ node_id: str,
242
+ path: str,
243
+ framework: str,
244
+ ctx: DetectorContext,
245
+ line: int,
246
+ ) -> GraphNode:
247
+ return GraphNode(
248
+ id=node_id,
249
+ kind=NodeKind.ENDPOINT,
250
+ label=f"route {path}",
251
+ fqn=f"{ctx.file_path}::route:{path}",
252
+ module=ctx.module_name,
253
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
254
+ properties={
255
+ "protocol": "frontend_route",
256
+ "framework": framework,
257
+ "route_path": path,
258
+ },
259
+ )