codeanalyzer-python 0.3.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. codeanalyzer/__main__.py +77 -4
  2. codeanalyzer/core.py +174 -72
  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/__init__.py +1 -1
  19. codeanalyzer/neo4j/bolt.py +19 -4
  20. codeanalyzer/neo4j/cypher.py +9 -3
  21. codeanalyzer/neo4j/emit.py +10 -5
  22. codeanalyzer/neo4j/project.py +307 -60
  23. codeanalyzer/neo4j/rows.py +18 -15
  24. codeanalyzer/neo4j/schema.py +297 -15
  25. codeanalyzer/options/options.py +4 -0
  26. codeanalyzer/provenance.py +61 -0
  27. codeanalyzer/schema/__init__.py +19 -0
  28. codeanalyzer/schema/assign_ids.py +37 -0
  29. codeanalyzer/schema/call_graph_ids.py +12 -0
  30. codeanalyzer/schema/ids.py +23 -0
  31. codeanalyzer/schema/l1_body.py +29 -0
  32. codeanalyzer/schema/l2_callees.py +36 -0
  33. codeanalyzer/schema/py_schema.py +175 -26
  34. codeanalyzer/semantic_analysis/call_graph.py +24 -27
  35. codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
  36. codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
  37. codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
  38. codeanalyzer/syntactic_analysis/symbol_table_builder.py +103 -20
  39. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +233 -43
  40. codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
  41. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +1 -1
  42. codeanalyzer/neo4j/catalog.py +0 -245
  43. codeanalyzer_python-0.3.0.dist-info/RECORD +0 -38
  44. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
  45. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,5 +1,6 @@
1
1
  import ast
2
2
  import hashlib
3
+ import os
3
4
  import tokenize
4
5
  from ast import AST, ClassDef
5
6
  from io import StringIO
@@ -13,6 +14,7 @@ from jedi.api.project import Project
13
14
  from codeanalyzer.schema.py_schema import (
14
15
  PyCallable,
15
16
  PyCallableParameter,
17
+ PyCallArgument,
16
18
  PyCallsite,
17
19
  PyClass,
18
20
  PyClassAttribute,
@@ -21,6 +23,8 @@ from codeanalyzer.schema.py_schema import (
21
23
  PyModule,
22
24
  PySymbol,
23
25
  PyVariableDeclaration,
26
+ Span,
27
+ byte_offsets,
24
28
  )
25
29
 
26
30
 
@@ -28,7 +32,11 @@ class SymbolTableBuilder:
28
32
  """A class for building a symbol table for a Python project."""
29
33
 
30
34
  def __init__(self, project_dir: Union[Path, str], virtualenv: Union[Path, str, None]) -> None:
31
- self.project_dir = Path(project_dir)
35
+ # Jedi reports absolute Script paths, so a relative project_dir would
36
+ # crash every relative_to() fallback below. abspath (not resolve())
37
+ # keeps symlinks intact, matching Jedi's own absolute()-style
38
+ # normalization under symlinked roots like macOS /tmp.
39
+ self.project_dir = Path(os.path.abspath(project_dir))
32
40
  if virtualenv is None:
33
41
  # If no virtual environment is provided, create a jedi project without an environment.
34
42
  self.jedi_project: Project = jedi.Project(path=self.project_dir)
@@ -39,6 +47,16 @@ class SymbolTableBuilder:
39
47
  environment_path=Path(virtualenv) / "bin" / "python",
40
48
  )
41
49
 
50
+ def _fallback_signature(self, script_path: Union[Path, str], name: str) -> str:
51
+ """Path-derived qualified name used when Jedi can't name a definition.
52
+
53
+ Strips only the terminal ``.py`` suffix — a bare ``str.replace``
54
+ would also eat interior ``.py`` substrings and corrupt the module
55
+ prefix (``odoo/tools/pycompat.py`` → ``odoo.toolscompat``).
56
+ """
57
+ relative = Path(script_path).relative_to(self.project_dir)
58
+ return ".".join(relative.with_suffix("").parts) + f".{name}"
59
+
42
60
  @staticmethod
43
61
  def _infer_type(script: Script, line: int, column: int) -> str:
44
62
  """Tries to infer the type at a given position using Jedi."""
@@ -97,6 +115,44 @@ class SymbolTableBuilder:
97
115
  except Exception:
98
116
  return None, False
99
117
 
