Skip to content

Commit 2baea28

Browse files
aksOpsclaude
andcommitted
Phase 4a: 6 high-impact structure + framework detectors
New detectors (auto-discovered, 82 total): - python_structures: classes, functions, imports, decorators, __all__ - typescript_structures: interfaces, types, classes, enums, namespaces - go_web: Gin, Echo, Chi, gorilla/mux, net/http endpoint detection - go_orm: GORM entities/migrations, sqlx, database/sql connections - csharp_efcore: DbContext, DbSet, migrations, Fluent API relationships - csharp_minimal_apis: MapGet/Post, auth middleware, DI registration Impact: contoso-real-estate 2,314 → 3,785 nodes (+63%) Coverage: 44K Python + 40K TS + 57K Go + 527 C# files now get structural extraction beyond framework-specific patterns. 82 detectors, 1,360 tests, deterministic, no performance regression. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 592daba commit 2baea28

13 files changed

Lines changed: 2446 additions & 4 deletions

File tree

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@
2020
<a href="https://github.com/RandomCodeSpace/code-iq/security/dependabot"><img src="https://img.shields.io/badge/dependabot-enabled-brightgreen?style=flat-square&logo=dependabot&logoColor=white" alt="Dependabot enabled"></a>
2121
<a href="https://github.com/RandomCodeSpace/code-iq/security/code-scanning"><img src="https://img.shields.io/badge/CodeQL-enabled-brightgreen?style=flat-square&logo=github&logoColor=white" alt="CodeQL"></a>
2222
<!-- DYNAMIC:vulnerabilities --><a href="https://github.com/RandomCodeSpace/code-iq/security/dependabot"><img src="https://img.shields.io/badge/vulnerabilities-0-brightgreen?style=flat-square&logo=hackthebox&logoColor=white" alt="0 Vulnerabilities"></a><!-- /DYNAMIC:vulnerabilities -->
23-
<!-- DYNAMIC:detectors --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/detectors-76-brightgreen?style=flat-square&logo=codefactor&logoColor=white" alt="76 Detectors"></a><!-- /DYNAMIC:detectors -->
23+
<!-- DYNAMIC:detectors --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/detectors-82-brightgreen?style=flat-square&logo=codefactor&logoColor=white" alt="82 Detectors"></a><!-- /DYNAMIC:detectors -->
2424
<!-- DYNAMIC:languages --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/languages-35-blue?style=flat-square&logo=stackblitz&logoColor=white" alt="35 Languages"></a><!-- /DYNAMIC:languages -->
25-
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1241%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1241 passed Tests"></a><!-- /DYNAMIC:tests -->
26-
<!-- DYNAMIC:files --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/files-242-informational?style=flat-square&logo=files&logoColor=white" alt="242 Files"></a><!-- /DYNAMIC:files -->
27-
<!-- DYNAMIC:loc --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/LOC-28%2C130-informational?style=flat-square&logo=codacy&logoColor=white" alt="28,130 Loc"></a><!-- /DYNAMIC:loc -->
25+
<<<<<<< HEAD
26+
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1360%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1360 passed Tests"></a><!-- /DYNAMIC:tests -->
27+
<!-- DYNAMIC:files --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/files-254-informational?style=flat-square&logo=files&logoColor=white" alt="254 Files"></a><!-- /DYNAMIC:files -->
28+
<!-- DYNAMIC:loc --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/LOC-30%2C566-informational?style=flat-square&logo=codacy&logoColor=white" alt="30,566 Loc"></a><!-- /DYNAMIC:loc -->
29+
=======
30+
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1360%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1360 passed Tests"></a><!-- /DYNAMIC:tests -->
31+
<!-- DYNAMIC:files --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/files-254-informational?style=flat-square&logo=files&logoColor=white" alt="254 Files"></a><!-- /DYNAMIC:files -->
32+
<!-- DYNAMIC:loc --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/LOC-30%2C566-informational?style=flat-square&logo=codacy&logoColor=white" alt="30,566 Loc"></a><!-- /DYNAMIC:loc -->
33+
>>>>>>> edb98b7 (Phase 4a: 6 high-impact structure + framework detectors)
2834
</p>
2935
3036
---
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 code_intelligence.detectors.base import DetectorContext, DetectorResult
8+
from code_intelligence.detectors.utils import decode_text, find_line_number
9+
from code_intelligence.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)
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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 code_intelligence.detectors.base import DetectorContext, DetectorResult
8+
from code_intelligence.detectors.utils import decode_text, find_line_number
9+
from code_intelligence.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+
line_num = find_line_number(text, m.start())
133+
134+
if impl_name:
135+
result.edges.append(GraphEdge(
136+
source=f"dotnet:*:{impl_name}",
137+
target=f"dotnet:*:{interface_name}",
138+
kind=EdgeKind.DEPENDS_ON,
139+
label=f"{impl_name} registered as {interface_name} ({lifetime})",
140+
properties={
141+
"lifetime": lifetime.lower(),
142+
"framework": "dotnet_minimal_api",
143+
},
144+
))
145+
else:
146+
# Self-registration like AddScoped<MyService>()
147+
result.edges.append(GraphEdge(
148+
source=f"dotnet:{ctx.file_path}:di:{interface_name}",
149+
target=f"dotnet:*:{interface_name}",
150+
kind=EdgeKind.DEPENDS_ON,
151+
label=f"{interface_name} registered as {lifetime}",
152+
properties={
153+
"lifetime": lifetime.lower(),
154+
"framework": "dotnet_minimal_api",
155+
},
156+
))
157+
158+
return result

0 commit comments

Comments
 (0)