Coverage for mcp_bridge/tools/code_search.py: 5%
170 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-01-10 00:20 -0500
« prev ^ index » next coverage.py v7.10.1, created at 2026-01-10 00:20 -0500
1"""
2LSP Tools - Language Server Protocol Operations
4These tools provide LSP functionality for Claude Code via subprocess calls
5to language servers. Claude Code has native LSP support, so these serve as
6supplementary utilities for advanced operations.
7"""
9import json
10import subprocess
11from pathlib import Path
14async def lsp_diagnostics(file_path: str, severity: str = "all") -> str:
15 """
16 Get diagnostics (errors, warnings) for a file using language server.
18 For TypeScript/JavaScript, uses `tsc` or `biome`.
19 For Python, uses `pyright` or `ruff`.
21 Args:
22 file_path: Path to the file to analyze
23 severity: Filter by severity (error, warning, information, hint, all)
25 Returns:
26 Formatted diagnostics output.
27 """
28 # USER-VISIBLE NOTIFICATION
29 import sys
30 print(f"🩺 LSP-DIAG: file={file_path} severity={severity}", file=sys.stderr)
32 path = Path(file_path)
33 if not path.exists():
34 return f"Error: File not found: {file_path}"
36 suffix = path.suffix.lower()
38 try:
39 if suffix in (".ts", ".tsx", ".js", ".jsx"):
40 # Use TypeScript compiler for diagnostics
41 result = subprocess.run(
42 ["npx", "tsc", "--noEmit", "--pretty", str(path)],
43 capture_output=True,
44 text=True,
45 timeout=30,
46 )
47 output = result.stdout + result.stderr
48 if not output.strip():
49 return "No diagnostics found"
50 return output
52 elif suffix == ".py":
53 # Use ruff for Python diagnostics
54 result = subprocess.run(
55 ["ruff", "check", str(path), "--output-format=concise"],
56 capture_output=True,
57 text=True,
58 timeout=30,
59 )
60 output = result.stdout + result.stderr
61 if not output.strip():
62 return "No diagnostics found"
63 return output
65 else:
66 return f"No diagnostics available for file type: {suffix}"
68 except FileNotFoundError as e:
69 return f"Tool not found: {e.filename}. Install required tools."
70 except subprocess.TimeoutExpired:
71 return "Diagnostics timed out"
72 except Exception as e:
73 return f"Error: {str(e)}"
76async def check_ai_comment_patterns(file_path: str) -> str:
77 """
78 Detect AI-generated or placeholder comment patterns that indicate incomplete work.
80 Patterns detected:
81 - # TODO: implement, # FIXME, # placeholder
82 - // TODO, // FIXME, // placeholder
83 - AI-style verbose comments: "This function handles...", "This method is responsible for..."
84 - Placeholder phrases: "implement this", "add logic here", "your code here"
86 Args:
87 file_path: Path to the file to check
89 Returns:
90 List of detected AI-style patterns with line numbers, or "No AI patterns detected"
91 """
92 # USER-VISIBLE NOTIFICATION
93 import sys
94 print(f"🤖 AI-CHECK: {file_path}", file=sys.stderr)
96 path = Path(file_path)
97 if not path.exists():
98 return f"Error: File not found: {file_path}"
100 # Patterns that indicate AI-generated or placeholder code
101 ai_patterns = [
102 # Placeholder comments
103 r"#\s*(TODO|FIXME|XXX|HACK):\s*(implement|add|placeholder|your code)",
104 r"//\s*(TODO|FIXME|XXX|HACK):\s*(implement|add|placeholder|your code)",
105 # AI-style verbose descriptions
106 r"#\s*This (function|method|class) (handles|is responsible for|manages|processes)",
107 r"//\s*This (function|method|class) (handles|is responsible for|manages|processes)",
108 r'"""This (function|method|class) (handles|is responsible for|manages|processes)',
109 # Placeholder implementations
110 r"pass\s*#\s*(TODO|implement|placeholder)",
111 r"raise NotImplementedError.*implement",
112 # Common AI filler phrases
113 r"#.*\b(as needed|as required|as appropriate|if necessary)\b",
114 r"//.*\b(as needed|as required|as appropriate|if necessary)\b",
115 ]
117 import re
119 try:
120 content = path.read_text()
121 lines = content.split("\n")
122 findings = []
124 for i, line in enumerate(lines, 1):
125 for pattern in ai_patterns:
126 if re.search(pattern, line, re.IGNORECASE):
127 findings.append(f" Line {i}: {line.strip()[:80]}")
128 break
130 if findings:
131 return f"AI/Placeholder patterns detected in {file_path}:\n" + "\n".join(findings)
132 return "No AI patterns detected"
134 except Exception as e:
135 return f"Error reading file: {str(e)}"
138async def ast_grep_search(pattern: str, directory: str = ".", language: str = "") -> str:
139 """
140 Search codebase using ast-grep for structural patterns.
142 ast-grep uses AST-aware pattern matching, finding code by structure
143 rather than just text. More precise than regex for code search.
145 Args:
146 pattern: ast-grep pattern to search for
147 directory: Directory to search in
148 language: Filter by language (typescript, python, rust, etc.)
150 Returns:
151 Matched code locations and snippets.
152 """
153 # USER-VISIBLE NOTIFICATION
154 import sys
155 lang_info = f" lang={language}" if language else ""
156 print(f"🔍 AST-GREP: pattern='{pattern[:50]}...'{lang_info}", file=sys.stderr)
158 try:
159 cmd = ["sg", "run", "-p", pattern, directory]
160 if language:
161 cmd.extend(["--lang", language])
162 cmd.append("--json")
164 result = subprocess.run(
165 cmd,
166 capture_output=True,
167 text=True,
168 timeout=60,
169 )
171 if result.returncode != 0 and not result.stdout:
172 return result.stderr or "No matches found"
174 # Parse and format JSON output
175 try:
176 matches = json.loads(result.stdout)
177 if not matches:
178 return "No matches found"
180 lines = []
181 for match in matches[:20]: # Limit to 20 results
182 file_path = match.get("file", "unknown")
183 start_line = match.get("range", {}).get("start", {}).get("line", 0)
184 text = match.get("text", "")
185 lines.append(f"{file_path}:{start_line}: {text[:100]}")
187 return "\n".join(lines)
188 except json.JSONDecodeError:
189 return result.stdout or "No matches found"
191 except FileNotFoundError:
192 return "ast-grep (sg) not found. Install with: npm install -g @ast-grep/cli"
193 except subprocess.TimeoutExpired:
194 return "Search timed out"
195 except Exception as e:
196 return f"Error: {str(e)}"
199async def grep_search(pattern: str, directory: str = ".", file_pattern: str = "") -> str:
200 """
201 Fast text search using ripgrep.
203 Args:
204 pattern: Search pattern (supports regex)
205 directory: Directory to search in
206 file_pattern: Glob pattern to filter files (e.g., "*.py", "*.ts")
208 Returns:
209 Matched lines with file paths and line numbers.
210 """
211 # USER-VISIBLE NOTIFICATION
212 import sys
213 glob_info = f" glob={file_pattern}" if file_pattern else ""
214 print(f"🔎 GREP: pattern='{pattern[:50]}'{glob_info} dir={directory}", file=sys.stderr)
216 try:
217 cmd = ["rg", "--line-number", "--max-count=50", pattern, directory]
218 if file_pattern:
219 cmd.extend(["--glob", file_pattern])
221 result = subprocess.run(
222 cmd,
223 capture_output=True,
224 text=True,
225 timeout=30,
226 )
228 output = result.stdout
229 if not output.strip():
230 return "No matches found"
232 # Limit output lines
233 lines = output.strip().split("\n")
234 if len(lines) > 50:
235 lines = lines[:50]
236 lines.append("... and more (showing first 50 matches)")
238 return "\n".join(lines)
240 except FileNotFoundError:
241 return "ripgrep (rg) not found. Install with: brew install ripgrep"
242 except subprocess.TimeoutExpired:
243 return "Search timed out"
244 except Exception as e:
245 return f"Error: {str(e)}"
248async def glob_files(pattern: str, directory: str = ".") -> str:
249 """
250 Find files matching a glob pattern.
252 Args:
253 pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts")
254 directory: Base directory for search
256 Returns:
257 List of matching file paths.
258 """
259 # USER-VISIBLE NOTIFICATION
260 import sys
261 print(f"📁 GLOB: pattern='{pattern}' dir={directory}", file=sys.stderr)
263 try:
264 cmd = ["fd", "--type", "f", "--glob", pattern, directory]
266 result = subprocess.run(
267 cmd,
268 capture_output=True,
269 text=True,
270 timeout=30,
271 )
273 output = result.stdout
274 if not output.strip():
275 return "No files found"
277 # Limit output
278 lines = output.strip().split("\n")
279 if len(lines) > 100:
280 lines = lines[:100]
281 lines.append(f"... and {len(lines) - 100} more files")
283 return "\n".join(lines)
285 except FileNotFoundError:
286 return "fd not found. Install with: brew install fd"
287 except subprocess.TimeoutExpired:
288 return "Search timed out"
289 except Exception as e:
290 return f"Error: {str(e)}"
293async def ast_grep_replace(
294 pattern: str,
295 replacement: str,
296 directory: str = ".",
297 language: str = "",
298 dry_run: bool = True
299) -> str:
300 """
301 Replace code patterns using ast-grep's AST-aware replacement.
303 ast-grep uses structural pattern matching for precise code transformations.
304 More reliable than text-based search/replace for refactoring.
306 Args:
307 pattern: ast-grep pattern to search for (e.g., "console.log($A)")
308 replacement: Replacement pattern (e.g., "logger.debug($A)")
309 directory: Directory to search in
310 language: Filter by language (typescript, python, rust, etc.)
311 dry_run: If True (default), only show what would change without applying
313 Returns:
314 Preview of changes or confirmation of applied changes.
315 """
316 # USER-VISIBLE NOTIFICATION
317 import sys
318 mode = "dry-run" if dry_run else "APPLY"
319 lang_info = f" lang={language}" if language else ""
320 print(f"🔄 AST-REPLACE: '{pattern[:30]}' → '{replacement[:30]}'{lang_info} [{mode}]", file=sys.stderr)
322 try:
323 # Build command
324 cmd = ["sg", "run", "-p", pattern, "-r", replacement, directory]
325 if language:
326 cmd.extend(["--lang", language])
328 if dry_run:
329 # Show what would change
330 cmd.append("--json")
331 result = subprocess.run(
332 cmd,
333 capture_output=True,
334 text=True,
335 timeout=60,
336 )
338 if result.returncode != 0 and not result.stdout:
339 return result.stderr or "No matches found"
341 try:
342 matches = json.loads(result.stdout)
343 if not matches:
344 return "No matches found for pattern"
346 lines = [f"**Dry run** - {len(matches)} matches found:"]
347 for match in matches[:15]:
348 file_path = match.get("file", "unknown")
349 start_line = match.get("range", {}).get("start", {}).get("line", 0)
350 original = match.get("text", "")[:80]
351 lines.append(f"\n`{file_path}:{start_line}`")
352 lines.append(f"```\n{original}\n```")
354 if len(matches) > 15:
355 lines.append(f"\n... and {len(matches) - 15} more matches")
357 lines.append("\n**To apply changes**, call with `dry_run=False`")
358 return "\n".join(lines)
360 except json.JSONDecodeError:
361 return result.stdout or "No matches found"
362 else:
363 # Actually apply the changes
364 cmd_apply = ["sg", "run", "-p", pattern, "-r", replacement, directory, "--update-all"]
365 if language:
366 cmd_apply.extend(["--lang", language])
368 result = subprocess.run(
369 cmd_apply,
370 capture_output=True,
371 text=True,
372 timeout=60,
373 )
375 if result.returncode == 0:
376 return f"✅ Successfully applied replacement:\n- Pattern: `{pattern}`\n- Replacement: `{replacement}`\n\n{result.stdout}"
377 else:
378 return f"❌ Failed to apply replacement:\n{result.stderr}"
380 except FileNotFoundError:
381 return "ast-grep (sg) not found. Install with: npm install -g @ast-grep/cli"
382 except subprocess.TimeoutExpired:
383 return "Replacement timed out"
384 except Exception as e:
385 return f"Error: {str(e)}"