118
+ @staticmethod
119
+ def _callee_anchor(node: ast.Call) -> Tuple[int, int]:
120
+ """Position of the callee *name* for Jedi inference.
121
+
122
+ An ``ast.Call``'s own ``lineno``/``col_offset`` is the first
123
+ character of the whole call expression — for an attribute call
124
+ ``receiver.method(...)`` that is the receiver token, and Jedi
125
+ would infer the receiver's type instead of the invoked method
126
+ (issue #80). Anchor attribute calls inside the attribute name —
127
+ its last character, so one-character names stay in range; other
128
+ callee shapes keep the call-expression start.
129
+ """
130
+ func_expr = node.func
131
+ if isinstance(func_expr, ast.Attribute):
132
+ return func_expr.end_lineno, func_expr.end_col_offset - 1
133
+ return node.lineno, node.col_offset
134
+
135
+ @staticmethod
136
+ def _infer_call_return_type(script: Script, line: int, column: int) -> Optional[str]:
137
+ """Inferred type of the call's *result*, not of the callee itself.
138
+
139
+ ``Script.infer`` at the callee name yields the function/class
140
+ being called; executing that definition yields what the call
141
+ evaluates to — a function's inferred return type, or the
142
+ instance for a constructor call. Returns ``None`` when Jedi
143
+ can't tell, so an unknown stays absent instead of masquerading
144
+ as the callee's own name.
145
+ """
146
+ try:
147
+ definitions = script.infer(line=line, column=column)
148
+ if definitions:
149
+ results = definitions[0].execute()
150
+ if results:
151
+ return results[0].name
152
+ except Exception:
153
+ pass
154
+ return None
155
+
100
156
  def build_pymodule_from_file(self, py_file: Path) -> PyModule:
