codeanalyzer-python 1.5.3__py3-none-any.whl → 1.6.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 +3 -4
- codeanalyzer/artifacts/config_keys.py +1 -6
- codeanalyzer/artifacts/parsers.py +1 -6
- codeanalyzer/core.py +119 -3
- codeanalyzer/dataflow/access_paths.py +1 -1
- codeanalyzer/dataflow/builder.py +52 -5
- codeanalyzer/dataflow/cfg.py +2 -2
- codeanalyzer/dataflow/defuse.py +1 -1
- codeanalyzer/dataflow/pdg.py +1 -1
- codeanalyzer/dataflow/sdg.py +28 -10
- codeanalyzer/dataflow/summaries.py +10 -4
- codeanalyzer/entrypoints/matching.py +5 -1
- codeanalyzer/neo4j/cypher.py +83 -0
- codeanalyzer/neo4j/emit.py +5 -3
- codeanalyzer/neo4j/project.py +5 -2
- codeanalyzer/schema/__init__.py +15 -56
- codeanalyzer/schema/py_schema.py +60 -46
- {codeanalyzer_python-1.5.3.dist-info → codeanalyzer_python-1.6.0.dist-info}/METADATA +11 -19
- {codeanalyzer_python-1.5.3.dist-info → codeanalyzer_python-1.6.0.dist-info}/RECORD +23 -23
- {codeanalyzer_python-1.5.3.dist-info → codeanalyzer_python-1.6.0.dist-info}/WHEEL +1 -1
- {codeanalyzer_python-1.5.3.dist-info → codeanalyzer_python-1.6.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-1.5.3.dist-info → codeanalyzer_python-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-1.5.3.dist-info → codeanalyzer_python-1.6.0.dist-info}/licenses/NOTICE +0 -0
codeanalyzer/__main__.py
CHANGED
|
@@ -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
|
|
|
@@ -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
|
|
codeanalyzer/core.py
CHANGED
|
@@ -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)
|
|
@@ -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
|
|
codeanalyzer/dataflow/builder.py
CHANGED
|
@@ -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
|
|
codeanalyzer/dataflow/cfg.py
CHANGED
|
@@ -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
|
codeanalyzer/dataflow/defuse.py
CHANGED
|
@@ -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
|
codeanalyzer/dataflow/pdg.py
CHANGED
codeanalyzer/dataflow/sdg.py
CHANGED
|
@@ -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
|
|
@@ -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)
|
codeanalyzer/neo4j/cypher.py
CHANGED
|
@@ -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))
|
codeanalyzer/neo4j/emit.py
CHANGED
|
@@ -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}")
|
codeanalyzer/neo4j/project.py
CHANGED
|
@@ -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"):
|
codeanalyzer/schema/__init__.py
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
from importlib.metadata import version, PackageNotFoundError
|
|
2
|
-
from packaging.version import parse as parse_version
|
|
3
|
-
|
|
4
1
|
from .py_schema import (
|
|
5
2
|
Analysis,
|
|
6
3
|
BodyNode,
|
|
@@ -43,25 +40,10 @@ __all__ = [
|
|
|
43
40
|
"ParamEdge",
|
|
44
41
|
]
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
pydantic_version = "0.0.0" # fallback or raise if appropriate
|
|
50
|
-
|
|
51
|
-
PYDANTIC_V2 = parse_version(pydantic_version) >= parse_version("2.0.0")
|
|
43
|
+
# Pydantic v2 is the only supported major since Python 3.11 became the floor;
|
|
44
|
+
# kept as a constant for importers that still branch on it.
|
|
45
|
+
PYDANTIC_V2 = True
|
|
52
46
|
|
|
53
|
-
if not PYDANTIC_V2:
|
|
54
|
-
# Safe to pass localns
|
|
55
|
-
PyCallable.update_forward_refs(PyClass=PyClass)
|
|
56
|
-
PyClass.update_forward_refs(PyCallable=PyCallable)
|
|
57
|
-
PyModule.update_forward_refs(PyCallable=PyCallable, PyClass=PyClass)
|
|
58
|
-
PyApplication.update_forward_refs(
|
|
59
|
-
PyCallable=PyCallable,
|
|
60
|
-
PyClass=PyClass,
|
|
61
|
-
PyModule=PyModule
|
|
62
|
-
)
|
|
63
|
-
Analysis.update_forward_refs(PyApplication=PyApplication)
|
|
64
|
-
|
|
65
47
|
# Fields the analyzer keeps in memory (and in the cache) but never emits.
|
|
66
48
|
# `call_sites` is the internal record `body{}` call nodes are derived from (#120);
|
|
67
49
|
# emitting both shipped the same fact twice under two identity schemes.
|
|
@@ -87,50 +69,27 @@ def strip_internal_only(data):
|
|
|
87
69
|
return data
|
|
88
70
|
|
|
89
71
|
|
|
90
|
-
#
|
|
72
|
+
# Serialization helpers, kept as the one call site for every dump/copy/load.
|
|
91
73
|
def model_dump_json(model, **kwargs):
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
return model.model_dump_json(**kwargs)
|
|
95
|
-
else:
|
|
96
|
-
# Map Pydantic v2 parameters to v1 equivalents
|
|
97
|
-
v1_kwargs = {}
|
|
98
|
-
if 'indent' in kwargs:
|
|
99
|
-
v1_kwargs['indent'] = kwargs['indent']
|
|
100
|
-
if 'exclude_none' in kwargs:
|
|
101
|
-
v1_kwargs['exclude_none'] = kwargs['exclude_none']
|
|
102
|
-
if 'separators' in kwargs:
|
|
103
|
-
# In v1, separators is passed to dumps_kwargs
|
|
104
|
-
v1_kwargs['separators'] = kwargs['separators']
|
|
105
|
-
return model.json(**v1_kwargs)
|
|
74
|
+
"""Serialize a model to a JSON string."""
|
|
75
|
+
return model.model_dump_json(**kwargs)
|
|
106
76
|
|
|
107
77
|
def model_dump(model, **kwargs):
|
|
108
|
-
"""
|
|
109
|
-
|
|
110
|
-
``mode="json"`` (v2) maps to a json round-trip on v1 so both versions
|
|
111
|
-
yield JSON-safe primitives.
|
|
112
|
-
"""
|
|
113
|
-
if PYDANTIC_V2:
|
|
114
|
-
return model.model_dump(**kwargs)
|
|
115
|
-
import json as _json
|
|
116
|
-
mode = kwargs.pop("mode", None)
|
|
117
|
-
v1_kwargs = {k: v for k, v in kwargs.items() if k in ("exclude_none", "exclude")}
|
|
118
|
-
if mode == "json":
|
|
119
|
-
return _json.loads(model.json(**v1_kwargs))
|
|
120
|
-
return model.dict(**v1_kwargs)
|
|
78
|
+
"""Serialize a model to a dict (``mode="json"`` yields JSON-safe primitives)."""
|
|
79
|
+
return model.model_dump(**kwargs)
|
|
121
80
|
|
|
122
81
|
|
|
123
82
|
def model_copy(model):
|
|
124
|
-
"""
|
|
125
|
-
|
|
83
|
+
"""Shallow-copy a model or a dataclass leaf."""
|
|
84
|
+
import copy as _copy, dataclasses as _dc
|
|
85
|
+
if _dc.is_dataclass(model): # LOW-MEM PATCH: slotted dataclass leaf (shallow copy, like model_copy)
|
|
86
|
+
return _copy.copy(model)
|
|
87
|
+
return model.model_copy()
|
|
126
88
|
|
|
127
89
|
|
|
128
90
|
def model_validate_json(model_class, json_data):
|
|
129
|
-
"""
|
|
130
|
-
|
|
131
|
-
return model_class.model_validate_json(json_data)
|
|
132
|
-
else:
|
|
133
|
-
return model_class.parse_raw(json_data)
|
|
91
|
+
"""Deserialize a model from a JSON string."""
|
|
92
|
+
return model_class.model_validate_json(json_data)
|
|
134
93
|
|
|
135
94
|
__all__.extend([
|
|
136
95
|
"PYDANTIC_V2",
|
codeanalyzer/schema/py_schema.py
CHANGED
|
@@ -21,6 +21,8 @@ for static analysis purposes.
|
|
|
21
21
|
"""
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
from pathlib import Path
|
|
24
|
+
import dataclasses as _dataclasses
|
|
25
|
+
from dataclasses import dataclass, field as _dc_field
|
|
24
26
|
from typing import Any, Dict, List, Optional, Tuple
|
|
25
27
|
from pydantic import BaseModel
|
|
26
28
|
from typing_extensions import Literal
|
|
@@ -50,22 +52,27 @@ def builder(cls):
|
|
|
50
52
|
# defaults straight off the model instead. Required fields are intentionally
|
|
51
53
|
# omitted (seeded ``None``) — the builder chain must set them.
|
|
52
54
|
defaults = {}
|
|
53
|
-
|
|
54
|
-
if
|
|
55
|
-
for
|
|
55
|
+
factories = {} # LOW-MEM PATCH: dataclass default_factory, called per builder
|
|
56
|
+
if _dataclasses.is_dataclass(cls): # LOW-MEM PATCH: slotted dataclass leaf
|
|
57
|
+
for f in _dataclasses.fields(cls):
|
|
58
|
+
if f.default is not _dataclasses.MISSING:
|
|
59
|
+
defaults[f.name] = f.default
|
|
60
|
+
elif f.default_factory is not _dataclasses.MISSING:
|
|
61
|
+
factories[f.name] = f.default_factory
|
|
62
|
+
else:
|
|
63
|
+
for name, field in cls.model_fields.items():
|
|
56
64
|
if not field.is_required():
|
|
57
65
|
defaults[name] = field.get_default(call_default_factory=True)
|
|
58
|
-
else: # Pydantic v1
|
|
59
|
-
for name, field in getattr(cls, "__fields__", {}).items():
|
|
60
|
-
if not field.required:
|
|
61
|
-
defaults[name] = field.get_default()
|
|
62
66
|
# Create a namespace for the builder class.
|
|
63
67
|
namespace = {}
|
|
64
68
|
|
|
65
69
|
# Create an __init__ method for the builder class that initializes all fields to their default values.
|
|
66
70
|
def __init__(self):
|
|
67
71
|
for field in annotations:
|
|
68
|
-
default
|
|
72
|
+
if field in factories: # LOW-MEM PATCH: fresh mutable default per instance
|
|
73
|
+
default = factories[field]()
|
|
74
|
+
else:
|
|
75
|
+
default = defaults.get(field, None)
|
|
69
76
|
setattr(self, f"_{field}", default)
|
|
70
77
|
|
|
71
78
|
namespace["__init__"] = __init__
|
|
@@ -114,8 +121,13 @@ def byte_offsets(source: str, start_line: int, start_col: int,
|
|
|
114
121
|
return offset(start_line, start_col), offset(end_line, end_col)
|
|
115
122
|
|
|
116
123
|
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
# LOW-MEM PATCH: the per-statement / per-edge records below are the bulk of a
|
|
125
|
+
# large project's tree (tens of millions of instances). As pydantic models each
|
|
126
|
+
# instance costs ~650 B (``__dict__`` + ``__pydantic_fields_set__`` + ...); as
|
|
127
|
+
# slotted stdlib dataclasses ~230 B, and pydantic serializes/validates them
|
|
128
|
+
# identically when they appear as fields of the enclosing models.
|
|
129
|
+
@dataclass(slots=True)
|
|
130
|
+
class Span:
|
|
119
131
|
"""Where a node lives in source. `start`/`end` are [line, col] (1-based line,
|
|
120
132
|
0-based col, ast semantics); `bytes` are utf-8 offsets into module.source."""
|
|
121
133
|
start: Tuple[int, int]
|
|
@@ -123,8 +135,8 @@ class Span(BaseModel):
|
|
|
123
135
|
bytes: Tuple[int, int]
|
|
124
136
|
|
|
125
137
|
|
|
126
|
-
@
|
|
127
|
-
class BodyNode
|
|
138
|
+
@dataclass(slots=True)
|
|
139
|
+
class BodyNode:
|
|
128
140
|
"""A node in a callable's `body`: an AST region (statement/call/branch/…) or
|
|
129
141
|
a synthetic analysis vertex (entry/exit/formal_in/out/actual_in/out)."""
|
|
130
142
|
kind: str
|
|
@@ -146,31 +158,31 @@ class BodyNode(BaseModel):
|
|
|
146
158
|
receiver_type: Optional[str] = None
|
|
147
159
|
return_type: Optional[str] = None
|
|
148
160
|
is_constructor_call: Optional[bool] = None
|
|
149
|
-
arguments: List["PyCallArgument"] =
|
|
161
|
+
arguments: List["PyCallArgument"] = _dc_field(default_factory=list)
|
|
150
162
|
|
|
151
163
|
|
|
152
|
-
@
|
|
153
|
-
class CfgEdge
|
|
164
|
+
@dataclass(slots=True)
|
|
165
|
+
class CfgEdge:
|
|
154
166
|
src: str; dst: str; kind: str = "fallthrough"
|
|
155
167
|
|
|
156
168
|
|
|
157
|
-
@
|
|
158
|
-
class CdgEdge
|
|
169
|
+
@dataclass(slots=True)
|
|
170
|
+
class CdgEdge:
|
|
159
171
|
src: str; dst: str
|
|
160
172
|
|
|
161
173
|
|
|
162
|
-
@
|
|
163
|
-
class DdgEdge
|
|
164
|
-
src: str; dst: str; var: Optional[str] = None; prov: List[str] =
|
|
174
|
+
@dataclass(slots=True)
|
|
175
|
+
class DdgEdge:
|
|
176
|
+
src: str; dst: str; var: Optional[str] = None; prov: List[str] = _dc_field(default_factory=list)
|
|
165
177
|
|
|
166
178
|
|
|
167
|
-
@
|
|
168
|
-
class SummaryEdge
|
|
179
|
+
@dataclass(slots=True)
|
|
180
|
+
class SummaryEdge:
|
|
169
181
|
src: str; dst: str
|
|
170
182
|
|
|
171
183
|
|
|
172
|
-
@
|
|
173
|
-
class ParamEdge
|
|
184
|
+
@dataclass(slots=True)
|
|
185
|
+
class ParamEdge:
|
|
174
186
|
"""A `param_in` (actual_in → formal_in) or `param_out` (formal_out → actual_out)
|
|
175
187
|
edge at application scope. `var` is the callee-side formal's variable — the
|
|
176
188
|
parameter name, or `<return>` for the return port — always set (#195), the
|
|
@@ -180,7 +192,8 @@ class ParamEdge(BaseModel):
|
|
|
180
192
|
|
|
181
193
|
|
|
182
194
|
@builder
|
|
183
|
-
|
|
195
|
+
@dataclass(slots=True) # LOW-MEM PATCH: leaf record
|
|
196
|
+
class PyImport:
|
|
184
197
|
"""Represents a Python import statement."""
|
|
185
198
|
|
|
186
199
|
module: str
|
|
@@ -194,7 +207,8 @@ class PyImport(BaseModel):
|
|
|
194
207
|
|
|
195
208
|
|
|
196
209
|
@builder
|
|
197
|
-
|
|
210
|
+
@dataclass(slots=True) # LOW-MEM PATCH: leaf record
|
|
211
|
+
class PyComment:
|
|
198
212
|
"""Represents a Python comment."""
|
|
199
213
|
|
|
200
214
|
content: str
|
|
@@ -206,7 +220,8 @@ class PyComment(BaseModel):
|
|
|
206
220
|
|
|
207
221
|
|
|
208
222
|
@builder
|
|
209
|
-
|
|
223
|
+
@dataclass(slots=True) # LOW-MEM PATCH: leaf record
|
|
224
|
+
class PySymbol:
|
|
210
225
|
"""Represents a symbol used or declared in Python code."""
|
|
211
226
|
|
|
212
227
|
name: str
|
|
@@ -220,7 +235,8 @@ class PySymbol(BaseModel):
|
|
|
220
235
|
|
|
221
236
|
|
|
222
237
|
@builder
|
|
223
|
-
|
|
238
|
+
@dataclass(slots=True) # LOW-MEM PATCH: leaf record
|
|
239
|
+
class PyVariableDeclaration:
|
|
224
240
|
"""Represents a Python variable declaration."""
|
|
225
241
|
|
|
226
242
|
name: str
|
|
@@ -238,7 +254,8 @@ class PyVariableDeclaration(BaseModel):
|
|
|
238
254
|
|
|
239
255
|
|
|
240
256
|
@builder
|
|
241
|
-
|
|
257
|
+
@dataclass(slots=True) # LOW-MEM PATCH: leaf record
|
|
258
|
+
class PyDecorator:
|
|
242
259
|
"""One decorator application, structured rather than a source string (#128).
|
|
243
260
|
|
|
244
261
|
``name`` is the spelling as written (``lru_cache``, ``builtins.staticmethod``);
|
|
@@ -249,8 +266,8 @@ class PyDecorator(BaseModel):
|
|
|
249
266
|
|
|
250
267
|
name: str
|
|
251
268
|
qualified_name: Optional[str] = None
|
|
252
|
-
positional_arguments: List[str] =
|
|
253
|
-
keyword_arguments: Dict[str, str] =
|
|
269
|
+
positional_arguments: List[str] = _dc_field(default_factory=list)
|
|
270
|
+
keyword_arguments: Dict[str, str] = _dc_field(default_factory=dict)
|
|
254
271
|
expression: str = ""
|
|
255
272
|
span: Optional[Span] = None
|
|
256
273
|
|
|
@@ -291,7 +308,8 @@ class PyEntrypointReport(BaseModel):
|
|
|
291
308
|
|
|
292
309
|
|
|
293
310
|
@builder
|
|
294
|
-
|
|
311
|
+
@dataclass(slots=True) # LOW-MEM PATCH: leaf record
|
|
312
|
+
class PyCallableParameter:
|
|
295
313
|
"""Represents a parameter of a Python callable (function/method)."""
|
|
296
314
|
|
|
297
315
|
name: str
|
|
@@ -300,7 +318,7 @@ class PyCallableParameter(BaseModel):
|
|
|
300
318
|
id: str = ""
|
|
301
319
|
type: Optional[str] = None
|
|
302
320
|
default_value: Optional[str] = None
|
|
303
|
-
decorators: List[PyDecorator] =
|
|
321
|
+
decorators: List[PyDecorator] = _dc_field(default_factory=list)
|
|
304
322
|
start_line: int = -1
|
|
305
323
|
end_line: int = -1
|
|
306
324
|
start_column: int = -1
|
|
@@ -308,7 +326,8 @@ class PyCallableParameter(BaseModel):
|
|
|
308
326
|
|
|
309
327
|
|
|
310
328
|
@builder
|
|
311
|
-
|
|
329
|
+
@dataclass(slots=True) # LOW-MEM PATCH: leaf record
|
|
330
|
+
class PyCallArgument:
|
|
312
331
|
"""One call-site argument: AST category + inferred type, kept separate.
|
|
313
332
|
|
|
314
333
|
The legacy ``PyCallsite.argument_types`` mixed these two vocabularies
|
|
@@ -330,22 +349,16 @@ class PyCallArgument(BaseModel):
|
|
|
330
349
|
name: Optional[str] = None
|
|
331
350
|
|
|
332
351
|
|
|
333
|
-
# BodyNode.arguments forward-references PyCallArgument (defined later);
|
|
334
|
-
# pydantic v1 resolves string annotations only when told to, while v2
|
|
335
|
-
# rebuilds automatically (and its update_forward_refs shim rejects localns).
|
|
336
|
-
if not hasattr(BodyNode, "model_rebuild"): # pydantic v1
|
|
337
|
-
BodyNode.update_forward_refs(PyCallArgument=PyCallArgument)
|
|
338
|
-
|
|
339
|
-
|
|
340
352
|
@builder
|
|
341
|
-
|
|
353
|
+
@dataclass(slots=True) # LOW-MEM PATCH: leaf record
|
|
354
|
+
class PyCallsite:
|
|
342
355
|
"""Represents a Python call site (function or method invocation) with contextual metadata."""
|
|
343
356
|
|
|
344
357
|
method_name: str
|
|
345
358
|
receiver_expr: Optional[str] = None
|
|
346
359
|
receiver_type: Optional[str] = None
|
|
347
|
-
argument_types: List[str] =
|
|
348
|
-
arguments: List[PyCallArgument] =
|
|
360
|
+
argument_types: List[str] = _dc_field(default_factory=list)
|
|
361
|
+
arguments: List[PyCallArgument] = _dc_field(default_factory=list)
|
|
349
362
|
return_type: Optional[str] = None
|
|
350
363
|
callee_signature: Optional[str] = None
|
|
351
364
|
is_constructor_call: bool = False
|
|
@@ -464,7 +477,8 @@ class PyModule(BaseModel):
|
|
|
464
477
|
|
|
465
478
|
|
|
466
479
|
@builder
|
|
467
|
-
|
|
480
|
+
@dataclass(slots=True) # LOW-MEM PATCH: ~1M instances on a large project
|
|
481
|
+
class PyCallEdge:
|
|
468
482
|
"""Identity-only call-graph edge with weight (keystone shape: the list name
|
|
469
483
|
IS the edge type, so there is no ``type`` field).
|
|
470
484
|
|
|
@@ -477,7 +491,7 @@ class PyCallEdge(BaseModel):
|
|
|
477
491
|
src: str # caller callable id
|
|
478
492
|
dst: str # callee callable (or external) id
|
|
479
493
|
weight: int = 1
|
|
480
|
-
prov: List[Literal["jedi", "defuse"]] =
|
|
494
|
+
prov: List[Literal["jedi", "defuse"]] = _dc_field(default_factory=list)
|
|
481
495
|
|
|
482
496
|
|
|
483
497
|
@builder
|
|
@@ -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
|
|
|
@@ -1,29 +1,29 @@
|
|
|
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=eSp7cOmEKZ-BJn4C4JrPdNil3lVrE06yajPOr0iTTso,16027
|
|
3
|
+
codeanalyzer/core.py,sha256=bclHJjcJPYJbXbq1-tqaBXDeTIbdA1G4dkUKzjX-nXk,53184
|
|
4
4
|
codeanalyzer/provenance.py,sha256=DT-DqwdVwO7g-GyK3sC0_4vDNCUjZYEY1fyYCt-7VRM,2306
|
|
5
5
|
codeanalyzer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
codeanalyzer/artifacts/__init__.py,sha256=317sEeZLS1AYsfYDYIk7tLR3AdDFwWw_cmkeZDabkJo,906
|
|
7
|
-
codeanalyzer/artifacts/config_keys.py,sha256=
|
|
7
|
+
codeanalyzer/artifacts/config_keys.py,sha256=ki3ImlNMc9QfN1heEOzjbbeMaZf6gO7LQv3tje_xF_w,26710
|
|
8
8
|
codeanalyzer/artifacts/config_use.py,sha256=1-jGacW249dwF7b6DOrTFuJ7sA5Ho9khgrgZqND0nro,27173
|
|
9
9
|
codeanalyzer/artifacts/config_use_rules.yml,sha256=fNgI6IceOsj78LYmgoDH-vQ4-Mfb2ZFGKIQhLuoL4ik,1483
|
|
10
10
|
codeanalyzer/artifacts/dependencies.py,sha256=h_-XV5KhrT-1Ytguw0c4fyI9zSFzn9Aj7kR66omvL3M,10853
|
|
11
11
|
codeanalyzer/artifacts/discovery.py,sha256=urN4ZvtnXs0w9Kv2-yeOQza1TV1mjLL007zHfbdjNSg,8416
|
|
12
|
-
codeanalyzer/artifacts/parsers.py,sha256=
|
|
12
|
+
codeanalyzer/artifacts/parsers.py,sha256=ZxO0d7whPPh4o-cb0PIsUYK3iNZfxaePqcBQn66OvDg,8902
|
|
13
13
|
codeanalyzer/dataflow/__init__.py,sha256=HCNOH24rAGLUMG94N9aC-2SsAWSntk3uys7e39ipU5o,1725
|
|
14
|
-
codeanalyzer/dataflow/access_paths.py,sha256=
|
|
14
|
+
codeanalyzer/dataflow/access_paths.py,sha256=rbH_5AhTvR3C6s4OCiIuZTY2qSjDcxgovBXeHO2HURM,23659
|
|
15
15
|
codeanalyzer/dataflow/alias.py,sha256=ZYKY0DiR3GHv6YJr7C001Lode9rZsk6-gxdomUyIvwg,3928
|
|
16
|
-
codeanalyzer/dataflow/builder.py,sha256=
|
|
17
|
-
codeanalyzer/dataflow/cfg.py,sha256=
|
|
18
|
-
codeanalyzer/dataflow/defuse.py,sha256
|
|
16
|
+
codeanalyzer/dataflow/builder.py,sha256=Q2T1r7rpLnhpZSaGB2uOd0ugPDkd9vCuUrIaUMiHv6A,35587
|
|
17
|
+
codeanalyzer/dataflow/cfg.py,sha256=ilXFzDSoBEAYszmf1LlGAdlEae41WrkO1SmbCTNTv9U,25379
|
|
18
|
+
codeanalyzer/dataflow/defuse.py,sha256=-rxJl6dlMXPfysm8XcjDXzpNFNlCYNJ0XBDAhKe7PSk,4413
|
|
19
19
|
codeanalyzer/dataflow/dominance.py,sha256=X7Ki1QRdVqaseYsJtiUL7m9MbcC2WHEv9b8bczSLFUA,5086
|
|
20
20
|
codeanalyzer/dataflow/identity.py,sha256=6aAz3iPSOpoS7l_moC7XGr2RS_GqJp_FlsWGglMJDjM,3812
|
|
21
|
-
codeanalyzer/dataflow/pdg.py,sha256=
|
|
21
|
+
codeanalyzer/dataflow/pdg.py,sha256=_BbyrqYF-FTs98MqQY5f3qeiDqaHqLS_Ox6tZCcoiRk,3767
|
|
22
22
|
codeanalyzer/dataflow/scalpel_oracle.py,sha256=FxRadKrTWar0VKHyQJM40n3cq25sld3Sj4lMdOm5NFk,11494
|
|
23
23
|
codeanalyzer/dataflow/scc.py,sha256=Doa_0-5f3agCu_5UmeEDlCUErg34TtniKIv8Uqd7Jiw,3493
|
|
24
|
-
codeanalyzer/dataflow/sdg.py,sha256=
|
|
24
|
+
codeanalyzer/dataflow/sdg.py,sha256=SaR-fi86gKhs2lz9xYtXTpYdxKmjMYRhb8QGp25VtGw,19517
|
|
25
25
|
codeanalyzer/dataflow/slicing.py,sha256=lWZ7jHhlR8m7rECWJQXvwfs5GNZeJY55Cud3Ji5UbNU,3654
|
|
26
|
-
codeanalyzer/dataflow/summaries.py,sha256=
|
|
26
|
+
codeanalyzer/dataflow/summaries.py,sha256=VZRly_6CEHUrvBxV8_cBM5b-oG4v65jDrH_vZD2Knfk,9573
|
|
27
27
|
codeanalyzer/dataflow/syntactic.py,sha256=AbHyXjKX_1xkGgKH48BpXYCWBauActGUwSMF_OD-uys,1124
|
|
28
28
|
codeanalyzer/dataflow/scalpel/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
29
29
|
codeanalyzer/dataflow/scalpel/README.md,sha256=YN-LxqYYDekIdhx39AG28sg9UySMmqvLQJivTvR2Fz8,1543
|
|
@@ -38,7 +38,7 @@ codeanalyzer/dataflow/scalpel/core/func_call_visitor.py,sha256=ps0snjTchBXilhor3
|
|
|
38
38
|
codeanalyzer/dataflow/scalpel/core/vars_visitor.py,sha256=gE5fNyJS6jslD6vsMRbFLoy3n1xTwH-b54mBcNYO72M,5660
|
|
39
39
|
codeanalyzer/entrypoints/__init__.py,sha256=VaMd4mSEPLuPlRxxAve_nTJ9ZH62stXZGTPNwU_BQ50,99
|
|
40
40
|
codeanalyzer/entrypoints/detect.py,sha256=fsWRQz1njvB9d3LIJEHxsNFE1GrB7q52KCvozw_UIUQ,4594
|
|
41
|
-
codeanalyzer/entrypoints/matching.py,sha256=
|
|
41
|
+
codeanalyzer/entrypoints/matching.py,sha256=fppta04o1_HWBEHe6boV_7T964cdLuZlOoQHAW4yt0Q,8321
|
|
42
42
|
codeanalyzer/entrypoints/pipeline.py,sha256=vVSdttMfSAWour2fsC3LomMtwFLzRqmJHixJS82VY1I,7828
|
|
43
43
|
codeanalyzer/entrypoints/rules.py,sha256=yG772JHmNc2L10jIpxfg1xTh-QouJa6lNU9ubo3EZYg,6003
|
|
44
44
|
codeanalyzer/entrypoints/rules.yml,sha256=sguICRfDDNGjJPfZz-JSwkI_hzbTJaau1liHV_y4rTA,4637
|
|
@@ -46,20 +46,20 @@ codeanalyzer/jedi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
|
46
46
|
codeanalyzer/jedi/jedi.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
47
|
codeanalyzer/neo4j/__init__.py,sha256=8Ei6uThTOqmul6GhnKoGOjLXJZbyDwF5Jo-8I8NxnAk,1574
|
|
48
48
|
codeanalyzer/neo4j/bolt.py,sha256=8l70iGDjY3Kdm-W1sb3b3O1uqLjDrVPYTpXhnkXdZOY,13694
|
|
49
|
-
codeanalyzer/neo4j/cypher.py,sha256
|
|
50
|
-
codeanalyzer/neo4j/emit.py,sha256=
|
|
51
|
-
codeanalyzer/neo4j/project.py,sha256=
|
|
49
|
+
codeanalyzer/neo4j/cypher.py,sha256=cQ3lY7ffbNYdIhFqbxn8l3boh7tBlGqyM3jRXvs63JU,9406
|
|
50
|
+
codeanalyzer/neo4j/emit.py,sha256=R8rJjTPOSA5AXO0eTQqY5QTdoIxph_EPiT5478fL8WM,3806
|
|
51
|
+
codeanalyzer/neo4j/project.py,sha256=CC-Pu6-QPW5jLimCAjCRXPDVn9pFFwWqO3K3NxGaEiA,42639
|
|
52
52
|
codeanalyzer/neo4j/rows.py,sha256=n_J3NVIusLPsHMjPCWaYvlL5sFm6V-kl_Z6B_BTH8Fk,10140
|
|
53
53
|
codeanalyzer/neo4j/schema.py,sha256=0qZ1qJgmutlMHyIjdfD5N9QtA-gwl8Qcgh4NGguQEMs,18089
|
|
54
54
|
codeanalyzer/options/__init__.py,sha256=HvKzVXBI91lBM941JSFN7npG4S2fHlVEKo9VC2lRso8,156
|
|
55
55
|
codeanalyzer/options/options.py,sha256=DAo1i08D0K16jCivTrdOcBgKRPO50SngFZTPAL_kpmY,1862
|
|
56
|
-
codeanalyzer/schema/__init__.py,sha256=
|
|
56
|
+
codeanalyzer/schema/__init__.py,sha256=4eyp9AXJBLE9mNLsUbNGdOD_UvGQ-8GTj-qm7CVJg6g,2731
|
|
57
57
|
codeanalyzer/schema/assign_ids.py,sha256=pQHluLaO5a6hFREp3vjVRsUeAfTYSJ95RXS9TZvnoys,1590
|
|
58
58
|
codeanalyzer/schema/call_graph_ids.py,sha256=mWAhJDBsW8i-siJiINOHCXEm4qM6F_5se9-HuHWIFqU,568
|
|
59
59
|
codeanalyzer/schema/ids.py,sha256=eShvV3R6oQt1lCQ2DFAhWekSWom63wQJDfeWXJ7KovQ,5986
|
|
60
60
|
codeanalyzer/schema/l1_body.py,sha256=-rBXZH1nhaCyQ2DVN-iIswuuSQ7Jsa8voSAaljv_c80,1737
|
|
61
61
|
codeanalyzer/schema/l2_callees.py,sha256=UGubBSLA_-GCK9Kak2rqkmoFlGifiImVnPDrypjHfrA,2441
|
|
62
|
-
codeanalyzer/schema/py_schema.py,sha256=
|
|
62
|
+
codeanalyzer/schema/py_schema.py,sha256=tVNLHR1lcimb-osSHSjXvkoJVHWtilDeZBBgVWMWsC4,25687
|
|
63
63
|
codeanalyzer/semantic_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
64
|
codeanalyzer/semantic_analysis/call_graph.py,sha256=6YEB_wTn5-oQYLrbIhJYE0BsHl4fpWPoy5Hwd9mTnGc,11918
|
|
65
65
|
codeanalyzer/semantic_analysis/defuse_linker.py,sha256=3n1ew94eyBsSI98cFqP0uMHFUv7ob7QR9DL2hTKMHsQ,65099
|
|
@@ -70,9 +70,9 @@ codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=tKed-VCSd_albMeZf
|
|
|
70
70
|
codeanalyzer/utils/__init__.py,sha256=hC6VWdR5rerSqBxzu9KQHTASWqwrrYJv-CMDwrTlzkc,137
|
|
71
71
|
codeanalyzer/utils/logging.py,sha256=Fw0tattPAOMs3o0JMjjXhRVLIF64f-SCcygUXF9jqeg,904
|
|
72
72
|
codeanalyzer/utils/progress_bar.py,sha256=C9JtzVdd10lIxTv-KA6PebqjKWueC_vMGwVzAtHuHIw,2818
|
|
73
|
-
codeanalyzer_python-1.
|
|
74
|
-
codeanalyzer_python-1.
|
|
75
|
-
codeanalyzer_python-1.
|
|
76
|
-
codeanalyzer_python-1.
|
|
77
|
-
codeanalyzer_python-1.
|
|
78
|
-
codeanalyzer_python-1.
|
|
73
|
+
codeanalyzer_python-1.6.0.dist-info/METADATA,sha256=r7izrINncIgH_0mITPQWdmyaz4Gp3YHp5OJah_2KUsk,44156
|
|
74
|
+
codeanalyzer_python-1.6.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
75
|
+
codeanalyzer_python-1.6.0.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
|
|
76
|
+
codeanalyzer_python-1.6.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
77
|
+
codeanalyzer_python-1.6.0.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
|
|
78
|
+
codeanalyzer_python-1.6.0.dist-info/RECORD,,
|
{codeanalyzer_python-1.5.3.dist-info → codeanalyzer_python-1.6.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{codeanalyzer_python-1.5.3.dist-info → codeanalyzer_python-1.6.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|