codeanalyzer-python 1.5.1__py3-none-any.whl → 1.5.2__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.
@@ -50,7 +50,7 @@ from codeanalyzer.schema import (
50
50
  )
51
51
  from codeanalyzer.schema import model_dump
52
52
  from codeanalyzer.schema.ids import application_id, external_id, global_ordinal, purl_pypi
53
- from codeanalyzer.schema.py_schema import PyDecorator
53
+ from codeanalyzer.schema.py_schema import PyDecorator, byte_offsets
54
54
 
55
55
 
56
56
  def project(app: PyApplication, app_name: str, sig_to_id: dict,
@@ -182,6 +182,15 @@ def _project_program_graphs(
182
182
  if not c.id:
183
183
  continue # unstamped callable — assign_ids must run first
184
184
  owner = _sym(c.id) # the :PyCallable node, keyed by its can:// id
185
+ # ``callee_signature`` lives on ``PyCallable.call_sites``, not on the body
186
+ # node, so the graph joins the two on the call site's position (#203).
187
+ # ``argument_types`` is deliberately not joined: it is the legacy field #86
188
+ # split into ``PyCallArgument``, already carried as ``arguments_json``.
189
+ sig_by_pos = {
190
+ (cs.start_line, cs.start_column): cs.callee_signature
191
+ for cs in (c.call_sites or [])
192
+ if cs.callee_signature
193
+ }
185
194
  for local_key, node in (c.body or {}).items():
186
195
  span = node.span
187
196
  # L4 param vertices carry the variable they model (``of``) and
@@ -194,8 +203,11 @@ def _project_program_graphs(
194
203
  prune(
195
204
  {
196
205
  "kind": node.kind,
197
- "start_line": span.start[0] if span else None,
198
- "end_line": span.end[0] if span else None,
206
+ **_span_props(span),
207
+ "callee_signature": (
208
+ sig_by_pos.get((span.start[0], span.start[1]))
209
+ if span else None
210
+ ),
199
211
  "var": node.of,
200
212
  "call_node": node.parent,
201
213
  # Call-site detail (#120). The JSON emits one node per
@@ -346,8 +358,7 @@ def _project_artifacts(b: RowBuilder, app: PyApplication, app_name: str, app_ref
346
358
  "namespace": ck.namespace,
347
359
  "value": ck.value,
348
360
  "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,
361
+ **_span_props(ck.span),
351
362
  }
352
363
  ),
353
364
  )
@@ -569,7 +580,7 @@ def _project_module_body(
569
580
  _project_class(b, file_key, mod_ref, "PY_DECLARES", cl, externals, sig_to_id,
570
581
  mod.source, base_ref)
571
582
  for v in mod.variables or []:
572
- _project_variable(b, file_key, mod_ref, v)
583
+ _project_variable(b, file_key, mod_ref, v, mod.source or "")
573
584
  _project_imports(b, mod_ref, mod, module_id_by_key)
574
585
 
575
586
 
@@ -597,9 +608,15 @@ def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule,
597
608
  continue
598
609
  key = im.resolved_module or im.module
599
610
  a = agg.setdefault(
600
- key, {"spellings": set(), "names": set(), "aliases": set(), "resolved": im.resolved_module}
611
+ key,
612
+ {"spellings": set(), "names": set(), "aliases": set(), "positions": {},
613
+ "resolved": im.resolved_module},
601
614
  )
602
615
  a["spellings"].add(im.module)
616
+ # Keyed by spelling, never by index: ``spellings`` is emitted sorted, so a
617
+ # parallel position array has already lost its alignment (#203).
618
+ a["positions"][im.module] = [im.start_line, im.start_column,
619
+ im.end_line, im.end_column]
603
620
  if im.name:
604
621
  a["names"].add(im.name)
605
622
  if im.alias:
@@ -623,6 +640,7 @@ def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule,
623
640
  "spellings": sorted(a["spellings"]),
624
641
  "imported_names": sorted(a["names"]) or None,
625
642
  "aliases": sorted(a["aliases"]) or None,
643
+ "positions_json": json.dumps(a["positions"], sort_keys=True),
626
644
  }
627
645
  ),
628
646
  )
@@ -675,7 +693,7 @@ def _project_callable(
675
693
  _project_decorator(b, ref, d)
676
694
 
677
695
  for v in c.local_variables or []:
678
- _project_variable(b, file_key, ref, v)
696
+ _project_variable(b, file_key, ref, v, source)
679
697
  for ic in (c.callables or {}).values():
680
698
  _project_callable(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id,
681
699
  source, base_ref)
@@ -700,12 +718,14 @@ def _project_variable(
700
718
  file_key: str,
701
719
  owner: NodeRef,
702
720
  v: PyVariableDeclaration,
721
+ source: str,
703
722
  ) -> None:
704
723
  # ``<owner can:// id>/<name>@<line>`` (#173) — the owner is the module or the
705
724
  # callable, so a module-level variable sits under ``<module-id>/`` like every
706
725
  # other declaration and the module's prefix purge reaches it.
707
726
  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))
