Skip to content

Commit ecf5d38

Browse files
aksOpsclaude
andcommitted
Phase 4b: 5 ORM/database detectors (Prisma, Sequelize, Mongoose, Pydantic, Django models)
New detectors (auto-discovered, 87 total): - prisma_orm: PrismaClient, model operations, $transaction - sequelize_orm: define/Model.init, associations, Sequelize connections - mongoose_orm: mongoose.model, Schema, connect, lifecycle hooks - pydantic_models: BaseModel/BaseSettings, fields, validators - django_models: models.Model, FK/M2M/OneToOne, Meta, Manager 56 new tests. 1,451 total, all passing. Benchmark: 3,787 nodes / 2,906 edges, 4.7s, 100% deterministic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 99318e1 commit ecf5d38

11 files changed

Lines changed: 1519 additions & 7 deletions

File tree

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,26 @@
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-82-brightgreen?style=flat-square&logo=codefactor&logoColor=white" alt="82 Detectors"></a><!-- /DYNAMIC:detectors -->
23+
<!-- DYNAMIC:detectors --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/detectors-87-brightgreen?style=flat-square&logo=codefactor&logoColor=white" alt="87 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 -->
2525
<<<<<<< HEAD
26-
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1359%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1359 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 -->
26+
<<<<<<< HEAD
27+
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1451%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1451 passed Tests"></a><!-- /DYNAMIC:tests -->
28+
<!-- DYNAMIC:files --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/files-264-informational?style=flat-square&logo=files&logoColor=white" alt="264 Files"></a><!-- /DYNAMIC:files -->
29+
<!-- DYNAMIC:loc --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/LOC-32%2C068-informational?style=flat-square&logo=codacy&logoColor=white" alt="32,068 Loc"></a><!-- /DYNAMIC:loc -->
30+
=======
31+
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1451%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1451 passed Tests"></a><!-- /DYNAMIC:tests -->
32+
<!-- DYNAMIC:files --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/files-264-informational?style=flat-square&logo=files&logoColor=white" alt="264 Files"></a><!-- /DYNAMIC:files -->
33+
<!-- DYNAMIC:loc --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/LOC-32%2C068-informational?style=flat-square&logo=codacy&logoColor=white" alt="32,068 Loc"></a><!-- /DYNAMIC:loc -->
34+
=======
35+
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1451%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1451 passed Tests"></a><!-- /DYNAMIC:tests -->
36+
<!-- DYNAMIC:files --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/files-264-informational?style=flat-square&logo=files&logoColor=white" alt="264 Files"></a><!-- /DYNAMIC:files -->
37+
<!-- DYNAMIC:loc --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/LOC-32%2C068-informational?style=flat-square&logo=codacy&logoColor=white" alt="32,068 Loc"></a><!-- /DYNAMIC:loc -->
2938
=======
30-
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1359%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1359 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 -->
39+
<!-- DYNAMIC:tests --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/tests-1451%20passed-brightgreen?style=flat-square&logo=pytest&logoColor=white" alt="1451 passed Tests"></a><!-- /DYNAMIC:tests -->
40+
<!-- DYNAMIC:files --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/files-264-informational?style=flat-square&logo=files&logoColor=white" alt="264 Files"></a><!-- /DYNAMIC:files -->
41+
<!-- DYNAMIC:loc --><a href="https://github.com/RandomCodeSpace/code-iq"><img src="https://img.shields.io/badge/LOC-32%2C068-informational?style=flat-square&logo=codacy&logoColor=white" alt="32,068 Loc"></a><!-- /DYNAMIC:loc -->
42+
>>>>>>> 82cf48b (Phase 4b: 5 ORM/database detectors (Prisma, Sequelize, Mongoose, Pydantic, Django models))
3343
>>>>>>> edb98b7 (Phase 4a: 6 high-impact structure + framework detectors)
3444
</p>
3545
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""Django model detector."""
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
9+
from code_intelligence.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
10+
11+
12+
class DjangoModelDetector:
13+
"""Detects Django model, manager, and relationship definitions."""
14+
15+
name: str = "python.django_models"
16+
supported_languages: tuple[str, ...] = ("python",)
17+
18+
_DJANGO_MODEL_RE = re.compile(
19+
r"^class\s+(\w+)\s*\(\s*(?:models\.Model|[\w.]*Model)\s*\)", re.MULTILINE
20+
)
21+
_FK_RE = re.compile(
22+
r"(\w+)\s*=\s*models\.(?:ForeignKey|OneToOneField)\s*\(\s*[\"']?(\w+)",
23+
re.MULTILINE,
24+
)
25+
_M2M_RE = re.compile(
26+
r"(\w+)\s*=\s*models\.ManyToManyField\s*\(\s*[\"']?(\w+)", re.MULTILINE
27+
)
28+
_FIELD_RE = re.compile(r"(\w+)\s*=\s*models\.(\w+Field)\s*\(", re.MULTILINE)
29+
_META_TABLE_RE = re.compile(r"db_table\s*=\s*[\"'](\w+)[\"']")
30+
_META_ORDERING_RE = re.compile(r"ordering\s*=\s*(\[.*?\])")
31+
_MANAGER_RE = re.compile(
32+
r"^class\s+(\w+)\s*\(\s*(?:models\.Manager|[\w.]*Manager)\s*\)", re.MULTILINE
33+
)
34+
_MANAGER_ASSIGNMENT_RE = re.compile(r"(\w+)\s*=\s*(\w+)\s*\(\s*\)", re.MULTILINE)
35+
36+
def detect(self, ctx: DetectorContext) -> DetectorResult:
37+
result = DetectorResult()
38+
text = decode_text(ctx)
39+
40+
# Detect managers first so we can link them
41+
manager_names: dict[str, str] = {}
42+
for match in self._MANAGER_RE.finditer(text):
43+
mgr_name = match.group(1)
44+
line = text[: match.start()].count("\n") + 1
45+
node_id = f"django:{ctx.file_path}:manager:{mgr_name}"
46+
manager_names[mgr_name] = node_id
47+
result.nodes.append(
48+
GraphNode(
49+
id=node_id,
50+
kind=NodeKind.REPOSITORY,
51+
label=mgr_name,
52+
fqn=f"{ctx.file_path}::{mgr_name}",
53+
module=ctx.module_name,
54+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
55+
properties={"framework": "django", "type": "manager"},
56+
)
57+
)
58+
59+
# Detect models
60+
for match in self._DJANGO_MODEL_RE.finditer(text):
61+
class_name = match.group(1)
62+
line = text[: match.start()].count("\n") + 1
63+
64+
# Determine class body boundaries
65+
class_start = match.start()
66+
next_class = re.search(r"\nclass\s+\w+", text[match.end() :])
67+
class_body = (
68+
text[class_start : match.end() + next_class.start()]
69+
if next_class
70+
else text[class_start:]
71+
)
72+
73+
# Extract fields
74+
fields: dict[str, str] = {}
75+
for fm in self._FIELD_RE.finditer(class_body):
76+
fields[fm.group(1)] = fm.group(2)
77+
78+
# Extract Meta properties
79+
table_name: str | None = None
80+
ordering: str | None = None
81+
meta_match = re.search(r"class\s+Meta\s*:", class_body)
82+
if meta_match:
83+
meta_start = meta_match.end()
84+
meta_end = len(class_body)
85+
for cm in re.finditer(r"\n\s{4}\S", class_body[meta_start:]):
86+
meta_end = meta_start + cm.start()
87+
break
88+
meta_block = class_body[meta_start:meta_end]
89+
table_match = self._META_TABLE_RE.search(meta_block)
90+
if table_match:
91+
table_name = table_match.group(1)
92+
ordering_match = self._META_ORDERING_RE.search(meta_block)
93+
if ordering_match:
94+
ordering = ordering_match.group(1)
95+
96+
node_id = f"django:{ctx.file_path}:model:{class_name}"
97+
props: dict = {
98+
"fields": fields,
99+
"framework": "django",
100+
}
101+
if table_name:
102+
props["table_name"] = table_name
103+
if ordering:
104+
props["ordering"] = ordering
105+
106+
result.nodes.append(
107+
GraphNode(
108+
id=node_id,
109+
kind=NodeKind.ENTITY,
110+
label=class_name,
111+
fqn=f"{ctx.file_path}::{class_name}",
112+
module=ctx.module_name,
113+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
114+
properties=props,
115+
)
116+
)
117+
118+
# FK / OneToOne edges
119+
for fk in self._FK_RE.finditer(class_body):
120+
target = fk.group(2)
121+
target_id = f"django:{ctx.file_path}:model:{target}"
122+
result.edges.append(
123+
GraphEdge(
124+
source=node_id,
125+
target=target_id,
126+
kind=EdgeKind.DEPENDS_ON,
127+
label=fk.group(1),
128+
)
129+
)
130+
131+
# M2M edges
132+
for m2m in self._M2M_RE.finditer(class_body):
133+
target = m2m.group(2)
134+
target_id = f"django:{ctx.file_path}:model:{target}"
135+
result.edges.append(
136+
GraphEdge(
137+
source=node_id,
138+
target=target_id,
139+
kind=EdgeKind.DEPENDS_ON,
140+
label=m2m.group(1),
141+
)
142+
)
143+
144+
# Manager assignments (objects = MyManager())
145+
for ma in self._MANAGER_ASSIGNMENT_RE.finditer(class_body):
146+
mgr_class = ma.group(2)
147+
if mgr_class in manager_names:
148+
result.edges.append(
149+
GraphEdge(
150+
source=node_id,
151+
target=manager_names[mgr_class],
152+
kind=EdgeKind.QUERIES,
153+
label=ma.group(1),
154+
)
155+
)
156+
157+
return result
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""Pydantic model detector."""
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
9+
from code_intelligence.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 commit comments

Comments
 (0)