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,160 @@
1
+ """Remix route 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 RemixRouteDetector:
13
+ """Detects Remix loader, action exports, default component exports, and data hooks."""
14
+
15
+ name: str = "remix_routes"
16
+ supported_languages: tuple[str, ...] = ("typescript", "javascript")
17
+
18
+ # export async function loader( or export function loader(
19
+ _LOADER_PATTERN = re.compile(
20
+ r"export\s+(?:async\s+)?function\s+loader\s*\("
21
+ )
22
+
23
+ # export async function action( or export function action(
24
+ _ACTION_PATTERN = re.compile(
25
+ r"export\s+(?:async\s+)?function\s+action\s*\("
26
+ )
27
+
28
+ # export default function ComponentName( or export default function(
29
+ _DEFAULT_COMPONENT_PATTERN = re.compile(
30
+ r"export\s+default\s+function\s+(\w*)\s*\("
31
+ )
32
+
33
+ # useLoaderData() or useActionData()
34
+ _USE_LOADER_DATA = re.compile(r"\buseLoaderData\s*\(\s*\)")
35
+ _USE_ACTION_DATA = re.compile(r"\buseActionData\s*\(\s*\)")
36
+
37
+ def _derive_route_path(self, file_path: str) -> str | None:
38
+ """Derive route path from Remix file path convention.
39
+
40
+ app/routes/users.tsx -> /users
41
+ app/routes/users.$id.tsx -> /users/:id
42
+ app/routes/_index.tsx -> /
43
+ app/routes/users_.tsx -> /users
44
+ app/routes/blog.articles.tsx -> /blog/articles
45
+ """
46
+ # Only process files under app/routes/
47
+ if "app/routes/" not in file_path:
48
+ return None
49
+
50
+ # Extract the route segment after app/routes/
51
+ segment = file_path.split("app/routes/", 1)[1]
52
+
53
+ # Remove file extension
54
+ segment = re.sub(r"\.(tsx?|jsx?)$", "", segment)
55
+
56
+ # Handle _index convention
57
+ if segment == "_index" or segment.endswith("/_index"):
58
+ prefix = segment.rsplit("_index", 1)[0].rstrip("/.")
59
+ if not prefix:
60
+ return "/"
61
+ return "/" + prefix.replace(".", "/")
62
+
63
+ # Replace Remix $ params with :param style
64
+ parts = segment.split(".")
65
+ path_parts = []
66
+ for part in parts:
67
+ if part.startswith("$"):
68
+ path_parts.append(f":{part[1:]}")
69
+ elif part.endswith("_"):
70
+ # Pathless layout route - include the segment but it's a layout
71
+ path_parts.append(part.rstrip("_"))
72
+ else:
73
+ path_parts.append(part)
74
+
75
+ return "/" + "/".join(path_parts)
76
+
77
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
78
+ result = DetectorResult()
79
+ text = decode_text(ctx)
80
+ route_path = self._derive_route_path(ctx.file_path)
81
+
82
+ # Detect loader exports
83
+ for match in self._LOADER_PATTERN.finditer(text):
84
+ line = text[: match.start()].count("\n") + 1
85
+ node_id = f"remix:{ctx.file_path}:loader:{line}"
86
+ properties: dict = {
87
+ "framework": "remix",
88
+ "type": "loader",
89
+ "http_method": "GET",
90
+ }
91
+ if route_path:
92
+ properties["route_path"] = route_path
93
+
94
+ result.nodes.append(
95
+ GraphNode(
96
+ id=node_id,
97
+ kind=NodeKind.ENDPOINT,
98
+ label=f"loader {route_path or ctx.file_path}",
99
+ fqn=f"{ctx.file_path}::loader",
100
+ module=ctx.module_name,
101
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
102
+ properties=properties,
103
+ )
104
+ )
105
+
106
+ # Detect action exports
107
+ for match in self._ACTION_PATTERN.finditer(text):
108
+ line = text[: match.start()].count("\n") + 1
109
+ node_id = f"remix:{ctx.file_path}:action:{line}"
110
+ properties = {
111
+ "framework": "remix",
112
+ "type": "action",
113
+ "http_method": "POST",
114
+ }
115
+ if route_path:
116
+ properties["route_path"] = route_path
117
+
118
+ result.nodes.append(
119
+ GraphNode(
120
+ id=node_id,
121
+ kind=NodeKind.ENDPOINT,
122
+ label=f"action {route_path or ctx.file_path}",
123
+ fqn=f"{ctx.file_path}::action",
124
+ module=ctx.module_name,
125
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
126
+ properties=properties,
127
+ )
128
+ )
129
+
130
+ # Detect default component export
131
+ for match in self._DEFAULT_COMPONENT_PATTERN.finditer(text):
132
+ comp_name = match.group(1) or "default"
133
+ line = text[: match.start()].count("\n") + 1
134
+ node_id = f"remix:{ctx.file_path}:component:{comp_name}"
135
+ properties: dict = {
136
+ "framework": "remix",
137
+ "type": "component",
138
+ }
139
+ if route_path:
140
+ properties["route_path"] = route_path
141
+
142
+ # Check for data hook usage
143
+ if self._USE_LOADER_DATA.search(text):
144
+ properties["uses_loader_data"] = True
145
+ if self._USE_ACTION_DATA.search(text):
146
+ properties["uses_action_data"] = True
147
+
148
+ result.nodes.append(
149
+ GraphNode(
150
+ id=node_id,
151
+ kind=NodeKind.COMPONENT,
152
+ label=comp_name,
153
+ fqn=f"{ctx.file_path}::{comp_name}",
154
+ module=ctx.module_name,
155
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
156
+ properties=properties,
157
+ )
158
+ )
159
+
160
+ return result
@@ -0,0 +1,136 @@
1
+ """Sequelize ORM detector for TypeScript/JavaScript."""
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 EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
10
+
11
+
12
+ class SequelizeORMDetector:
13
+ """Detects Sequelize ORM usage patterns in TypeScript/JavaScript files."""
14
+
15
+ name: str = "sequelize_orm"
16
+ supported_languages: tuple[str, ...] = ("typescript", "javascript")
17
+
18
+ # sequelize.define('ModelName', { ... })
19
+ _DEFINE_RE = re.compile(r"sequelize\.define\s*\(\s*['\"](\w+)['\"]")
20
+ # class User extends Model { ... }
21
+ _EXTENDS_MODEL_RE = re.compile(r"class\s+(\w+)\s+extends\s+Model\s*\{")
22
+ # Model.init({ ... }, { sequelize })
23
+ _MODEL_INIT_RE = re.compile(r"(\w+)\.init\s*\(\s*\{")
24
+ # new Sequelize( or new Sequelize.Sequelize(
25
+ _CONNECTION_RE = re.compile(r"new\s+Sequelize(?:\.Sequelize)?\s*\(")
26
+ # Model.belongsTo(, Model.hasMany(, etc.
27
+ _ASSOCIATION_RE = re.compile(
28
+ r"(\w+)\.(belongsTo|hasMany|hasOne|belongsToMany)\s*\(\s*(\w+)"
29
+ )
30
+ # Model.findAll(, Model.findOne(, Model.create(, etc.
31
+ _QUERY_RE = re.compile(
32
+ r"(\w+)\.(findAll|findOne|findByPk|findOrCreate|create|bulkCreate|update|destroy|count|max|min|sum)\s*\("
33
+ )
34
+
35
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
36
+ result = DetectorResult()
37
+ text = decode_text(ctx)
38
+
39
+ seen_models: dict[str, str] = {}
40
+
41
+ # Detect Sequelize connection -> DATABASE_CONNECTION
42
+ for match in self._CONNECTION_RE.finditer(text):
43
+ line = text[: match.start()].count("\n") + 1
44
+ node_id = f"sequelize:{ctx.file_path}:connection:{line}"
45
+ result.nodes.append(
46
+ GraphNode(
47
+ id=node_id,
48
+ kind=NodeKind.DATABASE_CONNECTION,
49
+ label="Sequelize",
50
+ fqn=f"{ctx.file_path}::Sequelize",
51
+ module=ctx.module_name,
52
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
53
+ properties={"framework": "sequelize"},
54
+ )
55
+ )
56
+
57
+ # Detect sequelize.define('ModelName', { ... }) -> ENTITY
58
+ for match in self._DEFINE_RE.finditer(text):
59
+ model_name = match.group(1)
60
+ line = text[: match.start()].count("\n") + 1
61
+ model_id = f"sequelize:{ctx.file_path}:model:{model_name}"
62
+ seen_models[model_name] = model_id
63
+ result.nodes.append(
64
+ GraphNode(
65
+ id=model_id,
66
+ kind=NodeKind.ENTITY,
67
+ label=model_name,
68
+ fqn=f"{ctx.file_path}::{model_name}",
69
+ module=ctx.module_name,
70
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
71
+ properties={"framework": "sequelize", "definition": "define"},
72
+ )
73
+ )
74
+
75
+ # Detect class X extends Model -> ENTITY
76
+ for match in self._EXTENDS_MODEL_RE.finditer(text):
77
+ class_name = match.group(1)
78
+ line = text[: match.start()].count("\n") + 1
79
+ if class_name not in seen_models:
80
+ model_id = f"sequelize:{ctx.file_path}:model:{class_name}"
81
+ seen_models[class_name] = model_id
82
+ result.nodes.append(
83
+ GraphNode(
84
+ id=model_id,
85
+ kind=NodeKind.ENTITY,
86
+ label=class_name,
87
+ fqn=f"{ctx.file_path}::{class_name}",
88
+ module=ctx.module_name,
89
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
90
+ properties={"framework": "sequelize", "definition": "class"},
91
+ )
92
+ )
93
+
94
+ # Detect associations -> DEPENDS_ON edges
95
+ for match in self._ASSOCIATION_RE.finditer(text):
96
+ source_model = match.group(1)
97
+ assoc_type = match.group(2)
98
+ target_model = match.group(3)
99
+ line = text[: match.start()].count("\n") + 1
100
+
101
+ source_id = seen_models.get(
102
+ source_model, f"sequelize:{ctx.file_path}:model:{source_model}"
103
+ )
104
+ target_id = seen_models.get(
105
+ target_model, f"sequelize:{ctx.file_path}:model:{target_model}"
106
+ )
107
+ result.edges.append(
108
+ GraphEdge(
109
+ source=source_id,
110
+ target=target_id,
111
+ kind=EdgeKind.DEPENDS_ON,
112
+ label=assoc_type,
113
+ properties={"association": assoc_type, "line": line},
114
+ )
115
+ )
116
+
117
+ # Detect query operations -> QUERIES edges
118
+ for match in self._QUERY_RE.finditer(text):
119
+ model_name = match.group(1)
120
+ operation = match.group(2)
121
+ line = text[: match.start()].count("\n") + 1
122
+
123
+ target_id = seen_models.get(
124
+ model_name, f"sequelize:{ctx.file_path}:model:{model_name}"
125
+ )
126
+ result.edges.append(
127
+ GraphEdge(
128
+ source=ctx.file_path,
129
+ target=target_id,
130
+ kind=EdgeKind.QUERIES,
131
+ label=f"{model_name}.{operation}",
132
+ properties={"operation": operation, "line": line},
133
+ )
134
+ )
135
+
136
+ return result
@@ -0,0 +1,86 @@
1
+ """TypeORM / Prisma entity detector for TypeScript."""
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 EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
10
+
11
+
12
+ class TypeORMEntityDetector:
13
+ """Detects TypeORM entity definitions (@Entity decorator)."""
14
+
15
+ name: str = "typescript.typeorm_entities"
16
+ supported_languages: tuple[str, ...] = ("typescript",)
17
+
18
+ # @Entity('users') or @Entity() class User { ... }
19
+ _ENTITY_PATTERN = re.compile(
20
+ r"@Entity\(\s*['\"`]?(\w*)['\"`]?\s*\)\s*\n\s*(?:export\s+)?class\s+(\w+)"
21
+ )
22
+
23
+ # @Column() name: string;
24
+ _COLUMN_PATTERN = re.compile(
25
+ r"@Column\([^)]*\)\s*\n?\s*(\w+)\s*[!?]?\s*:\s*(\w+)"
26
+ )
27
+
28
+ # @ManyToOne, @OneToMany, @ManyToMany, @OneToOne
29
+ _RELATION_PATTERN = re.compile(
30
+ r"@(ManyToOne|OneToMany|ManyToMany|OneToOne)\(\s*\(\)\s*=>\s*(\w+)"
31
+ )
32
+
33
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
34
+ result = DetectorResult()
35
+ text = decode_text(ctx)
36
+
37
+ for match in self._ENTITY_PATTERN.finditer(text):
38
+ table_name = match.group(1) or match.group(2).lower() + "s"
39
+ class_name = match.group(2)
40
+ line = text[:match.start()].count("\n") + 1
41
+
42
+ # Find columns in class body (rough heuristic)
43
+ class_start = match.end()
44
+ brace_count = 0
45
+ class_end = len(text)
46
+ for i, ch in enumerate(text[class_start:], class_start):
47
+ if ch == "{":
48
+ brace_count += 1
49
+ elif ch == "}":
50
+ brace_count -= 1
51
+ if brace_count == 0:
52
+ class_end = i
53
+ break
54
+ class_body = text[class_start:class_end]
55
+
56
+ columns = [m.group(1) for m in self._COLUMN_PATTERN.finditer(class_body)]
57
+
58
+ node_id = f"entity:{ctx.module_name or ''}:{class_name}"
59
+ result.nodes.append(GraphNode(
60
+ id=node_id,
61
+ kind=NodeKind.ENTITY,
62
+ label=class_name,
63
+ fqn=f"{ctx.file_path}::{class_name}",
64
+ module=ctx.module_name,
65
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
66
+ annotations=["@Entity"],
67
+ properties={
68
+ "table_name": table_name,
69
+ "columns": columns,
70
+ "framework": "typeorm",
71
+ },
72
+ ))
73
+
74
+ # Detect relationships
75
+ for rel_match in self._RELATION_PATTERN.finditer(class_body):
76
+ rel_type = rel_match.group(1)
77
+ target_entity = rel_match.group(2)
78
+ target_id = f"entity:{ctx.module_name or ''}:{target_entity}"
79
+ result.edges.append(GraphEdge(
80
+ source=node_id,
81
+ target=target_id,
82
+ kind=EdgeKind.MAPS_TO,
83
+ label=rel_type,
84
+ ))
85
+
86
+ return result
@@ -0,0 +1,185 @@
1
+ """Regex-based TypeScript/JavaScript structures 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, find_line_number
9
+ from osscodeiq.models.graph import (
10
+ EdgeKind,
11
+ GraphEdge,
12
+ GraphNode,
13
+ NodeKind,
14
+ SourceLocation,
15
+ )
16
+
17
+ _INTERFACE_RE = re.compile(r'^\s*(?:export\s+)?interface\s+(\w+)', re.MULTILINE)
18
+ _TYPE_RE = re.compile(r'^\s*(?:export\s+)?type\s+(\w+)\s*=', re.MULTILINE)
19
+ _CLASS_RE = re.compile(r'^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)', re.MULTILINE)
20
+ _FUNC_RE = re.compile(
21
+ r'^\s*(?:export\s+)?(default\s+)?(?:(async)\s+)?function\s+(\w+)',
22
+ re.MULTILINE,
23
+ )
24
+ _CONST_FUNC_RE = re.compile(
25
+ r'^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:(async)\s+)?\(',
26
+ re.MULTILINE,
27
+ )
28
+ _ENUM_RE = re.compile(r'^\s*(?:export\s+)?(?:const\s+)?enum\s+(\w+)', re.MULTILINE)
29
+ _IMPORT_RE = re.compile(r'''import\s+.*?\s+from\s+['"]([^'"]+)['"]''', re.MULTILINE)
30
+ _NAMESPACE_RE = re.compile(r'^\s*(?:export\s+)?namespace\s+(\w+)', re.MULTILINE)
31
+
32
+
33
+ class TypeScriptStructuresDetector:
34
+ """Detects TypeScript/JavaScript interfaces, types, classes, functions, enums, imports, and namespaces."""
35
+
36
+ name: str = "typescript_structures"
37
+ supported_languages: tuple[str, ...] = ("typescript", "javascript")
38
+
39
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
40
+ result = DetectorResult()
41
+ text = decode_text(ctx)
42
+ fp = ctx.file_path
43
+ file_node_id = fp
44
+
45
+ # Interfaces
46
+ for m in _INTERFACE_RE.finditer(text):
47
+ iface_name = m.group(1)
48
+ node_id = f"ts:{fp}:interface:{iface_name}"
49
+ result.nodes.append(GraphNode(
50
+ id=node_id,
51
+ kind=NodeKind.INTERFACE,
52
+ label=iface_name,
53
+ fqn=iface_name,
54
+ module=ctx.module_name,
55
+ location=SourceLocation(
56
+ file_path=fp,
57
+ line_start=find_line_number(text, m.start()),
58
+ ),
59
+ ))
60
+
61
+ # Type aliases
62
+ for m in _TYPE_RE.finditer(text):
63
+ type_name = m.group(1)
64
+ node_id = f"ts:{fp}:type:{type_name}"
65
+ result.nodes.append(GraphNode(
66
+ id=node_id,
67
+ kind=NodeKind.CLASS,
68
+ label=type_name,
69
+ fqn=type_name,
70
+ module=ctx.module_name,
71
+ location=SourceLocation(
72
+ file_path=fp,
73
+ line_start=find_line_number(text, m.start()),
74
+ ),
75
+ properties={"type_alias": True},
76
+ ))
77
+
78
+ # Classes
79
+ for m in _CLASS_RE.finditer(text):
80
+ class_name = m.group(1)
81
+ node_id = f"ts:{fp}:class:{class_name}"
82
+ result.nodes.append(GraphNode(
83
+ id=node_id,
84
+ kind=NodeKind.CLASS,
85
+ label=class_name,
86
+ fqn=class_name,
87
+ module=ctx.module_name,
88
+ location=SourceLocation(
89
+ file_path=fp,
90
+ line_start=find_line_number(text, m.start()),
91
+ ),
92
+ ))
93
+
94
+ # Named functions (export [default] [async] function name)
95
+ for m in _FUNC_RE.finditer(text):
96
+ is_default = m.group(1) is not None
97
+ is_async = m.group(2) is not None
98
+ func_name = m.group(3)
99
+ node_id = f"ts:{fp}:func:{func_name}"
100
+ properties: dict = {}
101
+ if is_default:
102
+ properties["default"] = True
103
+ if is_async:
104
+ properties["async"] = True
105
+ result.nodes.append(GraphNode(
106
+ id=node_id,
107
+ kind=NodeKind.METHOD,
108
+ label=func_name,
109
+ fqn=func_name,
110
+ module=ctx.module_name,
111
+ location=SourceLocation(
112
+ file_path=fp,
113
+ line_start=find_line_number(text, m.start()),
114
+ ),
115
+ properties=properties,
116
+ ))
117
+
118
+ # Arrow / const functions (export const name = [async] ()
119
+ for m in _CONST_FUNC_RE.finditer(text):
120
+ func_name = m.group(1)
121
+ is_async = m.group(2) is not None
122
+ node_id = f"ts:{fp}:func:{func_name}"
123
+ # Avoid duplicate if a named function already captured this id
124
+ existing_ids = {n.id for n in result.nodes}
125
+ if node_id in existing_ids:
126
+ continue
127
+ properties = {}
128
+ if is_async:
129
+ properties["async"] = True
130
+ result.nodes.append(GraphNode(
131
+ id=node_id,
132
+ kind=NodeKind.METHOD,
133
+ label=func_name,
134
+ fqn=func_name,
135
+ module=ctx.module_name,
136
+ location=SourceLocation(
137
+ file_path=fp,
138
+ line_start=find_line_number(text, m.start()),
139
+ ),
140
+ properties=properties,
141
+ ))
142
+
143
+ # Enums
144
+ for m in _ENUM_RE.finditer(text):
145
+ enum_name = m.group(1)
146
+ node_id = f"ts:{fp}:enum:{enum_name}"
147
+ result.nodes.append(GraphNode(
148
+ id=node_id,
149
+ kind=NodeKind.ENUM,
150
+ label=enum_name,
151
+ fqn=enum_name,
152
+ module=ctx.module_name,
153
+ location=SourceLocation(
154
+ file_path=fp,
155
+ line_start=find_line_number(text, m.start()),
156
+ ),
157
+ ))
158
+
159
+ # Imports
160
+ for m in _IMPORT_RE.finditer(text):
161
+ module_path = m.group(1)
162
+ result.edges.append(GraphEdge(
163
+ source=file_node_id,
164
+ target=module_path,
165
+ kind=EdgeKind.IMPORTS,
166
+ label=f"{fp} imports {module_path}",
167
+ ))
168
+
169
+ # Namespaces
170
+ for m in _NAMESPACE_RE.finditer(text):
171
+ ns_name = m.group(1)
172
+ node_id = f"ts:{fp}:namespace:{ns_name}"
173
+ result.nodes.append(GraphNode(
174
+ id=node_id,
175
+ kind=NodeKind.MODULE,
176
+ label=ns_name,
177
+ fqn=ns_name,
178
+ module=ctx.module_name,
179
+ location=SourceLocation(
180
+ file_path=fp,
181
+ line_start=find_line_number(text, m.start()),
182
+ ),
183
+ ))
184
+
185
+ return result
@@ -0,0 +1,49 @@
1
+ """Shared utilities for OSSCodeIQ detectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterator
6
+
7
+ from osscodeiq.detectors.base import DetectorContext
8
+
9
+
10
+ def decode_text(ctx: DetectorContext) -> str:
11
+ """Decode raw bytes to text, handling encoding errors gracefully."""
12
+ return ctx.content.decode("utf-8", errors="replace")
13
+
14
+
15
+ def iter_lines(ctx: DetectorContext) -> Iterator[tuple[int, str]]:
16
+ """Yield (line_number, line_text) tuples from detector context.
17
+
18
+ Line numbers are 1-based (matching source file conventions).
19
+ """
20
+ text = decode_text(ctx)
21
+ for i, line in enumerate(text.split("\n")):
22
+ yield i + 1, line
23
+
24
+
25
+ def find_line_number(text: str, byte_offset: int) -> int:
26
+ """Find the 1-based line number for a byte offset in text."""
27
+ return text[:byte_offset].count("\n") + 1
28
+
29
+
30
+ def filename(ctx: DetectorContext) -> str:
31
+ """Extract the filename (without path) from the detector context."""
32
+ return ctx.file_path.rsplit("/", 1)[-1] if "/" in ctx.file_path else ctx.file_path
33
+
34
+
35
+ def matches_filename(ctx: DetectorContext, *patterns: str) -> bool:
36
+ """Check if the file matches any of the given filename patterns.
37
+
38
+ Supports exact match and prefix+suffix matching.
39
+ Examples: matches_filename(ctx, "package.json", "tsconfig.*.json")
40
+ """
41
+ name = filename(ctx)
42
+ for pattern in patterns:
43
+ if "*" in pattern:
44
+ prefix, suffix = pattern.split("*", 1)
45
+ if name.startswith(prefix) and name.endswith(suffix):
46
+ return True
47
+ elif name == pattern:
48
+ return True
49
+ return False
@@ -0,0 +1,11 @@
1
+ from osscodeiq.discovery.file_discovery import (
2
+ ChangeType,
3
+ DiscoveredFile,
4
+ FileDiscovery,
5
+ )
6
+
7
+ __all__ = [
8
+ "ChangeType",
9
+ "DiscoveredFile",
10
+ "FileDiscovery",
11
+ ]