vigil-codeintel 0.1.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.
- vigil_codeintel-0.1.0.dist-info/METADATA +780 -0
- vigil_codeintel-0.1.0.dist-info/RECORD +131 -0
- vigil_codeintel-0.1.0.dist-info/WHEEL +5 -0
- vigil_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
- vigil_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
- vigil_codeintel-0.1.0.dist-info/top_level.txt +3 -0
- vigil_forensic/__init__.py +224 -0
- vigil_forensic/_git_utils.py +178 -0
- vigil_forensic/_shared.py +510 -0
- vigil_forensic/_stubs.py +156 -0
- vigil_forensic/gate_checks/__init__.py +1 -0
- vigil_forensic/gate_checks/_ast_helpers.py +629 -0
- vigil_forensic/gate_checks/_deployment_detector.py +573 -0
- vigil_forensic/gate_checks/atomic_write_checks.py +1143 -0
- vigil_forensic/gate_checks/authority_checks.py +95 -0
- vigil_forensic/gate_checks/boundary_breach_checks.py +202 -0
- vigil_forensic/gate_checks/broad_except_checks.py +301 -0
- vigil_forensic/gate_checks/broad_except_hidden_sentinel_checks.py +365 -0
- vigil_forensic/gate_checks/common.py +253 -0
- vigil_forensic/gate_checks/config_safety_checks.py +704 -0
- vigil_forensic/gate_checks/config_ssot_checks.py +78 -0
- vigil_forensic/gate_checks/conflict_checks.py +193 -0
- vigil_forensic/gate_checks/context_fallback_checks.py +697 -0
- vigil_forensic/gate_checks/context_health_checks.py +289 -0
- vigil_forensic/gate_checks/contract_shape_drift_checks.py +459 -0
- vigil_forensic/gate_checks/dirty_baseline_check.py +274 -0
- vigil_forensic/gate_checks/duplication_checks.py +387 -0
- vigil_forensic/gate_checks/embedded_string_checks.py +123 -0
- vigil_forensic/gate_checks/empty_output_checks.py +87 -0
- vigil_forensic/gate_checks/encoding_checks.py +847 -0
- vigil_forensic/gate_checks/export_completeness_checks.py +156 -0
- vigil_forensic/gate_checks/fallback_checks.py +41 -0
- vigil_forensic/gate_checks/file_proliferation_checks.py +171 -0
- vigil_forensic/gate_checks/fix_without_test_checks.py +69 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/__init__.py +9 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/_helpers.py +71 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/advanced_checks.py +322 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/core.py +273 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/integrity_checks.py +203 -0
- vigil_forensic/gate_checks/forensic_cluster_runners/quality_checks.py +666 -0
- vigil_forensic/gate_checks/forensic_clusters/__init__.py +193 -0
- vigil_forensic/gate_checks/forensic_clusters/allowlist.py +426 -0
- vigil_forensic/gate_checks/forensic_clusters/allowlist_writer.py +302 -0
- vigil_forensic/gate_checks/forensic_clusters/api_protocol.py +231 -0
- vigil_forensic/gate_checks/forensic_clusters/async_quality.py +1156 -0
- vigil_forensic/gate_checks/forensic_clusters/code_style.py +808 -0
- vigil_forensic/gate_checks/forensic_clusters/core.py +319 -0
- vigil_forensic/gate_checks/forensic_clusters/data_quality.py +763 -0
- vigil_forensic/gate_checks/forensic_clusters/dead_code.py +480 -0
- vigil_forensic/gate_checks/forensic_clusters/edit_mutation.py +842 -0
- vigil_forensic/gate_checks/forensic_clusters/exception_boundary.py +240 -0
- vigil_forensic/gate_checks/forensic_clusters/legacy_debt.py +556 -0
- vigil_forensic/gate_checks/forensic_clusters/static_analysis.py +834 -0
- vigil_forensic/gate_checks/forensic_clusters/structural_quality.py +298 -0
- vigil_forensic/gate_checks/god_object_zones_checks.py +173 -0
- vigil_forensic/gate_checks/hallucination_checks.py +566 -0
- vigil_forensic/gate_checks/hunter_artifact_completeness_check.py +139 -0
- vigil_forensic/gate_checks/implementation_overfit_checks.py +380 -0
- vigil_forensic/gate_checks/import_integrity_checks.py +233 -0
- vigil_forensic/gate_checks/imports_in_function_checks.py +283 -0
- vigil_forensic/gate_checks/ml_checks.py +318 -0
- vigil_forensic/gate_checks/performance_checks.py +106 -0
- vigil_forensic/gate_checks/project_specific_runner.py +691 -0
- vigil_forensic/gate_checks/provider_capability_checks.py +73 -0
- vigil_forensic/gate_checks/refactor_completeness_checks.py +274 -0
- vigil_forensic/gate_checks/reliability_checks.py +389 -0
- vigil_forensic/gate_checks/reporting_checks.py +55 -0
- vigil_forensic/gate_checks/runtime_behavior_checks.py +220 -0
- vigil_forensic/gate_checks/security_injection_checks.py +332 -0
- vigil_forensic/gate_checks/semantic_intent_checks.py +139 -0
- vigil_forensic/gate_checks/size_complexity_checks.py +336 -0
- vigil_forensic/gate_checks/stuck_feature_flag_checks.py +354 -0
- vigil_forensic/gate_checks/syntax_validity_checks.py +217 -0
- vigil_forensic/gate_checks/temporal_freshness_checks.py +79 -0
- vigil_forensic/gate_checks/test_quality_checks.py +946 -0
- vigil_forensic/gate_checks/testing_checks.py +149 -0
- vigil_forensic/gate_checks/toctou_checks.py +367 -0
- vigil_forensic/gate_checks/type_checking_checks.py +316 -0
- vigil_forensic/gate_models.py +392 -0
- vigil_forensic/gate_packs/__init__.py +1 -0
- vigil_forensic/gate_packs/universal.py +179 -0
- vigil_forensic/gate_profile.json +31 -0
- vigil_forensic/gate_registry.py +21 -0
- vigil_forensic/language_profiles.py +219 -0
- vigil_forensic/meta_findings.py +207 -0
- vigil_forensic/self_audit.py +725 -0
- vigil_forensic/source_analysis.py +175 -0
- vigil_mapper/__init__.py +103 -0
- vigil_mapper/_ast_helpers_minimal.py +229 -0
- vigil_mapper/_extract_imports_impl.py +123 -0
- vigil_mapper/_file_count_guard.py +129 -0
- vigil_mapper/_git_utils.py +178 -0
- vigil_mapper/_runtime_ast.py +438 -0
- vigil_mapper/_runtime_dispatch.py +137 -0
- vigil_mapper/_seed_helpers.py +82 -0
- vigil_mapper/authority_builder.py +1102 -0
- vigil_mapper/cli_entry.py +731 -0
- vigil_mapper/conflict_builder.py +818 -0
- vigil_mapper/data_contract_builder.py +446 -0
- vigil_mapper/findings_builder.py +716 -0
- vigil_mapper/fingerprint.py +53 -0
- vigil_mapper/hotspot_builder.py +539 -0
- vigil_mapper/map_common.py +449 -0
- vigil_mapper/map_errors.py +55 -0
- vigil_mapper/map_models.py +431 -0
- vigil_mapper/map_models_ext.py +206 -0
- vigil_mapper/map_models_findings.py +130 -0
- vigil_mapper/map_storage.py +455 -0
- vigil_mapper/parse_cache.py +795 -0
- vigil_mapper/refactor_boundary_builder.py +266 -0
- vigil_mapper/runtime_builder.py +527 -0
- vigil_mapper/runtime_tracer.py +243 -0
- vigil_mapper/runtime_tracer_entry.py +199 -0
- vigil_mapper/semantic_diff.py +71 -0
- vigil_mapper/source_adapters/__init__.py +109 -0
- vigil_mapper/source_adapters/_base.py +264 -0
- vigil_mapper/source_adapters/_ir.py +156 -0
- vigil_mapper/source_adapters/_lexer.py +309 -0
- vigil_mapper/source_adapters/_patterns.py +212 -0
- vigil_mapper/source_adapters/_treesitter.py +182 -0
- vigil_mapper/source_adapters/go.py +553 -0
- vigil_mapper/source_adapters/java.py +541 -0
- vigil_mapper/source_adapters/javascript.py +626 -0
- vigil_mapper/source_adapters/python.py +325 -0
- vigil_mapper/source_adapters/typescript.py +749 -0
- vigil_mapper/structural_builder.py +586 -0
- vigil_mcp/__init__.py +1 -0
- vigil_mcp/_jobs.py +587 -0
- vigil_mcp/_paths.py +93 -0
- vigil_mcp/forensic_server.py +419 -0
- vigil_mcp/map_server.py +452 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Python source adapter -- wraps stdlib ``ast`` for IR signal extraction.
|
|
2
|
+
|
|
3
|
+
Does NOT use regex. Does NOT depend on ``_lexer``. Uses ``ast.parse``
|
|
4
|
+
exclusively, inheriting empty-list fallbacks from ``RegexAdapterBase`` for
|
|
5
|
+
capability methods not yet wired into builders (L1 stubs).
|
|
6
|
+
|
|
7
|
+
Capabilities:
|
|
8
|
+
- extract_imports: working AST walker (``ast.Import`` / ``ast.ImportFrom``).
|
|
9
|
+
- extract_symbols: working AST walker (top-level class / function defs).
|
|
10
|
+
- extract_contracts: L1 stub -- returns []. L3+ wires data_contract_builder.
|
|
11
|
+
- extract_runtime: L1 stub -- returns []. L3+ wires runtime_builder.
|
|
12
|
+
- extract_writer_calls: L1 stub -- returns []. L3+ wires authority_builder.
|
|
13
|
+
|
|
14
|
+
Builders do NOT consume IR in L1 -- they continue calling their internal
|
|
15
|
+
helpers directly. PythonAdapter is ready for L2+ dispatch wiring.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
import logging
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ._base import RegexAdapterBase
|
|
24
|
+
from ._ir import (
|
|
25
|
+
AuthorityWriteCandidate,
|
|
26
|
+
ContractCandidate,
|
|
27
|
+
ImportEdge,
|
|
28
|
+
RuntimeSignal,
|
|
29
|
+
SymbolDef,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = ["PythonAdapter"]
|
|
33
|
+
|
|
34
|
+
_log = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PythonAdapter(RegexAdapterBase):
|
|
38
|
+
"""Python adapter using stdlib ``ast``. All four map capabilities declared.
|
|
39
|
+
|
|
40
|
+
extract_imports and extract_symbols are fully implemented via AST walking.
|
|
41
|
+
extract_contracts, extract_runtime, and extract_writer_calls are L1 stubs
|
|
42
|
+
that return empty lists -- their existing builder implementations remain
|
|
43
|
+
authoritative until L3+ dispatch wiring.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
language = "python"
|
|
47
|
+
file_extensions = (".py",)
|
|
48
|
+
supports_structural = True
|
|
49
|
+
supports_contracts = True
|
|
50
|
+
supports_runtime_signals = True
|
|
51
|
+
supports_authority_writes = True
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
# Structural: imports + symbols
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def extract_imports(self, content: str, path: Path) -> list[ImportEdge]:
|
|
58
|
+
"""Parse *content* with ``ast`` and return one ImportEdge per import.
|
|
59
|
+
|
|
60
|
+
Handles:
|
|
61
|
+
``import X`` -- kind="absolute"
|
|
62
|
+
``import X as Y`` -- kind="absolute" (alias ignored; module name kept)
|
|
63
|
+
``from X import Y`` -- kind="absolute"
|
|
64
|
+
``from .X import Y`` -- kind="relative" (leading dots preserved)
|
|
65
|
+
``from ..X import Y`` -- kind="relative"
|
|
66
|
+
|
|
67
|
+
Returns [] on ``SyntaxError`` without raising.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
tree = ast.parse(content)
|
|
71
|
+
except SyntaxError as exc:
|
|
72
|
+
_log.debug(
|
|
73
|
+
"extract_imports: SyntaxError in %s at line %s -- returning []",
|
|
74
|
+
path,
|
|
75
|
+
getattr(exc, "lineno", "?"),
|
|
76
|
+
)
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
imports: list[ImportEdge] = []
|
|
80
|
+
|
|
81
|
+
for node in ast.walk(tree):
|
|
82
|
+
if isinstance(node, ast.Import):
|
|
83
|
+
for alias in node.names:
|
|
84
|
+
imports.append(
|
|
85
|
+
ImportEdge(
|
|
86
|
+
from_file=path.as_posix(),
|
|
87
|
+
to_module=alias.name,
|
|
88
|
+
kind="absolute",
|
|
89
|
+
line=node.lineno,
|
|
90
|
+
confidence=1.0,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
elif isinstance(node, ast.ImportFrom):
|
|
95
|
+
if node.module is None:
|
|
96
|
+
# ``from . import X`` — no module name; emit one edge per name
|
|
97
|
+
dots = "." * (node.level or 0)
|
|
98
|
+
for alias in node.names:
|
|
99
|
+
imports.append(
|
|
100
|
+
ImportEdge(
|
|
101
|
+
from_file=path.as_posix(),
|
|
102
|
+
to_module=f"{dots}{alias.name}",
|
|
103
|
+
kind="relative",
|
|
104
|
+
line=node.lineno,
|
|
105
|
+
confidence=1.0,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
dots = "." * (node.level or 0)
|
|
110
|
+
kind = "relative" if node.level else "absolute"
|
|
111
|
+
imports.append(
|
|
112
|
+
ImportEdge(
|
|
113
|
+
from_file=path.as_posix(),
|
|
114
|
+
to_module=f"{dots}{node.module}",
|
|
115
|
+
kind=kind,
|
|
116
|
+
line=node.lineno,
|
|
117
|
+
confidence=1.0,
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return imports
|
|
122
|
+
|
|
123
|
+
def extract_symbols(self, content: str, path: Path) -> list[SymbolDef]:
|
|
124
|
+
"""Parse *content* with ``ast`` and return top-level class/function defs.
|
|
125
|
+
|
|
126
|
+
Only inspects ``tree.body`` (module-level statements) -- nested classes
|
|
127
|
+
and functions are not emitted in L1. Visibility follows Python convention:
|
|
128
|
+
names starting with ``_`` are ``"private"``, all others ``"public"``.
|
|
129
|
+
|
|
130
|
+
Returns [] on ``SyntaxError`` without raising.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
tree = ast.parse(content)
|
|
134
|
+
except SyntaxError as exc:
|
|
135
|
+
_log.debug(
|
|
136
|
+
"extract_symbols: SyntaxError in %s at line %s -- returning []",
|
|
137
|
+
path,
|
|
138
|
+
getattr(exc, "lineno", "?"),
|
|
139
|
+
)
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
syms: list[SymbolDef] = []
|
|
143
|
+
|
|
144
|
+
for node in tree.body:
|
|
145
|
+
if isinstance(node, ast.ClassDef):
|
|
146
|
+
syms.append(
|
|
147
|
+
SymbolDef(
|
|
148
|
+
name=node.name,
|
|
149
|
+
kind="class",
|
|
150
|
+
line=node.lineno,
|
|
151
|
+
visibility=(
|
|
152
|
+
"private" if node.name.startswith("_") else "public"
|
|
153
|
+
),
|
|
154
|
+
confidence=1.0,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
158
|
+
syms.append(
|
|
159
|
+
SymbolDef(
|
|
160
|
+
name=node.name,
|
|
161
|
+
kind="function",
|
|
162
|
+
line=node.lineno,
|
|
163
|
+
visibility=(
|
|
164
|
+
"private" if node.name.startswith("_") else "public"
|
|
165
|
+
),
|
|
166
|
+
confidence=1.0,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return syms
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
# AST helpers
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _dotted(node: ast.AST) -> str:
|
|
178
|
+
"""Return the dotted name of a Name/Attribute chain (best effort)."""
|
|
179
|
+
parts: list[str] = []
|
|
180
|
+
cur = node
|
|
181
|
+
while isinstance(cur, ast.Attribute):
|
|
182
|
+
parts.append(cur.attr)
|
|
183
|
+
cur = cur.value
|
|
184
|
+
if isinstance(cur, ast.Name):
|
|
185
|
+
parts.append(cur.id)
|
|
186
|
+
return ".".join(reversed(parts))
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _receiver_hint(func_node: ast.AST) -> str:
|
|
190
|
+
"""For an attribute call ``x.write_text(...)`` return the receiver name."""
|
|
191
|
+
if isinstance(func_node, ast.Attribute):
|
|
192
|
+
recv = func_node.value
|
|
193
|
+
if isinstance(recv, ast.Name):
|
|
194
|
+
return recv.id
|
|
195
|
+
if isinstance(recv, ast.Attribute):
|
|
196
|
+
return recv.attr
|
|
197
|
+
return ""
|
|
198
|
+
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
# Contracts: @dataclass / NamedTuple / TypedDict / pydantic.BaseModel
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def extract_contracts(self, content: str, path: Path) -> list[ContractCandidate]:
|
|
204
|
+
"""Detect data-contract classes via ``ast`` (parity with Go/Java/TS)."""
|
|
205
|
+
try:
|
|
206
|
+
tree = ast.parse(content)
|
|
207
|
+
except SyntaxError:
|
|
208
|
+
return []
|
|
209
|
+
out: list[ContractCandidate] = []
|
|
210
|
+
for node in ast.walk(tree):
|
|
211
|
+
if not isinstance(node, ast.ClassDef):
|
|
212
|
+
continue
|
|
213
|
+
kind: str | None = None
|
|
214
|
+
for dec in node.decorator_list:
|
|
215
|
+
target = dec.func if isinstance(dec, ast.Call) else dec
|
|
216
|
+
if self._dotted(target).split(".")[-1] == "dataclass":
|
|
217
|
+
kind = "dataclass"
|
|
218
|
+
break
|
|
219
|
+
if kind is None:
|
|
220
|
+
for base in node.bases:
|
|
221
|
+
leaf = self._dotted(base).split(".")[-1]
|
|
222
|
+
if leaf == "BaseModel":
|
|
223
|
+
kind = "pydantic_model"
|
|
224
|
+
break
|
|
225
|
+
if leaf == "TypedDict":
|
|
226
|
+
kind = "TypedDict"
|
|
227
|
+
break
|
|
228
|
+
if leaf == "NamedTuple":
|
|
229
|
+
kind = "NamedTuple"
|
|
230
|
+
break
|
|
231
|
+
if kind:
|
|
232
|
+
out.append(ContractCandidate(
|
|
233
|
+
name=node.name, contract_kind=kind, line=node.lineno, confidence=1.0,
|
|
234
|
+
))
|
|
235
|
+
return out
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
# Runtime: import-time side effects, decorator registries, env reads
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
_REGISTRY_DECORATORS = frozenset({
|
|
242
|
+
"route", "register", "task", "command", "on_event",
|
|
243
|
+
"get", "post", "put", "delete", "fixture", "app",
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
def extract_runtime(self, content: str, path: Path) -> list[RuntimeSignal]:
|
|
247
|
+
"""Detect import-time side effects / decorator registries / env reads via ``ast``."""
|
|
248
|
+
try:
|
|
249
|
+
tree = ast.parse(content)
|
|
250
|
+
except SyntaxError:
|
|
251
|
+
return []
|
|
252
|
+
out: list[RuntimeSignal] = []
|
|
253
|
+
# module-level bare calls = import-time side effects
|
|
254
|
+
for stmt in tree.body:
|
|
255
|
+
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
|
|
256
|
+
fname = self._dotted(stmt.value.func)
|
|
257
|
+
out.append(RuntimeSignal(
|
|
258
|
+
signal_kind="import_time_side_effects",
|
|
259
|
+
detail=f"module-level call {fname}()",
|
|
260
|
+
line=stmt.lineno, confidence=0.8,
|
|
261
|
+
))
|
|
262
|
+
for node in ast.walk(tree):
|
|
263
|
+
if isinstance(node, ast.Call):
|
|
264
|
+
fn = self._dotted(node.func)
|
|
265
|
+
if fn in ("os.getenv",) or fn.endswith("environ.get") or fn.endswith("os.environ"):
|
|
266
|
+
out.append(RuntimeSignal(
|
|
267
|
+
signal_kind="env_var_read", detail=fn,
|
|
268
|
+
line=int(getattr(node, "lineno", 0) or 0), confidence=0.9,
|
|
269
|
+
))
|
|
270
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
271
|
+
for dec in node.decorator_list:
|
|
272
|
+
target = dec.func if isinstance(dec, ast.Call) else dec
|
|
273
|
+
dn = self._dotted(target)
|
|
274
|
+
if dn.split(".")[-1] in self._REGISTRY_DECORATORS:
|
|
275
|
+
out.append(RuntimeSignal(
|
|
276
|
+
signal_kind="decorator_registry",
|
|
277
|
+
detail=f"@{dn} on {node.name}",
|
|
278
|
+
line=node.lineno, confidence=0.7,
|
|
279
|
+
))
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
# ------------------------------------------------------------------
|
|
283
|
+
# Authority writes: .write_text/.write_bytes/.save/json.dump/open("w")
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
def extract_writer_calls(
|
|
287
|
+
self, content: str, path: Path
|
|
288
|
+
) -> list[AuthorityWriteCandidate]:
|
|
289
|
+
"""Detect write/save operations via ``ast`` (parity with other adapters)."""
|
|
290
|
+
try:
|
|
291
|
+
tree = ast.parse(content)
|
|
292
|
+
except SyntaxError:
|
|
293
|
+
return []
|
|
294
|
+
out: list[AuthorityWriteCandidate] = []
|
|
295
|
+
for node in ast.walk(tree):
|
|
296
|
+
if not isinstance(node, ast.Call):
|
|
297
|
+
continue
|
|
298
|
+
fn = self._dotted(node.func)
|
|
299
|
+
leaf = fn.split(".")[-1]
|
|
300
|
+
ln = int(getattr(node, "lineno", 0) or 0)
|
|
301
|
+
if leaf in ("write_text", "write_bytes"):
|
|
302
|
+
out.append(AuthorityWriteCandidate(
|
|
303
|
+
write_kind=leaf, target_hint=self._receiver_hint(node.func),
|
|
304
|
+
line=ln, confidence=0.9,
|
|
305
|
+
))
|
|
306
|
+
elif leaf == "save":
|
|
307
|
+
out.append(AuthorityWriteCandidate(
|
|
308
|
+
write_kind="save", target_hint=self._receiver_hint(node.func),
|
|
309
|
+
line=ln, confidence=0.7,
|
|
310
|
+
))
|
|
311
|
+
elif fn in ("json.dump",):
|
|
312
|
+
out.append(AuthorityWriteCandidate(
|
|
313
|
+
write_kind="json_dump", target_hint="", line=ln, confidence=0.9,
|
|
314
|
+
))
|
|
315
|
+
elif leaf == "open" and len(node.args) >= 2:
|
|
316
|
+
mode = node.args[1]
|
|
317
|
+
if (
|
|
318
|
+
isinstance(mode, ast.Constant)
|
|
319
|
+
and isinstance(mode.value, str)
|
|
320
|
+
and any(c in mode.value for c in "wax+")
|
|
321
|
+
):
|
|
322
|
+
out.append(AuthorityWriteCandidate(
|
|
323
|
+
write_kind="open_write", target_hint="", line=ln, confidence=0.8,
|
|
324
|
+
))
|
|
325
|
+
return out
|