codeanalyzer-python 1.5.3__tar.gz → 1.6.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.5.3 → codeanalyzer_python-1.6.0}/PKG-INFO +11 -19
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/README.md +1 -1
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/__main__.py +3 -4
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/config_keys.py +1 -6
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/parsers.py +1 -6
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/core.py +119 -3
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/access_paths.py +1 -1
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/builder.py +52 -5
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/cfg.py +2 -2
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/defuse.py +1 -1
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/pdg.py +1 -1
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/sdg.py +28 -10
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/summaries.py +10 -4
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/matching.py +5 -1
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/cypher.py +83 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/emit.py +5 -3
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/project.py +5 -2
- codeanalyzer_python-1.6.0/codeanalyzer/schema/__init__.py +100 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/py_schema.py +60 -46
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/pyproject.toml +17 -37
- codeanalyzer_python-1.5.3/codeanalyzer/schema/__init__.py +0 -141
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/.gitignore +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/LICENSE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/NOTICE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/config_use.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/config_use_rules.yml +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/dependencies.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/discovery.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/alias.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/dominance.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/identity.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/LICENSE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/SSA/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/SSA/const.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/cfg/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/cfg/builder.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/cfg/model.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/core/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/core/func_call_visitor.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/core/vars_visitor.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scc.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/slicing.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/syntactic.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/detect.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/pipeline.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/rules.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/rules.yml +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/jedi/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/jedi/jedi.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/bolt.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/rows.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/schema.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/options/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/options/options.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/provenance.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/py.typed +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/assign_ids.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/call_graph_ids.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/ids.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/l1_body.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/l2_callees.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/semantic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/semantic_analysis/defuse_linker.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/utils/__init__.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/utils/logging.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/utils/progress_bar.py +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/docs/handoff/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/docs/handoff/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/schema-uml.drawio +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/flask/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/LICENSE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/NOTICE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
- {codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/docs/README.md +0 -0
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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
|
|
7
7
|
License-File: NOTICE
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
9
|
Requires-Dist: astor<0.9.0,>=0.8.1
|
|
10
|
-
Requires-Dist: jedi
|
|
11
|
-
Requires-Dist:
|
|
12
|
-
Requires-Dist: networkx<3.2.0,>=2.6.0; python_version < '3.11'
|
|
13
|
-
Requires-Dist: networkx<4.0.0,>=3.0.0; python_version >= '3.11'
|
|
10
|
+
Requires-Dist: jedi<=0.19.2
|
|
11
|
+
Requires-Dist: networkx<4.0.0,>=3.0.0
|
|
14
12
|
Requires-Dist: packaging>=25.0
|
|
15
13
|
Requires-Dist: parso>=0.8.5
|
|
16
|
-
Requires-Dist: pydantic<
|
|
17
|
-
Requires-Dist: pydantic<3.0.0,>=2.0.0; python_version >= '3.11'
|
|
14
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
18
15
|
Requires-Dist: pyyaml<7.0,>=6.0
|
|
19
|
-
Requires-Dist: ray<3.0.0,>=2.10.0
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist:
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
25
|
-
Requires-Dist: typer<1.0.0,>=0.9.0; python_version < '3.11'
|
|
26
|
-
Requires-Dist: typer<2.0.0,>=0.9.0; python_version >= '3.11'
|
|
27
|
-
Requires-Dist: typing-extensions<5.0.0,>=4.0.0; python_version < '3.11'
|
|
28
|
-
Requires-Dist: typing-extensions<6.0.0,>=4.5.0; python_version >= '3.11'
|
|
16
|
+
Requires-Dist: ray<3.0.0,>=2.10.0
|
|
17
|
+
Requires-Dist: requests<3.0.0,>=2.20.0
|
|
18
|
+
Requires-Dist: rich<15.0.0,>=14.0.0
|
|
19
|
+
Requires-Dist: typer<2.0.0,>=0.9.0
|
|
20
|
+
Requires-Dist: typing-extensions<6.0.0,>=4.5.0
|
|
29
21
|
Requires-Dist: uv>=0.5.0
|
|
30
22
|
Provides-Extra: neo4j
|
|
31
23
|
Requires-Dist: neo4j<6.0.0,>=5.0.0; extra == 'neo4j'
|
|
@@ -112,7 +104,7 @@ needs.
|
|
|
112
104
|
|
|
113
105
|
### Prerequisites
|
|
114
106
|
|
|
115
|
-
- **Python 3.
|
|
107
|
+
- **Python 3.11 or newer.**
|
|
116
108
|
- A C toolchain and the `venv` / development headers — the analyzer builds an isolated virtual
|
|
117
109
|
environment per project (via Python's `venv`) so Jedi can resolve types and imports:
|
|
118
110
|
|
|
@@ -79,7 +79,7 @@ needs.
|
|
|
79
79
|
|
|
80
80
|
### Prerequisites
|
|
81
81
|
|
|
82
|
-
- **Python 3.
|
|
82
|
+
- **Python 3.11 or newer.**
|
|
83
83
|
- A C toolchain and the `venv` / development headers — the analyzer builds an isolated virtual
|
|
84
84
|
environment per project (via Python's `venv`) so Jedi can resolve types and imports:
|
|
85
85
|
|
|
@@ -406,11 +406,10 @@ def _write_output(artifacts, output_dir: Path):
|
|
|
406
406
|
# Use Pydantic's model_dump_json() for compact output
|
|
407
407
|
# Strip internal-only fields here rather than with a field-level Pydantic
|
|
408
408
|
# `exclude`: the analysis cache shares the serializer and must keep them.
|
|
409
|
-
|
|
410
|
-
strip_internal_only(model_dump(artifacts, mode="json", exclude_none=True))
|
|
411
|
-
)
|
|
409
|
+
from codeanalyzer.core import stream_analysis_json, _log_mem # LOW-MEM PATCH
|
|
412
410
|
with output_file.open("w") as f:
|
|
413
|
-
f
|
|
411
|
+
stream_analysis_json(artifacts, f, strip_internal=True)
|
|
412
|
+
_log_mem("after analysis.json write")
|
|
414
413
|
logger.info(f"Analysis saved to {output_file}")
|
|
415
414
|
|
|
416
415
|
|
{codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/config_keys.py
RENAMED
|
@@ -58,14 +58,9 @@ from __future__ import annotations
|
|
|
58
58
|
import configparser
|
|
59
59
|
import json
|
|
60
60
|
import re
|
|
61
|
-
import
|
|
61
|
+
import tomllib
|
|
62
62
|
from typing import Callable, Dict, List, Optional, Tuple
|
|
63
63
|
|
|
64
|
-
if sys.version_info >= (3, 11):
|
|
65
|
-
import tomllib
|
|
66
|
-
else: # pragma: no cover - exercised on the 3.10 CI leg
|
|
67
|
-
import tomli as tomllib
|
|
68
|
-
|
|
69
64
|
import yaml
|
|
70
65
|
|
|
71
66
|
from codeanalyzer.schema.ids import config_key_id
|
|
@@ -4,15 +4,10 @@ import ast
|
|
|
4
4
|
import configparser
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
-
import
|
|
7
|
+
import tomllib
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from typing import Dict, List, Optional, Tuple
|
|
10
10
|
|
|
11
|
-
if sys.version_info >= (3, 11):
|
|
12
|
-
import tomllib
|
|
13
|
-
else: # pragma: no cover - exercised on the 3.10 CI leg
|
|
14
|
-
import tomli as tomllib
|
|
15
|
-
|
|
16
11
|
import yaml
|
|
17
12
|
|
|
18
13
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import gc
|
|
1
2
|
import hashlib
|
|
3
|
+
import json
|
|
2
4
|
import os
|
|
5
|
+
import resource
|
|
3
6
|
import shutil
|
|
4
7
|
import subprocess
|
|
5
8
|
import sys
|
|
@@ -53,6 +56,90 @@ def _artifact_full_text(project_dir: Path, path: str, art) -> str:
|
|
|
53
56
|
return art.source
|
|
54
57
|
|
|
55
58
|
|
|
59
|
+
def _log_mem(label: str) -> None:
|
|
60
|
+
"""LOW-MEM PATCH: log current/peak RSS of the driver at phase boundaries."""
|
|
61
|
+
try:
|
|
62
|
+
cur = int(os.popen(f"ps -o rss= -p {os.getpid()}").read().strip() or 0) / 1024
|
|
63
|
+
except Exception:
|
|
64
|
+
cur = -1
|
|
65
|
+
peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
66
|
+
peak = peak / (1024 * 1024) if sys.platform == "darwin" else peak / 1024 # bytes on macOS, KB on Linux
|
|
67
|
+
logger.info("[mem] %-40s rss=%6.0f MB peak=%6.0f MB", label, cur, peak)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def stream_analysis_json(analysis, fh, *, strip_internal: bool) -> None:
|
|
71
|
+
"""LOW-MEM PATCH: serialize an ``Analysis`` envelope to ``fh`` one module at a
|
|
72
|
+
time instead of materializing the whole tree as one JSON string (cache) or
|
|
73
|
+
as a dict tree + stripped copy + string (output). The JSON *content* equals
|
|
74
|
+
``model_dump_json(exclude_none=True)`` / ``json.dumps(strip_internal_only(
|
|
75
|
+
model_dump(mode="json", exclude_none=True)))``; only whitespace differs
|
|
76
|
+
(compact separators throughout). ``call_sites`` live solely under the
|
|
77
|
+
symbol table's callables, so the envelope needs no stripping."""
|
|
78
|
+
from codeanalyzer.schema import strip_internal_only
|
|
79
|
+
|
|
80
|
+
from typing import List as _List
|
|
81
|
+
from pydantic import TypeAdapter
|
|
82
|
+
from codeanalyzer.schema.py_schema import ParamEdge, PyCallEdge
|
|
83
|
+
|
|
84
|
+
app = analysis.application
|
|
85
|
+
# Detach the big collections, dump the (now small) envelope, then splice
|
|
86
|
+
# each collection back in at its own slot, streaming it in chunks.
|
|
87
|
+
big = {
|
|
88
|
+
"symbol_table": (app.symbol_table, "{}"),
|
|
89
|
+
"call_graph": (app.call_graph, "[]"),
|
|
90
|
+
"param_in": (app.param_in, "[]"),
|
|
91
|
+
"param_out": (app.param_out, "[]"),
|
|
92
|
+
}
|
|
93
|
+
try:
|
|
94
|
+
app.symbol_table, app.call_graph, app.param_in, app.param_out = {}, [], [], []
|
|
95
|
+
envelope = analysis.model_dump_json(exclude_none=True)
|
|
96
|
+
finally:
|
|
97
|
+
app.symbol_table = big["symbol_table"][0]
|
|
98
|
+
app.call_graph = big["call_graph"][0]
|
|
99
|
+
app.param_in = big["param_in"][0]
|
|
100
|
+
app.param_out = big["param_out"][0]
|
|
101
|
+
slots = []
|
|
102
|
+
for name, (_value, empty) in big.items():
|
|
103
|
+
marker = f'"{name}":{empty}'
|
|
104
|
+
idx = envelope.find(marker)
|
|
105
|
+
if idx < 0 or envelope.find(marker, idx + 1) >= 0:
|
|
106
|
+
raise RuntimeError(f"stream_analysis_json: could not locate a unique {name} slot")
|
|
107
|
+
slots.append((idx + len(marker) - 1, name)) # position of the closing bracket
|
|
108
|
+
slots.sort()
|
|
109
|
+
adapters = {
|
|
110
|
+
"call_graph": TypeAdapter(_List[PyCallEdge]),
|
|
111
|
+
"param_in": TypeAdapter(_List[ParamEdge]),
|
|
112
|
+
"param_out": TypeAdapter(_List[ParamEdge]),
|
|
113
|
+
}
|
|
114
|
+
CHUNK = 20000
|
|
115
|
+
pos = 0
|
|
116
|
+
for cut, name in slots:
|
|
117
|
+
fh.write(envelope[pos:cut])
|
|
118
|
+
pos = cut
|
|
119
|
+
value = big[name][0]
|
|
120
|
+
first = True
|
|
121
|
+
if name == "symbol_table":
|
|
122
|
+
for key, module in value.items():
|
|
123
|
+
if strip_internal:
|
|
124
|
+
body = json.dumps(
|
|
125
|
+
strip_internal_only(module.model_dump(mode="json", exclude_none=True)),
|
|
126
|
+
separators=(",", ":"),
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
body = module.model_dump_json(exclude_none=True)
|
|
130
|
+
fh.write(("" if first else ",") + json.dumps(key) + ":" + body)
|
|
131
|
+
first = False
|
|
132
|
+
else:
|
|
133
|
+
adapter = adapters[name]
|
|
134
|
+
for i in range(0, len(value), CHUNK):
|
|
135
|
+
chunk = adapter.dump_json(value[i:i + CHUNK], exclude_none=True).decode()
|
|
136
|
+
inner = chunk[1:-1] # strip the chunk's own [ ]
|
|
137
|
+
if inner:
|
|
138
|
+
fh.write(("" if first else ",") + inner)
|
|
139
|
+
first = False
|
|
140
|
+
fh.write(envelope[pos:])
|
|
141
|
+
|
|
142
|
+
|
|
56
143
|
def _ensure_ray() -> None:
|
|
57
144
|
"""Initialize Ray with the driver's pinned hash seed in the workers.
|
|
58
145
|
|
|
@@ -60,13 +147,23 @@ def _ensure_ray() -> None:
|
|
|
60
147
|
interpreters, so Jedi inference in Ray workers would run with random
|
|
61
148
|
set-iteration order and the emitted edges vary run to run (issue #99)."""
|
|
62
149
|
if not ray.is_initialized():
|
|
63
|
-
|
|
150
|
+
# LOW-MEM PATCH: start a private local cluster with object spilling to
|
|
151
|
+
# disk disabled, a bounded object store, and an optional worker cap
|
|
152
|
+
# (CANPY_RAY_CPUS); shut down after the symbol-table phase so the idle
|
|
153
|
+
# workers release their memory before the single-process L2-L4 phases.
|
|
154
|
+
init_kwargs = dict(
|
|
64
155
|
runtime_env={
|
|
65
156
|
"env_vars": {
|
|
66
157
|
"PYTHONHASHSEED": os.environ.get("PYTHONHASHSEED", "0")
|
|
67
158
|
}
|
|
68
159
|
},
|
|
160
|
+
_system_config={"automatic_object_spilling_enabled": False},
|
|
161
|
+
object_store_memory=int(os.environ.get("CANPY_RAY_OBJECT_STORE", 1_000_000_000)),
|
|
162
|
+
include_dashboard=False,
|
|
69
163
|
)
|
|
164
|
+
if os.environ.get("CANPY_RAY_CPUS"):
|
|
165
|
+
init_kwargs["num_cpus"] = int(os.environ["CANPY_RAY_CPUS"])
|
|
166
|
+
ray.init(**init_kwargs)
|
|
70
167
|
|
|
71
168
|
|
|
72
169
|
@ray.remote
|
|
@@ -747,7 +844,16 @@ class Codeanalyzer:
|
|
|
747
844
|
k=self.options.graph_field_depth,
|
|
748
845
|
oracle_factory=lambda c, fast: SyntacticOracle(),
|
|
749
846
|
)
|
|
847
|
+
del _func_asts # LOW-MEM PATCH: unused at L3; drop the def-node ASTs now
|
|
750
848
|
emit_l3_body(app, infos, sig_to_id, set(self.options.graphs.split(",")))
|
|
849
|
+
# LOW-MEM PATCH: L4 only needs the syntactic DDG *sets* (for the
|
|
850
|
+
# points-to delta), not the PDGs with their statement ASTs.
|
|
851
|
+
from codeanalyzer.dataflow.builder import syntactic_ddg_sets
|
|
852
|
+
syn_ddg_sets = syntactic_ddg_sets(app, infos, sig_to_id)
|
|
853
|
+
infos.clear()
|
|
854
|
+
del infos
|
|
855
|
+
gc.collect()
|
|
856
|
+
_log_mem("after L3 emit (L3 PDGs freed)")
|
|
751
857
|
|
|
752
858
|
# L4: interprocedural dataflow (param vertices + summary + param_in/out)
|
|
753
859
|
# layered on top of the L3 syntactic overlay (L3 ⊆ L4). Scalpel is the
|
|
@@ -772,7 +878,14 @@ class Codeanalyzer:
|
|
|
772
878
|
# Semantic ddg delta: the alias-derived def-use edges the real
|
|
773
879
|
# oracle adds beyond the L3 syntactic set, tagged prov=["points-to"].
|
|
774
880
|
# ``infos`` are the syntactic (L3) PDGs from the >=3 block above.
|
|
775
|
-
emit_ddg_pointsto_delta(app,
|
|
881
|
+
emit_ddg_pointsto_delta(app, syn_ddg_sets, ir, sig_to_id)
|
|
882
|
+
# LOW-MEM PATCH: the L3/L4 intermediates (per-function PDGs with their
|
|
883
|
+
# statement ASTs, SDG IR) are dead from here on; free them before the
|
|
884
|
+
# config tiers and serialization instead of at analyze() return.
|
|
885
|
+
del ir
|
|
886
|
+
del syn_ddg_sets
|
|
887
|
+
gc.collect()
|
|
888
|
+
_log_mem("after L4 emit (intermediates freed)")
|
|
776
889
|
|
|
777
890
|
# config_use (#162) resolution: literal tier always runs at >= 2;
|
|
778
891
|
# the dataflow tiers widen the set as substrate comes online -- intra
|
|
@@ -872,7 +985,8 @@ class Codeanalyzer:
|
|
|
872
985
|
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
873
986
|
|
|
874
987
|
with cache_file.open('w') as f:
|
|
875
|
-
|
|
988
|
+
stream_analysis_json(analysis, f, strip_internal=False) # LOW-MEM PATCH
|
|
989
|
+
_log_mem("after cache save")
|
|
876
990
|
|
|
877
991
|
logger.info(f"Analysis cached to {cache_file}")
|
|
878
992
|
|
|
@@ -1013,6 +1127,8 @@ class Codeanalyzer:
|
|
|
1013
1127
|
if result:
|
|
1014
1128
|
symbol_table.update(result)
|
|
1015
1129
|
progress.advance()
|
|
1130
|
+
ray.shutdown() # LOW-MEM PATCH: release the worker pool's memory
|
|
1131
|
+
logger.info("Ray shut down after the symbol-table phase.")
|
|
1016
1132
|
else:
|
|
1017
1133
|
logger.info("Building symbol table serially.")
|
|
1018
1134
|
symbol_table_builder = SymbolTableBuilder(self.project_dir, self.virtualenv)
|
{codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/access_paths.py
RENAMED
|
@@ -126,7 +126,7 @@ class FunctionScope:
|
|
|
126
126
|
return "global" # unknown free name: a module/builtin binding
|
|
127
127
|
|
|
128
128
|
|
|
129
|
-
@dataclass
|
|
129
|
+
@dataclass(slots=True) # LOW-MEM PATCH: slots
|
|
130
130
|
class StatementFacts:
|
|
131
131
|
"""Defs and uses (k-limited access-path strings) of one CFG node."""
|
|
132
132
|
|
|
@@ -50,6 +50,14 @@ from codeanalyzer.utils import logger
|
|
|
50
50
|
|
|
51
51
|
DEFAULT_K_LIMIT = 3
|
|
52
52
|
|
|
53
|
+
# LOW-MEM PATCH: one shared provenance list per kind instead of a fresh
|
|
54
|
+
# ``["ssa"]`` list object on each of the (tens of) millions of DDG edges. The
|
|
55
|
+
# lists are never mutated; equality comparisons (``e.prov != [...]``) and JSON
|
|
56
|
+
# serialization are unaffected.
|
|
57
|
+
_PROV_SSA = ["ssa"]
|
|
58
|
+
_PROV_REACHING_DEFS = ["reaching-defs"]
|
|
59
|
+
_PROV_POINTS_TO = ["points-to"]
|
|
60
|
+
|
|
53
61
|
|
|
54
62
|
def _walk_callables(
|
|
55
63
|
module: PyModule,
|
|
@@ -339,7 +347,7 @@ def emit_l3_body(
|
|
|
339
347
|
src=im.local(e.source),
|
|
340
348
|
dst=im.local(e.target),
|
|
341
349
|
var=e.var,
|
|
342
|
-
prov=
|
|
350
|
+
prov=_PROV_SSA,
|
|
343
351
|
)
|
|
344
352
|
for e in pdg.edges
|
|
345
353
|
if e.type == "DDG"
|
|
@@ -442,6 +450,16 @@ def build_program_graphs(
|
|
|
442
450
|
if nested_sig in infos:
|
|
443
451
|
info.nested_defs.append((node.id, nested_sig))
|
|
444
452
|
|
|
453
|
+
# LOW-MEM PATCH: from here on (summary fixpoint, SDG assembly, L4 emit) no
|
|
454
|
+
# stage reads a statement's AST -- only CFG node ids/positions, facts and
|
|
455
|
+
# oracles. Drop the def-node map and the per-node AST references so the
|
|
456
|
+
# whole-program AST copy this pass parsed is collected before the two most
|
|
457
|
+
# memory-hungry stages run.
|
|
458
|
+
func_asts.clear()
|
|
459
|
+
for info in infos.values():
|
|
460
|
+
for node in info.pdg.cfg.nodes:
|
|
461
|
+
node.ast_node = None
|
|
462
|
+
|
|
445
463
|
call_edges = [
|
|
446
464
|
(e.src, e.dst)
|
|
447
465
|
for e in app.call_graph
|
|
@@ -609,7 +627,7 @@ def emit_l4(
|
|
|
609
627
|
continue
|
|
610
628
|
seen.add(key)
|
|
611
629
|
rows.append(
|
|
612
|
-
DdgEdge(src=src, dst=dst, var=e.var, prov=
|
|
630
|
+
DdgEdge(src=src, dst=dst, var=e.var, prov=_PROV_REACHING_DEFS)
|
|
613
631
|
)
|
|
614
632
|
rows.sort(key=lambda r: (r.src, r.dst, r.var or ""))
|
|
615
633
|
pycallable.ddg.extend(rows)
|
|
@@ -629,9 +647,36 @@ def _ddg_local_set(im, pdg) -> Set[Tuple[str, str, Optional[str]]]:
|
|
|
629
647
|
}
|
|
630
648
|
|
|
631
649
|
|
|
650
|
+
def syntactic_ddg_sets(
|
|
651
|
+
app: PyApplication,
|
|
652
|
+
infos: Dict[str, FunctionInfo],
|
|
653
|
+
sig_to_id: Dict[str, str],
|
|
654
|
+
) -> Dict[str, Set[Tuple[str, str, Optional[str]]]]:
|
|
655
|
+
"""LOW-MEM PATCH: precompute, per signature, the L3 syntactic DDG set in the
|
|
656
|
+
exact form :func:`emit_ddg_pointsto_delta` derives from ``syntactic_infos``
|
|
657
|
+
(local ids are position-based, so an IdentityMap over the syntactic PDG
|
|
658
|
+
yields the same locals as one over the L4 PDG). Lets the caller free the L3
|
|
659
|
+
``FunctionInfo`` records -- and the statement ASTs they pin -- before L4."""
|
|
660
|
+
from codeanalyzer.dataflow.identity import IdentityMap
|
|
661
|
+
|
|
662
|
+
sig_to_callable: Dict[str, PyCallable] = {}
|
|
663
|
+
for module in app.symbol_table.values():
|
|
664
|
+
for pycallable, _chain in _walk_callables(module):
|
|
665
|
+
sig_to_callable[pycallable.signature] = pycallable
|
|
666
|
+
out: Dict[str, Set[Tuple[str, str, Optional[str]]]] = {}
|
|
667
|
+
for sig, info in infos.items():
|
|
668
|
+
pycallable = sig_to_callable.get(sig)
|
|
669
|
+
if pycallable is None:
|
|
670
|
+
continue
|
|
671
|
+
callable_id = sig_to_id.get(sig) or pycallable.id
|
|
672
|
+
im = IdentityMap.for_function(callable_id, info.pdg)
|
|
673
|
+
out[sig] = _ddg_local_set(im, info.pdg)
|
|
674
|
+
return out
|
|
675
|
+
|
|
676
|
+
|
|
632
677
|
def emit_ddg_pointsto_delta(
|
|
633
678
|
app: PyApplication,
|
|
634
|
-
syntactic_infos
|
|
679
|
+
syntactic_infos,
|
|
635
680
|
ir: ProgramGraphsIR,
|
|
636
681
|
sig_to_id: Dict[str, str],
|
|
637
682
|
) -> None:
|
|
@@ -678,12 +723,14 @@ def emit_ddg_pointsto_delta(
|
|
|
678
723
|
# untouched — they are L3's and this delta is strictly additive over them.
|
|
679
724
|
pycallable.ddg = [e for e in pycallable.ddg if e.prov != ["points-to"]]
|
|
680
725
|
|
|
681
|
-
|
|
726
|
+
# LOW-MEM PATCH: accept a precomputed syntactic set (see syntactic_ddg_sets).
|
|
727
|
+
syn_set = syn if isinstance(syn, (set, frozenset)) else _ddg_local_set(im, syn.pdg)
|
|
728
|
+
delta = _ddg_local_set(im, fg.pdg) - syn_set
|
|
682
729
|
for src, dst, var in sorted(delta, key=lambda t: (t[0], t[1], t[2] or "")):
|
|
683
730
|
if src not in pycallable.body or dst not in pycallable.body:
|
|
684
731
|
continue
|
|
685
732
|
pycallable.ddg.append(
|
|
686
|
-
DdgEdge(src=src, dst=dst, var=var, prov=
|
|
733
|
+
DdgEdge(src=src, dst=dst, var=var, prov=_PROV_POINTS_TO)
|
|
687
734
|
)
|
|
688
735
|
|
|
689
736
|
|
|
@@ -102,7 +102,7 @@ EDGE_KINDS = (
|
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
@dataclass
|
|
105
|
+
@dataclass(slots=True) # LOW-MEM PATCH: slots
|
|
106
106
|
class CFGNode:
|
|
107
107
|
"""A statement-level CFG node. ``id`` is assigned in source-span order
|
|
108
108
|
after construction (ENTRY = 0, EXIT = last CFG id)."""
|
|
@@ -118,7 +118,7 @@ class CFGNode:
|
|
|
118
118
|
ast_node: Optional[ast.AST] = field(default=None, repr=False, compare=False)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
@dataclass(frozen=True)
|
|
121
|
+
@dataclass(slots=True, frozen=True) # LOW-MEM PATCH: slots
|
|
122
122
|
class CFGEdge:
|
|
123
123
|
source: int
|
|
124
124
|
target: int
|
|
@@ -44,7 +44,7 @@ from codeanalyzer.dataflow.alias import TypeBasedAliasOracle
|
|
|
44
44
|
from codeanalyzer.dataflow.cfg import ControlFlowGraph
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
@dataclass(frozen=True)
|
|
47
|
+
@dataclass(slots=True, frozen=True) # LOW-MEM PATCH: slots
|
|
48
48
|
class DDGEdge:
|
|
49
49
|
source: int # the def node
|
|
50
50
|
target: int # the use node
|
|
@@ -59,7 +59,7 @@ CAPTURE_PREFIX = "<capture>:"
|
|
|
59
59
|
GLOBAL_PREFIX = "<global>:"
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
@dataclass
|
|
62
|
+
@dataclass(slots=True) # LOW-MEM PATCH: slots
|
|
63
63
|
class ParamNode:
|
|
64
64
|
id: int
|
|
65
65
|
kind: str # formal_in | formal_out | actual_in | actual_out
|
|
@@ -69,7 +69,7 @@ class ParamNode:
|
|
|
69
69
|
end_line: int = -1
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
@dataclass(frozen=True)
|
|
72
|
+
@dataclass(slots=True, frozen=True) # LOW-MEM PATCH: slots
|
|
73
73
|
class SDGEdge:
|
|
74
74
|
source_sig: str
|
|
75
75
|
source_node: int
|
|
@@ -142,6 +142,26 @@ class _FunctionAssembler:
|
|
|
142
142
|
|
|
143
143
|
# ---------------------------------------------------------------- formals
|
|
144
144
|
|
|
145
|
+
def formal_for(self, var: str) -> Optional[int]:
|
|
146
|
+
"""The ``formal_in`` vertex that defines ``var`` in this callable.
|
|
147
|
+
|
|
148
|
+
A parameter, a capture and a read global are all defined at a
|
|
149
|
+
``formal_in`` port, not at the synthetic CFG ENTRY node. Reaching-def
|
|
150
|
+
analysis reports ENTRY as the source for all three, so every place that
|
|
151
|
+
consumes a raw def source has to remap it here, or the SDG loses the
|
|
152
|
+
hop from the parameter to whatever uses it. Returns ``None`` when
|
|
153
|
+
``var`` is not one of this callable's formals, so a caller can keep the
|
|
154
|
+
original source.
|
|
155
|
+
"""
|
|
156
|
+
b = base_of(var)
|
|
157
|
+
if b in self.formal_in:
|
|
158
|
+
return self.formal_in[b]
|
|
159
|
+
if CAPTURE_PREFIX + b in self.formal_in:
|
|
160
|
+
return self.formal_in[CAPTURE_PREFIX + b]
|
|
161
|
+
if "::" in b and GLOBAL_PREFIX + b in self.formal_in:
|
|
162
|
+
return self.formal_in[GLOBAL_PREFIX + b]
|
|
163
|
+
return None
|
|
164
|
+
|
|
145
165
|
def build_formals(self) -> None:
|
|
146
166
|
scope, summary = self.scope, self.summary
|
|
147
167
|
params = list(scope.params)
|
|
@@ -168,14 +188,8 @@ class _FunctionAssembler:
|
|
|
168
188
|
for e in self.ddg:
|
|
169
189
|
if e.source != entry:
|
|
170
190
|
continue
|
|
171
|
-
|
|
172
|
-
if
|
|
173
|
-
fid = self.formal_in[b]
|
|
174
|
-
elif CAPTURE_PREFIX + b in self.formal_in:
|
|
175
|
-
fid = self.formal_in[CAPTURE_PREFIX + b]
|
|
176
|
-
elif "::" in b and GLOBAL_PREFIX + b in self.formal_in:
|
|
177
|
-
fid = self.formal_in[GLOBAL_PREFIX + b]
|
|
178
|
-
else:
|
|
191
|
+
fid = self.formal_for(e.var)
|
|
192
|
+
if fid is None:
|
|
179
193
|
continue
|
|
180
194
|
self.extra.append(PDGEdge(source=fid, target=e.target, type="DDG", var=e.var))
|
|
181
195
|
|
|
@@ -268,6 +282,8 @@ class _FunctionAssembler:
|
|
|
268
282
|
for src, var in self._defs_reaching_call_matching(
|
|
269
283
|
cs.node_id, path
|
|
270
284
|
):
|
|
285
|
+
if src == self.cfg.entry_id:
|
|
286
|
+
src = self.formal_for(var) or src
|
|
271
287
|
self.extra.append(
|
|
272
288
|
PDGEdge(source=src, target=aid, type="DDG", var=var)
|
|
273
289
|
)
|
|
@@ -296,6 +312,8 @@ class _FunctionAssembler:
|
|
|
296
312
|
for src, var in self._defs_reaching_call_matching(
|
|
297
313
|
cs.node_id, g
|
|
298
314
|
):
|
|
315
|
+
if src == self.cfg.entry_id:
|
|
316
|
+
src = self.formal_for(var) or src
|
|
299
317
|
self.extra.append(
|
|
300
318
|
PDGEdge(source=src, target=aid, type="DDG", var=var)
|
|
301
319
|
)
|
|
@@ -102,15 +102,21 @@ def _is_global(path: str) -> bool:
|
|
|
102
102
|
|
|
103
103
|
def augmented_facts(info: FunctionInfo, summaries: Dict[str, FunctionSummary]):
|
|
104
104
|
"""Per-node facts with callee global footprints injected at callsites."""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
# LOW-MEM PATCH: copy-on-write. Only call-site nodes are ever augmented, and
|
|
106
|
+
# every downstream consumer (reaching-defs, flows, the SDG assembler) only
|
|
107
|
+
# reads facts, so the untouched nodes can share the PDG's own records
|
|
108
|
+
# instead of being deep-copied for every solve.
|
|
109
|
+
out = dict(info.pdg.facts)
|
|
110
|
+
copied = set()
|
|
109
111
|
for cs in info.call_sites:
|
|
110
112
|
for target in cs.targets:
|
|
111
113
|
s = summaries.get(target)
|
|
112
114
|
if s is None:
|
|
113
115
|
continue
|
|
116
|
+
if cs.node_id not in copied:
|
|
117
|
+
f = out[cs.node_id]
|
|
118
|
+
out[cs.node_id] = type(f)(defs=set(f.defs), uses=set(f.uses))
|
|
119
|
+
copied.add(cs.node_id)
|
|
114
120
|
out[cs.node_id].uses |= s.global_reads
|
|
115
121
|
out[cs.node_id].defs |= s.global_writes
|
|
116
122
|
return out
|
{codeanalyzer_python-1.5.3 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/matching.py
RENAMED
|
@@ -104,8 +104,12 @@ def _methods_of(dec, spec: Optional[Dict[str, Any]], qualified: Optional[str] =
|
|
|
104
104
|
return []
|
|
105
105
|
source = spec.get("from")
|
|
106
106
|
if source == "match_suffix":
|
|
107
|
+
# Only a real verb: `heuristic.http-verb` also matches `.websocket`, and a
|
|
108
|
+
# rule may accept any suffix, but `http_methods` is what a consumer filters
|
|
109
|
+
# on to enumerate methods -- a value that is not one is worse there than an
|
|
110
|
+
# empty list (#213). The dispatch path below already filters the same way.
|
|
107
111
|
verb = (qualified or dec.qualified_name or "").rsplit(".", 1)[-1]
|
|
108
|
-
return [verb.upper()]
|
|
112
|
+
return [verb.upper()] if verb.lower() in _HTTP_VERBS else []
|
|
109
113
|
if source == "keyword":
|
|
110
114
|
raw = (dec.keyword_arguments or {}).get(spec.get("name", ""))
|
|
111
115
|
value = _literal(raw)
|
|
@@ -156,3 +156,86 @@ def _edge_statements(edges: List[EdgeRow]) -> List[str]:
|
|
|
156
156
|
f"SET r += row.p;"
|
|
157
157
|
)
|
|
158
158
|
return blocks
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ----------------------------------------------------------------------------------------------
|
|
162
|
+
# LOW-MEM PATCH: streaming snapshot writer (see write_cypher_streaming).
|
|
163
|
+
# ----------------------------------------------------------------------------------------------
|
|
164
|
+
from codeanalyzer.neo4j.rows import EdgeRow, GraphRows, NodeRef, RowBuilder # noqa: E402
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class _NodesOnlyBuilder(RowBuilder):
|
|
168
|
+
"""Pass 1: nodes (merged props/labels, exactly as RowBuilder) + key set; no edges."""
|
|
169
|
+
|
|
170
|
+
def edge(self, type_, from_ref, to_ref, props=None, key=None):
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
def edge_to_symbol(self, type_, from_ref, target_ref, props=None):
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class _EdgeStreamBuilder(RowBuilder):
|
|
178
|
+
"""Pass 2: nodes are already on disk; each edge batch is rendered with
|
|
179
|
+
``_edge_statements`` (same grouping/BATCH) the moment it fills."""
|
|
180
|
+
|
|
181
|
+
def __init__(self, keys, fh):
|
|
182
|
+
super().__init__()
|
|
183
|
+
self._keys = keys
|
|
184
|
+
self._fh = fh
|
|
185
|
+
self._pending: Dict[str, List[EdgeRow]] = {}
|
|
186
|
+
|
|
187
|
+
def node(self, labels, key_prop, value, props):
|
|
188
|
+
return NodeRef(labels[0], key_prop, value)
|
|
189
|
+
|
|
190
|
+
def edge(self, type_, from_ref, to_ref, props=None, key=None):
|
|
191
|
+
e = EdgeRow(type_, from_ref, to_ref, dict(props or {}), key)
|
|
192
|
+
g = (
|
|
193
|
+
f"{e.type}|{e.from_ref.label}.{e.from_ref.key_prop}"
|
|
194
|
+
f"|{e.to_ref.label}.{e.to_ref.key_prop}|{e.key is not None}"
|
|
195
|
+
)
|
|
196
|
+
batch = self._pending.setdefault(g, [])
|
|
197
|
+
batch.append(e)
|
|
198
|
+
if len(batch) >= BATCH:
|
|
199
|
+
self._flush(g)
|
|
200
|
+
|
|
201
|
+
def edge_to_symbol(self, type_, from_ref, target_ref, props=None):
|
|
202
|
+
if (target_ref.label, target_ref.value) in self._keys:
|
|
203
|
+
self.edge(type_, from_ref, target_ref, props)
|
|
204
|
+
|
|
205
|
+
def _flush(self, g):
|
|
206
|
+
batch = self._pending.pop(g, None)
|
|
207
|
+
if batch:
|
|
208
|
+
for stmt in _edge_statements(batch):
|
|
209
|
+
self._fh.write(stmt)
|
|
210
|
+
self._fh.write("\n")
|
|
211
|
+
|
|
212
|
+
def finish(self) -> GraphRows:
|
|
213
|
+
for g in list(self._pending):
|
|
214
|
+
self._flush(g)
|
|
215
|
+
return GraphRows([], [])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def write_cypher_streaming(fh, app, app_name: str, sig_to_id: dict, analyzer=None) -> None:
|
|
219
|
+
"""Write the ``graph.cypher`` snapshot for ``app`` to ``fh`` without holding the
|
|
220
|
+
edge rows or the rendered text in memory (two projection passes)."""
|
|
221
|
+
from codeanalyzer.neo4j.project import project
|
|
222
|
+
|
|
223
|
+
nodes_b = _NodesOnlyBuilder()
|
|
224
|
+
project(app, app_name, sig_to_id, analyzer=analyzer, builder=nodes_b)
|
|
225
|
+
rows = nodes_b.finish()
|
|
226
|
+
keys = nodes_b._keys
|
|
227
|
+
|
|
228
|
+
fh.write("// ── constraints & indexes ──\n")
|
|
229
|
+
for stmt in CONSTRAINTS:
|
|
230
|
+
fh.write(f"{stmt};\n")
|
|
231
|
+
for stmt in INDEXES:
|
|
232
|
+
fh.write(f"{stmt};\n")
|
|
233
|
+
fh.write("\n// ── wipe this project's prior subgraph (externals/packages/decorators are shared) ──\n")
|
|
234
|
+
fh.write(_wipe(app_name) + "\n")
|
|
235
|
+
fh.write("\n// ── nodes ──\n")
|
|
236
|
+
for stmt in _node_statements(rows.nodes):
|
|
237
|
+
fh.write(stmt + "\n")
|
|
238
|
+
del rows, nodes_b
|
|
239
|
+
|
|
240
|
+
fh.write("\n// ── relationships ──\n")
|
|
241
|
+
project(app, app_name, sig_to_id, analyzer=analyzer, builder=_EdgeStreamBuilder(keys, fh))
|