llmcode-cli 1.0.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.
Files changed (212) hide show
  1. llm_code/__init__.py +2 -0
  2. llm_code/analysis/__init__.py +6 -0
  3. llm_code/analysis/cache.py +33 -0
  4. llm_code/analysis/engine.py +256 -0
  5. llm_code/analysis/go_rules.py +114 -0
  6. llm_code/analysis/js_rules.py +84 -0
  7. llm_code/analysis/python_rules.py +311 -0
  8. llm_code/analysis/rules.py +140 -0
  9. llm_code/analysis/rust_rules.py +108 -0
  10. llm_code/analysis/universal_rules.py +111 -0
  11. llm_code/api/__init__.py +0 -0
  12. llm_code/api/client.py +90 -0
  13. llm_code/api/errors.py +73 -0
  14. llm_code/api/openai_compat.py +390 -0
  15. llm_code/api/provider.py +35 -0
  16. llm_code/api/sse.py +52 -0
  17. llm_code/api/types.py +140 -0
  18. llm_code/cli/__init__.py +0 -0
  19. llm_code/cli/commands.py +70 -0
  20. llm_code/cli/image.py +122 -0
  21. llm_code/cli/render.py +214 -0
  22. llm_code/cli/status_line.py +79 -0
  23. llm_code/cli/streaming.py +92 -0
  24. llm_code/cli/tui_main.py +220 -0
  25. llm_code/computer_use/__init__.py +11 -0
  26. llm_code/computer_use/app_detect.py +49 -0
  27. llm_code/computer_use/app_tier.py +57 -0
  28. llm_code/computer_use/coordinator.py +99 -0
  29. llm_code/computer_use/input_control.py +71 -0
  30. llm_code/computer_use/screenshot.py +93 -0
  31. llm_code/cron/__init__.py +13 -0
  32. llm_code/cron/parser.py +145 -0
  33. llm_code/cron/scheduler.py +135 -0
  34. llm_code/cron/storage.py +126 -0
  35. llm_code/enterprise/__init__.py +1 -0
  36. llm_code/enterprise/audit.py +59 -0
  37. llm_code/enterprise/auth.py +26 -0
  38. llm_code/enterprise/oidc.py +95 -0
  39. llm_code/enterprise/rbac.py +65 -0
  40. llm_code/harness/__init__.py +5 -0
  41. llm_code/harness/config.py +33 -0
  42. llm_code/harness/engine.py +129 -0
  43. llm_code/harness/guides.py +41 -0
  44. llm_code/harness/sensors.py +68 -0
  45. llm_code/harness/templates.py +84 -0
  46. llm_code/hida/__init__.py +1 -0
  47. llm_code/hida/classifier.py +187 -0
  48. llm_code/hida/engine.py +49 -0
  49. llm_code/hida/profiles.py +95 -0
  50. llm_code/hida/types.py +28 -0
  51. llm_code/ide/__init__.py +1 -0
  52. llm_code/ide/bridge.py +80 -0
  53. llm_code/ide/detector.py +76 -0
  54. llm_code/ide/server.py +169 -0
  55. llm_code/logging.py +29 -0
  56. llm_code/lsp/__init__.py +0 -0
  57. llm_code/lsp/client.py +298 -0
  58. llm_code/lsp/detector.py +42 -0
  59. llm_code/lsp/manager.py +56 -0
  60. llm_code/lsp/tools.py +288 -0
  61. llm_code/marketplace/__init__.py +0 -0
  62. llm_code/marketplace/builtin_registry.py +102 -0
  63. llm_code/marketplace/installer.py +162 -0
  64. llm_code/marketplace/plugin.py +78 -0
  65. llm_code/marketplace/registry.py +360 -0
  66. llm_code/mcp/__init__.py +0 -0
  67. llm_code/mcp/bridge.py +87 -0
  68. llm_code/mcp/client.py +117 -0
  69. llm_code/mcp/health.py +120 -0
  70. llm_code/mcp/manager.py +214 -0
  71. llm_code/mcp/oauth.py +219 -0
  72. llm_code/mcp/transport.py +254 -0
  73. llm_code/mcp/types.py +53 -0
  74. llm_code/remote/__init__.py +0 -0
  75. llm_code/remote/client.py +136 -0
  76. llm_code/remote/protocol.py +22 -0
  77. llm_code/remote/server.py +275 -0
  78. llm_code/remote/ssh_proxy.py +56 -0
  79. llm_code/runtime/__init__.py +0 -0
  80. llm_code/runtime/auto_commit.py +56 -0
  81. llm_code/runtime/auto_diagnose.py +62 -0
  82. llm_code/runtime/checkpoint.py +70 -0
  83. llm_code/runtime/checkpoint_recovery.py +142 -0
  84. llm_code/runtime/compaction.py +35 -0
  85. llm_code/runtime/compressor.py +415 -0
  86. llm_code/runtime/config.py +533 -0
  87. llm_code/runtime/context.py +49 -0
  88. llm_code/runtime/conversation.py +921 -0
  89. llm_code/runtime/cost_tracker.py +126 -0
  90. llm_code/runtime/dream.py +127 -0
  91. llm_code/runtime/file_protection.py +150 -0
  92. llm_code/runtime/hardware.py +85 -0
  93. llm_code/runtime/hooks.py +223 -0
  94. llm_code/runtime/indexer.py +230 -0
  95. llm_code/runtime/knowledge_compiler.py +232 -0
  96. llm_code/runtime/memory.py +132 -0
  97. llm_code/runtime/memory_layers.py +467 -0
  98. llm_code/runtime/memory_lint.py +252 -0
  99. llm_code/runtime/model_aliases.py +37 -0
  100. llm_code/runtime/ollama.py +93 -0
  101. llm_code/runtime/overlay.py +124 -0
  102. llm_code/runtime/permissions.py +200 -0
  103. llm_code/runtime/plan.py +45 -0
  104. llm_code/runtime/prompt.py +238 -0
  105. llm_code/runtime/repo_map.py +174 -0
  106. llm_code/runtime/sandbox.py +116 -0
  107. llm_code/runtime/session.py +268 -0
  108. llm_code/runtime/skill_resolver.py +61 -0
  109. llm_code/runtime/skills.py +133 -0
  110. llm_code/runtime/speculative.py +75 -0
  111. llm_code/runtime/streaming_executor.py +216 -0
  112. llm_code/runtime/telemetry.py +196 -0
  113. llm_code/runtime/token_budget.py +26 -0
  114. llm_code/runtime/vcr.py +142 -0
  115. llm_code/runtime/vision.py +102 -0
  116. llm_code/swarm/__init__.py +1 -0
  117. llm_code/swarm/backend_subprocess.py +108 -0
  118. llm_code/swarm/backend_tmux.py +103 -0
  119. llm_code/swarm/backend_worktree.py +306 -0
  120. llm_code/swarm/checkpoint.py +74 -0
  121. llm_code/swarm/coordinator.py +236 -0
  122. llm_code/swarm/mailbox.py +88 -0
  123. llm_code/swarm/manager.py +202 -0
  124. llm_code/swarm/memory_sync.py +80 -0
  125. llm_code/swarm/recovery.py +21 -0
  126. llm_code/swarm/team.py +67 -0
  127. llm_code/swarm/types.py +31 -0
  128. llm_code/task/__init__.py +16 -0
  129. llm_code/task/diagnostics.py +93 -0
  130. llm_code/task/manager.py +162 -0
  131. llm_code/task/types.py +112 -0
  132. llm_code/task/verifier.py +104 -0
  133. llm_code/tools/__init__.py +0 -0
  134. llm_code/tools/agent.py +145 -0
  135. llm_code/tools/agent_roles.py +82 -0
  136. llm_code/tools/base.py +94 -0
  137. llm_code/tools/bash.py +565 -0
  138. llm_code/tools/computer_use_tools.py +278 -0
  139. llm_code/tools/coordinator_tool.py +75 -0
  140. llm_code/tools/cron_create.py +90 -0
  141. llm_code/tools/cron_delete.py +49 -0
  142. llm_code/tools/cron_list.py +51 -0
  143. llm_code/tools/deferred.py +92 -0
  144. llm_code/tools/dump.py +116 -0
  145. llm_code/tools/edit_file.py +282 -0
  146. llm_code/tools/git_tools.py +531 -0
  147. llm_code/tools/glob_search.py +112 -0
  148. llm_code/tools/grep_search.py +144 -0
  149. llm_code/tools/ide_diagnostics.py +59 -0
  150. llm_code/tools/ide_open.py +58 -0
  151. llm_code/tools/ide_selection.py +52 -0
  152. llm_code/tools/memory_tools.py +138 -0
  153. llm_code/tools/multi_edit.py +143 -0
  154. llm_code/tools/notebook_edit.py +107 -0
  155. llm_code/tools/notebook_read.py +81 -0
  156. llm_code/tools/parsing.py +63 -0
  157. llm_code/tools/read_file.py +154 -0
  158. llm_code/tools/registry.py +58 -0
  159. llm_code/tools/search_backends/__init__.py +56 -0
  160. llm_code/tools/search_backends/brave.py +56 -0
  161. llm_code/tools/search_backends/duckduckgo.py +129 -0
  162. llm_code/tools/search_backends/searxng.py +71 -0
  163. llm_code/tools/search_backends/tavily.py +73 -0
  164. llm_code/tools/swarm_create.py +109 -0
  165. llm_code/tools/swarm_delete.py +95 -0
  166. llm_code/tools/swarm_list.py +44 -0
  167. llm_code/tools/swarm_message.py +109 -0
  168. llm_code/tools/task_close.py +79 -0
  169. llm_code/tools/task_plan.py +79 -0
  170. llm_code/tools/task_verify.py +90 -0
  171. llm_code/tools/tool_search.py +65 -0
  172. llm_code/tools/web_common.py +258 -0
  173. llm_code/tools/web_fetch.py +223 -0
  174. llm_code/tools/web_search.py +280 -0
  175. llm_code/tools/write_file.py +118 -0
  176. llm_code/tui/__init__.py +1 -0
  177. llm_code/tui/app.py +2432 -0
  178. llm_code/tui/chat_view.py +82 -0
  179. llm_code/tui/chat_widgets.py +309 -0
  180. llm_code/tui/header_bar.py +46 -0
  181. llm_code/tui/input_bar.py +349 -0
  182. llm_code/tui/keybindings.py +142 -0
  183. llm_code/tui/marketplace.py +210 -0
  184. llm_code/tui/status_bar.py +72 -0
  185. llm_code/tui/theme.py +96 -0
  186. llm_code/utils/__init__.py +0 -0
  187. llm_code/utils/diff.py +111 -0
  188. llm_code/utils/errors.py +70 -0
  189. llm_code/utils/hyperlink.py +73 -0
  190. llm_code/utils/notebook.py +179 -0
  191. llm_code/utils/search.py +69 -0
  192. llm_code/utils/text_normalize.py +28 -0
  193. llm_code/utils/version_check.py +62 -0
  194. llm_code/vim/__init__.py +4 -0
  195. llm_code/vim/engine.py +51 -0
  196. llm_code/vim/motions.py +172 -0
  197. llm_code/vim/operators.py +183 -0
  198. llm_code/vim/text_objects.py +139 -0
  199. llm_code/vim/transitions.py +279 -0
  200. llm_code/vim/types.py +68 -0
  201. llm_code/voice/__init__.py +1 -0
  202. llm_code/voice/languages.py +43 -0
  203. llm_code/voice/recorder.py +136 -0
  204. llm_code/voice/stt.py +36 -0
  205. llm_code/voice/stt_anthropic.py +66 -0
  206. llm_code/voice/stt_google.py +32 -0
  207. llm_code/voice/stt_whisper.py +52 -0
  208. llmcode_cli-1.0.0.dist-info/METADATA +524 -0
  209. llmcode_cli-1.0.0.dist-info/RECORD +212 -0
  210. llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
  211. llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
  212. llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
