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,115 @@
|
|
|
1
|
+
"""Pydantic model 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 EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PydanticModelDetector:
|
|
13
|
+
"""Detects Pydantic model and settings definitions."""
|
|
14
|
+
|
|
15
|
+
name: str = "python.pydantic_models"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
17
|
+
|
|
18
|
+
_PYDANTIC_CLASS_RE = re.compile(
|
|
19
|
+
r"^class\s+(\w+)\s*\(\s*(\w*(?:BaseModel|BaseSettings)\w*)\s*\)", re.MULTILINE
|
|
20
|
+
)
|
|
21
|
+
_FIELD_RE = re.compile(r"^\s+(\w+)\s*:\s*(\w[\w\[\], |]*)", re.MULTILINE)
|
|
22
|
+
_VALIDATOR_RE = re.compile(
|
|
23
|
+
r"@(?:validator|field_validator)\s*\(\s*[\"'](\w+)", re.MULTILINE
|
|
24
|
+
)
|
|
25
|
+
_CONFIG_CLASS_RE = re.compile(r"^\s+class\s+Config\s*:", re.MULTILINE)
|
|
26
|
+
_CONFIG_ATTR_RE = re.compile(r"^\s{8}(\w+)\s*=\s*(.+)", re.MULTILINE)
|
|
27
|
+
|
|
28
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
29
|
+
result = DetectorResult()
|
|
30
|
+
text = decode_text(ctx)
|
|
31
|
+
|
|
32
|
+
# Track known model names for inheritance edges
|
|
33
|
+
known_models: dict[str, str] = {}
|
|
34
|
+
|
|
35
|
+
for match in self._PYDANTIC_CLASS_RE.finditer(text):
|
|
36
|
+
class_name = match.group(1)
|
|
37
|
+
base_class = match.group(2)
|
|
38
|
+
line = text[: match.start()].count("\n") + 1
|
|
39
|
+
|
|
40
|
+
is_settings = "BaseSettings" in base_class
|
|
41
|
+
|
|
42
|
+
# Determine class body boundaries
|
|
43
|
+
class_start = match.start()
|
|
44
|
+
next_class = re.search(r"\nclass\s+\w+", text[match.end() :])
|
|
45
|
+
class_body = (
|
|
46
|
+
text[class_start : match.end() + next_class.start()]
|
|
47
|
+
if next_class
|
|
48
|
+
else text[class_start:]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Extract fields
|
|
52
|
+
fields = []
|
|
53
|
+
field_types: dict[str, str] = {}
|
|
54
|
+
for fm in self._FIELD_RE.finditer(class_body):
|
|
55
|
+
fname = fm.group(1)
|
|
56
|
+
ftype = fm.group(2).strip()
|
|
57
|
+
if fname not in ("class", "Config", "model_config"):
|
|
58
|
+
fields.append(fname)
|
|
59
|
+
field_types[fname] = ftype
|
|
60
|
+
|
|
61
|
+
# Extract validators
|
|
62
|
+
validators = [
|
|
63
|
+
vm.group(1) for vm in self._VALIDATOR_RE.finditer(class_body)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
# Extract Config class properties
|
|
67
|
+
config_props: dict[str, str] = {}
|
|
68
|
+
config_match = self._CONFIG_CLASS_RE.search(class_body)
|
|
69
|
+
if config_match:
|
|
70
|
+
config_block_start = config_match.end()
|
|
71
|
+
# Find next dedented line or end
|
|
72
|
+
config_block_end = len(class_body)
|
|
73
|
+
for cm in re.finditer(r"\n\S", class_body[config_block_start:]):
|
|
74
|
+
config_block_end = config_block_start + cm.start()
|
|
75
|
+
break
|
|
76
|
+
config_block = class_body[config_block_start:config_block_end]
|
|
77
|
+
for attr_match in self._CONFIG_ATTR_RE.finditer(config_block):
|
|
78
|
+
config_props[attr_match.group(1)] = attr_match.group(2).strip()
|
|
79
|
+
|
|
80
|
+
node_kind = NodeKind.CONFIG_DEFINITION if is_settings else NodeKind.ENTITY
|
|
81
|
+
node_id = f"pydantic:{ctx.file_path}:model:{class_name}"
|
|
82
|
+
|
|
83
|
+
result.nodes.append(
|
|
84
|
+
GraphNode(
|
|
85
|
+
id=node_id,
|
|
86
|
+
kind=node_kind,
|
|
87
|
+
label=class_name,
|
|
88
|
+
fqn=f"{ctx.file_path}::{class_name}",
|
|
89
|
+
module=ctx.module_name,
|
|
90
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
91
|
+
annotations=validators,
|
|
92
|
+
properties={
|
|
93
|
+
"fields": fields,
|
|
94
|
+
"field_types": field_types,
|
|
95
|
+
"framework": "pydantic",
|
|
96
|
+
"base_class": base_class,
|
|
97
|
+
**({"config": config_props} if config_props else {}),
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
known_models[class_name] = node_id
|
|
103
|
+
|
|
104
|
+
# Check for inheritance from a known model
|
|
105
|
+
if base_class in known_models:
|
|
106
|
+
result.edges.append(
|
|
107
|
+
GraphEdge(
|
|
108
|
+
source=node_id,
|
|
109
|
+
target=known_models[base_class],
|
|
110
|
+
kind=EdgeKind.EXTENDS,
|
|
111
|
+
label=f"{class_name} extends {base_class}",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return result
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Regex-based Python structures detector for Python 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, find_line_number
|
|
9
|
+
from osscodeiq.models.graph import (
|
|
10
|
+
EdgeKind,
|
|
11
|
+
GraphEdge,
|
|
12
|
+
GraphNode,
|
|
13
|
+
NodeKind,
|
|
14
|
+
SourceLocation,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_CLASS_RE = re.compile(r'^class\s+(\w+)(?:\(([^)]*)\))?:', re.MULTILINE)
|
|
18
|
+
_FUNC_RE = re.compile(r'^([^\S\n]*)(async\s+)?def\s+(\w+)\s*\(', re.MULTILINE)
|
|
19
|
+
_IMPORT_RE = re.compile(r'^(?:from\s+([\w.]+)\s+)?import\s+([\w., ]+)', re.MULTILINE)
|
|
20
|
+
_DECORATOR_RE = re.compile(r'^([^\S\n]*)@(\w[\w.]*)', re.MULTILINE)
|
|
21
|
+
_ALL_RE = re.compile(r'__all__\s*=\s*\[([^\]]*)\]', re.DOTALL)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _collect_decorators(text: str) -> dict[int, list[str]]:
|
|
25
|
+
"""Map each decorator's line number to the decorator name.
|
|
26
|
+
|
|
27
|
+
Returns a dict keyed by 1-based line number of the decorator.
|
|
28
|
+
"""
|
|
29
|
+
result: dict[int, list[str]] = {}
|
|
30
|
+
for m in _DECORATOR_RE.finditer(text):
|
|
31
|
+
line = find_line_number(text, m.start())
|
|
32
|
+
result.setdefault(line, []).append(m.group(2))
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _find_decorators_for_line(
|
|
37
|
+
decorator_map: dict[int, list[str]], target_line: int
|
|
38
|
+
) -> list[str]:
|
|
39
|
+
"""Collect all decorator names immediately above a target line."""
|
|
40
|
+
decorators: list[str] = []
|
|
41
|
+
line = target_line - 1
|
|
42
|
+
while line in decorator_map:
|
|
43
|
+
decorators.extend(decorator_map[line])
|
|
44
|
+
line -= 1
|
|
45
|
+
# Reverse so top-most decorator is first
|
|
46
|
+
decorators.reverse()
|
|
47
|
+
return decorators
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _find_enclosing_class(
|
|
51
|
+
class_ranges: list[tuple[str, int, int]], line: int, func_indent: int
|
|
52
|
+
) -> str | None:
|
|
53
|
+
"""Find the class name that encloses a given line number.
|
|
54
|
+
|
|
55
|
+
A function is considered inside a class if it appears after the class
|
|
56
|
+
declaration and is indented more than the class itself.
|
|
57
|
+
|
|
58
|
+
class_ranges is a list of (class_name, start_line, indent_col).
|
|
59
|
+
"""
|
|
60
|
+
for class_name, start_line, class_indent in reversed(class_ranges):
|
|
61
|
+
if line > start_line and func_indent > class_indent:
|
|
62
|
+
return class_name
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PythonStructuresDetector:
|
|
67
|
+
"""Detects Python classes, functions, imports, decorators, and __all__ exports."""
|
|
68
|
+
|
|
69
|
+
name: str = "python_structures"
|
|
70
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
71
|
+
|
|
72
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
73
|
+
result = DetectorResult()
|
|
74
|
+
text = decode_text(ctx)
|
|
75
|
+
fp = ctx.file_path
|
|
76
|
+
file_node_id = fp
|
|
77
|
+
|
|
78
|
+
# Collect decorators by line number
|
|
79
|
+
decorator_map = _collect_decorators(text)
|
|
80
|
+
|
|
81
|
+
# __all__ exports
|
|
82
|
+
all_match = _ALL_RE.search(text)
|
|
83
|
+
all_exports: list[str] | None = None
|
|
84
|
+
if all_match:
|
|
85
|
+
raw = all_match.group(1)
|
|
86
|
+
# Extract quoted names
|
|
87
|
+
all_exports = re.findall(r"""['"](\w+)['"]""", raw)
|
|
88
|
+
|
|
89
|
+
# Classes — track them for method association
|
|
90
|
+
class_ranges: list[tuple[str, int, int]] = [] # (name, line, indent_col)
|
|
91
|
+
for m in _CLASS_RE.finditer(text):
|
|
92
|
+
class_name = m.group(1)
|
|
93
|
+
bases_str = m.group(2)
|
|
94
|
+
line = find_line_number(text, m.start())
|
|
95
|
+
node_id = f"py:{fp}:class:{class_name}"
|
|
96
|
+
|
|
97
|
+
# Find indent of the class declaration
|
|
98
|
+
line_start_offset = text.rfind("\n", 0, m.start()) + 1
|
|
99
|
+
indent = m.start() - line_start_offset
|
|
100
|
+
|
|
101
|
+
class_ranges.append((class_name, line, indent))
|
|
102
|
+
|
|
103
|
+
annotations = _find_decorators_for_line(decorator_map, line)
|
|
104
|
+
|
|
105
|
+
properties: dict = {}
|
|
106
|
+
if bases_str:
|
|
107
|
+
bases = [b.strip() for b in bases_str.split(",") if b.strip()]
|
|
108
|
+
properties["bases"] = bases
|
|
109
|
+
if all_exports and class_name in all_exports:
|
|
110
|
+
properties["exported"] = True
|
|
111
|
+
|
|
112
|
+
result.nodes.append(GraphNode(
|
|
113
|
+
id=node_id,
|
|
114
|
+
kind=NodeKind.CLASS,
|
|
115
|
+
label=class_name,
|
|
116
|
+
fqn=class_name,
|
|
117
|
+
module=ctx.module_name,
|
|
118
|
+
location=SourceLocation(
|
|
119
|
+
file_path=fp,
|
|
120
|
+
line_start=line,
|
|
121
|
+
),
|
|
122
|
+
annotations=annotations,
|
|
123
|
+
properties=properties,
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
# EXTENDS edges for base classes
|
|
127
|
+
if bases_str:
|
|
128
|
+
bases = [b.strip() for b in bases_str.split(",") if b.strip()]
|
|
129
|
+
for base in bases:
|
|
130
|
+
result.edges.append(GraphEdge(
|
|
131
|
+
source=node_id,
|
|
132
|
+
target=base,
|
|
133
|
+
kind=EdgeKind.EXTENDS,
|
|
134
|
+
label=f"{class_name} extends {base}",
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
# Functions and methods
|
|
138
|
+
for m in _FUNC_RE.finditer(text):
|
|
139
|
+
indent_str = m.group(1)
|
|
140
|
+
is_async = m.group(2) is not None
|
|
141
|
+
func_name = m.group(3)
|
|
142
|
+
line = find_line_number(text, m.start())
|
|
143
|
+
indent_len = len(indent_str)
|
|
144
|
+
|
|
145
|
+
annotations = _find_decorators_for_line(decorator_map, line)
|
|
146
|
+
|
|
147
|
+
properties: dict = {}
|
|
148
|
+
if is_async:
|
|
149
|
+
properties["async"] = True
|
|
150
|
+
if all_exports and func_name in all_exports:
|
|
151
|
+
properties["exported"] = True
|
|
152
|
+
|
|
153
|
+
if indent_len == 0:
|
|
154
|
+
# Top-level function
|
|
155
|
+
node_id = f"py:{fp}:func:{func_name}"
|
|
156
|
+
result.nodes.append(GraphNode(
|
|
157
|
+
id=node_id,
|
|
158
|
+
kind=NodeKind.METHOD,
|
|
159
|
+
label=func_name,
|
|
160
|
+
fqn=func_name,
|
|
161
|
+
module=ctx.module_name,
|
|
162
|
+
location=SourceLocation(
|
|
163
|
+
file_path=fp,
|
|
164
|
+
line_start=line,
|
|
165
|
+
),
|
|
166
|
+
annotations=annotations,
|
|
167
|
+
properties=properties,
|
|
168
|
+
))
|
|
169
|
+
else:
|
|
170
|
+
# Indented def — check if it's inside a class
|
|
171
|
+
enclosing_class = _find_enclosing_class(class_ranges, line, indent_len)
|
|
172
|
+
if enclosing_class:
|
|
173
|
+
node_id = f"py:{fp}:class:{enclosing_class}:method:{func_name}"
|
|
174
|
+
properties["class"] = enclosing_class
|
|
175
|
+
result.nodes.append(GraphNode(
|
|
176
|
+
id=node_id,
|
|
177
|
+
kind=NodeKind.METHOD,
|
|
178
|
+
label=f"{enclosing_class}.{func_name}",
|
|
179
|
+
fqn=f"{enclosing_class}.{func_name}",
|
|
180
|
+
module=ctx.module_name,
|
|
181
|
+
location=SourceLocation(
|
|
182
|
+
file_path=fp,
|
|
183
|
+
line_start=line,
|
|
184
|
+
),
|
|
185
|
+
annotations=annotations,
|
|
186
|
+
properties=properties,
|
|
187
|
+
))
|
|
188
|
+
# DEFINES edge from class to method
|
|
189
|
+
class_node_id = f"py:{fp}:class:{enclosing_class}"
|
|
190
|
+
result.edges.append(GraphEdge(
|
|
191
|
+
source=class_node_id,
|
|
192
|
+
target=node_id,
|
|
193
|
+
kind=EdgeKind.DEFINES,
|
|
194
|
+
label=f"{enclosing_class} defines {func_name}",
|
|
195
|
+
))
|
|
196
|
+
|
|
197
|
+
# Imports
|
|
198
|
+
for m in _IMPORT_RE.finditer(text):
|
|
199
|
+
from_module = m.group(1)
|
|
200
|
+
import_names = m.group(2)
|
|
201
|
+
if from_module:
|
|
202
|
+
result.edges.append(GraphEdge(
|
|
203
|
+
source=file_node_id,
|
|
204
|
+
target=from_module,
|
|
205
|
+
kind=EdgeKind.IMPORTS,
|
|
206
|
+
label=f"{fp} imports {from_module}",
|
|
207
|
+
))
|
|
208
|
+
else:
|
|
209
|
+
for name in import_names.split(","):
|
|
210
|
+
name = name.strip()
|
|
211
|
+
if name:
|
|
212
|
+
result.edges.append(GraphEdge(
|
|
213
|
+
source=file_node_id,
|
|
214
|
+
target=name,
|
|
215
|
+
kind=EdgeKind.IMPORTS,
|
|
216
|
+
label=f"{fp} imports {name}",
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
# __all__ property on a file-level module node (only if present)
|
|
220
|
+
if all_exports is not None:
|
|
221
|
+
result.nodes.append(GraphNode(
|
|
222
|
+
id=f"py:{fp}:module",
|
|
223
|
+
kind=NodeKind.MODULE,
|
|
224
|
+
label=fp,
|
|
225
|
+
fqn=fp,
|
|
226
|
+
module=ctx.module_name,
|
|
227
|
+
location=SourceLocation(
|
|
228
|
+
file_path=fp,
|
|
229
|
+
line_start=find_line_number(text, all_match.start()),
|
|
230
|
+
),
|
|
231
|
+
properties={"__all__": all_exports},
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
return result
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""SQLAlchemy model 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 EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SQLAlchemyModelDetector:
|
|
13
|
+
"""Detects SQLAlchemy ORM models (declarative base classes)."""
|
|
14
|
+
|
|
15
|
+
name: str = "python.sqlalchemy_models"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
17
|
+
|
|
18
|
+
# class User(Base): or class User(db.Model):
|
|
19
|
+
_MODEL_PATTERN = re.compile(
|
|
20
|
+
r"class\s+(\w+)\(([^)]*(?:Base|Model|DeclarativeBase)[^)]*)\):"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# __tablename__ = 'users'
|
|
24
|
+
_TABLE_NAME = re.compile(r"__tablename__\s*=\s*['\"](\w+)['\"]")
|
|
25
|
+
|
|
26
|
+
# Column definitions: name = Column(String, ...) or name: Mapped[str] = mapped_column(...)
|
|
27
|
+
_COLUMN_PATTERN = re.compile(
|
|
28
|
+
r"(\w+)\s*(?::\s*Mapped\[.*?\])?\s*=\s*(?:Column|mapped_column)\("
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Relationship: orders = relationship("Order", ...) or orders: Mapped[list["Order"]]
|
|
32
|
+
_RELATIONSHIP_PATTERN = re.compile(
|
|
33
|
+
r"(\w+)\s*(?::\s*Mapped\[.*?\])?\s*=\s*relationship\(\s*['\"](\w+)['\"]"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
37
|
+
result = DetectorResult()
|
|
38
|
+
text = decode_text(ctx)
|
|
39
|
+
|
|
40
|
+
for match in self._MODEL_PATTERN.finditer(text):
|
|
41
|
+
class_name = match.group(1)
|
|
42
|
+
line = text[:match.start()].count("\n") + 1
|
|
43
|
+
|
|
44
|
+
# Find the class body (rough: from class line to next class or end)
|
|
45
|
+
class_start = match.start()
|
|
46
|
+
next_class = re.search(r"\nclass\s+\w+", text[match.end():])
|
|
47
|
+
class_body = text[class_start:match.end() + next_class.start()] if next_class else text[class_start:]
|
|
48
|
+
|
|
49
|
+
# Extract table name
|
|
50
|
+
table_match = self._TABLE_NAME.search(class_body)
|
|
51
|
+
table_name = table_match.group(1) if table_match else class_name.lower() + "s"
|
|
52
|
+
|
|
53
|
+
# Extract columns
|
|
54
|
+
columns = [m.group(1) for m in self._COLUMN_PATTERN.finditer(class_body)]
|
|
55
|
+
|
|
56
|
+
node_id = f"entity:{ctx.module_name or ''}:{class_name}"
|
|
57
|
+
result.nodes.append(GraphNode(
|
|
58
|
+
id=node_id,
|
|
59
|
+
kind=NodeKind.ENTITY,
|
|
60
|
+
label=class_name,
|
|
61
|
+
fqn=f"{ctx.file_path}::{class_name}",
|
|
62
|
+
module=ctx.module_name,
|
|
63
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
64
|
+
properties={
|
|
65
|
+
"table_name": table_name,
|
|
66
|
+
"columns": columns,
|
|
67
|
+
"framework": "sqlalchemy",
|
|
68
|
+
},
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
# Extract relationships
|
|
72
|
+
for rel_match in self._RELATIONSHIP_PATTERN.finditer(class_body):
|
|
73
|
+
target_class = rel_match.group(2)
|
|
74
|
+
target_id = f"entity:{ctx.module_name or ''}:{target_class}"
|
|
75
|
+
result.edges.append(GraphEdge(
|
|
76
|
+
source=node_id,
|
|
77
|
+
target=target_id,
|
|
78
|
+
kind=EdgeKind.MAPS_TO,
|
|
79
|
+
label=rel_match.group(1),
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
return result
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Plugin registry for OSSCodeIQ detectors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
import logging
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from osscodeiq.detectors.base import Detector
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DetectorRegistry:
|
|
17
|
+
"""Registry that manages detector instances and plugin discovery."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._detectors: list[Detector] = []
|
|
21
|
+
self._by_name: dict[str, Detector] = {}
|
|
22
|
+
|
|
23
|
+
def register(self, detector: Detector) -> None:
|
|
24
|
+
"""Add a detector to the registry."""
|
|
25
|
+
if detector.name in self._by_name:
|
|
26
|
+
logger.warning("Detector %r already registered, skipping", detector.name)
|
|
27
|
+
return
|
|
28
|
+
self._detectors.append(detector)
|
|
29
|
+
self._by_name[detector.name] = detector
|
|
30
|
+
|
|
31
|
+
def load_builtin_detectors(self) -> None:
|
|
32
|
+
"""Import and register all built-in detectors via package scanning."""
|
|
33
|
+
import pkgutil
|
|
34
|
+
|
|
35
|
+
import osscodeiq.detectors as detectors_pkg
|
|
36
|
+
|
|
37
|
+
# Walk all subpackages under osscodeiq.detectors
|
|
38
|
+
skip = {"registry", "base", "utils", "__init__"}
|
|
39
|
+
module_paths = []
|
|
40
|
+
for importer, modname, ispkg in pkgutil.walk_packages(
|
|
41
|
+
detectors_pkg.__path__,
|
|
42
|
+
prefix="osscodeiq.detectors.",
|
|
43
|
+
):
|
|
44
|
+
# Skip non-detector modules
|
|
45
|
+
short_name = modname.rsplit(".", 1)[-1]
|
|
46
|
+
if short_name in skip or short_name.startswith("_"):
|
|
47
|
+
continue
|
|
48
|
+
module_paths.append(modname)
|
|
49
|
+
|
|
50
|
+
# Sort for deterministic registration order
|
|
51
|
+
module_paths.sort()
|
|
52
|
+
|
|
53
|
+
for module_path in module_paths:
|
|
54
|
+
try:
|
|
55
|
+
mod = importlib.import_module(module_path)
|
|
56
|
+
# Convention: each module exposes a `detector` attribute or a class
|
|
57
|
+
# ending with "Detector"
|
|
58
|
+
if hasattr(mod, "detector"):
|
|
59
|
+
self.register(mod.detector)
|
|
60
|
+
else:
|
|
61
|
+
for attr_name in dir(mod):
|
|
62
|
+
obj = getattr(mod, attr_name)
|
|
63
|
+
if (
|
|
64
|
+
isinstance(obj, type)
|
|
65
|
+
and attr_name.endswith("Detector")
|
|
66
|
+
and hasattr(obj, "detect")
|
|
67
|
+
):
|
|
68
|
+
self.register(obj())
|
|
69
|
+
break
|
|
70
|
+
except Exception:
|
|
71
|
+
logger.debug("Could not load detector module %s", module_path, exc_info=True)
|
|
72
|
+
|
|
73
|
+
def load_plugin_detectors(self) -> None:
|
|
74
|
+
"""Discover detectors via setuptools entry points."""
|
|
75
|
+
try:
|
|
76
|
+
eps = importlib.metadata.entry_points(group="osscodeiq.detectors")
|
|
77
|
+
except TypeError:
|
|
78
|
+
# Python < 3.12 compat
|
|
79
|
+
eps = importlib.metadata.entry_points().get("osscodeiq.detectors", [])
|
|
80
|
+
for ep in eps:
|
|
81
|
+
try:
|
|
82
|
+
detector_or_factory = ep.load()
|
|
83
|
+
if callable(detector_or_factory) and isinstance(detector_or_factory, type):
|
|
84
|
+
self.register(detector_or_factory())
|
|
85
|
+
else:
|
|
86
|
+
self.register(detector_or_factory)
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.warning("Failed to load plugin detector %s", ep.name, exc_info=True)
|
|
89
|
+
|
|
90
|
+
def detectors_for_language(self, language: str) -> list[Detector]:
|
|
91
|
+
"""Return all detectors that support a given language."""
|
|
92
|
+
return [d for d in self._detectors if language in d.supported_languages]
|
|
93
|
+
|
|
94
|
+
def all_detectors(self) -> list[Detector]:
|
|
95
|
+
"""Return all registered detectors."""
|
|
96
|
+
return list(self._detectors)
|
|
97
|
+
|
|
98
|
+
def get(self, name: str) -> Detector | None:
|
|
99
|
+
"""Get a detector by name."""
|
|
100
|
+
return self._by_name.get(name)
|
|
File without changes
|