codeanalyzer-python 0.2.0__py3-none-any.whl → 0.3.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 +99 -6
- codeanalyzer/core.py +192 -215
- 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/__init__.py +2 -2
- codeanalyzer/options/options.py +21 -1
- codeanalyzer/schema/__init__.py +2 -0
- codeanalyzer/schema/py_schema.py +16 -1
- codeanalyzer/semantic_analysis/call_graph.py +24 -3
- codeanalyzer/semantic_analysis/pycg/__init__.py +20 -0
- codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +1054 -0
- codeanalyzer/semantic_analysis/{codeql/__init__.py → pycg/pycg_exceptions.py} +5 -8
- codeanalyzer/semantic_analysis/pycg/shard_planner.py +401 -0
- codeanalyzer/utils/logging.py +5 -2
- codeanalyzer/utils/progress_bar.py +15 -7
- codeanalyzer_python-0.3.0.dist-info/METADATA +550 -0
- codeanalyzer_python-0.3.0.dist-info/RECORD +38 -0
- codeanalyzer/semantic_analysis/codeql/codeql_analysis.py +0 -382
- codeanalyzer/semantic_analysis/codeql/codeql_exceptions.py +0 -12
- codeanalyzer/semantic_analysis/codeql/codeql_loader.py +0 -91
- codeanalyzer/semantic_analysis/codeql/codeql_query_runner.py +0 -185
- codeanalyzer_python-0.2.0.dist-info/METADATA +0 -393
- codeanalyzer_python-0.2.0.dist-info/RECORD +0 -39
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.3.0.dist-info}/WHEEL +0 -0
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.3.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-0.2.0.dist-info → codeanalyzer_python-0.3.0.dist-info}/licenses/NOTICE +0 -0
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/__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .options import AnalysisOptions, EmitTarget, OutputFormat
|
|
1
|
+
from .options import AnalysisOptions, EmitTarget, OutputFormat, ShardStrategy
|
|
2
2
|
|
|
3
|
-
__all__ = ["AnalysisOptions", "EmitTarget", "OutputFormat"]
|
|
3
|
+
__all__ = ["AnalysisOptions", "EmitTarget", "OutputFormat", "ShardStrategy"]
|
codeanalyzer/options/options.py
CHANGED
|
@@ -23,6 +23,20 @@ class EmitTarget(str, Enum):
|
|
|
23
23
|
SCHEMA = "schema"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class ShardStrategy(str, Enum):
|
|
27
|
+
"""How ``--pycg-shard`` groups files into shards (level 2 only).
|
|
28
|
+
|
|
29
|
+
- ``jedi`` : partition the Jedi module-dependency graph (strongly-
|
|
30
|
+
connected-component condensation + Louvain) so tightly-
|
|
31
|
+
coupled modules co-compute and few call edges are severed
|
|
32
|
+
between shards. Import cycles are never split.
|
|
33
|
+
- ``package`` : legacy one-shard-per-package-directory grouping.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
JEDI = "jedi"
|
|
37
|
+
PACKAGE = "package"
|
|
38
|
+
|
|
39
|
+
|
|
26
40
|
@dataclass
|
|
27
41
|
class AnalysisOptions:
|
|
28
42
|
input: Path
|
|
@@ -34,11 +48,17 @@ class AnalysisOptions:
|
|
|
34
48
|
neo4j_user: str = "neo4j"
|
|
35
49
|
neo4j_password: str = "neo4j"
|
|
36
50
|
neo4j_database: Optional[str] = None
|
|
37
|
-
|
|
51
|
+
analysis_level: int = 1
|
|
38
52
|
using_ray: bool = False
|
|
39
53
|
rebuild_analysis: bool = False
|
|
40
54
|
skip_tests: bool = True
|
|
55
|
+
no_venv: bool = False
|
|
41
56
|
file_name: Optional[Path] = None
|
|
42
57
|
cache_dir: Optional[Path] = None
|
|
43
58
|
clear_cache: bool = False
|
|
44
59
|
verbosity: int = 0
|
|
60
|
+
pycg_shard: bool = False
|
|
61
|
+
pycg_shard_ceiling: int = 100
|
|
62
|
+
pycg_shard_timeout: int = 120
|
|
63
|
+
pycg_shard_strategy: ShardStrategy = ShardStrategy.JEDI
|
|
64
|
+
pycg_max_iter: int = 50
|
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
|
@@ -355,7 +355,18 @@ class PyCallEdge(BaseModel):
|
|
|
355
355
|
target: str # callee's PyCallable.signature
|
|
356
356
|
type: Literal["CALL_DEP"] = "CALL_DEP"
|
|
357
357
|
weight: int = 1
|
|
358
|
-
provenance: List[Literal["jedi", "
|
|
358
|
+
provenance: List[Literal["jedi", "pycg", "joern"]] = []
|
|
359
|
+
|
|
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"
|
|
359
370
|
|
|
360
371
|
|
|
361
372
|
@builder
|
|
@@ -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] = {}
|
|
@@ -173,7 +173,7 @@ def jedi_call_graph_edges(
|
|
|
173
173
|
|
|
174
174
|
Edges are coalesced on ``(source, target)``: ``weight`` is the count of
|
|
175
175
|
matching sites. Provenance is always ``["jedi"]``; combine with
|
|
176
|
-
|
|
176
|
+
PyCG-derived edges via ``merge_edges``.
|
|
177
177
|
"""
|
|
178
178
|
counts: Counter = Counter()
|
|
179
179
|
for caller in iter_callables_in_symbol_table(symbol_table):
|
|
@@ -191,7 +191,7 @@ def jedi_call_graph_edges(
|
|
|
191
191
|
def resolve_unresolved_constructors(symbol_table: Dict[str, PyModule]) -> int:
|
|
192
192
|
"""Fill in ``PyCallsite.callee_signature`` for unresolved constructor sites.
|
|
193
193
|
|
|
194
|
-
When
|
|
194
|
+
When Jedi fails to resolve a constructor call (commonly
|
|
195
195
|
for classes nested inside functions or methods, where static-analysis
|
|
196
196
|
points-to is weakest), Jedi still flags the site as
|
|
197
197
|
``is_constructor_call=True`` with ``method_name`` set to the class's
|
|
@@ -246,12 +246,33 @@ def resolve_unresolved_constructors(symbol_table: Dict[str, PyModule]) -> int:
|
|
|
246
246
|
return resolved
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
def filter_external_edges(
|
|
250
|
+
edges: List[PyCallEdge],
|
|
251
|
+
symbol_table: Dict[str, PyModule],
|
|
252
|
+
) -> List[PyCallEdge]:
|
|
253
|
+
"""Remove edges where both source and target are outside the app namespace.
|
|
254
|
+
|
|
255
|
+
Edges where an app callable calls a library function (or vice-versa) are
|
|
256
|
+
retained; only lib→lib edges are dropped. The app symbol set is built by
|
|
257
|
+
walking every callable in the symbol table recursively (including nested
|
|
258
|
+
functions and closures via ``inner_callables``) plus every class, so
|
|
259
|
+
PyCG-discovered closure nodes are correctly recognised as app symbols.
|
|
260
|
+
"""
|
|
261
|
+
app_symbols: set = {c.signature for c in iter_callables_in_symbol_table(symbol_table)}
|
|
262
|
+
app_symbols.update(cls.signature for cls in iter_classes_in_symbol_table(symbol_table))
|
|
263
|
+
|
|
264
|
+
return [
|
|
265
|
+
e for e in edges
|
|
266
|
+
if e.source in app_symbols or e.target in app_symbols
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
|
|
249
270
|
def merge_edges(*edge_lists: list) -> list:
|
|
250
271
|
"""Merge multiple ``List[PyCallEdge]`` into one.
|
|
251
272
|
|
|
252
273
|
Edges with the same ``(source, target)`` are coalesced: weights sum,
|
|
253
274
|
provenance is the sorted union. Useful for combining edges produced
|
|
254
|
-
by different backends (e.g. Jedi +
|
|
275
|
+
by different backends (e.g. Jedi + PyCG).
|
|
255
276
|
"""
|
|
256
277
|
by_key: Dict[Tuple[str, str], PyCallEdge] = {}
|
|
257
278
|
for edges in edge_lists:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Copyright IBM Corporation 2025
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
################################################################################
|
|
16
|
+
|
|
17
|
+
from codeanalyzer.semantic_analysis.pycg.pycg_analysis import PyCG
|
|
18
|
+
from codeanalyzer.semantic_analysis.pycg.pycg_exceptions import PyCGExceptions
|
|
19
|
+
|
|
20
|
+
__all__ = ["PyCG", "PyCGExceptions"]
|