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,184 @@
|
|
|
1
|
+
"""Regex-based Entity Framework Core detector for C# 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
|
+
_DBCONTEXT_RE = re.compile(r'class\s+(\w+)\s*:\s*(?:[\w.]+\.)?DbContext', re.MULTILINE)
|
|
18
|
+
_DBSET_RE = re.compile(r'DbSet<(\w+)>', re.MULTILINE)
|
|
19
|
+
_KEY_RE = re.compile(r'\[Key\]')
|
|
20
|
+
_FK_RE = re.compile(r'\[ForeignKey\("(\w+)"\)\]')
|
|
21
|
+
_TABLE_RE = re.compile(r'\[Table\("(\w+)"\)\]')
|
|
22
|
+
_FLUENT_RE = re.compile(r'\.(HasOne|HasMany|WithMany|WithOne)\s*\(', re.MULTILINE)
|
|
23
|
+
_MIGRATION_RE = re.compile(r'class\s+(\w+)\s*:\s*Migration', re.MULTILINE)
|
|
24
|
+
_CREATE_TABLE_RE = re.compile(r'CreateTable\s*\(\s*(?:name:\s*)?"(\w+)"', re.MULTILINE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CSharpEfcoreDetector:
|
|
28
|
+
"""Detects Entity Framework Core patterns: DbContext, DbSet, annotations, fluent API, and migrations."""
|
|
29
|
+
|
|
30
|
+
name: str = "csharp_efcore"
|
|
31
|
+
supported_languages: tuple[str, ...] = ("csharp",)
|
|
32
|
+
|
|
33
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
34
|
+
result = DetectorResult()
|
|
35
|
+
text = decode_text(ctx)
|
|
36
|
+
|
|
37
|
+
context_ids: list[str] = []
|
|
38
|
+
|
|
39
|
+
# DbContext classes
|
|
40
|
+
for m in _DBCONTEXT_RE.finditer(text):
|
|
41
|
+
context_name = m.group(1)
|
|
42
|
+
node_id = f"efcore:{ctx.file_path}:context:{context_name}"
|
|
43
|
+
context_ids.append(node_id)
|
|
44
|
+
result.nodes.append(GraphNode(
|
|
45
|
+
id=node_id,
|
|
46
|
+
kind=NodeKind.REPOSITORY,
|
|
47
|
+
label=context_name,
|
|
48
|
+
fqn=context_name,
|
|
49
|
+
module=ctx.module_name,
|
|
50
|
+
location=SourceLocation(
|
|
51
|
+
file_path=ctx.file_path,
|
|
52
|
+
line_start=find_line_number(text, m.start()),
|
|
53
|
+
),
|
|
54
|
+
properties={"framework": "efcore"},
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
# DbSet properties -> ENTITY nodes + QUERIES edges from context
|
|
58
|
+
for m in _DBSET_RE.finditer(text):
|
|
59
|
+
entity_name = m.group(1)
|
|
60
|
+
entity_id = f"efcore:{ctx.file_path}:entity:{entity_name}"
|
|
61
|
+
line_num = find_line_number(text, m.start())
|
|
62
|
+
result.nodes.append(GraphNode(
|
|
63
|
+
id=entity_id,
|
|
64
|
+
kind=NodeKind.ENTITY,
|
|
65
|
+
label=entity_name,
|
|
66
|
+
fqn=entity_name,
|
|
67
|
+
module=ctx.module_name,
|
|
68
|
+
location=SourceLocation(
|
|
69
|
+
file_path=ctx.file_path,
|
|
70
|
+
line_start=line_num,
|
|
71
|
+
),
|
|
72
|
+
properties={"framework": "efcore"},
|
|
73
|
+
))
|
|
74
|
+
# Link each context to this entity
|
|
75
|
+
for ctx_id in context_ids:
|
|
76
|
+
result.edges.append(GraphEdge(
|
|
77
|
+
source=ctx_id,
|
|
78
|
+
target=entity_id,
|
|
79
|
+
kind=EdgeKind.QUERIES,
|
|
80
|
+
label=f"{ctx_id} queries {entity_name}",
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
# [Table("tablename")] annotation -> property on nearest entity
|
|
84
|
+
for m in _TABLE_RE.finditer(text):
|
|
85
|
+
table_name = m.group(1)
|
|
86
|
+
line_num = find_line_number(text, m.start())
|
|
87
|
+
# Find the nearest DbSet entity declared after this annotation
|
|
88
|
+
# or create an entity node with table_name property
|
|
89
|
+
nearest_entity = _find_nearest_entity(result, ctx.file_path, line_num)
|
|
90
|
+
if nearest_entity:
|
|
91
|
+
nearest_entity.properties["table_name"] = table_name
|
|
92
|
+
|
|
93
|
+
# [Key] annotation
|
|
94
|
+
for m in _KEY_RE.finditer(text):
|
|
95
|
+
line_num = find_line_number(text, m.start())
|
|
96
|
+
nearest_entity = _find_nearest_entity(result, ctx.file_path, line_num)
|
|
97
|
+
if nearest_entity:
|
|
98
|
+
if "annotations" not in nearest_entity.properties:
|
|
99
|
+
nearest_entity.properties["annotations"] = []
|
|
100
|
+
if "Key" not in nearest_entity.properties["annotations"]:
|
|
101
|
+
nearest_entity.properties["annotations"].append("Key")
|
|
102
|
+
|
|
103
|
+
# [ForeignKey("Name")] -> DEPENDS_ON edge
|
|
104
|
+
for m in _FK_RE.finditer(text):
|
|
105
|
+
fk_target = m.group(1)
|
|
106
|
+
line_num = find_line_number(text, m.start())
|
|
107
|
+
nearest_entity = _find_nearest_entity(result, ctx.file_path, line_num)
|
|
108
|
+
if nearest_entity:
|
|
109
|
+
result.edges.append(GraphEdge(
|
|
110
|
+
source=nearest_entity.id,
|
|
111
|
+
target=f"efcore:*:entity:{fk_target}",
|
|
112
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
113
|
+
label=f"{nearest_entity.label} depends on {fk_target}",
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
# Fluent API relationship methods -> DEPENDS_ON edges
|
|
117
|
+
for m in _FLUENT_RE.finditer(text):
|
|
118
|
+
method_name = m.group(1)
|
|
119
|
+
line_num = find_line_number(text, m.start())
|
|
120
|
+
# Link from the context to signal a relationship
|
|
121
|
+
for ctx_id in context_ids:
|
|
122
|
+
result.edges.append(GraphEdge(
|
|
123
|
+
source=ctx_id,
|
|
124
|
+
target=f"efcore:{ctx.file_path}:fluent:{method_name}:{line_num}",
|
|
125
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
126
|
+
label=f"{method_name} relationship",
|
|
127
|
+
properties={"fluent_method": method_name},
|
|
128
|
+
))
|
|
129
|
+
|
|
130
|
+
# Migration classes
|
|
131
|
+
for m in _MIGRATION_RE.finditer(text):
|
|
132
|
+
migration_name = m.group(1)
|
|
133
|
+
migration_id = f"efcore:{ctx.file_path}:migration:{migration_name}"
|
|
134
|
+
result.nodes.append(GraphNode(
|
|
135
|
+
id=migration_id,
|
|
136
|
+
kind=NodeKind.MIGRATION,
|
|
137
|
+
label=migration_name,
|
|
138
|
+
fqn=migration_name,
|
|
139
|
+
module=ctx.module_name,
|
|
140
|
+
location=SourceLocation(
|
|
141
|
+
file_path=ctx.file_path,
|
|
142
|
+
line_start=find_line_number(text, m.start()),
|
|
143
|
+
),
|
|
144
|
+
properties={"framework": "efcore"},
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
# migrationBuilder.CreateTable("name") -> ENTITY node
|
|
148
|
+
for m in _CREATE_TABLE_RE.finditer(text):
|
|
149
|
+
table_name = m.group(1)
|
|
150
|
+
entity_id = f"efcore:{ctx.file_path}:entity:{table_name}"
|
|
151
|
+
# Avoid duplicates
|
|
152
|
+
if not any(n.id == entity_id for n in result.nodes):
|
|
153
|
+
result.nodes.append(GraphNode(
|
|
154
|
+
id=entity_id,
|
|
155
|
+
kind=NodeKind.ENTITY,
|
|
156
|
+
label=table_name,
|
|
157
|
+
fqn=table_name,
|
|
158
|
+
module=ctx.module_name,
|
|
159
|
+
location=SourceLocation(
|
|
160
|
+
file_path=ctx.file_path,
|
|
161
|
+
line_start=find_line_number(text, m.start()),
|
|
162
|
+
),
|
|
163
|
+
properties={"framework": "efcore", "source": "migration"},
|
|
164
|
+
))
|
|
165
|
+
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _find_nearest_entity(result: DetectorResult, file_path: str, line_num: int) -> GraphNode | None:
|
|
170
|
+
"""Find the nearest ENTITY node at or after the given line in the same file."""
|
|
171
|
+
candidates = [
|
|
172
|
+
n for n in result.nodes
|
|
173
|
+
if n.kind == NodeKind.ENTITY
|
|
174
|
+
and n.location is not None
|
|
175
|
+
and n.location.file_path == file_path
|
|
176
|
+
]
|
|
177
|
+
if not candidates:
|
|
178
|
+
return None
|
|
179
|
+
# Find the entity whose line_start is closest to (and >= ) line_num
|
|
180
|
+
after = [n for n in candidates if n.location and n.location.line_start and n.location.line_start >= line_num]
|
|
181
|
+
if after:
|
|
182
|
+
return min(after, key=lambda n: n.location.line_start) # type: ignore[union-attr, return-value]
|
|
183
|
+
# Fallback: nearest entity before line_num
|
|
184
|
+
return max(candidates, key=lambda n: n.location.line_start if n.location and n.location.line_start else 0)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Regex-based .NET 6+ Minimal API detector for C# 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
|
+
_MAP_RE = re.compile(r'\.Map(Get|Post|Put|Delete|Patch)\s*\(\s*"([^"]*)"', re.MULTILINE)
|
|
18
|
+
_BUILDER_RE = re.compile(r'WebApplication\.CreateBuilder\s*\(', re.MULTILINE)
|
|
19
|
+
_AUTH_USE_RE = re.compile(r'\.Use(Authentication|Authorization)\s*\(', re.MULTILINE)
|
|
20
|
+
_AUTH_ADD_RE = re.compile(r'\.Add(Authentication|Authorization)\s*\(', re.MULTILINE)
|
|
21
|
+
_DI_RE = re.compile(r'\.Add(Scoped|Transient|Singleton)<(\w+)(?:,\s*(\w+))?>', re.MULTILINE)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CSharpMinimalApisDetector:
|
|
25
|
+
"""Detects .NET 6+ Minimal API patterns: endpoints, auth middleware, and DI registration."""
|
|
26
|
+
|
|
27
|
+
name: str = "csharp_minimal_apis"
|
|
28
|
+
supported_languages: tuple[str, ...] = ("csharp",)
|
|
29
|
+
|
|
30
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
31
|
+
result = DetectorResult()
|
|
32
|
+
text = decode_text(ctx)
|
|
33
|
+
|
|
34
|
+
app_module_id: str | None = None
|
|
35
|
+
|
|
36
|
+
# WebApplication.CreateBuilder() -> MODULE node
|
|
37
|
+
builder_match = _BUILDER_RE.search(text)
|
|
38
|
+
if builder_match:
|
|
39
|
+
app_module_id = f"dotnet:{ctx.file_path}:app"
|
|
40
|
+
line_num = find_line_number(text, builder_match.start())
|
|
41
|
+
result.nodes.append(GraphNode(
|
|
42
|
+
id=app_module_id,
|
|
43
|
+
kind=NodeKind.MODULE,
|
|
44
|
+
label=f"WebApplication({ctx.file_path})",
|
|
45
|
+
fqn=ctx.file_path,
|
|
46
|
+
module=ctx.module_name,
|
|
47
|
+
location=SourceLocation(
|
|
48
|
+
file_path=ctx.file_path,
|
|
49
|
+
line_start=line_num,
|
|
50
|
+
),
|
|
51
|
+
properties={"framework": "dotnet_minimal_api"},
|
|
52
|
+
))
|
|
53
|
+
|
|
54
|
+
# Map{Method}("/path", handler) -> ENDPOINT nodes
|
|
55
|
+
for m in _MAP_RE.finditer(text):
|
|
56
|
+
http_method = m.group(1).upper()
|
|
57
|
+
path = m.group(2)
|
|
58
|
+
line_num = find_line_number(text, m.start())
|
|
59
|
+
endpoint_id = f"dotnet:{ctx.file_path}:endpoint:{http_method}:{path}:{line_num}"
|
|
60
|
+
result.nodes.append(GraphNode(
|
|
61
|
+
id=endpoint_id,
|
|
62
|
+
kind=NodeKind.ENDPOINT,
|
|
63
|
+
label=f"{http_method} {path}",
|
|
64
|
+
fqn=f"{http_method} {path}",
|
|
65
|
+
module=ctx.module_name,
|
|
66
|
+
location=SourceLocation(
|
|
67
|
+
file_path=ctx.file_path,
|
|
68
|
+
line_start=line_num,
|
|
69
|
+
),
|
|
70
|
+
properties={
|
|
71
|
+
"http_method": http_method,
|
|
72
|
+
"path": path,
|
|
73
|
+
"framework": "dotnet_minimal_api",
|
|
74
|
+
},
|
|
75
|
+
))
|
|
76
|
+
# Link endpoint to app module if present
|
|
77
|
+
if app_module_id:
|
|
78
|
+
result.edges.append(GraphEdge(
|
|
79
|
+
source=app_module_id,
|
|
80
|
+
target=endpoint_id,
|
|
81
|
+
kind=EdgeKind.EXPOSES,
|
|
82
|
+
label=f"app exposes {http_method} {path}",
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
# UseAuthentication / UseAuthorization -> GUARD nodes
|
|
86
|
+
for m in _AUTH_USE_RE.finditer(text):
|
|
87
|
+
auth_type = m.group(1)
|
|
88
|
+
line_num = find_line_number(text, m.start())
|
|
89
|
+
guard_id = f"dotnet:{ctx.file_path}:guard:Use{auth_type}:{line_num}"
|
|
90
|
+
result.nodes.append(GraphNode(
|
|
91
|
+
id=guard_id,
|
|
92
|
+
kind=NodeKind.GUARD,
|
|
93
|
+
label=f"Use{auth_type}",
|
|
94
|
+
fqn=f"Use{auth_type}",
|
|
95
|
+
module=ctx.module_name,
|
|
96
|
+
location=SourceLocation(
|
|
97
|
+
file_path=ctx.file_path,
|
|
98
|
+
line_start=line_num,
|
|
99
|
+
),
|
|
100
|
+
properties={
|
|
101
|
+
"guard_type": auth_type.lower(),
|
|
102
|
+
"framework": "dotnet_minimal_api",
|
|
103
|
+
},
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
# AddAuthentication / AddAuthorization -> GUARD nodes
|
|
107
|
+
for m in _AUTH_ADD_RE.finditer(text):
|
|
108
|
+
auth_type = m.group(1)
|
|
109
|
+
line_num = find_line_number(text, m.start())
|
|
110
|
+
guard_id = f"dotnet:{ctx.file_path}:guard:Add{auth_type}:{line_num}"
|
|
111
|
+
result.nodes.append(GraphNode(
|
|
112
|
+
id=guard_id,
|
|
113
|
+
kind=NodeKind.GUARD,
|
|
114
|
+
label=f"Add{auth_type}",
|
|
115
|
+
fqn=f"Add{auth_type}",
|
|
116
|
+
module=ctx.module_name,
|
|
117
|
+
location=SourceLocation(
|
|
118
|
+
file_path=ctx.file_path,
|
|
119
|
+
line_start=line_num,
|
|
120
|
+
),
|
|
121
|
+
properties={
|
|
122
|
+
"guard_type": auth_type.lower(),
|
|
123
|
+
"framework": "dotnet_minimal_api",
|
|
124
|
+
},
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
# DI registration: AddScoped<IService, ServiceImpl>() -> DEPENDS_ON edge
|
|
128
|
+
for m in _DI_RE.finditer(text):
|
|
129
|
+
lifetime = m.group(1)
|
|
130
|
+
interface_name = m.group(2)
|
|
131
|
+
impl_name = m.group(3) # May be None for single-type registrations
|
|
132
|
+
if impl_name:
|
|
133
|
+
result.edges.append(GraphEdge(
|
|
134
|
+
source=f"dotnet:*:{impl_name}",
|
|
135
|
+
target=f"dotnet:*:{interface_name}",
|
|
136
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
137
|
+
label=f"{impl_name} registered as {interface_name} ({lifetime})",
|
|
138
|
+
properties={
|
|
139
|
+
"lifetime": lifetime.lower(),
|
|
140
|
+
"framework": "dotnet_minimal_api",
|
|
141
|
+
},
|
|
142
|
+
))
|
|
143
|
+
else:
|
|
144
|
+
# Self-registration like AddScoped<MyService>()
|
|
145
|
+
result.edges.append(GraphEdge(
|
|
146
|
+
source=f"dotnet:{ctx.file_path}:di:{interface_name}",
|
|
147
|
+
target=f"dotnet:*:{interface_name}",
|
|
148
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
149
|
+
label=f"{interface_name} registered as {lifetime}",
|
|
150
|
+
properties={
|
|
151
|
+
"lifetime": lifetime.lower(),
|
|
152
|
+
"framework": "dotnet_minimal_api",
|
|
153
|
+
},
|
|
154
|
+
))
|
|
155
|
+
|
|
156
|
+
return result
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Regex-based C# structures detector for C# source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
9
|
+
from osscodeiq.detectors.utils import decode_text, find_line_number
|
|
10
|
+
from osscodeiq.models.graph import (
|
|
11
|
+
EdgeKind,
|
|
12
|
+
GraphEdge,
|
|
13
|
+
GraphNode,
|
|
14
|
+
NodeKind,
|
|
15
|
+
SourceLocation,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_CLASS_RE = re.compile(
|
|
19
|
+
r'(?:public|internal|private|protected)?\s*'
|
|
20
|
+
r'(?:abstract|static|sealed|partial)?\s*'
|
|
21
|
+
r'class\s+(\w+)(?:\s*<[^>]+>)?(?:\s*:\s*([^{]+))?'
|
|
22
|
+
)
|
|
23
|
+
_INTERFACE_RE = re.compile(
|
|
24
|
+
r'(?:public|internal)?\s*interface\s+(\w+)(?:\s*<[^>]+>)?(?:\s*:\s*([^{]+))?'
|
|
25
|
+
)
|
|
26
|
+
_ENUM_RE = re.compile(r'(?:public|internal)?\s*enum\s+(\w+)')
|
|
27
|
+
_NAMESPACE_RE = re.compile(r'namespace\s+([\w.]+)')
|
|
28
|
+
_METHOD_RE = re.compile(
|
|
29
|
+
r'(?:public|protected|private|internal)\s+'
|
|
30
|
+
r'(?:static\s+|virtual\s+|override\s+|async\s+|abstract\s+)*'
|
|
31
|
+
r'(?:[\w<>\[\]?,\s]+)\s+(\w+)\s*\('
|
|
32
|
+
)
|
|
33
|
+
_USING_RE = re.compile(r'^\s*using\s+([\w.]+)\s*;', re.MULTILINE)
|
|
34
|
+
_HTTP_ATTR_RE = re.compile(r'\[(Http(?:Get|Post|Put|Delete|Patch))\s*(?:\("([^"]*)"\))?\]')
|
|
35
|
+
_ROUTE_RE = re.compile(r'\[Route\("([^"]*)"\)\]')
|
|
36
|
+
_API_CONTROLLER_RE = re.compile(r'\[ApiController\]')
|
|
37
|
+
_FUNCTION_RE = re.compile(r'\[Function\("([^"]+)"\)\]')
|
|
38
|
+
_HTTP_TRIGGER_RE = re.compile(r'\[HttpTrigger\(')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_base_types(base_str: str | None) -> tuple[str | None, list[str]]:
|
|
43
|
+
"""Parse the base type list after ':' in a class declaration.
|
|
44
|
+
|
|
45
|
+
Returns (base_class_or_none, list_of_interfaces).
|
|
46
|
+
Convention: interfaces in C# start with 'I' followed by an uppercase letter.
|
|
47
|
+
"""
|
|
48
|
+
if not base_str:
|
|
49
|
+
return None, []
|
|
50
|
+
parts = [p.strip() for p in base_str.split(",")]
|
|
51
|
+
parts = [p for p in parts if p]
|
|
52
|
+
base_class = None
|
|
53
|
+
interfaces: list[str] = []
|
|
54
|
+
for part in parts:
|
|
55
|
+
# Strip generic parameters for classification
|
|
56
|
+
clean = re.sub(r'<[^>]*>', '', part).strip()
|
|
57
|
+
if not clean:
|
|
58
|
+
continue
|
|
59
|
+
if len(clean) >= 2 and clean[0] == "I" and clean[1].isupper():
|
|
60
|
+
interfaces.append(clean)
|
|
61
|
+
elif base_class is None:
|
|
62
|
+
base_class = clean
|
|
63
|
+
else:
|
|
64
|
+
# Ambiguous; treat as interface
|
|
65
|
+
interfaces.append(clean)
|
|
66
|
+
return base_class, interfaces
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CSharpStructuresDetector:
|
|
70
|
+
"""Detects C# classes, interfaces, enums, namespaces, methods, and endpoints."""
|
|
71
|
+
|
|
72
|
+
name: str = "csharp_structures"
|
|
73
|
+
supported_languages: tuple[str, ...] = ("csharp",)
|
|
74
|
+
|
|
75
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
76
|
+
result = DetectorResult()
|
|
77
|
+
text = decode_text(ctx)
|
|
78
|
+
lines = text.split("\n")
|
|
79
|
+
|
|
80
|
+
# Namespace
|
|
81
|
+
namespace: str | None = None
|
|
82
|
+
ns_match = _NAMESPACE_RE.search(text)
|
|
83
|
+
if ns_match:
|
|
84
|
+
namespace = ns_match.group(1)
|
|
85
|
+
result.nodes.append(GraphNode(
|
|
86
|
+
id=f"{ctx.file_path}:namespace:{namespace}",
|
|
87
|
+
kind=NodeKind.MODULE,
|
|
88
|
+
label=namespace,
|
|
89
|
+
fqn=namespace,
|
|
90
|
+
module=ctx.module_name,
|
|
91
|
+
location=SourceLocation(
|
|
92
|
+
file_path=ctx.file_path,
|
|
93
|
+
line_start=find_line_number(text, ns_match.start()),
|
|
94
|
+
),
|
|
95
|
+
properties={},
|
|
96
|
+
))
|
|
97
|
+
|
|
98
|
+
# Using statements (imports)
|
|
99
|
+
for m in _USING_RE.finditer(text):
|
|
100
|
+
using_ns = m.group(1)
|
|
101
|
+
result.edges.append(GraphEdge(
|
|
102
|
+
source=ctx.file_path,
|
|
103
|
+
target=using_ns,
|
|
104
|
+
kind=EdgeKind.IMPORTS,
|
|
105
|
+
label=f"{ctx.file_path} imports {using_ns}",
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
# Detect class-level route for ASP.NET controllers
|
|
109
|
+
class_route: str | None = None
|
|
110
|
+
is_api_controller = bool(_API_CONTROLLER_RE.search(text))
|
|
111
|
+
|
|
112
|
+
# Classes
|
|
113
|
+
for m in _CLASS_RE.finditer(text):
|
|
114
|
+
class_name = m.group(1)
|
|
115
|
+
base_str = m.group(2)
|
|
116
|
+
line_num = find_line_number(text, m.start())
|
|
117
|
+
|
|
118
|
+
# Check if abstract
|
|
119
|
+
match_text = text[max(0, m.start() - 60):m.start() + len(m.group(0))]
|
|
120
|
+
is_abstract = "abstract" in match_text
|
|
121
|
+
kind = NodeKind.ABSTRACT_CLASS if is_abstract else NodeKind.CLASS
|
|
122
|
+
|
|
123
|
+
fqn = f"{namespace}.{class_name}" if namespace else class_name
|
|
124
|
+
node_id = f"{ctx.file_path}:{class_name}"
|
|
125
|
+
|
|
126
|
+
base_class, iface_list = _parse_base_types(base_str)
|
|
127
|
+
|
|
128
|
+
properties: dict[str, Any] = {}
|
|
129
|
+
if is_abstract:
|
|
130
|
+
properties["is_abstract"] = True
|
|
131
|
+
if base_class:
|
|
132
|
+
properties["base_class"] = base_class
|
|
133
|
+
if iface_list:
|
|
134
|
+
properties["interfaces"] = iface_list
|
|
135
|
+
|
|
136
|
+
result.nodes.append(GraphNode(
|
|
137
|
+
id=node_id,
|
|
138
|
+
kind=kind,
|
|
139
|
+
label=class_name,
|
|
140
|
+
fqn=fqn,
|
|
141
|
+
module=ctx.module_name,
|
|
142
|
+
location=SourceLocation(
|
|
143
|
+
file_path=ctx.file_path,
|
|
144
|
+
line_start=line_num,
|
|
145
|
+
),
|
|
146
|
+
properties=properties,
|
|
147
|
+
))
|
|
148
|
+
|
|
149
|
+
# Extends edge
|
|
150
|
+
if base_class:
|
|
151
|
+
result.edges.append(GraphEdge(
|
|
152
|
+
source=node_id,
|
|
153
|
+
target=f"*:{base_class}",
|
|
154
|
+
kind=EdgeKind.EXTENDS,
|
|
155
|
+
label=f"{class_name} extends {base_class}",
|
|
156
|
+
))
|
|
157
|
+
|
|
158
|
+
# Implements edges
|
|
159
|
+
for iface in iface_list:
|
|
160
|
+
result.edges.append(GraphEdge(
|
|
161
|
+
source=node_id,
|
|
162
|
+
target=f"*:{iface}",
|
|
163
|
+
kind=EdgeKind.IMPLEMENTS,
|
|
164
|
+
label=f"{class_name} implements {iface}",
|
|
165
|
+
))
|
|
166
|
+
|
|
167
|
+
# Check for [Route] attribute above this class
|
|
168
|
+
class_line_idx = line_num - 1
|
|
169
|
+
for j in range(max(0, class_line_idx - 5), class_line_idx):
|
|
170
|
+
route_m = _ROUTE_RE.search(lines[j])
|
|
171
|
+
if route_m:
|
|
172
|
+
class_route = route_m.group(1)
|
|
173
|
+
# Replace [controller] placeholder
|
|
174
|
+
controller_name = class_name
|
|
175
|
+
if controller_name.endswith("Controller"):
|
|
176
|
+
controller_name = controller_name[:-len("Controller")]
|
|
177
|
+
class_route = class_route.replace("[controller]", controller_name)
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
# Interfaces
|
|
181
|
+
for m in _INTERFACE_RE.finditer(text):
|
|
182
|
+
iface_name = m.group(1)
|
|
183
|
+
base_str = m.group(2)
|
|
184
|
+
fqn = f"{namespace}.{iface_name}" if namespace else iface_name
|
|
185
|
+
node_id = f"{ctx.file_path}:{iface_name}"
|
|
186
|
+
|
|
187
|
+
_, extended_ifaces = _parse_base_types(base_str)
|
|
188
|
+
|
|
189
|
+
properties = {}
|
|
190
|
+
if extended_ifaces:
|
|
191
|
+
properties["extends"] = extended_ifaces
|
|
192
|
+
|
|
193
|
+
result.nodes.append(GraphNode(
|
|
194
|
+
id=node_id,
|
|
195
|
+
kind=NodeKind.INTERFACE,
|
|
196
|
+
label=iface_name,
|
|
197
|
+
fqn=fqn,
|
|
198
|
+
module=ctx.module_name,
|
|
199
|
+
location=SourceLocation(
|
|
200
|
+
file_path=ctx.file_path,
|
|
201
|
+
line_start=find_line_number(text, m.start()),
|
|
202
|
+
),
|
|
203
|
+
properties=properties,
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
for ext in extended_ifaces:
|
|
207
|
+
result.edges.append(GraphEdge(
|
|
208
|
+
source=node_id,
|
|
209
|
+
target=f"*:{ext}",
|
|
210
|
+
kind=EdgeKind.EXTENDS,
|
|
211
|
+
label=f"{iface_name} extends {ext}",
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
# Enums
|
|
215
|
+
for m in _ENUM_RE.finditer(text):
|
|
216
|
+
enum_name = m.group(1)
|
|
217
|
+
fqn = f"{namespace}.{enum_name}" if namespace else enum_name
|
|
218
|
+
node_id = f"{ctx.file_path}:{enum_name}"
|
|
219
|
+
result.nodes.append(GraphNode(
|
|
220
|
+
id=node_id,
|
|
221
|
+
kind=NodeKind.ENUM,
|
|
222
|
+
label=enum_name,
|
|
223
|
+
fqn=fqn,
|
|
224
|
+
module=ctx.module_name,
|
|
225
|
+
location=SourceLocation(
|
|
226
|
+
file_path=ctx.file_path,
|
|
227
|
+
line_start=find_line_number(text, m.start()),
|
|
228
|
+
),
|
|
229
|
+
properties={},
|
|
230
|
+
))
|
|
231
|
+
|
|
232
|
+
# Methods and HTTP endpoints
|
|
233
|
+
for i, line in enumerate(lines):
|
|
234
|
+
method_m = _METHOD_RE.search(line)
|
|
235
|
+
if not method_m:
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
method_name = method_m.group(1)
|
|
239
|
+
# Skip common false positives
|
|
240
|
+
if method_name in ("if", "for", "while", "switch", "catch", "using", "return", "new", "class"):
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Look backwards for HTTP attribute annotations
|
|
244
|
+
http_method_str: str | None = None
|
|
245
|
+
http_path: str | None = None
|
|
246
|
+
for j in range(max(0, i - 5), i):
|
|
247
|
+
http_m = _HTTP_ATTR_RE.search(lines[j])
|
|
248
|
+
if http_m:
|
|
249
|
+
attr_name = http_m.group(1)
|
|
250
|
+
http_method_str = attr_name.replace("Http", "").upper()
|
|
251
|
+
http_path = http_m.group(2)
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
# Build endpoint node for HTTP-annotated methods
|
|
255
|
+
if http_method_str is not None:
|
|
256
|
+
path = http_path or ""
|
|
257
|
+
if class_route:
|
|
258
|
+
full_path = f"/{class_route.strip('/')}"
|
|
259
|
+
if path:
|
|
260
|
+
full_path = f"{full_path}/{path.lstrip('/')}"
|
|
261
|
+
else:
|
|
262
|
+
full_path = f"/{path.lstrip('/')}" if path else "/"
|
|
263
|
+
|
|
264
|
+
endpoint_label = f"{http_method_str} {full_path}"
|
|
265
|
+
endpoint_id = f"endpoint:{ctx.module_name}:{method_name}:{http_method_str}:{full_path}"
|
|
266
|
+
|
|
267
|
+
result.nodes.append(GraphNode(
|
|
268
|
+
id=endpoint_id,
|
|
269
|
+
kind=NodeKind.ENDPOINT,
|
|
270
|
+
label=endpoint_label,
|
|
271
|
+
fqn=f"{namespace}.{method_name}" if namespace else method_name,
|
|
272
|
+
module=ctx.module_name,
|
|
273
|
+
location=SourceLocation(
|
|
274
|
+
file_path=ctx.file_path,
|
|
275
|
+
line_start=i + 1,
|
|
276
|
+
),
|
|
277
|
+
annotations=[f"[{http_m.group(1)}]"],
|
|
278
|
+
properties={
|
|
279
|
+
"http_method": http_method_str,
|
|
280
|
+
"path": full_path,
|
|
281
|
+
},
|
|
282
|
+
))
|
|
283
|
+
|
|
284
|
+
# Azure Functions
|
|
285
|
+
for i, line in enumerate(lines):
|
|
286
|
+
func_m = _FUNCTION_RE.search(line)
|
|
287
|
+
if not func_m:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
func_name = func_m.group(1)
|
|
291
|
+
# Check if next few lines have HttpTrigger
|
|
292
|
+
is_http_trigger = False
|
|
293
|
+
for j in range(i, min(i + 10, len(lines))):
|
|
294
|
+
if _HTTP_TRIGGER_RE.search(lines[j]):
|
|
295
|
+
is_http_trigger = True
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
func_id = f"{ctx.file_path}:function:{func_name}"
|
|
299
|
+
properties: dict[str, Any] = {"function_name": func_name}
|
|
300
|
+
if is_http_trigger:
|
|
301
|
+
properties["trigger_type"] = "http"
|
|
302
|
+
|
|
303
|
+
result.nodes.append(GraphNode(
|
|
304
|
+
id=func_id,
|
|
305
|
+
kind=NodeKind.AZURE_FUNCTION,
|
|
306
|
+
label=f"Function({func_name})",
|
|
307
|
+
fqn=f"{namespace}.{func_name}" if namespace else func_name,
|
|
308
|
+
module=ctx.module_name,
|
|
309
|
+
location=SourceLocation(
|
|
310
|
+
file_path=ctx.file_path,
|
|
311
|
+
line_start=i + 1,
|
|
312
|
+
),
|
|
313
|
+
annotations=[f'[Function("{func_name}")]'],
|
|
314
|
+
properties=properties,
|
|
315
|
+
))
|
|
316
|
+
|
|
317
|
+
return result
|
|
File without changes
|