localcode 0.3.8__py3-none-any.whl → 0.3.9__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.
- localcode/__init__.py +1 -1
- localcode/tools/syntax_check.py +78 -115
- {localcode-0.3.8.dist-info → localcode-0.3.9.dist-info}/METADATA +3 -1
- {localcode-0.3.8.dist-info → localcode-0.3.9.dist-info}/RECORD +8 -8
- {localcode-0.3.8.dist-info → localcode-0.3.9.dist-info}/WHEEL +0 -0
- {localcode-0.3.8.dist-info → localcode-0.3.9.dist-info}/entry_points.txt +0 -0
- {localcode-0.3.8.dist-info → localcode-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {localcode-0.3.8.dist-info → localcode-0.3.9.dist-info}/top_level.txt +0 -0
localcode/__init__.py
CHANGED
localcode/tools/syntax_check.py
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
"""One-shot syntax validation for files the agent writes.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
with LOCALCODE_SYNTAX_CHECK=0.
|
|
1
|
+
"""One-shot syntax validation for files the agent writes — in-process, offline.
|
|
2
|
+
|
|
3
|
+
Tier-1 verification. The big agents use a persistent language server (LSP);
|
|
4
|
+
opencode's own docs say that's often worse than running diagnostics directly
|
|
5
|
+
(LSPs "get out of sync, use significant memory, slow down agent workflows") —
|
|
6
|
+
decisive for a 16 GB Mac already running a large local model.
|
|
7
|
+
|
|
8
|
+
Crucially this must work with NO tools the user has to install and NO network:
|
|
9
|
+
it's bundled. We parse with tree-sitter (grammars compiled into the wheel — one
|
|
10
|
+
~4.5 MB native extension covering 100+ languages) and flag any syntax error
|
|
11
|
+
in-process. So when a small quantized model drifts into a structural typo
|
|
12
|
+
(`useState(0]`, an unterminated string, a missing bracket), the write tool
|
|
13
|
+
appends the exact error and line, and the model fixes THAT line instead of
|
|
14
|
+
re-reading the broken file and reproducing the error (the self-conditioning
|
|
15
|
+
loop). Python/JSON keep their native checkers for sharper messages.
|
|
16
|
+
|
|
17
|
+
Semantic/type errors (wrong names, missing imports) are caught separately by
|
|
18
|
+
project_check at completion (tsc/ruff/go/cargo). Disable with
|
|
19
|
+
LOCALCODE_SYNTAX_CHECK=0.
|
|
21
20
|
"""
|
|
22
21
|
from __future__ import annotations
|
|
23
22
|
|
|
@@ -27,56 +26,39 @@ import subprocess
|
|
|
27
26
|
|
|
28
27
|
_TIMEOUT = 5.0
|
|
29
28
|
|
|
29
|
+
# File extension → tree-sitter language name (all bundled in the language pack).
|
|
30
|
+
_TS_LANG = {
|
|
31
|
+
".js": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
32
|
+
".jsx": "tsx", ".ts": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
33
|
+
".tsx": "tsx", ".go": "go", ".rs": "rust", ".rb": "ruby", ".php": "php",
|
|
34
|
+
".sh": "bash", ".bash": "bash", ".zsh": "bash", ".lua": "lua",
|
|
35
|
+
".c": "c", ".h": "c", ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
|
|
36
|
+
".hpp": "cpp", ".hh": "cpp", ".java": "java", ".kt": "kotlin",
|
|
37
|
+
".swift": "swift", ".cs": "csharp", ".scala": "scala",
|
|
38
|
+
".css": "css", ".scss": "scss", ".html": "html", ".xml": "xml",
|
|
39
|
+
".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".sql": "sql",
|
|
40
|
+
}
|
|
30
41
|
|
|
31
|
-
def check_syntax(path: str, content: str) -> str | None:
|
|
32
|
-
"""Return a one-line syntax-error string, or None if OK / not checkable.
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"""
|
|
43
|
+
def check_syntax(path: str, content: str) -> str | None:
|
|
44
|
+
"""Return a one-line syntax-error string, or None if OK / not checkable."""
|
|
37
45
|
if os.environ.get("LOCALCODE_SYNTAX_CHECK") == "0":
|
|
38
46
|
return None
|
|
39
47
|
ext = os.path.splitext(path)[1].lower()
|
|
40
48
|
try:
|
|
49
|
+
# Native checkers give the sharpest messages; use them where free.
|
|
41
50
|
if ext == ".json":
|
|
42
51
|
return _check_json(content)
|
|
43
52
|
if ext in (".py", ".pyi"):
|
|
44
53
|
return _check_python(content, path)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return _check_esbuild(path)
|
|
49
|
-
# Per-file syntax linters for other languages (best-effort — only if the
|
|
50
|
-
# interpreter/tool is on PATH). Whole-project TYPE checks (go/rust/tsc)
|
|
51
|
-
# run separately at completion in project_check.
|
|
52
|
-
if ext == ".rb":
|
|
53
|
-
return _check_cli(["ruby", "-wc", path], "ruby")
|
|
54
|
-
if ext == ".php":
|
|
55
|
-
return _check_cli(["php", "-l", path], "php")
|
|
56
|
-
if ext in (".sh", ".bash"):
|
|
57
|
-
return _check_cli(["bash", "-n", path], "bash")
|
|
58
|
-
if ext == ".lua":
|
|
59
|
-
return _check_cli(["luac", "-p", path], "luac")
|
|
60
|
-
if ext == ".rs":
|
|
61
|
-
# rustc syntax-only parse (no full build): --emit=metadata is slow;
|
|
62
|
-
# a lightweight parse-only pass isn't stable, so skip unless a fast
|
|
63
|
-
# tool exists. cargo check at completion covers Rust type errors.
|
|
64
|
-
return None
|
|
54
|
+
lang = _TS_LANG.get(ext)
|
|
55
|
+
if lang:
|
|
56
|
+
return _check_treesitter(content, lang)
|
|
65
57
|
except Exception:
|
|
66
58
|
return None
|
|
67
59
|
return None
|
|
68
60
|
|
|
69
61
|
|
|
70
|
-
def _check_cli(argv: list, tool: str) -> str | None:
|
|
71
|
-
import shutil
|
|
72
|
-
if not shutil.which(tool):
|
|
73
|
-
return None
|
|
74
|
-
r = subprocess.run(argv, capture_output=True, text=True, timeout=_TIMEOUT)
|
|
75
|
-
if r.returncode != 0:
|
|
76
|
-
return _first_error_line((r.stderr or "") + "\n" + (r.stdout or ""))
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
|
|
80
62
|
def _check_json(content: str) -> str | None:
|
|
81
63
|
try:
|
|
82
64
|
json.loads(content)
|
|
@@ -94,66 +76,47 @@ def _check_python(content: str, path: str) -> str | None:
|
|
|
94
76
|
return f"Python syntax error at {loc}: {e.msg}"
|
|
95
77
|
|
|
96
78
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
if not shutil.which("node"):
|
|
100
|
-
return None
|
|
101
|
-
r = subprocess.run(
|
|
102
|
-
["node", "--check", path],
|
|
103
|
-
capture_output=True, text=True, timeout=_TIMEOUT,
|
|
104
|
-
)
|
|
105
|
-
if r.returncode != 0:
|
|
106
|
-
return _first_error_line(r.stderr)
|
|
107
|
-
return None
|
|
79
|
+
def _check_treesitter(content: str, lang: str) -> str | None:
|
|
80
|
+
"""Parse with a bundled tree-sitter grammar; report the first syntax error.
|
|
108
81
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"""Walk up from the file's dir to find node_modules/.bin/<name>."""
|
|
112
|
-
d = os.path.dirname(os.path.abspath(start))
|
|
113
|
-
for _ in range(25):
|
|
114
|
-
cand = os.path.join(d, "node_modules", ".bin", name)
|
|
115
|
-
if os.path.exists(cand):
|
|
116
|
-
return cand
|
|
117
|
-
parent = os.path.dirname(d)
|
|
118
|
-
if parent == d:
|
|
119
|
-
break
|
|
120
|
-
d = parent
|
|
121
|
-
return None
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _check_esbuild(path: str) -> str | None:
|
|
125
|
-
# esbuild parses TS/TSX/JSX and reports syntax errors instantly. Only used
|
|
126
|
-
# when the project has it (Vite/most JS projects do post-install); if not
|
|
127
|
-
# present we skip rather than risk a false alarm from a wrong parser.
|
|
128
|
-
esbuild = _find_bin(path, "esbuild")
|
|
129
|
-
if not esbuild:
|
|
130
|
-
return None
|
|
131
|
-
loader = "tsx" if path.endswith((".tsx", ".jsx")) else "ts"
|
|
132
|
-
r = subprocess.run(
|
|
133
|
-
[esbuild, path, f"--loader={loader}", "--log-level=error"],
|
|
134
|
-
capture_output=True, text=True, timeout=_TIMEOUT,
|
|
135
|
-
)
|
|
136
|
-
if r.returncode != 0 and r.stderr.strip():
|
|
137
|
-
return _first_error_line(r.stderr)
|
|
138
|
-
return None
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _first_error_line(stderr: str) -> str | None:
|
|
142
|
-
"""Pull the human-meaningful error out of a node/esbuild stderr dump.
|
|
143
|
-
|
|
144
|
-
Prefer the actual `SyntaxError: …` / `error: …` message, skipping node's
|
|
145
|
-
internal loader/stack frames (`node:internal/…`, ` at …`).
|
|
82
|
+
No external tools, no network — the grammars are compiled into the wheel.
|
|
83
|
+
Returns None if tree-sitter isn't available (graceful) or the file parses.
|
|
146
84
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
85
|
+
try:
|
|
86
|
+
from tree_sitter_language_pack import get_language
|
|
87
|
+
from tree_sitter import Parser
|
|
88
|
+
except Exception:
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
parser = Parser(get_language(lang))
|
|
92
|
+
tree = parser.parse(content.encode("utf-8"))
|
|
93
|
+
except Exception:
|
|
94
|
+
return None
|
|
95
|
+
root = tree.root_node
|
|
96
|
+
if not root.has_error:
|
|
97
|
+
return None
|
|
98
|
+
node = _first_error_node(root)
|
|
99
|
+
if node is None:
|
|
100
|
+
return f"syntax error detected in this {lang} file (unbalanced brackets/quotes or a stray token)"
|
|
101
|
+
line = node.start_point[0] + 1
|
|
102
|
+
col = node.start_point[1] + 1
|
|
103
|
+
snippet = ""
|
|
104
|
+
try:
|
|
105
|
+
src_line = content.splitlines()[node.start_point[0]].strip()
|
|
106
|
+
snippet = f" — near: {src_line[:80]}"
|
|
107
|
+
except Exception:
|
|
108
|
+
snippet = ""
|
|
109
|
+
kind = "missing token" if node.is_missing else "unexpected token"
|
|
110
|
+
return f"syntax error (line {line} col {col}): {kind}{snippet}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _first_error_node(root):
|
|
114
|
+
"""DFS for the first ERROR or MISSING node (the actual syntax fault)."""
|
|
115
|
+
stack = [root]
|
|
116
|
+
while stack:
|
|
117
|
+
n = stack.pop()
|
|
118
|
+
if n.is_error or n.is_missing:
|
|
119
|
+
return n
|
|
120
|
+
# push children in order so we return the earliest error
|
|
121
|
+
stack.extend(reversed(n.children))
|
|
159
122
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localcode
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: High-performance AI coding on consumer hardware.
|
|
5
5
|
Author: LocalCode contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -35,6 +35,8 @@ Requires-Dist: prompt_toolkit>=3.0.52
|
|
|
35
35
|
Requires-Dist: rich>=13.7.1
|
|
36
36
|
Requires-Dist: scikit-learn>=1.3.0
|
|
37
37
|
Requires-Dist: textual>=8.2.4
|
|
38
|
+
Requires-Dist: tree-sitter>=0.23.0
|
|
39
|
+
Requires-Dist: tree-sitter-language-pack>=1.0.0
|
|
38
40
|
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
|
|
39
41
|
Provides-Extra: voice
|
|
40
42
|
Requires-Dist: pywhispercpp>=1.2.0; extra == "voice"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
localcode/__init__.py,sha256=
|
|
1
|
+
localcode/__init__.py,sha256=dv_OdIY5KS_HiRjBMSmtKnar5Axf3OVYIDjhWUodlAY,49
|
|
2
2
|
localcode/__main__.py,sha256=_4Tblte9F1KOpB-oGBQuTlaSN-IncLmhihqhBkpH3pU,69
|
|
3
3
|
localcode/_subproc_env.py,sha256=eltq_kKvqGj22pIFivB5BZft9-s5fziBW0lsV9FhEV4,2383
|
|
4
4
|
localcode/app.py,sha256=FSwpce6Jfn7zJmhL1ipaqTq0xKh0MxCMnEnMQuFV8L8,67820
|
|
@@ -112,7 +112,7 @@ localcode/tools/plan_mode.py,sha256=2RU6QH1IJoOTKuoAr7Ga7uTaebAS8v7OFt5v1zLEXFw,
|
|
|
112
112
|
localcode/tools/project_check.py,sha256=B3Q6IB2XfllW3Nlxwz2pCCQwF_b0VSUBnrpCHS4eOE8,5508
|
|
113
113
|
localcode/tools/read_file.py,sha256=gH852nnPiVD2Go9VcdS_EFbtnqYaGOPyAk5jo1cNbX8,4556
|
|
114
114
|
localcode/tools/skill_tool.py,sha256=vTVPyRWxICtDCVYWMiAMInGnc-wGAk2L1EZn-6e79nE,1478
|
|
115
|
-
localcode/tools/syntax_check.py,sha256=
|
|
115
|
+
localcode/tools/syntax_check.py,sha256=tpymYFeXs7LYU5qmfHGJeVX0WUdFYBineI9GN3jONYU,4726
|
|
116
116
|
localcode/tools/todo_write.py,sha256=8A4k9MzoqOtzJ6y2Gd3lEGkINcpV3JVCd2lYBmdd6qI,5801
|
|
117
117
|
localcode/tools/web_fetch.py,sha256=UPhMrLipwN5xJx5-zr634gL4p6nIRqYI2_7tbUw_8Go,2770
|
|
118
118
|
localcode/tools/web_search.py,sha256=ILfBBx50p00OjA939HzdDmdao4QLL8sRuxlBoUDjmC8,1122
|
|
@@ -133,9 +133,9 @@ localcode/tui/widgets/chat_log.py,sha256=D5DUkrQXL_ilxyC8N1xIPLw4Pqeh0jSWt1mPgho
|
|
|
133
133
|
localcode/tui/widgets/voice_visualizer.py,sha256=zOhrIxaMcg-F2Y8LvHJTddq227phmiTjtEyRbkx1XxU,4709
|
|
134
134
|
localcode/tui/widgets/messages/__init__.py,sha256=952AZ1qVMGUIqPIry7mwxnYbMkgPlcY5FAwZtEa5YPg,967
|
|
135
135
|
localcode/tui/widgets/messages/diff.py,sha256=khDf_MThrQz0S62-t-m7i3OU5cdNolUk1TCeKMRPDQI,10910
|
|
136
|
-
localcode-0.3.
|
|
137
|
-
localcode-0.3.
|
|
138
|
-
localcode-0.3.
|
|
139
|
-
localcode-0.3.
|
|
140
|
-
localcode-0.3.
|
|
141
|
-
localcode-0.3.
|
|
136
|
+
localcode-0.3.9.dist-info/licenses/LICENSE,sha256=Ahg0cteZ-6bIRB7KC15WEtdqWnzy8HY_yzsZBoVRql0,11294
|
|
137
|
+
localcode-0.3.9.dist-info/METADATA,sha256=iT5SYY0r4-zju8UthKU8MTPF4w_3HRk0H11CIB_8dQ0,7896
|
|
138
|
+
localcode-0.3.9.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
139
|
+
localcode-0.3.9.dist-info/entry_points.txt,sha256=rE7elTGUvMsBd-Quy22cuTVJ-zGVOmx0nEuC6ni9Tg4,87
|
|
140
|
+
localcode-0.3.9.dist-info/top_level.txt,sha256=58eL3Rw8v0OGmYNxjx8uS4ERWatrzUbyCL5inlukkTo,10
|
|
141
|
+
localcode-0.3.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|