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