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.
Files changed (42) hide show
  1. codeanalyzer/__main__.py +86 -4
  2. codeanalyzer/core.py +175 -82
  3. codeanalyzer/dataflow/__init__.py +35 -0
  4. codeanalyzer/dataflow/access_paths.py +563 -0
  5. codeanalyzer/dataflow/alias.py +93 -0
  6. codeanalyzer/dataflow/builder.py +688 -0
  7. codeanalyzer/dataflow/cfg.py +605 -0
  8. codeanalyzer/dataflow/defuse.py +113 -0
  9. codeanalyzer/dataflow/dominance.py +140 -0
  10. codeanalyzer/dataflow/identity.py +91 -0
  11. codeanalyzer/dataflow/pdg.py +100 -0
  12. codeanalyzer/dataflow/scalpel_oracle.py +269 -0
  13. codeanalyzer/dataflow/scc.py +91 -0
  14. codeanalyzer/dataflow/sdg.py +424 -0
  15. codeanalyzer/dataflow/slicing.py +93 -0
  16. codeanalyzer/dataflow/summaries.py +217 -0
  17. codeanalyzer/dataflow/syntactic.py +26 -0
  18. codeanalyzer/neo4j/bolt.py +19 -4
  19. codeanalyzer/neo4j/cypher.py +9 -3
  20. codeanalyzer/neo4j/emit.py +8 -3
  21. codeanalyzer/neo4j/project.py +241 -60
  22. codeanalyzer/neo4j/rows.py +18 -15
  23. codeanalyzer/neo4j/schema.py +43 -7
  24. codeanalyzer/options/options.py +4 -0
  25. codeanalyzer/schema/__init__.py +19 -0
  26. codeanalyzer/schema/assign_ids.py +37 -0
  27. codeanalyzer/schema/call_graph_ids.py +12 -0
  28. codeanalyzer/schema/ids.py +23 -0
  29. codeanalyzer/schema/l1_body.py +29 -0
  30. codeanalyzer/schema/l2_callees.py +36 -0
  31. codeanalyzer/schema/py_schema.py +141 -30
  32. codeanalyzer/semantic_analysis/call_graph.py +24 -27
  33. codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +77 -16
  34. codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
  35. codeanalyzer/syntactic_analysis/symbol_table_builder.py +65 -27
  36. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/METADATA +248 -61
  37. codeanalyzer_python-1.0.1.dist-info/RECORD +59 -0
  38. codeanalyzer_python-0.3.1.dist-info/RECORD +0 -39
  39. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/WHEEL +0 -0
  40. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/entry_points.txt +0 -0
  41. {codeanalyzer_python-0.3.1.dist-info → codeanalyzer_python-1.0.1.dist-info}/licenses/LICENSE +0 -0
  42. {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 = Path(tempfile.mkdtemp(prefix="canpy_pycg_shard_"))
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
- root, entry_points = _materialize_shard_root(files, project_dir)
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
- yield root, entry_points
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
- shutil.rmtree(root, ignore_errors=True)
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.source, edge.target)
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
- provenance=existing.provenance,
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
- return paths
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(source=src, target=dst, weight=count, provenance=["pycg"])
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(source=s, target=t, weight=w, provenance=["pycg"])
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.source, edge.target)
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
- provenance=existing.provenance,
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(source=s, target=t, weight=w, provenance=["pycg"])
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.source, edge.target)
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
- provenance=existing.provenance,
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 ``provenance=["pycg"]``.
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.inner_callables.values():
69
+ for inner in c.callables.values():
70
70
  yield from _walk_callable_sigs(inner)
71
- for inner_cls in c.inner_classes.values():
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.methods.values():
77
+ for method in cls.callables.values():
78
78
  yield from _walk_callable_sigs(method)
79
- for inner in cls.inner_classes.values():
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.classes.values():
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.source)
156
- dst = sig_to_file.get(edge.target)
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
- inference = script.infer(line=line, column=column)
63
- if inference:
64
- return inference[0].name # or .full_name
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
- definitions = script.infer(line=line, column=column)
84
- if definitions:
85
- return definitions[0].full_name
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
- definitions = script.infer(line=line, column=column)
105
- if not definitions:
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
- definitions = script.infer(line=line, column=column)
146
- if definitions:
147
- results = definitions[0].execute()
148
- if results:
149
- return results[0].name
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
- .classes(self._add_class(module, script))
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
- .methods(self._callables(child, script, prefix=signature)) # Pass class signature as prefix
306
+ .callables(self._callables(child, script, source, prefix=signature)) # Pass class signature as prefix
278
307
  .attributes(self._class_attributes(child, script))
279
- .inner_classes(self._add_class(child, script, prefix=signature)) # Pass class signature as prefix
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
- .inner_callables(self._callables(child, script, signature)) # Pass current signature as prefix
337
- .inner_classes(self._add_class(child, script, signature)) # Pass current signature as prefix
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
- definitions = script.infer(line=lineno, column=col_offset)
959
- if definitions:
960
- d = definitions[0]
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":