Skip to content

Commit 4497b00

Browse files
committed
feat(schema): PyEntrypoint records on callables and classes (#27)
1 parent 25086c3 commit 4497b00

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

codeanalyzer/schema/py_schema.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,41 @@ class PyDecorator(BaseModel):
235235
span: Optional[Span] = None
236236

237237

238+
@builder
239+
class PyEntrypoint(BaseModel):
240+
"""One way a callable or class is invoked from outside the application (#27).
241+
242+
A node may hold several: two ``@app.route`` decorators, or a function that
243+
is both a Celery task and a CLI command. ``confidence`` lets a consumer
244+
threshold on evidence quality rather than inheriting this analyzer's
245+
judgement.
246+
"""
247+
248+
framework: str
249+
confidence: str = "certain" # "declared" | "certain" | "heuristic"
250+
rule: str = "" # rules.yml `id:`, or an engine name
251+
ruleset: str = "shipped" # "shipped" | "user:<path>"
252+
evidence: Optional[str] = None
253+
route: Optional[str] = None
254+
http_methods: List[str] = []
255+
via: Optional[str] = None # can:// id of the routed node dispatching here
256+
257+
258+
@builder
259+
class PyEntrypointReport(BaseModel):
260+
"""Coverage and failure record for the entrypoint pass (#27).
261+
262+
The pass under-approximates by design, so silence is its failure mode.
263+
This is what makes a gap visible instead of indistinguishable from
264+
"this project has no entrypoints".
265+
"""
266+
267+
frameworks_detected: List[str] = []
268+
rulesets: List[str] = []
269+
unresolved: Dict[str, int] = {}
270+
errors: List[str] = []
271+
272+
238273
@builder
239274
class PyCallableParameter(BaseModel):
240275
"""Represents a parameter of a Python callable (function/method)."""
@@ -291,6 +326,8 @@ class PyCallable(BaseModel):
291326
span: Optional[Span] = None
292327
comments: List[PyComment] = []
293328
decorators: List[PyDecorator] = []
329+
entrypoints: List[PyEntrypoint] = []
330+
is_entrypoint: bool = False
294331
parameters: List[PyCallableParameter] = []
295332
return_type: Optional[str] = None
296333
start_line: int = -1
@@ -340,6 +377,8 @@ class PyClass(BaseModel):
340377
comments: List[PyComment] = []
341378
base_classes: List[str] = []
342379
decorators: List[PyDecorator] = []
380+
entrypoints: List[PyEntrypoint] = []
381+
is_entrypoint: bool = False
343382
callables: Dict[str, PyCallable] = {} # methods, keystone containment name
344383
attributes: Dict[str, PyClassAttribute] = {}
345384
types: Dict[str, "PyClass"] = {} # inner classes, keystone containment name
@@ -432,6 +471,8 @@ class PyApplication(BaseModel):
432471
# builtin members), keyed by signature. Populated by the analyzer so every
433472
# backend (JSON and Neo4j) shares one authoritative external-symbol set.
434473
external_symbols: Dict[str, PyExternalSymbol] = {}
474+
# Coverage/failure record for the entrypoint pass; see PyEntrypointReport (#27).
475+
entrypoint_report: PyEntrypointReport = PyEntrypointReport()
435476
# Git provenance of the analyzed checkout, captured at analysis time.
436477
repository: Optional[PyRepositoryInfo] = None
437478
# Interprocedural parameter-passing edges (formal↔actual); populated at L4.

test/test_entrypoint_schema.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from codeanalyzer.schema.py_schema import (
2+
PyApplication, PyCallable, PyClass, PyEntrypoint, PyEntrypointReport,
3+
)
4+
5+
6+
def test_entrypoint_record_defaults():
7+
e = PyEntrypoint(framework="flask", confidence="certain", rule="flask.route", ruleset="shipped")
8+
assert e.evidence is None and e.route is None and e.via is None
9+
assert e.http_methods == []
10+
11+
12+
def test_callable_and_class_carry_entrypoints():
13+
c = PyCallable(name="f", path="a.py", signature="a.f")
14+
k = PyClass(name="C", signature="a.C")
15+
assert c.entrypoints == [] and c.is_entrypoint is False
16+
assert k.entrypoints == [] and k.is_entrypoint is False
17+
18+
19+
def test_application_carries_a_report():
20+
app = PyApplication(symbol_table={})
21+
assert isinstance(app.entrypoint_report, PyEntrypointReport)
22+
assert app.entrypoint_report.frameworks_detected == []

0 commit comments

Comments
 (0)