Skip to content

Commit 083abce

Browse files
committed
feat(entrypoints): end-to-end detection and Neo4j projection (#27)
Adds the local-decorator fixture and e2e test proving detection through the real CLI, including the --entrypoint-rules path. The fixture ships a requirements.txt naming its detect package so Stage 0 exercises the manifest-detection path (no other test covers it). Projects is_entrypoint/entrypoint_frameworks onto :PyCallable and :PyClass in the Neo4j schema and row builder, and regenerates the schema snapshot.
1 parent 9cbb34a commit 083abce

7 files changed

Lines changed: 88 additions & 2 deletions

File tree

codeanalyzer/neo4j/project.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,8 @@ def _class_props(cl: PyClass, file_key: str, source: str) -> Props:
517517
"start_line": cl.start_line,
518518
"end_line": cl.end_line,
519519
"_module": file_key,
520+
"is_entrypoint": bool(cl.entrypoints),
521+
"entrypoint_frameworks": sorted({e.framework for e in (cl.entrypoints or [])}),
520522
}
521523
)
522524

@@ -539,6 +541,8 @@ def _callable_props(c: PyCallable, file_key: str, source: str) -> Props:
539541
"parameters_json": _stringify_if(c.parameters),
540542
"accessed_symbols_json": _stringify_if(c.accessed_symbols),
541543
"_module": file_key,
544+
"is_entrypoint": bool(c.entrypoints),
545+
"entrypoint_frameworks": sorted({e.framework for e in (c.entrypoints or [])}),
542546
}
543547
)
544548

codeanalyzer/neo4j/schema.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ class RelType:
105105
"docstring": "string",
106106
**_SPAN,
107107
"_module": "string",
108+
"is_entrypoint": "boolean",
109+
"entrypoint_frameworks": "string[]",
108110
},
109111
),
110112
NodeLabel(
@@ -126,6 +128,8 @@ class RelType:
126128
"parameters_json": "string",
127129
"accessed_symbols_json": "string",
128130
"_module": "string",
131+
"is_entrypoint": "boolean",
132+
"entrypoint_frameworks": "string[]",
129133
},
130134
),
131135
NodeLabel(

schema.neo4j.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
"docstring": "string",
4646
"start_line": "integer",
4747
"end_line": "integer",
48-
"_module": "string"
48+
"_module": "string",
49+
"is_entrypoint": "boolean",
50+
"entrypoint_frameworks": "string[]"
4951
}
5052
},
5153
{
@@ -67,7 +69,9 @@
6769
"decorators": "string[]",
6870
"parameters_json": "string",
6971
"accessed_symbols_json": "string",
70-
"_module": "string"
72+
"_module": "string",
73+
"is_entrypoint": "boolean",
74+
"entrypoint_frameworks": "string[]"
7175
}
7276
},
7377
{
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
def route(path, methods=None):
2+
"""Stands in for a framework's routing decorator."""
3+
def deco(fn):
4+
return fn
5+
return deco
6+
7+
8+
@route("/products", methods=["POST"])
9+
def create_product():
10+
return helper()
11+
12+
13+
def helper():
14+
"""Called only internally - must NOT be flagged."""
15+
return {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
app
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
version: 1
2+
frameworks:
3+
inhouse:
4+
detect: [app]
5+
decorators:
6+
- id: inhouse.route
7+
match: "app.route"
8+
route: {from: positional, index: 0}
9+
methods: {from: keyword, name: methods, default: [GET]}

test/test_entrypoints_e2e.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""End-to-end coverage: entrypoint detection driven through the real CLI,
2+
including the ``--entrypoint-rules`` user-rules path (#27).
3+
4+
A local decorator (not Flask) so matching resolves deterministically via
5+
Jedi without a venv/network dependency -- see the fixture's ``rules.yml``
6+
for why. The fixture also carries a ``requirements.txt`` naming ``app`` so
7+
Stage 0 detection has a real manifest signal to key off of (the fixture
8+
module is not imported by anything, so an import scan alone would never
9+
see it).
10+
"""
11+
import json
12+
import subprocess
13+
from pathlib import Path
14+
15+
FIXTURE = Path(__file__).parent / "fixtures" / "single_functionalities" / "entrypoints_local"
16+
17+
18+
def test_decorated_function_flagged_and_helper_not(tmp_path):
19+
subprocess.run(
20+
[
21+
"uv", "run", "canpy",
22+
"-i", str(FIXTURE),
23+
"-a", "1",
24+
"-o", str(tmp_path),
25+
"--no-venv",
26+
# Cache defaults to the input dir; keep it in tmp_path so the
27+
# checked-in fixture directory is never mutated by a test run
28+
# and each run starts from a clean (entrypoint-free) cache.
29+
"--cache-dir", str(tmp_path / "cache"),
30+
"--entrypoint-rules", str(FIXTURE / "rules.yml"),
31+
],
32+
check=True,
33+
)
34+
data = json.loads((tmp_path / "analysis.json").read_text())
35+
fns = data["application"]["symbol_table"]["app.py"]["functions"]
36+
37+
create = fns["create_product"]
38+
assert create["is_entrypoint"] is True
39+
(ep,) = create["entrypoints"]
40+
assert ep["framework"] == "inhouse" and ep["rule"] == "inhouse.route"
41+
assert ep["route"] == "/products" and ep["http_methods"] == ["POST"]
42+
assert ep["ruleset"].startswith("user:")
43+
44+
assert fns["helper"]["is_entrypoint"] is False
45+
assert fns["helper"]["entrypoints"] == []
46+
47+
report = data["application"]["entrypoint_report"]
48+
assert "inhouse" in report["frameworks_detected"]
49+
assert report["errors"] == []

0 commit comments

Comments
 (0)