osscodeiq 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. osscodeiq/__init__.py +0 -0
  2. osscodeiq/analyzer.py +467 -0
  3. osscodeiq/cache/__init__.py +0 -0
  4. osscodeiq/cache/hasher.py +23 -0
  5. osscodeiq/cache/store.py +300 -0
  6. osscodeiq/classifiers/__init__.py +0 -0
  7. osscodeiq/classifiers/layer_classifier.py +69 -0
  8. osscodeiq/cli.py +721 -0
  9. osscodeiq/config.py +113 -0
  10. osscodeiq/detectors/__init__.py +0 -0
  11. osscodeiq/detectors/auth/__init__.py +0 -0
  12. osscodeiq/detectors/auth/certificate_auth.py +139 -0
  13. osscodeiq/detectors/auth/ldap_auth.py +89 -0
  14. osscodeiq/detectors/auth/session_header_auth.py +120 -0
  15. osscodeiq/detectors/base.py +41 -0
  16. osscodeiq/detectors/config/__init__.py +0 -0
  17. osscodeiq/detectors/config/batch_structure.py +128 -0
  18. osscodeiq/detectors/config/cloudformation.py +183 -0
  19. osscodeiq/detectors/config/docker_compose.py +179 -0
  20. osscodeiq/detectors/config/github_actions.py +150 -0
  21. osscodeiq/detectors/config/gitlab_ci.py +216 -0
  22. osscodeiq/detectors/config/helm_chart.py +187 -0
  23. osscodeiq/detectors/config/ini_structure.py +101 -0
  24. osscodeiq/detectors/config/json_structure.py +72 -0
  25. osscodeiq/detectors/config/kubernetes.py +305 -0
  26. osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
  27. osscodeiq/detectors/config/openapi.py +194 -0
  28. osscodeiq/detectors/config/package_json.py +99 -0
  29. osscodeiq/detectors/config/properties_detector.py +108 -0
  30. osscodeiq/detectors/config/pyproject_toml.py +169 -0
  31. osscodeiq/detectors/config/sql_structure.py +155 -0
  32. osscodeiq/detectors/config/toml_structure.py +93 -0
  33. osscodeiq/detectors/config/tsconfig_json.py +105 -0
  34. osscodeiq/detectors/config/yaml_structure.py +82 -0
  35. osscodeiq/detectors/cpp/__init__.py +0 -0
  36. osscodeiq/detectors/cpp/cpp_structures.py +192 -0
  37. osscodeiq/detectors/csharp/__init__.py +0 -0
  38. osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
  39. osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
  40. osscodeiq/detectors/csharp/csharp_structures.py +317 -0
  41. osscodeiq/detectors/docs/__init__.py +0 -0
  42. osscodeiq/detectors/docs/markdown_structure.py +117 -0
  43. osscodeiq/detectors/frontend/__init__.py +0 -0
  44. osscodeiq/detectors/frontend/angular_components.py +177 -0
  45. osscodeiq/detectors/frontend/frontend_routes.py +259 -0
  46. osscodeiq/detectors/frontend/react_components.py +148 -0
  47. osscodeiq/detectors/frontend/svelte_components.py +84 -0
  48. osscodeiq/detectors/frontend/vue_components.py +150 -0
  49. osscodeiq/detectors/generic/__init__.py +1 -0
  50. osscodeiq/detectors/generic/imports_detector.py +413 -0
  51. osscodeiq/detectors/go/__init__.py +0 -0
  52. osscodeiq/detectors/go/go_orm.py +202 -0
  53. osscodeiq/detectors/go/go_structures.py +162 -0
  54. osscodeiq/detectors/go/go_web.py +157 -0
  55. osscodeiq/detectors/iac/__init__.py +0 -0
  56. osscodeiq/detectors/iac/bicep.py +135 -0
  57. osscodeiq/detectors/iac/dockerfile.py +182 -0
  58. osscodeiq/detectors/iac/terraform.py +188 -0
  59. osscodeiq/detectors/java/__init__.py +0 -0
  60. osscodeiq/detectors/java/azure_functions.py +424 -0
  61. osscodeiq/detectors/java/azure_messaging.py +350 -0
  62. osscodeiq/detectors/java/class_hierarchy.py +349 -0
  63. osscodeiq/detectors/java/config_def.py +82 -0
  64. osscodeiq/detectors/java/cosmos_db.py +105 -0
  65. osscodeiq/detectors/java/graphql_resolver.py +188 -0
  66. osscodeiq/detectors/java/grpc_service.py +142 -0
  67. osscodeiq/detectors/java/ibm_mq.py +178 -0
  68. osscodeiq/detectors/java/jaxrs.py +160 -0
  69. osscodeiq/detectors/java/jdbc.py +196 -0
  70. osscodeiq/detectors/java/jms.py +116 -0
  71. osscodeiq/detectors/java/jpa_entity.py +143 -0
  72. osscodeiq/detectors/java/kafka.py +113 -0
  73. osscodeiq/detectors/java/kafka_protocol.py +70 -0
  74. osscodeiq/detectors/java/micronaut.py +248 -0
  75. osscodeiq/detectors/java/module_deps.py +191 -0
  76. osscodeiq/detectors/java/public_api.py +206 -0
  77. osscodeiq/detectors/java/quarkus.py +176 -0
  78. osscodeiq/detectors/java/rabbitmq.py +150 -0
  79. osscodeiq/detectors/java/raw_sql.py +136 -0
  80. osscodeiq/detectors/java/repository.py +131 -0
  81. osscodeiq/detectors/java/rmi.py +129 -0
  82. osscodeiq/detectors/java/spring_events.py +117 -0
  83. osscodeiq/detectors/java/spring_rest.py +168 -0
  84. osscodeiq/detectors/java/spring_security.py +212 -0
  85. osscodeiq/detectors/java/tibco_ems.py +193 -0
  86. osscodeiq/detectors/java/websocket.py +188 -0
  87. osscodeiq/detectors/kotlin/__init__.py +0 -0
  88. osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
  89. osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
  90. osscodeiq/detectors/proto/__init__.py +0 -0
  91. osscodeiq/detectors/proto/proto_structure.py +153 -0
  92. osscodeiq/detectors/python/__init__.py +0 -0
  93. osscodeiq/detectors/python/celery_tasks.py +88 -0
  94. osscodeiq/detectors/python/django_auth.py +132 -0
  95. osscodeiq/detectors/python/django_models.py +157 -0
  96. osscodeiq/detectors/python/django_views.py +74 -0
  97. osscodeiq/detectors/python/fastapi_auth.py +143 -0
  98. osscodeiq/detectors/python/fastapi_routes.py +68 -0
  99. osscodeiq/detectors/python/flask_routes.py +67 -0
  100. osscodeiq/detectors/python/kafka_python.py +175 -0
  101. osscodeiq/detectors/python/pydantic_models.py +115 -0
  102. osscodeiq/detectors/python/python_structures.py +234 -0
  103. osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
  104. osscodeiq/detectors/registry.py +100 -0
  105. osscodeiq/detectors/rust/__init__.py +0 -0
  106. osscodeiq/detectors/rust/actix_web.py +234 -0
  107. osscodeiq/detectors/rust/rust_structures.py +174 -0
  108. osscodeiq/detectors/scala/__init__.py +0 -0
  109. osscodeiq/detectors/scala/scala_structures.py +128 -0
  110. osscodeiq/detectors/shell/__init__.py +0 -0
  111. osscodeiq/detectors/shell/bash_detector.py +127 -0
  112. osscodeiq/detectors/shell/powershell_detector.py +118 -0
  113. osscodeiq/detectors/typescript/__init__.py +0 -0
  114. osscodeiq/detectors/typescript/express_routes.py +55 -0
  115. osscodeiq/detectors/typescript/fastify_routes.py +156 -0
  116. osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
  117. osscodeiq/detectors/typescript/kafka_js.py +164 -0
  118. osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
  119. osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
  120. osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
  121. osscodeiq/detectors/typescript/passport_jwt.py +133 -0
  122. osscodeiq/detectors/typescript/prisma_orm.py +96 -0
  123. osscodeiq/detectors/typescript/remix_routes.py +160 -0
  124. osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
  125. osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
  126. osscodeiq/detectors/typescript/typescript_structures.py +185 -0
  127. osscodeiq/detectors/utils.py +49 -0
  128. osscodeiq/discovery/__init__.py +11 -0
  129. osscodeiq/discovery/change_detector.py +97 -0
  130. osscodeiq/discovery/file_discovery.py +342 -0
  131. osscodeiq/flow/__init__.py +0 -0
  132. osscodeiq/flow/engine.py +78 -0
  133. osscodeiq/flow/models.py +72 -0
  134. osscodeiq/flow/renderer.py +127 -0
  135. osscodeiq/flow/templates/interactive.html +252 -0
  136. osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
  137. osscodeiq/flow/vendor/cytoscape.min.js +32 -0
  138. osscodeiq/flow/vendor/dagre.min.js +3809 -0
  139. osscodeiq/flow/views.py +357 -0
  140. osscodeiq/graph/__init__.py +0 -0
  141. osscodeiq/graph/backend.py +52 -0
  142. osscodeiq/graph/backends/__init__.py +23 -0
  143. osscodeiq/graph/backends/kuzu.py +576 -0
  144. osscodeiq/graph/backends/networkx.py +135 -0
  145. osscodeiq/graph/backends/sqlite_backend.py +406 -0
  146. osscodeiq/graph/builder.py +297 -0
  147. osscodeiq/graph/query.py +228 -0
  148. osscodeiq/graph/store.py +183 -0
  149. osscodeiq/graph/views.py +231 -0
  150. osscodeiq/models/__init__.py +17 -0
  151. osscodeiq/models/graph.py +116 -0
  152. osscodeiq/output/__init__.py +0 -0
  153. osscodeiq/output/dot.py +171 -0
  154. osscodeiq/output/mermaid.py +160 -0
  155. osscodeiq/output/safety.py +58 -0
  156. osscodeiq/output/serializers.py +42 -0
  157. osscodeiq/parsing/__init__.py +5 -0
  158. osscodeiq/parsing/languages/__init__.py +0 -0
  159. osscodeiq/parsing/languages/base.py +23 -0
  160. osscodeiq/parsing/languages/java.py +68 -0
  161. osscodeiq/parsing/languages/python.py +57 -0
  162. osscodeiq/parsing/languages/typescript.py +95 -0
  163. osscodeiq/parsing/parser_manager.py +125 -0
  164. osscodeiq/parsing/structured/__init__.py +0 -0
  165. osscodeiq/parsing/structured/gradle_parser.py +78 -0
  166. osscodeiq/parsing/structured/json_parser.py +24 -0
  167. osscodeiq/parsing/structured/properties_parser.py +56 -0
  168. osscodeiq/parsing/structured/sql_parser.py +54 -0
  169. osscodeiq/parsing/structured/xml_parser.py +148 -0
  170. osscodeiq/parsing/structured/yaml_parser.py +38 -0
  171. osscodeiq/server/__init__.py +7 -0
  172. osscodeiq/server/app.py +53 -0
  173. osscodeiq/server/mcp_server.py +174 -0
  174. osscodeiq/server/middleware.py +16 -0
  175. osscodeiq/server/routes.py +184 -0
  176. osscodeiq/server/service.py +445 -0
  177. osscodeiq/server/templates/welcome.html +56 -0
  178. osscodeiq-0.0.0.dist-info/METADATA +30 -0
  179. osscodeiq-0.0.0.dist-info/RECORD +183 -0
  180. osscodeiq-0.0.0.dist-info/WHEEL +5 -0
  181. osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
  182. osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
  183. osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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