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 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
- json_str = json.dumps(
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.write(json_str)
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 sys
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 sys
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
- ray.init(
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, infos, ir, sig_to_id)
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
- f.write(model_dump_json(analysis, indent=2))
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
 
@@ -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=["ssa"],
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=["reaching-defs"])
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: Dict[str, FunctionInfo],
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
- delta = _ddg_local_set(im, fg.pdg) - _ddg_local_set(im, syn.pdg)
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=["points-to"])
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
@@ -42,7 +42,7 @@ from codeanalyzer.dataflow.defuse import ddg_edges
42
42
  from codeanalyzer.dataflow.dominance import control_dependence
43
43
 
44
44
 
45
- @dataclass(frozen=True)
45
+ @dataclass(slots=True, frozen=True) # LOW-MEM PATCH: slots
46
46
  class PDGEdge:
47
47
  source: int
48
48
  target: int
@@ -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
- b = base_of(e.var)
172
- if b in self.formal_in:
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
- facts = {nid: f for nid, f in info.pdg.facts.items()}
106
- out = {}
107
- for nid, f in facts.items():
108
- out[nid] = type(f)(defs=set(f.defs), uses=set(f.uses))
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)
@@ -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.write_text(render_cypher(rows, app_name))
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"):
@@ -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
- try:
47
- pydantic_version = version("pydantic")
48
- except PackageNotFoundError:
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
- # Compatibility helpers for Pydantic v1/v2
72
+ # Serialization helpers, kept as the one call site for every dump/copy/load.
91
73
  def model_dump_json(model, **kwargs):
92
- """Compatibility helper for JSON serialization."""
93
- if PYDANTIC_V2:
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
- """Compatibility helper for dict serialization (v2 model_dump / v1 dict).
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
- """Compatibility helper for copying a model (v2 model_copy / v1 copy)."""
125
- return model.model_copy() if PYDANTIC_V2 else model.copy()
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
- """Compatibility helper for JSON deserialization."""
130
- if PYDANTIC_V2:
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",
@@ -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
- model_fields = getattr(cls, "model_fields", None) # Pydantic v2
54
- if model_fields:
55
- for name, field in model_fields.items():
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 = defaults.get(field, None)
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
- @builder
118
- class Span(BaseModel):
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
- @builder
127
- class BodyNode(BaseModel):
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
- @builder
153
- class CfgEdge(BaseModel):
164
+ @dataclass(slots=True)
165
+ class CfgEdge:
154
166
  src: str; dst: str; kind: str = "fallthrough"
155
167
 
156
168
 
157
- @builder
158
- class CdgEdge(BaseModel):
169
+ @dataclass(slots=True)
170
+ class CdgEdge:
159
171
  src: str; dst: str
160
172
 
161
173
 
162
- @builder
163
- class DdgEdge(BaseModel):
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
- @builder
168
- class SummaryEdge(BaseModel):
179
+ @dataclass(slots=True)
180
+ class SummaryEdge:
169
181
  src: str; dst: str
170
182
 
171
183
 
172
- @builder
173
- class ParamEdge(BaseModel):
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
- class PyImport(BaseModel):
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
- class PyComment(BaseModel):
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
- class PySymbol(BaseModel):
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
- class PyVariableDeclaration(BaseModel):
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
- class PyDecorator(BaseModel):
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
- class PyCallableParameter(BaseModel):
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
- class PyCallArgument(BaseModel):
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
- class PyCallsite(BaseModel):
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
- class PyCallEdge(BaseModel):
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.5.3
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.9
8
+ Requires-Python: >=3.11
9
9
  Requires-Dist: astor<0.9.0,>=0.8.1
10
- Requires-Dist: jedi<0.20.0,>=0.18.0; python_version < '3.11'
11
- Requires-Dist: jedi<=0.19.2; python_version >= '3.11'
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<2.0.0,>=1.8.0; python_version < '3.11'
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; python_version >= '3.11'
20
- Requires-Dist: ray==2.0.0; python_version < '3.11'
21
- Requires-Dist: requests<3.0.0,>=2.20.0; python_version >= '3.11'
22
- Requires-Dist: rich<14.0.0,>=12.6.0; python_version < '3.11'
23
- Requires-Dist: rich<15.0.0,>=14.0.0; python_version >= '3.11'
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.10 or newer.**
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=noiZpfaiyFlnmdzHoWOAImXpxDkiQzTxSO4gmyZVyGI,15981
3
- codeanalyzer/core.py,sha256=UoK-7xgzJx-f3A_fmmeEZkBshiOWNDwc_RJTPhCTU0Y,47655
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=ovwptAErLYutlCzUwvyim2kH6-6vUKxuRcBV2KJS71Y,26843
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=xOjC53tT0Mv0k7ONGwzl5XUvpBLnZlEcGmyrX5DS45k,9035
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=wC8Q9qD-RZzkoFWMVvu_6uNNmYP8z48OGp9h9v3F1d4,23623
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=Ku16XuEJj46lnfuigMMyR6yheRW52f5U-ih9msxqSyo,33402
17
- codeanalyzer/dataflow/cfg.py,sha256=u5YXJjAaDshdgv-9kWWITbBJFP-MwXKq6hbNh0fhDm8,25307
18
- codeanalyzer/dataflow/defuse.py,sha256=LrX1ToOZzR79NlAGg5T647UAFkpiEqEnT7t0K5RXOiI,4377
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=1Bm7AnoRqXBryZulII2idPw0feV8eZQ1VqqprQ8PW-g,3731
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=taDXkIg0BZUtDEJUjRJB_dxrFl6tKNOUdFyj0zCiso0,18533
24
+ codeanalyzer/dataflow/sdg.py,sha256=SaR-fi86gKhs2lz9xYtXTpYdxKmjMYRhb8QGp25VtGw,19517
25
25
  codeanalyzer/dataflow/slicing.py,sha256=lWZ7jHhlR8m7rECWJQXvwfs5GNZeJY55Cud3Ji5UbNU,3654
26
- codeanalyzer/dataflow/summaries.py,sha256=TLtc5h4bLBC4lViuMhMlEp5rp_nBpDPOl4_wfBuRUY4,9210
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=rQeY_w2dyG36DBUjle7GLXxqDKsoMhJYPjN4s11OGtM,7948
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=-wledvXVrcLJRtSC5MCn1N3tqgC06rNfPBn0w6POqHo,6239
50
- codeanalyzer/neo4j/emit.py,sha256=7QYosqiadH2GIsjaUSmSlJ5g8yLjAAcMplsmFaJLnC4,3599
51
- codeanalyzer/neo4j/project.py,sha256=o6jdjwvHRxpNPNn-DXpOzebFq-fXgxoXTrKZ4_2ZVbQ,42383
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=hIyz02abUJNPW8yUgguaeKflZjTmOZWbDiKICNBD7yk,4141
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=4xJto32nWqHx0dkge4qHsV2JZSjbyv4ojnqpyBm78sQ,24588
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.5.3.dist-info/METADATA,sha256=mYN0yUedKfOOzWquD1DxyaoE3c9yUpK9Prs9AXqj6d0,44845
74
- codeanalyzer_python-1.5.3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
75
- codeanalyzer_python-1.5.3.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
76
- codeanalyzer_python-1.5.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
- codeanalyzer_python-1.5.3.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
78
- codeanalyzer_python-1.5.3.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any