codeanalyzer-python 1.5.2__py3-none-any.whl → 1.5.4__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 +2 -2
- codeanalyzer/artifacts/discovery.py +53 -1
- codeanalyzer/core.py +20 -1
- codeanalyzer/dataflow/builder.py +19 -9
- codeanalyzer/dataflow/sdg.py +26 -8
- codeanalyzer/entrypoints/matching.py +5 -1
- codeanalyzer/neo4j/emit.py +2 -2
- codeanalyzer/neo4j/project.py +10 -9
- codeanalyzer/options/__init__.py +2 -2
- codeanalyzer/options/options.py +7 -0
- codeanalyzer/schema/ids.py +37 -1
- codeanalyzer/schema/l1_body.py +2 -3
- codeanalyzer/schema/l2_callees.py +4 -3
- codeanalyzer/semantic_analysis/defuse_linker.py +7 -4
- codeanalyzer/syntactic_analysis/symbol_table_builder.py +22 -8
- {codeanalyzer_python-1.5.2.dist-info → codeanalyzer_python-1.5.4.dist-info}/METADATA +1 -1
- {codeanalyzer_python-1.5.2.dist-info → codeanalyzer_python-1.5.4.dist-info}/RECORD +21 -21
- {codeanalyzer_python-1.5.2.dist-info → codeanalyzer_python-1.5.4.dist-info}/WHEEL +0 -0
- {codeanalyzer_python-1.5.2.dist-info → codeanalyzer_python-1.5.4.dist-info}/entry_points.txt +0 -0
- {codeanalyzer_python-1.5.2.dist-info → codeanalyzer_python-1.5.4.dist-info}/licenses/LICENSE +0 -0
- {codeanalyzer_python-1.5.2.dist-info → codeanalyzer_python-1.5.4.dist-info}/licenses/NOTICE +0 -0
codeanalyzer/__main__.py
CHANGED
|
@@ -39,7 +39,7 @@ def _pin_hash_seed() -> None:
|
|
|
39
39
|
from codeanalyzer.core import Codeanalyzer
|
|
40
40
|
from codeanalyzer.utils import _set_log_level, logger
|
|
41
41
|
from codeanalyzer.schema import model_dump, model_dump_json, strip_internal_only
|
|
42
|
-
from codeanalyzer.options import AnalysisOptions, EmitTarget
|
|
42
|
+
from codeanalyzer.options import ANALYSIS_JSON, AnalysisOptions, EmitTarget
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def _version_callback(value: bool) -> None:
|
|
@@ -402,7 +402,7 @@ def main(
|
|
|
402
402
|
|
|
403
403
|
def _write_output(artifacts, output_dir: Path):
|
|
404
404
|
"""Write analysis.json (the single wire format since #118)."""
|
|
405
|
-
output_file = output_dir /
|
|
405
|
+
output_file = output_dir / ANALYSIS_JSON
|
|
406
406
|
# Use Pydantic's model_dump_json() for compact output
|
|
407
407
|
# Strip internal-only fields here rather than with a field-level Pydantic
|
|
408
408
|
# `exclude`: the analysis cache shares the serializer and must keep them.
|
|
@@ -3,10 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import fnmatch
|
|
4
4
|
import hashlib
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Dict, List, Tuple
|
|
6
|
+
from typing import Dict, Iterable, List, Tuple
|
|
7
7
|
|
|
8
8
|
from codeanalyzer.schema.ids import artifact_id
|
|
9
9
|
from codeanalyzer.schema.py_schema import PyArtifact
|
|
10
|
+
from codeanalyzer.utils import logger
|
|
10
11
|
|
|
11
12
|
# (glob pattern against the repo-relative POSIX path, format, roles).
|
|
12
13
|
# First match wins; patterns are checked in order.
|
|
@@ -67,6 +68,43 @@ _IGNORED_DIRS = {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
def _resolve_exclusions(project_dir: Path, exclude_paths: Iterable[Path]) -> List[Path]:
|
|
72
|
+
"""Resolve the paths a run writes to, dropping any that would take the whole
|
|
73
|
+
project with them (#207).
|
|
74
|
+
|
|
75
|
+
A *directory* exclusion at or above the project root would empty the
|
|
76
|
+
inventory, which is worse than the bug it guards against, so it is refused.
|
|
77
|
+
That case is still covered, because the caller also passes the individual
|
|
78
|
+
output *files* -- excluding ``<project>/analysis.json`` costs one file
|
|
79
|
+
instead of the entire tree.
|
|
80
|
+
"""
|
|
81
|
+
root = project_dir.resolve()
|
|
82
|
+
kept = []
|
|
83
|
+
for given in exclude_paths:
|
|
84
|
+
resolved = given.resolve()
|
|
85
|
+
if root.is_relative_to(resolved):
|
|
86
|
+
logger.warning(
|
|
87
|
+
f"Not excluding {resolved} from artifact discovery: it holds the "
|
|
88
|
+
f"project itself. Only this run's own output files under it are "
|
|
89
|
+
f"skipped; anything else written there is ingested as an artifact."
|
|
90
|
+
)
|
|
91
|
+
continue
|
|
92
|
+
kept.append(resolved)
|
|
93
|
+
return kept
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_excluded(path: Path, excluded: List[Path]) -> bool:
|
|
97
|
+
"""True when ``path`` is, or sits inside, one of ``excluded`` (a file entry
|
|
98
|
+
matches only itself). Checked on the path as walked *and* on its resolved
|
|
99
|
+
form, so an output directory reached through a symlink is caught from either
|
|
100
|
+
side."""
|
|
101
|
+
if not excluded:
|
|
102
|
+
return False
|
|
103
|
+
if any(path.is_relative_to(directory) for directory in excluded):
|
|
104
|
+
return True
|
|
105
|
+
return any(path.resolve().is_relative_to(directory) for directory in excluded)
|
|
106
|
+
|
|
107
|
+
|
|
70
108
|
def _classify(rel_posix: str) -> Tuple[str, List[str]] | None:
|
|
71
109
|
name = rel_posix.rsplit("/", 1)[-1]
|
|
72
110
|
for pattern, fmt, roles in RULES:
|
|
@@ -81,6 +119,7 @@ def discover_artifacts(
|
|
|
81
119
|
app_name: str,
|
|
82
120
|
*,
|
|
83
121
|
capture_text: bool = True,
|
|
122
|
+
exclude_paths: Iterable[Path] = (),
|
|
84
123
|
) -> Dict[str, PyArtifact]:
|
|
85
124
|
"""Walk the project and return every file as an artifact, sorted by path.
|
|
86
125
|
|
|
@@ -93,15 +132,28 @@ def discover_artifacts(
|
|
|
93
132
|
deliberate exception -- it IS rule-matched (a dependency-manifest), so it
|
|
94
133
|
is captured like any other manifest despite the `.py` suffix.
|
|
95
134
|
|
|
135
|
+
``exclude_paths`` names what this run writes -- the ``--output`` and cache
|
|
136
|
+
directories, and the output files inside them (#207). Without them a run
|
|
137
|
+
whose output lands inside the project ingests the previous run's whole
|
|
138
|
+
``analysis.json``, and each run embeds the one before it until the process is
|
|
139
|
+
killed decoding its own output. Matching is on resolved paths, so a
|
|
140
|
+
relative, `..`-laden or symlinked target excludes the same tree, and a target
|
|
141
|
+
outside the project excludes nothing. A directory that holds the project
|
|
142
|
+
itself is refused (it would empty the inventory); the file entries still
|
|
143
|
+
cover that case.
|
|
144
|
+
|
|
96
145
|
``source`` is the WHOLE file or nothing -- never a prefix (#172). A
|
|
97
146
|
decodable file is captured in full; ``capture_text=False`` empties
|
|
98
147
|
``source`` everywhere (inventory otherwise identical), and an undecodable
|
|
99
148
|
file gets ``""`` as ``binary``. ``sha256``/``size_bytes`` always reflect
|
|
100
149
|
the full file regardless."""
|
|
101
150
|
out: Dict[str, PyArtifact] = {}
|
|
151
|
+
excluded = _resolve_exclusions(project_dir, exclude_paths)
|
|
102
152
|
for path in sorted(project_dir.rglob("*")):
|
|
103
153
|
if not path.is_file():
|
|
104
154
|
continue
|
|
155
|
+
if _is_excluded(path, excluded):
|
|
156
|
+
continue
|
|
105
157
|
rel = path.relative_to(project_dir)
|
|
106
158
|
if any(part in _IGNORED_DIRS for part in rel.parts):
|
|
107
159
|
continue
|
codeanalyzer/core.py
CHANGED
|
@@ -35,7 +35,7 @@ from codeanalyzer.syntactic_analysis.exceptions import SymbolTableBuilderRayErro
|
|
|
35
35
|
from codeanalyzer.syntactic_analysis.import_resolver import resolve_imports
|
|
36
36
|
from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder
|
|
37
37
|
from codeanalyzer.utils import ProgressBar
|
|
38
|
-
from codeanalyzer.options import AnalysisOptions
|
|
38
|
+
from codeanalyzer.options import ANALYSIS_JSON, GRAPH_CYPHER, AnalysisOptions, EmitTarget
|
|
39
39
|
from codeanalyzer.provenance import analyzer_info, repository_info
|
|
40
40
|
|
|
41
41
|
def _artifact_full_text(project_dir: Path, path: str, art) -> str:
|
|
@@ -572,6 +572,24 @@ class Codeanalyzer:
|
|
|
572
572
|
)
|
|
573
573
|
return externals
|
|
574
574
|
|
|
575
|
+
def _own_output_paths(self) -> List[Path]:
|
|
576
|
+
"""Where this run writes: the output and cache directories, plus the
|
|
577
|
+
output files themselves (#207).
|
|
578
|
+
|
|
579
|
+
Artifact discovery skips these, so a run whose ``-o``/``-c`` lands inside
|
|
580
|
+
``-i`` does not ingest its own previous output (each run embedding the
|
|
581
|
+
last until the process is killed decoding it). The file entries carry the
|
|
582
|
+
degenerate case where the output directory *is* the project root, which
|
|
583
|
+
cannot be skipped wholesale without emptying the inventory.
|
|
584
|
+
"""
|
|
585
|
+
paths: List[Path] = [self.cache_dir]
|
|
586
|
+
if self.options.output is not None:
|
|
587
|
+
paths += [self.options.output, self.options.output / ANALYSIS_JSON]
|
|
588
|
+
if self.options.emit is EmitTarget.NEO4J:
|
|
589
|
+
# No -o means the cypher snapshot lands in the working directory.
|
|
590
|
+
paths.append((self.options.output or Path.cwd()) / GRAPH_CYPHER)
|
|
591
|
+
return paths
|
|
592
|
+
|
|
575
593
|
def analyze(self) -> Analysis:
|
|
576
594
|
"""Analyze the project and return the v2 ``Analysis`` envelope.
|
|
577
595
|
|
|
@@ -673,6 +691,7 @@ class Codeanalyzer:
|
|
|
673
691
|
app.artifacts = discover_artifacts(
|
|
674
692
|
self.project_dir, app_name,
|
|
675
693
|
capture_text=self.options.artifact_text,
|
|
694
|
+
exclude_paths=self._own_output_paths(),
|
|
676
695
|
)
|
|
677
696
|
app.dependencies, app.unresolved_imports = build_dependency_view(
|
|
678
697
|
app.artifacts,
|
codeanalyzer/dataflow/builder.py
CHANGED
|
@@ -35,6 +35,7 @@ the result degrades gracefully instead of crashing (contract rule).
|
|
|
35
35
|
from __future__ import annotations
|
|
36
36
|
|
|
37
37
|
import ast
|
|
38
|
+
from collections import defaultdict
|
|
38
39
|
from pathlib import Path
|
|
39
40
|
from typing import Callable, Dict, List, Optional, Set, Tuple
|
|
40
41
|
|
|
@@ -43,7 +44,7 @@ from codeanalyzer.dataflow.alias import TypeBasedAliasOracle
|
|
|
43
44
|
from codeanalyzer.dataflow.pdg import build_pdg
|
|
44
45
|
from codeanalyzer.dataflow.sdg import ProgramGraphsIR, assemble_sdg
|
|
45
46
|
from codeanalyzer.dataflow.summaries import CallSite, FunctionInfo, compute_summaries
|
|
46
|
-
from codeanalyzer.schema.ids import stamp_body_ids
|
|
47
|
+
from codeanalyzer.schema.ids import call_body_keys, stamp_body_ids
|
|
47
48
|
from codeanalyzer.schema.py_schema import PyApplication, PyCallable, PyClass, PyModule
|
|
48
49
|
from codeanalyzer.utils import logger
|
|
49
50
|
|
|
@@ -291,6 +292,11 @@ def emit_l3_body(
|
|
|
291
292
|
continue
|
|
292
293
|
pycallable.body[local] = BodyNode(kind=node.kind, span=span)
|
|
293
294
|
|
|
295
|
+
keys_at: Dict[str, List[str]] = defaultdict(list)
|
|
296
|
+
for key, n in pycallable.body.items():
|
|
297
|
+
if n.kind == "call":
|
|
298
|
+
keys_at[key.split("/", 1)[0]].append(key)
|
|
299
|
+
|
|
294
300
|
# #115: anchor nested call vertices to their statement. A bare-call
|
|
295
301
|
# statement shares its key with its CFG node (handled above); a call
|
|
296
302
|
# nested inside a larger statement (`y = f(x)`) has its own key and
|
|
@@ -301,12 +307,16 @@ def emit_l3_body(
|
|
|
301
307
|
continue
|
|
302
308
|
stmt_local = im.local(node.id)
|
|
303
309
|
for call in _calls_in(node.ast_node):
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
child
|
|
310
|
+
# Every call node at this position, not just one: nested calls
|
|
311
|
+
# that share a start column are keyed `line:col`, `line:col/2`,
|
|
312
|
+
# ... and each of them needs the anchor (#215).
|
|
313
|
+
base = f"{call.lineno}:{call.col_offset}"
|
|
314
|
+
for call_key in keys_at.get(base, ()):
|
|
315
|
+
child = pycallable.body.get(call_key)
|
|
316
|
+
if child is None or call_key == stmt_local:
|
|
317
|
+
continue
|
|
318
|
+
if child.kind == "call":
|
|
319
|
+
child.parent = stmt_local
|
|
310
320
|
|
|
311
321
|
if want_cfg:
|
|
312
322
|
pycallable.cfg = [
|
|
@@ -378,7 +388,7 @@ def build_program_graphs(
|
|
|
378
388
|
calls_by_pos.setdefault(pos, (node.id, call))
|
|
379
389
|
calls_by_line.setdefault(call.lineno, (node.id, call))
|
|
380
390
|
|
|
381
|
-
for site in pycallable.call_sites
|
|
391
|
+
for site_key, site in call_body_keys(pycallable.call_sites):
|
|
382
392
|
# Prefer the callsite's body-backfilled callee over Jedi's own
|
|
383
393
|
# callee_signature side channel: under cross-test parso/Jedi cache
|
|
384
394
|
# pressure that inference can silently degrade (full-suite-only
|
|
@@ -388,7 +398,7 @@ def build_program_graphs(
|
|
|
388
398
|
# Only a resolved INTERNAL target counts (id_to_sig misses on an
|
|
389
399
|
# external/unresolved callee); falls through to callee_signature
|
|
390
400
|
# exactly as before whenever the body doesn't have an answer.
|
|
391
|
-
body_node = pycallable.body.get(
|
|
401
|
+
body_node = pycallable.body.get(site_key)
|
|
392
402
|
target = (id_to_sig.get(body_node.callee) if body_node else None) or site.callee_signature
|
|
393
403
|
if not target:
|
|
394
404
|
continue
|
codeanalyzer/dataflow/sdg.py
CHANGED
|
@@ -142,6 +142,26 @@ class _FunctionAssembler:
|
|
|
142
142
|
|
|
143
143
|
# ---------------------------------------------------------------- formals
|
|
144
144
|
|
|
145
|
+
def formal_for(self, var: str) -> Optional[int]:
|
|
146
|
+
"""The ``formal_in`` vertex that defines ``var`` in this callable.
|
|
147
|
+
|
|
148
|
+
A parameter, a capture and a read global are all defined at a
|
|
149
|
+
``formal_in`` port, not at the synthetic CFG ENTRY node. Reaching-def
|
|
150
|
+
analysis reports ENTRY as the source for all three, so every place that
|
|
151
|
+
consumes a raw def source has to remap it here, or the SDG loses the
|
|
152
|
+
hop from the parameter to whatever uses it. Returns ``None`` when
|
|
153
|
+
``var`` is not one of this callable's formals, so a caller can keep the
|
|
154
|
+
original source.
|
|
155
|
+
"""
|
|
156
|
+
b = base_of(var)
|
|
157
|
+
if b in self.formal_in:
|
|
158
|
+
return self.formal_in[b]
|
|
159
|
+
if CAPTURE_PREFIX + b in self.formal_in:
|
|
160
|
+
return self.formal_in[CAPTURE_PREFIX + b]
|
|
161
|
+
if "::" in b and GLOBAL_PREFIX + b in self.formal_in:
|
|
162
|
+
return self.formal_in[GLOBAL_PREFIX + b]
|
|
163
|
+
return None
|
|
164
|
+
|
|
145
165
|
def build_formals(self) -> None:
|
|
146
166
|
scope, summary = self.scope, self.summary
|
|
147
167
|
params = list(scope.params)
|
|
@@ -168,14 +188,8 @@ class _FunctionAssembler:
|
|
|
168
188
|
for e in self.ddg:
|
|
169
189
|
if e.source != entry:
|
|
170
190
|
continue
|
|
171
|
-
|
|
172
|
-
if
|
|
173
|
-
fid = self.formal_in[b]
|
|
174
|
-
elif CAPTURE_PREFIX + b in self.formal_in:
|
|
175
|
-
fid = self.formal_in[CAPTURE_PREFIX + b]
|
|
176
|
-
elif "::" in b and GLOBAL_PREFIX + b in self.formal_in:
|
|
177
|
-
fid = self.formal_in[GLOBAL_PREFIX + b]
|
|
178
|
-
else:
|
|
191
|
+
fid = self.formal_for(e.var)
|
|
192
|
+
if fid is None:
|
|
179
193
|
continue
|
|
180
194
|
self.extra.append(PDGEdge(source=fid, target=e.target, type="DDG", var=e.var))
|
|
181
195
|
|
|
@@ -268,6 +282,8 @@ class _FunctionAssembler:
|
|
|
268
282
|
for src, var in self._defs_reaching_call_matching(
|
|
269
283
|
cs.node_id, path
|
|
270
284
|
):
|
|
285
|
+
if src == self.cfg.entry_id:
|
|
286
|
+
src = self.formal_for(var) or src
|
|
271
287
|
self.extra.append(
|
|
272
288
|
PDGEdge(source=src, target=aid, type="DDG", var=var)
|
|
273
289
|
)
|
|
@@ -296,6 +312,8 @@ class _FunctionAssembler:
|
|
|
296
312
|
for src, var in self._defs_reaching_call_matching(
|
|
297
313
|
cs.node_id, g
|
|
298
314
|
):
|
|
315
|
+
if src == self.cfg.entry_id:
|
|
316
|
+
src = self.formal_for(var) or src
|
|
299
317
|
self.extra.append(
|
|
300
318
|
PDGEdge(source=src, target=aid, type="DDG", var=var)
|
|
301
319
|
)
|
|
@@ -104,8 +104,12 @@ def _methods_of(dec, spec: Optional[Dict[str, Any]], qualified: Optional[str] =
|
|
|
104
104
|
return []
|
|
105
105
|
source = spec.get("from")
|
|
106
106
|
if source == "match_suffix":
|
|
107
|
+
# Only a real verb: `heuristic.http-verb` also matches `.websocket`, and a
|
|
108
|
+
# rule may accept any suffix, but `http_methods` is what a consumer filters
|
|
109
|
+
# on to enumerate methods -- a value that is not one is worse there than an
|
|
110
|
+
# empty list (#213). The dispatch path below already filters the same way.
|
|
107
111
|
verb = (qualified or dec.qualified_name or "").rsplit(".", 1)[-1]
|
|
108
|
-
return [verb.upper()]
|
|
112
|
+
return [verb.upper()] if verb.lower() in _HTTP_VERBS else []
|
|
109
113
|
if source == "keyword":
|
|
110
114
|
raw = (dec.keyword_arguments or {}).get(spec.get("name", ""))
|
|
111
115
|
value = _literal(raw)
|
codeanalyzer/neo4j/emit.py
CHANGED
|
@@ -31,7 +31,7 @@ from codeanalyzer.neo4j.bolt import BoltConfig, bolt_writer
|
|
|
31
31
|
from codeanalyzer.neo4j.schema import build_schema_document
|
|
32
32
|
from codeanalyzer.neo4j.cypher import render_cypher
|
|
33
33
|
from codeanalyzer.neo4j.project import project
|
|
34
|
-
from codeanalyzer.options import AnalysisOptions
|
|
34
|
+
from codeanalyzer.options import GRAPH_CYPHER, AnalysisOptions
|
|
35
35
|
from codeanalyzer.schema import Analysis
|
|
36
36
|
from codeanalyzer.schema.assign_ids import assign_ids
|
|
37
37
|
from codeanalyzer.utils import logger
|
|
@@ -75,6 +75,6 @@ def emit_neo4j(analysis: Analysis, options: AnalysisOptions) -> None:
|
|
|
75
75
|
|
|
76
76
|
out_dir = options.output if options.output is not None else Path.cwd()
|
|
77
77
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
-
target = out_dir /
|
|
78
|
+
target = out_dir / GRAPH_CYPHER
|
|
79
79
|
target.write_text(render_cypher(rows, app_name))
|
|
80
80
|
logger.info(f"Neo4j graph written to {target}")
|
codeanalyzer/neo4j/project.py
CHANGED
|
@@ -49,7 +49,9 @@ from codeanalyzer.schema import (
|
|
|
49
49
|
PyVariableDeclaration,
|
|
50
50
|
)
|
|
51
51
|
from codeanalyzer.schema import model_dump
|
|
52
|
-
from codeanalyzer.schema.ids import
|
|
52
|
+
from codeanalyzer.schema.ids import (
|
|
53
|
+
application_id, call_body_keys, external_id, global_ordinal, purl_pypi,
|
|
54
|
+
)
|
|
53
55
|
from codeanalyzer.schema.py_schema import PyDecorator, byte_offsets
|
|
54
56
|
|
|
55
57
|
|
|
@@ -183,12 +185,14 @@ def _project_program_graphs(
|
|
|
183
185
|
continue # unstamped callable — assign_ids must run first
|
|
184
186
|
owner = _sym(c.id) # the :PyCallable node, keyed by its can:// id
|
|
185
187
|
# ``callee_signature`` lives on ``PyCallable.call_sites``, not on the body
|
|
186
|
-
# node, so the graph joins the two on the call site's
|
|
188
|
+
# node, so the graph joins the two on the call site's BODY KEY (#203,
|
|
189
|
+
# #215) -- re-derived from the same sequence L1 keyed ``body`` with, so
|
|
190
|
+
# two calls that start at one position keep their own signatures.
|
|
187
191
|
# ``argument_types`` is deliberately not joined: it is the legacy field #86
|
|
188
192
|
# split into ``PyCallArgument``, already carried as ``arguments_json``.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
for cs in (c.call_sites
|
|
193
|
+
sig_by_key = {
|
|
194
|
+
key: cs.callee_signature
|
|
195
|
+
for key, cs in call_body_keys(c.call_sites)
|
|
192
196
|
if cs.callee_signature
|
|
193
197
|
}
|
|
194
198
|
for local_key, node in (c.body or {}).items():
|
|
@@ -204,10 +208,7 @@ def _project_program_graphs(
|
|
|
204
208
|
{
|
|
205
209
|
"kind": node.kind,
|
|
206
210
|
**_span_props(span),
|
|
207
|
-
"callee_signature": (
|
|
208
|
-
sig_by_pos.get((span.start[0], span.start[1]))
|
|
209
|
-
if span else None
|
|
210
|
-
),
|
|
211
|
+
"callee_signature": sig_by_key.get(local_key),
|
|
211
212
|
"var": node.of,
|
|
212
213
|
"call_node": node.parent,
|
|
213
214
|
# Call-site detail (#120). The JSON emits one node per
|
codeanalyzer/options/__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .options import AnalysisOptions, EmitTarget
|
|
1
|
+
from .options import ANALYSIS_JSON, GRAPH_CYPHER, AnalysisOptions, EmitTarget
|
|
2
2
|
|
|
3
|
-
__all__ = ["AnalysisOptions", "EmitTarget"]
|
|
3
|
+
__all__ = ["AnalysisOptions", "EmitTarget", "ANALYSIS_JSON", "GRAPH_CYPHER"]
|
codeanalyzer/options/options.py
CHANGED
|
@@ -4,6 +4,13 @@ from typing import Optional, Tuple
|
|
|
4
4
|
from enum import Enum
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
# The files a run writes into its output directory. Named once so artifact
|
|
8
|
+
# discovery can recognize -- and skip -- the run's own output when it lands
|
|
9
|
+
# inside the analyzed project (#207).
|
|
10
|
+
ANALYSIS_JSON = "analysis.json"
|
|
11
|
+
GRAPH_CYPHER = "graph.cypher"
|
|
12
|
+
|
|
13
|
+
|
|
7
14
|
class EmitTarget(str, Enum):
|
|
8
15
|
"""Output target selected by ``--emit``.
|
|
9
16
|
|
codeanalyzer/schema/ids.py
CHANGED
|
@@ -10,7 +10,7 @@ and artifacts alike — which is what the prefix-scoped destructive statements
|
|
|
10
10
|
an application named ``python`` mints ``can://python/python/...``, so a test
|
|
11
11
|
for ``can://python/`` no longer means "a python id"; test the scheme instead."""
|
|
12
12
|
from __future__ import annotations
|
|
13
|
-
from typing import List, Optional
|
|
13
|
+
from typing import Iterable, Iterator, List, Optional, Tuple
|
|
14
14
|
|
|
15
15
|
SCHEME = "can://"
|
|
16
16
|
|
|
@@ -52,6 +52,42 @@ def external_id(app_id: str, module: Optional[str], name: str) -> str:
|
|
|
52
52
|
return f"{base}/{module}/{name}" if module else f"{base}/{name}"
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
def call_body_keys(sites: Iterable) -> Iterator[Tuple[str, object]]:
|
|
56
|
+
"""The body key of each call site, in recording order: ``line:col``,
|
|
57
|
+
disambiguated ``/2``, ``/3``, ... when nested calls share a start position
|
|
58
|
+
(#215).
|
|
59
|
+
|
|
60
|
+
`getattr(o, n)(x)` begins the outer application and the inner `getattr` at the
|
|
61
|
+
same column, so a bare ``line:col`` key keeps one of the two and the dynamic
|
|
62
|
+
invocation is lost. Call sites are recorded pre-order, so the bare key goes to
|
|
63
|
+
the OUTERMOST call and the nested ones take the suffixes. The spelling is
|
|
64
|
+
codeanalyzer-typescript's (``callBodyKeys``, ``src/schema/l1Body.ts``), adopted
|
|
65
|
+
verbatim; the ``/`` never collides with a param-vertex segment, which always
|
|
66
|
+
begins ``actual_``.
|
|
67
|
+
|
|
68
|
+
The SINGLE definition of the sequence -- L1 builds ``body`` with it, and L2, the
|
|
69
|
+
dataflow builder, the defuse linker and the Neo4j projection re-derive the same
|
|
70
|
+
pairing from it rather than re-deriving a key from a position.
|
|
71
|
+
"""
|
|
72
|
+
used = set()
|
|
73
|
+
for cs in sites or []:
|
|
74
|
+
base = f"{cs.start_line}:{cs.start_column}"
|
|
75
|
+
key = base
|
|
76
|
+
k = 2
|
|
77
|
+
while key in used:
|
|
78
|
+
key = f"{base}/{k}"
|
|
79
|
+
k += 1
|
|
80
|
+
used.add(key)
|
|
81
|
+
yield key, cs
|
|
82
|
+
|
|
83
|
+
def call_body_key(callable_, site) -> Optional[str]:
|
|
84
|
+
"""``site``'s body key within ``callable_`` — the pairing of
|
|
85
|
+
:func:`call_body_keys`, for a caller that holds one site rather than the list."""
|
|
86
|
+
for key, cs in call_body_keys(callable_.call_sites):
|
|
87
|
+
if cs is site:
|
|
88
|
+
return key
|
|
89
|
+
return None
|
|
90
|
+
|
|
55
91
|
def global_ordinal(callable_id: str, local_key: str) -> str:
|
|
56
92
|
"""The GLOBAL ordinal id of a body node from its LOCAL key: synthetic keys
|
|
57
93
|
(`@entry`, `@formal_in:0`) already carry the `@`; positional keys (`15:2`,
|
codeanalyzer/schema/l1_body.py
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"""L1 body population: materialize `call` nodes from existing call sites.
|
|
2
2
|
`callee` is left None here — the sanctioned null→id refinement happens at L2."""
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from codeanalyzer.schema.ids import stamp_body_ids
|
|
4
|
+
from codeanalyzer.schema.ids import call_body_keys, stamp_body_ids
|
|
5
5
|
from codeanalyzer.schema.py_schema import PyApplication, PyClass, PyCallable, BodyNode, Span, byte_offsets
|
|
6
6
|
|
|
7
7
|
def _do_callable(source: str, c: PyCallable) -> None:
|
|
8
|
-
for cs in c.call_sites
|
|
9
|
-
key = f"{cs.start_line}:{cs.start_column}"
|
|
8
|
+
for key, cs in call_body_keys(c.call_sites):
|
|
10
9
|
span = Span(start=(cs.start_line, cs.start_column),
|
|
11
10
|
end=(cs.end_line, cs.end_column),
|
|
12
11
|
bytes=byte_offsets(source, cs.start_line, cs.start_column, cs.end_line, cs.end_column)) if source else None
|
|
@@ -5,17 +5,18 @@ signature; an unresolved call site leaves `callee` absent.
|
|
|
5
5
|
|
|
6
6
|
Two resolution sources feed the backfill: Jedi's `callee_signature` on the
|
|
7
7
|
call site itself, and the defuse linker's returned map (keyed by caller
|
|
8
|
-
signature +
|
|
8
|
+
signature + the call site's body key, which carries a `/N` disambiguator when
|
|
9
|
+
nested calls share a start position -- #215). The linker's resolutions are deliberately NOT written
|
|
9
10
|
into `callee_signature` — the symbol table round-trips through the analysis
|
|
10
11
|
cache, and a persisted resolution would resurface on a warm run as a Jedi
|
|
11
12
|
edge, silently changing provenance."""
|
|
12
13
|
from __future__ import annotations
|
|
14
|
+
from codeanalyzer.schema.ids import call_body_keys
|
|
13
15
|
from codeanalyzer.schema.py_schema import PyApplication, PyClass, PyCallable
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
def _do_callable(c: PyCallable, sig_to_id: dict, resolutions: dict) -> None:
|
|
17
|
-
for cs in c.call_sites
|
|
18
|
-
key = f"{cs.start_line}:{cs.start_column}"
|
|
19
|
+
for key, cs in call_body_keys(c.call_sites):
|
|
19
20
|
jedi_sig = cs.callee_signature
|
|
20
21
|
if jedi_sig and jedi_sig.startswith("typing."):
|
|
21
22
|
# A decorator-typed callable resolved to its annotation, not a
|
|
@@ -40,11 +40,14 @@ import ast
|
|
|
40
40
|
import builtins as _py_builtins
|
|
41
41
|
from typing import Dict, List, Optional, Tuple
|
|
42
42
|
|
|
43
|
+
from codeanalyzer.schema.ids import call_body_key
|
|
43
44
|
from codeanalyzer.schema.py_schema import PyCallable, PyCallEdge, PyClass, PyModule
|
|
44
45
|
|
|
45
46
|
__all__ = ["defuse_linker_edges"]
|
|
46
47
|
|
|
47
|
-
# (caller signature,
|
|
48
|
+
# (caller signature, the call site's body key) -> resolved callee signature.
|
|
49
|
+
# The key is `call_body_key`, not a bare "line:col": two nested calls can start
|
|
50
|
+
# at one position and only one of them is the resolution being recorded (#215).
|
|
48
51
|
Resolutions = Dict[Tuple[str, str], str]
|
|
49
52
|
|
|
50
53
|
_MAX_CHAIN = 16 # assignment-chain hops before giving up (cycle safety net)
|
|
@@ -1095,7 +1098,7 @@ def defuse_linker_edges(
|
|
|
1095
1098
|
oracle.vote(sig, site)
|
|
1096
1099
|
bump(caller.signature, sig)
|
|
1097
1100
|
resolutions[
|
|
1098
|
-
(caller.signature,
|
|
1101
|
+
(caller.signature, call_body_key(caller, site))
|
|
1099
1102
|
] = sig
|
|
1100
1103
|
|
|
1101
1104
|
# Calls Jedi's extractor never recorded as sites at all (with-
|
|
@@ -1398,7 +1401,7 @@ def defuse_linker_edges(
|
|
|
1398
1401
|
oracle.vote(sig, site)
|
|
1399
1402
|
bump(caller.signature, sig)
|
|
1400
1403
|
resolutions[
|
|
1401
|
-
(caller.signature,
|
|
1404
|
+
(caller.signature, call_body_key(caller, site))
|
|
1402
1405
|
] = sig
|
|
1403
1406
|
made_progress = True
|
|
1404
1407
|
else:
|
|
@@ -1452,7 +1455,7 @@ def defuse_linker_edges(
|
|
|
1452
1455
|
oracle.vote(sig, site)
|
|
1453
1456
|
bump(caller.signature, sig)
|
|
1454
1457
|
resolutions[
|
|
1455
|
-
(caller.signature,
|
|
1458
|
+
(caller.signature, call_body_key(caller, site))
|
|
1456
1459
|
] = sig
|
|
1457
1460
|
made_progress = True
|
|
1458
1461
|
remaining = still
|
|
@@ -130,8 +130,9 @@ class SymbolTableBuilder:
|
|
|
130
130
|
return None, False
|
|
131
131
|
|
|
132
132
|
@staticmethod
|
|
133
|
-
def _callee_anchor(node: ast.Call) -> Tuple[int, int]:
|
|
134
|
-
"""Position of the callee *name* for Jedi inference
|
|
133
|
+
def _callee_anchor(node: ast.Call) -> Optional[Tuple[int, int]]:
|
|
134
|
+
"""Position of the callee *name* for Jedi inference, or ``None`` when the
|
|
135
|
+
callee is not a name at all.
|
|
135
136
|
|
|
136
137
|
An ``ast.Call``'s own ``lineno``/``col_offset`` is the first
|
|
137
138
|
character of the whole call expression — for an attribute call
|
|
@@ -139,11 +140,17 @@ class SymbolTableBuilder:
|
|
|
139
140
|
would infer the receiver's type instead of the invoked method
|
|
140
141
|
(issue #80). Anchor attribute calls inside the attribute name —
|
|
141
142
|
its last character, so one-character names stay in range; other
|
|
142
|
-
callee shapes keep the call-expression start.
|
|
143
|
+
callee shapes keep the call-expression start. A callee that is itself a
|
|
144
|
+
call has no name to anchor on, so it yields ``None`` (#215).
|
|
143
145
|
"""
|
|
144
146
|
func_expr = node.func
|
|
145
147
|
if isinstance(func_expr, ast.Attribute):
|
|
146
148
|
return func_expr.end_lineno, func_expr.end_col_offset - 1
|
|
149
|
+
if isinstance(func_expr, ast.Call):
|
|
150
|
+
# `getattr(o, n)(x)`: the callee IS a call, so the expression start is
|
|
151
|
+
# the INNER call's name and inferring there labels this site a call to
|
|
152
|
+
# `getattr` -- the thing that produced the callee, not the callee (#215).
|
|
153
|
+
return None
|
|
147
154
|
return node.lineno, node.col_offset
|
|
148
155
|
|
|
149
156
|
@staticmethod
|
|
@@ -766,11 +773,18 @@ class SymbolTableBuilder:
|
|
|
766
773
|
func_expr = node.func
|
|
767
774
|
|
|
768
775
|
method_name = "<unknown>"
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
776
|
+
anchor = self._callee_anchor(node)
|
|
777
|
+
if anchor is None:
|
|
778
|
+
# A dynamic invocation: the site is recorded, and it resolves to
|
|
779
|
+
# nothing. Guessing a target here is what produced a graph full of
|
|
780
|
+
# calls to `builtins.getattr` (#215).
|
|
781
|
+
callee_signature, is_constructor, return_type = None, False, None
|
|
782
|
+
else:
|
|
783
|
+
anchor_line, anchor_col = anchor
|
|
784
|
+
callee_signature, is_constructor = self._infer_callee(
|
|
785
|
+
script, anchor_line, anchor_col
|
|
786
|
+
)
|
|
787
|
+
return_type = self._infer_call_return_type(script, anchor_line, anchor_col)
|
|
774
788
|
|
|
775
789
|
receiver_expr = None
|
|
776
790
|
receiver_type = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4
|
|
4
4
|
Summary: Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph.
|
|
5
5
|
Author-email: Rahul Krishna <i.m.ralk@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
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=noiZpfaiyFlnmdzHoWOAImXpxDkiQzTxSO4gmyZVyGI,15981
|
|
3
|
+
codeanalyzer/core.py,sha256=UoK-7xgzJx-f3A_fmmeEZkBshiOWNDwc_RJTPhCTU0Y,47655
|
|
4
4
|
codeanalyzer/provenance.py,sha256=DT-DqwdVwO7g-GyK3sC0_4vDNCUjZYEY1fyYCt-7VRM,2306
|
|
5
5
|
codeanalyzer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
codeanalyzer/artifacts/__init__.py,sha256=317sEeZLS1AYsfYDYIk7tLR3AdDFwWw_cmkeZDabkJo,906
|
|
@@ -8,12 +8,12 @@ codeanalyzer/artifacts/config_keys.py,sha256=ovwptAErLYutlCzUwvyim2kH6-6vUKxuRcB
|
|
|
8
8
|
codeanalyzer/artifacts/config_use.py,sha256=1-jGacW249dwF7b6DOrTFuJ7sA5Ho9khgrgZqND0nro,27173
|
|
9
9
|
codeanalyzer/artifacts/config_use_rules.yml,sha256=fNgI6IceOsj78LYmgoDH-vQ4-Mfb2ZFGKIQhLuoL4ik,1483
|
|
10
10
|
codeanalyzer/artifacts/dependencies.py,sha256=h_-XV5KhrT-1Ytguw0c4fyI9zSFzn9Aj7kR66omvL3M,10853
|
|
11
|
-
codeanalyzer/artifacts/discovery.py,sha256=
|
|
11
|
+
codeanalyzer/artifacts/discovery.py,sha256=urN4ZvtnXs0w9Kv2-yeOQza1TV1mjLL007zHfbdjNSg,8416
|
|
12
12
|
codeanalyzer/artifacts/parsers.py,sha256=xOjC53tT0Mv0k7ONGwzl5XUvpBLnZlEcGmyrX5DS45k,9035
|
|
13
13
|
codeanalyzer/dataflow/__init__.py,sha256=HCNOH24rAGLUMG94N9aC-2SsAWSntk3uys7e39ipU5o,1725
|
|
14
14
|
codeanalyzer/dataflow/access_paths.py,sha256=wC8Q9qD-RZzkoFWMVvu_6uNNmYP8z48OGp9h9v3F1d4,23623
|
|
15
15
|
codeanalyzer/dataflow/alias.py,sha256=ZYKY0DiR3GHv6YJr7C001Lode9rZsk6-gxdomUyIvwg,3928
|
|
16
|
-
codeanalyzer/dataflow/builder.py,sha256=
|
|
16
|
+
codeanalyzer/dataflow/builder.py,sha256=Ku16XuEJj46lnfuigMMyR6yheRW52f5U-ih9msxqSyo,33402
|
|
17
17
|
codeanalyzer/dataflow/cfg.py,sha256=u5YXJjAaDshdgv-9kWWITbBJFP-MwXKq6hbNh0fhDm8,25307
|
|
18
18
|
codeanalyzer/dataflow/defuse.py,sha256=LrX1ToOZzR79NlAGg5T647UAFkpiEqEnT7t0K5RXOiI,4377
|
|
19
19
|
codeanalyzer/dataflow/dominance.py,sha256=X7Ki1QRdVqaseYsJtiUL7m9MbcC2WHEv9b8bczSLFUA,5086
|
|
@@ -21,7 +21,7 @@ codeanalyzer/dataflow/identity.py,sha256=6aAz3iPSOpoS7l_moC7XGr2RS_GqJp_FlsWGglM
|
|
|
21
21
|
codeanalyzer/dataflow/pdg.py,sha256=1Bm7AnoRqXBryZulII2idPw0feV8eZQ1VqqprQ8PW-g,3731
|
|
22
22
|
codeanalyzer/dataflow/scalpel_oracle.py,sha256=FxRadKrTWar0VKHyQJM40n3cq25sld3Sj4lMdOm5NFk,11494
|
|
23
23
|
codeanalyzer/dataflow/scc.py,sha256=Doa_0-5f3agCu_5UmeEDlCUErg34TtniKIv8Uqd7Jiw,3493
|
|
24
|
-
codeanalyzer/dataflow/sdg.py,sha256=
|
|
24
|
+
codeanalyzer/dataflow/sdg.py,sha256=_dcR6nNc0ZTETaLHbmliEMeTG-AcQU0x-7DiB1cvoPs,19445
|
|
25
25
|
codeanalyzer/dataflow/slicing.py,sha256=lWZ7jHhlR8m7rECWJQXvwfs5GNZeJY55Cud3Ji5UbNU,3654
|
|
26
26
|
codeanalyzer/dataflow/summaries.py,sha256=TLtc5h4bLBC4lViuMhMlEp5rp_nBpDPOl4_wfBuRUY4,9210
|
|
27
27
|
codeanalyzer/dataflow/syntactic.py,sha256=AbHyXjKX_1xkGgKH48BpXYCWBauActGUwSMF_OD-uys,1124
|
|
@@ -38,7 +38,7 @@ codeanalyzer/dataflow/scalpel/core/func_call_visitor.py,sha256=ps0snjTchBXilhor3
|
|
|
38
38
|
codeanalyzer/dataflow/scalpel/core/vars_visitor.py,sha256=gE5fNyJS6jslD6vsMRbFLoy3n1xTwH-b54mBcNYO72M,5660
|
|
39
39
|
codeanalyzer/entrypoints/__init__.py,sha256=VaMd4mSEPLuPlRxxAve_nTJ9ZH62stXZGTPNwU_BQ50,99
|
|
40
40
|
codeanalyzer/entrypoints/detect.py,sha256=fsWRQz1njvB9d3LIJEHxsNFE1GrB7q52KCvozw_UIUQ,4594
|
|
41
|
-
codeanalyzer/entrypoints/matching.py,sha256=
|
|
41
|
+
codeanalyzer/entrypoints/matching.py,sha256=fppta04o1_HWBEHe6boV_7T964cdLuZlOoQHAW4yt0Q,8321
|
|
42
42
|
codeanalyzer/entrypoints/pipeline.py,sha256=vVSdttMfSAWour2fsC3LomMtwFLzRqmJHixJS82VY1I,7828
|
|
43
43
|
codeanalyzer/entrypoints/rules.py,sha256=yG772JHmNc2L10jIpxfg1xTh-QouJa6lNU9ubo3EZYg,6003
|
|
44
44
|
codeanalyzer/entrypoints/rules.yml,sha256=sguICRfDDNGjJPfZz-JSwkI_hzbTJaau1liHV_y4rTA,4637
|
|
@@ -47,32 +47,32 @@ codeanalyzer/jedi/jedi.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
47
47
|
codeanalyzer/neo4j/__init__.py,sha256=8Ei6uThTOqmul6GhnKoGOjLXJZbyDwF5Jo-8I8NxnAk,1574
|
|
48
48
|
codeanalyzer/neo4j/bolt.py,sha256=8l70iGDjY3Kdm-W1sb3b3O1uqLjDrVPYTpXhnkXdZOY,13694
|
|
49
49
|
codeanalyzer/neo4j/cypher.py,sha256=-wledvXVrcLJRtSC5MCn1N3tqgC06rNfPBn0w6POqHo,6239
|
|
50
|
-
codeanalyzer/neo4j/emit.py,sha256=
|
|
51
|
-
codeanalyzer/neo4j/project.py,sha256=
|
|
50
|
+
codeanalyzer/neo4j/emit.py,sha256=7QYosqiadH2GIsjaUSmSlJ5g8yLjAAcMplsmFaJLnC4,3599
|
|
51
|
+
codeanalyzer/neo4j/project.py,sha256=o6jdjwvHRxpNPNn-DXpOzebFq-fXgxoXTrKZ4_2ZVbQ,42383
|
|
52
52
|
codeanalyzer/neo4j/rows.py,sha256=n_J3NVIusLPsHMjPCWaYvlL5sFm6V-kl_Z6B_BTH8Fk,10140
|
|
53
53
|
codeanalyzer/neo4j/schema.py,sha256=0qZ1qJgmutlMHyIjdfD5N9QtA-gwl8Qcgh4NGguQEMs,18089
|
|
54
|
-
codeanalyzer/options/__init__.py,sha256=
|
|
55
|
-
codeanalyzer/options/options.py,sha256=
|
|
54
|
+
codeanalyzer/options/__init__.py,sha256=HvKzVXBI91lBM941JSFN7npG4S2fHlVEKo9VC2lRso8,156
|
|
55
|
+
codeanalyzer/options/options.py,sha256=DAo1i08D0K16jCivTrdOcBgKRPO50SngFZTPAL_kpmY,1862
|
|
56
56
|
codeanalyzer/schema/__init__.py,sha256=hIyz02abUJNPW8yUgguaeKflZjTmOZWbDiKICNBD7yk,4141
|
|
57
57
|
codeanalyzer/schema/assign_ids.py,sha256=pQHluLaO5a6hFREp3vjVRsUeAfTYSJ95RXS9TZvnoys,1590
|
|
58
58
|
codeanalyzer/schema/call_graph_ids.py,sha256=mWAhJDBsW8i-siJiINOHCXEm4qM6F_5se9-HuHWIFqU,568
|
|
59
|
-
codeanalyzer/schema/ids.py,sha256=
|
|
60
|
-
codeanalyzer/schema/l1_body.py,sha256
|
|
61
|
-
codeanalyzer/schema/l2_callees.py,sha256=
|
|
59
|
+
codeanalyzer/schema/ids.py,sha256=eShvV3R6oQt1lCQ2DFAhWekSWom63wQJDfeWXJ7KovQ,5986
|
|
60
|
+
codeanalyzer/schema/l1_body.py,sha256=-rBXZH1nhaCyQ2DVN-iIswuuSQ7Jsa8voSAaljv_c80,1737
|
|
61
|
+
codeanalyzer/schema/l2_callees.py,sha256=UGubBSLA_-GCK9Kak2rqkmoFlGifiImVnPDrypjHfrA,2441
|
|
62
62
|
codeanalyzer/schema/py_schema.py,sha256=4xJto32nWqHx0dkge4qHsV2JZSjbyv4ojnqpyBm78sQ,24588
|
|
63
63
|
codeanalyzer/semantic_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
64
|
codeanalyzer/semantic_analysis/call_graph.py,sha256=6YEB_wTn5-oQYLrbIhJYE0BsHl4fpWPoy5Hwd9mTnGc,11918
|
|
65
|
-
codeanalyzer/semantic_analysis/defuse_linker.py,sha256=
|
|
65
|
+
codeanalyzer/semantic_analysis/defuse_linker.py,sha256=3n1ew94eyBsSI98cFqP0uMHFUv7ob7QR9DL2hTKMHsQ,65099
|
|
66
66
|
codeanalyzer/syntactic_analysis/__init__.py,sha256=EUQkJEh6wHjWx2qTTKbTbUgwSbfKeNieKHNy7RknVXA,476
|
|
67
67
|
codeanalyzer/syntactic_analysis/exceptions.py,sha256=whs_n0vIu655Jkk1a7iOoXY6iIca4pZqJnU40V9Ejaw,537
|
|
68
68
|
codeanalyzer/syntactic_analysis/import_resolver.py,sha256=Q8noZwSdDNt4P4NMqDTB9FaS-M0_d4ASK9GnJop9MDI,2751
|
|
69
|
-
codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=
|
|
69
|
+
codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=tKed-VCSd_albMeZfUuPRCcKz_WEBic38DU21gM8o7g,48797
|
|
70
70
|
codeanalyzer/utils/__init__.py,sha256=hC6VWdR5rerSqBxzu9KQHTASWqwrrYJv-CMDwrTlzkc,137
|
|
71
71
|
codeanalyzer/utils/logging.py,sha256=Fw0tattPAOMs3o0JMjjXhRVLIF64f-SCcygUXF9jqeg,904
|
|
72
72
|
codeanalyzer/utils/progress_bar.py,sha256=C9JtzVdd10lIxTv-KA6PebqjKWueC_vMGwVzAtHuHIw,2818
|
|
73
|
-
codeanalyzer_python-1.5.
|
|
74
|
-
codeanalyzer_python-1.5.
|
|
75
|
-
codeanalyzer_python-1.5.
|
|
76
|
-
codeanalyzer_python-1.5.
|
|
77
|
-
codeanalyzer_python-1.5.
|
|
78
|
-
codeanalyzer_python-1.5.
|
|
73
|
+
codeanalyzer_python-1.5.4.dist-info/METADATA,sha256=pky-NFu9SoPYS5JNaPnJF4J-9yTTp02bDGA0qxkorpA,44845
|
|
74
|
+
codeanalyzer_python-1.5.4.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
75
|
+
codeanalyzer_python-1.5.4.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
|
|
76
|
+
codeanalyzer_python-1.5.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
77
|
+
codeanalyzer_python-1.5.4.dist-info/licenses/NOTICE,sha256=MdVkNYqHJ20on2FmWvgD4WpMsX5mir33xdyPvTXd0A0,1223
|
|
78
|
+
codeanalyzer_python-1.5.4.dist-info/RECORD,,
|
|
File without changes
|
{codeanalyzer_python-1.5.2.dist-info → codeanalyzer_python-1.5.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{codeanalyzer_python-1.5.2.dist-info → codeanalyzer_python-1.5.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|