727
+ ref = b.node(["PyVariable"], "id", var_id,
728
+ _variable_props(v, var_id, file_key, source))
709
729
  b.edge("PY_DECLARES_VAR", owner, ref)
710
730
 
711
731
 
@@ -737,6 +757,7 @@ def _project_decorator(b: RowBuilder, on: NodeRef, decorator: PyDecorator) -> No
737
757
  "keyword_arguments_json": json.dumps(
738
758
  dict(decorator.keyword_arguments or {}), sort_keys=True
739
759
  ),
760
+ **_span_props(decorator.span),
740
761
  },
741
762
  )
742
763
 
@@ -752,6 +773,9 @@ def _module_props(mod: PyModule, file_key: str) -> Props:
752
773
  "id": mod.id,
753
774
  "file_key": file_key,
754
775
  "module_name": mod.module_name,
776
+ # Always present, never pruned: "" for an empty file, so a consumer can
777
+ # never confuse "not carried" with "empty" (#202).
778
+ "source": mod.source or "",
755
779
  "content_hash": mod.content_hash,
756
780
  "last_modified": mod.last_modified,
757
781
  "file_size": mod.file_size,
@@ -760,6 +784,36 @@ def _module_props(mod: PyModule, file_key: str) -> Props:
760
784
  )
761
785
 
762
786
 
787
+ def _span_props(span) -> Props:
788
+ """The six flattened span properties from a v2 ``Span`` (#202). Lines and columns
789
+ come straight off the model; ``bytes`` is already utf-8 and is what makes the span
790
+ sliceable out of ``:PyModule.source``."""
791
+ if span is None:
792
+ return {}
793
+ return {
794
+ "start_line": span.start[0], "start_column": span.start[1],
795
+ "end_line": span.end[0], "end_column": span.end[1],
796
+ "start_byte": span.bytes[0], "end_byte": span.bytes[1],
797
+ }
798
+
799
+
800
+ def _flat_span_props(source: str, start_line: int, start_column: int,
801
+ end_line: int, end_column: int) -> Props:
802
+ """The same six for a model carrying flat ast positions and no ``Span``
803
+ (:PyAttribute, :PyVariable). The byte pair is computed here rather than left
804
+ absent, so ``_SPAN`` means one thing on every label that spreads it (#202)."""
805
+ if start_line < 0 or end_line < 0 or not source:
806
+ return {}
807
+ start_column = max(start_column, 0)
808
+ end_column = max(end_column, 0)
809
+ lo, hi = byte_offsets(source, start_line, start_column, end_line, end_column)
810
+ return {
811
+ "start_line": start_line, "start_column": start_column,
812
+ "end_line": end_line, "end_column": end_column,
813
+ "start_byte": lo, "end_byte": hi,
814
+ }
815
+
816
+
763
817
  def _span_code(source: str, span) -> str | None:
764
818
  """A declaration's text: the owning module's ``source`` sliced by the node's
765
819
  utf-8 byte span. Schema v2 stores source once per module, so the graph's
@@ -781,8 +835,8 @@ def _class_props(cl: PyClass, file_key: str, source: str) -> Props:
781
835
  "base_classes": list(cl.base_classes or []),
782
836
  "decorators": [d.qualified_name or d.name for d in (cl.decorators or [])],
783
837
  "docstring": _docstring_of(cl.comments),
784
- "start_line": cl.start_line,
785
- "end_line": cl.end_line,
838
+ **(_span_props(cl.span) or {"start_line": cl.start_line,
839
+ "end_line": cl.end_line}),
786
840
  "_module": file_key,
787
841
  "is_entrypoint": bool(cl.entrypoints),
788
842
  "entrypoint_frameworks": sorted({e.framework for e in (cl.entrypoints or [])}),
@@ -801,8 +855,8 @@ def _callable_props(c: PyCallable, file_key: str, source: str) -> Props:
801
855
  "cyclomatic_complexity": c.cyclomatic_complexity,
802
856
  "code": _span_code(source, c.span),
803
857
  "code_start_line": c.code_start_line,
804
- "start_line": c.start_line,
805
- "end_line": c.end_line,
858
+ **(_span_props(c.span) or {"start_line": c.start_line,
859
+ "end_line": c.end_line}),
806
860
  "docstring": _docstring_of(c.comments),
807
861
  "decorators": [d.qualified_name or d.name for d in (c.decorators or [])],
808
862
  "modifiers": list(c.modifiers or []),
@@ -823,6 +877,8 @@ def _attribute_props(a: PyClassAttribute, attr_id: str, file_key: str) -> Props:
823
877
  "type": a.type,
824
878
  "initializer": a.initializer,
825
879
  "docstring": _docstring_of(a.comments),
880
+ # Line-only: PyClassAttribute has no columns, so there is nothing to
881
+ # derive bytes from. See the note on :PyAttribute in schema.py.
826
882
  "start_line": a.start_line,
827
883
  "end_line": a.end_line,
828
884
  "_module": file_key,
@@ -830,16 +886,21 @@ def _attribute_props(a: PyClassAttribute, attr_id: str, file_key: str) -> Props:
830
886
  )
831
887
 
832
888
 
833
- def _variable_props(v: PyVariableDeclaration, var_id: str, file_key: str) -> Props:
889
+ def _variable_props(v: PyVariableDeclaration, var_id: str, file_key: str,
890
+ source: str) -> Props:
834
891
  return prune(
835
892
  {
836
893
  "id": var_id,
837
894
  "name": v.name,
838
895
  "type": v.type,
839
896
  "initializer": v.initializer,
897
+ # ``value`` is Optional[Any] and a Neo4j property is a scalar or an array
898
+ # of scalars, so a dict/list value needs a serialization rather than a raw
899
+ # put -- always encoded, one shape for every value (#203).
900
+ "value_json": _stringify_if(v.value) if v.value is not None else None,
840
901
  "scope": v.scope,
841
- "start_line": v.start_line,
842
- "end_line": v.end_line,
902
+ **_flat_span_props(source, v.start_line, v.start_column,
903
+ v.end_line, v.end_column),
843
904
  "_module": file_key,
844
905
  }
845
906
  )
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeanalyzer-python
3
- Version: 1.5.1
3
+ Version: 1.5.2
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
@@ -48,9 +48,9 @@ codeanalyzer/neo4j/__init__.py,sha256=8Ei6uThTOqmul6GhnKoGOjLXJZbyDwF5Jo-8I8NxnA
48
48
  codeanalyzer/neo4j/bolt.py,sha256=8l70iGDjY3Kdm-W1sb3b3O1uqLjDrVPYTpXhnkXdZOY,13694
49
49
  codeanalyzer/neo4j/cypher.py,sha256=-wledvXVrcLJRtSC5MCn1N3tqgC06rNfPBn0w6POqHo,6239
50
50
  codeanalyzer/neo4j/emit.py,sha256=OBCO77TDTaNpqk9Di3JIQzBouLpHPdgoXuPSs44iJuU,3587
51
- codeanalyzer/neo4j/project.py,sha256=wKvX7C1vamGswzoBRX09BZ1hOm1NJWoSdtQ7UNt2qTE,39102
51
+ codeanalyzer/neo4j/project.py,sha256=gUAmIXLt0agWB1q1OS1Nzvlo1M6rqzzQymoy7lWxTps,42348
52
52
  codeanalyzer/neo4j/rows.py,sha256=n_J3NVIusLPsHMjPCWaYvlL5sFm6V-kl_Z6B_BTH8Fk,10140
53
- codeanalyzer/neo4j/schema.py,sha256=ZRjBxpagBsQrPbD5BbeAZlj3ilTeYe4AsquztRDaH4M,14735
53
+ codeanalyzer/neo4j/schema.py,sha256=0qZ1qJgmutlMHyIjdfD5N9QtA-gwl8Qcgh4NGguQEMs,18089
54
54
  codeanalyzer/options/__init__.py,sha256=6NewN0a-1HAEgKcxQjYyVn2WEDLrhax8h3WLgZddeqI,94
55
55
  codeanalyzer/options/options.py,sha256=2jDCcs74iSEOhz90LoRGts6PC8yV9IDPVxnLPNmonOA,1609
56
56
  codeanalyzer/schema/__init__.py,sha256=hIyz02abUJNPW8yUgguaeKflZjTmOZWbDiKICNBD7yk,4141
@@ -70,9 +70,9 @@ codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=eTknBDlUuuQd3JEwb
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.2.dist-info/METADATA,sha256=hivDuG8vX-aDD0NW-giMx8A4XXclXcFCtnFR5dYJF2g,44845
74
+ codeanalyzer_python-1.5.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
75
+ codeanalyzer_python-1.5.2.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
76
+ codeanalyzer_python-1.5.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
+ codeanalyzer_python-1.5.2.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
78
+ codeanalyzer_python-1.5.2.dist-info/RECORD,,