101
157
  """Builds a PyModule from a Python file.
102
158
 
@@ -123,11 +179,12 @@ class SymbolTableBuilder:
123
179
  PyModule.builder()
124
180
  .file_path(str(py_file))
125
181
  .module_name(py_file.stem)
182
+ .source(source)
126
183
  .comments(self._pycomments(module, source))
127
184
  .imports(self._imports(module))
128
185
  .variables(self._module_variables(module, script))
129
- .classes(self._add_class(module, script))
130
- .functions(self._callables(module, script))
186
+ .types(self._add_class(module, script, source))
187
+ .functions(self._callables(module, script, source))
131
188
  .content_hash(content_hash)
132
189
  .last_modified(last_modified)
133
190
  .file_size(file_size)
@@ -183,7 +240,7 @@ class SymbolTableBuilder:
183
240
 
184
241
  return imports
185
242
 
186
- def _add_class(self, node: AST, script: Script, prefix: str = "") -> Dict[str, PyClass]:
243
+ def _add_class(self, node: AST, script: Script, source: str, prefix: str = "") -> Dict[str, PyClass]:
187
244
  classes: Dict[str, PyClass] = {}
188
245
 
189
246
  for child in ast.iter_child_nodes(node):
@@ -194,6 +251,14 @@ class SymbolTableBuilder:
194
251
  start_line = child.lineno
195
252
  end_line = getattr(child, "end_lineno", start_line + len(child.body))
196
253
  code = ast.unparse(child).strip()
254
+ span = Span(
255
+ start=(child.lineno, child.col_offset),
256
+ end=(getattr(child, "end_lineno", child.lineno),
257
+ getattr(child, "end_col_offset", child.col_offset)),
258
+ bytes=byte_offsets(source, child.lineno, child.col_offset,
259
+ getattr(child, "end_lineno", child.lineno),
260
+ getattr(child, "end_col_offset", child.col_offset)),
261
+ )
197
262
 
198
263
  # Try resolving full signature with Jedi
199
264
  if prefix:
@@ -203,26 +268,26 @@ class SymbolTableBuilder:
203
268
  definitions = script.goto(line=start_line, column=child.col_offset)
204
269
  signature = next(
205
270
  (d.full_name for d in definitions if d.type == "class"),
206
- f"{Path(script.path).relative_to(self.project_dir).__str__().replace('/', '.').replace('.py', '')}.{class_name}"
271
+ self._fallback_signature(script.path, class_name),
207
272
  )
208
273
  except Exception:
209
- signature = f"{Path(script.path).relative_to(self.project_dir).__str__().replace('/', '.').replace('.py', '')}.{class_name}"
274
+ signature = self._fallback_signature(script.path, class_name)
210
275
  py_class = (
211
276
  PyClass.builder()
212
277
  .name(class_name)
213
278
  .signature(signature)
279
+ .span(span)
214
280
  .start_line(start_line)
215
281
  .end_line(end_line)
216
- .code(code)
217
282
  .comments(self._pycomments(child, code))
218
283
  .base_classes([
219
284
  ast.unparse(base)
220
285
  for base in child.bases
221
286
  if isinstance(base, ast.expr)
222
287
  ])
223
- .methods(self._callables(child, script, prefix=signature)) # Pass class signature as prefix
288
+ .callables(self._callables(child, script, source, prefix=signature)) # Pass class signature as prefix
224
289
  .attributes(self._class_attributes(child, script))
225
- .inner_classes(self._add_class(child, script, prefix=signature)) # Pass class signature as prefix
290
+ .types(self._add_class(child, script, source, prefix=signature)) # Pass class signature as prefix
226
291
  .build()
227
292
  )
228
293
 
@@ -231,7 +296,7 @@ class SymbolTableBuilder:
231
296
  return classes
232
297
 
233
298
 
234
- def _callables(self, node: AST, script: Script, prefix: str = "") -> Dict[str, PyCallable]:
299
+ def _callables(self, node: AST, script: Script, source: str, prefix: str = "") -> Dict[str, PyCallable]:
235
300
  callables: Dict[str, PyCallable] = {}
236
301
 
237
302
  for child in ast.iter_child_nodes(node):
@@ -240,6 +305,14 @@ class SymbolTableBuilder:
240
305
  start_line = child.lineno
241
306
  end_line = getattr(child, "end_lineno", start_line + len(child.body))
242
307
  code = ast.unparse(child).strip()
308
+ span = Span(
309
+ start=(child.lineno, child.col_offset),
310
+ end=(getattr(child, "end_lineno", child.lineno),
311
+ getattr(child, "end_col_offset", child.col_offset)),
312
+ bytes=byte_offsets(source, child.lineno, child.col_offset,
313
+ getattr(child, "end_lineno", child.lineno),
314
+ getattr(child, "end_col_offset", child.col_offset)),
315
+ )
243
316
  decorators = [ast.unparse(d) for d in child.decorator_list]
244
317
 
245
318
  if prefix:
@@ -258,15 +331,14 @@ class SymbolTableBuilder:
258
331
 
259
332
  # If Jedi didn't provide a signature, build one relative to project_dir
260
333
  if not signature:
261
- relative_path = Path(script.path).relative_to(self.project_dir)
262
- signature = f"{str(relative_path).replace('/', '.').replace('.py', '')}.{method_name}"
334
+ signature = self._fallback_signature(script.path, method_name)
263
335
  py_callable = (
264
336
  PyCallable.builder()
265
337
  .name(method_name) # Use the actual method name, not the full signature
266
338
  .path(str(script.path))
267
339
  .signature(signature) # Use the full signature here
340
+ .span(span)
268
341
  .decorators(decorators)
269
- .code(code)
270
342
  .start_line(start_line)
271
343
  .end_line(end_line)
272
344
  .code_start_line(child.body[0].lineno if child.body else start_line)
@@ -280,8 +352,8 @@ class SymbolTableBuilder:
280
352
  if child.returns else self._infer_type(script, child.lineno, child.col_offset)
281
353
  )
282
354
  .comments(self._pycomments(child, code))
283
- .inner_callables(self._callables(child, script, signature)) # Pass current signature as prefix
284
- .inner_classes(self._add_class(child, script, signature)) # Pass current signature as prefix
355
+ .callables(self._callables(child, script, source, signature)) # Pass current signature as prefix
356
+ .types(self._add_class(child, script, source, signature)) # Pass current signature as prefix
285
357
  .build()
286
358
  )
287
359
 
@@ -380,6 +452,7 @@ class SymbolTableBuilder:
380
452
  script, target.lineno, target.col_offset
381
453
  )
382
454
  )
455
+ .initializer(ast.unparse(stmt.value) if stmt.value else None)
383
456
  .start_line(getattr(target, "lineno", -1))
384
457
  .end_line(getattr(stmt, "end_lineno", stmt.lineno))
385
458
  .build()
@@ -398,6 +471,7 @@ class SymbolTableBuilder:
398
471
  script, target.lineno, target.col_offset
399
472
  )
400
473
  )
474
+ .initializer(ast.unparse(stmt.value) if stmt.value else None)
401
475
  .start_line(getattr(target, "lineno", -1))
402
476
  .end_line(getattr(stmt, "end_lineno", stmt.lineno))
403
477
  .build()
@@ -588,10 +662,11 @@ class SymbolTableBuilder:
588
662
  func_expr = node.func
589
663
 
590
664
  method_name = "<unknown>"
665
+ anchor_line, anchor_col = self._callee_anchor(node)
591
666
  callee_signature, is_constructor = self._infer_callee(
592
- script, node.lineno, node.col_offset
667
+ script, anchor_line, anchor_col
593
668
  )
594
- return_type = self._infer_type(script, node.lineno, node.col_offset)
669
+ return_type = self._infer_call_return_type(script, anchor_line, anchor_col)
595
670
 
596
671
  receiver_expr = None
597
672
  receiver_type = None
@@ -604,18 +679,26 @@ class SymbolTableBuilder:
604
679
  elif isinstance(func_expr, ast.Name):
605
680
  method_name = func_expr.id
606
681
 
607
- argument_types = [
608
- self._infer_type(script, arg.lineno, arg.col_offset)
609
- or type(arg).__name__
682
+ arguments = [
683
+ PyCallArgument(
684
+ ast_kind=type(arg).__name__,
685
+ inferred_type=self._infer_type(script, arg.lineno, arg.col_offset),
686
+ )
610
687
  for arg in node.args
611
688
  ]
612
689
 
690
+ # Legacy field, derived from the structured arguments above rather
691
+ # than re-inferring: byte-identical to the old
692
+ # `self._infer_type(...) or type(arg).__name__` per argument.
693
+ argument_types = [a.inferred_type or a.ast_kind for a in arguments]
694
+
613
695
  call_sites.append(
614
696
  PyCallsite.builder()
615
697
  .method_name(method_name)
616
698
  .receiver_expr(receiver_expr)
617
699
  .receiver_type(receiver_type)
618
700
  .argument_types(argument_types)
701
+ .arguments(arguments)
619
702
  .return_type(return_type)
620
703
  .callee_signature(callee_signature)
621
704
  .is_constructor_call(is_constructor)