codeanalyzer-python 1.5.1__py3-none-any.whl → 1.5.3__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
@@ -39,7 +39,7 @@ def _pin_hash_seed() -> None:
39
39
  from codeanalyzer.core import Codeanalyzer
40
40
  from codeanalyzer.utils import _set_log_level, logger
41
41
  from codeanalyzer.schema import model_dump, model_dump_json, strip_internal_only
42
- from codeanalyzer.options import AnalysisOptions, EmitTarget
42
+ from codeanalyzer.options import ANALYSIS_JSON, AnalysisOptions, EmitTarget
43
43
 
44
44
 
45
45
  def _version_callback(value: bool) -> None:
@@ -402,7 +402,7 @@ def main(
402
402
 
403
403
  def _write_output(artifacts, output_dir: Path):
404
404
  """Write analysis.json (the single wire format since #118)."""
405
- output_file = output_dir / "analysis.json"
405
+ output_file = output_dir / ANALYSIS_JSON
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.
@@ -3,10 +3,11 @@ from __future__ import annotations
3
3
  import fnmatch
4
4
  import hashlib
5
5
  from pathlib import Path
6
- from typing import Dict, List, Tuple
6
+ from typing import Dict, Iterable, List, Tuple
7
7
 
8
8
  from codeanalyzer.schema.ids import artifact_id
9
9
  from codeanalyzer.schema.py_schema import PyArtifact
10
+ from codeanalyzer.utils import logger
10
11
 
11
12
  # (glob pattern against the repo-relative POSIX path, format, roles).
12
13
  # First match wins; patterns are checked in order.
@@ -67,6 +68,43 @@ _IGNORED_DIRS = {
67
68
  }
68
69
 
69
70
 
71
+ def _resolve_exclusions(project_dir: Path, exclude_paths: Iterable[Path]) -> List[Path]:
72
+ """Resolve the paths a run writes to, dropping any that would take the whole
73
+ project with them (#207).
74
+
75
+ A *directory* exclusion at or above the project root would empty the
76
+ inventory, which is worse than the bug it guards against, so it is refused.
77
+ That case is still covered, because the caller also passes the individual
78
+ output *files* -- excluding ``<project>/analysis.json`` costs one file
79
+ instead of the entire tree.
80
+ """
81
+ root = project_dir.resolve()
82
+ kept = []
83
+ for given in exclude_paths:
84
+ resolved = given.resolve()
85
+ if root.is_relative_to(resolved):
86
+ logger.warning(
87
+ f"Not excluding {resolved} from artifact discovery: it holds the "
88
+ f"project itself. Only this run's own output files under it are "
89
+ f"skipped; anything else written there is ingested as an artifact."
90
+ )
91
+ continue
92
+ kept.append(resolved)
93
+ return kept
94
+
95
+
96
+ def _is_excluded(path: Path, excluded: List[Path]) -> bool:
97
+ """True when ``path`` is, or sits inside, one of ``excluded`` (a file entry
98
+ matches only itself). Checked on the path as walked *and* on its resolved
99
+ form, so an output directory reached through a symlink is caught from either
100
+ side."""
101
+ if not excluded:
102
+ return False
103
+ if any(path.is_relative_to(directory) for directory in excluded):
104
+ return True
105
+ return any(path.resolve().is_relative_to(directory) for directory in excluded)
106
+
107
+
70
108
  def _classify(rel_posix: str) -> Tuple[str, List[str]] | None:
71
109
  name = rel_posix.rsplit("/", 1)[-1]
72
110
  for pattern, fmt, roles in RULES:
@@ -81,6 +119,7 @@ def discover_artifacts(
81
119
  app_name: str,
82
120
  *,
83
121
  capture_text: bool = True,
122
+ exclude_paths: Iterable[Path] = (),
84
123
  ) -> Dict[str, PyArtifact]:
85
124
  """Walk the project and return every file as an artifact, sorted by path.
86
125
 
@@ -93,15 +132,28 @@ def discover_artifacts(
93
132
  deliberate exception -- it IS rule-matched (a dependency-manifest), so it
94
133
  is captured like any other manifest despite the `.py` suffix.
95
134
 
135
+ ``exclude_paths`` names what this run writes -- the ``--output`` and cache
136
+ directories, and the output files inside them (#207). Without them a run
137
+ whose output lands inside the project ingests the previous run's whole
138
+ ``analysis.json``, and each run embeds the one before it until the process is
139
+ killed decoding its own output. Matching is on resolved paths, so a
140
+ relative, `..`-laden or symlinked target excludes the same tree, and a target
141
+ outside the project excludes nothing. A directory that holds the project
142
+ itself is refused (it would empty the inventory); the file entries still
143
+ cover that case.
144
+
96
145
  ``source`` is the WHOLE file or nothing -- never a prefix (#172). A
97
146
  decodable file is captured in full; ``capture_text=False`` empties
98
147
  ``source`` everywhere (inventory otherwise identical), and an undecodable
99
148
  file gets ``""`` as ``binary``. ``sha256``/``size_bytes`` always reflect
100
149
  the full file regardless."""
101
150
  out: Dict[str, PyArtifact] = {}
151
+ excluded = _resolve_exclusions(project_dir, exclude_paths)
102
152
  for path in sorted(project_dir.rglob("*")):
103
153
  if not path.is_file():
104
154
  continue
155
+ if _is_excluded(path, excluded):
156
+ continue
105
157
  rel = path.relative_to(project_dir)
106
158
  if any(part in _IGNORED_DIRS for part in rel.parts):
107
159
  continue
codeanalyzer/core.py CHANGED
@@ -35,7 +35,7 @@ from codeanalyzer.syntactic_analysis.exceptions import SymbolTableBuilderRayErro
35
35
  from codeanalyzer.syntactic_analysis.import_resolver import resolve_imports
36
36
  from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder
37
37
  from codeanalyzer.utils import ProgressBar
38
- from codeanalyzer.options import AnalysisOptions
38
+ from codeanalyzer.options import ANALYSIS_JSON, GRAPH_CYPHER, AnalysisOptions, EmitTarget
39
39
  from codeanalyzer.provenance import analyzer_info, repository_info
40
40
 
41
41
  def _artifact_full_text(project_dir: Path, path: str, art) -> str:
@@ -572,6 +572,24 @@ class Codeanalyzer:
572
572
  )
