Skip to content

Commit a5fc904

Browse files
committed
feat(cli): emit the v2 Analysis envelope (schema_version/max_level/application)
1 parent 106f984 commit a5fc904

5 files changed

Lines changed: 36 additions & 9 deletions

File tree

codeanalyzer/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def main(
353353

354354
emit_neo4j(artifacts, options)
355355
elif options.output is None:
356-
print(model_dump_json(artifacts, separators=(",", ":")))
356+
print(model_dump_json(artifacts, exclude_none=True))
357357
else:
358358
options.output.mkdir(parents=True, exist_ok=True)
359359
_write_output(artifacts, options.output, options.format)
@@ -364,7 +364,7 @@ def _write_output(artifacts, output_dir: Path, format: OutputFormat):
364364
if format == OutputFormat.JSON:
365365
output_file = output_dir / "analysis.json"
366366
# Use Pydantic's model_dump_json() for compact output
367-
json_str = model_dump_json(artifacts, indent=None)
367+
json_str = model_dump_json(artifacts, indent=None, exclude_none=True)
368368
with output_file.open("w") as f:
369369
f.write(json_str)
370370
logger.info(f"Analysis saved to {output_file}")

codeanalyzer/core.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import ray
1212
from codeanalyzer.utils import logger
1313
from codeanalyzer.schema import (
14+
Analysis,
1415
PyApplication,
1516
PyExternalSymbol,
1617
PyModule,
@@ -407,9 +408,9 @@ def walk_class(cl):
407408
externals[sig] = PyExternalSymbol(name=name, module=module)
408409
return externals
409410

410-
def analyze(self) -> PyApplication:
411-
"""Analyze the project and return a PyApplication with symbol table.
412-
411+
def analyze(self) -> Analysis:
412+
"""Analyze the project and return the v2 ``Analysis`` envelope.
413+
413414
Uses caching to avoid re-analyzing unchanged files.
414415
"""
415416
cache_file = self.cache_dir / "analysis_cache.json"
@@ -465,7 +466,11 @@ def analyze(self) -> PyApplication:
465466
# Save to cache
466467
self._save_analysis_cache(app, cache_file)
467468

468-
return app
469+
return Analysis(
470+
max_level=self.analysis_level,
471+
k_limit=self.options.graph_field_depth,
472+
application=app,
473+
)
469474

470475
def _load_pyapplication_from_cache(self, cache_file: Path) -> PyApplication:
471476
"""Load cached analysis from file.

codeanalyzer/neo4j/emit.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from codeanalyzer.neo4j.cypher import render_cypher
3333
from codeanalyzer.neo4j.project import project
3434
from codeanalyzer.options import AnalysisOptions
35-
from codeanalyzer.schema import PyApplication
35+
from codeanalyzer.schema import Analysis
3636
from codeanalyzer.utils import logger
3737

3838

@@ -49,11 +49,11 @@ def emit_schema(output: Optional[Path]) -> None:
4949
logger.info(f"Neo4j schema written to {output / 'schema.json'}")
5050

5151

52-
def emit_neo4j(app: PyApplication, options: AnalysisOptions) -> None:
52+
def emit_neo4j(analysis: Analysis, options: AnalysisOptions) -> None:
5353
"""Project the analysis to a graph and write it: a live Bolt push when
5454
``--neo4j-uri`` is set, otherwise a self-contained ``graph.cypher`` snapshot."""
5555
app_name = options.app_name or Path(options.input).resolve().name
56-
rows = project(app, app_name)
56+
rows = project(analysis.application, app_name)
5757

5858
if options.neo4j_uri:
5959
cfg = BoltConfig(

codeanalyzer/schema/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ def model_dump_json(model, **kwargs):
7272
v1_kwargs = {}
7373
if 'indent' in kwargs:
7474
v1_kwargs['indent'] = kwargs['indent']
75+
if 'exclude_none' in kwargs:
76+
v1_kwargs['exclude_none'] = kwargs['exclude_none']
7577
if 'separators' in kwargs:
7678
# In v1, separators is passed to dumps_kwargs
7779
v1_kwargs['separators'] = kwargs['separators']

test/test_v2_conformance.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import json
2+
import subprocess
3+
import sys
4+
from pathlib import Path
5+
16
from codeanalyzer.schema.assign_ids import assign_ids
27
from codeanalyzer.schema.py_schema import PyApplication, PyModule, PyClass, PyCallable
38

@@ -13,3 +18,18 @@ def test_ids_assigned_down_the_tree():
1318
assert mod.id == "can://python/myapp/pkg/m.py"
1419
assert cl.id == "can://python/myapp/pkg/m.py/Hasher"
1520
assert fn.id == "can://python/myapp/pkg/m.py/Hasher/hash()"
21+
22+
23+
def test_cli_emits_v2_envelope(tmp_path: Path):
24+
proj = tmp_path / "proj"; proj.mkdir()
25+
(proj / "m.py").write_text("def f():\n return 1\n", encoding="utf-8")
26+
out = subprocess.run(
27+
[sys.executable, "-m", "codeanalyzer", "-i", str(proj), "-a", "1", "--no-venv"],
28+
capture_output=True, text=True, check=True,
29+
).stdout
30+
payload = json.loads(out)
31+
assert payload["schema_version"] == "2.0.0"
32+
assert payload["language"] == "python"
33+
assert payload["max_level"] == 1
34+
assert payload["application"]["kind"] == "application"
35+
assert "program_graphs" not in payload # dissolved into the tree

0 commit comments

Comments
 (0)