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.
- codeanalyzer/__main__.py +77 -4
- codeanalyzer/core.py +174 -72
- 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/__init__.py +1 -1
- codeanalyzer/neo4j/bolt.py +19 -4
- codeanalyzer/neo4j/cypher.py +9 -3
- codeanalyzer/neo4j/emit.py +10 -5
- codeanalyzer/neo4j/project.py +307 -60
- codeanalyzer/neo4j/rows.py +18 -15
- codeanalyzer/neo4j/schema.py +297 -15
- codeanalyzer/options/options.py +4 -0
- codeanalyzer/provenance.py +61 -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 +175 -26
- codeanalyzer/semantic_analysis/call_graph.py +24 -27
- codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +10 -10
- codeanalyzer/semantic_analysis/pycg/shard_planner.py +7 -7
- codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +103 -20
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/METADATA +233 -43
- codeanalyzer_python-1.0.0.dist-info/RECORD +59 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/WHEEL +1 -1
- codeanalyzer/neo4j/catalog.py +0 -245
- codeanalyzer_python-0.3.0.dist-info/RECORD +0 -38
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
271
|
+
self._fallback_signature(script.path, class_name),
|
|
207
272
|
)
|
|
208
273
|
except Exception:
|
|
209
|
-
signature =
|
|
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
|
-
.
|
|
288
|
+
.callables(self._callables(child, script, source, prefix=signature)) # Pass class signature as prefix
|
|
224
289
|
.attributes(self._class_attributes(child, script))
|
|
225
|
-
.
|
|
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
|
-
|
|
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
|
-
.
|
|
284
|
-
.
|
|
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,
|
|
667
|
+
script, anchor_line, anchor_col
|
|
593
668
|
)
|
|
594
|
-
return_type = self.
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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)
|