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
codeanalyzer/neo4j/schema.py
CHANGED
|
@@ -14,26 +14,308 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
################################################################################
|
|
16
16
|
|
|
17
|
-
"""The Cypher DDL — uniqueness constraints and indexes — shared by both writers.
|
|
18
|
-
Run BEFORE any load so MERGE uses an index seek (not a label scan) and the
|
|
19
|
-
identity invariant is enforced by the database. Every statement is idempotent
|
|
20
|
-
(``IF NOT EXISTS``).
|
|
21
17
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
The declarative Neo4j schema — the single in-repo source of truth for the graph contract: node
|
|
19
|
+
labels with their keys and typed properties, relationship types and their endpoints, and the
|
|
20
|
+
Cypher DDL (uniqueness constraints + indexes). The constraints are DERIVED from the node labels
|
|
21
|
+
(one per distinct mergeLabel/key) so a new label brings its own constraint — there is no second
|
|
22
|
+
list to keep in sync. `--emit schema` serializes all of this to a machine-readable schema.json,
|
|
23
|
+
and the conformance test (``test/test_neo4j_schema.py``) asserts the real emitter never produces a
|
|
24
|
+
label / relationship / property that isn't declared here — so this file cannot silently drift
|
|
25
|
+
from :mod:`codeanalyzer.neo4j.project`.
|
|
26
|
+
|
|
27
|
+
SCHEMA_VERSION is the contract version: bump MAJOR on a breaking change (renamed/removed label,
|
|
28
|
+
relationship or key), MINOR on an additive change (new label/rel/property). It is stamped onto
|
|
29
|
+
the :PyApplication node of every emitted graph so any consumer can detect a producer/consumer
|
|
30
|
+
mismatch at runtime.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import Dict, List
|
|
37
|
+
|
|
38
|
+
SCHEMA_VERSION = "2.0.0"
|
|
39
|
+
|
|
40
|
+
# PropType ∈ {"string", "integer", "float", "boolean", "string[]", "integer[]"}.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class NodeLabel:
|
|
45
|
+
label: str # the specific label (also the catalog key)
|
|
46
|
+
merge_label: str # the label the uniqueness constraint / MERGE is on
|
|
47
|
+
key: str
|
|
48
|
+
properties: Dict[str, str]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class RelType:
|
|
53
|
+
type: str
|
|
54
|
+
from_labels: List[str]
|
|
55
|
+
to_labels: List[str]
|
|
56
|
+
properties: Dict[str, str] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Labels layered onto a node in addition to its primary/specific label.
|
|
60
|
+
MARKER_LABELS: List[str] = []
|
|
61
|
+
|
|
62
|
+
_SPAN = {"start_line": "integer", "end_line": "integer"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
NODE_LABELS: List[NodeLabel] = [
|
|
66
|
+
NodeLabel(
|
|
67
|
+
"PyApplication",
|
|
68
|
+
"PyApplication",
|
|
69
|
+
"name",
|
|
70
|
+
{
|
|
71
|
+
"name": "string",
|
|
72
|
+
"schema_version": "string",
|
|
73
|
+
"analyzer_name": "string",
|
|
74
|
+
"analyzer_version": "string",
|
|
75
|
+
"repo_uri": "string",
|
|
76
|
+
"source_revision": "string",
|
|
77
|
+
"repo_dirty": "boolean",
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
NodeLabel(
|
|
81
|
+
"PyModule",
|
|
82
|
+
"PyModule",
|
|
83
|
+
"id",
|
|
84
|
+
{
|
|
85
|
+
"id": "string",
|
|
86
|
+
"file_key": "string",
|
|
87
|
+
"module_name": "string",
|
|
88
|
+
"content_hash": "string",
|
|
89
|
+
"last_modified": "float",
|
|
90
|
+
"file_size": "integer",
|
|
91
|
+
"_module": "string",
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
NodeLabel(
|
|
95
|
+
"PyClass",
|
|
96
|
+
"PySymbol",
|
|
97
|
+
"id",
|
|
98
|
+
{
|
|
99
|
+
"id": "string",
|
|
100
|
+
"signature": "string",
|
|
101
|
+
"name": "string",
|
|
102
|
+
"code": "string",
|
|
103
|
+
"base_classes": "string[]",
|
|
104
|
+
"docstring": "string",
|
|
105
|
+
**_SPAN,
|
|
106
|
+
"_module": "string",
|
|
107
|
+
},
|
|
108
|
+
),
|
|
109
|
+
NodeLabel(
|
|
110
|
+
"PyCallable",
|
|
111
|
+
"PySymbol",
|
|
112
|
+
"id",
|
|
113
|
+
{
|
|
114
|
+
"id": "string",
|
|
115
|
+
"signature": "string",
|
|
116
|
+
"name": "string",
|
|
117
|
+
"path": "string",
|
|
118
|
+
"return_type": "string",
|
|
119
|
+
"cyclomatic_complexity": "integer",
|
|
120
|
+
"code": "string",
|
|
121
|
+
"code_start_line": "integer",
|
|
122
|
+
**_SPAN,
|
|
123
|
+
"docstring": "string",
|
|
124
|
+
"decorators": "string[]",
|
|
125
|
+
"parameters_json": "string",
|
|
126
|
+
"accessed_symbols_json": "string",
|
|
127
|
+
"_module": "string",
|
|
128
|
+
},
|
|
129
|
+
),
|
|
130
|
+
NodeLabel(
|
|
131
|
+
"PyExternal",
|
|
132
|
+
"PySymbol",
|
|
133
|
+
"id",
|
|
134
|
+
{"id": "string", "name": "string", "module": "string"},
|
|
135
|
+
),
|
|
136
|
+
NodeLabel("PyPackage", "PyPackage", "name", {"name": "string"}),
|
|
137
|
+
NodeLabel(
|
|
138
|
+
"PyDecorator",
|
|
139
|
+
"PyDecorator",
|
|
140
|
+
"name",
|
|
141
|
+
{"name": "string"},
|
|
142
|
+
),
|
|
143
|
+
NodeLabel(
|
|
144
|
+
"PyCallSite",
|
|
145
|
+
"PyCallSite",
|
|
146
|
+
"id",
|
|
147
|
+
{
|
|
148
|
+
"id": "string",
|
|
149
|
+
"method_name": "string",
|
|
150
|
+
"receiver_expr": "string",
|
|
151
|
+
"receiver_type": "string",
|
|
152
|
+
"argument_types": "string[]",
|
|
153
|
+
"arguments_json": "string",
|
|
154
|
+
"return_type": "string",
|
|
155
|
+
"callee_signature": "string",
|
|
156
|
+
"is_constructor_call": "boolean",
|
|
157
|
+
"start_line": "integer",
|
|
158
|
+
"start_column": "integer",
|
|
159
|
+
"end_line": "integer",
|
|
160
|
+
"end_column": "integer",
|
|
161
|
+
"_module": "string",
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
NodeLabel(
|
|
165
|
+
"PyAttribute",
|
|
166
|
+
"PyAttribute",
|
|
167
|
+
"id",
|
|
168
|
+
{
|
|
169
|
+
"id": "string",
|
|
170
|
+
"name": "string",
|
|
171
|
+
"type": "string",
|
|
172
|
+
"initializer": "string",
|
|
173
|
+
"docstring": "string",
|
|
174
|
+
**_SPAN,
|
|
175
|
+
"_module": "string",
|
|
176
|
+
},
|
|
177
|
+
),
|
|
178
|
+
NodeLabel(
|
|
179
|
+
"PyVariable",
|
|
180
|
+
"PyVariable",
|
|
181
|
+
"id",
|
|
182
|
+
{
|
|
183
|
+
"id": "string",
|
|
184
|
+
"name": "string",
|
|
185
|
+
"type": "string",
|
|
186
|
+
"initializer": "string",
|
|
187
|
+
"scope": "string",
|
|
188
|
+
**_SPAN,
|
|
189
|
+
"_module": "string",
|
|
190
|
+
},
|
|
191
|
+
),
|
|
192
|
+
# Level-3 CPG overlay (present only at -a 3). The dataflow vocabulary is
|
|
193
|
+
# shared cross-language in *shape* (same suffixes, props, semantics) but
|
|
194
|
+
# namespaced per language like every other row family — a multi-language
|
|
195
|
+
# Neo4j database must never mingle one analyzer's dependence edges with
|
|
196
|
+
# another's. `id` = "<signature>#<node_id>"; parameter-passing nodes
|
|
197
|
+
# (formal/actual in/out) ride the same label with `var`/`call_node`.
|
|
198
|
+
NodeLabel(
|
|
199
|
+
"PyCFGNode",
|
|
200
|
+
"PyCFGNode",
|
|
201
|
+
"id",
|
|
202
|
+
{
|
|
203
|
+
"id": "string",
|
|
204
|
+
"kind": "string",
|
|
205
|
+
"var": "string",
|
|
206
|
+
"call_node": "string",
|
|
207
|
+
**_SPAN,
|
|
208
|
+
"_module": "string",
|
|
209
|
+
},
|
|
210
|
+
),
|
|
33
211
|
]
|
|
34
212
|
|
|
213
|
+
_DECL_TARGETS = ["PyClass", "PyCallable"]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
REL_TYPES: List[RelType] = [
|
|
217
|
+
RelType("PY_HAS_MODULE", ["PyApplication"], ["PyModule"]),
|
|
218
|
+
RelType("PY_DECLARES", ["PyModule", "PyClass", "PyCallable"], _DECL_TARGETS),
|
|
219
|
+
RelType("PY_HAS_METHOD", ["PyClass"], ["PyCallable"]),
|
|
220
|
+
RelType("PY_HAS_ATTRIBUTE", ["PyClass"], ["PyAttribute"]),
|
|
221
|
+
RelType("PY_DECLARES_VAR", ["PyModule", "PyCallable"], ["PyVariable"]),
|
|
222
|
+
RelType("PY_HAS_CALLSITE", ["PyCallable"], ["PyCallSite"]),
|
|
223
|
+
RelType("PY_RESOLVES_TO", ["PyCallSite"], ["PyCallable", "PyExternal"]),
|
|
224
|
+
RelType(
|
|
225
|
+
"PY_CALLS",
|
|
226
|
+
["PyCallable", "PyExternal"],
|
|
227
|
+
["PyCallable", "PyExternal"],
|
|
228
|
+
{"weight": "integer", "prov": "string[]"},
|
|
229
|
+
),
|
|
230
|
+
RelType("PY_EXTENDS", ["PyClass"], ["PyClass"]),
|
|
231
|
+
RelType(
|
|
232
|
+
"PY_IMPORTS",
|
|
233
|
+
["PyModule"],
|
|
234
|
+
["PyModule", "PyPackage"],
|
|
235
|
+
{"spellings": "string[]", "imported_names": "string[]", "aliases": "string[]"},
|
|
236
|
+
),
|
|
237
|
+
RelType("PY_DECORATED_BY", ["PyCallable"], ["PyDecorator"]),
|
|
238
|
+
# Level-3 CPG overlay (-a 3 only): the cross-language dataflow vocabulary,
|
|
239
|
+
# PY_-namespaced so per-language SDK backends can scope their queries.
|
|
240
|
+
RelType("PY_HAS_CFG_NODE", ["PyCallable"], ["PyCFGNode"]),
|
|
241
|
+
# ``_k`` is the relationship-identity discriminant (internal, underscore-
|
|
242
|
+
# prefixed like ``_module``): PY_CFG_NEXT merges per ``kind`` (a conditional's
|
|
243
|
+
# true/false pair), PY_DDG per ``(var, prov)`` (one dependence per variable,
|
|
244
|
+
# and the ssa/points-to split) — a plain endpoint-pair MERGE would collapse
|
|
245
|
+
# legitimately-distinct edges.
|
|
246
|
+
RelType("PY_CFG_NEXT", ["PyCFGNode"], ["PyCFGNode"], {"kind": "string", "_k": "string"}),
|
|
247
|
+
RelType("PY_CDG", ["PyCFGNode"], ["PyCFGNode"]),
|
|
248
|
+
RelType("PY_DDG", ["PyCFGNode"], ["PyCFGNode"], {"var": "string", "prov": "string[]", "_k": "string"}),
|
|
249
|
+
RelType("PY_PARAM_IN", ["PyCFGNode"], ["PyCFGNode"], {"var": "string"}),
|
|
250
|
+
RelType("PY_PARAM_OUT", ["PyCFGNode"], ["PyCFGNode"], {"var": "string"}),
|
|
251
|
+
RelType("PY_SUMMARY", ["PyCFGNode"], ["PyCFGNode"]),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def uniqueness_constraints() -> list[str]:
|
|
256
|
+
"""One uniqueness constraint per distinct (merge_label, key)."""
|
|
257
|
+
seen: set[tuple[str, str]] = set()
|
|
258
|
+
out: list[str] = []
|
|
259
|
+
|
|
260
|
+
for node in NODE_LABELS:
|
|
261
|
+
identifier = (node.merge_label, node.key)
|
|
262
|
+
if identifier in seen:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
seen.add(identifier)
|
|
266
|
+
out.append(
|
|
267
|
+
f"CREATE CONSTRAINT {node.merge_label.lower()}_{node.key} "
|
|
268
|
+
f"IF NOT EXISTS FOR (x:{node.merge_label}) "
|
|
269
|
+
f"REQUIRE x.{node.key} IS UNIQUE"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return out
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
CONSTRAINTS: List[str] = uniqueness_constraints()
|
|
276
|
+
|
|
35
277
|
INDEXES: List[str] = [
|
|
36
278
|
"CREATE INDEX py_callable_name IF NOT EXISTS FOR (c:PyCallable) ON (c.name)",
|
|
37
279
|
"CREATE INDEX py_class_name IF NOT EXISTS FOR (c:PyClass) ON (c.name)",
|
|
38
280
|
"CREATE FULLTEXT INDEX py_code_fts IF NOT EXISTS FOR (c:PyCallable) ON EACH [c.code, c.docstring]",
|
|
39
281
|
]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@dataclass
|
|
285
|
+
class SchemaDocument:
|
|
286
|
+
schema_version: str
|
|
287
|
+
generator: str
|
|
288
|
+
marker_labels: List[str]
|
|
289
|
+
node_labels: List[NodeLabel]
|
|
290
|
+
relationship_types: List[RelType]
|
|
291
|
+
constraints: List[str]
|
|
292
|
+
indexes: List[str]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def build_schema_document() -> dict:
|
|
296
|
+
"""Build the full machine-readable schema document emitted by ``--emit schema``."""
|
|
297
|
+
return {
|
|
298
|
+
"schema_version": SCHEMA_VERSION,
|
|
299
|
+
"generator": "codeanalyzer-python",
|
|
300
|
+
"marker_labels": list(MARKER_LABELS),
|
|
301
|
+
"node_labels": [
|
|
302
|
+
{
|
|
303
|
+
"label": n.label,
|
|
304
|
+
"merge_label": n.merge_label,
|
|
305
|
+
"key": n.key,
|
|
306
|
+
"properties": n.properties,
|
|
307
|
+
}
|
|
308
|
+
for n in NODE_LABELS
|
|
309
|
+
],
|
|
310
|
+
"relationship_types": [
|
|
311
|
+
{
|
|
312
|
+
"type": r.type,
|
|
313
|
+
"from": r.from_labels,
|
|
314
|
+
"to": r.to_labels,
|
|
315
|
+
"properties": r.properties,
|
|
316
|
+
}
|
|
317
|
+
for r in REL_TYPES
|
|
318
|
+
],
|
|
319
|
+
"constraints": list(CONSTRAINTS),
|
|
320
|
+
"indexes": list(INDEXES),
|
|
321
|
+
}
|
codeanalyzer/options/options.py
CHANGED
|
@@ -49,6 +49,10 @@ class AnalysisOptions:
|
|
|
49
49
|
neo4j_password: str = "neo4j"
|
|
50
50
|
neo4j_database: Optional[str] = None
|
|
51
51
|
analysis_level: int = 1
|
|
52
|
+
# Level-3 dataflow knobs: which program graphs to emit (csv of
|
|
53
|
+
# cfg|dfg|pdg|sdg) and the access-path k-limit.
|
|
54
|
+
graphs: str = "cfg,dfg,pdg,sdg"
|
|
55
|
+
graph_field_depth: int = 3
|
|
52
56
|
using_ray: bool = False
|
|
53
57
|
rebuild_analysis: bool = False
|
|
54
58
|
skip_tests: bool = True
|
|
@@ -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/__init__.py
CHANGED
|
@@ -2,6 +2,12 @@ from importlib.metadata import version, PackageNotFoundError
|
|
|
2
2
|
from packaging.version import parse as parse_version
|
|
3
3
|
|
|
4
4
|
from .py_schema import (
|
|
5
|
+
Analysis,
|
|
6
|
+
BodyNode,
|
|
7
|
+
CdgEdge,
|
|
8
|
+
CfgEdge,
|
|
9
|
+
DdgEdge,
|
|
10
|
+
ParamEdge,
|
|
5
11
|
PyApplication,
|
|
6
12
|
PyCallable,
|
|
7
13
|
PyCallableParameter,
|
|
@@ -12,9 +18,12 @@ from .py_schema import (
|
|
|
12
18
|
PyImport,
|
|
13
19
|
PyModule,
|
|
14
20
|
PyVariableDeclaration,
|
|
21
|
+
Span,
|
|
22
|
+
SummaryEdge,
|
|
15
23
|
)
|
|
16
24
|
|
|
17
25
|
__all__ = [
|
|
26
|
+
"Analysis",
|
|
18
27
|
"PyApplication",
|
|
19
28
|
"PyExternalSymbol",
|
|
20
29
|
"PyImport",
|
|
@@ -25,6 +34,13 @@ __all__ = [
|
|
|
25
34
|
"PyCallable",
|
|
26
35
|
"PyClassAttribute",
|
|
27
36
|
"PyCallableParameter",
|
|
37
|
+
"Span",
|
|
38
|
+
"BodyNode",
|
|
39
|
+
"CfgEdge",
|
|
40
|
+
"CdgEdge",
|
|
41
|
+
"DdgEdge",
|
|
42
|
+
"SummaryEdge",
|
|
43
|
+
"ParamEdge",
|
|
28
44
|
]
|
|
29
45
|
|
|
30
46
|
try:
|
|
@@ -44,6 +60,7 @@ if not PYDANTIC_V2:
|
|
|
44
60
|
PyClass=PyClass,
|
|
45
61
|
PyModule=PyModule
|
|
46
62
|
)
|
|
63
|
+
Analysis.update_forward_refs(PyApplication=PyApplication)
|
|
47
64
|
|
|
48
65
|
# Compatibility helpers for Pydantic v1/v2
|
|
49
66
|
def model_dump_json(model, **kwargs):
|
|
@@ -55,6 +72,8 @@ def model_dump_json(model, **kwargs):
|
|
|
55
72
|
v1_kwargs = {}
|
|
56
73
|
if 'indent' in kwargs:
|
|
57
74
|
v1_kwargs['indent'] = kwargs['indent']
|
|
75
|
+
if 'exclude_none' in kwargs:
|
|
76
|
+
v1_kwargs['exclude_none'] = kwargs['exclude_none']
|
|
58
77
|
if 'separators' in kwargs:
|
|
59
78
|
# In v1, separators is passed to dumps_kwargs
|
|
60
79
|
v1_kwargs['separators'] = kwargs['separators']
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Walk the symbol-table tree and stamp every node with its can:// id."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from codeanalyzer.schema import ids
|
|
5
|
+
from codeanalyzer.schema.py_schema import PyApplication, PyModule, PyClass, PyCallable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def assign_ids(app: PyApplication, app_name: str) -> Dict[str, str]:
|
|
9
|
+
"""Sets `.id` on the app + every module/class/callable. Returns a
|
|
10
|
+
`signature -> can://id` map for later stages (identity layer input)."""
|
|
11
|
+
app.id = ids.application_id(app_name); app.kind = "application"
|
|
12
|
+
sig_to_id: Dict[str, str] = {}
|
|
13
|
+
|
|
14
|
+
def do_callable(parent_id: str, c: PyCallable) -> None:
|
|
15
|
+
seg = ids.callable_sig_segment(c.name, [p.name for p in c.parameters])
|
|
16
|
+
c.id = ids.child_id(parent_id, seg)
|
|
17
|
+
sig_to_id[c.signature] = c.id
|
|
18
|
+
for ic in (c.callables or {}).values():
|
|
19
|
+
do_callable(c.id, ic)
|
|
20
|
+
for icl in (c.types or {}).values():
|
|
21
|
+
do_class(c.id, icl)
|
|
22
|
+
|
|
23
|
+
def do_class(parent_id: str, cl: PyClass) -> None:
|
|
24
|
+
cl.id = ids.child_id(parent_id, cl.name); cl.kind = "class"
|
|
25
|
+
sig_to_id[cl.signature] = cl.id
|
|
26
|
+
for m in (cl.callables or {}).values():
|
|
27
|
+
do_callable(cl.id, m)
|
|
28
|
+
for ic in (cl.types or {}).values():
|
|
29
|
+
do_class(cl.id, ic)
|
|
30
|
+
|
|
31
|
+
for file_key, mod in app.symbol_table.items():
|
|
32
|
+
mod.id = ids.module_id(app_name, file_key); mod.kind = "module"
|
|
33
|
+
for fn in (mod.functions or {}).values():
|
|
34
|
+
do_callable(mod.id, fn)
|
|
35
|
+
for cl in (mod.types or {}).values():
|
|
36
|
+
do_class(mod.id, cl)
|
|
37
|
+
return sig_to_id
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Re-identify call-graph edge endpoints onto canonical can:// ids so the JSON
|
|
2
|
+
call_graph agrees with the Neo4j PY_CALLS projection. Declared endpoints map
|
|
3
|
+
through sig_to_id; external/library endpoints keep their dotted signature
|
|
4
|
+
(they have no can:// id)."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from codeanalyzer.schema.py_schema import PyApplication
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def reidentify_call_graph(app: PyApplication, sig_to_id: dict) -> None:
|
|
10
|
+
for edge in app.call_graph or []:
|
|
11
|
+
edge.src = sig_to_id.get(edge.src, edge.src)
|
|
12
|
+
edge.dst = sig_to_id.get(edge.dst, edge.dst)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Canonical `can://` id construction for schema v2 (durable ids, ≥ callable).
|
|
2
|
+
Ordinal ids (< callable) are `ordinal_id(callable_id, tag)`. Pure functions;
|
|
3
|
+
ids are opaque handles (the <file> segment itself contains '/')."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
_SCHEME = "can://python"
|
|
8
|
+
|
|
9
|
+
def application_id(app_name: str) -> str:
|
|
10
|
+
return f"{_SCHEME}/{app_name}"
|
|
11
|
+
|
|
12
|
+
def module_id(app_name: str, file_key: str) -> str:
|
|
13
|
+
rel = file_key.replace("\\", "/").lstrip("./")
|
|
14
|
+
return f"{application_id(app_name)}/{rel}"
|
|
15
|
+
|
|
16
|
+
def child_id(parent_id: str, segment: str) -> str:
|
|
17
|
+
return f"{parent_id}/{segment}"
|
|
18
|
+
|
|
19
|
+
def callable_sig_segment(name: str, param_names: List[str]) -> str:
|
|
20
|
+
return f"{name}({','.join(param_names)})"
|
|
21
|
+
|
|
22
|
+
def ordinal_id(callable_id: str, tag: str) -> str:
|
|
23
|
+
return f"{callable_id}@{tag}"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""L1 body population: materialize `call` nodes from existing call sites.
|
|
2
|
+
`callee` is left None here — the sanctioned null→id refinement happens at L2."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from codeanalyzer.schema.py_schema import PyApplication, PyClass, PyCallable, BodyNode, Span, byte_offsets
|
|
5
|
+
|
|
6
|
+
def _do_callable(source: str, c: PyCallable) -> None:
|
|
7
|
+
for cs in c.call_sites or []:
|
|
8
|
+
key = f"{cs.start_line}:{cs.start_column}"
|
|
9
|
+
span = Span(start=(cs.start_line, cs.start_column),
|
|
10
|
+
end=(cs.end_line, cs.end_column),
|
|
11
|
+
bytes=byte_offsets(source, cs.start_line, cs.start_column, cs.end_line, cs.end_column)) if source else None
|
|
12
|
+
c.body[key] = BodyNode(kind="call", span=span, callee=None)
|
|
13
|
+
for ic in (c.callables or {}).values():
|
|
14
|
+
_do_callable(source, ic)
|
|
15
|
+
for icl in (c.types or {}).values():
|
|
16
|
+
_do_class(source, icl)
|
|
17
|
+
|
|
18
|
+
def _do_class(source: str, cl: PyClass) -> None:
|
|
19
|
+
for m in (cl.callables or {}).values():
|
|
20
|
+
_do_callable(source, m)
|
|
21
|
+
for ic in (cl.types or {}).values():
|
|
22
|
+
_do_class(source, ic)
|
|
23
|
+
|
|
24
|
+
def populate_l1_body(app: PyApplication) -> None:
|
|
25
|
+
for mod in app.symbol_table.values():
|
|
26
|
+
for fn in (mod.functions or {}).values():
|
|
27
|
+
_do_callable(mod.source, fn)
|
|
28
|
+
for cl in (mod.types or {}).values():
|
|
29
|
+
_do_class(mod.source, cl)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""L2 refinement: fill each L1 `call` body node's `callee` (null→id) from the
|
|
2
|
+
call site's resolved signature — the one sanctioned value change. A declared
|
|
3
|
+
target becomes its can:// id; an external/library target keeps its dotted
|
|
4
|
+
signature; an unresolved call site leaves `callee` absent."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from codeanalyzer.schema.py_schema import PyApplication, PyClass, PyCallable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _do_callable(c: PyCallable, sig_to_id: dict) -> None:
|
|
10
|
+
for cs in c.call_sites or []:
|
|
11
|
+
if cs.callee_signature is None:
|
|
12
|
+
continue
|
|
13
|
+
key = f"{cs.start_line}:{cs.start_column}"
|
|
14
|
+
node = c.body.get(key)
|
|
15
|
+
if node is None or node.kind != "call":
|
|
16
|
+
continue
|
|
17
|
+
node.callee = sig_to_id.get(cs.callee_signature, cs.callee_signature)
|
|
18
|
+
for ic in (c.callables or {}).values():
|
|
19
|
+
_do_callable(ic, sig_to_id)
|
|
20
|
+
for icl in (c.types or {}).values():
|
|
21
|
+
_do_class(icl, sig_to_id)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _do_class(cl: PyClass, sig_to_id: dict) -> None:
|
|
25
|
+
for m in (cl.callables or {}).values():
|
|
26
|
+
_do_callable(m, sig_to_id)
|
|
27
|
+
for ic in (cl.types or {}).values():
|
|
28
|
+
_do_class(ic, sig_to_id)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def backfill_callees(app: PyApplication, sig_to_id: dict) -> None:
|
|
32
|
+
for mod in app.symbol_table.values():
|
|
33
|
+
for fn in (mod.functions or {}).values():
|
|
34
|
+
_do_callable(fn, sig_to_id)
|
|
35
|
+
for cl in (mod.types or {}).values():
|
|
36
|
+
_do_class(cl, sig_to_id)
|