codeanalyzer-python 1.2.0__tar.gz → 1.3.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.3.0}/.gitignore +4 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/PKG-INFO +109 -5
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/README.md +107 -4
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/__main__.py +29 -0
- codeanalyzer_python-1.3.0/codeanalyzer/artifacts/__init__.py +20 -0
- codeanalyzer_python-1.3.0/codeanalyzer/artifacts/config_keys.py +588 -0
- codeanalyzer_python-1.3.0/codeanalyzer/artifacts/config_use.py +597 -0
- codeanalyzer_python-1.3.0/codeanalyzer/artifacts/config_use_rules.yml +58 -0
- codeanalyzer_python-1.3.0/codeanalyzer/artifacts/dependencies.py +237 -0
- codeanalyzer_python-1.3.0/codeanalyzer/artifacts/discovery.py +167 -0
- codeanalyzer_python-1.3.0/codeanalyzer/artifacts/parsers.py +248 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/core.py +91 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/builder.py +15 -1
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/project.py +192 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/schema.py +57 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/options/options.py +5 -0
- codeanalyzer_python-1.3.0/codeanalyzer/schema/ids.py +44 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/py_schema.py +118 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +11 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/pyproject.toml +4 -1
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/schema.neo4j.json +142 -1
- codeanalyzer_python-1.2.0/codeanalyzer/schema/ids.py +0 -23
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/NOTICE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/access_paths.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/alias.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/cfg.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/defuse.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/dominance.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/identity.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/pdg.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/SSA/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/SSA/const.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/builder.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/model.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/func_call_visitor.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/vars_visitor.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scc.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/sdg.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/slicing.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/summaries.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/syntactic.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/detect.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/matching.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/pipeline.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/rules.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/rules.yml +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/jedi/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/jedi/jedi.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/bolt.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/cypher.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/emit.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/rows.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/options/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/provenance.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/py.typed +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/assign_ids.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/call_graph_ids.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/l1_body.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/l2_callees.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/semantic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/semantic_analysis/defuse_linker.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/__init__.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/logging.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/progress_bar.py +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/docs/handoff/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/docs/handoff/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/schema-uml.drawio +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/NOTICE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
- {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.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.3.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'
|
|
@@ -315,6 +316,16 @@ $ canpy --help
|
|
|
315
316
|
│ environment │
|
|
316
317
|
│ instead. │
|
|
317
318
|
│ [default: venv] │
|
|
319
|
+
│ --resolve-instal… Additionally │
|
|
320
|
+
│ bind imports via │
|
|
321
|
+
│ the project │
|
|
322
|
+
│ venv's installed │
|
|
323
|
+
│ metadata │
|
|
324
|
+
│ (*.dist-info); │
|
|
325
|
+
│ output becomes │
|
|
326
|
+
│ machine-depende… │
|
|
327
|
+
│ (prov: │
|
|
328
|
+
│ installed-metad… │
|
|
318
329
|
│ --file-name <path> Analyze only the │
|
|
319
330
|
│ specified file │
|
|
320
331
|
│ (relative to │
|
|
@@ -346,6 +357,30 @@ $ canpy --help
|
|
|
346
357
|
│ shipped rules. A │
|
|
347
358
|
│ malformed file │
|
|
348
359
|
│ is an error. │
|
|
360
|
+
│ --artifact-text --no-artifact-… Capture verbatim │
|
|
361
|
+
│ `source` text on │
|
|
362
|
+
│ discovered │
|
|
363
|
+
│ artifacts. │
|
|
364
|
+
│ --no-artifact-t… │
|
|
365
|
+
│ empties `source` │
|
|
366
|
+
│ everywhere │
|
|
367
|
+
│ (inventory │
|
|
368
|
+
│ unchanged). │
|
|
369
|
+
│ [default: │
|
|
370
|
+
│ artifact-text] │
|
|
371
|
+
│ --artifact-text-… <int range> Per-file byte │
|
|
372
|
+
│ [x>=1] cap on captured │
|
|
373
|
+
│ artifact │
|
|
374
|
+
│ `source`; a │
|
|
375
|
+
│ decodable file │
|
|
376
|
+
│ over the cap is │
|
|
377
|
+
│ truncated │
|
|
378
|
+
│ (text_truncated… │
|
|
379
|
+
│ sha256/size_byt… │
|
|
380
|
+
│ always reflect │
|
|
381
|
+
│ the full file. │
|
|
382
|
+
│ [default: │
|
|
383
|
+
│ 262144] │
|
|
349
384
|
│ --help Show this │
|
|
350
385
|
│ message and │
|
|
351
386
|
│ exit. │
|
|
@@ -517,14 +552,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
|
|
|
517
552
|
}
|
|
518
553
|
```
|
|
519
554
|
|
|
555
|
+
The application envelope also contains three substrate sections:
|
|
556
|
+
|
|
557
|
+
- **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
|
|
558
|
+
packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
|
|
559
|
+
`full`; default `none`), keyed by relative path; each artifact carries the
|
|
560
|
+
`can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
|
|
561
|
+
(keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
|
|
562
|
+
- **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
|
|
563
|
+
locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
|
|
564
|
+
installed metadata).
|
|
565
|
+
- **`unresolved_imports`** — modules imported but not resolvable in the declared dependency set,
|
|
566
|
+
one entry per module.
|
|
567
|
+
|
|
520
568
|
Notable properties:
|
|
521
569
|
|
|
522
570
|
- **Durable `can://` ids** identify every node at callable granularity and above
|
|
523
571
|
(`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
|
|
524
572
|
(`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
|
|
525
573
|
- **`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;
|
|
574
|
+
- **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
|
|
527
575
|
the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
|
|
576
|
+
`config_uses` resolve from call-site key arguments to `ConfigKey` nodes; unresolved reads
|
|
577
|
+
go to `config_reads_unresolved`.
|
|
528
578
|
- **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
|
|
529
579
|
callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
|
|
530
580
|
homed in `application.external_symbols`.
|
|
@@ -545,9 +595,13 @@ binary format).
|
|
|
545
595
|
|
|
546
596
|
### Neo4j graph
|
|
547
597
|
|
|
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
|
|
598
|
+
`--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
|
|
599
|
+
Python-specific node label is `Py`-prefixed and every Python-specific relationship type is
|
|
600
|
+
`PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple language analyzers can share one database
|
|
601
|
+
without label or relationship-type collisions. The one deliberate exception is the language-neutral
|
|
602
|
+
`Artifact`/`Package` subgraph (non-code files and third-party dependencies) — those nodes carry no
|
|
603
|
+
`Py` prefix, since they are meant as cross-language merge targets: a sibling-language analyzer over
|
|
604
|
+
the same repo should land on the same `Artifact`/`Package` nodes, not a per-language duplicate.
|
|
551
605
|
Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
|
|
552
606
|
inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
|
|
553
607
|
**CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
|
|
@@ -608,6 +662,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
|
|
|
608
662
|
RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
|
|
609
663
|
```
|
|
610
664
|
|
|
665
|
+
## Graph query cookbook
|
|
666
|
+
|
|
667
|
+
Example Cypher over the projected graph (`--emit neo4j`, then load `graph.cypher` or push via Bolt).
|
|
668
|
+
|
|
669
|
+
```cypher
|
|
670
|
+
// who calls this function? (direct callers)
|
|
671
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(t:PyCallable {name: "process_payment"})
|
|
672
|
+
RETURN c.id
|
|
673
|
+
|
|
674
|
+
// every callable that reaches a given library, via the external ghosts
|
|
675
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(e:PyExternal)
|
|
676
|
+
WHERE e.id CONTAINS "/@external/requests/"
|
|
677
|
+
RETURN DISTINCT c.id
|
|
678
|
+
|
|
679
|
+
// entrypoints and the frameworks that invoke them
|
|
680
|
+
MATCH (m:PyCallable {is_entrypoint: true})
|
|
681
|
+
RETURN m.id, m.entrypoint_frameworks
|
|
682
|
+
|
|
683
|
+
// data dependences into one statement (level 3+)
|
|
684
|
+
MATCH (s:PyBodyNode {id: $stmt})<-[d:PY_DDG]-(src:PyBodyNode)
|
|
685
|
+
RETURN src.id, d.var, d.prov
|
|
686
|
+
|
|
687
|
+
// interprocedural flow through a parameter (level 4)
|
|
688
|
+
MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
|
|
689
|
+
WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
|
|
690
|
+
RETURN a.id, f.id
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Artifact and dependency queries (1.3.0+):
|
|
694
|
+
|
|
695
|
+
```cypher
|
|
696
|
+
// all container/orchestration configs in the app
|
|
697
|
+
MATCH (a:PyApplication)-[:HAS_ARTIFACT]->(f:Artifact)
|
|
698
|
+
WHERE any(r IN f.roles WHERE r IN ["service-topology", "container-image"])
|
|
699
|
+
RETURN f.id, f.format
|
|
700
|
+
|
|
701
|
+
// every callable that reaches code from a declared package
|
|
702
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(:PyExternal)<-[:PY_PROVIDES]-(p:Package {id: "pkg:pypi/requests"})
|
|
703
|
+
RETURN c.id
|
|
704
|
+
|
|
705
|
+
// undeclared imports (dependency hygiene)
|
|
706
|
+
MATCH (a:PyApplication)-[u:PY_UNRESOLVED_IMPORT]->(e:PyExternal)
|
|
707
|
+
WHERE NOT (e)<-[:PY_PROVIDES]-(:Package)
|
|
708
|
+
RETURN e.id, u.prov
|
|
709
|
+
|
|
710
|
+
// which lock file pins this package, and to what
|
|
711
|
+
MATCH (f:Artifact)-[l:LOCKS]->(p:Package {id: "pkg:pypi/numpy"})
|
|
712
|
+
RETURN f.id, l.version
|
|
713
|
+
```
|
|
714
|
+
|
|
611
715
|
## License
|
|
612
716
|
|
|
613
717
|
Apache 2.0 — see [LICENSE](./LICENSE).
|
|
@@ -283,6 +283,16 @@ $ canpy --help
|
|
|
283
283
|
│ environment │
|
|
284
284
|
│ instead. │
|
|
285
285
|
│ [default: venv] │
|
|
286
|
+
│ --resolve-instal… Additionally │
|
|
287
|
+
│ bind imports via │
|
|
288
|
+
│ the project │
|
|
289
|
+
│ venv's installed │
|
|
290
|
+
│ metadata │
|
|
291
|
+
│ (*.dist-info); │
|
|
292
|
+
│ output becomes │
|
|
293
|
+
│ machine-depende… │
|
|
294
|
+
│ (prov: │
|
|
295
|
+
│ installed-metad… │
|
|
286
296
|
│ --file-name <path> Analyze only the │
|
|
287
297
|
│ specified file │
|
|
288
298
|
│ (relative to │
|
|
@@ -314,6 +324,30 @@ $ canpy --help
|
|
|
314
324
|
│ shipped rules. A │
|
|
315
325
|
│ malformed file │
|
|
316
326
|
│ is an error. │
|
|
327
|
+
│ --artifact-text --no-artifact-… Capture verbatim │
|
|
328
|
+
│ `source` text on │
|
|
329
|
+
│ discovered │
|
|
330
|
+
│ artifacts. │
|
|
331
|
+
│ --no-artifact-t… │
|
|
332
|
+
│ empties `source` │
|
|
333
|
+
│ everywhere │
|
|
334
|
+
│ (inventory │
|
|
335
|
+
│ unchanged). │
|
|
336
|
+
│ [default: │
|
|
337
|
+
│ artifact-text] │
|
|
338
|
+
│ --artifact-text-… <int range> Per-file byte │
|
|
339
|
+
│ [x>=1] cap on captured │
|
|
340
|
+
│ artifact │
|
|
341
|
+
│ `source`; a │
|
|
342
|
+
│ decodable file │
|
|
343
|
+
│ over the cap is │
|
|
344
|
+
│ truncated │
|
|
345
|
+
│ (text_truncated… │
|
|
346
|
+
│ sha256/size_byt… │
|
|
347
|
+
│ always reflect │
|
|
348
|
+
│ the full file. │
|
|
349
|
+
│ [default: │
|
|
350
|
+
│ 262144] │
|
|
317
351
|
│ --help Show this │
|
|
318
352
|
│ message and │
|
|
319
353
|
│ exit. │
|
|
@@ -485,14 +519,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
|
|
|
485
519
|
}
|
|
486
520
|
```
|
|
487
521
|
|
|
522
|
+
The application envelope also contains three substrate sections:
|
|
523
|
+
|
|
524
|
+
- **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
|
|
525
|
+
packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
|
|
526
|
+
`full`; default `none`), keyed by relative path; each artifact carries the
|
|
527
|
+
`can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
|
|
528
|
+
(keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
|
|
529
|
+
- **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
|
|
530
|
+
locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
|
|
531
|
+
installed metadata).
|
|
532
|
+
- **`unresolved_imports`** — modules imported but not resolvable in the declared dependency set,
|
|
533
|
+
one entry per module.
|
|
534
|
+
|
|
488
535
|
Notable properties:
|
|
489
536
|
|
|
490
537
|
- **Durable `can://` ids** identify every node at callable granularity and above
|
|
491
538
|
(`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
|
|
492
539
|
(`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
|
|
493
540
|
- **`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;
|
|
541
|
+
- **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
|
|
495
542
|
the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
|
|
543
|
+
`config_uses` resolve from call-site key arguments to `ConfigKey` nodes; unresolved reads
|
|
544
|
+
go to `config_reads_unresolved`.
|
|
496
545
|
- **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
|
|
497
546
|
callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
|
|
498
547
|
homed in `application.external_symbols`.
|
|
@@ -513,9 +562,13 @@ binary format).
|
|
|
513
562
|
|
|
514
563
|
### Neo4j graph
|
|
515
564
|
|
|
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
|
|
565
|
+
`--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
|
|
566
|
+
Python-specific node label is `Py`-prefixed and every Python-specific relationship type is
|
|
567
|
+
`PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple language analyzers can share one database
|
|
568
|
+
without label or relationship-type collisions. The one deliberate exception is the language-neutral
|
|
569
|
+
`Artifact`/`Package` subgraph (non-code files and third-party dependencies) — those nodes carry no
|
|
570
|
+
`Py` prefix, since they are meant as cross-language merge targets: a sibling-language analyzer over
|
|
571
|
+
the same repo should land on the same `Artifact`/`Package` nodes, not a per-language duplicate.
|
|
519
572
|
Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
|
|
520
573
|
inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
|
|
521
574
|
**CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
|
|
@@ -576,6 +629,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
|
|
|
576
629
|
RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
|
|
577
630
|
```
|
|
578
631
|
|
|
632
|
+
## Graph query cookbook
|
|
633
|
+
|
|
634
|
+
Example Cypher over the projected graph (`--emit neo4j`, then load `graph.cypher` or push via Bolt).
|
|
635
|
+
|
|
636
|
+
```cypher
|
|
637
|
+
// who calls this function? (direct callers)
|
|
638
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(t:PyCallable {name: "process_payment"})
|
|
639
|
+
RETURN c.id
|
|
640
|
+
|
|
641
|
+
// every callable that reaches a given library, via the external ghosts
|
|
642
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(e:PyExternal)
|
|
643
|
+
WHERE e.id CONTAINS "/@external/requests/"
|
|
644
|
+
RETURN DISTINCT c.id
|
|
645
|
+
|
|
646
|
+
// entrypoints and the frameworks that invoke them
|
|
647
|
+
MATCH (m:PyCallable {is_entrypoint: true})
|
|
648
|
+
RETURN m.id, m.entrypoint_frameworks
|
|
649
|
+
|
|
650
|
+
// data dependences into one statement (level 3+)
|
|
651
|
+
MATCH (s:PyBodyNode {id: $stmt})<-[d:PY_DDG]-(src:PyBodyNode)
|
|
652
|
+
RETURN src.id, d.var, d.prov
|
|
653
|
+
|
|
654
|
+
// interprocedural flow through a parameter (level 4)
|
|
655
|
+
MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
|
|
656
|
+
WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
|
|
657
|
+
RETURN a.id, f.id
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Artifact and dependency queries (1.3.0+):
|
|
661
|
+
|
|
662
|
+
```cypher
|
|
663
|
+
// all container/orchestration configs in the app
|
|
664
|
+
MATCH (a:PyApplication)-[:HAS_ARTIFACT]->(f:Artifact)
|
|
665
|
+
WHERE any(r IN f.roles WHERE r IN ["service-topology", "container-image"])
|
|
666
|
+
RETURN f.id, f.format
|
|
667
|
+
|
|
668
|
+
// every callable that reaches code from a declared package
|
|
669
|
+
MATCH (c:PyCallable)-[:PY_CALLS]->(:PyExternal)<-[:PY_PROVIDES]-(p:Package {id: "pkg:pypi/requests"})
|
|
670
|
+
RETURN c.id
|
|
671
|
+
|
|
672
|
+
// undeclared imports (dependency hygiene)
|
|
673
|
+
MATCH (a:PyApplication)-[u:PY_UNRESOLVED_IMPORT]->(e:PyExternal)
|
|
674
|
+
WHERE NOT (e)<-[:PY_PROVIDES]-(:Package)
|
|
675
|
+
RETURN e.id, u.prov
|
|
676
|
+
|
|
677
|
+
// which lock file pins this package, and to what
|
|
678
|
+
MATCH (f:Artifact)-[l:LOCKS]->(p:Package {id: "pkg:pypi/numpy"})
|
|
679
|
+
RETURN f.id, l.version
|
|
680
|
+
```
|
|
681
|
+
|
|
579
682
|
## License
|
|
580
683
|
|
|
581
684
|
Apache 2.0 — see [LICENSE](./LICENSE).
|
|
@@ -193,6 +193,14 @@ def main(
|
|
|
193
193
|
"imports against the ambient Python environment instead.",
|
|
194
194
|
),
|
|
195
195
|
] = False,
|
|
196
|
+
resolve_installed: Annotated[
|
|
197
|
+
bool,
|
|
198
|
+
typer.Option(
|
|
199
|
+
"--resolve-installed",
|
|
200
|
+
help="Additionally bind imports via the project venv's installed metadata "
|
|
201
|
+
"(*.dist-info); output becomes machine-dependent (prov: installed-metadata).",
|
|
202
|
+
),
|
|
203
|
+
] = False,
|
|
196
204
|
file_name: Annotated[
|
|
197
205
|
Optional[Path],
|
|
198
206
|
typer.Option(
|
|
@@ -226,6 +234,24 @@ def main(
|
|
|
226
234
|
"the shipped rules. A malformed file is an error.",
|
|
227
235
|
),
|
|
228
236
|
] = None,
|
|
237
|
+
artifact_text: Annotated[
|
|
238
|
+
bool,
|
|
239
|
+
typer.Option(
|
|
240
|
+
"--artifact-text/--no-artifact-text",
|
|
241
|
+
help="Capture verbatim `source` text on discovered artifacts. "
|
|
242
|
+
"--no-artifact-text empties `source` everywhere (inventory unchanged).",
|
|
243
|
+
),
|
|
244
|
+
] = True,
|
|
245
|
+
artifact_text_max_bytes: Annotated[
|
|
246
|
+
int,
|
|
247
|
+
typer.Option(
|
|
248
|
+
"--artifact-text-max-bytes",
|
|
249
|
+
help="Per-file byte cap on captured artifact `source`; a decodable "
|
|
250
|
+
"file over the cap is truncated (text_truncated=True). "
|
|
251
|
+
"sha256/size_bytes always reflect the full file.",
|
|
252
|
+
min=1,
|
|
253
|
+
),
|
|
254
|
+
] = 262144,
|
|
229
255
|
):
|
|
230
256
|
# Determinism: pin the interpreter hash seed before any analysis (no-op
|
|
231
257
|
# when PYTHONHASHSEED is already set; --version exits before this).
|
|
@@ -303,11 +329,14 @@ def main(
|
|
|
303
329
|
rebuild_analysis=rebuild_analysis,
|
|
304
330
|
skip_tests=skip_tests,
|
|
305
331
|
no_venv=no_venv,
|
|
332
|
+
resolve_installed=resolve_installed,
|
|
306
333
|
file_name=file_name,
|
|
307
334
|
cache_dir=cache_dir,
|
|
308
335
|
clear_cache=clear_cache,
|
|
309
336
|
verbosity=verbosity,
|
|
310
337
|
entrypoint_rules=tuple(entrypoint_rules or ()),
|
|
338
|
+
artifact_text=artifact_text,
|
|
339
|
+
artifact_text_max_bytes=artifact_text_max_bytes,
|
|
311
340
|
)
|
|
312
341
|
|
|
313
342
|
_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
|
+
]
|