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
@@ -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
+ )
File without changes