codeanalyzer-python 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.whl
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/__main__.py +24 -1
- codeanalyzer/artifacts/__init__.py +20 -0
- codeanalyzer/artifacts/config_keys.py +588 -0
- codeanalyzer/artifacts/config_use.py +597 -0
- codeanalyzer/artifacts/config_use_rules.yml +58 -0
- codeanalyzer/artifacts/dependencies.py +237 -0
- codeanalyzer/artifacts/discovery.py +141 -0
- codeanalyzer/artifacts/parsers.py +248 -0
- codeanalyzer/core.py +90 -0
- codeanalyzer/dataflow/builder.py +22 -3
- codeanalyzer/dataflow/sdg.py +17 -2
- codeanalyzer/dataflow/summaries.py +27 -4
- codeanalyzer/neo4j/bolt.py +35 -6
- codeanalyzer/neo4j/emit.py +3 -2
- codeanalyzer/neo4j/project.py +191 -0
- codeanalyzer/neo4j/schema.py +74 -0
- codeanalyzer/options/options.py +4 -0
- codeanalyzer/schema/ids.py +21 -0
- codeanalyzer/schema/py_schema.py +117 -0
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +11 -0
- {codeanalyzer_python-1.2.0.dist-info → codeanalyzer_python-1.4.0.dist-info}/METADATA +117 -7
- {codeanalyzer_python-1.2.0.dist-info → codeanalyzer_python-1.4.0.dist-info}/RECORD +26 -19
- {codeanalyzer_python-1.2.0.dist-info → codeanalyzer_python-1.4.0.dist-info}/WHEEL +0 -0
- {codeanalyzer_python-1.2.0.dist-info → codeanalyzer_python-1.4.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-1.2.0.dist-info → codeanalyzer_python-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-1.2.0.dist-info → codeanalyzer_python-1.4.0.dist-info}/licenses/NOTICE +0 -0
codeanalyzer/options/options.py
CHANGED
|
@@ -37,8 +37,12 @@ class AnalysisOptions:
|
|
|
37
37
|
rebuild_analysis: bool = False
|
|
38
38
|
skip_tests: bool = True
|
|
39
39
|
no_venv: bool = False
|
|
40
|
+
resolve_installed: bool = False
|
|
40
41
|
file_name: Optional[Path] = None
|
|
41
42
|
cache_dir: Optional[Path] = None
|
|
42
43
|
clear_cache: bool = False
|
|
43
44
|
verbosity: int = 0
|
|
44
45
|
entrypoint_rules: Tuple[Path, ...] = ()
|
|
46
|
+
# Artifact text capture (#157 follow-up): whether to capture `source` at
|
|
47
|
+
# all. There is no byte cap -- `source` is the whole file or "" (#172).
|
|
48
|
+
artifact_text: bool = True
|
codeanalyzer/schema/ids.py
CHANGED
|
@@ -21,3 +21,24 @@ def callable_sig_segment(name: str, param_names: List[str]) -> str:
|
|
|
21
21
|
|
|
22
22
|
def ordinal_id(callable_id: str, tag: str) -> str:
|
|
23
23
|
return f"{callable_id}@{tag}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def artifact_id(app_name: str, rel_path: str) -> str:
|
|
27
|
+
"""Language-neutral artifact id: ``can://artifact/<app>/<rel-path>``.
|
|
28
|
+
|
|
29
|
+
The first segment is a namespace (a language for code nodes, the literal
|
|
30
|
+
``artifact`` for files), so sibling analyzers over the same repo emit the
|
|
31
|
+
same id for the same file."""
|
|
32
|
+
return f"can://artifact/{app_name}/{rel_path}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def config_key_id(artifact_id: str, dotted_key: str) -> str:
|
|
36
|
+
"""A ``PyConfigKey`` extracted from an artifact: ``<artifact-id>@key/<dotted.key>``.
|
|
37
|
+
``dotted_key`` uses numeric segments for array indices (e.g.
|
|
38
|
+
``services.web.ports.0``); ids are opaque, do not re-split them."""
|
|
39
|
+
return f"{artifact_id}@key/{dotted_key}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def purl_pypi(name: str) -> str:
|
|
43
|
+
"""Package URL for a (PEP 503 normalized) PyPI distribution name."""
|
|
44
|
+
return f"pkg:pypi/{name}"
|
codeanalyzer/schema/py_schema.py
CHANGED
|
@@ -306,6 +306,17 @@ class PyCallArgument(BaseModel):
|
|
|
306
306
|
|
|
307
307
|
ast_kind: str
|
|
308
308
|
inferred_type: Optional[str] = None
|
|
309
|
+
# Literal capture (#162): populated only for an `ast.Constant` argument
|
|
310
|
+
# whose value is str/int/float/bool/None, JSON-encoded (`json.dumps`) --
|
|
311
|
+
# decode with `json.loads` to recover the Python constant. `None` for
|
|
312
|
+
# every non-constant argument (and for a Constant of another type, e.g.
|
|
313
|
+
# bytes/complex/Ellipsis).
|
|
314
|
+
value: Optional[str] = None
|
|
315
|
+
# Bare-identifier capture (#162): the `id` of an `ast.Name` argument
|
|
316
|
+
# (e.g. `f(KEY)` -> `"KEY"`) -- ships alongside `value` as the dataflow
|
|
317
|
+
# tier's join point back to the variable's own definitions. `None` for
|
|
318
|
+
# every other argument shape.
|
|
319
|
+
name: Optional[str] = None
|
|
309
320
|
|
|
310
321
|
|
|
311
322
|
# BodyNode.arguments forward-references PyCallArgument (defined later);
|
|
@@ -470,6 +481,103 @@ class PyExternalSymbol(BaseModel):
|
|
|
470
481
|
module: Optional[str] = None # best-effort owning module, e.g. "requests"
|
|
471
482
|
|
|
472
483
|
|
|
484
|
+
@builder
|
|
485
|
+
class PyConfigKey(BaseModel):
|
|
486
|
+
"""A configuration key flattened out of a config-bearing ``PyArtifact``
|
|
487
|
+
(#152). Graph vocabulary stays neutral (label ``ConfigKey``, edge
|
|
488
|
+
``DEFINES_CONFIG``) -- the ``Py`` prefix here is only the ``PyArtifact``
|
|
489
|
+
naming precedent, not a Python-specific claim. L1 data, identical at
|
|
490
|
+
every analysis level; nested under the owning artifact, containment
|
|
491
|
+
mirrors ``DEFINES_CONFIG``."""
|
|
492
|
+
|
|
493
|
+
id: str = "" # <artifact-id>@key/<dotted.key>
|
|
494
|
+
key: str # dotted path; numeric segments for arrays, e.g. "services.web.ports.0"
|
|
495
|
+
namespace: str # env|yaml|json|toml|ini|properties|dockerfile
|
|
496
|
+
value: Optional[str] = None # populated only when options.artifact_text is on
|
|
497
|
+
span: Optional[Span] = None # into the artifact's source; best-effort for yaml/json/toml
|
|
498
|
+
references: List[str] = [] # raw recognized tokens, order of appearance, deduplicated
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@builder
|
|
502
|
+
class PyArtifact(BaseModel):
|
|
503
|
+
"""Any non-`.py` project file (config, manifest, CI, container spec, or
|
|
504
|
+
plain data/binary) -- never dropped from the walk. Captured broadly (node
|
|
505
|
+
+ verbatim ``source``); *meaning* is extracted narrowly -- only
|
|
506
|
+
``dependency-manifest`` roles feed ``dependencies`` today. ``id`` is
|
|
507
|
+
language-neutral (``can://artifact/<app>/<path>``)."""
|
|
508
|
+
|
|
509
|
+
id: str = ""
|
|
510
|
+
kind: str = "artifact"
|
|
511
|
+
path: str # repo-relative POSIX path (also the map key)
|
|
512
|
+
format: str # toml|yaml|json|ini|properties|requirements|dockerfile|text|binary
|
|
513
|
+
roles: List[str] = []
|
|
514
|
+
size_bytes: int = 0
|
|
515
|
+
sha256: str = "" # always the full file's hash, even when source is empty
|
|
516
|
+
source: str = "" # the WHOLE file, or "" for binary / when capture is disabled -- never a prefix
|
|
517
|
+
extraction: str = "none" # none|partial|full
|
|
518
|
+
config_keys: List[PyConfigKey] = [] # flattened config keys (#152); [] when not namespace-eligible
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
@builder
|
|
522
|
+
class PyConfigUseEdge(BaseModel):
|
|
523
|
+
"""One resolved config read (#162): a detector-matched call's key
|
|
524
|
+
argument closed on exactly one string literal that matches a declared
|
|
525
|
+
``PyConfigKey``. ``src`` is the call's GLOBAL ordinal id
|
|
526
|
+
(``<callable-id>@<local-id>``); ``dst`` is the matched ``PyConfigKey.id``
|
|
527
|
+
-- application scope, mirroring ``param_in`` (endpoints span callables/
|
|
528
|
+
artifacts). Superset-monotonic across levels, same additive contract as
|
|
529
|
+
the DDG's ``prov`` widening: literal (``-a 2``+) subset of +dataflow
|
|
530
|
+
(``-a 3``/``-a 4``)."""
|
|
531
|
+
|
|
532
|
+
src: str
|
|
533
|
+
dst: str
|
|
534
|
+
prov: List[Literal["literal", "dataflow"]] = []
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@builder
|
|
538
|
+
class PyConfigRead(BaseModel):
|
|
539
|
+
"""A detector-matched call whose key did not close on exactly one string
|
|
540
|
+
literal -- first-class so a config read nobody can trace is as visible
|
|
541
|
+
as one that resolves (#162). ``key`` is the decoded literal text only
|
|
542
|
+
when it IS a literal but matches no declared ``PyConfigKey``
|
|
543
|
+
(``reason="undefined-key"``); ``None`` for a key that never closed on a
|
|
544
|
+
literal at all (``reason="non-literal"``). ``prov`` lists every tier
|
|
545
|
+
that was attempted before giving up."""
|
|
546
|
+
|
|
547
|
+
site: str # GLOBAL ordinal id
|
|
548
|
+
callee: str # external id (can://.../@external/<module>/<name>)
|
|
549
|
+
key: Optional[str] = None
|
|
550
|
+
reason: Literal["non-literal", "undefined-key"]
|
|
551
|
+
prov: List[Literal["literal", "dataflow"]] = []
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@builder
|
|
555
|
+
class PyDependency(BaseModel):
|
|
556
|
+
"""One declared third-party dependency, evidence-tagged via ``prov``."""
|
|
557
|
+
|
|
558
|
+
name: str # PEP 503 normalized
|
|
559
|
+
ecosystem: str = "pypi" # SDK symmetry with purl (#152 rider); the only ecosystem this analyzer emits
|
|
560
|
+
spec: str = ""
|
|
561
|
+
kind: str = "runtime" # runtime|dev|optional|build
|
|
562
|
+
extras: List[str] = []
|
|
563
|
+
declared_in: str = "" # PyArtifact id
|
|
564
|
+
# False for lockfile-only (transitive) dependencies -- pinned in a lock
|
|
565
|
+
# with no manifest declaration (#152 reconciliation).
|
|
566
|
+
direct: bool = True
|
|
567
|
+
locked_version: Optional[str] = None
|
|
568
|
+
provides_imports: List[str] = []
|
|
569
|
+
prov: List[str] = [] # declared|lockfile|installed-metadata|heuristic
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@builder
|
|
573
|
+
class PyImportBinding(BaseModel):
|
|
574
|
+
"""A top-level import no declared dependency accounts for."""
|
|
575
|
+
|
|
576
|
+
module: str
|
|
577
|
+
bound_to: Optional[str] = None # best-effort distribution name
|
|
578
|
+
prov: List[str] = []
|
|
579
|
+
|
|
580
|
+
|
|
473
581
|
@builder
|
|
474
582
|
class PyRepositoryInfo(BaseModel):
|
|
475
583
|
"""Where the analyzed source came from: git provenance captured at analysis time."""
|
|
@@ -502,6 +610,11 @@ class PyApplication(BaseModel):
|
|
|
502
610
|
# builtin members), keyed by signature. Populated by the analyzer so every
|
|
503
611
|
# backend (JSON and Neo4j) shares one authoritative external-symbol set.
|
|
504
612
|
external_symbols: Dict[str, PyExternalSymbol] = {}
|
|
613
|
+
# Non-code artifacts, declared dependencies, and undeclared imports
|
|
614
|
+
# (spec 2026-08-27). L1 data: identical at every analysis level.
|
|
615
|
+
artifacts: Dict[str, PyArtifact] = {}
|
|
616
|
+
dependencies: List[PyDependency] = []
|
|
617
|
+
unresolved_imports: List[PyImportBinding] = []
|
|
505
618
|
# Coverage/failure record for the entrypoint pass; see PyEntrypointReport (#27).
|
|
506
619
|
entrypoint_report: PyEntrypointReport = PyEntrypointReport()
|
|
507
620
|
# Git provenance of the analyzed checkout, captured at analysis time.
|
|
@@ -509,6 +622,10 @@ class PyApplication(BaseModel):
|
|
|
509
622
|
# Interprocedural parameter-passing edges (formal↔actual); populated at L4.
|
|
510
623
|
param_in: List[ParamEdge] = []
|
|
511
624
|
param_out: List[ParamEdge] = []
|
|
625
|
+
# config_use (#162): PY_USES_CONFIG edges + first-class unresolved reads.
|
|
626
|
+
# Literal tier from L2; dataflow tiers widen the set at L3/L4 (additive).
|
|
627
|
+
config_uses: List[PyConfigUseEdge] = []
|
|
628
|
+
config_reads_unresolved: List[PyConfigRead] = []
|
|
512
629
|
|
|
513
630
|
|
|
514
631
|
@builder
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import hashlib
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import tokenize
|
|
5
6
|
from ast import AST, ClassDef
|
|
@@ -786,6 +787,16 @@ class SymbolTableBuilder:
|
|
|
786
787
|
PyCallArgument(
|
|
787
788
|
ast_kind=type(arg).__name__,
|
|
788
789
|
inferred_type=self._infer_type(script, arg.lineno, arg.col_offset),
|
|
790
|
+
# Literal/name capture (#162): value JSON-encoded for a
|
|
791
|
+
# Constant of a JSON-safe type; name for a bare Name --
|
|
792
|
+
# never both, per the AST shapes involved.
|
|
793
|
+
value=(
|
|
794
|
+
json.dumps(arg.value)
|
|
795
|
+
if isinstance(arg, ast.Constant)
|
|
796
|
+
and isinstance(arg.value, (str, int, float, bool, type(None)))
|
|
797
|
+
else None
|
|
798
|
+
),
|
|
799
|
+
name=arg.id if isinstance(arg, ast.Name) else None,
|
|
789
800
|
)
|
|
790
801
|
for arg in node.args
|
|
791
802
|
]
|
|
@@ -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).
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
codeanalyzer/__init__.py,sha256=BZ3Kuwl-F_F-8H8cepLnVJ4Ku4NNUjjqg0Y6ujPQSsI,108
|
|
2
|
-
codeanalyzer/__main__.py,sha256=
|
|
3
|
-
codeanalyzer/core.py,sha256=
|
|
2
|
+
codeanalyzer/__main__.py,sha256=o5_9ct61l3T7ryIsag8bz304NabVV-el_ooGD8tnbhA,15968
|
|
3
|
+
codeanalyzer/core.py,sha256=tk_3dz81ECXXv8CEciWTIVfBAHHIfTVCsZte1GAZ080,46620
|
|
4
4
|
codeanalyzer/provenance.py,sha256=DT-DqwdVwO7g-GyK3sC0_4vDNCUjZYEY1fyYCt-7VRM,2306
|
|
5
5
|
codeanalyzer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
codeanalyzer/artifacts/__init__.py,sha256=317sEeZLS1AYsfYDYIk7tLR3AdDFwWw_cmkeZDabkJo,906
|
|
7
|
+
codeanalyzer/artifacts/config_keys.py,sha256=ovwptAErLYutlCzUwvyim2kH6-6vUKxuRcBV2KJS71Y,26843
|
|
8
|
+
codeanalyzer/artifacts/config_use.py,sha256=1-jGacW249dwF7b6DOrTFuJ7sA5Ho9khgrgZqND0nro,27173
|
|
9
|
+
codeanalyzer/artifacts/config_use_rules.yml,sha256=fNgI6IceOsj78LYmgoDH-vQ4-Mfb2ZFGKIQhLuoL4ik,1483
|
|
10
|
+
codeanalyzer/artifacts/dependencies.py,sha256=h_-XV5KhrT-1Ytguw0c4fyI9zSFzn9Aj7kR66omvL3M,10853
|
|
11
|
+
codeanalyzer/artifacts/discovery.py,sha256=cUVRSdV82G2hVGhkRGV7u24-tEAxUBmH9nkTg24ur0Y,5968
|
|
12
|
+
codeanalyzer/artifacts/parsers.py,sha256=xOjC53tT0Mv0k7ONGwzl5XUvpBLnZlEcGmyrX5DS45k,9035
|
|
6
13
|
codeanalyzer/dataflow/__init__.py,sha256=HCNOH24rAGLUMG94N9aC-2SsAWSntk3uys7e39ipU5o,1725
|
|
7
14
|
codeanalyzer/dataflow/access_paths.py,sha256=wC8Q9qD-RZzkoFWMVvu_6uNNmYP8z48OGp9h9v3F1d4,23623
|
|
8
15
|
codeanalyzer/dataflow/alias.py,sha256=ZYKY0DiR3GHv6YJr7C001Lode9rZsk6-gxdomUyIvwg,3928
|
|
9
|
-
codeanalyzer/dataflow/builder.py,sha256=
|
|
16
|
+
codeanalyzer/dataflow/builder.py,sha256=KiISa2bIuBf_0N0v_PzlgMGm0sFtMICEwA-Y6DFXiM4,32687
|
|
10
17
|
codeanalyzer/dataflow/cfg.py,sha256=u5YXJjAaDshdgv-9kWWITbBJFP-MwXKq6hbNh0fhDm8,25307
|
|
11
18
|
codeanalyzer/dataflow/defuse.py,sha256=LrX1ToOZzR79NlAGg5T647UAFkpiEqEnT7t0K5RXOiI,4377
|
|
12
19
|
codeanalyzer/dataflow/dominance.py,sha256=X7Ki1QRdVqaseYsJtiUL7m9MbcC2WHEv9b8bczSLFUA,5086
|
|
@@ -14,9 +21,9 @@ codeanalyzer/dataflow/identity.py,sha256=WAIal6XchmQqdnXbvbEgu8J6vJdXNRdie1KHz1v
|
|
|
14
21
|
codeanalyzer/dataflow/pdg.py,sha256=1Bm7AnoRqXBryZulII2idPw0feV8eZQ1VqqprQ8PW-g,3731
|
|
15
22
|
codeanalyzer/dataflow/scalpel_oracle.py,sha256=FxRadKrTWar0VKHyQJM40n3cq25sld3Sj4lMdOm5NFk,11494
|
|
16
23
|
codeanalyzer/dataflow/scc.py,sha256=Doa_0-5f3agCu_5UmeEDlCUErg34TtniKIv8Uqd7Jiw,3493
|
|
17
|
-
codeanalyzer/dataflow/sdg.py,sha256=
|
|
24
|
+
codeanalyzer/dataflow/sdg.py,sha256=taDXkIg0BZUtDEJUjRJB_dxrFl6tKNOUdFyj0zCiso0,18533
|
|
18
25
|
codeanalyzer/dataflow/slicing.py,sha256=lWZ7jHhlR8m7rECWJQXvwfs5GNZeJY55Cud3Ji5UbNU,3654
|
|
19
|
-
codeanalyzer/dataflow/summaries.py,sha256=
|
|
26
|
+
codeanalyzer/dataflow/summaries.py,sha256=TLtc5h4bLBC4lViuMhMlEp5rp_nBpDPOl4_wfBuRUY4,9210
|
|
20
27
|
codeanalyzer/dataflow/syntactic.py,sha256=AbHyXjKX_1xkGgKH48BpXYCWBauActGUwSMF_OD-uys,1124
|
|
21
28
|
codeanalyzer/dataflow/scalpel/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
22
29
|
codeanalyzer/dataflow/scalpel/README.md,sha256=YN-LxqYYDekIdhx39AG28sg9UySMmqvLQJivTvR2Fz8,1543
|
|
@@ -38,34 +45,34 @@ codeanalyzer/entrypoints/rules.yml,sha256=rgDglVOcUNXnQ5FXLMfnJbfI7xHRiRmegTP5bm
|
|
|
38
45
|
codeanalyzer/jedi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
46
|
codeanalyzer/jedi/jedi.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
47
|
codeanalyzer/neo4j/__init__.py,sha256=8Ei6uThTOqmul6GhnKoGOjLXJZbyDwF5Jo-8I8NxnAk,1574
|
|
41
|
-
codeanalyzer/neo4j/bolt.py,sha256=
|
|
48
|
+
codeanalyzer/neo4j/bolt.py,sha256=wobEBSQn5z9uCER3fNtTl98Q1z5WuXG-O1rPRoFwFWI,11949
|
|
42
49
|
codeanalyzer/neo4j/cypher.py,sha256=a7YSmxxDbPcYc5gI4TW9sM6_WOE3RgLXTGBR9K8W8kw,5321
|
|
43
|
-
codeanalyzer/neo4j/emit.py,sha256=
|
|
44
|
-
codeanalyzer/neo4j/project.py,sha256=
|
|
50
|
+
codeanalyzer/neo4j/emit.py,sha256=OBCO77TDTaNpqk9Di3JIQzBouLpHPdgoXuPSs44iJuU,3587
|
|
51
|
+
codeanalyzer/neo4j/project.py,sha256=U-2ZurR3aYFY0k8WGpEIYTE4YEET2pJeOyxqb9i_Xyo,35044
|
|
45
52
|
codeanalyzer/neo4j/rows.py,sha256=med0XFa63PPUVUhJMq1TtVcJxgaVW39sgxBRZJDIJ_w,7811
|
|
46
|
-
codeanalyzer/neo4j/schema.py,sha256=
|
|
53
|
+
codeanalyzer/neo4j/schema.py,sha256=88F_biHRhd4NV1RSNvFCwLa062gKJb12_P_-vV6L-rE,15379
|
|
47
54
|
codeanalyzer/options/__init__.py,sha256=6NewN0a-1HAEgKcxQjYyVn2WEDLrhax8h3WLgZddeqI,94
|
|
48
|
-
codeanalyzer/options/options.py,sha256=
|
|
55
|
+
codeanalyzer/options/options.py,sha256=2jDCcs74iSEOhz90LoRGts6PC8yV9IDPVxnLPNmonOA,1609
|
|
49
56
|
codeanalyzer/schema/__init__.py,sha256=hIyz02abUJNPW8yUgguaeKflZjTmOZWbDiKICNBD7yk,4141
|
|
50
57
|
codeanalyzer/schema/assign_ids.py,sha256=pQHluLaO5a6hFREp3vjVRsUeAfTYSJ95RXS9TZvnoys,1590
|
|
51
58
|
codeanalyzer/schema/call_graph_ids.py,sha256=mWAhJDBsW8i-siJiINOHCXEm4qM6F_5se9-HuHWIFqU,568
|
|
52
|
-
codeanalyzer/schema/ids.py,sha256=
|
|
59
|
+
codeanalyzer/schema/ids.py,sha256=aOzsgVaOo6x72dnRgNxlnt6b5wbZzhTm0JOz6pdI57I,1672
|
|
53
60
|
codeanalyzer/schema/l1_body.py,sha256=5Su347kwAPNflJDf7SvBR3sNXx9PVdGEml2pOpQCwo0,1684
|
|
54
61
|
codeanalyzer/schema/l2_callees.py,sha256=CTcBeGoO04DstDC6h-1sHMbbIrHDEOMxP01U9VLBcfc,2327
|
|
55
|
-
codeanalyzer/schema/py_schema.py,sha256=
|
|
62
|
+
codeanalyzer/schema/py_schema.py,sha256=5G8K6uqBopU5iLwlUW30AE5A8jgTEdNaQmOeizhccag,23885
|
|
56
63
|
codeanalyzer/semantic_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
64
|
codeanalyzer/semantic_analysis/call_graph.py,sha256=6YEB_wTn5-oQYLrbIhJYE0BsHl4fpWPoy5Hwd9mTnGc,11918
|
|
58
65
|
codeanalyzer/semantic_analysis/defuse_linker.py,sha256=bSn1rCun28OQqSd2NOc9aVBSm47Gt9Iul4r_gwfqO1w,64930
|
|
59
66
|
codeanalyzer/syntactic_analysis/__init__.py,sha256=EUQkJEh6wHjWx2qTTKbTbUgwSbfKeNieKHNy7RknVXA,476
|
|
60
67
|
codeanalyzer/syntactic_analysis/exceptions.py,sha256=whs_n0vIu655Jkk1a7iOoXY6iIca4pZqJnU40V9Ejaw,537
|
|
61
68
|
codeanalyzer/syntactic_analysis/import_resolver.py,sha256=Q8noZwSdDNt4P4NMqDTB9FaS-M0_d4ASK9GnJop9MDI,2751
|
|
62
|
-
codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=
|
|
69
|
+
codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=eTknBDlUuuQd3JEwbRtJt5pGVU89kOnzfmT4D5J_GKQ,47917
|
|
63
70
|
codeanalyzer/utils/__init__.py,sha256=hC6VWdR5rerSqBxzu9KQHTASWqwrrYJv-CMDwrTlzkc,137
|
|
64
71
|
codeanalyzer/utils/logging.py,sha256=Fw0tattPAOMs3o0JMjjXhRVLIF64f-SCcygUXF9jqeg,904
|
|
65
72
|
codeanalyzer/utils/progress_bar.py,sha256=C9JtzVdd10lIxTv-KA6PebqjKWueC_vMGwVzAtHuHIw,2818
|
|
66
|
-
codeanalyzer_python-1.
|
|
67
|
-
codeanalyzer_python-1.
|
|
68
|
-
codeanalyzer_python-1.
|
|
69
|
-
codeanalyzer_python-1.
|
|
70
|
-
codeanalyzer_python-1.
|
|
71
|
-
codeanalyzer_python-1.
|
|
73
|
+
codeanalyzer_python-1.4.0.dist-info/METADATA,sha256=DJKj-64VCcSuUoxUbwsesDi7jSkBv69YSrsePcgAzGM,42786
|
|
74
|
+
codeanalyzer_python-1.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
75
|
+
codeanalyzer_python-1.4.0.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
|
|
76
|
+
codeanalyzer_python-1.4.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
77
|
+
codeanalyzer_python-1.4.0.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
|
|
78
|
+
codeanalyzer_python-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
{codeanalyzer_python-1.2.0.dist-info → codeanalyzer_python-1.4.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{codeanalyzer_python-1.2.0.dist-info → codeanalyzer_python-1.4.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|