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.
- llm_code/__init__.py +2 -0
- llm_code/analysis/__init__.py +6 -0
- llm_code/analysis/cache.py +33 -0
- llm_code/analysis/engine.py +256 -0
- llm_code/analysis/go_rules.py +114 -0
- llm_code/analysis/js_rules.py +84 -0
- llm_code/analysis/python_rules.py +311 -0
- llm_code/analysis/rules.py +140 -0
- llm_code/analysis/rust_rules.py +108 -0
- llm_code/analysis/universal_rules.py +111 -0
- llm_code/api/__init__.py +0 -0
- llm_code/api/client.py +90 -0
- llm_code/api/errors.py +73 -0
- llm_code/api/openai_compat.py +390 -0
- llm_code/api/provider.py +35 -0
- llm_code/api/sse.py +52 -0
- llm_code/api/types.py +140 -0
- llm_code/cli/__init__.py +0 -0
- llm_code/cli/commands.py +70 -0
- llm_code/cli/image.py +122 -0
- llm_code/cli/render.py +214 -0
- llm_code/cli/status_line.py +79 -0
- llm_code/cli/streaming.py +92 -0
- llm_code/cli/tui_main.py +220 -0
- llm_code/computer_use/__init__.py +11 -0
- llm_code/computer_use/app_detect.py +49 -0
- llm_code/computer_use/app_tier.py +57 -0
- llm_code/computer_use/coordinator.py +99 -0
- llm_code/computer_use/input_control.py +71 -0
- llm_code/computer_use/screenshot.py +93 -0
- llm_code/cron/__init__.py +13 -0
- llm_code/cron/parser.py +145 -0
- llm_code/cron/scheduler.py +135 -0
- llm_code/cron/storage.py +126 -0
- llm_code/enterprise/__init__.py +1 -0
- llm_code/enterprise/audit.py +59 -0
- llm_code/enterprise/auth.py +26 -0
- llm_code/enterprise/oidc.py +95 -0
- llm_code/enterprise/rbac.py +65 -0
- llm_code/harness/__init__.py +5 -0
- llm_code/harness/config.py +33 -0
- llm_code/harness/engine.py +129 -0
- llm_code/harness/guides.py +41 -0
- llm_code/harness/sensors.py +68 -0
- llm_code/harness/templates.py +84 -0
- llm_code/hida/__init__.py +1 -0
- llm_code/hida/classifier.py +187 -0
- llm_code/hida/engine.py +49 -0
- llm_code/hida/profiles.py +95 -0
- llm_code/hida/types.py +28 -0
- llm_code/ide/__init__.py +1 -0
- llm_code/ide/bridge.py +80 -0
- llm_code/ide/detector.py +76 -0
- llm_code/ide/server.py +169 -0
- llm_code/logging.py +29 -0
- llm_code/lsp/__init__.py +0 -0
- llm_code/lsp/client.py +298 -0
- llm_code/lsp/detector.py +42 -0
- llm_code/lsp/manager.py +56 -0
- llm_code/lsp/tools.py +288 -0
- llm_code/marketplace/__init__.py +0 -0
- llm_code/marketplace/builtin_registry.py +102 -0
- llm_code/marketplace/installer.py +162 -0
- llm_code/marketplace/plugin.py +78 -0
- llm_code/marketplace/registry.py +360 -0
- llm_code/mcp/__init__.py +0 -0
- llm_code/mcp/bridge.py +87 -0
- llm_code/mcp/client.py +117 -0
- llm_code/mcp/health.py +120 -0
- llm_code/mcp/manager.py +214 -0
- llm_code/mcp/oauth.py +219 -0
- llm_code/mcp/transport.py +254 -0
- llm_code/mcp/types.py +53 -0
- llm_code/remote/__init__.py +0 -0
- llm_code/remote/client.py +136 -0
- llm_code/remote/protocol.py +22 -0
- llm_code/remote/server.py +275 -0
- llm_code/remote/ssh_proxy.py +56 -0
- llm_code/runtime/__init__.py +0 -0
- llm_code/runtime/auto_commit.py +56 -0
- llm_code/runtime/auto_diagnose.py +62 -0
- llm_code/runtime/checkpoint.py +70 -0
- llm_code/runtime/checkpoint_recovery.py +142 -0
- llm_code/runtime/compaction.py +35 -0
- llm_code/runtime/compressor.py +415 -0
- llm_code/runtime/config.py +533 -0
- llm_code/runtime/context.py +49 -0
- llm_code/runtime/conversation.py +921 -0
- llm_code/runtime/cost_tracker.py +126 -0
- llm_code/runtime/dream.py +127 -0
- llm_code/runtime/file_protection.py +150 -0
- llm_code/runtime/hardware.py +85 -0
- llm_code/runtime/hooks.py +223 -0
- llm_code/runtime/indexer.py +230 -0
- llm_code/runtime/knowledge_compiler.py +232 -0
- llm_code/runtime/memory.py +132 -0
- llm_code/runtime/memory_layers.py +467 -0
- llm_code/runtime/memory_lint.py +252 -0
- llm_code/runtime/model_aliases.py +37 -0
- llm_code/runtime/ollama.py +93 -0
- llm_code/runtime/overlay.py +124 -0
- llm_code/runtime/permissions.py +200 -0
- llm_code/runtime/plan.py +45 -0
- llm_code/runtime/prompt.py +238 -0
- llm_code/runtime/repo_map.py +174 -0
- llm_code/runtime/sandbox.py +116 -0
- llm_code/runtime/session.py +268 -0
- llm_code/runtime/skill_resolver.py +61 -0
- llm_code/runtime/skills.py +133 -0
- llm_code/runtime/speculative.py +75 -0
- llm_code/runtime/streaming_executor.py +216 -0
- llm_code/runtime/telemetry.py +196 -0
- llm_code/runtime/token_budget.py +26 -0
- llm_code/runtime/vcr.py +142 -0
- llm_code/runtime/vision.py +102 -0
- llm_code/swarm/__init__.py +1 -0
- llm_code/swarm/backend_subprocess.py +108 -0
- llm_code/swarm/backend_tmux.py +103 -0
- llm_code/swarm/backend_worktree.py +306 -0
- llm_code/swarm/checkpoint.py +74 -0
- llm_code/swarm/coordinator.py +236 -0
- llm_code/swarm/mailbox.py +88 -0
- llm_code/swarm/manager.py +202 -0
- llm_code/swarm/memory_sync.py +80 -0
- llm_code/swarm/recovery.py +21 -0
- llm_code/swarm/team.py +67 -0
- llm_code/swarm/types.py +31 -0
- llm_code/task/__init__.py +16 -0
- llm_code/task/diagnostics.py +93 -0
- llm_code/task/manager.py +162 -0
- llm_code/task/types.py +112 -0
- llm_code/task/verifier.py +104 -0
- llm_code/tools/__init__.py +0 -0
- llm_code/tools/agent.py +145 -0
- llm_code/tools/agent_roles.py +82 -0
- llm_code/tools/base.py +94 -0
- llm_code/tools/bash.py +565 -0
- llm_code/tools/computer_use_tools.py +278 -0
- llm_code/tools/coordinator_tool.py +75 -0
- llm_code/tools/cron_create.py +90 -0
- llm_code/tools/cron_delete.py +49 -0
- llm_code/tools/cron_list.py +51 -0
- llm_code/tools/deferred.py +92 -0
- llm_code/tools/dump.py +116 -0
- llm_code/tools/edit_file.py +282 -0
- llm_code/tools/git_tools.py +531 -0
- llm_code/tools/glob_search.py +112 -0
- llm_code/tools/grep_search.py +144 -0
- llm_code/tools/ide_diagnostics.py +59 -0
- llm_code/tools/ide_open.py +58 -0
- llm_code/tools/ide_selection.py +52 -0
- llm_code/tools/memory_tools.py +138 -0
- llm_code/tools/multi_edit.py +143 -0
- llm_code/tools/notebook_edit.py +107 -0
- llm_code/tools/notebook_read.py +81 -0
- llm_code/tools/parsing.py +63 -0
- llm_code/tools/read_file.py +154 -0
- llm_code/tools/registry.py +58 -0
- llm_code/tools/search_backends/__init__.py +56 -0
- llm_code/tools/search_backends/brave.py +56 -0
- llm_code/tools/search_backends/duckduckgo.py +129 -0
- llm_code/tools/search_backends/searxng.py +71 -0
- llm_code/tools/search_backends/tavily.py +73 -0
- llm_code/tools/swarm_create.py +109 -0
- llm_code/tools/swarm_delete.py +95 -0
- llm_code/tools/swarm_list.py +44 -0
- llm_code/tools/swarm_message.py +109 -0
- llm_code/tools/task_close.py +79 -0
- llm_code/tools/task_plan.py +79 -0
- llm_code/tools/task_verify.py +90 -0
- llm_code/tools/tool_search.py +65 -0
- llm_code/tools/web_common.py +258 -0
- llm_code/tools/web_fetch.py +223 -0
- llm_code/tools/web_search.py +280 -0
- llm_code/tools/write_file.py +118 -0
- llm_code/tui/__init__.py +1 -0
- llm_code/tui/app.py +2432 -0
- llm_code/tui/chat_view.py +82 -0
- llm_code/tui/chat_widgets.py +309 -0
- llm_code/tui/header_bar.py +46 -0
- llm_code/tui/input_bar.py +349 -0
- llm_code/tui/keybindings.py +142 -0
- llm_code/tui/marketplace.py +210 -0
- llm_code/tui/status_bar.py +72 -0
- llm_code/tui/theme.py +96 -0
- llm_code/utils/__init__.py +0 -0
- llm_code/utils/diff.py +111 -0
- llm_code/utils/errors.py +70 -0
- llm_code/utils/hyperlink.py +73 -0
- llm_code/utils/notebook.py +179 -0
- llm_code/utils/search.py +69 -0
- llm_code/utils/text_normalize.py +28 -0
- llm_code/utils/version_check.py +62 -0
- llm_code/vim/__init__.py +4 -0
- llm_code/vim/engine.py +51 -0
- llm_code/vim/motions.py +172 -0
- llm_code/vim/operators.py +183 -0
- llm_code/vim/text_objects.py +139 -0
- llm_code/vim/transitions.py +279 -0
- llm_code/vim/types.py +68 -0
- llm_code/voice/__init__.py +1 -0
- llm_code/voice/languages.py +43 -0
- llm_code/voice/recorder.py +136 -0
- llm_code/voice/stt.py +36 -0
- llm_code/voice/stt_anthropic.py +66 -0
- llm_code/voice/stt_google.py +32 -0
- llm_code/voice/stt_whisper.py +52 -0
- llmcode_cli-1.0.0.dist-info/METADATA +524 -0
- llmcode_cli-1.0.0.dist-info/RECORD +212 -0
- llmcode_cli-1.0.0.dist-info/WHEEL +4 -0
- llmcode_cli-1.0.0.dist-info/entry_points.txt +2 -0
- llmcode_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
llm_code/__init__.py
ADDED
|
@@ -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
|
+
))
|