llm_code/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """llm-code: CLI coding agent for local LLMs."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Code analysis — deterministic rule engine for Python and JS/TS."""
2
+ from __future__ import annotations
3
+
4
+ from llm_code.analysis.rules import AnalysisResult, Rule, RuleRegistry, Violation
5
+
6
+ __all__ = ["AnalysisResult", "Rule", "RuleRegistry", "Violation"]
@@ -0,0 +1,33 @@
1
+ """Analysis result cache -- save/load to .llm-code/last_analysis.json."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from llm_code.analysis.rules import Violation
9
+
10
+
11
+ def save_results(cwd: Path, violations: tuple[Violation, ...]) -> Path:
12
+ """Save violations to .llm-code/last_analysis.json."""
13
+ cache_dir = cwd / ".llm-code"
14
+ cache_dir.mkdir(parents=True, exist_ok=True)
15
+ cache_path = cache_dir / "last_analysis.json"
16
+ data = {
17
+ "timestamp": datetime.now(timezone.utc).isoformat(),
18
+ "violations": [v.to_dict() for v in violations],
19
+ }
20
+ cache_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
21
+ return cache_path
22
+
23
+
24
+ def load_results(cwd: Path) -> tuple[Violation, ...]:
25
+ """Load cached violations. Returns empty tuple if no cache."""
26
+ cache_path = cwd / ".llm-code" / "last_analysis.json"
27
+ if not cache_path.exists():
28
+ return ()
29
+ try:
30
+ data = json.loads(cache_path.read_text(encoding="utf-8"))
31
+ return tuple(Violation.from_dict(v) for v in data.get("violations", []))
32
+ except (json.JSONDecodeError, KeyError, TypeError):
33
+ return ()
@@ -0,0 +1,256 @@
1
+ """Analysis engine -- orchestrates file discovery, rule execution, and caching."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ import subprocess
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from llm_code.analysis.cache import load_results, save_results
10
+ from llm_code.analysis.go_rules import register_go_rules
11
+ from llm_code.analysis.rust_rules import register_rust_rules
12
+ from llm_code.analysis.js_rules import register_js_rules
13
+ from llm_code.analysis.python_rules import check_circular_import, register_python_rules
14
+ from llm_code.analysis.rules import AnalysisResult, RuleRegistry, Violation
15
+ from llm_code.analysis.universal_rules import register_universal_rules
16
+
17
+ _SKIP_DIRS = frozenset({
18
+ ".git",
19
+ "__pycache__",
20
+ "node_modules",
21
+ ".venv",
22
+ "venv",
23
+ "dist",
24
+ "build",
25
+ ".egg-info",
26
+ ".tox",
27
+ ".mypy_cache",
28
+ ".llm-code",
29
+ })
30
+
31
+ _PYTHON_EXTS = frozenset({".py"})
32
+ _JS_EXTS = frozenset({".js", ".ts", ".jsx", ".tsx"})
33
+ _GO_EXTS = frozenset({".go"})
34
+ _RUST_EXTS = frozenset({".rs"})
35
+ _ANALYSABLE_EXTS = _PYTHON_EXTS | _JS_EXTS | _GO_EXTS | _RUST_EXTS
36
+ _MAX_FILES = 500
37
+
38
+
39
+ def _discover_files(cwd: Path, max_files: int = _MAX_FILES) -> list[Path]:
40
+ """Walk cwd and collect source files, skipping irrelevant directories."""
41
+ files: list[Path] = []
42
+ for path in sorted(cwd.rglob("*")):
43
+ if any(part in _SKIP_DIRS for part in path.parts):
44
+ continue
45
+ if not path.is_file():
46
+ continue
47
+ if path.suffix not in _ANALYSABLE_EXTS:
48
+ continue
49
+ files.append(path)
50
+ if len(files) >= max_files:
51
+ break
52
+ return files
53
+
54
+
55
+ def _language_for_file(path: Path) -> str:
56
+ """Determine the language category for a file based on extension."""
57
+ if path.suffix in _PYTHON_EXTS:
58
+ return "python"
59
+ if path.suffix in _JS_EXTS:
60
+ return "javascript"
61
+ if path.suffix in _GO_EXTS:
62
+ return "go"
63
+ if path.suffix in _RUST_EXTS:
64
+ return "rust"
65
+ return "other"
66
+
67
+
68
+ def _build_registry() -> RuleRegistry:
69
+ """Create a fresh registry with all rules registered."""
70
+ registry = RuleRegistry()
71
+ register_universal_rules(registry)
72
+ register_python_rules(registry)
73
+ register_js_rules(registry)
74
+ register_go_rules(registry)
75
+ register_rust_rules(registry)
76
+ return registry
77
+
78
+
79
+ def run_analysis(cwd: Path, max_files: int = _MAX_FILES) -> AnalysisResult:
80
+ """Run all code analysis rules on the codebase."""
81
+ start = time.monotonic()
82
+
83
+ registry = _build_registry()
84
+ files = _discover_files(cwd, max_files)
85
+
86
+ all_violations: list[Violation] = []
87
+ python_contents: dict[str, str] = {}
88
+
89
+ for file_path in files:
90
+ try:
91
+ content = file_path.read_text(encoding="utf-8")
92
+ except (UnicodeDecodeError, OSError):
93
+ continue
94
+
95
+ rel_path = str(file_path.relative_to(cwd))
96
+ lang = _language_for_file(file_path)
97
+
98
+ # rules_for_language already includes "*" (universal) rules
99
+ rules = registry.rules_for_language(lang)
100
+
101
+ # Deduplicate by key
102
+ seen_keys: set[str] = set()
103
+ unique_rules = []
104
+ for r in rules:
105
+ if r.key not in seen_keys:
106
+ seen_keys.add(r.key)
107
+ unique_rules.append(r)
108
+
109
+ # Parse AST for Python files
110
+ tree: ast.Module | None = None
111
+ if lang == "python":
112
+ try:
113
+ tree = ast.parse(content, filename=rel_path)
114
+ except SyntaxError:
115
+ pass
116
+
117
+ for rule in unique_rules:
118
+ # Skip cross-file rules in per-file loop
119
+ if rule.key == "circular-import":
120
+ continue
121
+ try:
122
+ violations = rule.check(rel_path, content, tree=tree)
123
+ except TypeError:
124
+ # Rule doesn't accept tree keyword
125
+ try:
126
+ violations = rule.check(rel_path, content)
127
+ except Exception:
128
+ continue
129
+ except Exception:
130
+ continue
131
+ all_violations.extend(violations)
132
+
133
+ if lang == "python":
134
+ python_contents[rel_path] = content
135
+
136
+ # Cross-file: circular imports
137
+ if python_contents:
138
+ try:
139
+ circular = check_circular_import(python_contents)
140
+ all_violations.extend(circular)
141
+ except Exception:
142
+ pass
143
+
144
+ # Sort by severity then file then line
145
+ severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
146
+ all_violations.sort(
147
+ key=lambda v: (severity_order.get(v.severity, 9), v.file_path, v.line),
148
+ )
149
+
150
+ result = AnalysisResult(
151
+ violations=tuple(all_violations),
152
+ file_count=len(files),
153
+ duration_ms=(time.monotonic() - start) * 1000,
154
+ )
155
+
156
+ # Cache results
157
+ try:
158
+ save_results(cwd, result.violations)
159
+ except Exception:
160
+ pass
161
+
162
+ return result
163
+
164
+
165
+ def run_diff_check(cwd: Path) -> tuple[list[Violation], list[Violation]]:
166
+ """Run analysis only on changed files, compare with cached results.
167
+
168
+ Returns (new_violations, fixed_violations).
169
+ """
170
+ changed_files = _get_changed_files(cwd)
171
+ if not changed_files:
172
+ return ([], [])
173
+
174
+ # Load previous results
175
+ old_violations = load_results(cwd)
176
+ old_by_key = {(v.rule_key, v.file_path, v.line): v for v in old_violations}
177
+
178
+ # Run analysis on changed files only
179
+ registry = _build_registry()
180
+
181
+ current_violations: list[Violation] = []
182
+ for rel_path in changed_files:
183
+ file_path = cwd / rel_path
184
+ if not file_path.exists():
185
+ continue
186
+ try:
187
+ content = file_path.read_text(encoding="utf-8")
188
+ except (UnicodeDecodeError, OSError):
189
+ continue
190
+
191
+ lang = _language_for_file(file_path)
192
+ rules = registry.rules_for_language(lang)
193
+
194
+ seen_keys: set[str] = set()
195
+ unique_rules = []
196
+ for r in rules:
197
+ if r.key not in seen_keys:
198
+ seen_keys.add(r.key)
199
+ unique_rules.append(r)
200
+
201
+ tree: ast.Module | None = None
202
+ if lang == "python":
203
+ try:
204
+ tree = ast.parse(content, filename=rel_path)
205
+ except SyntaxError:
206
+ pass
207
+
208
+ for rule in unique_rules:
209
+ if rule.key == "circular-import":
210
+ continue
211
+ try:
212
+ violations = rule.check(rel_path, content, tree=tree)
213
+ except TypeError:
214
+ try:
215
+ violations = rule.check(rel_path, content)
216
+ except Exception:
217
+ continue
218
+ except Exception:
219
+ continue
220
+ current_violations.extend(violations)
221
+
222
+ current_by_key = {(v.rule_key, v.file_path, v.line): v for v in current_violations}
223
+
224
+ changed_set = set(changed_files)
225
+ new_violations = [v for k, v in current_by_key.items() if k not in old_by_key]
226
+ fixed_violations = [
227
+ v
228
+ for k, v in old_by_key.items()
229
+ if v.file_path in changed_set and k not in current_by_key
230
+ ]
231
+
232
+ return (new_violations, fixed_violations)
233
+
234
+
235
+ def _get_changed_files(cwd: Path) -> list[str]:
236
+ """Get list of changed files from git (unstaged + staged)."""
237
+ files: set[str] = set()
238
+ for cmd in (
239
+ ["git", "diff", "--name-only"],
240
+ ["git", "diff", "--cached", "--name-only"],
241
+ ):
242
+ try:
243
+ result = subprocess.run(
244
+ cmd,
245
+ cwd=str(cwd),
246
+ capture_output=True,
247
+ text=True,
248
+ timeout=5,
249
+ )
250
+ if result.returncode == 0:
251
+ for line in result.stdout.strip().split("\n"):
252
+ if line.strip():
253
+ files.add(line.strip())
254
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
255
+ continue
256
+ return sorted(files)
@@ -0,0 +1,114 @@
1
+ """Go regex-based code analysis rules."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+
6
+ from llm_code.analysis.rules import Rule, RuleRegistry, Violation
7
+
8
+ _EMPTY_ERROR_CHECK = re.compile(
9
+ r"if\s+err\s*!=\s*nil\s*\{\s*\}",
10
+ re.MULTILINE,
11
+ )
12
+
13
+ _FMT_PRINT_PATTERN = re.compile(
14
+ r"fmt\.(Println|Printf|Print)\s*\(",
15
+ )
16
+
17
+ _UNDERSCORE_ERROR = re.compile(
18
+ r"_\s*=\s*\w+[\w.]*\([^)]*\)",
19
+ )
20
+
21
+ _TEST_SUFFIXES = ("_test.go",)
22
+
23
+
24
+ def _is_test_file(file_path: str) -> bool:
25
+ return any(file_path.endswith(s) for s in _TEST_SUFFIXES)
26
+
27
+
28
+ def check_empty_error_check(file_path: str, content: str, tree: object = None) -> list[Violation]:
29
+ """Detect empty error check blocks: if err != nil { }."""
30
+ violations: list[Violation] = []
31
+ for match in _EMPTY_ERROR_CHECK.finditer(content):
32
+ line = content[: match.start()].count("\n") + 1
33
+ violations.append(
34
+ Violation(
35
+ rule_key="go-empty-error-check",
36
+ severity="high",
37
+ file_path=file_path,
38
+ line=line,
39
+ message="Empty error check: if err != nil {}",
40
+ )
41
+ )
42
+ return violations
43
+
44
+
45
+ def check_fmt_println(file_path: str, content: str, tree: object = None) -> list[Violation]:
46
+ """Detect fmt.Println/Printf/Print in non-test Go files."""
47
+ if _is_test_file(file_path):
48
+ return []
49
+
50
+ violations: list[Violation] = []
51
+ for match in _FMT_PRINT_PATTERN.finditer(content):
52
+ line = content[: match.start()].count("\n") + 1
53
+ method = match.group(1)
54
+ violations.append(
55
+ Violation(
56
+ rule_key="go-fmt-println",
57
+ severity="low",
58
+ file_path=file_path,
59
+ line=line,
60
+ message=f"fmt.{method}() in production code — prefer structured logging",
61
+ )
62
+ )
63
+ return violations
64
+
65
+
66
+ def check_underscore_error(file_path: str, content: str, tree: object = None) -> list[Violation]:
67
+ """Detect discarded errors: _ = someFunc()."""
68
+ if _is_test_file(file_path):
69
+ return []
70
+
71
+ violations: list[Violation] = []
72
+ for match in _UNDERSCORE_ERROR.finditer(content):
73
+ line = content[: match.start()].count("\n") + 1
74
+ violations.append(
75
+ Violation(
76
+ rule_key="go-underscore-error",
77
+ severity="medium",
78
+ file_path=file_path,
79
+ line=line,
80
+ message=f"Discarded error: {match.group(0).strip()}",
81
+ )
82
+ )
83
+ return violations
84
+
85
+
86
+ def register_go_rules(registry: RuleRegistry) -> None:
87
+ """Register all Go rules with the given registry."""
88
+ registry.register(
89
+ Rule(
90
+ key="go-empty-error-check",
91
+ name="Empty error check",
92
+ severity="high",
93
+ languages=("go",),
94
+ check=check_empty_error_check,
95
+ )
96
+ )
97
+ registry.register(
98
+ Rule(
99
+ key="go-fmt-println",
100
+ name="fmt.Println in production",
101
+ severity="low",
102
+ languages=("go",),
103
+ check=check_fmt_println,
104
+ )
105
+ )
106
+ registry.register(
107
+ Rule(
108
+ key="go-underscore-error",
109
+ name="Discarded error",
110
+ severity="medium",
111
+ languages=("go",),
112
+ check=check_underscore_error,
113
+ )
114
+ )
@@ -0,0 +1,84 @@
1
+ """JS/TS regex-based code analysis rules."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from pathlib import PurePosixPath
6
+
7
+ from llm_code.analysis.rules import Rule, RuleRegistry, Violation
8
+
9
+ _EMPTY_CATCH_PATTERN = re.compile(
10
+ r"catch\s*\([^)]*\)\s*\{\s*\}",
11
+ re.MULTILINE,
12
+ )
13
+
14
+ _CONSOLE_LOG_PATTERN = re.compile(
15
+ r"console\.(log|debug|info|warn|error)\s*\(",
16
+ )
17
+
18
+ _TEST_DIR_NAMES = frozenset({"tests", "test", "__tests__"})
19
+ _TEST_SUFFIXES = frozenset({".test", ".spec"})
20
+
21
+
22
+ def _is_test_file(file_path: str) -> bool:
23
+ """Check if a file is a test file by path or naming convention."""
24
+ parts = PurePosixPath(file_path).parts
25
+ if any(p in _TEST_DIR_NAMES for p in parts):
26
+ return True
27
+ stem = PurePosixPath(file_path).stem
28
+ for suffix in _TEST_SUFFIXES:
29
+ if stem.endswith(suffix):
30
+ return True
31
+ return False
32
+
33
+
34
+ def check_empty_catch(file_path: str, content: str, tree: object = None) -> list[Violation]:
35
+ """Detect empty catch blocks via regex."""
36
+ violations: list[Violation] = []
37
+ for match in _EMPTY_CATCH_PATTERN.finditer(content):
38
+ # Approximate line number
39
+ line = content[:match.start()].count("\n") + 1
40
+ violations.append(Violation(
41
+ rule_key="empty-catch",
42
+ severity="medium",
43
+ file_path=file_path,
44
+ line=line,
45
+ message="Empty catch block",
46
+ ))
47
+ return violations
48
+
49
+
50
+ def check_console_log(file_path: str, content: str, tree: object = None) -> list[Violation]:
51
+ """Detect console.log/debug/info/warn/error in non-test files."""
52
+ if _is_test_file(file_path):
53
+ return []
54
+
55
+ violations: list[Violation] = []
56
+ for match in _CONSOLE_LOG_PATTERN.finditer(content):
57
+ line = content[:match.start()].count("\n") + 1
58
+ method = match.group(1)
59
+ violations.append(Violation(
60
+ rule_key="console-log",
61
+ severity="low",
62
+ file_path=file_path,
63
+ line=line,
64
+ message=f"console.{method}() in production code",
65
+ ))
66
+ return violations
67
+
68
+
69
+ def register_js_rules(registry: RuleRegistry) -> None:
70
+ """Register all JS/TS rules with the given registry."""
71
+ registry.register(Rule(
72
+ key="empty-catch",
73
+ name="Empty catch block",
74
+ severity="medium",
75
+ languages=("javascript", "typescript"),
76
+ check=check_empty_catch,
77
+ ))
78
+ registry.register(Rule(
79
+ key="console-log",
80
+ name="console.log in production",
81
+ severity="low",
82
+ languages=("javascript", "typescript"),
83
+ check=check_console_log,
84
+ ))