573
573
  return externals
574
574
 
575
+ def _own_output_paths(self) -> List[Path]:
576
+ """Where this run writes: the output and cache directories, plus the
577
+ output files themselves (#207).
578
+
579
+ Artifact discovery skips these, so a run whose ``-o``/``-c`` lands inside
580
+ ``-i`` does not ingest its own previous output (each run embedding the
581
+ last until the process is killed decoding it). The file entries carry the
582
+ degenerate case where the output directory *is* the project root, which
583
+ cannot be skipped wholesale without emptying the inventory.
584
+ """
585
+ paths: List[Path] = [self.cache_dir]
586
+ if self.options.output is not None:
587
+ paths += [self.options.output, self.options.output / ANALYSIS_JSON]
588
+ if self.options.emit is EmitTarget.NEO4J:
589
+ # No -o means the cypher snapshot lands in the working directory.
590
+ paths.append((self.options.output or Path.cwd()) / GRAPH_CYPHER)
591
+ return paths
592
+
575
593
  def analyze(self) -> Analysis:
576
594
  """Analyze the project and return the v2 ``Analysis`` envelope.
577
595
 
@@ -673,6 +691,7 @@ class Codeanalyzer:
673
691
  app.artifacts = discover_artifacts(
674
692
  self.project_dir, app_name,
675
693
  capture_text=self.options.artifact_text,
694
+ exclude_paths=self._own_output_paths(),
676
695
  )
677
696
  app.dependencies, app.unresolved_imports = build_dependency_view(
678
697
  app.artifacts,
@@ -35,6 +35,7 @@ the result degrades gracefully instead of crashing (contract rule).
35
35
  from __future__ import annotations
36
36
 
37
37
  import ast
38
+ from collections import defaultdict
38
39
  from pathlib import Path
39
40
  from typing import Callable, Dict, List, Optional, Set, Tuple
40
41
 
@@ -43,7 +44,7 @@ from codeanalyzer.dataflow.alias import TypeBasedAliasOracle
43
44
  from codeanalyzer.dataflow.pdg import build_pdg
44
45
  from codeanalyzer.dataflow.sdg import ProgramGraphsIR, assemble_sdg
45
46
  from codeanalyzer.dataflow.summaries import CallSite, FunctionInfo, compute_summaries
46
- from codeanalyzer.schema.ids import stamp_body_ids
47
+ from codeanalyzer.schema.ids import call_body_keys, stamp_body_ids
47
48
  from codeanalyzer.schema.py_schema import PyApplication, PyCallable, PyClass, PyModule
48
49
  from codeanalyzer.utils import logger
49
50
 
@@ -291,6 +292,11 @@ def emit_l3_body(
291
292
  continue
292
293
  pycallable.body[local] = BodyNode(kind=node.kind, span=span)
293
294
 
295
+ keys_at: Dict[str, List[str]] = defaultdict(list)
296
+ for key, n in pycallable.body.items():
297
+ if n.kind == "call":
298
+ keys_at[key.split("/", 1)[0]].append(key)
299
+
294
300
  # #115: anchor nested call vertices to their statement. A bare-call
295
301
  # statement shares its key with its CFG node (handled above); a call
296
302
  # nested inside a larger statement (`y = f(x)`) has its own key and
@@ -301,12 +307,16 @@ def emit_l3_body(
301
307
  continue
302
308
  stmt_local = im.local(node.id)
303
309
  for call in _calls_in(node.ast_node):
304
- call_key = f"{call.lineno}:{call.col_offset}"
305
- child = pycallable.body.get(call_key)
306
- if child is None or call_key == stmt_local:
307
- continue
308
- if child.kind == "call":
309
- child.parent = stmt_local
310
+ # Every call node at this position, not just one: nested calls
311
+ # that share a start column are keyed `line:col`, `line:col/2`,
312
+ # ... and each of them needs the anchor (#215).
313
+ base = f"{call.lineno}:{call.col_offset}"
314
+ for call_key in keys_at.get(base, ()):
315
+ child = pycallable.body.get(call_key)
316
+ if child is None or call_key == stmt_local:
317
+ continue
318
+ if child.kind == "call":
319
+ child.parent = stmt_local
310
320
 
311
321
  if want_cfg:
312
322
  pycallable.cfg = [
@@ -378,7 +388,7 @@ def build_program_graphs(
378
388
  calls_by_pos.setdefault(pos, (node.id, call))
379
389
  calls_by_line.setdefault(call.lineno, (node.id, call))
380
390
 
381
- for site in pycallable.call_sites or []:
391
+ for site_key, site in call_body_keys(pycallable.call_sites):
382
392
  # Prefer the callsite's body-backfilled callee over Jedi's own
383
393
  # callee_signature side channel: under cross-test parso/Jedi cache
384
394
  # pressure that inference can silently degrade (full-suite-only
@@ -388,7 +398,7 @@ def build_program_graphs(
388
398
  # Only a resolved INTERNAL target counts (id_to_sig misses on an
389
399
  # external/unresolved callee); falls through to callee_signature
390
400
  # exactly as before whenever the body doesn't have an answer.
391
- body_node = pycallable.body.get(f"{site.start_line}:{site.start_column}")
401
+ body_node = pycallable.body.get(site_key)
392
402
  target = (id_to_sig.get(body_node.callee) if body_node else None) or site.callee_signature
393
403
  if not target:
394
404
  continue
@@ -31,7 +31,7 @@ from codeanalyzer.neo4j.bolt import BoltConfig, bolt_writer
31
31
  from codeanalyzer.neo4j.schema import build_schema_document
32
32
  from codeanalyzer.neo4j.cypher import render_cypher
33
33
  from codeanalyzer.neo4j.project import project
34
- from codeanalyzer.options import AnalysisOptions
34
+ from codeanalyzer.options import GRAPH_CYPHER, AnalysisOptions
35
35
  from codeanalyzer.schema import Analysis
36
36
  from codeanalyzer.schema.assign_ids import assign_ids
37
37
  from codeanalyzer.utils import logger
@@ -75,6 +75,6 @@ def emit_neo4j(analysis: Analysis, options: AnalysisOptions) -> None:
75
75
 
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
- target = out_dir / "graph.cypher"
78
+ target = out_dir / GRAPH_CYPHER
79
79
  target.write_text(render_cypher(rows, app_name))
80
80
  logger.info(f"Neo4j graph written to {target}")
@@ -49,8 +49,10 @@ from codeanalyzer.schema import (
49
49
  PyVariableDeclaration,
50
50
  )
51
51
  from codeanalyzer.schema import model_dump
52
- from codeanalyzer.schema.ids import application_id, external_id, global_ordinal, purl_pypi
53
- from codeanalyzer.schema.py_schema import PyDecorator
52
+ from codeanalyzer.schema.ids import (
53
+ application_id, call_body_keys, external_id, global_ordinal, purl_pypi,
54
+ )
55
+ from codeanalyzer.schema.py_schema import PyDecorator, byte_offsets
54
56
 
55
57
 
56
58
  def project(app: PyApplication, app_name: str, sig_to_id: dict,
@@ -182,6 +184,17 @@ def _project_program_graphs(
182
184
  if not c.id:
183
185
  continue # unstamped callable — assign_ids must run first
184
186
  owner = _sym(c.id) # the :PyCallable node, keyed by its can:// id
187
+ # ``callee_signature`` lives on ``PyCallable.call_sites``, not on the body
188
+ # node, so the graph joins the two on the call site's BODY KEY (#203,
189
+ # #215) -- re-derived from the same sequence L1 keyed ``body`` with, so
190
+ # two calls that start at one position keep their own signatures.
191
+ # ``argument_types`` is deliberately not joined: it is the legacy field #86
192
+ # split into ``PyCallArgument``, already carried as ``arguments_json``.
193
+ sig_by_key = {
194
+ key: cs.callee_signature
195
+ for key, cs in call_body_keys(c.call_sites)
196
+ if cs.callee_signature
197
+ }
185
198
  for local_key, node in (c.body or {}).items():
186
199
  span = node.span
187
200
  # L4 param vertices carry the variable they model (``of``) and
@@ -194,8 +207,8 @@ def _project_program_graphs(
194
207
  prune(
195
208
  {
196
209
  "kind": node.kind,
197
- "start_line": span.start[0] if span else None,
198
- "end_line": span.end[0] if span else None,
210
+ **_span_props(span),
211
+ "callee_signature": sig_by_key.get(local_key),
199
212
  "var": node.of,
200
213
  "call_node": node.parent,
201
214
  # Call-site detail (#120). The JSON emits one node per
@@ -346,8 +359,7 @@ def _project_artifacts(b: RowBuilder, app: PyApplication, app_name: str, app_ref
346
359
  "namespace": ck.namespace,
347
360
  "value": ck.value,
348
361
  "references": list(ck.references or []),
349
- "start_line": ck.span.start[0] if ck.span else None,
350
- "end_line": ck.span.end[0] if ck.span else None,
362
+ **_span_props(ck.span),
351
363
  }
352
364
  ),
353
365
  )
@@ -569,7 +581,7 @@ def _project_module_body(
569
581
  _project_class(b, file_key, mod_ref, "PY_DECLARES", cl, externals, sig_to_id,
570
582
  mod.source, base_ref)
571
583
  for v in mod.variables or []:
572
- _project_variable(b, file_key, mod_ref, v)
584
+ _project_variable(b, file_key, mod_ref, v, mod.source or "")
573
585
  _project_imports(b, mod_ref, mod, module_id_by_key)
574
586
 
575
587
 
@@ -597,9 +609,15 @@ def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule,
597
609
  continue
598
610
  key = im.resolved_module or im.module
599
611
  a = agg.setdefault(
600
- key, {"spellings": set(), "names": set(), "aliases": set(), "resolved": im.resolved_module}
612
+ key,
613
+ {"spellings": set(), "names": set(), "aliases": set(), "positions": {},
614
+ "resolved": im.resolved_module},
601
615
  )
602
616
  a["spellings"].add(im.module)
617
+ # Keyed by spelling, never by index: ``spellings`` is emitted sorted, so a
618
+ # parallel position array has already lost its alignment (#203).
619
+ a["positions"][im.module] = [im.start_line, im.start_column,
620
+ im.end_line, im.end_column]
603
621
  if im.name:
604
622
  a["names"].add(im.name)
605
623
  if im.alias:
@@ -623,6 +641,7 @@ def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule,
623
641
  "spellings": sorted(a["spellings"]),
624
642
  "imported_names": sorted(a["names"]) or None,
625
643
  "aliases": sorted(a["aliases"]) or None,
644
+ "positions_json": json.dumps(a["positions"], sort_keys=True),
626
645
  }
627
646
  ),
628
647
  )
@@ -675,7 +694,7 @@ def _project_callable(
675
694
  _project_decorator(b, ref, d)
676
695
 
677
696
  for v in c.local_variables or []:
678
- _project_variable(b, file_key, ref, v)
697
+ _project_variable(b, file_key, ref, v, source)
679
698
  for ic in (c.callables or {}).values():
680
699
  _project_callable(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id,
681
700
  source, base_ref)
@@ -700,12 +719,14 @@ def _project_variable(
700
719
  file_key: str,
701
720
  owner: NodeRef,
702
721
  v: PyVariableDeclaration,
722
+ source: str,
703
723
  ) -> None:
704
724
  # ``<owner can:// id>/<name>@<line>`` (#173) — the owner is the module or the
705
725
  # callable, so a module-level variable sits under ``<module-id>/`` like every
706
726
  # other declaration and the module's prefix purge reaches it.
707
727
  var_id = f"{owner.value}/{v.name}@{v.start_line}"
708
- ref = b.node(["PyVariable"], "id", var_id, _variable_props(v, var_id, file_key))
728
+ ref = b.node(["PyVariable"], "id", var_id,
729
+ _variable_props(v, var_id, file_key, source))
709
730
  b.edge("PY_DECLARES_VAR", owner, ref)
710
731
 
711
732
 
@@ -737,6 +758,7 @@ def _project_decorator(b: RowBuilder, on: NodeRef, decorator: PyDecorator) -> No
737
758
  "keyword_arguments_json": json.dumps(
738
759
  dict(decorator.keyword_arguments or {}), sort_keys=True
739
760
  ),
761
+ **_span_props(decorator.span),
740
762
  },
741
763
  )
742
764
 
@@ -752,6 +774,9 @@ def _module_props(mod: PyModule, file_key: str) -> Props:
752
774
  "id": mod.id,
753
775
  "file_key": file_key,
754
776
  "module_name": mod.module_name,
777
+ # Always present, never pruned: "" for an empty file, so a consumer can
778
+ # never confuse "not carried" with "empty" (#202).
779
+ "source": mod.source or "",
755
780
  "content_hash": mod.content_hash,
756
781
  "last_modified": mod.last_modified,
757
782
  "file_size": mod.file_size,
@@ -760,6 +785,36 @@ def _module_props(mod: PyModule, file_key: str) -> Props:
760
785
  )
761
786
 
762
787
 
788
+ def _span_props(span) -> Props:
789
+ """The six flattened span properties from a v2 ``Span`` (#202). Lines and columns
790
+ come straight off the model; ``bytes`` is already utf-8 and is what makes the span
791
+ sliceable out of ``:PyModule.source``."""
792
+ if span is None:
793
+ return {}
794
+ return {
795
+ "start_line": span.start[0], "start_column": span.start[1],
796
+ "end_line": span.end[0], "end_column": span.end[1],
797
+ "start_byte": span.bytes[0], "end_byte": span.bytes[1],
798
+ }
799
+
800
+
801
+ def _flat_span_props(source: str, start_line: int, start_column: int,
802
+ end_line: int, end_column: int) -> Props:
803
+ """The same six for a model carrying flat ast positions and no ``Span``
804
+ (:PyAttribute, :PyVariable). The byte pair is computed here rather than left
805
+ absent, so ``_SPAN`` means one thing on every label that spreads it (#202)."""
806
+ if start_line < 0 or end_line < 0 or not source:
807
+ return {}
808
+ start_column = max(start_column, 0)
809
+ end_column = max(end_column, 0)
810
+ lo, hi = byte_offsets(source, start_line, start_column, end_line, end_column)
811
+ return {
812
+ "start_line": start_line, "start_column": start_column,
813
+ "end_line": end_line, "end_column": end_column,
814
+ "start_byte": lo, "end_byte": hi,
815
+ }
816
+
817
+
763
818
  def _span_code(source: str, span) -> str | None:
764
819
  """A declaration's text: the owning module's ``source`` sliced by the node's
765
820
  utf-8 byte span. Schema v2 stores source once per module, so the graph's
@@ -781,8 +836,8 @@ def _class_props(cl: PyClass, file_key: str, source: str) -> Props:
781
836
  "base_classes": list(cl.base_classes or []),
782
837
  "decorators": [d.qualified_name or d.name for d in (cl.decorators or [])],
783
838
  "docstring": _docstring_of(cl.comments),
784
- "start_line": cl.start_line,
785
- "end_line": cl.end_line,
839
+ **(_span_props(cl.span) or {"start_line": cl.start_line,
840
+ "end_line": cl.end_line}),
786
841
  "_module": file_key,
787
842
  "is_entrypoint": bool(cl.entrypoints),
788
843
  "entrypoint_frameworks": sorted({e.framework for e in (cl.entrypoints or [])}),
@@ -801,8 +856,8 @@ def _callable_props(c: PyCallable, file_key: str, source: str) -> Props:
801
856
  "cyclomatic_complexity": c.cyclomatic_complexity,
802
857
  "code": _span_code(source, c.span),
803
858
  "code_start_line": c.code_start_line,
804
- "start_line": c.start_line,
805
- "end_line": c.end_line,
859
+ **(_span_props(c.span) or {"start_line": c.start_line,
860
+ "end_line": c.end_line}),
806
861
  "docstring": _docstring_of(c.comments),
807
862
  "decorators": [d.qualified_name or d.name for d in (c.decorators or [])],
808
863
  "modifiers": list(c.modifiers or []),
@@ -823,6 +878,8 @@ def _attribute_props(a: PyClassAttribute, attr_id: str, file_key: str) -> Props:
823
878
  "type": a.type,
824
879
  "initializer": a.initializer,
825
880
  "docstring": _docstring_of(a.comments),
881
+ # Line-only: PyClassAttribute has no columns, so there is nothing to
882
+ # derive bytes from. See the note on :PyAttribute in schema.py.
826
883
  "start_line": a.start_line,
827
884
  "end_line": a.end_line,
828
885
  "_module": file_key,
@@ -830,16 +887,21 @@ def _attribute_props(a: PyClassAttribute, attr_id: str, file_key: str) -> Props:
830
887
  )
831
888
 
832
889
 
833
- def _variable_props(v: PyVariableDeclaration, var_id: str, file_key: str) -> Props:
890
+ def _variable_props(v: PyVariableDeclaration, var_id: str, file_key: str,
891
+ source: str) -> Props:
834
892
  return prune(
835
893
  {
836
894
  "id": var_id,
837
895
  "name": v.name,
838
896
  "type": v.type,
839
897
  "initializer": v.initializer,
898
+ # ``value`` is Optional[Any] and a Neo4j property is a scalar or an array
899
+ # of scalars, so a dict/list value needs a serialization rather than a raw
900
+ # put -- always encoded, one shape for every value (#203).
901
+ "value_json": _stringify_if(v.value) if v.value is not None else None,
840
902
  "scope": v.scope,
841
- "start_line": v.start_line,
842
- "end_line": v.end_line,
903
+ **_flat_span_props(source, v.start_line, v.start_column,
904
+ v.end_line, v.end_column),
843
905
  "_module": file_key,
844
906
  }
845
907
  )
@@ -28,6 +28,13 @@ SCHEMA_VERSION is the contract version: bump MAJOR on a breaking change (renamed
28
28
  relationship or key), MINOR on an additive change (new label/rel/property). It is stamped onto
29
29
  the :PyApplication node of every emitted graph so any consumer can detect a producer/consumer
30
30
  mismatch at runtime.
31
+
32
+ **The additive-MINOR rule is suspended for the 2.0.0 line.** Per the 2026-09-07 ruling (all three
33
+ analyzers), payload ``schema_version`` and this version both hold at ``2.0.0`` until the 2.0.0 line
34
+ leaves release-candidate, so the additive properties of #202/#203 ship without a bump and are
35
+ detectable only by presence. Consumers gate on the **analyzer version** instead -- the python-sdk
36
+ Neo4j backends carry an analyzer floor and refuse anything below it at attach. The rule above
37
+ resumes at the coordinated re-baseline.
31
38
  """
32
39
 
33
40
  from __future__ import annotations
@@ -61,7 +68,18 @@ class RelType:
61
68
  # anchor for the prefix-scoped destructive statements (see ``rows.CAN_NODE``).
62
69
  MARKER_LABELS: List[str] = ["PyCanNode"]
63
70
 
64
- _SPAN = {"start_line": "integer", "end_line": "integer"}
71
+ # The flattened span. ``start_column``/``end_column``/``start_byte``/``end_byte``
72
+ # are adopted **verbatim** from codeanalyzer-java#255, which coined them: a term
73
+ # coined twice is permanently wrong under the cross-language parity clause. The
74
+ # byte pair makes every span sliceable out of :PyModule.source -- for the labels
75
+ # whose JSON model carries flat ast positions and no ``Span`` (:PyAttribute,
76
+ # :PyVariable) the projector computes it, so this dict means one thing on every
77
+ # label that spreads it (#202).
78
+ _SPAN = {
79
+ "start_line": "integer", "end_line": "integer",
80
+ "start_column": "integer", "end_column": "integer",
81
+ "start_byte": "integer", "end_byte": "integer",
82
+ }
65
83
 
66
84
 
67
85
  NODE_LABELS: List[NodeLabel] = [
@@ -90,6 +108,10 @@ NODE_LABELS: List[NodeLabel] = [
90
108
  "id": "string",
91
109
  "file_key": "string",
92
110
  "module_name": "string",
111
+ # The primary text: schema v2 stores source once per module and every
112
+ # narrower node's text is a byte slice of it. Always present -- an empty
113
+ # file yields "", so "not carried" is never confusable with "empty" (#202).
114
+ "source": "string",
93
115
  "content_hash": "string",
94
116
  "last_modified": "float",
95
117
  "file_size": "integer",
@@ -158,7 +180,13 @@ NODE_LABELS: List[NodeLabel] = [
158
180
  "type": "string",
159
181
  "initializer": "string",
160
182
  "docstring": "string",
161
- **_SPAN,
183
+ # The one _SPAN exception: ``PyClassAttribute`` carries start/end LINE only
184
+ # -- no columns, hence no derivable byte offsets. Emitting a fabricated
185
+ # column 0 would make the span unsliceable while claiming otherwise, so the
186
+ # line pair is declared honestly instead. Filed for the JSON model to gain
187
+ # columns; until then this label is line-granular (#203).
188
+ "start_line": "integer",
189
+ "end_line": "integer",
162
190
  },
163
191
  ),
164
192
  NodeLabel(
@@ -170,6 +198,11 @@ NODE_LABELS: List[NodeLabel] = [
170
198
  "name": "string",
171
199
  "type": "string",
172
200
  "initializer": "string",
201
+ # ``PyVariableDeclaration.value`` -- the literal-evaluated result, always
202
+ # JSON-encoded because it is ``Optional[Any]`` and a Neo4j property is a
203
+ # scalar or an array of scalars (the ``arguments_json`` precedent, #203).
204
+ # ``initializer`` stays the raw source text.
205
+ "value_json": "string",
173
206
  "scope": "string",
174
207
  **_SPAN,
175
208
  },
@@ -197,6 +230,12 @@ NODE_LABELS: List[NodeLabel] = [
197
230
  "return_type": "string",
198
231
  "is_constructor_call": "boolean",
199
232
  "arguments_json": "string",
233
+ # What distinguishes overload targets at a resolved call site. Joined
234
+ # from ``PyCallable.call_sites`` at projection time, since ``BodyNode``
235
+ # does not carry it in JSON (#203). ``argument_types`` is deliberately
236
+ # NOT here: it is the legacy field #86 split into ``PyCallArgument``,
237
+ # already carried as ``arguments_json``.
238
+ "callee_signature": "string",
200
239
  **_SPAN,
201
240
  },
202
241
  ),
@@ -247,16 +286,26 @@ REL_TYPES: List[RelType] = [
247
286
  "PY_IMPORTS",
248
287
  ["PyModule"],
249
288
  ["PyModule", "PyPackage"],
250
- {"spellings": "string[]", "imported_names": "string[]", "aliases": "string[]"},
289
+ # ``positions_json`` keys on the spelling, not on an index: this edge
290
+ # pre-aggregates per (module, target) and emits ``spellings`` sorted, so
291
+ # parallel position arrays have already lost their alignment (#203).
292
+ {"spellings": "string[]", "imported_names": "string[]", "aliases": "string[]",
293
+ "positions_json": "string"},
251
294
  ),
252
295
  RelType(
253
296
  "PY_DECORATED_BY",
254
297
  ["PyCallable", "PyClass"],
255
298
  ["PyDecorator"],
299
+ # The span rides here, not on :PyDecorator: that node is merged on the
300
+ # resolved ``qualified_name`` and carries no ``_module``, so it is never
301
+ # pruned and any per-application fact on it would accumulate across every
302
+ # project in the database -- the same reason ``expression`` and the
303
+ # arguments already ride the relationship (#203).
256
304
  {
257
305
  "expression": "string",
258
306
  "positional_arguments": "string[]",
259
307
  "keyword_arguments_json": "string",
308
+ **_SPAN,
260
309
  },
261
310
  ),
262
311
  # Level-3 CPG overlay (-a 3 only): the cross-language dataflow vocabulary,
@@ -1,3 +1,3 @@
1
- from .options import AnalysisOptions, EmitTarget
1
+ from .options import ANALYSIS_JSON, GRAPH_CYPHER, AnalysisOptions, EmitTarget
2
2
 
3
- __all__ = ["AnalysisOptions", "EmitTarget"]
3
+ __all__ = ["AnalysisOptions", "EmitTarget", "ANALYSIS_JSON", "GRAPH_CYPHER"]
@@ -4,6 +4,13 @@ from typing import Optional, Tuple
4
4
  from enum import Enum
5
5
 
6
6
 
7
+ # The files a run writes into its output directory. Named once so artifact
8
+ # discovery can recognize -- and skip -- the run's own output when it lands
9
+ # inside the analyzed project (#207).
10
+ ANALYSIS_JSON = "analysis.json"
11
+ GRAPH_CYPHER = "graph.cypher"
12
+
13
+
7
14
  class EmitTarget(str, Enum):
8
15
  """Output target selected by ``--emit``.
9
16
 
@@ -10,7 +10,7 @@ and artifacts alike — which is what the prefix-scoped destructive statements
10
10
  an application named ``python`` mints ``can://python/python/...``, so a test
11
11
  for ``can://python/`` no longer means "a python id"; test the scheme instead."""
12
12
  from __future__ import annotations
13
- from typing import List, Optional
13
+ from typing import Iterable, Iterator, List, Optional, Tuple
14
14
 
15
15
  SCHEME = "can://"
16
16
 
@@ -52,6 +52,42 @@ def external_id(app_id: str, module: Optional[str], name: str) -> str:
52
52
  return f"{base}/{module}/{name}" if module else f"{base}/{name}"
53
53
 
54
54
 
55
+ def call_body_keys(sites: Iterable) -> Iterator[Tuple[str, object]]:
56
+ """The body key of each call site, in recording order: ``line:col``,
57
+ disambiguated ``/2``, ``/3``, ... when nested calls share a start position
58
+ (#215).
59
+
60
+ `getattr(o, n)(x)` begins the outer application and the inner `getattr` at the
61
+ same column, so a bare ``line:col`` key keeps one of the two and the dynamic
62
+ invocation is lost. Call sites are recorded pre-order, so the bare key goes to
63
+ the OUTERMOST call and the nested ones take the suffixes. The spelling is
64
+ codeanalyzer-typescript's (``callBodyKeys``, ``src/schema/l1Body.ts``), adopted
65
+ verbatim; the ``/`` never collides with a param-vertex segment, which always
66
+ begins ``actual_``.
67
+
68
+ The SINGLE definition of the sequence -- L1 builds ``body`` with it, and L2, the
69
+ dataflow builder, the defuse linker and the Neo4j projection re-derive the same
70
+ pairing from it rather than re-deriving a key from a position.
71
+ """
72
+ used = set()
73
+ for cs in sites or []:
74
+ base = f"{cs.start_line}:{cs.start_column}"
75
+ key = base
76
+ k = 2
77
+ while key in used:
78
+ key = f"{base}/{k}"
79
+ k += 1
80
+ used.add(key)
81
+ yield key, cs
82
+
83
+ def call_body_key(callable_, site) -> Optional[str]:
84
+ """``site``'s body key within ``callable_`` — the pairing of
85
+ :func:`call_body_keys`, for a caller that holds one site rather than the list."""
86
+ for key, cs in call_body_keys(callable_.call_sites):
87
+ if cs is site:
88
+ return key
89
+ return None
90
+
55
91
  def global_ordinal(callable_id: str, local_key: str) -> str:
56
92
  """The GLOBAL ordinal id of a body node from its LOCAL key: synthetic keys
57
93
  (`@entry`, `@formal_in:0`) already carry the `@`; positional keys (`15:2`,
@@ -1,12 +1,11 @@
1
1
  """L1 body population: materialize `call` nodes from existing call sites.
2
2
  `callee` is left None here — the sanctioned null→id refinement happens at L2."""
3
3
  from __future__ import annotations
4
- from codeanalyzer.schema.ids import stamp_body_ids
4
+ from codeanalyzer.schema.ids import call_body_keys, stamp_body_ids
5
5
  from codeanalyzer.schema.py_schema import PyApplication, PyClass, PyCallable, BodyNode, Span, byte_offsets
6
6
 
7
7
  def _do_callable(source: str, c: PyCallable) -> None:
8
- for cs in c.call_sites or []:
9
- key = f"{cs.start_line}:{cs.start_column}"
8
+ for key, cs in call_body_keys(c.call_sites):
10
9
  span = Span(start=(cs.start_line, cs.start_column),
11
10
  end=(cs.end_line, cs.end_column),
12
11
  bytes=byte_offsets(source, cs.start_line, cs.start_column, cs.end_line, cs.end_column)) if source else None
@@ -5,17 +5,18 @@ signature; an unresolved call site leaves `callee` absent.
5
5
 
6
6
  Two resolution sources feed the backfill: Jedi's `callee_signature` on the
7
7
  call site itself, and the defuse linker's returned map (keyed by caller
8
- signature + "line:col"). The linker's resolutions are deliberately NOT written
8
+ signature + the call site's body key, which carries a `/N` disambiguator when
9
+ nested calls share a start position -- #215). The linker's resolutions are deliberately NOT written
9
10
  into `callee_signature` — the symbol table round-trips through the analysis
10
11
  cache, and a persisted resolution would resurface on a warm run as a Jedi
11
12
  edge, silently changing provenance."""
12
13
  from __future__ import annotations
14
+ from codeanalyzer.schema.ids import call_body_keys
13
15
  from codeanalyzer.schema.py_schema import PyApplication, PyClass, PyCallable
14
16
 
15
17
 
16
18
  def _do_callable(c: PyCallable, sig_to_id: dict, resolutions: dict) -> None:
17
- for cs in c.call_sites or []:
18
- key = f"{cs.start_line}:{cs.start_column}"
19
+ for key, cs in call_body_keys(c.call_sites):
19
20
  jedi_sig = cs.callee_signature
20
21
  if jedi_sig and jedi_sig.startswith("typing."):
21
22
  # A decorator-typed callable resolved to its annotation, not a
@@ -40,11 +40,14 @@ import ast
40
40
  import builtins as _py_builtins
41
41
  from typing import Dict, List, Optional, Tuple
42
42
 
43
+ from codeanalyzer.schema.ids import call_body_key
43
44
  from codeanalyzer.schema.py_schema import PyCallable, PyCallEdge, PyClass, PyModule
44
45
 
45
46
  __all__ = ["defuse_linker_edges"]
46
47
 
47
- # (caller signature, "line:col" of the call site) -> resolved callee signature
48
+ # (caller signature, the call site's body key) -> resolved callee signature.
49
+ # The key is `call_body_key`, not a bare "line:col": two nested calls can start
50
+ # at one position and only one of them is the resolution being recorded (#215).
48
51
  Resolutions = Dict[Tuple[str, str], str]
49
52
 
50
53
  _MAX_CHAIN = 16 # assignment-chain hops before giving up (cycle safety net)
@@ -1095,7 +1098,7 @@ def defuse_linker_edges(
1095
1098
  oracle.vote(sig, site)
1096
1099
  bump(caller.signature, sig)
1097
1100
  resolutions[
1098
- (caller.signature, f"{site.start_line}:{site.start_column}")
1101
+ (caller.signature, call_body_key(caller, site))
1099
1102
  ] = sig
1100
1103
 
1101
1104
  # Calls Jedi's extractor never recorded as sites at all (with-
@@ -1398,7 +1401,7 @@ def defuse_linker_edges(
1398
1401
  oracle.vote(sig, site)
1399
1402
  bump(caller.signature, sig)
1400
1403
  resolutions[
1401
- (caller.signature, f"{site.start_line}:{site.start_column}")
1404
+ (caller.signature, call_body_key(caller, site))
1402
1405
  ] = sig
1403
1406
  made_progress = True
1404
1407
  else:
@@ -1452,7 +1455,7 @@ def defuse_linker_edges(
1452
1455
  oracle.vote(sig, site)
1453
1456
  bump(caller.signature, sig)
1454
1457
  resolutions[
1455
- (caller.signature, f"{site.start_line}:{site.start_column}")
1458
+ (caller.signature, call_body_key(caller, site))
1456
1459
  ] = sig
1457
1460
  made_progress = True
1458
1461
  remaining = still
@@ -130,8 +130,9 @@ class SymbolTableBuilder:
130
130
  return None, False
131
131
 
132
132
  @staticmethod
133
- def _callee_anchor(node: ast.Call) -> Tuple[int, int]:
134
- """Position of the callee *name* for Jedi inference.
133
+ def _callee_anchor(node: ast.Call) -> Optional[Tuple[int, int]]:
134
+ """Position of the callee *name* for Jedi inference, or ``None`` when the
135
+ callee is not a name at all.
135
136
 
136
137
  An ``ast.Call``'s own ``lineno``/``col_offset`` is the first
137
138
  character of the whole call expression — for an attribute call
@@ -139,11 +140,17 @@ class SymbolTableBuilder:
139
140
  would infer the receiver's type instead of the invoked method
140
141
  (issue #80). Anchor attribute calls inside the attribute name —
141
142
  its last character, so one-character names stay in range; other
142
- callee shapes keep the call-expression start.
143
+ callee shapes keep the call-expression start. A callee that is itself a
144
+ call has no name to anchor on, so it yields ``None`` (#215).
143
145
  """
144
146
  func_expr = node.func
145
147
  if isinstance(func_expr, ast.Attribute):
146
148
  return func_expr.end_lineno, func_expr.end_col_offset - 1
149
+ if isinstance(func_expr, ast.Call):
150
+ # `getattr(o, n)(x)`: the callee IS a call, so the expression start is
151
+ # the INNER call's name and inferring there labels this site a call to
152
+ # `getattr` -- the thing that produced the callee, not the callee (#215).
153
+ return None
147
154
  return node.lineno, node.col_offset
148
155
 
149
156
  @staticmethod
@@ -766,11 +773,18 @@ class SymbolTableBuilder:
766
773
  func_expr = node.func
767
774
 
768
775
  method_name = "<unknown>"
769
- anchor_line, anchor_col = self._callee_anchor(node)
770
- callee_signature, is_constructor = self._infer_callee(
771
- script, anchor_line, anchor_col
772
- )
773
- return_type = self._infer_call_return_type(script, anchor_line, anchor_col)
776
+ anchor = self._callee_anchor(node)
777
+ if anchor is None:
778
+ # A dynamic invocation: the site is recorded, and it resolves to
779
+ # nothing. Guessing a target here is what produced a graph full of
780
+ # calls to `builtins.getattr` (#215).
781
+ callee_signature, is_constructor, return_type = None, False, None
782
+ else:
783
+ anchor_line, anchor_col = anchor
784
+ callee_signature, is_constructor = self._infer_callee(
785
+ script, anchor_line, anchor_col
786
+ )
787
+ return_type = self._infer_call_return_type(script, anchor_line, anchor_col)
774
788
 
775
789
  receiver_expr = None
776
790
  receiver_type = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeanalyzer-python
3
- Version: 1.5.1
3
+ Version: 1.5.3
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
@@ -1,6 +1,6 @@
1
1
  codeanalyzer/__init__.py,sha256=BZ3Kuwl-F_F-8H8cepLnVJ4Ku4NNUjjqg0Y6ujPQSsI,108
2
- codeanalyzer/__main__.py,sha256=o5_9ct61l3T7ryIsag8bz304NabVV-el_ooGD8tnbhA,15968
3
- codeanalyzer/core.py,sha256=JfSsQEacBiVn38tQP34QWdJqQx7ecE70EXI8obISZN4,46598
2
+ codeanalyzer/__main__.py,sha256=noiZpfaiyFlnmdzHoWOAImXpxDkiQzTxSO4gmyZVyGI,15981
3
+ codeanalyzer/core.py,sha256=UoK-7xgzJx-f3A_fmmeEZkBshiOWNDwc_RJTPhCTU0Y,47655
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
@@ -8,12 +8,12 @@ codeanalyzer/artifacts/config_keys.py,sha256=ovwptAErLYutlCzUwvyim2kH6-6vUKxuRcB
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
- codeanalyzer/artifacts/discovery.py,sha256=cUVRSdV82G2hVGhkRGV7u24-tEAxUBmH9nkTg24ur0Y,5968
11
+ codeanalyzer/artifacts/discovery.py,sha256=urN4ZvtnXs0w9Kv2-yeOQza1TV1mjLL007zHfbdjNSg,8416
12
12
  codeanalyzer/artifacts/parsers.py,sha256=xOjC53tT0Mv0k7ONGwzl5XUvpBLnZlEcGmyrX5DS45k,9035
13
13
  codeanalyzer/dataflow/__init__.py,sha256=HCNOH24rAGLUMG94N9aC-2SsAWSntk3uys7e39ipU5o,1725
14
14
  codeanalyzer/dataflow/access_paths.py,sha256=wC8Q9qD-RZzkoFWMVvu_6uNNmYP8z48OGp9h9v3F1d4,23623
15
15
  codeanalyzer/dataflow/alias.py,sha256=ZYKY0DiR3GHv6YJr7C001Lode9rZsk6-gxdomUyIvwg,3928
16
- codeanalyzer/dataflow/builder.py,sha256=grzgpl82KzNiwlgdTx60INMxkTZvddT3JzgaBRO4xh4,32839
16
+ codeanalyzer/dataflow/builder.py,sha256=Ku16XuEJj46lnfuigMMyR6yheRW52f5U-ih9msxqSyo,33402
17
17
  codeanalyzer/dataflow/cfg.py,sha256=u5YXJjAaDshdgv-9kWWITbBJFP-MwXKq6hbNh0fhDm8,25307
18
18
  codeanalyzer/dataflow/defuse.py,sha256=LrX1ToOZzR79NlAGg5T647UAFkpiEqEnT7t0K5RXOiI,4377
19
19
  codeanalyzer/dataflow/dominance.py,sha256=X7Ki1QRdVqaseYsJtiUL7m9MbcC2WHEv9b8bczSLFUA,5086
@@ -47,32 +47,32 @@ 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
49
  codeanalyzer/neo4j/cypher.py,sha256=-wledvXVrcLJRtSC5MCn1N3tqgC06rNfPBn0w6POqHo,6239
50
- codeanalyzer/neo4j/emit.py,sha256=OBCO77TDTaNpqk9Di3JIQzBouLpHPdgoXuPSs44iJuU,3587
51
- codeanalyzer/neo4j/project.py,sha256=wKvX7C1vamGswzoBRX09BZ1hOm1NJWoSdtQ7UNt2qTE,39102
50
+ codeanalyzer/neo4j/emit.py,sha256=7QYosqiadH2GIsjaUSmSlJ5g8yLjAAcMplsmFaJLnC4,3599
51
+ codeanalyzer/neo4j/project.py,sha256=o6jdjwvHRxpNPNn-DXpOzebFq-fXgxoXTrKZ4_2ZVbQ,42383
52
52
  codeanalyzer/neo4j/rows.py,sha256=n_J3NVIusLPsHMjPCWaYvlL5sFm6V-kl_Z6B_BTH8Fk,10140
53
- codeanalyzer/neo4j/schema.py,sha256=ZRjBxpagBsQrPbD5BbeAZlj3ilTeYe4AsquztRDaH4M,14735
54
- codeanalyzer/options/__init__.py,sha256=6NewN0a-1HAEgKcxQjYyVn2WEDLrhax8h3WLgZddeqI,94
55
- codeanalyzer/options/options.py,sha256=2jDCcs74iSEOhz90LoRGts6PC8yV9IDPVxnLPNmonOA,1609
53
+ codeanalyzer/neo4j/schema.py,sha256=0qZ1qJgmutlMHyIjdfD5N9QtA-gwl8Qcgh4NGguQEMs,18089
54
+ codeanalyzer/options/__init__.py,sha256=HvKzVXBI91lBM941JSFN7npG4S2fHlVEKo9VC2lRso8,156
55
+ codeanalyzer/options/options.py,sha256=DAo1i08D0K16jCivTrdOcBgKRPO50SngFZTPAL_kpmY,1862
56
56
  codeanalyzer/schema/__init__.py,sha256=hIyz02abUJNPW8yUgguaeKflZjTmOZWbDiKICNBD7yk,4141
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
- codeanalyzer/schema/ids.py,sha256=YuioNK3Y-M2NcCBiGH4AB4rDezlqu6ycNO0R3O-Jxho,4388
60
- codeanalyzer/schema/l1_body.py,sha256=_sca0mTkMRb-5lasep87faGjDxZJd9jGB0hmBx3uCgk,1757
61
- codeanalyzer/schema/l2_callees.py,sha256=CTcBeGoO04DstDC6h-1sHMbbIrHDEOMxP01U9VLBcfc,2327
59
+ codeanalyzer/schema/ids.py,sha256=eShvV3R6oQt1lCQ2DFAhWekSWom63wQJDfeWXJ7KovQ,5986
60
+ codeanalyzer/schema/l1_body.py,sha256=-rBXZH1nhaCyQ2DVN-iIswuuSQ7Jsa8voSAaljv_c80,1737
61
+ codeanalyzer/schema/l2_callees.py,sha256=UGubBSLA_-GCK9Kak2rqkmoFlGifiImVnPDrypjHfrA,2441
62
62
  codeanalyzer/schema/py_schema.py,sha256=4xJto32nWqHx0dkge4qHsV2JZSjbyv4ojnqpyBm78sQ,24588
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
- codeanalyzer/semantic_analysis/defuse_linker.py,sha256=bSn1rCun28OQqSd2NOc9aVBSm47Gt9Iul4r_gwfqO1w,64930
65
+ codeanalyzer/semantic_analysis/defuse_linker.py,sha256=3n1ew94eyBsSI98cFqP0uMHFUv7ob7QR9DL2hTKMHsQ,65099
66
66
  codeanalyzer/syntactic_analysis/__init__.py,sha256=EUQkJEh6wHjWx2qTTKbTbUgwSbfKeNieKHNy7RknVXA,476
67
67
  codeanalyzer/syntactic_analysis/exceptions.py,sha256=whs_n0vIu655Jkk1a7iOoXY6iIca4pZqJnU40V9Ejaw,537
68
68
  codeanalyzer/syntactic_analysis/import_resolver.py,sha256=Q8noZwSdDNt4P4NMqDTB9FaS-M0_d4ASK9GnJop9MDI,2751
69
- codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=eTknBDlUuuQd3JEwbRtJt5pGVU89kOnzfmT4D5J_GKQ,47917
69
+ codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=tKed-VCSd_albMeZfUuPRCcKz_WEBic38DU21gM8o7g,48797
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.1.dist-info/METADATA,sha256=Ujo3QIsG8aBykzxm3Mjw_OLCD3b8oaiIrLP1wTvOVD8,44845
74
- codeanalyzer_python-1.5.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
75
- codeanalyzer_python-1.5.1.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
76
- codeanalyzer_python-1.5.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
- codeanalyzer_python-1.5.1.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
78
- codeanalyzer_python-1.5.1.dist-info/RECORD,,
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,,