codeanalyzer-python 1.2.0__tar.gz → 1.4.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-1.2.0 → codeanalyzer_python-1.4.0}/.gitignore +4 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/PKG-INFO +117 -7
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/README.md +115 -6
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/__main__.py +24 -1
- codeanalyzer_python-1.4.0/codeanalyzer/artifacts/__init__.py +20 -0
- codeanalyzer_python-1.4.0/codeanalyzer/artifacts/config_keys.py +588 -0
- codeanalyzer_python-1.4.0/codeanalyzer/artifacts/config_use.py +597 -0
- codeanalyzer_python-1.4.0/codeanalyzer/artifacts/config_use_rules.yml +58 -0
- codeanalyzer_python-1.4.0/codeanalyzer/artifacts/dependencies.py +237 -0
- codeanalyzer_python-1.4.0/codeanalyzer/artifacts/discovery.py +141 -0
- codeanalyzer_python-1.4.0/codeanalyzer/artifacts/parsers.py +248 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/core.py +90 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/builder.py +22 -3
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/sdg.py +17 -2
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/summaries.py +27 -4
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/bolt.py +35 -6
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/emit.py +3 -2
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/project.py +191 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/schema.py +74 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/options/options.py +4 -0
- codeanalyzer_python-1.4.0/codeanalyzer/schema/ids.py +44 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/py_schema.py +117 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +11 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/pyproject.toml +4 -1
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/schema.neo4j.json +148 -2
- codeanalyzer_python-1.2.0/codeanalyzer/schema/ids.py +0 -23
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/NOTICE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/access_paths.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/alias.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/cfg.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/defuse.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/dominance.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/identity.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/pdg.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/SSA/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/SSA/const.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/cfg/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/cfg/builder.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/cfg/model.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/core/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/core/func_call_visitor.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/core/vars_visitor.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scc.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/slicing.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/syntactic.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/detect.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/matching.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/pipeline.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/rules.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/rules.yml +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/jedi/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/jedi/jedi.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/cypher.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/rows.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/options/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/provenance.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/py.typed +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/assign_ids.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/call_graph_ids.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/l1_body.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/l2_callees.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/semantic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/semantic_analysis/defuse_linker.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/utils/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/utils/logging.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/utils/progress_bar.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/docs/handoff/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/docs/handoff/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/schema-uml.drawio +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/flask/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/NOTICE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/docs/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
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
|
|
@@ -21,6 +21,7 @@ Requires-Dist: ray==2.0.0; python_version < '3.11'
|
|
|
21
21
|
Requires-Dist: requests<3.0.0,>=2.20.0; python_version >= '3.11'
|
|
22
22
|
Requires-Dist: rich<14.0.0,>=12.6.0; python_version < '3.11'
|
|
23
23
|
Requires-Dist: rich<15.0.0,>=14.0.0; python_version >= '3.11'
|
|
24
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
24
25
|
Requires-Dist: typer<1.0.0,>=0.9.0; python_version < '3.11'
|
|
25
26
|
Requires-Dist: typer<2.0.0,>=0.9.0; python_version >= '3.11'
|
|
26
27
|
Requires-Dist: typing-extensions<5.0.0,>=4.0.0; python_version < '3.11'
|
|
@@ -98,7 +99,9 @@ needs.
|
|
|
98
99
|
**interprocedural SDG** (synthetic parameter vertices, `param_in`/`param_out`/`summary`,
|
|
99
100
|
alias-aware DDG) at level 4 — all built in-process from the stdlib `ast`.
|
|
100
101
|
- **Neo4j output** — project the analysis into a labeled property graph: a self-contained
|
|
101
|
-
`graph.cypher` snapshot, or an **incremental** push to a live database over Bolt.
|
|
102
|
+
`graph.cypher` snapshot, or an **incremental** push to a live database over Bolt. A push is
|
|
103
|
+
**additive by default** and never deletes: `--eager` is what permits it to remove declarations
|
|
104
|
+
and edges the source no longer has.
|
|
102
105
|
- **Versioned schema** — a machine-readable, version-stamped Neo4j schema contract (`--emit schema`),
|
|
103
106
|
checked in as `schema.neo4j.json` (`2.0.0`) and shipped with every release.
|
|
104
107
|
- **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
|
|
@@ -299,7 +302,19 @@ $ canpy --help
|
|
|
299
302
|
│ --eager --lazy Enable eager or │
|
|
300
303
|
│ lazy analysis. │
|
|
301
304
|
│ Defaults to │
|
|
302
|
-
│ lazy.
|
|
305
|
+
│ lazy. Also gates │
|
|
306
|
+
│ every │
|
|
307
|
+
│ destructive step │
|
|
308
|
+
│ of a '--emit │
|
|
309
|
+
│ neo4j' Bolt │
|
|
310
|
+
│ push: a lazy │
|
|
311
|
+
│ push only adds │
|
|
312
|
+
│ and updates, an │
|
|
313
|
+
│ eager one also │
|
|
314
|
+
│ removes │
|
|
315
|
+
│ declarations and │
|
|
316
|
+
│ edges the source │
|
|
317
|
+
│ no longer has. │
|
|
303
318
|
│ [default: lazy] │
|
|
304
319
|
│ --skip-tests --include-tests Skip test files │
|
|
305
320
|
│ in analysis. │
|
|
@@ -315,6 +330,16 @@ $ canpy --help
|
|
|
315
330
|
│ environment │
|
|
316
331
|
│ instead. │
|
|
317
332
|
│ [default: venv] │
|
|
333
|
+
│ --resolve-instal… Additionally │
|
|
334
|
+
│ bind imports via │
|
|
335
|
+
│ the project │
|
|
336
|
+
│ venv's installed │
|
|
337
|
+
│ metadata │
|
|
338
|
+
│ (*.dist-info); │
|
|
339
|
+
│ output becomes │
|
|
340
|
+
│ machine-depende… │
|
|
341
|
+
│ (prov: │
|
|
342
|
+
│ installed-metad… │
|
|
318
343
|
│ --file-name <path> Analyze only the │
|
|
319
344
|
│ specified file │
|
|
320
345
|
│ (relative to │
|
|
@@ -346,6 +371,22 @@ $ canpy --help
|
|
|
346
371
|
│ shipped rules. A │
|
|
347
372
|
│ malformed file │
|
|
348
373
|
│ is an error. │
|
|
374
|
+
│ --artifact-text --no-artifact-… Capture verbatim │
|
|
375
|
+
│ `source` text on │
|
|
376
|
+
│ discovered │
|
|
377
|
+
│ artifacts. │
|
|
378
|
+
│ `source` is the │
|
|
379
|
+
│ whole file; │
|
|
380
|
+
│ --no-artifact-t… │
|
|
381
|
+
│ empties it │
|
|
382
|
+
│ everywhere │
|
|
383
|
+
│ (inventory │
|
|
384
|
+
│ unchanged). │
|
|
385
|
+
│ sha256/size_byt… │
|
|
386
|
+
│ always reflect │
|
|
387
|
+
│ the full file. │
|
|
388
|
+
│ [default: │
|
|
389
|
+
│ artifact-text] │
|
|
349
390
|
│ --help Show this │
|
|
350
391
|
│ message and │
|
|
351
392
|
│ exit. │
|
|
@@ -517,14 +558,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
|
|
|
517
558
|
}
|
|
518
559
|
```
|
|
519
560
|
|
|
561
|
+
The application envelope also contains three substrate sections:
|
|
562
|
+
|
|
563
|
+
- **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
|
|
564
|
+
packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
|
|
565
|
+
`full`; default `none`), keyed by relative path; each artifact carries the
|
|
566
|
+
`can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
|
|
567
|
+
(keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
|
|
568
|
+
- **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
|
|
569
|
+
locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
|
|
570
|
+
installed metadata).
|
|
571
|
+
- **`unresolved_imports`** — modules imported but not resolvable in the declared dependency set,
|
|
572
|
+
one entry per module.
|
|
573
|
+
|
|
520
574
|
Notable properties:
|
|
521
575
|
|
|
522
576
|
- **Durable `can://` ids** identify every node at callable granularity and above
|
|
523
577
|
(`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
|
|
524
578
|
(`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
|
|
525
579
|
- **`source` lives once per module**; every node's text is the `module.source[span.bytes]` slice.
|
|
526
|
-
- **Cross-function edges** — `call_graph`, `param_in`, `param_out` — live at **application** scope;
|
|
580
|
+
- **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
|
|
527
581
|
the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
|
|
582
|
+
`config_uses` resolve from call-site key arguments to `ConfigKey` nodes; unresolved reads
|
|
583
|
+
go to `config_reads_unresolved`.
|
|
528
584
|
- **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
|
|
529
585
|
callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
|
|
530
586
|
homed in `application.external_symbols`.
|
|
@@ -545,9 +601,13 @@ binary format).
|
|
|
545
601
|
|
|
546
602
|
### Neo4j graph
|
|
547
603
|
|
|
548
|
-
`--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
|
|
549
|
-
label is `Py`-prefixed and every relationship type is
|
|
550
|
-
so multiple language analyzers can share one database
|
|
604
|
+
`--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
|
|
605
|
+
Python-specific node label is `Py`-prefixed and every Python-specific relationship type is
|
|
606
|
+
`PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple language analyzers can share one database
|
|
607
|
+
without label or relationship-type collisions. The one deliberate exception is the language-neutral
|
|
608
|
+
`Artifact`/`Package` subgraph (non-code files and third-party dependencies) — those nodes carry no
|
|
609
|
+
`Py` prefix, since they are meant as cross-language merge targets: a sibling-language analyzer over
|
|
610
|
+
the same repo should land on the same `Artifact`/`Package` nodes, not a per-language duplicate.
|
|
551
611
|
Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
|
|
552
612
|
inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
|
|
553
613
|
**CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
|
|
@@ -608,6 +668,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
|
|
|
608
668
|
RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
|
|
609
669
|
```
|
|
610
670
|
|
|
671
|
+
## Graph query cookbook
|
|
672
|
+
|
|
673
|
+
Example Cypher over the projected graph (`--emit neo4j`, then load `graph.cypher` or push via Bolt).
|
|
674
|
+
|
|
675
|
+
```cypher
|
|
676
|
+
// who calls this function? (direct callers)
|
|
677
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(t:PyCallable {name: "process_payment"})
|
|
678
|
+
RETURN c.id
|
|
679
|
+
|
|
680
|
+
// every callable that reaches a given library, via the external ghosts
|
|
681
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(e:PyExternal)
|
|
682
|
+
WHERE e.id CONTAINS "/@external/requests/"
|
|
683
|
+
RETURN DISTINCT c.id
|
|
684
|
+
|
|
685
|
+
// entrypoints and the frameworks that invoke them
|
|
686
|
+
MATCH (m:PyCallable {is_entrypoint: true})
|
|
687
|
+
RETURN m.id, m.entrypoint_frameworks
|
|
688
|
+
|
|
689
|
+
// data dependences into one statement (level 3+)
|
|
690
|
+
MATCH (s:PyBodyNode {id: $stmt})<-[d:PY_DDG]-(src:PyBodyNode)
|
|
691
|
+
RETURN src.id, d.var, d.prov
|
|
692
|
+
|
|
693
|
+
// interprocedural flow through a parameter (level 4)
|
|
694
|
+
MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
|
|
695
|
+
WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
|
|
696
|
+
RETURN a.id, f.id
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Artifact and dependency queries (1.3.0+):
|
|
700
|
+
|
|
701
|
+
```cypher
|
|
702
|
+
// all container/orchestration configs in the app
|
|
703
|
+
MATCH (a:PyApplication)-[:HAS_ARTIFACT]->(f:Artifact)
|
|
704
|
+
WHERE any(r IN f.roles WHERE r IN ["service-topology", "container-image"])
|
|
705
|
+
RETURN f.id, f.format
|
|
706
|
+
|
|
707
|
+
// every callable that reaches code from a declared package
|
|
708
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(:PyExternal)<-[:PY_PROVIDES]-(p:Package {id: "pkg:pypi/requests"})
|
|
709
|
+
RETURN c.id
|
|
710
|
+
|
|
711
|
+
// undeclared imports (dependency hygiene)
|
|
712
|
+
MATCH (a:PyApplication)-[u:PY_UNRESOLVED_IMPORT]->(e:PyExternal)
|
|
713
|
+
WHERE NOT (e)<-[:PY_PROVIDES]-(:Package)
|
|
714
|
+
RETURN e.id, u.prov
|
|
715
|
+
|
|
716
|
+
// which lock file pins this package, and to what
|
|
717
|
+
MATCH (f:Artifact)-[l:LOCKS]->(p:Package {id: "pkg:pypi/numpy"})
|
|
718
|
+
RETURN f.id, l.version
|
|
719
|
+
```
|
|
720
|
+
|
|
611
721
|
## License
|
|
612
722
|
|
|
613
723
|
Apache 2.0 — see [LICENSE](./LICENSE).
|
|
@@ -66,7 +66,9 @@ needs.
|
|
|
66
66
|
**interprocedural SDG** (synthetic parameter vertices, `param_in`/`param_out`/`summary`,
|
|
67
67
|
alias-aware DDG) at level 4 — all built in-process from the stdlib `ast`.
|
|
68
68
|
- **Neo4j output** — project the analysis into a labeled property graph: a self-contained
|
|
69
|
-
`graph.cypher` snapshot, or an **incremental** push to a live database over Bolt.
|
|
69
|
+
`graph.cypher` snapshot, or an **incremental** push to a live database over Bolt. A push is
|
|
70
|
+
**additive by default** and never deletes: `--eager` is what permits it to remove declarations
|
|
71
|
+
and edges the source no longer has.
|
|
70
72
|
- **Versioned schema** — a machine-readable, version-stamped Neo4j schema contract (`--emit schema`),
|
|
71
73
|
checked in as `schema.neo4j.json` (`2.0.0`) and shipped with every release.
|
|
72
74
|
- **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
|
|
@@ -267,7 +269,19 @@ $ canpy --help
|
|
|
267
269
|
│ --eager --lazy Enable eager or │
|
|
268
270
|
│ lazy analysis. │
|
|
269
271
|
│ Defaults to │
|
|
270
|
-
│ lazy.
|
|
272
|
+
│ lazy. Also gates │
|
|
273
|
+
│ every │
|
|
274
|
+
│ destructive step │
|
|
275
|
+
│ of a '--emit │
|
|
276
|
+
│ neo4j' Bolt │
|
|
277
|
+
│ push: a lazy │
|
|
278
|
+
│ push only adds │
|
|
279
|
+
│ and updates, an │
|
|
280
|
+
│ eager one also │
|
|
281
|
+
│ removes │
|
|
282
|
+
│ declarations and │
|
|
283
|
+
│ edges the source │
|
|
284
|
+
│ no longer has. │
|
|
271
285
|
│ [default: lazy] │
|
|
272
286
|
│ --skip-tests --include-tests Skip test files │
|
|
273
287
|
│ in analysis. │
|
|
@@ -283,6 +297,16 @@ $ canpy --help
|
|
|
283
297
|
│ environment │
|
|
284
298
|
│ instead. │
|
|
285
299
|
│ [default: venv] │
|
|
300
|
+
│ --resolve-instal… Additionally │
|
|
301
|
+
│ bind imports via │
|
|
302
|
+
│ the project │
|
|
303
|
+
│ venv's installed │
|
|
304
|
+
│ metadata │
|
|
305
|
+
│ (*.dist-info); │
|
|
306
|
+
│ output becomes │
|
|
307
|
+
│ machine-depende… │
|
|
308
|
+
│ (prov: │
|
|
309
|
+
│ installed-metad… │
|
|
286
310
|
│ --file-name <path> Analyze only the │
|
|
287
311
|
│ specified file │
|
|
288
312
|
│ (relative to │
|
|
@@ -314,6 +338,22 @@ $ canpy --help
|
|
|
314
338
|
│ shipped rules. A │
|
|
315
339
|
│ malformed file │
|
|
316
340
|
│ is an error. │
|
|
341
|
+
│ --artifact-text --no-artifact-… Capture verbatim │
|
|
342
|
+
│ `source` text on │
|
|
343
|
+
│ discovered │
|
|
344
|
+
│ artifacts. │
|
|
345
|
+
│ `source` is the │
|
|
346
|
+
│ whole file; │
|
|
347
|
+
│ --no-artifact-t… │
|
|
348
|
+
│ empties it │
|
|
349
|
+
│ everywhere │
|
|
350
|
+
│ (inventory │
|
|
351
|
+
│ unchanged). │
|
|
352
|
+
│ sha256/size_byt… │
|
|
353
|
+
│ always reflect │
|
|
354
|
+
│ the full file. │
|
|
355
|
+
│ [default: │
|
|
356
|
+
│ artifact-text] │
|
|
317
357
|
│ --help Show this │
|
|
318
358
|
│ message and │
|
|
319
359
|
│ exit. │
|
|
@@ -485,14 +525,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
|
|
|
485
525
|
}
|
|
486
526
|
```
|
|
487
527
|
|
|
528
|
+
The application envelope also contains three substrate sections:
|
|
529
|
+
|
|
530
|
+
- **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
|
|
531
|
+
packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
|
|
532
|
+
`full`; default `none`), keyed by relative path; each artifact carries the
|
|
533
|
+
`can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
|
|
534
|
+
(keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
|
|
535
|
+
- **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
|
|
536
|
+
locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
|
|
537
|
+
installed metadata).
|
|
538
|
+
- **`unresolved_imports`** — modules imported but not resolvable in the declared dependency set,
|
|
539
|
+
one entry per module.
|
|
540
|
+
|
|
488
541
|
Notable properties:
|
|
489
542
|
|
|
490
543
|
- **Durable `can://` ids** identify every node at callable granularity and above
|
|
491
544
|
(`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
|
|
492
545
|
(`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
|
|
493
546
|
- **`source` lives once per module**; every node's text is the `module.source[span.bytes]` slice.
|
|
494
|
-
- **Cross-function edges** — `call_graph`, `param_in`, `param_out` — live at **application** scope;
|
|
547
|
+
- **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
|
|
495
548
|
the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
|
|
549
|
+
`config_uses` resolve from call-site key arguments to `ConfigKey` nodes; unresolved reads
|
|
550
|
+
go to `config_reads_unresolved`.
|
|
496
551
|
- **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
|
|
497
552
|
callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
|
|
498
553
|
homed in `application.external_symbols`.
|
|
@@ -513,9 +568,13 @@ binary format).
|
|
|
513
568
|
|
|
514
569
|
### Neo4j graph
|
|
515
570
|
|
|
516
|
-
`--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
|
|
517
|
-
label is `Py`-prefixed and every relationship type is
|
|
518
|
-
so multiple language analyzers can share one database
|
|
571
|
+
`--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
|
|
572
|
+
Python-specific node label is `Py`-prefixed and every Python-specific relationship type is
|
|
573
|
+
`PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple language analyzers can share one database
|
|
574
|
+
without label or relationship-type collisions. The one deliberate exception is the language-neutral
|
|
575
|
+
`Artifact`/`Package` subgraph (non-code files and third-party dependencies) — those nodes carry no
|
|
576
|
+
`Py` prefix, since they are meant as cross-language merge targets: a sibling-language analyzer over
|
|
577
|
+
the same repo should land on the same `Artifact`/`Package` nodes, not a per-language duplicate.
|
|
519
578
|
Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
|
|
520
579
|
inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
|
|
521
580
|
**CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
|
|
@@ -576,6 +635,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
|
|
|
576
635
|
RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
|
|
577
636
|
```
|
|
578
637
|
|
|
638
|
+
## Graph query cookbook
|
|
639
|
+
|
|
640
|
+
Example Cypher over the projected graph (`--emit neo4j`, then load `graph.cypher` or push via Bolt).
|
|
641
|
+
|
|
642
|
+
```cypher
|
|
643
|
+
// who calls this function? (direct callers)
|
|
644
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(t:PyCallable {name: "process_payment"})
|
|
645
|
+
RETURN c.id
|
|
646
|
+
|
|
647
|
+
// every callable that reaches a given library, via the external ghosts
|
|
648
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(e:PyExternal)
|
|
649
|
+
WHERE e.id CONTAINS "/@external/requests/"
|
|
650
|
+
RETURN DISTINCT c.id
|
|
651
|
+
|
|
652
|
+
// entrypoints and the frameworks that invoke them
|
|
653
|
+
MATCH (m:PyCallable {is_entrypoint: true})
|
|
654
|
+
RETURN m.id, m.entrypoint_frameworks
|
|
655
|
+
|
|
656
|
+
// data dependences into one statement (level 3+)
|
|
657
|
+
MATCH (s:PyBodyNode {id: $stmt})<-[d:PY_DDG]-(src:PyBodyNode)
|
|
658
|
+
RETURN src.id, d.var, d.prov
|
|
659
|
+
|
|
660
|
+
// interprocedural flow through a parameter (level 4)
|
|
661
|
+
MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
|
|
662
|
+
WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
|
|
663
|
+
RETURN a.id, f.id
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Artifact and dependency queries (1.3.0+):
|
|
667
|
+
|
|
668
|
+
```cypher
|
|
669
|
+
// all container/orchestration configs in the app
|
|
670
|
+
MATCH (a:PyApplication)-[:HAS_ARTIFACT]->(f:Artifact)
|
|
671
|
+
WHERE any(r IN f.roles WHERE r IN ["service-topology", "container-image"])
|
|
672
|
+
RETURN f.id, f.format
|
|
673
|
+
|
|
674
|
+
// every callable that reaches code from a declared package
|
|
675
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(:PyExternal)<-[:PY_PROVIDES]-(p:Package {id: "pkg:pypi/requests"})
|
|
676
|
+
RETURN c.id
|
|
677
|
+
|
|
678
|
+
// undeclared imports (dependency hygiene)
|
|
679
|
+
MATCH (a:PyApplication)-[u:PY_UNRESOLVED_IMPORT]->(e:PyExternal)
|
|
680
|
+
WHERE NOT (e)<-[:PY_PROVIDES]-(:Package)
|
|
681
|
+
RETURN e.id, u.prov
|
|
682
|
+
|
|
683
|
+
// which lock file pins this package, and to what
|
|
684
|
+
MATCH (f:Artifact)-[l:LOCKS]->(p:Package {id: "pkg:pypi/numpy"})
|
|
685
|
+
RETURN f.id, l.version
|
|
686
|
+
```
|
|
687
|
+
|
|
579
688
|
## License
|
|
580
689
|
|
|
581
690
|
Apache 2.0 — see [LICENSE](./LICENSE).
|
|
@@ -175,7 +175,10 @@ def main(
|
|
|
175
175
|
bool,
|
|
176
176
|
typer.Option(
|
|
177
177
|
"--eager/--lazy",
|
|
178
|
-
help="Enable eager or lazy analysis. Defaults to lazy."
|
|
178
|
+
help="Enable eager or lazy analysis. Defaults to lazy. Also gates every "
|
|
179
|
+
"destructive step of a '--emit neo4j' Bolt push: a lazy push only adds and "
|
|
180
|
+
"updates, an eager one also removes declarations and edges the source no "
|
|
181
|
+
"longer has.",
|
|
179
182
|
),
|
|
180
183
|
] = False,
|
|
181
184
|
skip_tests: Annotated[
|
|
@@ -193,6 +196,14 @@ def main(
|
|
|
193
196
|
"imports against the ambient Python environment instead.",
|
|
194
197
|
),
|
|
195
198
|
] = False,
|
|
199
|
+
resolve_installed: Annotated[
|
|
200
|
+
bool,
|
|
201
|
+
typer.Option(
|
|
202
|
+
"--resolve-installed",
|
|
203
|
+
help="Additionally bind imports via the project venv's installed metadata "
|
|
204
|
+
"(*.dist-info); output becomes machine-dependent (prov: installed-metadata).",
|
|
205
|
+
),
|
|
206
|
+
] = False,
|
|
196
207
|
file_name: Annotated[
|
|
197
208
|
Optional[Path],
|
|
198
209
|
typer.Option(
|
|
@@ -226,6 +237,16 @@ def main(
|
|
|
226
237
|
"the shipped rules. A malformed file is an error.",
|
|
227
238
|
),
|
|
228
239
|
] = None,
|
|
240
|
+
artifact_text: Annotated[
|
|
241
|
+
bool,
|
|
242
|
+
typer.Option(
|
|
243
|
+
"--artifact-text/--no-artifact-text",
|
|
244
|
+
help="Capture verbatim `source` text on discovered artifacts. "
|
|
245
|
+
"`source` is the whole file; --no-artifact-text empties it "
|
|
246
|
+
"everywhere (inventory unchanged). sha256/size_bytes always "
|
|
247
|
+
"reflect the full file.",
|
|
248
|
+
),
|
|
249
|
+
] = True,
|
|
229
250
|
):
|
|
230
251
|
# Determinism: pin the interpreter hash seed before any analysis (no-op
|
|
231
252
|
# when PYTHONHASHSEED is already set; --version exits before this).
|
|
@@ -303,11 +324,13 @@ def main(
|
|
|
303
324
|
rebuild_analysis=rebuild_analysis,
|
|
304
325
|
skip_tests=skip_tests,
|
|
305
326
|
no_venv=no_venv,
|
|
327
|
+
resolve_installed=resolve_installed,
|
|
306
328
|
file_name=file_name,
|
|
307
329
|
cache_dir=cache_dir,
|
|
308
330
|
clear_cache=clear_cache,
|
|
309
331
|
verbosity=verbosity,
|
|
310
332
|
entrypoint_rules=tuple(entrypoint_rules or ()),
|
|
333
|
+
artifact_text=artifact_text,
|
|
311
334
|
)
|
|
312
335
|
|
|
313
336
|
_set_log_level(options.verbosity)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Non-code artifact capture and dependency extraction (spec 2026-08-27).
|
|
2
|
+
|
|
3
|
+
Capture never drops a file (every non-`.py` file becomes a
|
|
4
|
+
:class:`~codeanalyzer.schema.py_schema.PyArtifact`, rule-matched or not,
|
|
5
|
+
text or binary -- issue #157 follow-up); extraction is narrow (only
|
|
6
|
+
dependency manifests are parsed for meaning in this unit)."""
|
|
7
|
+
|
|
8
|
+
from codeanalyzer.artifacts.config_keys import extract_config_keys, is_config_eligible
|
|
9
|
+
from codeanalyzer.artifacts.config_use import (
|
|
10
|
+
dataflow_intra_tier, dataflow_interproc_tier, detect_config_reads, resolve_uses,
|
|
11
|
+
)
|
|
12
|
+
from codeanalyzer.artifacts.dependencies import build_dependency_view
|
|
13
|
+
from codeanalyzer.artifacts.discovery import discover_artifacts
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"discover_artifacts", "build_dependency_view",
|
|
17
|
+
"extract_config_keys", "is_config_eligible",
|
|
18
|
+
"detect_config_reads", "resolve_uses",
|
|
19
|
+
"dataflow_intra_tier", "dataflow_interproc_tier",
|
|
20
|
+
]
|