tzero-mcp 10.7.0 → 10.7.2
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.
- package/CHANGELOG.md +589 -0
- package/CONTRIBUTING.md +71 -0
- package/README.md +25 -17
- package/bin/postinstall.js +34 -0
- package/bin/tzero-mcp.js +16 -19
- package/bin/tzero.js +14 -19
- package/index.d.ts +24 -0
- package/index.js +7 -3
- package/package.json +24 -2
- package/src/addmcp.py +1194 -0
- package/src/main.py +60 -0
- package/src/siber_akademi_icon.py +77 -0
- package/src/tzero/jarvis/semantic_compressor.py +373 -0
- package/src/tzero.py +58 -0
- package/src/tzero_benchmark.py +475 -0
- package/src/tzero_bridge.py +362 -0
- package/src/tzero_ci.py +153 -0
- package/src/tzero_commit.py +192 -0
- package/src/tzero_cost.py +163 -0
- package/src/tzero_dashboard.py +1448 -0
- package/src/tzero_deps.py +287 -0
- package/src/tzero_drift.py +208 -0
- package/src/tzero_explain.py +240 -0
- package/src/tzero_features.py +3022 -0
- package/src/tzero_fix.py +228 -0
- package/src/tzero_fixer/__init__.py +63 -0
- package/src/tzero_fixer/amalgamation.py +334 -0
- package/src/tzero_fixer/ast_syntax_healer.py +464 -0
- package/src/tzero_fixer/ast_type_modernizer.py +288 -0
- package/src/tzero_fixer/async_io_modernizer.py +127 -0
- package/src/tzero_fixer/automated_regression_suite.py +600 -0
- package/src/tzero_fixer/benchmarks_and_diagnostics.py +174 -0
- package/src/tzero_fixer/ci_cd_self_healing_gate.py +246 -0
- package/src/tzero_fixer/code_duplicity_and_refactorer.py +151 -0
- package/src/tzero_fixer/code_smells_healer.py +260 -0
- package/src/tzero_fixer/cwe_remediator.py +171 -0
- package/src/tzero_fixer/dead_code_eliminator.py +150 -0
- package/src/tzero_fixer/dependency_vulnerability_auditor.py +309 -0
- package/src/tzero_fixer/git_advanced_surgeon.py +280 -0
- package/src/tzero_fixer/hardware_and_disk_doctor.py +325 -0
- package/src/tzero_fixer/import_graph_healer.py +412 -0
- package/src/tzero_fixer/interactive_tui.py +91 -0
- package/src/tzero_fixer/knowledge_base.py +295 -0
- package/src/tzero_fixer/knowledge_base_extended.py +940 -0
- package/src/tzero_fixer/linters_and_static_analyzer.py +307 -0
- package/src/tzero_fixer/mcp_server.py +447 -0
- package/src/tzero_fixer/models.py +385 -0
- package/src/tzero_fixer/orchestrator.py +375 -0
- package/src/tzero_fixer/os_doctor.py +502 -0
- package/src/tzero_fixer/packaging_abi_surgeon.py +323 -0
- package/src/tzero_fixer/pc_fixer_doctor.py +599 -0
- package/src/tzero_fixer/performance_and_memory_profiler.py +274 -0
- package/src/tzero_fixer/polyglot_ast_parsers.py +313 -0
- package/src/tzero_fixer/polyglot_build_healer.py +364 -0
- package/src/tzero_fixer/polyglot_config_healer.py +318 -0
- package/src/tzero_fixer/polyglot_doctor.py +372 -0
- package/src/tzero_fixer/polyglot_linters.py +619 -0
- package/src/tzero_fixer/polyglot_schema_validator.py +305 -0
- package/src/tzero_fixer/security_cwe_database.py +444 -0
- package/src/tzero_fixer/security_deep_rules.py +114 -0
- package/src/tzero_fixer/security_rules_catalog.py +110 -0
- package/src/tzero_fixer/security_zero_leak.py +326 -0
- package/src/tzero_fixer/smart_patch_recommender.py +223 -0
- package/src/tzero_fixer/synthetic_workspace_generator.py +220 -0
- package/src/tzero_fixer/system_diagnostics_extended.py +493 -0
- package/src/tzero_fixer/system_repair_recipes.py +447 -0
- package/src/tzero_fixer/venv_doctor.py +309 -0
- package/src/tzero_gateway.py +885 -0
- package/src/tzero_guard.py +243 -0
- package/src/tzero_infra.py +274 -0
- package/src/tzero_jarvis/__init__.py +124 -0
- package/src/tzero_jarvis/causal_graph.py +310 -0
- package/src/tzero_jarvis/graph_visualizer.py +395 -0
- package/src/tzero_jarvis/hybrid_search.py +214 -0
- package/src/tzero_jarvis/latent_press.py +137 -0
- package/src/tzero_jarvis/memory_consolidation.py +212 -0
- package/src/tzero_jarvis/minhash_lsh.py +184 -0
- package/src/tzero_jarvis/orchestrator.py +335 -0
- package/src/tzero_jarvis/self_healing.py +308 -0
- package/src/tzero_jarvis/semantic_compressor.py +887 -0
- package/src/tzero_jarvis/swarm.py +261 -0
- package/src/tzero_jarvis/swarm_bft.py +144 -0
- package/src/tzero_jarvis/workspace_orchestrator.py +1167 -0
- package/src/tzero_jarvis/zero_key_router.py +173 -0
- package/src/tzero_mcp.py +4858 -0
- package/src/tzero_neural_cortex.py +1425 -0
- package/src/tzero_packer.py +370 -0
- package/src/tzero_patch.py +624 -0
- package/src/tzero_pc_fixer.py +12657 -0
- package/src/tzero_pipe.py +95 -0
- package/src/tzero_polyglot.py +422 -0
- package/src/tzero_reachability.py +272 -0
- package/src/tzero_replay.py +141 -0
- package/src/tzero_security.py +570 -0
- package/src/tzero_snapshot.py +258 -0
- package/src/tzero_stubs.py +313 -0
- package/src/tzero_superdesign.py +774 -0
- package/src/tzero_sync.py +241 -0
- package/src/tzero_task_benchmark.py +363 -0
- package/src/tzero_tui.py +244 -0
- package/src/tzero_v3.py +9435 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""T-Zero AST Syntax Healer & Polyglot Source Code Doctor.
|
|
3
|
+
|
|
4
|
+
Performs intelligent, deterministic, and surgical healing for:
|
|
5
|
+
- Missing colons on compound statements (def, class, if, elif, else, try, except, finally, for, while, with, match, case).
|
|
6
|
+
- Legacy Python 2 idioms (print statements, except Exception, e, xrange, has_key, iteritems, unicode).
|
|
7
|
+
- Indentation errors, mixed tabs and spaces, and trailing indentation drift.
|
|
8
|
+
- Invisible characters (UTF-8 BOM, zero-width spaces, non-breaking spaces).
|
|
9
|
+
- Invalid regex escape sequences (automatic raw string prefixing).
|
|
10
|
+
- Unclosed delimiters and unclosed string literals.
|
|
11
|
+
|
|
12
|
+
Author: Toprak Ahmet Aydogmus (Siber Akademi)
|
|
13
|
+
Version: 10.4.0
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import ast
|
|
17
|
+
import difflib
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
25
|
+
|
|
26
|
+
from .models import (
|
|
27
|
+
DiagnosticCategory,
|
|
28
|
+
DiagnosticIssue,
|
|
29
|
+
HealAction,
|
|
30
|
+
SeverityLevel,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ASTSyntaxHealer:
|
|
35
|
+
"""Enterprise AST parser and surgical auto-repair engine for source code syntax defects."""
|
|
36
|
+
|
|
37
|
+
COMPOUND_STATEMENT_KEYWORDS = (
|
|
38
|
+
"def ", "async def ", "class ", "if ", "elif ", "else", "try", "except",
|
|
39
|
+
"finally", "for ", "async for ", "while ", "with ", "async with ", "match ", "case "
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
ZERO_WIDTH_CHARS = ("\u200b", "\ufeff", "\u200c", "\u200d", "\u200e", "\u200f")
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def scan_encoding_and_invisible_chars(cls, file_path: str, raw_bytes: bytes) -> List[DiagnosticIssue]:
|
|
46
|
+
"""Detect UTF-8 BOM and phantom zero-width spaces that disrupt lexers."""
|
|
47
|
+
issues: List[DiagnosticIssue] = []
|
|
48
|
+
|
|
49
|
+
# 1. Byte Order Mark (BOM)
|
|
50
|
+
if raw_bytes.startswith(b"\xef\xbb\xbf"):
|
|
51
|
+
issues.append(DiagnosticIssue(
|
|
52
|
+
category=DiagnosticCategory.ENCODING.value,
|
|
53
|
+
severity=SeverityLevel.WARNING.value,
|
|
54
|
+
file_path=file_path,
|
|
55
|
+
line=1,
|
|
56
|
+
column=0,
|
|
57
|
+
error_code="TZ-ENC-UTF8-BOM",
|
|
58
|
+
message="File begins with UTF-8 Byte Order Mark (BOM) which breaks some parsers and CLI tools",
|
|
59
|
+
suggested_fix="Strip UTF-8 BOM byte sequence from file header",
|
|
60
|
+
auto_fixable=True,
|
|
61
|
+
metadata={"action": "strip_bom", "path": file_path}
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
# 2. Zero-width spaces in text
|
|
65
|
+
try:
|
|
66
|
+
text = raw_bytes.decode("utf-8", errors="replace")
|
|
67
|
+
for char in cls.ZERO_WIDTH_CHARS:
|
|
68
|
+
if char in text:
|
|
69
|
+
count = text.count(char)
|
|
70
|
+
issues.append(DiagnosticIssue(
|
|
71
|
+
category=DiagnosticCategory.ENCODING.value,
|
|
72
|
+
severity=SeverityLevel.WARNING.value,
|
|
73
|
+
file_path=file_path,
|
|
74
|
+
line=1,
|
|
75
|
+
column=0,
|
|
76
|
+
error_code="TZ-ENC-ZERO-WIDTH",
|
|
77
|
+
message=f"File contains {count} invisible zero-width character(s) (U+{ord(char):04X})",
|
|
78
|
+
suggested_fix="Sanitize file by stripping zero-width whitespace",
|
|
79
|
+
auto_fixable=True,
|
|
80
|
+
metadata={"action": "strip_zero_width", "path": file_path}
|
|
81
|
+
))
|
|
82
|
+
break
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
return issues
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def detect_mixed_indentation(cls, file_path: str, content: str) -> List[DiagnosticIssue]:
|
|
90
|
+
"""Detect mixed tabs and spaces causing TabError or inconsistent blocks."""
|
|
91
|
+
issues: List[DiagnosticIssue] = []
|
|
92
|
+
lines = content.splitlines()
|
|
93
|
+
|
|
94
|
+
has_tabs = False
|
|
95
|
+
has_spaces = False
|
|
96
|
+
for idx, line in enumerate(lines, start=1):
|
|
97
|
+
stripped = line.lstrip()
|
|
98
|
+
if not stripped:
|
|
99
|
+
continue
|
|
100
|
+
indent = line[:len(line) - len(stripped)]
|
|
101
|
+
if "\t" in indent:
|
|
102
|
+
has_tabs = True
|
|
103
|
+
if " " in indent:
|
|
104
|
+
has_spaces = True
|
|
105
|
+
|
|
106
|
+
if "\t" in indent and " " in indent:
|
|
107
|
+
issues.append(DiagnosticIssue(
|
|
108
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
109
|
+
severity=SeverityLevel.ERROR.value,
|
|
110
|
+
file_path=file_path,
|
|
111
|
+
line=idx,
|
|
112
|
+
column=0,
|
|
113
|
+
error_code="TZ-SYN-TABERROR-MIXED",
|
|
114
|
+
message="Mixed tabs and spaces in indentation on the same line",
|
|
115
|
+
suggested_fix="Convert all leading tabs to 4 spaces",
|
|
116
|
+
auto_fixable=True,
|
|
117
|
+
metadata={"action": "normalize_tabs", "path": file_path}
|
|
118
|
+
))
|
|
119
|
+
return issues
|
|
120
|
+
|
|
121
|
+
if has_tabs and has_spaces:
|
|
122
|
+
issues.append(DiagnosticIssue(
|
|
123
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
124
|
+
severity=SeverityLevel.WARNING.value,
|
|
125
|
+
file_path=file_path,
|
|
126
|
+
line=1,
|
|
127
|
+
column=0,
|
|
128
|
+
error_code="TZ-SYN-MIXED-INDENT",
|
|
129
|
+
message="File uses inconsistent indentation (both tabs and spaces detected)",
|
|
130
|
+
suggested_fix="Standardize entire file to 4 spaces indentation (PEP 8)",
|
|
131
|
+
auto_fixable=True,
|
|
132
|
+
metadata={"action": "normalize_tabs", "path": file_path}
|
|
133
|
+
))
|
|
134
|
+
|
|
135
|
+
return issues
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def analyze_syntax_error(cls, file_path: str, content: str, err: SyntaxError) -> DiagnosticIssue:
|
|
139
|
+
"""Analyze Python SyntaxError traceback to determine surgical repair strategy."""
|
|
140
|
+
err_line = err.lineno or 1
|
|
141
|
+
err_col = err.offset or 0
|
|
142
|
+
err_msg = err.msg or "Syntax error"
|
|
143
|
+
lines = content.splitlines()
|
|
144
|
+
|
|
145
|
+
action = "generic_syntax"
|
|
146
|
+
suggested = "Fix syntax error"
|
|
147
|
+
|
|
148
|
+
if 0 < err_line <= len(lines):
|
|
149
|
+
line_str = lines[err_line - 1].strip()
|
|
150
|
+
|
|
151
|
+
# 1. Missing Colon
|
|
152
|
+
for kw in cls.COMPOUND_STATEMENT_KEYWORDS:
|
|
153
|
+
if line_str.startswith(kw):
|
|
154
|
+
# Check if line does not end with colon (ignoring comments)
|
|
155
|
+
code_part = line_str.split("#", 1)[0].rstrip()
|
|
156
|
+
if not code_part.endswith(":"):
|
|
157
|
+
action = "append_colon"
|
|
158
|
+
suggested = f"Append missing ':' to '{kw.strip()}' compound statement"
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
# 2. Python 2 print statement
|
|
162
|
+
if action == "generic_syntax":
|
|
163
|
+
if re.match(r"^print\s+.*", line_str) and not line_str.startswith("print(") and not line_str.startswith("print ("):
|
|
164
|
+
action = "convert_print2"
|
|
165
|
+
suggested = "Convert legacy Python 2 print statement to print(...) function call"
|
|
166
|
+
|
|
167
|
+
# 3. Python 2 except statement
|
|
168
|
+
if action == "generic_syntax":
|
|
169
|
+
if re.match(r"^except\s+[A-Za-z0-9_\.]+\s*,\s*[A-Za-z0-9_]+\s*:", line_str):
|
|
170
|
+
action = "convert_except2"
|
|
171
|
+
suggested = "Convert legacy 'except Error, e:' to 'except Error as e:'"
|
|
172
|
+
|
|
173
|
+
return DiagnosticIssue(
|
|
174
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
175
|
+
severity=SeverityLevel.CRITICAL.value,
|
|
176
|
+
file_path=file_path,
|
|
177
|
+
line=err_line,
|
|
178
|
+
column=err_col,
|
|
179
|
+
error_code=f"TZ-SYN-{action.upper()}",
|
|
180
|
+
message=f"SyntaxError: {err_msg} on line {err_line}",
|
|
181
|
+
suggested_fix=suggested,
|
|
182
|
+
auto_fixable=action in ("append_colon", "convert_print2", "convert_except2", "normalize_tabs"),
|
|
183
|
+
metadata={"action": action, "lineno": err_line, "path": file_path}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def scan_python_idioms_and_regex(cls, file_path: str, content: str) -> List[DiagnosticIssue]:
|
|
188
|
+
"""Detect legacy Python 2 idioms and invalid regex escape sequences."""
|
|
189
|
+
issues: List[DiagnosticIssue] = []
|
|
190
|
+
lines = content.splitlines()
|
|
191
|
+
|
|
192
|
+
for idx, line in enumerate(lines, start=1):
|
|
193
|
+
stripped = line.strip()
|
|
194
|
+
|
|
195
|
+
# Python 2 xrange
|
|
196
|
+
if "xrange(" in stripped and not stripped.startswith("#"):
|
|
197
|
+
issues.append(DiagnosticIssue(
|
|
198
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
199
|
+
severity=SeverityLevel.WARNING.value,
|
|
200
|
+
file_path=file_path,
|
|
201
|
+
line=idx,
|
|
202
|
+
column=stripped.find("xrange("),
|
|
203
|
+
error_code="TZ-IDIOM-XRANGE",
|
|
204
|
+
message="Use of deprecated Python 2 'xrange()' function",
|
|
205
|
+
suggested_fix="Replace 'xrange()' with standard 'range()'",
|
|
206
|
+
auto_fixable=True,
|
|
207
|
+
metadata={"action": "replace_xrange", "lineno": idx, "path": file_path}
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
# Python 2 has_key
|
|
211
|
+
if ".has_key(" in stripped and not stripped.startswith("#"):
|
|
212
|
+
issues.append(DiagnosticIssue(
|
|
213
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
214
|
+
severity=SeverityLevel.WARNING.value,
|
|
215
|
+
file_path=file_path,
|
|
216
|
+
line=idx,
|
|
217
|
+
column=stripped.find(".has_key("),
|
|
218
|
+
error_code="TZ-IDIOM-HASKEY",
|
|
219
|
+
message="Use of removed dictionary method 'dict.has_key(k)'",
|
|
220
|
+
suggested_fix="Replace 'dict.has_key(k)' with 'k in dict'",
|
|
221
|
+
auto_fixable=True,
|
|
222
|
+
metadata={"action": "replace_has_key", "lineno": idx, "path": file_path}
|
|
223
|
+
))
|
|
224
|
+
|
|
225
|
+
# Invalid regex escape sequence in standard string literal
|
|
226
|
+
if re.search(r'(?<!r)("|\')[^"\']*\\([dswbDSWB])[^"\']*("|\')', line):
|
|
227
|
+
if not stripped.startswith("#"):
|
|
228
|
+
issues.append(DiagnosticIssue(
|
|
229
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
230
|
+
severity=SeverityLevel.INFO.value,
|
|
231
|
+
file_path=file_path,
|
|
232
|
+
line=idx,
|
|
233
|
+
column=0,
|
|
234
|
+
error_code="TZ-SYN-ESCAPE-REGEX",
|
|
235
|
+
message="Invalid escape sequence in string literal (should be raw string r'...')",
|
|
236
|
+
suggested_fix="Prefix regular expression string with 'r' to prevent SyntaxWarning",
|
|
237
|
+
auto_fixable=True,
|
|
238
|
+
metadata={"action": "prefix_raw_string", "lineno": idx, "path": file_path}
|
|
239
|
+
))
|
|
240
|
+
|
|
241
|
+
return issues
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def heal_string(cls, content: str, filename: str = "snippet.py") -> Any:
|
|
245
|
+
"""In-memory string healing helper for syntax errors, colons, BOM, and legacy idioms."""
|
|
246
|
+
@dataclass
|
|
247
|
+
class StringHealResult:
|
|
248
|
+
original_content: str
|
|
249
|
+
repaired_content: str
|
|
250
|
+
changed: bool
|
|
251
|
+
issues: List[DiagnosticIssue]
|
|
252
|
+
|
|
253
|
+
import tempfile
|
|
254
|
+
suffix = Path(filename).suffix or ".py"
|
|
255
|
+
with tempfile.NamedTemporaryFile("w", suffix=suffix, encoding="utf-8", delete=False) as tf:
|
|
256
|
+
tf.write(content)
|
|
257
|
+
temp_path = tf.name
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
issues = cls.diagnose_file(temp_path)
|
|
261
|
+
cls.execute_syntax_heals(issues, dry_run=False)
|
|
262
|
+
with open(temp_path, "r", encoding="utf-8", errors="replace") as tf:
|
|
263
|
+
repaired = tf.read()
|
|
264
|
+
return StringHealResult(
|
|
265
|
+
original_content=content,
|
|
266
|
+
repaired_content=repaired,
|
|
267
|
+
changed=(repaired != content),
|
|
268
|
+
issues=issues,
|
|
269
|
+
)
|
|
270
|
+
finally:
|
|
271
|
+
if os.path.exists(temp_path):
|
|
272
|
+
try:
|
|
273
|
+
os.remove(temp_path)
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def diagnose_file(cls, file_path: str) -> List[DiagnosticIssue]:
|
|
279
|
+
"""Scan a single source file for all syntax, encoding, and idiom defects."""
|
|
280
|
+
issues: List[DiagnosticIssue] = []
|
|
281
|
+
path = Path(file_path)
|
|
282
|
+
if not path.is_file() or path.suffix != ".py":
|
|
283
|
+
return issues
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
with open(path, "rb") as f:
|
|
287
|
+
raw_bytes = f.read()
|
|
288
|
+
except Exception as e:
|
|
289
|
+
issues.append(DiagnosticIssue(
|
|
290
|
+
category=DiagnosticCategory.ENVIRONMENT.value,
|
|
291
|
+
severity=SeverityLevel.ERROR.value,
|
|
292
|
+
file_path=file_path,
|
|
293
|
+
line=None,
|
|
294
|
+
column=None,
|
|
295
|
+
error_code="TZ-FS-UNREADABLE",
|
|
296
|
+
message=f"Unable to read file: {e}",
|
|
297
|
+
suggested_fix="Check filesystem read permissions",
|
|
298
|
+
auto_fixable=False
|
|
299
|
+
))
|
|
300
|
+
return issues
|
|
301
|
+
|
|
302
|
+
# 1. Encoding & invisible chars
|
|
303
|
+
issues.extend(cls.scan_encoding_and_invisible_chars(file_path, raw_bytes))
|
|
304
|
+
|
|
305
|
+
# Decode text
|
|
306
|
+
content = raw_bytes.decode("utf-8", errors="replace")
|
|
307
|
+
|
|
308
|
+
# 2. Mixed indentation
|
|
309
|
+
issues.extend(cls.detect_mixed_indentation(file_path, content))
|
|
310
|
+
|
|
311
|
+
# 3. AST Parse verification
|
|
312
|
+
try:
|
|
313
|
+
ast.parse(content, filename=file_path)
|
|
314
|
+
except SyntaxError as se:
|
|
315
|
+
issues.append(cls.analyze_syntax_error(file_path, content, se))
|
|
316
|
+
return issues # Unparseable AST stops further semantic scans
|
|
317
|
+
|
|
318
|
+
# 4. Idioms & regex escapes
|
|
319
|
+
issues.extend(cls.scan_python_idioms_and_regex(file_path, content))
|
|
320
|
+
|
|
321
|
+
return issues
|
|
322
|
+
|
|
323
|
+
@classmethod
|
|
324
|
+
def execute_syntax_heals(cls, issues: List[DiagnosticIssue], dry_run: bool = False) -> List[HealAction]:
|
|
325
|
+
"""Execute surgical regex and line-level patches to heal syntax flaws."""
|
|
326
|
+
actions: List[HealAction] = []
|
|
327
|
+
files_modified: Set[str] = set()
|
|
328
|
+
|
|
329
|
+
for issue in issues:
|
|
330
|
+
action_type = issue.metadata.get("action")
|
|
331
|
+
file_p = issue.metadata.get("path") or issue.file_path
|
|
332
|
+
if not issue.auto_fixable or not action_type or not file_p:
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
t0 = time.time()
|
|
336
|
+
path = Path(file_p)
|
|
337
|
+
if not path.is_file():
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
# 1. Strip BOM
|
|
342
|
+
if action_type == "strip_bom":
|
|
343
|
+
with open(path, "rb") as f:
|
|
344
|
+
data = f.read()
|
|
345
|
+
if data.startswith(b"\xef\xbb\xbf"):
|
|
346
|
+
if not dry_run:
|
|
347
|
+
with open(path, "wb") as f:
|
|
348
|
+
f.write(data[3:])
|
|
349
|
+
actions.append(HealAction(
|
|
350
|
+
category=DiagnosticCategory.ENCODING.value,
|
|
351
|
+
action_type=action_type,
|
|
352
|
+
file_path=file_p,
|
|
353
|
+
description=f"Stripped UTF-8 Byte Order Mark (BOM) from {path.name}",
|
|
354
|
+
success=True,
|
|
355
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
356
|
+
))
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
# Read lines for text transforms
|
|
360
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
361
|
+
orig_text = f.read()
|
|
362
|
+
lines = orig_text.splitlines(keepends=True)
|
|
363
|
+
|
|
364
|
+
# 2. Strip zero-width chars
|
|
365
|
+
if action_type == "strip_zero_width":
|
|
366
|
+
mod_text = orig_text
|
|
367
|
+
for zw in cls.ZERO_WIDTH_CHARS:
|
|
368
|
+
mod_text = mod_text.replace(zw, "")
|
|
369
|
+
if mod_text != orig_text:
|
|
370
|
+
if not dry_run:
|
|
371
|
+
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|
372
|
+
f.write(mod_text)
|
|
373
|
+
actions.append(HealAction(
|
|
374
|
+
category=DiagnosticCategory.ENCODING.value,
|
|
375
|
+
action_type=action_type,
|
|
376
|
+
file_path=file_p,
|
|
377
|
+
description=f"Stripped invisible zero-width characters from {path.name}",
|
|
378
|
+
success=True,
|
|
379
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
380
|
+
))
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# 3. Normalize tabs to 4 spaces
|
|
384
|
+
if action_type == "normalize_tabs":
|
|
385
|
+
new_lines = []
|
|
386
|
+
for l in lines:
|
|
387
|
+
leading_tabs = len(l) - len(l.lstrip("\t"))
|
|
388
|
+
if leading_tabs > 0:
|
|
389
|
+
new_lines.append((" " * leading_tabs) + l[leading_tabs:])
|
|
390
|
+
else:
|
|
391
|
+
new_lines.append(l)
|
|
392
|
+
mod_text = "".join(new_lines)
|
|
393
|
+
if mod_text != orig_text:
|
|
394
|
+
if not dry_run:
|
|
395
|
+
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|
396
|
+
f.write(mod_text)
|
|
397
|
+
actions.append(HealAction(
|
|
398
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
399
|
+
action_type=action_type,
|
|
400
|
+
file_path=file_p,
|
|
401
|
+
description=f"Normalized tabs to 4 spaces in {path.name}",
|
|
402
|
+
success=True,
|
|
403
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
404
|
+
))
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
# Line-specific patches
|
|
408
|
+
lineno = issue.metadata.get("lineno")
|
|
409
|
+
if lineno and 0 < lineno <= len(lines):
|
|
410
|
+
orig_line = lines[lineno - 1]
|
|
411
|
+
mod_line = orig_line
|
|
412
|
+
|
|
413
|
+
# 4. Append missing colon
|
|
414
|
+
if action_type == "append_colon":
|
|
415
|
+
if "#" in orig_line:
|
|
416
|
+
code_p, comment_p = orig_line.split("#", 1)
|
|
417
|
+
mod_line = code_p.rstrip() + ": #" + comment_p
|
|
418
|
+
else:
|
|
419
|
+
mod_line = orig_line.rstrip("\r\n") + ":\n"
|
|
420
|
+
|
|
421
|
+
# 5. Convert Python 2 print
|
|
422
|
+
elif action_type == "convert_print2":
|
|
423
|
+
mod_line = re.sub(r"^(\s*)print\s+(.*)$", r"\1print(\2)", orig_line)
|
|
424
|
+
|
|
425
|
+
# 6. Convert Python 2 except
|
|
426
|
+
elif action_type == "convert_except2":
|
|
427
|
+
mod_line = re.sub(r"^(\s*except\s+[A-Za-z0-9_\.]+)\s*,\s*([A-Za-z0-9_]+)\s*:", r"\1 as \2:", orig_line)
|
|
428
|
+
|
|
429
|
+
# 7. Replace xrange
|
|
430
|
+
elif action_type == "replace_xrange":
|
|
431
|
+
mod_line = orig_line.replace("xrange(", "range(")
|
|
432
|
+
|
|
433
|
+
# 8. Prefix raw string regex
|
|
434
|
+
elif action_type == "prefix_raw_string":
|
|
435
|
+
mod_line = re.sub(r'(?<!r)(["\'])(\\[dswbDSWB].*?)(["\'])', r'r\1\2\3', orig_line)
|
|
436
|
+
|
|
437
|
+
if mod_line != orig_line:
|
|
438
|
+
diff = "".join(difflib.unified_diff(
|
|
439
|
+
[orig_line], [mod_line], fromfile="before", tofile="after"
|
|
440
|
+
))
|
|
441
|
+
lines[lineno - 1] = mod_line
|
|
442
|
+
if not dry_run:
|
|
443
|
+
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|
444
|
+
f.writelines(lines)
|
|
445
|
+
actions.append(HealAction(
|
|
446
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
447
|
+
action_type=action_type,
|
|
448
|
+
file_path=file_p,
|
|
449
|
+
description=f"Patched {action_type} on line {lineno} of {path.name}",
|
|
450
|
+
success=True,
|
|
451
|
+
diff=diff,
|
|
452
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
453
|
+
))
|
|
454
|
+
except Exception as e:
|
|
455
|
+
actions.append(HealAction(
|
|
456
|
+
category=DiagnosticCategory.SYNTAX.value,
|
|
457
|
+
action_type=action_type or "syntax_error",
|
|
458
|
+
file_path=file_p,
|
|
459
|
+
description=f"Failed healing syntax on {path.name}: {e}",
|
|
460
|
+
success=False,
|
|
461
|
+
duration_ms=round((time.time() - t0) * 1000, 2)
|
|
462
|
+
))
|
|
463
|
+
|
|
464
|
+
return actions
|