codeanalyzer-python 1.1.0__py3-none-any.whl → 1.2.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 +95 -123
- codeanalyzer/core.py +21 -45
- codeanalyzer/dataflow/access_paths.py +26 -4
- codeanalyzer/dataflow/builder.py +65 -1
- codeanalyzer/dataflow/identity.py +1 -1
- codeanalyzer/dataflow/pdg.py +7 -2
- codeanalyzer/dataflow/scc.py +1 -1
- codeanalyzer/entrypoints/__init__.py +3 -0
- codeanalyzer/entrypoints/detect.py +124 -0
- codeanalyzer/entrypoints/matching.py +182 -0
- codeanalyzer/entrypoints/pipeline.py +131 -0
- codeanalyzer/entrypoints/rules.py +159 -0
- codeanalyzer/entrypoints/rules.yml +88 -0
- codeanalyzer/neo4j/bolt.py +1 -1
- codeanalyzer/neo4j/project.py +85 -60
- codeanalyzer/neo4j/schema.py +35 -34
- codeanalyzer/options/__init__.py +2 -2
- codeanalyzer/options/options.py +2 -26
- codeanalyzer/schema/__init__.py +48 -0
- codeanalyzer/schema/l1_body.py +11 -1
- codeanalyzer/schema/l2_callees.py +29 -13
- codeanalyzer/schema/py_schema.py +95 -103
- codeanalyzer/semantic_analysis/call_graph.py +20 -4
- codeanalyzer/semantic_analysis/defuse_linker.py +1499 -0
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +88 -3
- {codeanalyzer_python-1.1.0.dist-info → codeanalyzer_python-1.2.0.dist-info}/METADATA +36 -161
- {codeanalyzer_python-1.1.0.dist-info → codeanalyzer_python-1.2.0.dist-info}/RECORD +31 -30
- {codeanalyzer_python-1.1.0.dist-info → codeanalyzer_python-1.2.0.dist-info}/WHEEL +1 -1
- codeanalyzer/config/__init__.py +0 -3
- codeanalyzer/config/config.py +0 -8
- codeanalyzer/semantic_analysis/pycg/__init__.py +0 -20
- codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +0 -1115
- codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -23
- codeanalyzer/semantic_analysis/pycg/shard_planner.py +0 -401
- {codeanalyzer_python-1.1.0.dist-info → codeanalyzer_python-1.2.0.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-1.1.0.dist-info → codeanalyzer_python-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-1.1.0.dist-info → codeanalyzer_python-1.2.0.dist-info}/licenses/NOTICE +0 -0
codeanalyzer/schema/py_schema.py
CHANGED
|
@@ -22,85 +22,8 @@ for static analysis purposes.
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any, Dict, List, Optional, Tuple
|
|
25
|
-
import gzip
|
|
26
|
-
|
|
27
25
|
from pydantic import BaseModel
|
|
28
26
|
from typing_extensions import Literal
|
|
29
|
-
import msgpack
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def msgpk(cls):
|
|
33
|
-
"""
|
|
34
|
-
Decorator that adds MessagePack serialization methods to Pydantic models.
|
|
35
|
-
|
|
36
|
-
Adds methods:
|
|
37
|
-
- to_msgpack_bytes() -> bytes: Serialize to compact binary format
|
|
38
|
-
- from_msgpack_bytes(data: bytes) -> cls: Deserialize from binary format
|
|
39
|
-
- to_msgpack_dict() -> dict: Convert to msgpack-compatible dict
|
|
40
|
-
- from_msgpack_dict(data: dict) -> cls: Create instance from msgpack dict
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def _prepare_for_serialization(obj: Any) -> Any:
|
|
44
|
-
"""Convert objects to serialization-friendly format."""
|
|
45
|
-
if isinstance(obj, Path):
|
|
46
|
-
return str(obj)
|
|
47
|
-
elif isinstance(obj, dict):
|
|
48
|
-
return {
|
|
49
|
-
_prepare_for_serialization(k): _prepare_for_serialization(v)
|
|
50
|
-
for k, v in obj.items()
|
|
51
|
-
}
|
|
52
|
-
elif isinstance(obj, list):
|
|
53
|
-
return [_prepare_for_serialization(item) for item in obj]
|
|
54
|
-
elif isinstance(obj, tuple):
|
|
55
|
-
return tuple(_prepare_for_serialization(item) for item in obj)
|
|
56
|
-
elif isinstance(obj, set):
|
|
57
|
-
return [_prepare_for_serialization(item) for item in obj]
|
|
58
|
-
elif hasattr(obj, "model_dump"): # Pydantic model
|
|
59
|
-
return _prepare_for_serialization(obj.model_dump())
|
|
60
|
-
else:
|
|
61
|
-
return obj
|
|
62
|
-
|
|
63
|
-
def to_msgpack_bytes(self) -> bytes:
|
|
64
|
-
"""Serialize the model to compact binary format using MessagePack + gzip."""
|
|
65
|
-
data = _prepare_for_serialization(self.model_dump())
|
|
66
|
-
msgpack_data = msgpack.packb(data, use_bin_type=True)
|
|
67
|
-
return gzip.compress(msgpack_data)
|
|
68
|
-
|
|
69
|
-
@classmethod
|
|
70
|
-
def from_msgpack_bytes(cls_obj, data: bytes):
|
|
71
|
-
"""Deserialize from MessagePack + gzip binary format."""
|
|
72
|
-
decompressed_data = gzip.decompress(data)
|
|
73
|
-
obj_dict = msgpack.unpackb(decompressed_data, raw=False)
|
|
74
|
-
return cls_obj.model_validate(obj_dict)
|
|
75
|
-
|
|
76
|
-
def to_msgpack_dict(self) -> dict:
|
|
77
|
-
"""Convert to msgpack-compatible dictionary format."""
|
|
78
|
-
return _prepare_for_serialization(self.model_dump())
|
|
79
|
-
|
|
80
|
-
@classmethod
|
|
81
|
-
def from_msgpack_dict(cls_obj, data: dict):
|
|
82
|
-
"""Create instance from msgpack-compatible dictionary."""
|
|
83
|
-
return cls_obj.model_validate(data)
|
|
84
|
-
|
|
85
|
-
def get_msgpack_size(self) -> int:
|
|
86
|
-
"""Get the size of the msgpack serialization in bytes."""
|
|
87
|
-
return len(self.to_msgpack_bytes())
|
|
88
|
-
|
|
89
|
-
def get_compression_ratio(self) -> float:
|
|
90
|
-
"""Get compression ratio compared to JSON."""
|
|
91
|
-
json_size = len(self.model_dump_json().encode("utf-8"))
|
|
92
|
-
msgpack_gzip_size = self.get_msgpack_size()
|
|
93
|
-
return msgpack_gzip_size / json_size if json_size > 0 else 1.0
|
|
94
|
-
|
|
95
|
-
# Add methods to the class
|
|
96
|
-
cls.to_msgpack_bytes = to_msgpack_bytes
|
|
97
|
-
cls.from_msgpack_bytes = from_msgpack_bytes
|
|
98
|
-
cls.to_msgpack_dict = to_msgpack_dict
|
|
99
|
-
cls.from_msgpack_dict = from_msgpack_dict
|
|
100
|
-
cls.get_msgpack_size = get_msgpack_size
|
|
101
|
-
cls.get_compression_ratio = get_compression_ratio
|
|
102
|
-
|
|
103
|
-
return cls
|
|
104
27
|
|
|
105
28
|
|
|
106
29
|
def builder(cls):
|
|
@@ -192,7 +115,6 @@ def byte_offsets(source: str, start_line: int, start_col: int,
|
|
|
192
115
|
|
|
193
116
|
|
|
194
117
|
@builder
|
|
195
|
-
@msgpk
|
|
196
118
|
class Span(BaseModel):
|
|
197
119
|
"""Where a node lives in source. `start`/`end` are [line, col] (1-based line,
|
|
198
120
|
0-based col, ast semantics); `bytes` are utf-8 offsets into module.source."""
|
|
@@ -202,7 +124,6 @@ class Span(BaseModel):
|
|
|
202
124
|
|
|
203
125
|
|
|
204
126
|
@builder
|
|
205
|
-
@msgpk
|
|
206
127
|
class BodyNode(BaseModel):
|
|
207
128
|
"""A node in a callable's `body`: an AST region (statement/call/branch/…) or
|
|
208
129
|
a synthetic analysis vertex (entry/exit/formal_in/out/actual_in/out)."""
|
|
@@ -211,40 +132,46 @@ class BodyNode(BaseModel):
|
|
|
211
132
|
callee: Optional[str] = None # only on `call` nodes; the sanctioned null→id slot
|
|
212
133
|
of: Optional[str] = None # param vertices: the variable/return they carry
|
|
213
134
|
parent: Optional[str] = None # actuals: owning callsite ordinal id
|
|
135
|
+
# Call-site detail (#120). Previously reachable only through the parallel
|
|
136
|
+
# `PyCallable.call_sites` list, which emitted the same fact a second time under
|
|
137
|
+
# an unrelated id scheme. `method_name` and `is_constructor_call` are carried
|
|
138
|
+
# rather than derived from `callee`: measured across `requests` and `flask`,
|
|
139
|
+
# 20-28% of call sites never resolve a callee, so deriving them would lose them
|
|
140
|
+
# on one call in four.
|
|
141
|
+
method_name: Optional[str] = None
|
|
142
|
+
receiver_expr: Optional[str] = None
|
|
143
|
+
receiver_type: Optional[str] = None
|
|
144
|
+
return_type: Optional[str] = None
|
|
145
|
+
is_constructor_call: Optional[bool] = None
|
|
146
|
+
arguments: List["PyCallArgument"] = []
|
|
214
147
|
|
|
215
148
|
|
|
216
149
|
@builder
|
|
217
|
-
@msgpk
|
|
218
150
|
class CfgEdge(BaseModel):
|
|
219
151
|
src: str; dst: str; kind: str = "fallthrough"
|
|
220
152
|
|
|
221
153
|
|
|
222
154
|
@builder
|
|
223
|
-
@msgpk
|
|
224
155
|
class CdgEdge(BaseModel):
|
|
225
156
|
src: str; dst: str
|
|
226
157
|
|
|
227
158
|
|
|
228
159
|
@builder
|
|
229
|
-
@msgpk
|
|
230
160
|
class DdgEdge(BaseModel):
|
|
231
161
|
src: str; dst: str; var: Optional[str] = None; prov: List[str] = []
|
|
232
162
|
|
|
233
163
|
|
|
234
164
|
@builder
|
|
235
|
-
@msgpk
|
|
236
165
|
class SummaryEdge(BaseModel):
|
|
237
166
|
src: str; dst: str
|
|
238
167
|
|
|
239
168
|
|
|
240
169
|
@builder
|
|
241
|
-
@msgpk
|
|
242
170
|
class ParamEdge(BaseModel):
|
|
243
171
|
src: str; dst: str
|
|
244
172
|
|
|
245
173
|
|
|
246
174
|
@builder
|
|
247
|
-
@msgpk
|
|
248
175
|
class PyImport(BaseModel):
|
|
249
176
|
"""Represents a Python import statement."""
|
|
250
177
|
|
|
@@ -259,7 +186,6 @@ class PyImport(BaseModel):
|
|
|
259
186
|
|
|
260
187
|
|
|
261
188
|
@builder
|
|
262
|
-
@msgpk
|
|
263
189
|
class PyComment(BaseModel):
|
|
264
190
|
"""Represents a Python comment."""
|
|
265
191
|
|
|
@@ -272,7 +198,6 @@ class PyComment(BaseModel):
|
|
|
272
198
|
|
|
273
199
|
|
|
274
200
|
@builder
|
|
275
|
-
@msgpk
|
|
276
201
|
class PySymbol(BaseModel):
|
|
277
202
|
"""Represents a symbol used or declared in Python code."""
|
|
278
203
|
|
|
@@ -287,7 +212,6 @@ class PySymbol(BaseModel):
|
|
|
287
212
|
|
|
288
213
|
|
|
289
214
|
@builder
|
|
290
|
-
@msgpk
|
|
291
215
|
class PyVariableDeclaration(BaseModel):
|
|
292
216
|
"""Represents a Python variable declaration."""
|
|
293
217
|
|
|
@@ -306,13 +230,66 @@ class PyVariableDeclaration(BaseModel):
|
|
|
306
230
|
|
|
307
231
|
|
|
308
232
|
@builder
|
|
309
|
-
|
|
233
|
+
class PyDecorator(BaseModel):
|
|
234
|
+
"""One decorator application, structured rather than a source string (#128).
|
|
235
|
+
|
|
236
|
+
``name`` is the spelling as written (``lru_cache``, ``builtins.staticmethod``);
|
|
237
|
+
``qualified_name`` is Jedi's resolution of it (``functools.lru_cache``) and is
|
|
238
|
+
absent when it cannot be resolved. ``expression`` keeps the full unparsed source
|
|
239
|
+
so nothing is lost for decorators too complex to decompose.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
name: str
|
|
243
|
+
qualified_name: Optional[str] = None
|
|
244
|
+
positional_arguments: List[str] = []
|
|
245
|
+
keyword_arguments: Dict[str, str] = {}
|
|
246
|
+
expression: str = ""
|
|
247
|
+
span: Optional[Span] = None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@builder
|
|
251
|
+
class PyEntrypoint(BaseModel):
|
|
252
|
+
"""One way a callable or class is invoked from outside the application (#27).
|
|
253
|
+
|
|
254
|
+
A node may hold several: two ``@app.route`` decorators, or a function that
|
|
255
|
+
is both a Celery task and a CLI command. ``confidence`` lets a consumer
|
|
256
|
+
threshold on evidence quality rather than inheriting this analyzer's
|
|
257
|
+
judgement.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
framework: str
|
|
261
|
+
confidence: str = "certain" # "declared" | "certain" | "heuristic"
|
|
262
|
+
rule: str = "" # rules.yml `id:`, or an engine name
|
|
263
|
+
ruleset: str = "shipped" # "shipped" | "user:<path>"
|
|
264
|
+
evidence: Optional[str] = None
|
|
265
|
+
route: Optional[str] = None
|
|
266
|
+
http_methods: List[str] = []
|
|
267
|
+
via: Optional[str] = None # can:// id of the routed node dispatching here
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@builder
|
|
271
|
+
class PyEntrypointReport(BaseModel):
|
|
272
|
+
"""Coverage and failure record for the entrypoint pass (#27).
|
|
273
|
+
|
|
274
|
+
The pass under-approximates by design, so silence is its failure mode.
|
|
275
|
+
This is what makes a gap visible instead of indistinguishable from
|
|
276
|
+
"this project has no entrypoints".
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
frameworks_detected: List[str] = []
|
|
280
|
+
rulesets: List[str] = []
|
|
281
|
+
unresolved: Dict[str, int] = {}
|
|
282
|
+
errors: List[str] = []
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@builder
|
|
310
286
|
class PyCallableParameter(BaseModel):
|
|
311
287
|
"""Represents a parameter of a Python callable (function/method)."""
|
|
312
288
|
|
|
313
289
|
name: str
|
|
314
290
|
type: Optional[str] = None
|
|
315
291
|
default_value: Optional[str] = None
|
|
292
|
+
decorators: List[PyDecorator] = []
|
|
316
293
|
start_line: int = -1
|
|
317
294
|
end_line: int = -1
|
|
318
295
|
start_column: int = -1
|
|
@@ -320,7 +297,6 @@ class PyCallableParameter(BaseModel):
|
|
|
320
297
|
|
|
321
298
|
|
|
322
299
|
@builder
|
|
323
|
-
@msgpk
|
|
324
300
|
class PyCallArgument(BaseModel):
|
|
325
301
|
"""One call-site argument: AST category + inferred type, kept separate.
|
|
326
302
|
|
|
@@ -332,8 +308,14 @@ class PyCallArgument(BaseModel):
|
|
|
332
308
|
inferred_type: Optional[str] = None
|
|
333
309
|
|
|
334
310
|
|
|
311
|
+
# BodyNode.arguments forward-references PyCallArgument (defined later);
|
|
312
|
+
# pydantic v1 resolves string annotations only when told to, while v2
|
|
313
|
+
# rebuilds automatically (and its update_forward_refs shim rejects localns).
|
|
314
|
+
if not hasattr(BodyNode, "model_rebuild"): # pydantic v1
|
|
315
|
+
BodyNode.update_forward_refs(PyCallArgument=PyCallArgument)
|
|
316
|
+
|
|
317
|
+
|
|
335
318
|
@builder
|
|
336
|
-
@msgpk
|
|
337
319
|
class PyCallsite(BaseModel):
|
|
338
320
|
"""Represents a Python call site (function or method invocation) with contextual metadata."""
|
|
339
321
|
|
|
@@ -352,7 +334,6 @@ class PyCallsite(BaseModel):
|
|
|
352
334
|
|
|
353
335
|
|
|
354
336
|
@builder
|
|
355
|
-
@msgpk
|
|
356
337
|
class PyCallable(BaseModel):
|
|
357
338
|
"""Represents a Python callable (function/method)."""
|
|
358
339
|
|
|
@@ -363,13 +344,27 @@ class PyCallable(BaseModel):
|
|
|
363
344
|
kind: str = "function"
|
|
364
345
|
span: Optional[Span] = None
|
|
365
346
|
comments: List[PyComment] = []
|
|
366
|
-
decorators: List[
|
|
347
|
+
decorators: List[PyDecorator] = []
|
|
348
|
+
# Language-level modifiers on the declaration itself (#130). `async` is the
|
|
349
|
+
# only one Python has today. It lives here rather than in `kind` because it
|
|
350
|
+
# is orthogonal to every kind -- an async method is both -- so encoding it
|
|
351
|
+
# in the discriminant would need async_function, async_method,
|
|
352
|
+
# async_generator and so on, combinatorially.
|
|
353
|
+
modifiers: List[str] = []
|
|
354
|
+
entrypoints: List[PyEntrypoint] = []
|
|
355
|
+
is_entrypoint: bool = False
|
|
367
356
|
parameters: List[PyCallableParameter] = []
|
|
368
357
|
return_type: Optional[str] = None
|
|
369
358
|
start_line: int = -1
|
|
370
359
|
end_line: int = -1
|
|
371
360
|
code_start_line: int = -1
|
|
372
361
|
accessed_symbols: List[PySymbol] = []
|
|
362
|
+
# Internal (#120): the Jedi-produced record that `l1_body` derives `body{}`
|
|
363
|
+
# call nodes from, and that `call_graph.py`, `l2_callees.py` and the dataflow
|
|
364
|
+
# builder all read. It is stripped at EMIT time (see `wire_json`), not with a
|
|
365
|
+
# field-level `exclude`: the analysis cache round-trips through the same
|
|
366
|
+
# serializer, so excluding it would drop it from the cache too and a warm-cache
|
|
367
|
+
# run would rebuild with no call sites at all.
|
|
373
368
|
call_sites: List[PyCallsite] = []
|
|
374
369
|
callables: Dict[str, "PyCallable"] = {} # nested callables (closures)
|
|
375
370
|
types: Dict[str, "PyClass"] = {} # nested (local) classes
|
|
@@ -389,7 +384,6 @@ class PyCallable(BaseModel):
|
|
|
389
384
|
|
|
390
385
|
|
|
391
386
|
@builder
|
|
392
|
-
@msgpk
|
|
393
387
|
class PyClassAttribute(BaseModel):
|
|
394
388
|
"""Represents a Python class attribute."""
|
|
395
389
|
|
|
@@ -397,12 +391,12 @@ class PyClassAttribute(BaseModel):
|
|
|
397
391
|
type: Optional[str] = None
|
|
398
392
|
initializer: Optional[str] = None
|
|
399
393
|
comments: List[PyComment] = []
|
|
394
|
+
decorators: List[PyDecorator] = []
|
|
400
395
|
start_line: int = -1
|
|
401
396
|
end_line: int = -1
|
|
402
397
|
|
|
403
398
|
|
|
404
399
|
@builder
|
|
405
|
-
@msgpk
|
|
406
400
|
class PyClass(BaseModel):
|
|
407
401
|
"""Represents a Python class."""
|
|
408
402
|
|
|
@@ -413,6 +407,9 @@ class PyClass(BaseModel):
|
|
|
413
407
|
span: Optional[Span] = None
|
|
414
408
|
comments: List[PyComment] = []
|
|
415
409
|
base_classes: List[str] = []
|
|
410
|
+
decorators: List[PyDecorator] = []
|
|
411
|
+
entrypoints: List[PyEntrypoint] = []
|
|
412
|
+
is_entrypoint: bool = False
|
|
416
413
|
callables: Dict[str, PyCallable] = {} # methods, keystone containment name
|
|
417
414
|
attributes: Dict[str, PyClassAttribute] = {}
|
|
418
415
|
types: Dict[str, "PyClass"] = {} # inner classes, keystone containment name
|
|
@@ -425,7 +422,6 @@ class PyClass(BaseModel):
|
|
|
425
422
|
|
|
426
423
|
|
|
427
424
|
@builder
|
|
428
|
-
@msgpk
|
|
429
425
|
class PyModule(BaseModel):
|
|
430
426
|
"""Represents a Python module."""
|
|
431
427
|
|
|
@@ -446,7 +442,6 @@ class PyModule(BaseModel):
|
|
|
446
442
|
|
|
447
443
|
|
|
448
444
|
@builder
|
|
449
|
-
@msgpk
|
|
450
445
|
class PyCallEdge(BaseModel):
|
|
451
446
|
"""Identity-only call-graph edge with weight (keystone shape: the list name
|
|
452
447
|
IS the edge type, so there is no ``type`` field).
|
|
@@ -460,11 +455,10 @@ class PyCallEdge(BaseModel):
|
|
|
460
455
|
src: str # caller callable id
|
|
461
456
|
dst: str # callee callable (or external) id
|
|
462
457
|
weight: int = 1
|
|
463
|
-
prov: List[Literal["jedi", "
|
|
458
|
+
prov: List[Literal["jedi", "defuse"]] = []
|
|
464
459
|
|
|
465
460
|
|
|
466
461
|
@builder
|
|
467
|
-
@msgpk
|
|
468
462
|
class PyExternalSymbol(BaseModel):
|
|
469
463
|
"""A call-graph target outside the analyzed project -- an imported library or
|
|
470
464
|
builtin member. An edge-endpoint id home, not a tree node: keyed in
|
|
@@ -477,7 +471,6 @@ class PyExternalSymbol(BaseModel):
|
|
|
477
471
|
|
|
478
472
|
|
|
479
473
|
@builder
|
|
480
|
-
@msgpk
|
|
481
474
|
class PyRepositoryInfo(BaseModel):
|
|
482
475
|
"""Where the analyzed source came from: git provenance captured at analysis time."""
|
|
483
476
|
|
|
@@ -487,7 +480,6 @@ class PyRepositoryInfo(BaseModel):
|
|
|
487
480
|
|
|
488
481
|
|
|
489
482
|
@builder
|
|
490
|
-
@msgpk
|
|
491
483
|
class PyAnalyzerInfo(BaseModel):
|
|
492
484
|
"""Which analyzer produced this snapshot, and how it was configured.
|
|
493
485
|
Lives on the ``Analysis`` envelope (keystone ``analyzer{name,version}``;
|
|
@@ -499,7 +491,6 @@ class PyAnalyzerInfo(BaseModel):
|
|
|
499
491
|
|
|
500
492
|
|
|
501
493
|
@builder
|
|
502
|
-
@msgpk
|
|
503
494
|
class PyApplication(BaseModel):
|
|
504
495
|
"""Represents a Python application."""
|
|
505
496
|
|
|
@@ -511,6 +502,8 @@ class PyApplication(BaseModel):
|
|
|
511
502
|
# builtin members), keyed by signature. Populated by the analyzer so every
|
|
512
503
|
# backend (JSON and Neo4j) shares one authoritative external-symbol set.
|
|
513
504
|
external_symbols: Dict[str, PyExternalSymbol] = {}
|
|
505
|
+
# Coverage/failure record for the entrypoint pass; see PyEntrypointReport (#27).
|
|
506
|
+
entrypoint_report: PyEntrypointReport = PyEntrypointReport()
|
|
514
507
|
# Git provenance of the analyzed checkout, captured at analysis time.
|
|
515
508
|
repository: Optional[PyRepositoryInfo] = None
|
|
516
509
|
# Interprocedural parameter-passing edges (formal↔actual); populated at L4.
|
|
@@ -519,7 +512,6 @@ class PyApplication(BaseModel):
|
|
|
519
512
|
|
|
520
513
|
|
|
521
514
|
@builder
|
|
522
|
-
@msgpk
|
|
523
515
|
class Analysis(BaseModel):
|
|
524
516
|
"""v2 payload root: envelope + the application tree node. ``k_limit`` is an
|
|
525
517
|
L3+ envelope key (None below the dataflow levels; exclude_none drops it)."""
|
|
@@ -28,6 +28,8 @@ from typing import Dict, Iterator, List, Tuple
|
|
|
28
28
|
|
|
29
29
|
import networkx as nx
|
|
30
30
|
|
|
31
|
+
from codeanalyzer.semantic_analysis.defuse_linker import _module_qual
|
|
32
|
+
from codeanalyzer.schema import model_copy
|
|
31
33
|
from codeanalyzer.schema.py_schema import (
|
|
32
34
|
PyApplication,
|
|
33
35
|
PyCallable,
|
|
@@ -170,7 +172,7 @@ def jedi_call_graph_edges(
|
|
|
170
172
|
|
|
171
173
|
Edges are coalesced on ``(source, target)``: ``weight`` is the count of
|
|
172
174
|
matching sites. Provenance is always ``["jedi"]``; combine with
|
|
173
|
-
|
|
175
|
+
defuse-linker edges via ``merge_edges``.
|
|
174
176
|
"""
|
|
175
177
|
counts: Counter = Counter()
|
|
176
178
|
for caller in iter_callables_in_symbol_table(symbol_table):
|
|
@@ -253,10 +255,24 @@ def filter_external_edges(
|
|
|
253
255
|
retained; only lib→lib edges are dropped. The app symbol set is built by
|
|
254
256
|
walking every callable in the symbol table recursively (including nested
|
|
255
257
|
functions and closures via ``callables``) plus every class, so
|
|
256
|
-
|
|
258
|
+
resolver-discovered closure nodes are correctly recognised as app symbols.
|
|
259
|
+
|
|
260
|
+
Module names count as app symbols too (#131). A resolver attributes a call in
|
|
261
|
+
module scope to the MODULE -- ``app -> functools.reduce`` for a module-level
|
|
262
|
+
``functools.reduce(...)``, or for a decorator applied to a top-level
|
|
263
|
+
definition, since a decorator executes in its enclosing scope. Without the
|
|
264
|
+
module names here, both endpoints looked third-party and every module-scope
|
|
265
|
+
call to a library target was discarded as lib→lib.
|
|
257
266
|
"""
|
|
258
267
|
app_symbols: set = {c.signature for c in iter_callables_in_symbol_table(symbol_table)}
|
|
259
268
|
app_symbols.update(cls.signature for cls in iter_classes_in_symbol_table(symbol_table))
|
|
269
|
+
app_symbols.update(
|
|
270
|
+
mod.module_name for mod in symbol_table.values() if mod.module_name
|
|
271
|
+
)
|
|
272
|
+
# Dotted module quals too: the defuse linker attributes module- and
|
|
273
|
+
# class-scope calls to "pkg.module" (collision-free across packages),
|
|
274
|
+
# which the bare `module_name` stems above never match.
|
|
275
|
+
app_symbols.update(_module_qual(key) for key in symbol_table)
|
|
260
276
|
|
|
261
277
|
return [
|
|
262
278
|
e for e in edges
|
|
@@ -269,7 +285,7 @@ def merge_edges(*edge_lists: list) -> list:
|
|
|
269
285
|
|
|
270
286
|
Edges with the same ``(source, target)`` are coalesced: weights sum,
|
|
271
287
|
provenance is the sorted union. Useful for combining edges produced
|
|
272
|
-
by different backends (e.g. Jedi +
|
|
288
|
+
by different backends (e.g. Jedi + the defuse linker).
|
|
273
289
|
"""
|
|
274
290
|
by_key: Dict[Tuple[str, str], PyCallEdge] = {}
|
|
275
291
|
for edges in edge_lists:
|
|
@@ -280,5 +296,5 @@ def merge_edges(*edge_lists: list) -> list:
|
|
|
280
296
|
cur.weight += e.weight
|
|
281
297
|
cur.prov = sorted(set(cur.prov) | set(e.prov))
|
|
282
298
|
else:
|
|
283
|
-
by_key[k] =
|
|
299
|
+
by_key[k] = model_copy(e)
|
|
284
300
|
return list(by_key.values())
|