codeanalyzer-python 0.3.0__py3-none-any.whl → 0.3.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.
- codeanalyzer/__main__.py +26 -0
- codeanalyzer/core.py +34 -3
- codeanalyzer/neo4j/__init__.py +1 -1
- codeanalyzer/neo4j/emit.py +2 -2
- codeanalyzer/neo4j/project.py +84 -18
- codeanalyzer/neo4j/schema.py +261 -15
- codeanalyzer/provenance.py +61 -0
- codeanalyzer/schema/py_schema.py +38 -0
- codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +74 -10
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-0.3.2.dist-info}/METADATA +23 -25
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-0.3.2.dist-info}/RECORD +16 -15
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-0.3.2.dist-info}/WHEEL +1 -1
- codeanalyzer/neo4j/catalog.py +0 -245
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-0.3.2.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-0.3.0.dist-info → codeanalyzer_python-0.3.2.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Provenance capture: repository (git) and analyzer identity for a snapshot.
|
|
2
|
+
|
|
3
|
+
Runs git only as a subprocess query against the analyzed project directory —
|
|
4
|
+
never mutates anything. Absence of git (no repo, no binary) degrades to None.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Union
|
|
12
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
13
|
+
|
|
14
|
+
from codeanalyzer.schema.py_schema import PyAnalyzerInfo, PyRepositoryInfo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _strip_userinfo(uri: str) -> str:
|
|
18
|
+
"""Drop credentials from URL-style remotes; scp-style git@host:path is a
|
|
19
|
+
username, not a secret, and parses with no netloc — left intact."""
|
|
20
|
+
parts = urlsplit(uri)
|
|
21
|
+
if parts.netloc and "@" in parts.netloc:
|
|
22
|
+
return urlunsplit(parts._replace(netloc=parts.netloc.rpartition("@")[2]))
|
|
23
|
+
return uri
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _git(project_dir: Union[Path, str], *args: str) -> Optional[str]:
|
|
27
|
+
"""One git query; None on any failure (no repo, no git, timeout)."""
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["git", "-C", str(project_dir), *args],
|
|
31
|
+
capture_output=True, text=True, timeout=10, check=False,
|
|
32
|
+
)
|
|
33
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
34
|
+
return None
|
|
35
|
+
if result.returncode != 0:
|
|
36
|
+
return None
|
|
37
|
+
return result.stdout.strip()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def repository_info(project_dir: Union[Path, str]) -> Optional[PyRepositoryInfo]:
|
|
41
|
+
"""Git provenance of ``project_dir``, or None when it isn't a git checkout."""
|
|
42
|
+
revision = _git(project_dir, "rev-parse", "HEAD")
|
|
43
|
+
if not revision:
|
|
44
|
+
return None
|
|
45
|
+
raw_uri = _git(project_dir, "remote", "get-url", "origin")
|
|
46
|
+
uri = _strip_userinfo(raw_uri) if raw_uri else None
|
|
47
|
+
status = _git(project_dir, "status", "--porcelain", "--untracked-files=no")
|
|
48
|
+
return PyRepositoryInfo(uri=uri, revision=revision, dirty=bool(status))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def analyzer_info(analysis_level: int) -> PyAnalyzerInfo:
|
|
52
|
+
"""Identity + configuration of this analyzer run."""
|
|
53
|
+
try:
|
|
54
|
+
version = _pkg_version("codeanalyzer-python")
|
|
55
|
+
except PackageNotFoundError:
|
|
56
|
+
version = "unknown"
|
|
57
|
+
return PyAnalyzerInfo(
|
|
58
|
+
name="codeanalyzer-python",
|
|
59
|
+
version=version,
|
|
60
|
+
config={"analysis_level": analysis_level},
|
|
61
|
+
)
|
codeanalyzer/schema/py_schema.py
CHANGED
|
@@ -176,6 +176,7 @@ class PyImport(BaseModel):
|
|
|
176
176
|
module: str
|
|
177
177
|
name: str
|
|
178
178
|
alias: Optional[str] = None
|
|
179
|
+
resolved_module: Optional[str] = None
|
|
179
180
|
start_line: int = -1
|
|
180
181
|
end_line: int = -1
|
|
181
182
|
start_column: int = -1
|
|
@@ -240,6 +241,19 @@ class PyCallableParameter(BaseModel):
|
|
|
240
241
|
end_column: int = -1
|
|
241
242
|
|
|
242
243
|
|
|
244
|
+
@builder
|
|
245
|
+
@msgpk
|
|
246
|
+
class PyCallArgument(BaseModel):
|
|
247
|
+
"""One call-site argument: AST category + inferred type, kept separate.
|
|
248
|
+
|
|
249
|
+
The legacy ``PyCallsite.argument_types`` mixed these two vocabularies
|
|
250
|
+
in one list; this model is the disambiguated replacement (#86).
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
ast_kind: str
|
|
254
|
+
inferred_type: Optional[str] = None
|
|
255
|
+
|
|
256
|
+
|
|
243
257
|
@builder
|
|
244
258
|
@msgpk
|
|
245
259
|
class PyCallsite(BaseModel):
|
|
@@ -249,6 +263,7 @@ class PyCallsite(BaseModel):
|
|
|
249
263
|
receiver_expr: Optional[str] = None
|
|
250
264
|
receiver_type: Optional[str] = None
|
|
251
265
|
argument_types: List[str] = []
|
|
266
|
+
arguments: List[PyCallArgument] = []
|
|
252
267
|
return_type: Optional[str] = None
|
|
253
268
|
callee_signature: Optional[str] = None
|
|
254
269
|
is_constructor_call: bool = False
|
|
@@ -295,6 +310,7 @@ class PyClassAttribute(BaseModel):
|
|
|
295
310
|
|
|
296
311
|
name: str
|
|
297
312
|
type: Optional[str] = None
|
|
313
|
+
initializer: Optional[str] = None
|
|
298
314
|
comments: List[PyComment] = []
|
|
299
315
|
start_line: int = -1
|
|
300
316
|
end_line: int = -1
|
|
@@ -369,6 +385,26 @@ class PyExternalSymbol(BaseModel):
|
|
|
369
385
|
module: Optional[str] = None # best-effort owning module, e.g. "requests"
|
|
370
386
|
|
|
371
387
|
|
|
388
|
+
@builder
|
|
389
|
+
@msgpk
|
|
390
|
+
class PyRepositoryInfo(BaseModel):
|
|
391
|
+
"""Where the analyzed source came from: git provenance captured at analysis time."""
|
|
392
|
+
|
|
393
|
+
uri: Optional[str] = None
|
|
394
|
+
revision: str
|
|
395
|
+
dirty: bool = False
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@builder
|
|
399
|
+
@msgpk
|
|
400
|
+
class PyAnalyzerInfo(BaseModel):
|
|
401
|
+
"""Which analyzer produced this snapshot, and how it was configured."""
|
|
402
|
+
|
|
403
|
+
name: str
|
|
404
|
+
version: str
|
|
405
|
+
config: Dict[str, Any] = {}
|
|
406
|
+
|
|
407
|
+
|
|
372
408
|
@builder
|
|
373
409
|
@msgpk
|
|
374
410
|
class PyApplication(BaseModel):
|
|
@@ -380,3 +416,5 @@ class PyApplication(BaseModel):
|
|
|
380
416
|
# builtin members), keyed by signature. Populated by the analyzer so every
|
|
381
417
|
# backend (JSON and Neo4j) shares one authoritative external-symbol set.
|
|
382
418
|
external_symbols: Dict[str, PyExternalSymbol] = {}
|
|
419
|
+
analyzer: Optional[PyAnalyzerInfo] = None
|
|
420
|
+
repository: Optional[PyRepositoryInfo] = None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Static resolution of import spellings against the analyzed module set.
|
|
2
|
+
|
|
3
|
+
Pure post-pass over a built ``PyApplication``: no filesystem access, no
|
|
4
|
+
sys.path semantics — a spelling resolves iff it names a module that was
|
|
5
|
+
itself analyzed (issue #82). External/library imports stay unresolved by
|
|
6
|
+
design and keep their :PyPackage projection.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Optional, Union
|
|
13
|
+
|
|
14
|
+
from codeanalyzer.schema.py_schema import PyApplication
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _dotted_candidates(app: PyApplication, project_dir: Union[Path, str]) -> Dict[str, str]:
|
|
18
|
+
"""dotted module path -> file_key, for every analyzed module.
|
|
19
|
+
|
|
20
|
+
``pkg/util.py`` -> ``pkg.util``; ``pkg/__init__.py`` -> ``pkg``.
|
|
21
|
+
file_keys share project_dir's form (both come from the same CLI arg),
|
|
22
|
+
so os.path.relpath keeps mixed absolute/relative setups consistent.
|
|
23
|
+
"""
|
|
24
|
+
mapping: Dict[str, str] = {}
|
|
25
|
+
for file_key in app.symbol_table:
|
|
26
|
+
rel = os.path.relpath(file_key, str(project_dir))
|
|
27
|
+
if rel.startswith(".."):
|
|
28
|
+
continue
|
|
29
|
+
parts = Path(rel).with_suffix("").parts
|
|
30
|
+
if parts and parts[-1] == "__init__":
|
|
31
|
+
parts = parts[:-1]
|
|
32
|
+
if parts:
|
|
33
|
+
mapping[".".join(parts)] = file_key
|
|
34
|
+
return mapping
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _resolve_one(
|
|
38
|
+
spelling: str, original_name: str, importer_rel_parts: tuple, candidates: Dict[str, str]
|
|
39
|
+
) -> Optional[str]:
|
|
40
|
+
if spelling.startswith("."):
|
|
41
|
+
level = len(spelling) - len(spelling.lstrip("."))
|
|
42
|
+
suffix = spelling.lstrip(".")
|
|
43
|
+
# level 1 = the importer's own package; each extra dot walks one up.
|
|
44
|
+
package_parts = importer_rel_parts[:-1] # drop the filename
|
|
45
|
+
if level - 1 > len(package_parts):
|
|
46
|
+
return None
|
|
47
|
+
base = package_parts[: len(package_parts) - (level - 1)]
|
|
48
|
+
stems = list(base) + (suffix.split(".") if suffix else [])
|
|
49
|
+
else:
|
|
50
|
+
stems = spelling.split(".")
|
|
51
|
+
dotted = ".".join(stems)
|
|
52
|
+
with_name = f"{dotted}.{original_name}" if dotted else original_name
|
|
53
|
+
return candidates.get(with_name) or candidates.get(dotted)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resolve_imports(app: PyApplication, project_dir: Union[Path, str]) -> None:
|
|
57
|
+
"""Stamp ``resolved_module`` on every import of every module, in place."""
|
|
58
|
+
candidates = _dotted_candidates(app, project_dir)
|
|
59
|
+
for file_key, module in app.symbol_table.items():
|
|
60
|
+
rel = os.path.relpath(file_key, str(project_dir))
|
|
61
|
+
importer_parts = Path(rel).parts
|
|
62
|
+
for im in module.imports or []:
|
|
63
|
+
if not im.module:
|
|
64
|
+
im.resolved_module = None
|
|
65
|
+
continue
|
|
66
|
+
original = im.alias or im.name
|
|
67
|
+
im.resolved_module = _resolve_one(im.module, original, importer_parts, candidates)
|
|
@@ -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,
|
|
@@ -28,7 +30,11 @@ class SymbolTableBuilder:
|
|
|
28
30
|
"""A class for building a symbol table for a Python project."""
|
|
29
31
|
|
|
30
32
|
def __init__(self, project_dir: Union[Path, str], virtualenv: Union[Path, str, None]) -> None:
|
|
31
|
-
|
|
33
|
+
# Jedi reports absolute Script paths, so a relative project_dir would
|
|
34
|
+
# crash every relative_to() fallback below. abspath (not resolve())
|
|
35
|
+
# keeps symlinks intact, matching Jedi's own absolute()-style
|
|
36
|
+
# normalization under symlinked roots like macOS /tmp.
|
|
37
|
+
self.project_dir = Path(os.path.abspath(project_dir))
|
|
32
38
|
if virtualenv is None:
|
|
33
39
|
# If no virtual environment is provided, create a jedi project without an environment.
|
|
34
40
|
self.jedi_project: Project = jedi.Project(path=self.project_dir)
|
|
@@ -39,6 +45,16 @@ class SymbolTableBuilder:
|
|
|
39
45
|
environment_path=Path(virtualenv) / "bin" / "python",
|
|
40
46
|
)
|
|
41
47
|
|
|
48
|
+
def _fallback_signature(self, script_path: Union[Path, str], name: str) -> str:
|
|
49
|
+
"""Path-derived qualified name used when Jedi can't name a definition.
|
|
50
|
+
|
|
51
|
+
Strips only the terminal ``.py`` suffix — a bare ``str.replace``
|
|
52
|
+
would also eat interior ``.py`` substrings and corrupt the module
|
|
53
|
+
prefix (``odoo/tools/pycompat.py`` → ``odoo.toolscompat``).
|
|
54
|
+
"""
|
|
55
|
+
relative = Path(script_path).relative_to(self.project_dir)
|
|
56
|
+
return ".".join(relative.with_suffix("").parts) + f".{name}"
|
|
57
|
+
|
|
42
58
|
@staticmethod
|
|
43
59
|
def _infer_type(script: Script, line: int, column: int) -> str:
|
|
44
60
|
"""Tries to infer the type at a given position using Jedi."""
|
|
@@ -97,6 +113,44 @@ class SymbolTableBuilder:
|
|
|
97
113
|
except Exception:
|
|
98
114
|
return None, False
|
|
99
115
|
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _callee_anchor(node: ast.Call) -> Tuple[int, int]:
|
|
118
|
+
"""Position of the callee *name* for Jedi inference.
|
|
119
|
+
|
|
120
|
+
An ``ast.Call``'s own ``lineno``/``col_offset`` is the first
|
|
121
|
+
character of the whole call expression — for an attribute call
|
|
122
|
+
``receiver.method(...)`` that is the receiver token, and Jedi
|
|
123
|
+
would infer the receiver's type instead of the invoked method
|
|
124
|
+
(issue #80). Anchor attribute calls inside the attribute name —
|
|
125
|
+
its last character, so one-character names stay in range; other
|
|
126
|
+
callee shapes keep the call-expression start.
|
|
127
|
+
"""
|
|
128
|
+
func_expr = node.func
|
|
129
|
+
if isinstance(func_expr, ast.Attribute):
|
|
130
|
+
return func_expr.end_lineno, func_expr.end_col_offset - 1
|
|
131
|
+
return node.lineno, node.col_offset
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _infer_call_return_type(script: Script, line: int, column: int) -> Optional[str]:
|
|
135
|
+
"""Inferred type of the call's *result*, not of the callee itself.
|
|
136
|
+
|
|
137
|
+
``Script.infer`` at the callee name yields the function/class
|
|
138
|
+
being called; executing that definition yields what the call
|
|
139
|
+
evaluates to — a function's inferred return type, or the
|
|
140
|
+
instance for a constructor call. Returns ``None`` when Jedi
|
|
141
|
+
can't tell, so an unknown stays absent instead of masquerading
|
|
142
|
+
as the callee's own name.
|
|
143
|
+
"""
|
|
144
|
+
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
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
return None
|
|
153
|
+
|
|
100
154
|
def build_pymodule_from_file(self, py_file: Path) -> PyModule:
|
|
101
155
|
"""Builds a PyModule from a Python file.
|
|
102
156
|
|
|
@@ -203,10 +257,10 @@ class SymbolTableBuilder:
|
|
|
203
257
|
definitions = script.goto(line=start_line, column=child.col_offset)
|
|
204
258
|
signature = next(
|
|
205
259
|
(d.full_name for d in definitions if d.type == "class"),
|
|
206
|
-
|
|
260
|
+
self._fallback_signature(script.path, class_name),
|
|
207
261
|
)
|
|
208
262
|
except Exception:
|
|
209
|
-
signature =
|
|
263
|
+
signature = self._fallback_signature(script.path, class_name)
|
|
210
264
|
py_class = (
|
|
211
265
|
PyClass.builder()
|
|
212
266
|
.name(class_name)
|
|
@@ -258,8 +312,7 @@ class SymbolTableBuilder:
|
|
|
258
312
|
|
|
259
313
|
# If Jedi didn't provide a signature, build one relative to project_dir
|
|
260
314
|
if not signature:
|
|
261
|
-
|
|
262
|
-
signature = f"{str(relative_path).replace('/', '.').replace('.py', '')}.{method_name}"
|
|
315
|
+
signature = self._fallback_signature(script.path, method_name)
|
|
263
316
|
py_callable = (
|
|
264
317
|
PyCallable.builder()
|
|
265
318
|
.name(method_name) # Use the actual method name, not the full signature
|
|
@@ -380,6 +433,7 @@ class SymbolTableBuilder:
|
|
|
380
433
|
script, target.lineno, target.col_offset
|
|
381
434
|
)
|
|
382
435
|
)
|
|
436
|
+
.initializer(ast.unparse(stmt.value) if stmt.value else None)
|
|
383
437
|
.start_line(getattr(target, "lineno", -1))
|
|
384
438
|
.end_line(getattr(stmt, "end_lineno", stmt.lineno))
|
|
385
439
|
.build()
|
|
@@ -398,6 +452,7 @@ class SymbolTableBuilder:
|
|
|
398
452
|
script, target.lineno, target.col_offset
|
|
399
453
|
)
|
|
400
454
|
)
|
|
455
|
+
.initializer(ast.unparse(stmt.value) if stmt.value else None)
|
|
401
456
|
.start_line(getattr(target, "lineno", -1))
|
|
402
457
|
.end_line(getattr(stmt, "end_lineno", stmt.lineno))
|
|
403
458
|
.build()
|
|
@@ -588,10 +643,11 @@ class SymbolTableBuilder:
|
|
|
588
643
|
func_expr = node.func
|
|
589
644
|
|
|
590
645
|
method_name = "<unknown>"
|
|
646
|
+
anchor_line, anchor_col = self._callee_anchor(node)
|
|
591
647
|
callee_signature, is_constructor = self._infer_callee(
|
|
592
|
-
script,
|
|
648
|
+
script, anchor_line, anchor_col
|
|
593
649
|
)
|
|
594
|
-
return_type = self.
|
|
650
|
+
return_type = self._infer_call_return_type(script, anchor_line, anchor_col)
|
|
595
651
|
|
|
596
652
|
receiver_expr = None
|
|
597
653
|
receiver_type = None
|
|
@@ -604,18 +660,26 @@ class SymbolTableBuilder:
|
|
|
604
660
|
elif isinstance(func_expr, ast.Name):
|
|
605
661
|
method_name = func_expr.id
|
|
606
662
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
663
|
+
arguments = [
|
|
664
|
+
PyCallArgument(
|
|
665
|
+
ast_kind=type(arg).__name__,
|
|
666
|
+
inferred_type=self._infer_type(script, arg.lineno, arg.col_offset),
|
|
667
|
+
)
|
|
610
668
|
for arg in node.args
|
|
611
669
|
]
|
|
612
670
|
|
|
671
|
+
# Legacy field, derived from the structured arguments above rather
|
|
672
|
+
# than re-inferring: byte-identical to the old
|
|
673
|
+
# `self._infer_type(...) or type(arg).__name__` per argument.
|
|
674
|
+
argument_types = [a.inferred_type or a.ast_kind for a in arguments]
|
|
675
|
+
|
|
613
676
|
call_sites.append(
|
|
614
677
|
PyCallsite.builder()
|
|
615
678
|
.method_name(method_name)
|
|
616
679
|
.receiver_expr(receiver_expr)
|
|
617
680
|
.receiver_type(receiver_type)
|
|
618
681
|
.argument_types(argument_types)
|
|
682
|
+
.arguments(arguments)
|
|
619
683
|
.return_type(return_type)
|
|
620
684
|
.callee_signature(callee_signature)
|
|
621
685
|
.is_constructor_call(is_constructor)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Static Analysis on Python source code using Jedi, CodeQL and Treesitter — emits analysis.json or a Neo4j property graph.
|
|
5
5
|
Author-email: Rahul Krishna <i.m.ralk@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -12,12 +12,7 @@ Requires-Dist: msgpack<1.0.7,>=1.0.0; python_version < '3.11'
|
|
|
12
12
|
Requires-Dist: msgpack<2.0.0,>=1.0.7; python_version >= '3.11'
|
|
13
13
|
Requires-Dist: networkx<3.2.0,>=2.6.0; python_version < '3.11'
|
|
14
14
|
Requires-Dist: networkx<4.0.0,>=3.0.0; python_version >= '3.11'
|
|
15
|
-
Requires-Dist: numpy<1.24.0,>=1.21.0; python_version < '3.11'
|
|
16
|
-
Requires-Dist: numpy<2.0.0,>=1.24.0; python_version >= '3.11' and python_version < '3.12'
|
|
17
|
-
Requires-Dist: numpy<2.0.0,>=1.26.0; python_version >= '3.12'
|
|
18
15
|
Requires-Dist: packaging>=25.0
|
|
19
|
-
Requires-Dist: pandas<2.0.0,>=1.3.0; python_version < '3.11'
|
|
20
|
-
Requires-Dist: pandas<3.0.0,>=2.0.0; python_version >= '3.11'
|
|
21
16
|
Requires-Dist: pycg>=0.0.6
|
|
22
17
|
Requires-Dist: pydantic<2.0.0,>=1.8.0; python_version < '3.11'
|
|
23
18
|
Requires-Dist: pydantic<3.0.0,>=2.0.0; python_version >= '3.11'
|
|
@@ -181,19 +176,22 @@ $ canpy --help
|
|
|
181
176
|
Static Analysis on Python source code using Jedi, PyCG and Tree sitter.
|
|
182
177
|
|
|
183
178
|
╭─ Options ────────────────────────────────────────────────────────────────────╮
|
|
184
|
-
│ --
|
|
179
|
+
│ --version Show the canpy │
|
|
180
|
+
│ version and │
|
|
181
|
+
│ exit. │
|
|
182
|
+
│ --input -i <path> Path to the │
|
|
185
183
|
│ project root │
|
|
186
184
|
│ directory (not │
|
|
187
185
|
│ required for │
|
|
188
186
|
│ --emit schema). │
|
|
189
|
-
│ --output -o
|
|
187
|
+
│ --output -o <path> Output directory │
|
|
190
188
|
│ for artifacts. │
|
|
191
|
-
│ --format -f
|
|
189
|
+
│ --format -f <json|msgpack> Output format │
|
|
192
190
|
│ for --emit json: │
|
|
193
191
|
│ json or msgpack. │
|
|
194
192
|
│ [default: json] │
|
|
195
|
-
│ --emit
|
|
196
|
-
│ ma
|
|
193
|
+
│ --emit <json|neo4j|sche Output target: │
|
|
194
|
+
│ ma> json │
|
|
197
195
|
│ (analysis.json, │
|
|
198
196
|
│ default) | neo4j │
|
|
199
197
|
│ (graph.cypher or │
|
|
@@ -203,13 +201,13 @@ $ canpy --help
|
|
|
203
201
|
│ schema.json │
|
|
204
202
|
│ contract). │
|
|
205
203
|
│ [default: json] │
|
|
206
|
-
│ --app-name
|
|
204
|
+
│ --app-name <str> Logical │
|
|
207
205
|
│ application name │
|
|
208
206
|
│ for the graph │
|
|
209
207
|
│ :PyApplication │
|
|
210
208
|
│ anchor (default: │
|
|
211
209
|
│ input dir name). │
|
|
212
|
-
│ --neo4j-uri
|
|
210
|
+
│ --neo4j-uri <str> Push the graph │
|
|
213
211
|
│ to a live Neo4j │
|
|
214
212
|
│ over Bolt │
|
|
215
213
|
│ (incremental); │
|
|
@@ -217,11 +215,11 @@ $ canpy --help
|
|
|
217
215
|
│ graph.cypher. │
|
|
218
216
|
│ [env var: │
|
|
219
217
|
│ NEO4J_URI] │
|
|
220
|
-
│ --neo4j-user
|
|
218
|
+
│ --neo4j-user <str> Neo4j username. │
|
|
221
219
|
│ [env var: │
|
|
222
220
|
│ NEO4J_USERNAME] │
|
|
223
221
|
│ [default: neo4j] │
|
|
224
|
-
│ --neo4j-password
|
|
222
|
+
│ --neo4j-password <str> Neo4j password. │
|
|
225
223
|
│ Prefer the env │
|
|
226
224
|
│ var over the │
|
|
227
225
|
│ flag (the flag │
|
|
@@ -231,12 +229,12 @@ $ canpy --help
|
|
|
231
229
|
│ [env var: │
|
|
232
230
|
│ NEO4J_PASSWORD] │
|
|
233
231
|
│ [default: neo4j] │
|
|
234
|
-
│ --neo4j-database
|
|
232
|
+
│ --neo4j-database <str> Neo4j database │
|
|
235
233
|
│ name (default: │
|
|
236
234
|
│ server default). │
|
|
237
235
|
│ [env var: │
|
|
238
236
|
│ NEO4J_DATABASE] │
|
|
239
|
-
│ --analysis-level -a
|
|
237
|
+
│ --analysis-level -a <int range> Analysis depth: │
|
|
240
238
|
│ [1<=x<=2] 1=symbol │
|
|
241
239
|
│ table+Jedi call │
|
|
242
240
|
│ graph, 2=+PyCG │
|
|
@@ -266,12 +264,12 @@ $ canpy --help
|
|
|
266
264
|
│ environment │
|
|
267
265
|
│ instead. │
|
|
268
266
|
│ [default: venv] │
|
|
269
|
-
│ --file-name
|
|
267
|
+
│ --file-name <path> Analyze only the │
|
|
270
268
|
│ specified file │
|
|
271
269
|
│ (relative to │
|
|
272
270
|
│ input │
|
|
273
271
|
│ directory). │
|
|
274
|
-
│ --cache-dir -c
|
|
272
|
+
│ --cache-dir -c <path> Directory to │
|
|
275
273
|
│ store analysis │
|
|
276
274
|
│ cache. Defaults │
|
|
277
275
|
│ to │
|
|
@@ -285,7 +283,7 @@ $ canpy --help
|
|
|
285
283
|
│ retained. │
|
|
286
284
|
│ [default: │
|
|
287
285
|
│ keep-cache] │
|
|
288
|
-
│ -v
|
|
286
|
+
│ -v <int> Increase │
|
|
289
287
|
│ verbosity: -v, │
|
|
290
288
|
│ -vv, -vvv │
|
|
291
289
|
│ [default: 0] │
|
|
@@ -312,7 +310,7 @@ $ canpy --help
|
|
|
312
310
|
│ Jedi-only edges. │
|
|
313
311
|
│ [default: │
|
|
314
312
|
│ no-pycg-shard] │
|
|
315
|
-
│ --pycg-shard-cei…
|
|
313
|
+
│ --pycg-shard-cei… <int range> Maximum files │
|
|
316
314
|
│ [x>=1] per shard when │
|
|
317
315
|
│ --pycg-shard is │
|
|
318
316
|
│ active (default │
|
|
@@ -334,7 +332,7 @@ $ canpy --help
|
|
|
334
332
|
│ heavy import │
|
|
335
333
|
│ graphs. │
|
|
336
334
|
│ [default: 100] │
|
|
337
|
-
│ --pycg-shard-tim…
|
|
335
|
+
│ --pycg-shard-tim… <int range> Per-shard │
|
|
338
336
|
│ [x>=0] wall-clock │
|
|
339
337
|
│ timeout in │
|
|
340
338
|
│ seconds when │
|
|
@@ -362,7 +360,7 @@ $ canpy --help
|
|
|
362
360
|
│ ignored on │
|
|
363
361
|
│ Windows. │
|
|
364
362
|
│ [default: 120] │
|
|
365
|
-
│ --pycg-shard-str…
|
|
363
|
+
│ --pycg-shard-str… <jedi|package> How --pycg-shard │
|
|
366
364
|
│ groups files │
|
|
367
365
|
│ (level 2 only). │
|
|
368
366
|
│ 'jedi' (default) │
|
|
@@ -384,7 +382,7 @@ $ canpy --help
|
|
|
384
382
|
│ one-shard-per-p… │
|
|
385
383
|
│ grouping. │
|
|
386
384
|
│ [default: jedi] │
|
|
387
|
-
│ --pycg-max-iter
|
|
385
|
+
│ --pycg-max-iter <int range> Cap on PyCG's │
|
|
388
386
|
│ [x>=-1] fixpoint passes │
|
|
389
387
|
│ per │
|
|
390
388
|
│ shard/project │
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
codeanalyzer/__init__.py,sha256=BZ3Kuwl-F_F-8H8cepLnVJ4Ku4NNUjjqg0Y6ujPQSsI,108
|
|
2
|
-
codeanalyzer/__main__.py,sha256
|
|
3
|
-
codeanalyzer/core.py,sha256=
|
|
2
|
+
codeanalyzer/__main__.py,sha256=GOGc6j-euSeRZbUQgfZykQClT32WNxcStKoO96s-VSY,12790
|
|
3
|
+
codeanalyzer/core.py,sha256=BCH5OynbXrnItPvYhcpAChcBtkXdQIMbH_fyFGCxITU,31185
|
|
4
|
+
codeanalyzer/provenance.py,sha256=DT-DqwdVwO7g-GyK3sC0_4vDNCUjZYEY1fyYCt-7VRM,2306
|
|
4
5
|
codeanalyzer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
6
|
codeanalyzer/config/__init__.py,sha256=9XBxAn1oWGRuhg3bEBUuVGs3hFNXEAKrr-Ce7tq9a2k,61
|
|
6
7
|
codeanalyzer/config/config.py,sha256=ZiKzc5uEUCIvih58-6BDtLLI1hPij41wGQjBcj9KNQM,188
|
|
7
8
|
codeanalyzer/jedi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
9
|
codeanalyzer/jedi/jedi.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
codeanalyzer/neo4j/__init__.py,sha256=
|
|
10
|
+
codeanalyzer/neo4j/__init__.py,sha256=8Ei6uThTOqmul6GhnKoGOjLXJZbyDwF5Jo-8I8NxnAk,1574
|
|
10
11
|
codeanalyzer/neo4j/bolt.py,sha256=-IFDb_d67IkGwxvmmbLNI6AvSQHY-XsutI3szibWidw,9635
|
|
11
|
-
codeanalyzer/neo4j/catalog.py,sha256=VQWtwcueck2Ug_cu8sitvAoRwbVZ-6KRiexRN8q1Wak,7418
|
|
12
12
|
codeanalyzer/neo4j/cypher.py,sha256=2zIWXA1AADrwCMhSTeqKjEXRgBjbob6o3bme_cwLu0s,5024
|
|
13
|
-
codeanalyzer/neo4j/emit.py,sha256=
|
|
14
|
-
codeanalyzer/neo4j/project.py,sha256=
|
|
13
|
+
codeanalyzer/neo4j/emit.py,sha256=NZL5BVY1Fb32igH22986_cUFUIgHNWJHbii2bfX2E3M,3099
|
|
14
|
+
codeanalyzer/neo4j/project.py,sha256=YMVtF1GjLZYwnhFl-7fy9Qj7fpGgd5aEGOp0JMF3VAY,14513
|
|
15
15
|
codeanalyzer/neo4j/rows.py,sha256=5xI3X-l-vwPe_gmKYUg7VuUQARcOlVAZnGmiQr9QyRk,7326
|
|
16
|
-
codeanalyzer/neo4j/schema.py,sha256=
|
|
16
|
+
codeanalyzer/neo4j/schema.py,sha256=tZjnpIFdTV3-GB1x2zTbDMxQOH16GqELYCf2cR2XhgU,8765
|
|
17
17
|
codeanalyzer/options/__init__.py,sha256=Ki4qhHFqpyuUWVsntO-NYJMVWrkeFOzPW4nQ7oxiUVI,155
|
|
18
18
|
codeanalyzer/options/options.py,sha256=5w3DZYlAv-0LVPavF1P5EEJp8XBRrSXidbDusdbXQAo,1976
|
|
19
19
|
codeanalyzer/schema/__init__.py,sha256=cLPjvowrnz8xzi7tZAsKQeIOjdOKRGHy4I7wbG0jHk8,2024
|
|
20
|
-
codeanalyzer/schema/py_schema.py,sha256=
|
|
20
|
+
codeanalyzer/schema/py_schema.py,sha256=yYKkg3ufNDteNvtiPiRJQz-pzK5NnnnNldhXcW7p2RI,13274
|
|
21
21
|
codeanalyzer/semantic_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
codeanalyzer/semantic_analysis/call_graph.py,sha256=H3IkfGp1VCkDxI75JPyLSSA-wOaJp_I-NYPsjCY04Bg,11185
|
|
23
23
|
codeanalyzer/semantic_analysis/pycg/__init__.py,sha256=Lsgz25iFM_RGGu_i2psY-LN-KwvYIZQPnKgRPL1JsjU,928
|
|
@@ -26,13 +26,14 @@ codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py,sha256=n4tRderrSYw9cUYgR2
|
|
|
26
26
|
codeanalyzer/semantic_analysis/pycg/shard_planner.py,sha256=PTXMG9YcrZX9hgnlnIlpQx6xmWkizP3aQrf0elcrnmw,15865
|
|
27
27
|
codeanalyzer/syntactic_analysis/__init__.py,sha256=EUQkJEh6wHjWx2qTTKbTbUgwSbfKeNieKHNy7RknVXA,476
|
|
28
28
|
codeanalyzer/syntactic_analysis/exceptions.py,sha256=whs_n0vIu655Jkk1a7iOoXY6iIca4pZqJnU40V9Ejaw,537
|
|
29
|
-
codeanalyzer/syntactic_analysis/
|
|
29
|
+
codeanalyzer/syntactic_analysis/import_resolver.py,sha256=Q8noZwSdDNt4P4NMqDTB9FaS-M0_d4ASK9GnJop9MDI,2751
|
|
30
|
+
codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=hQOEk2qRYM9Y_UoejNbZFCWSkDjBa-vRcU3kPURrMNc,41148
|
|
30
31
|
codeanalyzer/utils/__init__.py,sha256=hC6VWdR5rerSqBxzu9KQHTASWqwrrYJv-CMDwrTlzkc,137
|
|
31
32
|
codeanalyzer/utils/logging.py,sha256=Fw0tattPAOMs3o0JMjjXhRVLIF64f-SCcygUXF9jqeg,904
|
|
32
33
|
codeanalyzer/utils/progress_bar.py,sha256=C9JtzVdd10lIxTv-KA6PebqjKWueC_vMGwVzAtHuHIw,2818
|
|
33
|
-
codeanalyzer_python-0.3.
|
|
34
|
-
codeanalyzer_python-0.3.
|
|
35
|
-
codeanalyzer_python-0.3.
|
|
36
|
-
codeanalyzer_python-0.3.
|
|
37
|
-
codeanalyzer_python-0.3.
|
|
38
|
-
codeanalyzer_python-0.3.
|
|
34
|
+
codeanalyzer_python-0.3.2.dist-info/METADATA,sha256=oX5F_aEJQSK9cZWGnI9rqSbNXUQQyPZSZSRuXRNEJ20,33695
|
|
35
|
+
codeanalyzer_python-0.3.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
36
|
+
codeanalyzer_python-0.3.2.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
|
|
37
|
+
codeanalyzer_python-0.3.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
38
|
+
codeanalyzer_python-0.3.2.dist-info/licenses/NOTICE,sha256=YU0Z9NDWqKY-2jfFcbxeZ6fbnzz0oZeKmnUcO8a-bcQ,901
|
|
39
|
+
codeanalyzer_python-0.3.2.dist-info/RECORD,,
|