codeanalyzer-python 0.2.0__py3-none-any.whl → 0.2.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 +9 -0
- codeanalyzer/core.py +100 -18
- codeanalyzer/neo4j/bolt.py +13 -2
- codeanalyzer/neo4j/catalog.py +2 -2
- codeanalyzer/neo4j/project.py +18 -10
- codeanalyzer/neo4j/rows.py +10 -5
- codeanalyzer/options/options.py +1 -0
- codeanalyzer/schema/__init__.py +2 -0
- codeanalyzer/schema/py_schema.py +15 -0
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.2.1.dist-info}/METADATA +24 -2
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.2.1.dist-info}/RECORD +15 -15
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.2.1.dist-info}/WHEEL +0 -0
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.2.1.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.2.1.dist-info}/licenses/NOTICE +0 -0
codeanalyzer/__main__.py
CHANGED
|
@@ -104,6 +104,14 @@ def main(
|
|
|
104
104
|
help="Skip test files in analysis.",
|
|
105
105
|
),
|
|
106
106
|
] = True,
|
|
107
|
+
no_venv: Annotated[
|
|
108
|
+
bool,
|
|
109
|
+
typer.Option(
|
|
110
|
+
"--no-venv/--venv",
|
|
111
|
+
help="Skip virtualenv creation and dependency installation; resolve "
|
|
112
|
+
"imports against the ambient Python environment instead.",
|
|
113
|
+
),
|
|
114
|
+
] = False,
|
|
107
115
|
file_name: Annotated[
|
|
108
116
|
Optional[Path],
|
|
109
117
|
typer.Option(
|
|
@@ -144,6 +152,7 @@ def main(
|
|
|
144
152
|
using_ray=using_ray,
|
|
145
153
|
rebuild_analysis=rebuild_analysis,
|
|
146
154
|
skip_tests=skip_tests,
|
|
155
|
+
no_venv=no_venv,
|
|
147
156
|
file_name=file_name,
|
|
148
157
|
cache_dir=cache_dir,
|
|
149
158
|
clear_cache=clear_cache,
|
codeanalyzer/core.py
CHANGED
|
@@ -8,7 +8,13 @@ from typing import Any, Dict, Optional, Union, List
|
|
|
8
8
|
|
|
9
9
|
import ray
|
|
10
10
|
from codeanalyzer.utils import logger
|
|
11
|
-
from codeanalyzer.schema import
|
|
11
|
+
from codeanalyzer.schema import (
|
|
12
|
+
PyApplication,
|
|
13
|
+
PyExternalSymbol,
|
|
14
|
+
PyModule,
|
|
15
|
+
model_dump_json,
|
|
16
|
+
model_validate_json,
|
|
17
|
+
)
|
|
12
18
|
from codeanalyzer.schema.py_schema import PyCallEdge
|
|
13
19
|
from codeanalyzer.semantic_analysis.call_graph import (
|
|
14
20
|
jedi_call_graph_edges,
|
|
@@ -60,6 +66,7 @@ class Codeanalyzer:
|
|
|
60
66
|
self.skip_tests = options.skip_tests
|
|
61
67
|
self.using_codeql = options.using_codeql
|
|
62
68
|
self.rebuild_analysis = options.rebuild_analysis
|
|
69
|
+
self.no_venv = options.no_venv
|
|
63
70
|
self.cache_dir = (
|
|
64
71
|
options.cache_dir.resolve() if options.cache_dir is not None else self.project_dir
|
|
65
72
|
) / ".codeanalyzer"
|
|
@@ -226,13 +233,41 @@ class Codeanalyzer:
|
|
|
226
233
|
f"a working Python interpreter that can create virtual environments."
|
|
227
234
|
)
|
|
228
235
|
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _uv_bin() -> Optional[str]:
|
|
238
|
+
"""Path to a uv binary: the one bundled with the ``uv`` PyPI package (a
|
|
239
|
+
dependency, so normally always present -- including inside a Docker image),
|
|
240
|
+
else a uv on PATH, else ``None`` (callers fall back to pip)."""
|
|
241
|
+
try:
|
|
242
|
+
from uv import find_uv_bin
|
|
243
|
+
|
|
244
|
+
return str(find_uv_bin())
|
|
245
|
+
except Exception:
|
|
246
|
+
return shutil.which("uv")
|
|
247
|
+
|
|
248
|
+
def _install_into_venv(self, venv_python: Path, args: List[str]) -> None:
|
|
249
|
+
"""Install packages into the target venv, preferring uv for speed (parallel
|
|
250
|
+
downloads + a shared global cache) and falling back to the venv's own pip
|
|
251
|
+
when uv is unavailable."""
|
|
252
|
+
uv = self._uv_bin()
|
|
253
|
+
if uv:
|
|
254
|
+
cmd = [uv, "pip", "install", "--python", str(venv_python), *args]
|
|
255
|
+
else:
|
|
256
|
+
cmd = [str(venv_python), "-m", "pip", "install", *args]
|
|
257
|
+
self._cmd_exec_helper(cmd, cwd=self.project_dir, check=True)
|
|
258
|
+
|
|
229
259
|
def __enter__(self) -> "Codeanalyzer":
|
|
230
260
|
# If no virtualenv is provided, try to create one using requirements.txt or pyproject.toml
|
|
231
261
|
venv_path = self.cache_dir / self.project_dir.name / "virtualenv"
|
|
232
262
|
# Ensure the cache directory exists for this project
|
|
233
263
|
venv_path.parent.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
if self.no_venv:
|
|
265
|
+
logger.info(
|
|
266
|
+
"--no-venv: using the ambient Python environment "
|
|
267
|
+
"(skipping virtualenv creation and dependency installation)"
|
|
268
|
+
)
|
|
234
269
|
# Create the virtual environment if it does not exist
|
|
235
|
-
if not venv_path.exists() or self.rebuild_analysis:
|
|
270
|
+
if not self.no_venv and (not venv_path.exists() or self.rebuild_analysis):
|
|
236
271
|
logger.info(f"(Re-)creating virtual environment at {venv_path}")
|
|
237
272
|
self._cmd_exec_helper(
|
|
238
273
|
[str(self._get_base_interpreter()), "-m", "venv", str(venv_path)],
|
|
@@ -249,24 +284,19 @@ class Codeanalyzer:
|
|
|
249
284
|
("test-requirements.txt", ["-r"]),
|
|
250
285
|
]
|
|
251
286
|
|
|
252
|
-
for dep_file,
|
|
287
|
+
for dep_file, _ in dependency_files:
|
|
253
288
|
if (self.project_dir / dep_file).exists():
|
|
254
289
|
logger.info(f"Installing dependencies from {dep_file}")
|
|
255
|
-
self.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
check=True,
|
|
290
|
+
self._install_into_venv(
|
|
291
|
+
venv_python,
|
|
292
|
+
["--upgrade", "-r", str(self.project_dir / dep_file)],
|
|
259
293
|
)
|
|
260
294
|
|
|
261
295
|
# Handle Pipenv files
|
|
262
296
|
if (self.project_dir / "Pipfile").exists():
|
|
263
297
|
logger.info("Installing dependencies from Pipfile")
|
|
264
298
|
# Note: This would require pipenv to be installed
|
|
265
|
-
self.
|
|
266
|
-
[str(venv_python), "-m", "pip", "install", "pipenv"],
|
|
267
|
-
cwd=self.project_dir,
|
|
268
|
-
check=True,
|
|
269
|
-
)
|
|
299
|
+
self._install_into_venv(venv_python, ["pipenv"])
|
|
270
300
|
self._cmd_exec_helper(
|
|
271
301
|
["pipenv", "install", "--dev"],
|
|
272
302
|
cwd=self.project_dir,
|
|
@@ -289,14 +319,18 @@ class Codeanalyzer:
|
|
|
289
319
|
|
|
290
320
|
if any((self.project_dir / file).exists() for file in package_definition_files):
|
|
291
321
|
logger.info("Installing project in editable mode")
|
|
292
|
-
self.
|
|
293
|
-
[str(venv_python), "-m", "pip", "install", "-e", str(self.project_dir)],
|
|
294
|
-
cwd=self.project_dir,
|
|
295
|
-
check=True,
|
|
296
|
-
)
|
|
322
|
+
self._install_into_venv(venv_python, ["-e", str(self.project_dir)])
|
|
297
323
|
else:
|
|
298
324
|
logger.warning("No package definition files found, skipping editable installation")
|
|
299
325
|
|
|
326
|
+
# Point Jedi at the analysis venv so it resolves the project's third-party
|
|
327
|
+
# imports. This runs on both a fresh build and a lazy reuse of an existing
|
|
328
|
+
# venv -- previously self.virtualenv stayed None, so the install above was
|
|
329
|
+
# never actually used by the symbol-table builder. With --no-venv we leave
|
|
330
|
+
# it None so Jedi resolves against the ambient interpreter instead.
|
|
331
|
+
if not self.no_venv and venv_path.exists():
|
|
332
|
+
self.virtualenv = venv_path
|
|
333
|
+
|
|
300
334
|
if self.using_codeql:
|
|
301
335
|
logger.info(f"(Re-)initializing CodeQL analysis for {self.project_dir}")
|
|
302
336
|
|
|
@@ -358,6 +392,43 @@ class Codeanalyzer:
|
|
|
358
392
|
logger.info(f"Clearing cache directory: {self.cache_dir}")
|
|
359
393
|
shutil.rmtree(self.cache_dir)
|
|
360
394
|
|
|
395
|
+
@staticmethod
|
|
396
|
+
def _compute_external_symbols(symbol_table, call_graph):
|
|
397
|
+
"""Build the external-symbol map: every call-graph endpoint whose signature
|
|
398
|
+
is not a declared class/callable in the symbol table is an external (an
|
|
399
|
+
imported library or builtin member). ``name``/``module`` are derived from
|
|
400
|
+
the signature (best effort: split on the last dot)."""
|
|
401
|
+
declared = set()
|
|
402
|
+
|
|
403
|
+
def walk_callable(c):
|
|
404
|
+
declared.add(c.signature)
|
|
405
|
+
for ic in (c.inner_callables or {}).values():
|
|
406
|
+
walk_callable(ic)
|
|
407
|
+
for cl in (c.inner_classes or {}).values():
|
|
408
|
+
walk_class(cl)
|
|
409
|
+
|
|
410
|
+
def walk_class(cl):
|
|
411
|
+
declared.add(cl.signature)
|
|
412
|
+
for m in (cl.methods or {}).values():
|
|
413
|
+
walk_callable(m)
|
|
414
|
+
for ic in (cl.inner_classes or {}).values():
|
|
415
|
+
walk_class(ic)
|
|
416
|
+
|
|
417
|
+
for mod in symbol_table.values():
|
|
418
|
+
for c in (mod.functions or {}).values():
|
|
419
|
+
walk_callable(c)
|
|
420
|
+
for cl in (mod.classes or {}).values():
|
|
421
|
+
walk_class(cl)
|
|
422
|
+
|
|
423
|
+
externals: Dict[str, PyExternalSymbol] = {}
|
|
424
|
+
for edge in call_graph:
|
|
425
|
+
for sig in (edge.source, edge.target):
|
|
426
|
+
if sig in declared or sig in externals:
|
|
427
|
+
continue
|
|
428
|
+
module, name = sig.rsplit(".", 1) if "." in sig else (sig, sig)
|
|
429
|
+
externals[sig] = PyExternalSymbol(name=name, module=module)
|
|
430
|
+
return externals
|
|
431
|
+
|
|
361
432
|
def analyze(self) -> PyApplication:
|
|
362
433
|
"""Analyze the project and return a PyApplication with symbol table.
|
|
363
434
|
|
|
@@ -397,8 +468,19 @@ class Codeanalyzer:
|
|
|
397
468
|
jedi_edges = jedi_call_graph_edges(symbol_table)
|
|
398
469
|
call_graph = merge_edges(jedi_edges, codeql_edges)
|
|
399
470
|
|
|
471
|
+
# Classify call-graph endpoints that are not declared in the symbol table
|
|
472
|
+
# (imported library / builtin members) once, so the JSON and Neo4j backends
|
|
473
|
+
# share one authoritative external-symbol set.
|
|
474
|
+
external_symbols = self._compute_external_symbols(symbol_table, call_graph)
|
|
475
|
+
|
|
400
476
|
# Recreate pyapplication
|
|
401
|
-
app =
|
|
477
|
+
app = (
|
|
478
|
+
PyApplication.builder()
|
|
479
|
+
.symbol_table(symbol_table)
|
|
480
|
+
.call_graph(call_graph)
|
|
481
|
+
.external_symbols(external_symbols)
|
|
482
|
+
.build()
|
|
483
|
+
)
|
|
402
484
|
|
|
403
485
|
# Save to cache
|
|
404
486
|
self._save_analysis_cache(app, cache_file)
|
codeanalyzer/neo4j/bolt.py
CHANGED
|
@@ -77,6 +77,13 @@ def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool) -> None:
|
|
|
77
77
|
for stmt in [*CONSTRAINTS, *INDEXES]:
|
|
78
78
|
s.run(stmt)
|
|
79
79
|
|
|
80
|
+
# The application anchor (a shared node) — used to scope the orphan prune
|
|
81
|
+
# so it never touches modules belonging to a different :PyApplication.
|
|
82
|
+
app_name = next(
|
|
83
|
+
(n.value for n in rows.nodes if n.labels and n.labels[0] == "PyApplication"),
|
|
84
|
+
None,
|
|
85
|
+
)
|
|
86
|
+
|
|
80
87
|
# Partition nodes by owning module; shared nodes have no _module.
|
|
81
88
|
by_module: Dict[str, List[NodeRow]] = {}
|
|
82
89
|
shared: List[NodeRow] = []
|
|
@@ -135,13 +142,17 @@ def bolt_writer(rows: GraphRows, cfg: BoltConfig, full_run: bool) -> None:
|
|
|
135
142
|
_upsert_edges(session, neo4j, edges)
|
|
136
143
|
|
|
137
144
|
# 6. orphan prune — only safe on a full run (a targeted run can't tell deleted from untargeted).
|
|
138
|
-
|
|
145
|
+
# Scope to THIS application's anchor so a full run for application B never
|
|
146
|
+
# deletes application A's modules from a shared database.
|
|
147
|
+
if full_run and app_name is not None:
|
|
139
148
|
present = list(by_module.keys())
|
|
140
149
|
with session() as s:
|
|
141
150
|
res = s.run(
|
|
142
|
-
"MATCH (m:PyModule)
|
|
151
|
+
"MATCH (:PyApplication {name: $app})-[:PY_HAS_MODULE]->(m:PyModule) "
|
|
152
|
+
"WHERE NOT m.file_key IN $present "
|
|
143
153
|
f"OPTIONAL MATCH (m)-{DESCENDANTS}->(x) DETACH DELETE x, m "
|
|
144
154
|
"RETURN count(m) AS pruned",
|
|
155
|
+
app=app_name,
|
|
145
156
|
present=present,
|
|
146
157
|
)
|
|
147
158
|
pruned = res.single()
|
codeanalyzer/neo4j/catalog.py
CHANGED
|
@@ -34,7 +34,7 @@ from typing import Dict, List
|
|
|
34
34
|
|
|
35
35
|
from codeanalyzer.neo4j.schema import CONSTRAINTS, INDEXES
|
|
36
36
|
|
|
37
|
-
SCHEMA_VERSION = "1.
|
|
37
|
+
SCHEMA_VERSION = "1.1.0"
|
|
38
38
|
|
|
39
39
|
# PropType ∈ {"string", "integer", "float", "boolean", "string[]", "integer[]"}.
|
|
40
40
|
|
|
@@ -119,7 +119,7 @@ NODE_LABELS: List[NodeLabel] = [
|
|
|
119
119
|
"PyExternal",
|
|
120
120
|
"PySymbol",
|
|
121
121
|
"signature",
|
|
122
|
-
{"signature": "string", "name": "string"},
|
|
122
|
+
{"signature": "string", "name": "string", "module": "string"},
|
|
123
123
|
),
|
|
124
124
|
NodeLabel("PyPackage", "PyPackage", "name", {"name": "string"}),
|
|
125
125
|
NodeLabel(
|
codeanalyzer/neo4j/project.py
CHANGED
|
@@ -60,11 +60,12 @@ def project(app: PyApplication, app_name: str) -> GraphRows:
|
|
|
60
60
|
b.edge("PY_HAS_MODULE", app_ref, mod_ref)
|
|
61
61
|
_project_module_body(b, file_key, mod_ref, mod)
|
|
62
62
|
|
|
63
|
-
# The aggregated :PY_CALLS twin. Endpoints
|
|
64
|
-
# :PyExternal ghost nodes
|
|
63
|
+
# The aggregated :PY_CALLS twin. Endpoints listed in app.external_symbols become
|
|
64
|
+
# :PyExternal ghost nodes; the rest are declared :PySymbol nodes already emitted.
|
|
65
|
+
externals = app.external_symbols or {}
|
|
65
66
|
for e in app.call_graph:
|
|
66
|
-
src = _call_endpoint(b, e.source)
|
|
67
|
-
tgt = _call_endpoint(b, e.target)
|
|
67
|
+
src = _call_endpoint(b, e.source, externals)
|
|
68
|
+
tgt = _call_endpoint(b, e.target, externals)
|
|
68
69
|
b.edge("PY_CALLS", src, tgt, _call_edge_props(e.weight, list(e.provenance or [])))
|
|
69
70
|
|
|
70
71
|
return b.finish()
|
|
@@ -74,13 +75,20 @@ def _sym(signature: str) -> NodeRef:
|
|
|
74
75
|
return NodeRef("PySymbol", "signature", signature)
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
def _call_endpoint(b: RowBuilder, signature: str) -> NodeRef:
|
|
78
|
-
"""A call-graph endpoint: a
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
def _call_endpoint(b: RowBuilder, signature: str, externals: dict) -> NodeRef:
|
|
79
|
+
"""A call-graph endpoint: a declared callable already emitted, or an external
|
|
80
|
+
symbol (imported library / builtin member) materialized as a :PyExternal ghost.
|
|
81
|
+
|
|
82
|
+
Classification is authoritative -- it comes from ``app.external_symbols``, not a
|
|
83
|
+
"present in the graph" heuristic -- so an imported module name (which exists only
|
|
84
|
+
as a :PyPackage) can never shadow the call target. A small fallback still
|
|
85
|
+
materializes an external for any endpoint that is neither declared nor listed."""
|
|
86
|
+
ext = externals.get(signature)
|
|
87
|
+
if ext is None and b.has_key("PySymbol", signature):
|
|
81
88
|
return _sym(signature)
|
|
82
|
-
name = signature.rsplit(".", 1)[-1] if "." in signature else signature
|
|
83
|
-
|
|
89
|
+
name = ext.name if ext is not None else (signature.rsplit(".", 1)[-1] if "." in signature else signature)
|
|
90
|
+
module = ext.module if ext is not None else None
|
|
91
|
+
return b.node(["PySymbol", "PyExternal"], "signature", signature, prune({"name": name, "module": module}))
|
|
84
92
|
|
|
85
93
|
|
|
86
94
|
# ----------------------------------------------------------------------------------------------
|
codeanalyzer/neo4j/rows.py
CHANGED
|
@@ -83,7 +83,11 @@ class RowBuilder:
|
|
|
83
83
|
self._nodes: Dict[str, NodeRow] = {} # key: f"{labels[0]} {value}"
|
|
84
84
|
self._edges: List[EdgeRow] = []
|
|
85
85
|
self._deferred: List[EdgeRow] = [] # edges gated against node existence at finish()
|
|
86
|
-
|
|
86
|
+
# (merge_label, value) of every node seen, for resolved-gating. Keyed by
|
|
87
|
+
# label too so a :PyPackage name can't shadow a :PySymbol signature (and
|
|
88
|
+
# vice versa) — otherwise a call to an imported module name like ``os``
|
|
89
|
+
# resolves to a :PySymbol node that was never created and the edge is lost.
|
|
90
|
+
self._keys: set = set()
|
|
87
91
|
|
|
88
92
|
def node(self, labels: List[str], key_prop: str, value: str, props: Props) -> NodeRef:
|
|
89
93
|
"""Upsert a node. Re-seeing the same ``(labels[0], value)`` merges props
|
|
@@ -98,7 +102,7 @@ class RowBuilder:
|
|
|
98
102
|
existing.labels.append(label)
|
|
99
103
|
else:
|
|
100
104
|
self._nodes[node_id] = NodeRow(list(labels), key_prop, value, dict(props))
|
|
101
|
-
self._keys.add(value)
|
|
105
|
+
self._keys.add((labels[0], value))
|
|
102
106
|
return NodeRef(labels[0], key_prop, value)
|
|
103
107
|
|
|
104
108
|
def edge(self, type_: str, from_ref: NodeRef, to_ref: NodeRef, props: Optional[Props] = None) -> None:
|
|
@@ -121,12 +125,13 @@ class RowBuilder:
|
|
|
121
125
|
)
|
|
122
126
|
)
|
|
123
127
|
|
|
124
|
-
def has_key(self, value: str) -> bool:
|
|
125
|
-
|
|
128
|
+
def has_key(self, label: str, value: str) -> bool:
|
|
129
|
+
"""Whether a node with this ``(merge_label, value)`` identity was emitted."""
|
|
130
|
+
return (label, value) in self._keys
|
|
126
131
|
|
|
127
132
|
def finish(self) -> GraphRows:
|
|
128
133
|
for e in self._deferred:
|
|
129
|
-
if e.to_ref.value in self._keys:
|
|
134
|
+
if (e.to_ref.label, e.to_ref.value) in self._keys:
|
|
130
135
|
self._edges.append(e)
|
|
131
136
|
nodes = sorted(self._nodes.values(), key=lambda n: f"{n.labels[0]} {n.value}")
|
|
132
137
|
edges = sorted(self._edges, key=lambda e: f"{e.type} {e.from_ref.value} {e.to_ref.value}")
|
codeanalyzer/options/options.py
CHANGED
codeanalyzer/schema/__init__.py
CHANGED
|
@@ -8,6 +8,7 @@ from .py_schema import (
|
|
|
8
8
|
PyClass,
|
|
9
9
|
PyClassAttribute,
|
|
10
10
|
PyComment,
|
|
11
|
+
PyExternalSymbol,
|
|
11
12
|
PyImport,
|
|
12
13
|
PyModule,
|
|
13
14
|
PyVariableDeclaration,
|
|
@@ -15,6 +16,7 @@ from .py_schema import (
|
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
17
18
|
"PyApplication",
|
|
19
|
+
"PyExternalSymbol",
|
|
18
20
|
"PyImport",
|
|
19
21
|
"PyComment",
|
|
20
22
|
"PyModule",
|
codeanalyzer/schema/py_schema.py
CHANGED
|
@@ -358,6 +358,17 @@ class PyCallEdge(BaseModel):
|
|
|
358
358
|
provenance: List[Literal["jedi", "codeql", "joern"]] = []
|
|
359
359
|
|
|
360
360
|
|
|
361
|
+
@builder
|
|
362
|
+
@msgpk
|
|
363
|
+
class PyExternalSymbol(BaseModel):
|
|
364
|
+
"""A call-graph target outside the analyzed project -- an imported library or
|
|
365
|
+
builtin member. Mirrors codeanalyzer-typescript's ``TSExternalSymbol`` and is
|
|
366
|
+
keyed in ``PyApplication.external_symbols`` by its call-graph signature."""
|
|
367
|
+
|
|
368
|
+
name: str # the member/short name, e.g. "get" for "requests.get"
|
|
369
|
+
module: Optional[str] = None # best-effort owning module, e.g. "requests"
|
|
370
|
+
|
|
371
|
+
|
|
361
372
|
@builder
|
|
362
373
|
@msgpk
|
|
363
374
|
class PyApplication(BaseModel):
|
|
@@ -365,3 +376,7 @@ class PyApplication(BaseModel):
|
|
|
365
376
|
|
|
366
377
|
symbol_table: Dict[str, PyModule]
|
|
367
378
|
call_graph: List[PyCallEdge] = []
|
|
379
|
+
# Call-graph endpoints not declared in the symbol table (imported library /
|
|
380
|
+
# builtin members), keyed by signature. Populated by the analyzer so every
|
|
381
|
+
# backend (JSON and Neo4j) shares one authoritative external-symbol set.
|
|
382
|
+
external_symbols: Dict[str, PyExternalSymbol] = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -29,6 +29,7 @@ Requires-Dist: typer<1.0.0,>=0.9.0; python_version < '3.11'
|
|
|
29
29
|
Requires-Dist: typer<2.0.0,>=0.9.0; python_version >= '3.11'
|
|
30
30
|
Requires-Dist: typing-extensions<5.0.0,>=4.0.0; python_version < '3.11'
|
|
31
31
|
Requires-Dist: typing-extensions<6.0.0,>=4.5.0; python_version >= '3.11'
|
|
32
|
+
Requires-Dist: uv>=0.5.0
|
|
32
33
|
Provides-Extra: neo4j
|
|
33
34
|
Requires-Dist: neo4j<6.0.0,>=5.0.0; extra == 'neo4j'
|
|
34
35
|
Description-Content-Type: text/markdown
|
|
@@ -43,7 +44,8 @@ Description-Content-Type: text/markdown
|
|
|
43
44
|
|
|
44
45
|
[](https://pypi.org/project/codeanalyzer-python/)
|
|
45
46
|
[](https://pypi.org/project/codeanalyzer-python/)
|
|
46
|
-
[](https://github.com/codellm-devkit/codeanalyzer-python/releases/latest)
|
|
48
|
+
[](https://github.com/codellm-devkit/codeanalyzer-python/actions/workflows/release.yml)
|
|
47
49
|
[](./LICENSE)
|
|
48
50
|
|
|
49
51
|
</div>
|
|
@@ -70,6 +72,7 @@ and merges them with the Jedi-derived edges, also backfilling callees Jedi could
|
|
|
70
72
|
- [Prerequisites](#prerequisites)
|
|
71
73
|
- [Install via pip (PyPI)](#install-via-pip-pypi)
|
|
72
74
|
- [Install via shell script](#install-via-shell-script)
|
|
75
|
+
- [Install via Homebrew](#install-via-homebrew)
|
|
73
76
|
- [Build from source](#build-from-source)
|
|
74
77
|
- [Usage](#usage)
|
|
75
78
|
- [Options](#options)
|
|
@@ -136,6 +139,15 @@ Install the CLI as an isolated tool with the one-line installer (provisions via
|
|
|
136
139
|
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/codellm-devkit/codeanalyzer-python/releases/latest/download/canpy-installer.sh | sh
|
|
137
140
|
```
|
|
138
141
|
|
|
142
|
+
### Install via Homebrew
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
brew install codellm-devkit/tap/codeanalyzer-python
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The formula depends on [uv](https://docs.astral.sh/uv/) and installs `canpy` as an isolated,
|
|
149
|
+
version-pinned uv tool (the package and its dependencies are resolved and cached on first run).
|
|
150
|
+
|
|
139
151
|
### Build from source
|
|
140
152
|
|
|
141
153
|
This project uses [uv](https://docs.astral.sh/uv/) for dependency management.
|
|
@@ -240,6 +252,16 @@ $ canpy --help
|
|
|
240
252
|
│ in analysis. │
|
|
241
253
|
│ [default: │
|
|
242
254
|
│ skip-tests] │
|
|
255
|
+
│ --no-venv --venv Skip virtualenv │
|
|
256
|
+
│ creation and │
|
|
257
|
+
│ dependency │
|
|
258
|
+
│ installation; │
|
|
259
|
+
│ resolve imports │
|
|
260
|
+
│ against the │
|
|
261
|
+
│ ambient Python │
|
|
262
|
+
│ environment │
|
|
263
|
+
│ instead. │
|
|
264
|
+
│ [default: venv] │
|
|
243
265
|
│ --file-name PATH Analyze only the │
|
|
244
266
|
│ specified file │
|
|
245
267
|
│ (relative to │
|
|
@@ -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=UHSBihqrJVQrx-ldp4HBMJQAUouDsiuEakyYt6oWVTw,8444
|
|
3
|
+
codeanalyzer/core.py,sha256=O7hW2Y65hzdxR3Rk1Gt2Mwb63pBGZ-zCk3gyAxb3CRM,33976
|
|
4
4
|
codeanalyzer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
codeanalyzer/config/__init__.py,sha256=9XBxAn1oWGRuhg3bEBUuVGs3hFNXEAKrr-Ce7tq9a2k,61
|
|
6
6
|
codeanalyzer/config/config.py,sha256=ZiKzc5uEUCIvih58-6BDtLLI1hPij41wGQjBcj9KNQM,188
|
|
7
7
|
codeanalyzer/jedi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
codeanalyzer/jedi/jedi.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
codeanalyzer/neo4j/__init__.py,sha256=AcbFNAMuXwkMFWH4h_HCmla6PCKTF3Xe5yNqg8F_kYk,1575
|
|
10
|
-
codeanalyzer/neo4j/bolt.py,sha256
|
|
11
|
-
codeanalyzer/neo4j/catalog.py,sha256=
|
|
10
|
+
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
13
|
codeanalyzer/neo4j/emit.py,sha256=WtCndN6mA6PIzfzdgv9Xc5S5WP4rHUXCtB_r3G16rkg,3101
|
|
14
|
-
codeanalyzer/neo4j/project.py,sha256=
|
|
15
|
-
codeanalyzer/neo4j/rows.py,sha256=
|
|
14
|
+
codeanalyzer/neo4j/project.py,sha256=2GDHkFjmWibrpMaOiDMo21K6Lnj3aefl2QPCM7ANA7g,12485
|
|
15
|
+
codeanalyzer/neo4j/rows.py,sha256=5xI3X-l-vwPe_gmKYUg7VuUQARcOlVAZnGmiQr9QyRk,7326
|
|
16
16
|
codeanalyzer/neo4j/schema.py,sha256=5xz8cZVuL73GAF-vs9QtN2TuKxIty0rHEe68nwQmLTY,2136
|
|
17
17
|
codeanalyzer/options/__init__.py,sha256=FNBGxdnESayUb0wEs395MKIJxoWIX7bp-FaLDP6qYUw,123
|
|
18
|
-
codeanalyzer/options/options.py,sha256=
|
|
19
|
-
codeanalyzer/schema/__init__.py,sha256=
|
|
20
|
-
codeanalyzer/schema/py_schema.py,sha256=
|
|
18
|
+
codeanalyzer/options/options.py,sha256=JfHGU1NwXL_XCkQQsxMM94iuRL96J8kOYAa0pV5bmBM,1275
|
|
19
|
+
codeanalyzer/schema/__init__.py,sha256=cLPjvowrnz8xzi7tZAsKQeIOjdOKRGHy4I7wbG0jHk8,2024
|
|
20
|
+
codeanalyzer/schema/py_schema.py,sha256=sWJ-hWTYpd02Am1548zNEsp5XKeK3B5MbWTqsXAMC_8,12316
|
|
21
21
|
codeanalyzer/semantic_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
codeanalyzer/semantic_analysis/call_graph.py,sha256=3hgLA1sL1YFQa4fzUz_ifVbLs1I9V2QFe7ldwN18mcg,10323
|
|
23
23
|
codeanalyzer/semantic_analysis/codeql/__init__.py,sha256=ODMkdGvs3ebJdfIZle8T4VcHoCBhH_ZehWuWFpNh3NI,1022
|
|
@@ -31,9 +31,9 @@ codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=zmHFt8pN50jG-Ex4f
|
|
|
31
31
|
codeanalyzer/utils/__init__.py,sha256=hC6VWdR5rerSqBxzu9KQHTASWqwrrYJv-CMDwrTlzkc,137
|
|
32
32
|
codeanalyzer/utils/logging.py,sha256=0vTkGSl5EZN8yhhWa_5Mrn1n_twRCSW53rNwjzQ9RbI,601
|
|
33
33
|
codeanalyzer/utils/progress_bar.py,sha256=ZHJzGiCo5q4dyXq4CtsrJeq9Ip7sD84T3yZjNX7TBys,2443
|
|
34
|
-
codeanalyzer_python-0.2.
|
|
35
|
-
codeanalyzer_python-0.2.
|
|
36
|
-
codeanalyzer_python-0.2.
|
|
37
|
-
codeanalyzer_python-0.2.
|
|
38
|
-
codeanalyzer_python-0.2.
|
|
39
|
-
codeanalyzer_python-0.2.
|
|
34
|
+
codeanalyzer_python-0.2.1.dist-info/METADATA,sha256=UN2Bnyh-t1B3EzvQk708_n86Cp5kAOvyxcLTrk26g5k,22430
|
|
35
|
+
codeanalyzer_python-0.2.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
36
|
+
codeanalyzer_python-0.2.1.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
|
|
37
|
+
codeanalyzer_python-0.2.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
38
|
+
codeanalyzer_python-0.2.1.dist-info/licenses/NOTICE,sha256=YU0Z9NDWqKY-2jfFcbxeZ6fbnzz0oZeKmnUcO8a-bcQ,901
|
|
39
|
+
codeanalyzer_python-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
{codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.2.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.2.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|