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
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Python AST-based code analysis rules."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
|
|
7
|
+
from llm_code.analysis.rules import Rule, RuleRegistry, Violation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_bare_except(
|
|
11
|
+
file_path: str, content: str, tree: ast.Module | None = None
|
|
12
|
+
) -> list[Violation]:
|
|
13
|
+
"""Detect bare except clauses (except without a type)."""
|
|
14
|
+
if tree is None:
|
|
15
|
+
return []
|
|
16
|
+
violations: list[Violation] = []
|
|
17
|
+
for node in ast.walk(tree):
|
|
18
|
+
if isinstance(node, ast.ExceptHandler) and node.type is None:
|
|
19
|
+
violations.append(
|
|
20
|
+
Violation(
|
|
21
|
+
rule_key="bare-except",
|
|
22
|
+
severity="high",
|
|
23
|
+
file_path=file_path,
|
|
24
|
+
line=node.lineno,
|
|
25
|
+
message="Bare except clause",
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
return violations
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_empty_except(
|
|
32
|
+
file_path: str, content: str, tree: ast.Module | None = None
|
|
33
|
+
) -> list[Violation]:
|
|
34
|
+
"""Detect except blocks with only pass or ellipsis."""
|
|
35
|
+
if tree is None:
|
|
36
|
+
return []
|
|
37
|
+
violations: list[Violation] = []
|
|
38
|
+
for node in ast.walk(tree):
|
|
39
|
+
if isinstance(node, ast.ExceptHandler):
|
|
40
|
+
body = node.body
|
|
41
|
+
if len(body) == 1:
|
|
42
|
+
stmt = body[0]
|
|
43
|
+
is_pass = isinstance(stmt, ast.Pass)
|
|
44
|
+
is_ellipsis = (
|
|
45
|
+
isinstance(stmt, ast.Expr)
|
|
46
|
+
and isinstance(stmt.value, ast.Constant)
|
|
47
|
+
and stmt.value.value is ...
|
|
48
|
+
)
|
|
49
|
+
if is_pass or is_ellipsis:
|
|
50
|
+
violations.append(
|
|
51
|
+
Violation(
|
|
52
|
+
rule_key="empty-except",
|
|
53
|
+
severity="medium",
|
|
54
|
+
file_path=file_path,
|
|
55
|
+
line=node.lineno,
|
|
56
|
+
message="Empty except block",
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
return violations
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_unused_import(
|
|
63
|
+
file_path: str, content: str, tree: ast.Module | None = None
|
|
64
|
+
) -> list[Violation]:
|
|
65
|
+
"""Detect imported names that are never referenced in the file."""
|
|
66
|
+
if tree is None:
|
|
67
|
+
return []
|
|
68
|
+
# Skip __init__.py files (re-exports)
|
|
69
|
+
if PurePosixPath(file_path).name == "__init__.py":
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
# Collect imported names -> line numbers
|
|
73
|
+
imported: dict[str, int] = {}
|
|
74
|
+
for node in ast.walk(tree):
|
|
75
|
+
if isinstance(node, ast.Import):
|
|
76
|
+
for alias in node.names:
|
|
77
|
+
name = alias.asname or alias.name.split(".")[0]
|
|
78
|
+
imported[name] = node.lineno
|
|
79
|
+
elif isinstance(node, ast.ImportFrom):
|
|
80
|
+
for alias in node.names:
|
|
81
|
+
if alias.name == "*":
|
|
82
|
+
continue
|
|
83
|
+
name = alias.asname or alias.name
|
|
84
|
+
imported[name] = node.lineno
|
|
85
|
+
|
|
86
|
+
if not imported:
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
# Collect all Name references
|
|
90
|
+
used_names: set[str] = set()
|
|
91
|
+
for node in ast.walk(tree):
|
|
92
|
+
if isinstance(node, ast.Name):
|
|
93
|
+
used_names.add(node.id)
|
|
94
|
+
|
|
95
|
+
violations: list[Violation] = []
|
|
96
|
+
for name, lineno in sorted(imported.items(), key=lambda x: x[1]):
|
|
97
|
+
if name not in used_names:
|
|
98
|
+
violations.append(
|
|
99
|
+
Violation(
|
|
100
|
+
rule_key="unused-import",
|
|
101
|
+
severity="low",
|
|
102
|
+
file_path=file_path,
|
|
103
|
+
line=lineno,
|
|
104
|
+
message=f"Unused import: {name}",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return violations
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check_star_import(
|
|
111
|
+
file_path: str, content: str, tree: ast.Module | None = None
|
|
112
|
+
) -> list[Violation]:
|
|
113
|
+
"""Detect wildcard imports (from x import *)."""
|
|
114
|
+
if tree is None:
|
|
115
|
+
return []
|
|
116
|
+
violations: list[Violation] = []
|
|
117
|
+
for node in ast.walk(tree):
|
|
118
|
+
if isinstance(node, ast.ImportFrom) and node.names:
|
|
119
|
+
if node.names[0].name == "*":
|
|
120
|
+
module = node.module or ""
|
|
121
|
+
violations.append(
|
|
122
|
+
Violation(
|
|
123
|
+
rule_key="star-import",
|
|
124
|
+
severity="low",
|
|
125
|
+
file_path=file_path,
|
|
126
|
+
line=node.lineno,
|
|
127
|
+
message=f"Wildcard import: from {module} import *",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return violations
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def check_print_in_prod(
|
|
134
|
+
file_path: str, content: str, tree: ast.Module | None = None
|
|
135
|
+
) -> list[Violation]:
|
|
136
|
+
"""Detect print() calls in non-test files."""
|
|
137
|
+
if tree is None:
|
|
138
|
+
return []
|
|
139
|
+
# Skip test files
|
|
140
|
+
parts = PurePosixPath(file_path).parts
|
|
141
|
+
if any(p in ("tests", "test") for p in parts):
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
violations: list[Violation] = []
|
|
145
|
+
for node in ast.walk(tree):
|
|
146
|
+
if (
|
|
147
|
+
isinstance(node, ast.Call)
|
|
148
|
+
and isinstance(node.func, ast.Name)
|
|
149
|
+
and node.func.id == "print"
|
|
150
|
+
):
|
|
151
|
+
violations.append(
|
|
152
|
+
Violation(
|
|
153
|
+
rule_key="print-in-prod",
|
|
154
|
+
severity="low",
|
|
155
|
+
file_path=file_path,
|
|
156
|
+
line=node.lineno,
|
|
157
|
+
message="print() in production code",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return violations
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_circular_import(files: dict[str, str]) -> list[Violation]:
|
|
164
|
+
"""Detect circular import chains across multiple Python files.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
files: Mapping of relative file paths to their source content.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
A list of Violation for each detected cycle.
|
|
171
|
+
"""
|
|
172
|
+
# Build module name -> set of imported module names
|
|
173
|
+
graph: dict[str, set[str]] = {}
|
|
174
|
+
file_modules: set[str] = set()
|
|
175
|
+
|
|
176
|
+
for file_path, content in files.items():
|
|
177
|
+
module_name = PurePosixPath(file_path).stem
|
|
178
|
+
file_modules.add(module_name)
|
|
179
|
+
graph.setdefault(module_name, set())
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
tree = ast.parse(content, filename=file_path)
|
|
183
|
+
except SyntaxError:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
for node in ast.walk(tree):
|
|
187
|
+
if isinstance(node, ast.Import):
|
|
188
|
+
for alias in node.names:
|
|
189
|
+
dep = alias.name.split(".")[0]
|
|
190
|
+
graph[module_name].add(dep)
|
|
191
|
+
elif isinstance(node, ast.ImportFrom):
|
|
192
|
+
if node.module:
|
|
193
|
+
dep = node.module.split(".")[0]
|
|
194
|
+
graph[module_name].add(dep)
|
|
195
|
+
|
|
196
|
+
# Filter graph to only include project-internal modules
|
|
197
|
+
for mod in graph:
|
|
198
|
+
graph[mod] = graph[mod] & file_modules
|
|
199
|
+
|
|
200
|
+
# DFS cycle detection
|
|
201
|
+
visited: set[str] = set()
|
|
202
|
+
on_stack: set[str] = set()
|
|
203
|
+
cycles: list[list[str]] = []
|
|
204
|
+
|
|
205
|
+
def _dfs(node: str, path: list[str]) -> None:
|
|
206
|
+
visited.add(node)
|
|
207
|
+
on_stack.add(node)
|
|
208
|
+
path.append(node)
|
|
209
|
+
for neighbor in sorted(graph.get(node, set())):
|
|
210
|
+
if neighbor not in visited:
|
|
211
|
+
_dfs(neighbor, path)
|
|
212
|
+
elif neighbor in on_stack:
|
|
213
|
+
# Found a cycle: extract it
|
|
214
|
+
cycle_start = path.index(neighbor)
|
|
215
|
+
cycle = path[cycle_start:] + [neighbor]
|
|
216
|
+
cycles.append(cycle)
|
|
217
|
+
path.pop()
|
|
218
|
+
on_stack.discard(node)
|
|
219
|
+
|
|
220
|
+
for mod in sorted(graph):
|
|
221
|
+
if mod not in visited:
|
|
222
|
+
_dfs(mod, [])
|
|
223
|
+
|
|
224
|
+
# Deduplicate cycles by their canonical form (sorted rotation)
|
|
225
|
+
seen_cycles: set[tuple[str, ...]] = set()
|
|
226
|
+
violations: list[Violation] = []
|
|
227
|
+
|
|
228
|
+
for cycle in cycles:
|
|
229
|
+
# Normalize: rotate so smallest element is first
|
|
230
|
+
min_idx = cycle[:-1].index(min(cycle[:-1]))
|
|
231
|
+
canonical = tuple(cycle[min_idx:-1]) + (cycle[min_idx],)
|
|
232
|
+
if canonical in seen_cycles:
|
|
233
|
+
continue
|
|
234
|
+
seen_cycles.add(canonical)
|
|
235
|
+
|
|
236
|
+
chain = " → ".join(canonical)
|
|
237
|
+
# Report on the first module in the cycle
|
|
238
|
+
first_mod = canonical[0]
|
|
239
|
+
first_file = next(
|
|
240
|
+
(fp for fp in files if PurePosixPath(fp).stem == first_mod),
|
|
241
|
+
f"{first_mod}.py",
|
|
242
|
+
)
|
|
243
|
+
violations.append(
|
|
244
|
+
Violation(
|
|
245
|
+
rule_key="circular-import",
|
|
246
|
+
severity="high",
|
|
247
|
+
file_path=first_file,
|
|
248
|
+
line=0,
|
|
249
|
+
message=f"Circular import: {chain}",
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return violations
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def register_python_rules(registry: RuleRegistry) -> None:
|
|
257
|
+
"""Register all Python rules with the given registry."""
|
|
258
|
+
registry.register(
|
|
259
|
+
Rule(
|
|
260
|
+
key="bare-except",
|
|
261
|
+
name="Bare except clause",
|
|
262
|
+
severity="high",
|
|
263
|
+
languages=("python",),
|
|
264
|
+
check=check_bare_except,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
registry.register(
|
|
268
|
+
Rule(
|
|
269
|
+
key="empty-except",
|
|
270
|
+
name="Empty except block",
|
|
271
|
+
severity="medium",
|
|
272
|
+
languages=("python",),
|
|
273
|
+
check=check_empty_except,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
registry.register(
|
|
277
|
+
Rule(
|
|
278
|
+
key="unused-import",
|
|
279
|
+
name="Unused import",
|
|
280
|
+
severity="low",
|
|
281
|
+
languages=("python",),
|
|
282
|
+
check=check_unused_import,
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
registry.register(
|
|
286
|
+
Rule(
|
|
287
|
+
key="star-import",
|
|
288
|
+
name="Wildcard import",
|
|
289
|
+
severity="low",
|
|
290
|
+
languages=("python",),
|
|
291
|
+
check=check_star_import,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
registry.register(
|
|
295
|
+
Rule(
|
|
296
|
+
key="print-in-prod",
|
|
297
|
+
name="print() in production code",
|
|
298
|
+
severity="low",
|
|
299
|
+
languages=("python",),
|
|
300
|
+
check=check_print_in_prod,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
registry.register(
|
|
304
|
+
Rule(
|
|
305
|
+
key="circular-import",
|
|
306
|
+
name="Circular import chain",
|
|
307
|
+
severity="high",
|
|
308
|
+
languages=("python",),
|
|
309
|
+
check=check_circular_import, # type: ignore[arg-type] # cross-file signature
|
|
310
|
+
)
|
|
311
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Core types and rule registry for code analysis."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_SEVERITY_ORDER = ("critical", "high", "medium", "low")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Violation:
|
|
14
|
+
"""A single code analysis violation."""
|
|
15
|
+
|
|
16
|
+
rule_key: str
|
|
17
|
+
severity: str
|
|
18
|
+
file_path: str
|
|
19
|
+
line: int
|
|
20
|
+
message: str
|
|
21
|
+
end_line: int = 0
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"rule_key": self.rule_key,
|
|
26
|
+
"severity": self.severity,
|
|
27
|
+
"file_path": self.file_path,
|
|
28
|
+
"line": self.line,
|
|
29
|
+
"message": self.message,
|
|
30
|
+
"end_line": self.end_line,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, data: dict[str, Any]) -> Violation:
|
|
35
|
+
return cls(
|
|
36
|
+
rule_key=data["rule_key"],
|
|
37
|
+
severity=data["severity"],
|
|
38
|
+
file_path=data["file_path"],
|
|
39
|
+
line=data["line"],
|
|
40
|
+
message=data["message"],
|
|
41
|
+
end_line=data.get("end_line", 0),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class Rule:
|
|
47
|
+
"""A deterministic code analysis rule."""
|
|
48
|
+
|
|
49
|
+
key: str
|
|
50
|
+
name: str
|
|
51
|
+
severity: str
|
|
52
|
+
languages: tuple[str, ...]
|
|
53
|
+
check: Callable[..., list[Violation]]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class AnalysisResult:
|
|
58
|
+
"""Immutable result of a code analysis run."""
|
|
59
|
+
|
|
60
|
+
violations: tuple[Violation, ...]
|
|
61
|
+
file_count: int
|
|
62
|
+
duration_ms: float
|
|
63
|
+
|
|
64
|
+
def summary_counts(self) -> dict[str, int]:
|
|
65
|
+
counts = {s: 0 for s in _SEVERITY_ORDER}
|
|
66
|
+
for v in self.violations:
|
|
67
|
+
if v.severity in counts:
|
|
68
|
+
counts[v.severity] += 1
|
|
69
|
+
return counts
|
|
70
|
+
|
|
71
|
+
def format_chat(self) -> str:
|
|
72
|
+
"""Render violations for chat display."""
|
|
73
|
+
counts = self.summary_counts()
|
|
74
|
+
total = len(self.violations)
|
|
75
|
+
header = f"## Code Analysis — {self.file_count} files, {total} violations\n"
|
|
76
|
+
if total == 0:
|
|
77
|
+
return header + "\nNo violations found."
|
|
78
|
+
|
|
79
|
+
lines: list[str] = [header]
|
|
80
|
+
severity_key = {s: i for i, s in enumerate(_SEVERITY_ORDER)}
|
|
81
|
+
sorted_violations = sorted(
|
|
82
|
+
self.violations,
|
|
83
|
+
key=lambda v: (severity_key.get(v.severity, 99), v.file_path, v.line),
|
|
84
|
+
)
|
|
85
|
+
for v in sorted_violations:
|
|
86
|
+
label = v.severity.upper().ljust(8)
|
|
87
|
+
loc = f"{v.file_path}:{v.line}" if v.line > 0 else v.file_path
|
|
88
|
+
lines.append(f" {label} {loc:<30} {v.message}")
|
|
89
|
+
|
|
90
|
+
parts = [f"{c} {s}" for s, c in counts.items() if c > 0]
|
|
91
|
+
lines.append(f"\nSummary: {', '.join(parts)}")
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
def format_context(self, max_tokens: int = 1000) -> str:
|
|
95
|
+
"""Render compressed violations for agent context injection."""
|
|
96
|
+
max_chars = max_tokens * 4
|
|
97
|
+
total = len(self.violations)
|
|
98
|
+
lines = [f"[Code Analysis] {total} violations found:"]
|
|
99
|
+
|
|
100
|
+
severity_key = {s: i for i, s in enumerate(_SEVERITY_ORDER)}
|
|
101
|
+
sorted_violations = sorted(
|
|
102
|
+
self.violations,
|
|
103
|
+
key=lambda v: (severity_key.get(v.severity, 99), v.file_path, v.line),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
char_count = len(lines[0])
|
|
107
|
+
for v in sorted_violations:
|
|
108
|
+
loc = f"{v.file_path}:{v.line}" if v.line > 0 else v.file_path
|
|
109
|
+
line = f"- {v.severity.upper()} {loc} {v.message}"
|
|
110
|
+
if char_count + len(line) + 1 > max_chars:
|
|
111
|
+
if v.severity not in ("critical", "high"):
|
|
112
|
+
break
|
|
113
|
+
lines.append(line)
|
|
114
|
+
char_count += len(line) + 1
|
|
115
|
+
|
|
116
|
+
return "\n".join(lines)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RuleRegistry:
|
|
120
|
+
"""Registry of analysis rules."""
|
|
121
|
+
|
|
122
|
+
def __init__(self) -> None:
|
|
123
|
+
self._rules: dict[str, Rule] = {}
|
|
124
|
+
|
|
125
|
+
def register(self, rule: Rule) -> None:
|
|
126
|
+
if rule.key in self._rules:
|
|
127
|
+
raise ValueError(f"Rule '{rule.key}' already registered")
|
|
128
|
+
self._rules[rule.key] = rule
|
|
129
|
+
|
|
130
|
+
def get(self, key: str) -> Rule | None:
|
|
131
|
+
return self._rules.get(key)
|
|
132
|
+
|
|
133
|
+
def all_rules(self) -> list[Rule]:
|
|
134
|
+
return list(self._rules.values())
|
|
135
|
+
|
|
136
|
+
def rules_for_language(self, language: str) -> list[Rule]:
|
|
137
|
+
return [
|
|
138
|
+
r for r in self._rules.values()
|
|
139
|
+
if "*" in r.languages or language in r.languages
|
|
140
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Rust 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
|
+
_UNWRAP_PATTERN = re.compile(r"\.(unwrap|expect)\s*\(")
|
|
10
|
+
_TODO_MACRO_PATTERN = re.compile(r"\b(todo|unimplemented)!\s*\(")
|
|
11
|
+
_UNSAFE_BLOCK_PATTERN = re.compile(r"\bunsafe\s*\{")
|
|
12
|
+
|
|
13
|
+
_TEST_DIR_NAMES = frozenset({"tests", "test"})
|
|
14
|
+
_TEST_SUFFIXES = ("_test.rs",)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_test_file(file_path: str) -> bool:
|
|
18
|
+
parts = PurePosixPath(file_path).parts
|
|
19
|
+
if any(p in _TEST_DIR_NAMES for p in parts):
|
|
20
|
+
return True
|
|
21
|
+
return any(file_path.endswith(s) for s in _TEST_SUFFIXES)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check_unwrap(file_path: str, content: str, tree: object = None) -> list[Violation]:
|
|
25
|
+
"""Detect .unwrap() and .expect() in non-test Rust files."""
|
|
26
|
+
if _is_test_file(file_path):
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
violations: list[Violation] = []
|
|
30
|
+
for match in _UNWRAP_PATTERN.finditer(content):
|
|
31
|
+
line = content[: match.start()].count("\n") + 1
|
|
32
|
+
method = match.group(1)
|
|
33
|
+
violations.append(
|
|
34
|
+
Violation(
|
|
35
|
+
rule_key="rust-unwrap",
|
|
36
|
+
severity="medium",
|
|
37
|
+
file_path=file_path,
|
|
38
|
+
line=line,
|
|
39
|
+
message=f".{method}() in production code — prefer ? or explicit error handling",
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return violations
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_todo_macro(file_path: str, content: str, tree: object = None) -> list[Violation]:
|
|
46
|
+
"""Detect todo!() and unimplemented!() macros."""
|
|
47
|
+
violations: list[Violation] = []
|
|
48
|
+
for match in _TODO_MACRO_PATTERN.finditer(content):
|
|
49
|
+
line = content[: match.start()].count("\n") + 1
|
|
50
|
+
macro = match.group(1)
|
|
51
|
+
violations.append(
|
|
52
|
+
Violation(
|
|
53
|
+
rule_key="rust-todo-macro",
|
|
54
|
+
severity="medium",
|
|
55
|
+
file_path=file_path,
|
|
56
|
+
line=line,
|
|
57
|
+
message=f"{macro}!() macro — incomplete implementation",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
return violations
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_unsafe_block(file_path: str, content: str, tree: object = None) -> list[Violation]:
|
|
64
|
+
"""Detect unsafe blocks for review."""
|
|
65
|
+
violations: list[Violation] = []
|
|
66
|
+
for match in _UNSAFE_BLOCK_PATTERN.finditer(content):
|
|
67
|
+
line = content[: match.start()].count("\n") + 1
|
|
68
|
+
violations.append(
|
|
69
|
+
Violation(
|
|
70
|
+
rule_key="rust-unsafe-block",
|
|
71
|
+
severity="high",
|
|
72
|
+
file_path=file_path,
|
|
73
|
+
line=line,
|
|
74
|
+
message="unsafe block — requires safety review",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
return violations
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def register_rust_rules(registry: RuleRegistry) -> None:
|
|
81
|
+
"""Register all Rust rules with the given registry."""
|
|
82
|
+
registry.register(
|
|
83
|
+
Rule(
|
|
84
|
+
key="rust-unwrap",
|
|
85
|
+
name=".unwrap() in production",
|
|
86
|
+
severity="medium",
|
|
87
|
+
languages=("rust",),
|
|
88
|
+
check=check_unwrap,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
registry.register(
|
|
92
|
+
Rule(
|
|
93
|
+
key="rust-todo-macro",
|
|
94
|
+
name="todo!/unimplemented! macro",
|
|
95
|
+
severity="medium",
|
|
96
|
+
languages=("rust",),
|
|
97
|
+
check=check_todo_macro,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
registry.register(
|
|
101
|
+
Rule(
|
|
102
|
+
key="rust-unsafe-block",
|
|
103
|
+
name="unsafe block",
|
|
104
|
+
severity="high",
|
|
105
|
+
languages=("rust",),
|
|
106
|
+
check=check_unsafe_block,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Universal code analysis rules — language-agnostic checks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from llm_code.analysis.rules import Rule, RuleRegistry, Violation
|
|
7
|
+
|
|
8
|
+
# Files to skip for hardcoded-secret detection
|
|
9
|
+
_SECRET_SKIP_SUFFIXES = (".md", ".txt")
|
|
10
|
+
_SECRET_SKIP_NAMES = {".env.example"}
|
|
11
|
+
|
|
12
|
+
# Regex patterns
|
|
13
|
+
_SECRET_PATTERN = re.compile(
|
|
14
|
+
r"(api[_\-]?key|secret|password|token)\s*[:=]\s*[\"'][a-zA-Z0-9]{16,}[\"']",
|
|
15
|
+
re.IGNORECASE,
|
|
16
|
+
)
|
|
17
|
+
_TODO_PATTERN = re.compile(r"(#|//)\s*(TODO|FIXME|HACK|XXX)\b")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_hardcoded_secret(file_path: str, content: str) -> list[Violation]:
|
|
21
|
+
"""Detect hardcoded secrets (API keys, passwords, tokens) in source code."""
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
basename = os.path.basename(file_path)
|
|
25
|
+
|
|
26
|
+
# Skip excluded file types
|
|
27
|
+
if basename in _SECRET_SKIP_NAMES:
|
|
28
|
+
return []
|
|
29
|
+
for suffix in _SECRET_SKIP_SUFFIXES:
|
|
30
|
+
if file_path.endswith(suffix):
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
violations: list[Violation] = []
|
|
34
|
+
for line_no, line in enumerate(content.splitlines(), start=1):
|
|
35
|
+
if _SECRET_PATTERN.search(line):
|
|
36
|
+
violations.append(
|
|
37
|
+
Violation(
|
|
38
|
+
rule_key="hardcoded-secret",
|
|
39
|
+
severity="critical",
|
|
40
|
+
file_path=file_path,
|
|
41
|
+
line=line_no,
|
|
42
|
+
message=f"Hardcoded secret detected on line {line_no}",
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return violations
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_todo_fixme(file_path: str, content: str) -> list[Violation]:
|
|
49
|
+
"""Detect TODO, FIXME, HACK, and XXX comment markers."""
|
|
50
|
+
violations: list[Violation] = []
|
|
51
|
+
for line_no, line in enumerate(content.splitlines(), start=1):
|
|
52
|
+
m = _TODO_PATTERN.search(line)
|
|
53
|
+
if m:
|
|
54
|
+
matched_keyword = m.group(2)
|
|
55
|
+
violations.append(
|
|
56
|
+
Violation(
|
|
57
|
+
rule_key="todo-fixme",
|
|
58
|
+
severity="low",
|
|
59
|
+
file_path=file_path,
|
|
60
|
+
line=line_no,
|
|
61
|
+
message=f"{matched_keyword} comment found",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
return violations
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_god_module(file_path: str, content: str) -> list[Violation]:
|
|
68
|
+
"""Detect files exceeding 800 lines (god modules)."""
|
|
69
|
+
line_count = len(content.splitlines())
|
|
70
|
+
if line_count > 800:
|
|
71
|
+
return [
|
|
72
|
+
Violation(
|
|
73
|
+
rule_key="god-module",
|
|
74
|
+
severity="medium",
|
|
75
|
+
file_path=file_path,
|
|
76
|
+
line=0,
|
|
77
|
+
message=f"File has {line_count} lines (limit: 800)",
|
|
78
|
+
)
|
|
79
|
+
]
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register_universal_rules(registry: RuleRegistry) -> None:
|
|
84
|
+
"""Register all universal rules into the given registry."""
|
|
85
|
+
registry.register(
|
|
86
|
+
Rule(
|
|
87
|
+
key="hardcoded-secret",
|
|
88
|
+
name="Hardcoded Secret",
|
|
89
|
+
severity="critical",
|
|
90
|
+
languages=("*",),
|
|
91
|
+
check=check_hardcoded_secret,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
registry.register(
|
|
95
|
+
Rule(
|
|
96
|
+
key="todo-fixme",
|
|
97
|
+
name="TODO / FIXME Comment",
|
|
98
|
+
severity="low",
|
|
99
|
+
languages=("*",),
|
|
100
|
+
check=check_todo_fixme,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
registry.register(
|
|
104
|
+
Rule(
|
|
105
|
+
key="god-module",
|
|
106
|
+
name="God Module",
|
|
107
|
+
severity="medium",
|
|
108
|
+
languages=("*",),
|
|
109
|
+
check=check_god_module,
|
|
110
|
+
)
|
|
111
|
+
)
|
llm_code/api/__init__.py
ADDED
|
File without changes
|