Skip to content

Commit 0020669

Browse files
committed
refactor(cli)!: drop the single-value --format flag and the last PyCG references
With msgpack gone (#118), --format had exactly one legal value and its help read 'json: json' — a flag that cannot choose anything. Removed: the CLI option, the OutputFormat enum, its options field, and the codeanalyzer/config package that existed only to hold it. analysis.json is always written; anyone passing --format json drops the flag (the #118 changelog entry now says so). Also sweeps the last live PyCG references out of README's analysis walkthrough — the level-2 description, levels table, frozen-oracle note, and the prov example now describe the defuse linker — and regenerates the --help block. Zero pycg mentions remain outside test fixtures and the changelog's own removal notice. CLI surface: 21 options, each audited; --file-name and --rebuild-analysis kept (distinct, real semantics). Full suite: 291 passed, 6 skipped.
1 parent 6d07098 commit 0020669

10 files changed

Lines changed: 40 additions & 67 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
`--format msgpack` CLI choice, the `analysis.msgpack` artifact, the msgpack
2828
serialization mixin on schema models, and the `msgpack` dependency are gone.
2929
`analysis.json` is the single wire format; anyone passing `--format msgpack`
30-
must drop the flag. `--format json` still works unchanged.
30+
must drop the flag. With one format left, the `--format` flag itself is now
31+
removed too — `analysis.json` is always written; anyone passing
32+
`--format json` must simply drop the flag.
3133
- **`--emit neo4j` now enforces its always-full-depth contract** (#119): it
3234
runs at level 4 with every graph section regardless of defaults, and
3335
explicitly passing `-a`/`--graphs` alongside it is now the documented

README.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,6 @@ $ canpy --help
165165
│ --emit schema). │
166166
│ --output -o <path> Output directory for │
167167
│ artifacts. │
168-
│ --format -f <json> Output format for --emit │
169-
│ json: json. │
170-
│ [default: json] │
171168
│ --emit <json|neo4j|schema> Output target: json │
172169
│ (analysis.json, default) | │
173170
│ neo4j (graph.cypher or live │
@@ -273,13 +270,14 @@ $ canpy --help
273270
canpy --input ./my-python-project --output ./out --format msgpack # → ./out/analysis.msgpack
274271
```
275272

276-
3. **Enrich the call graph with PyCG (level 2):**
273+
3. **Enrich the call graph with the defuse linker (level 2):**
277274
```sh
278275
canpy --input ./my-python-project -a 2
279276
```
280-
Level 1 edges come from Jedi's lexical resolution. `-a 2` runs **PyCG** and merges its
281-
flow-sensitive edges in (RPC / third-party / dynamically-dispatched targets), backfilling
282-
callees Jedi could not resolve. Every edge is provenance-tagged (e.g. `jedi`, `pycg`).
277+
Level 1 edges come from Jedi's lexical resolution. `-a 2` runs the **defuse linker**
278+
per-callable resolution over lexical scopes, import bindings, class hierarchies, and a
279+
bounded type-propagation round — and merges its edges with Jedi's, backfilling the
280+
callees Jedi could not resolve. Every edge is provenance-tagged (`jedi`, `defuse`).
283281

284282
4. **Emit a Neo4j snapshot, or push to a live database:**
285283
```sh
@@ -321,7 +319,7 @@ levels are cumulative and additive — `analysis.json(-a 1) ⊆ … ⊆ analysis
321319
| Level | Flag | What it adds | Where it lands |
322320
| --- | --- | --- | --- |
323321
| **1** | `-a 1` (default) | Symbol table, Jedi call graph, and `call` nodes in each callable's `body` | `body` calls (`callee: null`) |
324-
| **2** | `-a 2` | PyCG call-graph enrichment; each call's `callee` backfilled to a `can://` id | `call_graph`, `body` callees |
322+
| **2** | `-a 2` | Defuse-linker call-graph enrichment; each call's `callee` backfilled to a `can://` id | `call_graph`, `body` callees |
325323
| **3** | `-a 3` | Native **intraprocedural** CFG/CDG/DDG (syntactic, name-equality, `prov: ["ssa"]`) | `cfg`, `cdg`, `ddg`, `@entry`/`@exit` on each callable |
326324
| **4** | `-a 4` | **Interprocedural** SDG: synthetic param vertices, alias-aware DDG (`prov: ["points-to"]`) | `param_in`, `param_out`, `summary`, semantic `ddg` |
327325

@@ -350,7 +348,7 @@ symbol-table signature by construction
350348
external dependency to install; the analyzer falls back to the built-in `TypeBasedAliasOracle`
351349
(Jedi-inferred types; unknown types conservatively alias) only when Scalpel can't resolve a
352350
construct or a per-callable build fails, keeping the `may_alias` interface total. Call dispatch
353-
comes from the merged Jedi(+PyCG) call graph, treated as a frozen oracle.
351+
comes from the merged Jedi + defuse-linker call graph, treated as a frozen oracle.
354352
- **Summaries:** relational formal-in → formal-out flows composed bottom-up over the Tarjan SCC
355353
condensation of the call graph, a monotone fixpoint within SCCs; globals ride as extra formals,
356354
closure captures bind at definition sites.
@@ -389,7 +387,7 @@ just populate more of the same tree:
389387
}
390388
},
391389
"call_graph": [ { "src": "can://…/main(a)", "dst": "can://…/helper(x)",
392-
"weight": 1, "prov": ["jedi", "pycg"] } ],
390+
"weight": 1, "prov": ["defuse", "jedi"] } ],
393391
"external_symbols": { // imported/builtin call targets, keyed by id
394392
"can://python/<app>/@external/os/getcwd":
395393
{ "id": "can://python/<app>/@external/os/getcwd", "kind": "external",

codeanalyzer/__main__.py

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ def _pin_hash_seed() -> None:
3838

3939
from codeanalyzer.core import Codeanalyzer
4040
from codeanalyzer.utils import _set_log_level, logger
41-
from codeanalyzer.config import OutputFormat
4241
from codeanalyzer.schema import model_dump_json, strip_internal_only
4342
from codeanalyzer.options import AnalysisOptions, EmitTarget
4443

@@ -81,15 +80,6 @@ def main(
8180
Optional[Path],
8281
typer.Option("-o", "--output", help="Output directory for artifacts."),
8382
] = None,
84-
format: Annotated[
85-
OutputFormat,
86-
typer.Option(
87-
"-f",
88-
"--format",
89-
help="Output format for --emit json: json.",
90-
case_sensitive=False,
91-
),
92-
] = OutputFormat.JSON,
9383
emit: Annotated[
9484
EmitTarget,
9585
typer.Option(
@@ -299,7 +289,7 @@ def main(
299289
options = AnalysisOptions(
300290
input=input,
301291
output=output,
302-
format=format,
292+
303293
emit=emit,
304294
app_name=app_name,
305295
neo4j_uri=neo4j_uri,
@@ -384,22 +374,21 @@ def main(
384374
)
385375
else:
386376
options.output.mkdir(parents=True, exist_ok=True)
387-
_write_output(artifacts, options.output, options.format)
388-
389-
390-
def _write_output(artifacts, output_dir: Path, format: OutputFormat):
391-
"""Write artifacts to file in the specified format."""
392-
if format == OutputFormat.JSON:
393-
output_file = output_dir / "analysis.json"
394-
# Use Pydantic's model_dump_json() for compact output
395-
# Strip internal-only fields here rather than with a field-level Pydantic
396-
# `exclude`: the analysis cache shares the serializer and must keep them.
397-
json_str = json.dumps(
398-
strip_internal_only(artifacts.model_dump(mode="json", exclude_none=True))
399-
)
400-
with output_file.open("w") as f:
401-
f.write(json_str)
402-
logger.info(f"Analysis saved to {output_file}")
377+
_write_output(artifacts, options.output)
378+
379+
380+
def _write_output(artifacts, output_dir: Path):
381+
"""Write analysis.json (the single wire format since #118)."""
382+
output_file = output_dir / "analysis.json"
383+
# Use Pydantic's model_dump_json() for compact output
384+
# Strip internal-only fields here rather than with a field-level Pydantic
385+
# `exclude`: the analysis cache shares the serializer and must keep them.
386+
json_str = json.dumps(
387+
strip_internal_only(artifacts.model_dump(mode="json", exclude_none=True))
388+
)
389+
with output_file.open("w") as f:
390+
f.write(json_str)
391+
logger.info(f"Analysis saved to {output_file}")
403392

404393

405394
app = typer.Typer(

codeanalyzer/config/__init__.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

codeanalyzer/config/config.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

codeanalyzer/options/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from .options import AnalysisOptions, EmitTarget, OutputFormat
1+
from .options import AnalysisOptions, EmitTarget
22

3-
__all__ = ["AnalysisOptions", "EmitTarget", "OutputFormat"]
3+
__all__ = ["AnalysisOptions", "EmitTarget"]

codeanalyzer/options/options.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44
from enum import Enum
55

66

7-
class OutputFormat(str, Enum):
8-
JSON = "json"
9-
10-
117
class EmitTarget(str, Enum):
128
"""Output target selected by ``--emit``.
139
@@ -26,7 +22,6 @@ class EmitTarget(str, Enum):
2622
class AnalysisOptions:
2723
input: Path
2824
output: Optional[Path] = None
29-
format: OutputFormat = OutputFormat.JSON
3025
emit: EmitTarget = EmitTarget.JSON
3126
app_name: Optional[str] = None
3227
neo4j_uri: Optional[str] = None

test/test_cli.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ def test_cli_call_symbol_table_with_json(cli_runner, whole_applications__xarray)
3434
"--cache-dir",
3535
str(whole_applications__xarray.joinpath("test", ".cache")),
3636
"--clear-cache",
37-
"--format=json",
3837
],
3938
env={"NO_COLOR": "1", "TERM": "dumb"},
4039
)
@@ -92,7 +91,6 @@ def test_single_file(cli_runner, single_functionalities__stuff_nested_in_functio
9291
"--output",
9392
str(output_dir),
9493
"--eager",
95-
"--format=json",
9694
],
9795
env={"NO_COLOR": "1", "TERM": "dumb"},
9896
)
@@ -122,7 +120,6 @@ def _run_analysis(cli_runner, fixture_dir, analysis_level=1, file_name=None, ext
122120
"--clear-cache",
123121
"--analysis-level", str(analysis_level),
124122
"--skip-tests",
125-
"--format=json",
126123
]
127124
if file_name:
128125
args += ["--file-name", str(file_name)]

test/test_cli_emit_gates.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,16 @@ def test_emit_neo4j_runs_full_depth_by_default(cli_runner, tiny_project, tmp_pat
7878
# ----------------------------------------------------------------------------------------------
7979

8080

81-
def test_format_msgpack_is_rejected(cli_runner, tiny_project, tmp_path):
82-
r = _invoke(
83-
cli_runner,
84-
"--input", str(tiny_project), "--output", str(tmp_path / "out"),
85-
"--no-venv", "--format", "msgpack",
86-
)
87-
assert r.exit_code != 0, "--format msgpack must no longer be accepted"
81+
def test_format_flag_is_gone(cli_runner, tiny_project, tmp_path):
82+
"""#118 removed msgpack; with one format left the flag itself is gone —
83+
any --format spelling is an unknown-option error."""
84+
for value in ("json", "msgpack"):
85+
r = _invoke(
86+
cli_runner,
87+
"--input", str(tiny_project), "--output", str(tmp_path / "out"),
88+
"--no-venv", "--format", value,
89+
)
90+
assert r.exit_code != 0, f"--format {value} must be rejected"
8891

8992

9093
def test_msgpack_absent_from_model_and_help(cli_runner):

test/test_env_interpreter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ def test_all_files_failing_emits_an_error(tmp_path, monkeypatch, caplog):
132132
(proj / "b.py").write_text("def g():\n return 2\n", encoding="utf-8")
133133

134134
from codeanalyzer.options.options import AnalysisOptions
135-
from codeanalyzer.config import OutputFormat
136135
from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder
137136

138137
def boom(self, py_file):
@@ -141,7 +140,7 @@ def boom(self, py_file):
141140
monkeypatch.setattr(SymbolTableBuilder, "build_pymodule_from_file", boom)
142141

143142
opts = AnalysisOptions(
144-
input=proj, output=None, format=OutputFormat.JSON,
143+
input=proj, output=None,
145144
skip_tests=True, no_venv=True, cache_dir=tmp_path / "cache",
146145
rebuild_analysis=True,
147146
)

0 commit comments

Comments
 (0)