codeanalyzer-python 1.5.4__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.4 → codeanalyzer_python-1.6.0}/PKG-INFO +11 -19
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/README.md +1 -1
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/__main__.py +3 -4
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/config_keys.py +1 -6
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/parsers.py +1 -6
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/core.py +119 -3
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/access_paths.py +1 -1
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/builder.py +52 -5
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/cfg.py +2 -2
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/defuse.py +1 -1
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/pdg.py +1 -1
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/sdg.py +2 -2
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/summaries.py +10 -4
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/cypher.py +83 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/emit.py +5 -3
- {codeanalyzer_python-1.5.4 → 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.4 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/py_schema.py +60 -46
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/pyproject.toml +17 -37
- codeanalyzer_python-1.5.4/codeanalyzer/schema/__init__.py +0 -141
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/.gitignore +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/LICENSE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/NOTICE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/config_use.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/config_use_rules.yml +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/dependencies.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/artifacts/discovery.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/alias.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/dominance.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/identity.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/LICENSE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/SSA/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/SSA/const.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/cfg/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/cfg/builder.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/cfg/model.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/core/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/core/func_call_visitor.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel/core/vars_visitor.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/scc.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/slicing.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/dataflow/syntactic.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/detect.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/matching.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/pipeline.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/rules.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/entrypoints/rules.yml +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/jedi/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/jedi/jedi.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/bolt.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/rows.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/neo4j/schema.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/options/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/options/options.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/provenance.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/py.typed +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/assign_ids.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/call_graph_ids.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/ids.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/l1_body.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/schema/l2_callees.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/semantic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/semantic_analysis/defuse_linker.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/utils/__init__.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/utils/logging.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/codeanalyzer/utils/progress_bar.py +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/docs/handoff/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/docs/handoff/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/schema-uml.drawio +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/flask/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/LICENSE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/NOTICE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
- {codeanalyzer_python-1.5.4 → codeanalyzer_python-1.6.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
- {codeanalyzer_python-1.5.4 → 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.4 → 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.4 → 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
|
|
@@ -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
|
|
@@ -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))
|
|
@@ -29,7 +29,7 @@ from typing import Optional
|
|
|
29
29
|
|
|
30
30
|
from codeanalyzer.neo4j.bolt import BoltConfig, bolt_writer
|
|
31
31
|
from codeanalyzer.neo4j.schema import build_schema_document
|
|
32
|
-
from codeanalyzer.neo4j.cypher import render_cypher
|
|
32
|
+
from codeanalyzer.neo4j.cypher import render_cypher, write_cypher_streaming
|
|
33
33
|
from codeanalyzer.neo4j.project import project
|
|
34
34
|
from codeanalyzer.options import GRAPH_CYPHER, AnalysisOptions
|
|
35
35
|
from codeanalyzer.schema import Analysis
|
|
@@ -58,9 +58,9 @@ def emit_neo4j(analysis: Analysis, options: AnalysisOptions) -> None:
|
|
|
58
58
|
# canonical ``can://`` id and returns the ``signature -> id`` map the projection
|
|
59
59
|
# keys nodes on, so the JSON and Neo4j projections agree.
|
|
60
60
|
sig_to_id = assign_ids(analysis.application, app_name)
|
|
61
|
-
rows = project(analysis.application, app_name, sig_to_id, analyzer=analysis.analyzer)
|
|
62
61
|
|
|
63
62
|
if options.neo4j_uri:
|
|
63
|
+
rows = project(analysis.application, app_name, sig_to_id, analyzer=analysis.analyzer)
|
|
64
64
|
cfg = BoltConfig(
|
|
65
65
|
uri=options.neo4j_uri,
|
|
66
66
|
user=options.neo4j_user,
|
|
@@ -76,5 +76,7 @@ def emit_neo4j(analysis: Analysis, options: AnalysisOptions) -> None:
|
|
|
76
76
|
out_dir = options.output if options.output is not None else Path.cwd()
|
|
77
77
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
78
78
|
target = out_dir / GRAPH_CYPHER
|
|
79
|
-
target.
|
|
79
|
+
with target.open("w") as fh: # LOW-MEM PATCH: streamed, see cypher.write_cypher_streaming
|
|
80
|
+
write_cypher_streaming(fh, analysis.application, app_name, sig_to_id,
|
|
81
|
+
analyzer=analysis.analyzer)
|
|
80
82
|
logger.info(f"Neo4j graph written to {target}")
|
|
@@ -56,11 +56,11 @@ from codeanalyzer.schema.py_schema import PyDecorator, byte_offsets
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def project(app: PyApplication, app_name: str, sig_to_id: dict,
|
|
59
|
-
analyzer: Optional[Any] = None) -> GraphRows:
|
|
59
|
+
analyzer: Optional[Any] = None, builder: Optional[RowBuilder] = None) -> GraphRows:
|
|
60
60
|
"""``analyzer`` is the envelope-level ``PyAnalyzerInfo`` (the keystone home
|
|
61
61
|
for analyzer identity); the caller that holds the ``Analysis`` envelope
|
|
62
62
|
passes it through so the :PyApplication node carries it as props."""
|
|
63
|
-
b = RowBuilder()
|
|
63
|
+
b = builder if builder is not None else RowBuilder() # LOW-MEM PATCH
|
|
64
64
|
|
|
65
65
|
# Keyed on the ``can://<app>`` id, not on ``--app-name``: two applications
|
|
66
66
|
# analyzed under the same free-text name used to MERGE onto one root, with
|
|
@@ -928,6 +928,9 @@ def _stringify_if(value: Any) -> Optional[str]:
|
|
|
928
928
|
|
|
929
929
|
|
|
930
930
|
def _jsonable(o: Any) -> Any:
|
|
931
|
+
import dataclasses as _dc # LOW-MEM PATCH: slotted-dataclass schema leaves
|
|
932
|
+
if _dc.is_dataclass(o) and not isinstance(o, type):
|
|
933
|
+
return _dc.asdict(o)
|
|
931
934
|
if hasattr(o, "model_dump"):
|
|
932
935
|
return o.model_dump()
|
|
933
936
|
if hasattr(o, "dict"):
|