codeanalyzer-python 0.3.0__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/.gitignore +11 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/PKG-INFO +233 -43
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/README.md +229 -41
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/__main__.py +77 -4
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/core.py +174 -72
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/__init__.py +35 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/access_paths.py +563 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/alias.py +93 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/builder.py +688 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/cfg.py +605 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/defuse.py +113 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/dominance.py +140 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/identity.py +91 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/pdg.py +100 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/scalpel_oracle.py +269 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/scc.py +91 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/sdg.py +424 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/slicing.py +93 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/summaries.py +217 -0
- codeanalyzer_python-1.0.0/codeanalyzer/dataflow/syntactic.py +26 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/neo4j/__init__.py +1 -1
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/neo4j/bolt.py +19 -4
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/neo4j/cypher.py +9 -3
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/neo4j/emit.py +10 -5
- codeanalyzer_python-1.0.0/codeanalyzer/neo4j/project.py +577 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/neo4j/rows.py +18 -15
- codeanalyzer_python-0.3.0/codeanalyzer/neo4j/catalog.py → codeanalyzer_python-1.0.0/codeanalyzer/neo4j/schema.py +100 -24
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/options/options.py +4 -0
- codeanalyzer_python-1.0.0/codeanalyzer/provenance.py +61 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/schema/__init__.py +19 -0
- codeanalyzer_python-1.0.0/codeanalyzer/schema/assign_ids.py +37 -0
- codeanalyzer_python-1.0.0/codeanalyzer/schema/call_graph_ids.py +12 -0
- codeanalyzer_python-1.0.0/codeanalyzer/schema/ids.py +23 -0
- codeanalyzer_python-1.0.0/codeanalyzer/schema/l1_body.py +29 -0
- codeanalyzer_python-1.0.0/codeanalyzer/schema/l2_callees.py +36 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/schema/py_schema.py +175 -26
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/semantic_analysis/call_graph.py +24 -27
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
- codeanalyzer_python-1.0.0/codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +103 -20
- codeanalyzer_python-1.0.0/docs/handoff/README.md +160 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0/docs/handoff}/schema.neo4j.json +124 -16
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/pyproject.toml +8 -2
- codeanalyzer_python-1.0.0/schema.neo4j.json +388 -0
- codeanalyzer_python-0.3.0/codeanalyzer/neo4j/project.py +0 -330
- codeanalyzer_python-0.3.0/codeanalyzer/neo4j/schema.py +0 -39
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/NOTICE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/config/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/config/config.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/jedi/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/jedi/jedi.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/options/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/py.typed +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/semantic_analysis/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/semantic_analysis/pycg/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/utils/__init__.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/utils/logging.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/codeanalyzer/utils/progress_bar.py +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/schema-uml.drawio +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/flask/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/requests/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/requests/NOTICE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/requests/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/xarray/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
- {codeanalyzer_python-0.3.0 → codeanalyzer_python-1.0.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/docs/README.md +0 -0
|
@@ -184,3 +184,14 @@ uv.lock
|
|
|
184
184
|
# Node / Astro docs-site build artifacts (never commit these)
|
|
185
185
|
node_modules/
|
|
186
186
|
.astro/
|
|
187
|
+
|
|
188
|
+
# Track this repo's CLAUDE.md even though a global gitignore excludes CLAUDE.md
|
|
189
|
+
!CLAUDE.md
|
|
190
|
+
|
|
191
|
+
# Track AGENTS.md (a symlink to CLAUDE.md) past the same global ignore
|
|
192
|
+
!AGENTS.md
|
|
193
|
+
|
|
194
|
+
# Track the schema decision log (SDK-model input) past a global .claude ignore
|
|
195
|
+
!.claude/
|
|
196
|
+
.claude/*
|
|
197
|
+
!.claude/SCHEMA_DECISIONS.md
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Static
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph.
|
|
5
5
|
Author-email: Rahul Krishna <i.m.ralk@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
License-File: NOTICE
|
|
@@ -33,6 +33,8 @@ Requires-Dist: typing-extensions<6.0.0,>=4.5.0; python_version >= '3.11'
|
|
|
33
33
|
Requires-Dist: uv>=0.5.0
|
|
34
34
|
Provides-Extra: neo4j
|
|
35
35
|
Requires-Dist: neo4j<6.0.0,>=5.0.0; extra == 'neo4j'
|
|
36
|
+
Provides-Extra: scalpel
|
|
37
|
+
Requires-Dist: python-scalpel>=1.0b0; extra == 'scalpel'
|
|
36
38
|
Description-Content-Type: text/markdown
|
|
37
39
|
|
|
38
40
|
<div align="center">
|
|
@@ -41,7 +43,7 @@ Description-Content-Type: text/markdown
|
|
|
41
43
|
|
|
42
44
|
# codeanalyzer-python (`canpy`)
|
|
43
45
|
|
|
44
|
-
**A Python static-analysis toolkit — the CLDK backend that emits
|
|
46
|
+
**A Python static-analysis toolkit — the CLDK backend that emits the canonical schema v2 Code Property Graph, as `analysis.json` or a Neo4j property graph.**
|
|
45
47
|
|
|
46
48
|
[](https://pypi.org/project/codeanalyzer-python/)
|
|
47
49
|
[](https://github.com/codellm-devkit/codeanalyzer-python/releases/latest)
|
|
@@ -52,18 +54,19 @@ Description-Content-Type: text/markdown
|
|
|
52
54
|
|
|
53
55
|
---
|
|
54
56
|
|
|
55
|
-
`canpy` is a static analyzer for Python built on [Jedi](https://jedi.readthedocs.io/),
|
|
56
|
-
[
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
[CLDK](https://github.com/codellm-devkit/python-sdk), mirroring its
|
|
57
|
+
`canpy` is a static analyzer for Python built on [Jedi](https://jedi.readthedocs.io/),
|
|
58
|
+
[PyCG](https://github.com/vitsalis/PyCG), and [Tree-sitter](https://tree-sitter.github.io/). It
|
|
59
|
+
emits the **canonical CodeLLM-DevKit (CLDK) schema v2** — a single, additive Code Property Graph
|
|
60
|
+
tree — either as `analysis.json` or projected into a **Neo4j property graph**. It is the Python
|
|
61
|
+
backend behind [CLDK](https://github.com/codellm-devkit/python-sdk), mirroring its
|
|
61
62
|
[TypeScript](https://github.com/codellm-devkit/codeanalyzer-typescript) (`cants`) and
|
|
62
63
|
[Java](https://github.com/codellm-devkit/codeanalyzer-java) siblings.
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
The payload is **one tree grown one layer at a time** across four analysis levels (`-a 1|2|3|4`): a
|
|
66
|
+
symbol table, a call graph, intraprocedural control- and data-dependence graphs, and a whole-program
|
|
67
|
+
interprocedural system dependence graph. Each level is a strict superset of the one below it
|
|
68
|
+
(`analysis.json(-a 1) ⊆ … ⊆ analysis.json(-a 4)`), so a consumer can request exactly the depth it
|
|
69
|
+
needs.
|
|
67
70
|
|
|
68
71
|
## Table of Contents
|
|
69
72
|
|
|
@@ -77,6 +80,9 @@ and merges them with the Jedi-derived edges, also backfilling callees Jedi could
|
|
|
77
80
|
- [Usage](#usage)
|
|
78
81
|
- [Options](#options)
|
|
79
82
|
- [Examples](#examples)
|
|
83
|
+
- [Analysis levels](#analysis-levels)
|
|
84
|
+
- [Architecture & Tooling](#architecture--tooling)
|
|
85
|
+
- [Output shape (canonical schema v2)](#output-shape-canonical-schema-v2)
|
|
80
86
|
- [Output targets](#output-targets)
|
|
81
87
|
- [`analysis.json` (default)](#analysisjson-default)
|
|
82
88
|
- [Neo4j graph](#neo4j-graph)
|
|
@@ -86,15 +92,21 @@ and merges them with the Jedi-derived edges, also backfilling callees Jedi could
|
|
|
86
92
|
|
|
87
93
|
## Features
|
|
88
94
|
|
|
95
|
+
- **Canonical schema v2** — one additive Code Property Graph tree (`schema_version` `2.0.0`),
|
|
96
|
+
stamped with `language`, `max_level`, `analyzer{name,version}`, and (at L3+) `k_limit`, rooted
|
|
97
|
+
at a single `application` node with durable `can://` ids on every callable and above.
|
|
89
98
|
- **Symbol table** — modules, classes, functions, methods, variables, decorators, imports, and
|
|
90
|
-
docstrings, with precise source spans.
|
|
91
|
-
- **Call graph** — Jedi's lexical resolver
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
docstrings, with precise byte-offset source spans; each module carries its `source` once.
|
|
100
|
+
- **Call graph** — Jedi's lexical resolver at level 1, enriched with **PyCG**-resolved edges at
|
|
101
|
+
level 2 (provenance-tagged, coupling-aware sharding for large apps).
|
|
102
|
+
- **Dataflow graphs** — native, per-callable exceptional **CFG** plus **control-** and
|
|
103
|
+
**data-dependence** edges (`cfg`/`cdg`/`ddg`) at level 3, stitched into a whole-program
|
|
104
|
+
**interprocedural SDG** (synthetic parameter vertices, `param_in`/`param_out`/`summary`,
|
|
105
|
+
alias-aware DDG) at level 4 — all built in-process from the stdlib `ast`.
|
|
94
106
|
- **Neo4j output** — project the analysis into a labeled property graph: a self-contained
|
|
95
107
|
`graph.cypher` snapshot, or an **incremental** push to a live database over Bolt.
|
|
96
108
|
- **Versioned schema** — a machine-readable, version-stamped Neo4j schema contract (`--emit schema`),
|
|
97
|
-
checked in as `schema.neo4j.json` and shipped with every release.
|
|
109
|
+
checked in as `schema.neo4j.json` (`2.0.0`) and shipped with every release.
|
|
98
110
|
- **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
|
|
99
111
|
reuses them, `--eager` forces a clean rebuild. `--ray` distributes the work across cores.
|
|
100
112
|
- **Compact output** — canonical `analysis.json`, or binary `analysis.msgpack` for smaller artifacts.
|
|
@@ -131,6 +143,13 @@ For the optional **live Neo4j push** (`--emit neo4j --neo4j-uri …`), install t
|
|
|
131
143
|
pip install 'codeanalyzer-python[neo4j]'
|
|
132
144
|
```
|
|
133
145
|
|
|
146
|
+
For the **Scalpel-backed points-to oracle** at level 4, install the `scalpel` extra. It is optional:
|
|
147
|
+
when it is absent, level 4 automatically falls back to the built-in type-based oracle.
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
pip install 'codeanalyzer-python[scalpel]'
|
|
151
|
+
```
|
|
152
|
+
|
|
134
153
|
### Install via shell script
|
|
135
154
|
|
|
136
155
|
Install the CLI as an isolated tool with the one-line installer (provisions via uv / pipx / pip):
|
|
@@ -181,6 +200,9 @@ $ canpy --help
|
|
|
181
200
|
Static Analysis on Python source code using Jedi, PyCG and Tree sitter.
|
|
182
201
|
|
|
183
202
|
╭─ Options ────────────────────────────────────────────────────────────────────╮
|
|
203
|
+
│ --version Show the canpy │
|
|
204
|
+
│ version and │
|
|
205
|
+
│ exit. │
|
|
184
206
|
│ --input -i PATH Path to the │
|
|
185
207
|
│ project root │
|
|
186
208
|
│ directory (not │
|
|
@@ -237,11 +259,47 @@ $ canpy --help
|
|
|
237
259
|
│ [env var: │
|
|
238
260
|
│ NEO4J_DATABASE] │
|
|
239
261
|
│ --analysis-level -a INTEGER RANGE Analysis depth: │
|
|
240
|
-
│ [1<=x<=
|
|
262
|
+
│ [1<=x<=4] 1=symbol │
|
|
241
263
|
│ table+Jedi call │
|
|
242
264
|
│ graph, 2=+PyCG │
|
|
243
|
-
│ call graph
|
|
265
|
+
│ call graph, │
|
|
266
|
+
│ 3=+native │
|
|
267
|
+
│ intraprocedural │
|
|
268
|
+
│ dataflow │
|
|
269
|
+
│ (CFG/PDG), │
|
|
270
|
+
│ 4=+interprocedu… │
|
|
271
|
+
│ SDG │
|
|
272
|
+
│ (param/summary │
|
|
273
|
+
│ edges, │
|
|
274
|
+
│ alias-aware │
|
|
275
|
+
│ DDG). │
|
|
244
276
|
│ [default: 1] │
|
|
277
|
+
│ --graphs TEXT Level 3+ only: │
|
|
278
|
+
│ comma-separated │
|
|
279
|
+
│ program-graph │
|
|
280
|
+
│ sections to emit │
|
|
281
|
+
│ (cfg, dfg, pdg, │
|
|
282
|
+
│ sdg). Default: │
|
|
283
|
+
│ cfg,dfg,pdg. │
|
|
284
|
+
│ `dfg` emits the │
|
|
285
|
+
│ PDG's data edges │
|
|
286
|
+
│ only; `sdg` │
|
|
287
|
+
│ requires -a 4. │
|
|
288
|
+
│ [default: │
|
|
289
|
+
│ cfg,dfg,pdg] │
|
|
290
|
+
│ --graph-field-de… INTEGER RANGE Level 3 only: │
|
|
291
|
+
│ [x>=1] k-limit on │
|
|
292
|
+
│ access-path │
|
|
293
|
+
│ depth (x.f.g.h │
|
|
294
|
+
│ with k=3 becomes │
|
|
295
|
+
│ x.f.g.*). │
|
|
296
|
+
│ Mandatory bound │
|
|
297
|
+
│ — it is what │
|
|
298
|
+
│ guarantees the │
|
|
299
|
+
│ interprocedural │
|
|
300
|
+
│ fixpoint │
|
|
301
|
+
│ terminates. │
|
|
302
|
+
│ [default: 3] │
|
|
245
303
|
│ --ray --no-ray Enable Ray for │
|
|
246
304
|
│ distributed │
|
|
247
305
|
│ analysis. │
|
|
@@ -438,14 +496,13 @@ $ canpy --help
|
|
|
438
496
|
canpy --input ./my-python-project --output ./out --format msgpack # → ./out/analysis.msgpack
|
|
439
497
|
```
|
|
440
498
|
|
|
441
|
-
3. **
|
|
499
|
+
3. **Enrich the call graph with PyCG (level 2):**
|
|
442
500
|
```sh
|
|
443
|
-
canpy --input ./my-python-project
|
|
501
|
+
canpy --input ./my-python-project -a 2
|
|
444
502
|
```
|
|
445
|
-
|
|
446
|
-
(
|
|
447
|
-
Jedi
|
|
448
|
-
integration is experimental; the CLI is downloaded into `<cache_dir>/codeql/` on first use.
|
|
503
|
+
Level 1 edges come from Jedi's lexical resolution. `-a 2` runs **PyCG** and merges its
|
|
504
|
+
flow-sensitive edges in (RPC / third-party / dynamically-dispatched targets), backfilling
|
|
505
|
+
callees Jedi could not resolve. Every edge is provenance-tagged (e.g. `jedi`, `pycg`).
|
|
449
506
|
|
|
450
507
|
4. **Emit a Neo4j snapshot, or push to a live database:**
|
|
451
508
|
```sh
|
|
@@ -465,31 +522,163 @@ $ canpy --help
|
|
|
465
522
|
canpy --input ./my-python-project --eager --cache-dir /path/to/custom-cache
|
|
466
523
|
```
|
|
467
524
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
525
|
+
7. **Dataflow graphs — intraprocedural (level 3) and interprocedural (level 4):**
|
|
526
|
+
```sh
|
|
527
|
+
canpy --input ./my-python-project -a 3 --output ./out # per-callable cfg/cdg/ddg
|
|
528
|
+
canpy --input ./my-python-project -a 4 --output ./out # + interprocedural SDG
|
|
529
|
+
canpy --input ./my-python-project -a 3 --graphs cfg,pdg # scope the emitted sections
|
|
530
|
+
canpy --input ./my-python-project -a 4 --graphs sdg # sdg requires -a 4
|
|
531
|
+
canpy --input ./my-python-project -a 3 --graph-field-depth 2 # tighter access-path k-limit
|
|
532
|
+
```
|
|
533
|
+
Levels 3 and 4 also enrich the Neo4j projection (`--emit neo4j`) with the CPG overlay
|
|
534
|
+
(`:PyCFGNode` nodes wired by `PY_CFG_NEXT`/`PY_CDG`/`PY_DDG`, plus the level-4
|
|
535
|
+
`PY_PARAM_IN`/`PY_PARAM_OUT`/`PY_SUMMARY` edges — the cross-language dataflow vocabulary,
|
|
536
|
+
PY_-namespaced like every other row family so multi-language databases never mingle
|
|
537
|
+
analyzers' edges).
|
|
538
|
+
|
|
539
|
+
## Analysis levels
|
|
540
|
+
|
|
541
|
+
Each level is the same tree grown one layer deeper, plus the edge family over that new layer. The
|
|
542
|
+
levels are cumulative and additive — `analysis.json(-a 1) ⊆ … ⊆ analysis.json(-a 4)`.
|
|
543
|
+
|
|
544
|
+
| Level | Flag | What it adds | Where it lands |
|
|
545
|
+
| --- | --- | --- | --- |
|
|
546
|
+
| **1** | `-a 1` (default) | Symbol table, Jedi call graph, and `call` nodes in each callable's `body` | `body` calls (`callee: null`) |
|
|
547
|
+
| **2** | `-a 2` | PyCG call-graph enrichment; each call's `callee` backfilled to a `can://` id | `call_graph`, `body` callees |
|
|
548
|
+
| **3** | `-a 3` | Native **intraprocedural** CFG/CDG/DDG (syntactic, name-equality, `prov: ["ssa"]`) | `cfg`, `cdg`, `ddg`, `@entry`/`@exit` on each callable |
|
|
549
|
+
| **4** | `-a 4` | **Interprocedural** SDG: synthetic param vertices, alias-aware DDG (`prov: ["points-to"]`) | `param_in`, `param_out`, `summary`, semantic `ddg` |
|
|
550
|
+
|
|
551
|
+
`-a 1`/`-a 2` timings and output are unaffected by the heavier levels — nothing at level 3+ runs
|
|
552
|
+
unless requested. Flag gating: `--graphs sdg` requires `-a 4`; `--graphs cfg,dfg,pdg` and
|
|
553
|
+
`--graph-field-depth` require `-a 3`.
|
|
554
|
+
|
|
555
|
+
## Architecture & Tooling
|
|
556
|
+
|
|
557
|
+
The dataflow substrate is hand-built from the standard library so every graph node joins back to a
|
|
558
|
+
symbol-table signature by construction
|
|
559
|
+
([#67](https://github.com/codellm-devkit/codeanalyzer-python/issues/67)):
|
|
560
|
+
|
|
561
|
+
- **CFG source:** a hand-built **exceptional** control-flow graph from the stdlib `ast` module — the
|
|
562
|
+
same parse the symbol-table builder uses. One synthetic `@entry`/`@exit` per callable,
|
|
563
|
+
statement-level nodes keyed `line:col` in source order, with exception / `yield` / `await` edges
|
|
564
|
+
first-class.
|
|
565
|
+
- **Def-use source:** hand-built **reaching definitions** (a classic forward worklist) over
|
|
566
|
+
k-limited access paths (`--graph-field-depth`, default 3) — there is no usable SSA library for
|
|
567
|
+
Python. This yields the level-3 syntactic DDG (name-equality, `prov: ["ssa"]`).
|
|
568
|
+
- **Points-to oracle (level 4):** the **Scalpel** may-alias oracle — `ScalpelAliasOracle`
|
|
569
|
+
(`codeanalyzer/dataflow/scalpel_oracle.py`) — consumes Scalpel's SSA copy/const facts to answer
|
|
570
|
+
`may_alias(path_a, path_b)`, adding the alias-aware DDG edges (`prov: ["points-to"]`) and the
|
|
571
|
+
interprocedural summaries. `python-scalpel` is an **optional dependency**
|
|
572
|
+
(`pip install 'codeanalyzer-python[scalpel]'`); when it is absent or cannot resolve a construct,
|
|
573
|
+
the analyzer automatically falls back to the built-in `TypeBasedAliasOracle` (Jedi-inferred types;
|
|
574
|
+
unknown types conservatively alias), keeping the `may_alias` interface total. Call dispatch comes
|
|
575
|
+
from the merged Jedi(+PyCG) call graph, treated as a frozen oracle.
|
|
576
|
+
- **Summaries:** relational formal-in → formal-out flows composed bottom-up over the Tarjan SCC
|
|
577
|
+
condensation of the call graph, a monotone fixpoint within SCCs; globals ride as extra formals,
|
|
578
|
+
closure captures bind at definition sites.
|
|
579
|
+
- **Slicing and taint are the SDK's responsibility.** A backward slicer ships in-process
|
|
580
|
+
(`codeanalyzer.dataflow.slicing`), but only as an **internal validation utility** for the L3/L4
|
|
581
|
+
gates — it is not a product surface. Once the SDG is emitted, slicing and taint become
|
|
582
|
+
language-independent labeled reachability and belong to the CLDK SDK across the provider/client
|
|
583
|
+
boundary; the analyzer emits the `summary` substrate and **no `taint_flows` section**.
|
|
584
|
+
- **Precision posture:** sound-leaning and over-approximate — prefer false positives to missed
|
|
585
|
+
flows. **Known unsoundness (documented, not silently absorbed):** `eval`/`exec`, reflection
|
|
586
|
+
(`getattr`/`setattr` with dynamic names), monkey-patching, C extensions, `import` side effects,
|
|
587
|
+
and module top-level statements (globals are modeled as formals instead).
|
|
588
|
+
|
|
589
|
+
## Output shape (canonical schema v2)
|
|
590
|
+
|
|
591
|
+
Every run produces the same envelope — an `Analysis` document — regardless of level; deeper levels
|
|
592
|
+
just populate more of the same tree:
|
|
471
593
|
|
|
472
|
-
|
|
594
|
+
```jsonc
|
|
595
|
+
{
|
|
596
|
+
"schema_version": "2.0.0",
|
|
597
|
+
"language": "python",
|
|
598
|
+
"max_level": 4, // the level this run was produced at
|
|
599
|
+
"k_limit": 3, // access-path depth bound (--graph-field-depth); L3+ only
|
|
600
|
+
"analyzer": { "name": "codeanalyzer-python", "version": "1.0.0" },
|
|
601
|
+
"application": {
|
|
602
|
+
"id": "can://python/<app>",
|
|
603
|
+
"kind": "application",
|
|
604
|
+
"symbol_table": { // relative POSIX path → module
|
|
605
|
+
"pkg/mod.py": {
|
|
606
|
+
"id": "can://python/<app>/pkg/mod.py",
|
|
607
|
+
"kind": "module",
|
|
608
|
+
"source": "…full file text, stored once per module…",
|
|
609
|
+
"types": { "<Class>": { "id": "…", "kind": "class", "callables": { /* methods */ } } },
|
|
610
|
+
"functions": { "<sig>": { /* callable, see below */ } }
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
"call_graph": [ { "src": "can://…/main(a)", "dst": "can://…/helper(x)",
|
|
614
|
+
"weight": 1, "prov": ["jedi", "pycg"] } ],
|
|
615
|
+
"external_symbols": { // imported/builtin call targets, keyed by id
|
|
616
|
+
"can://python/<app>/@external/os/getcwd":
|
|
617
|
+
{ "id": "can://python/<app>/@external/os/getcwd", "kind": "external",
|
|
618
|
+
"name": "getcwd", "module": "os" }
|
|
619
|
+
},
|
|
620
|
+
"param_in": [ { "src": "can://…/main(a)@6:4/actual_in:0", "dst": "can://…/helper(x)@formal_in:0" } ],
|
|
621
|
+
"param_out": [ { "src": "can://…/helper(x)@formal_out", "dst": "can://…/main(a)@6:4/actual_out" } ]
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
473
625
|
|
|
474
|
-
A
|
|
626
|
+
A **callable** (function or method) carries its own CPG, keyed by node id:
|
|
475
627
|
|
|
476
628
|
```jsonc
|
|
477
629
|
{
|
|
478
|
-
"
|
|
479
|
-
"
|
|
630
|
+
"id": "can://…/main(a)", "kind": "function",
|
|
631
|
+
"span": { "start": [5, 0], "end": [7, 12], "bytes": [43, 86] }, // byte offsets into module.source
|
|
632
|
+
"body": { // node id → node
|
|
633
|
+
"@entry": { "kind": "entry" },
|
|
634
|
+
"6:4": { "kind": "statement", "span": { … } },
|
|
635
|
+
"6:8": { "kind": "call", "span": { … }, "callee": "can://…/helper(x)" }, // callee null until L2
|
|
636
|
+
"@formal_in:0": { "kind": "formal_in", "of": "a" }, // L4 param vertices
|
|
637
|
+
"6:4/actual_in:0": { "kind": "actual_in", "of": "a", "parent": "6:4" },
|
|
638
|
+
"@exit": { "kind": "exit" }
|
|
639
|
+
},
|
|
640
|
+
"cfg": [ { "src": "@entry", "dst": "6:4", "kind": "fallthrough" } ], // L3
|
|
641
|
+
"cdg": [ { "src": "@entry", "dst": "6:4" } ], // L3
|
|
642
|
+
"ddg": [ { "src": "6:4", "dst": "7:4", "var": "h", "prov": ["ssa"] } ], // L3 ssa / L4 points-to
|
|
643
|
+
"summary": [ { "src": "6:4/actual_in:0", "dst": "6:4/actual_out" } ] // L4
|
|
480
644
|
}
|
|
481
645
|
```
|
|
482
646
|
|
|
483
|
-
|
|
484
|
-
|
|
647
|
+
Notable properties:
|
|
648
|
+
|
|
649
|
+
- **Durable `can://` ids** identify every node at callable granularity and above
|
|
650
|
+
(`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
|
|
651
|
+
(`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
|
|
652
|
+
- **`source` lives once per module**; every node's text is the `module.source[span.bytes]` slice.
|
|
653
|
+
- **Cross-function edges** — `call_graph`, `param_in`, `param_out` — live at **application** scope;
|
|
654
|
+
the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
|
|
655
|
+
- **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
|
|
656
|
+
callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
|
|
657
|
+
homed in `application.external_symbols`.
|
|
658
|
+
- **Breaking change from v1:** there is no more flat top-level `symbol_table`/`call_graph`, and no
|
|
659
|
+
separate program-graphs section. Everything now hangs off `application`, and the dataflow graphs
|
|
660
|
+
are inlined on each callable. Read `analysis.application.symbol_table` (was
|
|
661
|
+
`analysis.symbol_table`) and `analysis.application.call_graph` (was `analysis.call_graph`).
|
|
662
|
+
|
|
663
|
+
## Output targets
|
|
664
|
+
|
|
665
|
+
`canpy` builds one analysis in memory and can emit it three ways (`--emit`):
|
|
666
|
+
|
|
667
|
+
### `analysis.json` (default)
|
|
668
|
+
|
|
669
|
+
The `Analysis` envelope described above. By default it is printed to stdout as JSON; with `--output`
|
|
670
|
+
it is written to `analysis.json` (or `analysis.msgpack` with `--format msgpack`, a more compact
|
|
671
|
+
binary format).
|
|
485
672
|
|
|
486
673
|
### Neo4j graph
|
|
487
674
|
|
|
488
|
-
`--emit neo4j` projects the same analysis into a labeled property graph. Every node
|
|
489
|
-
`Py`-prefixed and every relationship type is `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`)
|
|
490
|
-
language analyzers can share one database without label or relationship-type collisions.
|
|
491
|
-
are keyed by their
|
|
492
|
-
decorators, and call sites are relationships
|
|
675
|
+
`--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every node
|
|
676
|
+
label is `Py`-prefixed and every relationship type is `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`)
|
|
677
|
+
so multiple language analyzers can share one database without label or relationship-type collisions.
|
|
678
|
+
Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
|
|
679
|
+
inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
|
|
680
|
+
**CPG overlay** — `:PyCFGNode` nodes (statements, and at level 4 the parameter vertices) wired by
|
|
681
|
+
`PY_CFG_NEXT`/`PY_CDG`/`PY_DDG`, plus the level-4 `PY_PARAM_IN`/`PY_PARAM_OUT`/`PY_SUMMARY` edges:
|
|
493
682
|
|
|
494
683
|
- **Without `--neo4j-uri`** — writes a self-contained `graph.cypher` (constraints + indexes, a scoped
|
|
495
684
|
wipe, then batched `MERGE`s). Load it with `cypher-shell < graph.cypher`. Needs no extra
|
|
@@ -516,10 +705,11 @@ canpy -i ./my-project --emit neo4j # credentials picked up from the environm
|
|
|
516
705
|
### Schema contract
|
|
517
706
|
|
|
518
707
|
`--emit schema` writes the machine-readable, version-stamped Neo4j schema (`schema.json`: node labels,
|
|
519
|
-
relationships, properties, constraints, and indexes). It needs no
|
|
520
|
-
as `schema.neo4j.json` and bundled in every release as a GitHub
|
|
521
|
-
validate producer/consumer compatibility without invoking the tool.
|
|
522
|
-
the
|
|
708
|
+
relationships, properties, constraints, and indexes; currently `schema_version` `2.0.0`). It needs no
|
|
709
|
+
project and is checked into the repo as `schema.neo4j.json` and bundled in every release as a GitHub
|
|
710
|
+
Release asset, so a consumer can validate producer/consumer compatibility without invoking the tool.
|
|
711
|
+
The shape of the contract matches the
|
|
712
|
+
[`codeanalyzer-typescript`](https://github.com/codellm-devkit/codeanalyzer-typescript) backend.
|
|
523
713
|
|
|
524
714
|
A UML of the `analysis.json` schema (the `PyApplication` containment tree) is checked in as
|
|
525
715
|
[`schema-uml.drawio`](./schema-uml.drawio), and the property-graph schema as
|