codeanalyzer-python 0.3.1__py3-none-any.whl → 1.0.1__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 +86 -4
- codeanalyzer/core.py +175 -82
- codeanalyzer/dataflow/__init__.py +35 -0
- codeanalyzer/dataflow/access_paths.py +563 -0
- codeanalyzer/dataflow/alias.py +93 -0
- codeanalyzer/dataflow/builder.py +688 -0
- codeanalyzer/dataflow/cfg.py +605 -0
- codeanalyzer/dataflow/defuse.py +113 -0
- codeanalyzer/dataflow/dominance.py +140 -0
- codeanalyzer/dataflow/identity.py +91 -0
- codeanalyzer/dataflow/pdg.py +100 -0
- codeanalyzer/dataflow/scalpel_oracle.py +269 -0
- codeanalyzer/dataflow/scc.py +91 -0
- codeanalyzer/dataflow/sdg.py +424 -0
- codeanalyzer/dataflow/slicing.py +93 -0
- codeanalyzer/dataflow/summaries.py +217 -0
- codeanalyzer/dataflow/syntactic.py +26 -0
- codeanalyzer/neo4j/bolt.py +19 -4
- codeanalyzer/neo4j/cypher.py +9 -3
- codeanalyzer/neo4j/emit.py +8 -3
- codeanalyzer/neo4j/project.py +241 -60
- codeanalyzer/neo4j/rows.py +18 -15
- codeanalyzer/neo4j/schema.py +43 -7
- codeanalyzer/options/options.py +4 -0
- codeanalyzer/schema/__init__.py +19 -0
- codeanalyzer/schema/assign_ids.py +37 -0
- codeanalyzer/schema/call_graph_ids.py +12 -0
- codeanalyzer/schema/ids.py +23 -0
- codeanalyzer/schema/l1_body.py +29 -0
- codeanalyzer/schema/l2_callees.py +36 -0
- codeanalyzer/schema/py_schema.py +141 -30
- codeanalyzer/semantic_analysis/call_graph.py +24 -27
- codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +77 -16
- codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +65 -27
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/METADATA +248 -61
- codeanalyzer_python-1.0.1.dist-info/RECORD +59 -0
- codeanalyzer_python-0.3.1.dist-info/RECORD +0 -39
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/WHEEL +0 -0
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -39,9 +39,12 @@ project-relative dotted paths so they align with the symbol table.
|
|
|
39
39
|
# re-enters PyCG's hook before its import graph is ready. Pre-importing
|
|
40
40
|
# these modules at import time ensures they're already in sys.modules when
|
|
41
41
|
# PyCG's hook is active, preventing the re-entrant ImportManagerError.
|
|
42
|
+
import fcntl
|
|
43
|
+
import hashlib
|
|
42
44
|
import importlib.metadata # noqa: F401
|
|
43
45
|
import importlib.util # noqa: F401
|
|
44
46
|
import contextlib
|
|
47
|
+
import os
|
|
45
48
|
import json # noqa: F401
|
|
46
49
|
import shutil
|
|
47
50
|
import signal
|
|
@@ -85,6 +88,15 @@ from codeanalyzer.semantic_analysis.pycg.shard_planner import plan_shards
|
|
|
85
88
|
from codeanalyzer.utils import ProgressBar, logger
|
|
86
89
|
|
|
87
90
|
|
|
91
|
+
def _shard_root_path(files: List[str], project_dir: Path) -> Path:
|
|
92
|
+
"""Content-derived mini-project root for a shard: same project + same file
|
|
93
|
+
set → same path on every run (determinism, issue #99)."""
|
|
94
|
+
digest = hashlib.sha1(
|
|
95
|
+
"\0".join([str(project_dir), *sorted(files)]).encode("utf-8")
|
|
96
|
+
).hexdigest()[:16]
|
|
97
|
+
return Path(tempfile.gettempdir()) / f"canpy_pycg_shard_{digest}"
|
|
98
|
+
|
|
99
|
+
|
|
88
100
|
def _materialize_shard_root(
|
|
89
101
|
files: List[str],
|
|
90
102
|
project_dir: Path,
|
|
@@ -103,10 +115,21 @@ def _materialize_shard_root(
|
|
|
103
115
|
|
|
104
116
|
The caller owns the returned *root* and must ``shutil.rmtree`` it.
|
|
105
117
|
"""
|
|
106
|
-
root
|
|
118
|
+
# Deterministic root: PyCG's capped fixpoint (--pycg-max-iter) is
|
|
119
|
+
# order-sensitive, and its internal state keys on absolute module paths —
|
|
120
|
+
# a random mkdtemp suffix changes those strings every run and shifts the
|
|
121
|
+
# iteration frontier, making the emitted edge set vary run-to-run
|
|
122
|
+
# (issue #99). Deriving the directory name from the shard's content keeps
|
|
123
|
+
# the path (and thus the analysis input) identical across runs. Callers
|
|
124
|
+
# that may run concurrently on the same shard serialize on the sidecar
|
|
125
|
+
# lock (see _shard_symlink_root).
|
|
126
|
+
root = _shard_root_path(files, project_dir)
|
|
127
|
+
if root.exists():
|
|
128
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
129
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
107
130
|
entry_points: List[str] = []
|
|
108
131
|
linked_inits: Set[Path] = set()
|
|
109
|
-
for f in files:
|
|
132
|
+
for f in sorted(files):
|
|
110
133
|
src = Path(f).resolve()
|
|
111
134
|
try:
|
|
112
135
|
rel = src.relative_to(project_dir)
|
|
@@ -142,12 +165,27 @@ def _shard_symlink_root(
|
|
|
142
165
|
"""Context-manager wrapper around :func:`_materialize_shard_root`.
|
|
143
166
|
|
|
144
167
|
Yields ``(root, entry_points)`` and removes the temp tree on exit.
|
|
168
|
+
|
|
169
|
+
The root path is content-derived (determinism, issue #99), so two
|
|
170
|
+
concurrent analyses of the same shard — e.g. a test suite and a manual
|
|
171
|
+
run on one project — would collide on it (one rmtree's the tree the
|
|
172
|
+
other is mid-analysis on). An exclusive flock on a sidecar lockfile
|
|
173
|
+
serializes them; distinct projects/shards hash to distinct roots and
|
|
174
|
+
never contend.
|
|
145
175
|
"""
|
|
146
|
-
|
|
176
|
+
digest_root = _shard_root_path(files, project_dir)
|
|
177
|
+
lock_path = digest_root.with_name(digest_root.name + ".lock")
|
|
178
|
+
lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR)
|
|
147
179
|
try:
|
|
148
|
-
|
|
180
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
181
|
+
root, entry_points = _materialize_shard_root(files, project_dir)
|
|
182
|
+
try:
|
|
183
|
+
yield root, entry_points
|
|
184
|
+
finally:
|
|
185
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
149
186
|
finally:
|
|
150
|
-
|
|
187
|
+
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
|
188
|
+
os.close(lock_fd)
|
|
151
189
|
|
|
152
190
|
|
|
153
191
|
def _pycg_shard_worker(
|
|
@@ -431,14 +469,14 @@ class PyCG:
|
|
|
431
469
|
"""Sum weights of duplicate ``(source, target)`` pairs across shards."""
|
|
432
470
|
merged: Dict[tuple, PyCallEdge] = {}
|
|
433
471
|
for edge in edges:
|
|
434
|
-
key = (edge.
|
|
472
|
+
key = (edge.src, edge.dst)
|
|
435
473
|
if key in merged:
|
|
436
474
|
existing = merged[key]
|
|
437
475
|
merged[key] = PyCallEdge(
|
|
438
476
|
source=existing.source,
|
|
439
477
|
target=existing.target,
|
|
440
478
|
weight=existing.weight + edge.weight,
|
|
441
|
-
|
|
479
|
+
prov=existing.prov,
|
|
442
480
|
)
|
|
443
481
|
else:
|
|
444
482
|
merged[key] = edge
|
|
@@ -469,7 +507,9 @@ class PyCG:
|
|
|
469
507
|
):
|
|
470
508
|
continue
|
|
471
509
|
paths.append(str(p))
|
|
472
|
-
|
|
510
|
+
# Sorted for run-to-run stability: rglob yields filesystem order, and
|
|
511
|
+
# PyCG's capped fixpoint is sensitive to entry-point order (issue #99).
|
|
512
|
+
return sorted(paths)
|
|
473
513
|
|
|
474
514
|
# ------------------------------------------------------------------
|
|
475
515
|
# Package-root helpers for sharding
|
|
@@ -564,7 +604,7 @@ class PyCG:
|
|
|
564
604
|
edge_counts[(resolver.resolve(src), resolver.resolve(dst))] += 1
|
|
565
605
|
|
|
566
606
|
return [
|
|
567
|
-
PyCallEdge(
|
|
607
|
+
PyCallEdge(src=src, dst=dst, weight=count, prov=["pycg"])
|
|
568
608
|
for (src, dst), count in edge_counts.items()
|
|
569
609
|
]
|
|
570
610
|
|
|
@@ -712,11 +752,14 @@ class PyCG:
|
|
|
712
752
|
"""
|
|
713
753
|
import os
|
|
714
754
|
import ray
|
|
755
|
+
from codeanalyzer.core import _ensure_ray
|
|
756
|
+
_ensure_ray()
|
|
715
757
|
|
|
716
758
|
os.environ.setdefault("RAY_IGNORE_UNHANDLED_ERRORS", "1")
|
|
717
759
|
remote_fn = ray.remote(_pycg_shard_worker)
|
|
718
760
|
|
|
719
761
|
roots: List[Path] = []
|
|
762
|
+
lock_fds: List[int] = []
|
|
720
763
|
futures: List[Any] = []
|
|
721
764
|
meta: Dict[Any, List[str]] = {} # ObjectRef -> shard file list
|
|
722
765
|
edges_all: List[PyCallEdge] = []
|
|
@@ -724,6 +767,16 @@ class PyCG:
|
|
|
724
767
|
try:
|
|
725
768
|
with ProgressBar(len(shards), "Building call graph shards (parallel)", item_label="shards") as progress:
|
|
726
769
|
for files in shards:
|
|
770
|
+
# Deterministic roots can collide across concurrent
|
|
771
|
+
# analyses of the same project — the driver holds each
|
|
772
|
+
# shard's sidecar lock for the whole Ray fan-out (released
|
|
773
|
+
# in the finally below with the root cleanup).
|
|
774
|
+
lock_fd = os.open(
|
|
775
|
+
str(_shard_root_path(files, self.project_dir).with_suffix(".lock")),
|
|
776
|
+
os.O_CREAT | os.O_RDWR,
|
|
777
|
+
)
|
|
778
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
779
|
+
lock_fds.append(lock_fd)
|
|
727
780
|
root, eps = _materialize_shard_root(files, self.project_dir)
|
|
728
781
|
roots.append(root)
|
|
729
782
|
fut = remote_fn.remote(eps, str(root), "", self.max_iter)
|
|
@@ -751,7 +804,7 @@ class PyCG:
|
|
|
751
804
|
try:
|
|
752
805
|
triples = ray.get(fut)
|
|
753
806
|
edges_all.extend(
|
|
754
|
-
PyCallEdge(
|
|
807
|
+
PyCallEdge(src=s, dst=t, weight=w, prov=["pycg"])
|
|
755
808
|
for s, t, w in triples
|
|
756
809
|
)
|
|
757
810
|
except Exception:
|
|
@@ -765,6 +818,12 @@ class PyCG:
|
|
|
765
818
|
finally:
|
|
766
819
|
for root in roots:
|
|
767
820
|
shutil.rmtree(root, ignore_errors=True)
|
|
821
|
+
for fd in lock_fds:
|
|
822
|
+
try:
|
|
823
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
824
|
+
os.close(fd)
|
|
825
|
+
except OSError:
|
|
826
|
+
pass
|
|
768
827
|
return edges_all, runaways
|
|
769
828
|
|
|
770
829
|
def _build_sharded(
|
|
@@ -841,14 +900,14 @@ class PyCG:
|
|
|
841
900
|
# Merge duplicate (source, target) pairs that appear in multiple shards.
|
|
842
901
|
merged: Dict[tuple, PyCallEdge] = {}
|
|
843
902
|
for edge in all_edges:
|
|
844
|
-
key = (edge.
|
|
903
|
+
key = (edge.src, edge.dst)
|
|
845
904
|
if key in merged:
|
|
846
905
|
existing = merged[key]
|
|
847
906
|
merged[key] = PyCallEdge(
|
|
848
907
|
source=existing.source,
|
|
849
908
|
target=existing.target,
|
|
850
909
|
weight=existing.weight + edge.weight,
|
|
851
|
-
|
|
910
|
+
prov=existing.prov,
|
|
852
911
|
)
|
|
853
912
|
else:
|
|
854
913
|
merged[key] = edge
|
|
@@ -870,6 +929,8 @@ class PyCG:
|
|
|
870
929
|
"""
|
|
871
930
|
import os
|
|
872
931
|
import ray
|
|
932
|
+
from codeanalyzer.core import _ensure_ray
|
|
933
|
+
_ensure_ray()
|
|
873
934
|
|
|
874
935
|
# force-cancel kills worker processes; suppress Ray's "worker died
|
|
875
936
|
# unexpectedly" noise since the death is intentional here.
|
|
@@ -923,7 +984,7 @@ class PyCG:
|
|
|
923
984
|
try:
|
|
924
985
|
triples = ray.get(fut)
|
|
925
986
|
edges = [
|
|
926
|
-
PyCallEdge(
|
|
987
|
+
PyCallEdge(src=s, dst=t, weight=w, prov=["pycg"])
|
|
927
988
|
for s, t, w in triples
|
|
928
989
|
]
|
|
929
990
|
all_edges.extend(edges)
|
|
@@ -956,14 +1017,14 @@ class PyCG:
|
|
|
956
1017
|
|
|
957
1018
|
merged: Dict[tuple, PyCallEdge] = {}
|
|
958
1019
|
for edge in all_edges:
|
|
959
|
-
key = (edge.
|
|
1020
|
+
key = (edge.src, edge.dst)
|
|
960
1021
|
if key in merged:
|
|
961
1022
|
existing = merged[key]
|
|
962
1023
|
merged[key] = PyCallEdge(
|
|
963
1024
|
source=existing.source,
|
|
964
1025
|
target=existing.target,
|
|
965
1026
|
weight=existing.weight + edge.weight,
|
|
966
|
-
|
|
1027
|
+
prov=existing.prov,
|
|
967
1028
|
)
|
|
968
1029
|
else:
|
|
969
1030
|
merged[key] = edge
|
|
@@ -984,7 +1045,7 @@ class PyCG:
|
|
|
984
1045
|
symbol_table: Dict[str, PyModule],
|
|
985
1046
|
jedi_edges: Optional[List[PyCallEdge]] = None,
|
|
986
1047
|
) -> List[PyCallEdge]:
|
|
987
|
-
"""Run PyCG and return ``PyCallEdge`` entries with ``
|
|
1048
|
+
"""Run PyCG and return ``PyCallEdge`` entries with ``prov=["pycg"]``.
|
|
988
1049
|
|
|
989
1050
|
Edges are coalesced on ``(source, target)`` — ``weight`` equals the
|
|
990
1051
|
number of times PyCG reports the same (caller, callee) pair (always 1
|
|
@@ -66,17 +66,17 @@ logger = logging.getLogger(__name__)
|
|
|
66
66
|
|
|
67
67
|
def _walk_callable_sigs(c: PyCallable) -> Iterator[str]:
|
|
68
68
|
yield c.signature
|
|
69
|
-
for inner in c.
|
|
69
|
+
for inner in c.callables.values():
|
|
70
70
|
yield from _walk_callable_sigs(inner)
|
|
71
|
-
for inner_cls in c.
|
|
71
|
+
for inner_cls in c.types.values():
|
|
72
72
|
yield from _walk_class_sigs(inner_cls)
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def _walk_class_sigs(cls: PyClass) -> Iterator[str]:
|
|
76
76
|
yield cls.signature
|
|
77
|
-
for method in cls.
|
|
77
|
+
for method in cls.callables.values():
|
|
78
78
|
yield from _walk_callable_sigs(method)
|
|
79
|
-
for inner in cls.
|
|
79
|
+
for inner in cls.types.values():
|
|
80
80
|
yield from _walk_class_sigs(inner)
|
|
81
81
|
|
|
82
82
|
|
|
@@ -95,7 +95,7 @@ def _signature_to_file(symbol_table: Dict[str, PyModule]) -> Dict[str, str]:
|
|
|
95
95
|
for fn in module.functions.values():
|
|
96
96
|
for sig in _walk_callable_sigs(fn):
|
|
97
97
|
sig_to_file[sig] = module.file_path
|
|
98
|
-
for cls in module.
|
|
98
|
+
for cls in module.types.values():
|
|
99
99
|
for sig in _walk_class_sigs(cls):
|
|
100
100
|
sig_to_file[sig] = module.file_path
|
|
101
101
|
return sig_to_file
|
|
@@ -152,8 +152,8 @@ def build_module_graph(
|
|
|
152
152
|
g.add_node(module.file_path, module_name=module.module_name)
|
|
153
153
|
|
|
154
154
|
for edge in jedi_edges:
|
|
155
|
-
src = sig_to_file.get(edge.
|
|
156
|
-
dst = sig_to_file.get(edge.
|
|
155
|
+
src = sig_to_file.get(edge.src)
|
|
156
|
+
dst = sig_to_file.get(edge.dst)
|
|
157
157
|
if src is None or dst is None or src == dst:
|
|
158
158
|
continue
|
|
159
159
|
if g.has_edge(src, dst):
|
|
@@ -23,6 +23,8 @@ from codeanalyzer.schema.py_schema import (
|
|
|
23
23
|
PyModule,
|
|
24
24
|
PySymbol,
|
|
25
25
|
PyVariableDeclaration,
|
|
26
|
+
Span,
|
|
27
|
+
byte_offsets,
|
|
26
28
|
)
|
|
27
29
|
|
|
28
30
|
|
|
@@ -55,13 +57,26 @@ class SymbolTableBuilder:
|
|
|
55
57
|
relative = Path(script_path).relative_to(self.project_dir)
|
|
56
58
|
return ".".join(relative.with_suffix("").parts) + f".{name}"
|
|
57
59
|
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _first_definition(definitions):
|
|
62
|
+
"""Deterministic pick from Jedi's inference candidates.
|
|
63
|
+
|
|
64
|
+
On a union-typed receiver Jedi may return several candidates whose
|
|
65
|
+
ORDER varies run to run (issue #99 — e.g. ``o.seek`` on
|
|
66
|
+
``_IOBase | BufferedRandom | TextIOWrapper``); taking whichever came
|
|
67
|
+
first made the emitted call graph nondeterministic. Sort on the
|
|
68
|
+
stable identity (full_name, then name) and take the smallest."""
|
|
69
|
+
if not definitions:
|
|
70
|
+
return None
|
|
71
|
+
return min(definitions, key=lambda d: (d.full_name or "", d.name or ""))
|
|
72
|
+
|
|
58
73
|
@staticmethod
|
|
59
74
|
def _infer_type(script: Script, line: int, column: int) -> str:
|
|
60
75
|
"""Tries to infer the type at a given position using Jedi."""
|
|
61
76
|
try:
|
|
62
|
-
|
|
63
|
-
if
|
|
64
|
-
return
|
|
77
|
+
d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
|
|
78
|
+
if d is not None:
|
|
79
|
+
return d.name # or .full_name
|
|
65
80
|
except Exception:
|
|
66
81
|
pass
|
|
67
82
|
return None
|
|
@@ -80,9 +95,9 @@ class SymbolTableBuilder:
|
|
|
80
95
|
Optional[str]: The fully qualified name if available, else None.
|
|
81
96
|
"""
|
|
82
97
|
try:
|
|
83
|
-
|
|
84
|
-
if
|
|
85
|
-
return
|
|
98
|
+
d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
|
|
99
|
+
if d is not None:
|
|
100
|
+
return d.full_name
|
|
86
101
|
except Exception:
|
|
87
102
|
pass
|
|
88
103
|
return None
|
|
@@ -101,10 +116,9 @@ class SymbolTableBuilder:
|
|
|
101
116
|
the call graph.
|
|
102
117
|
"""
|
|
103
118
|
try:
|
|
104
|
-
|
|
105
|
-
if
|
|
119
|
+
d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
|
|
120
|
+
if d is None:
|
|
106
121
|
return None, False
|
|
107
|
-
d = definitions[0]
|
|
108
122
|
is_class = (d.type == "class")
|
|
109
123
|
full = d.full_name
|
|
110
124
|
if is_class and full:
|
|
@@ -142,11 +156,17 @@ class SymbolTableBuilder:
|
|
|
142
156
|
as the callee's own name.
|
|
143
157
|
"""
|
|
144
158
|
try:
|
|
145
|
-
|
|
146
|
-
if
|
|
147
|
-
results
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
|
|
160
|
+
if d is not None:
|
|
161
|
+
# Drop NoneType results before picking: Jedi flaps between
|
|
162
|
+
# yielding {NoneType} and {} for the None arm of an Optional
|
|
163
|
+
# return (issue #99), and when a real type is also in the
|
|
164
|
+
# union it is the informative choice — a bare NoneType says
|
|
165
|
+
# nothing a missing return_type doesn't.
|
|
166
|
+
results = [r for r in d.execute() if r.name != "NoneType"]
|
|
167
|
+
r = SymbolTableBuilder._first_definition(results)
|
|
168
|
+
if r is not None:
|
|
169
|
+
return r.name
|
|
150
170
|
except Exception:
|
|
151
171
|
pass
|
|
152
172
|
return None
|
|
@@ -177,11 +197,12 @@ class SymbolTableBuilder:
|
|
|
177
197
|
PyModule.builder()
|
|
178
198
|
.file_path(str(py_file))
|
|
179
199
|
.module_name(py_file.stem)
|
|
200
|
+
.source(source)
|
|
180
201
|
.comments(self._pycomments(module, source))
|
|
181
202
|
.imports(self._imports(module))
|
|
182
203
|
.variables(self._module_variables(module, script))
|
|
183
|
-
.
|
|
184
|
-
.functions(self._callables(module, script))
|
|
204
|
+
.types(self._add_class(module, script, source))
|
|
205
|
+
.functions(self._callables(module, script, source))
|
|
185
206
|
.content_hash(content_hash)
|
|
186
207
|
.last_modified(last_modified)
|
|
187
208
|
.file_size(file_size)
|
|
@@ -237,7 +258,7 @@ class SymbolTableBuilder:
|
|
|
237
258
|
|
|
238
259
|
return imports
|
|
239
260
|
|
|
240
|
-
def _add_class(self, node: AST, script: Script, prefix: str = "") -> Dict[str, PyClass]:
|
|
261
|
+
def _add_class(self, node: AST, script: Script, source: str, prefix: str = "") -> Dict[str, PyClass]:
|
|
241
262
|
classes: Dict[str, PyClass] = {}
|
|
242
263
|
|
|
243
264
|
for child in ast.iter_child_nodes(node):
|
|
@@ -248,6 +269,14 @@ class SymbolTableBuilder:
|
|
|
248
269
|
start_line = child.lineno
|
|
249
270
|
end_line = getattr(child, "end_lineno", start_line + len(child.body))
|
|
250
271
|
code = ast.unparse(child).strip()
|
|
272
|
+
span = Span(
|
|
273
|
+
start=(child.lineno, child.col_offset),
|
|
274
|
+
end=(getattr(child, "end_lineno", child.lineno),
|
|
275
|
+
getattr(child, "end_col_offset", child.col_offset)),
|
|
276
|
+
bytes=byte_offsets(source, child.lineno, child.col_offset,
|
|
277
|
+
getattr(child, "end_lineno", child.lineno),
|
|
278
|
+
getattr(child, "end_col_offset", child.col_offset)),
|
|
279
|
+
)
|
|
251
280
|
|
|
252
281
|
# Try resolving full signature with Jedi
|
|
253
282
|
if prefix:
|
|
@@ -265,18 +294,18 @@ class SymbolTableBuilder:
|
|
|
265
294
|
PyClass.builder()
|
|
266
295
|
.name(class_name)
|
|
267
296
|
.signature(signature)
|
|
297
|
+
.span(span)
|
|
268
298
|
.start_line(start_line)
|
|
269
299
|
.end_line(end_line)
|
|
270
|
-
.code(code)
|
|
271
300
|
.comments(self._pycomments(child, code))
|
|
272
301
|
.base_classes([
|
|
273
302
|
ast.unparse(base)
|
|
274
303
|
for base in child.bases
|
|
275
304
|
if isinstance(base, ast.expr)
|
|
276
305
|
])
|
|
277
|
-
.
|
|
306
|
+
.callables(self._callables(child, script, source, prefix=signature)) # Pass class signature as prefix
|
|
278
307
|
.attributes(self._class_attributes(child, script))
|
|
279
|
-
.
|
|
308
|
+
.types(self._add_class(child, script, source, prefix=signature)) # Pass class signature as prefix
|
|
280
309
|
.build()
|
|
281
310
|
)
|
|
282
311
|
|
|
@@ -285,7 +314,7 @@ class SymbolTableBuilder:
|
|
|
285
314
|
return classes
|
|
286
315
|
|
|
287
316
|
|
|
288
|
-
def _callables(self, node: AST, script: Script, prefix: str = "") -> Dict[str, PyCallable]:
|
|
317
|
+
def _callables(self, node: AST, script: Script, source: str, prefix: str = "") -> Dict[str, PyCallable]:
|
|
289
318
|
callables: Dict[str, PyCallable] = {}
|
|
290
319
|
|
|
291
320
|
for child in ast.iter_child_nodes(node):
|
|
@@ -294,6 +323,14 @@ class SymbolTableBuilder:
|
|
|
294
323
|
start_line = child.lineno
|
|
295
324
|
end_line = getattr(child, "end_lineno", start_line + len(child.body))
|
|
296
325
|
code = ast.unparse(child).strip()
|
|
326
|
+
span = Span(
|
|
327
|
+
start=(child.lineno, child.col_offset),
|
|
328
|
+
end=(getattr(child, "end_lineno", child.lineno),
|
|
329
|
+
getattr(child, "end_col_offset", child.col_offset)),
|
|
330
|
+
bytes=byte_offsets(source, child.lineno, child.col_offset,
|
|
331
|
+
getattr(child, "end_lineno", child.lineno),
|
|
332
|
+
getattr(child, "end_col_offset", child.col_offset)),
|
|
333
|
+
)
|
|
297
334
|
decorators = [ast.unparse(d) for d in child.decorator_list]
|
|
298
335
|
|
|
299
336
|
if prefix:
|
|
@@ -318,8 +355,8 @@ class SymbolTableBuilder:
|
|
|
318
355
|
.name(method_name) # Use the actual method name, not the full signature
|
|
319
356
|
.path(str(script.path))
|
|
320
357
|
.signature(signature) # Use the full signature here
|
|
358
|
+
.span(span)
|
|
321
359
|
.decorators(decorators)
|
|
322
|
-
.code(code)
|
|
323
360
|
.start_line(start_line)
|
|
324
361
|
.end_line(end_line)
|
|
325
362
|
.code_start_line(child.body[0].lineno if child.body else start_line)
|
|
@@ -333,8 +370,8 @@ class SymbolTableBuilder:
|
|
|
333
370
|
if child.returns else self._infer_type(script, child.lineno, child.col_offset)
|
|
334
371
|
)
|
|
335
372
|
.comments(self._pycomments(child, code))
|
|
336
|
-
.
|
|
337
|
-
.
|
|
373
|
+
.callables(self._callables(child, script, source, signature)) # Pass current signature as prefix
|
|
374
|
+
.types(self._add_class(child, script, source, signature)) # Pass current signature as prefix
|
|
338
375
|
.build()
|
|
339
376
|
)
|
|
340
377
|
|
|
@@ -955,9 +992,10 @@ class SymbolTableBuilder:
|
|
|
955
992
|
|
|
956
993
|
if script:
|
|
957
994
|
try:
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
995
|
+
d = SymbolTableBuilder._first_definition(
|
|
996
|
+
script.infer(line=lineno, column=col_offset)
|
|
997
|
+
)
|
|
998
|
+
if d is not None:
|
|
961
999
|
inferred_type = d.name
|
|
962
1000
|
qname = d.full_name
|
|
963
1001
|
if d.type == "function":
|