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

1""" 

2LSP Tools - Language Server Protocol Operations 

3 

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""" 

8 

9import json 

10import subprocess 

11from pathlib import Path 

12 

13 

14async def lsp_diagnostics(file_path: str, severity: str = "all") -> str: 

15 """ 

16 Get diagnostics (errors, warnings) for a file using language server. 

17 

18 For TypeScript/JavaScript, uses `tsc` or `biome`. 

19 For Python, uses `pyright` or `ruff`. 

20 

21 Args: 

22 file_path: Path to the file to analyze 

23 severity: Filter by severity (error, warning, information, hint, all) 

24 

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) 

31 

32 path = Path(file_path) 

33 if not path.exists(): 

34 return f"Error: File not found: {file_path}" 

35 

36 suffix = path.suffix.lower() 

37 

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 

51 

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 

64 

65 else: 

66 return f"No diagnostics available for file type: {suffix}" 

67 

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)}" 

74 

75 

76async def check_ai_comment_patterns(file_path: str) -> str: 

77 """ 

78 Detect AI-generated or placeholder comment patterns that indicate incomplete work. 

79 

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" 

85 

86 Args: 

87 file_path: Path to the file to check 

88 

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) 

95 

96 path = Path(file_path) 

97 if not path.exists(): 

98 return f"Error: File not found: {file_path}" 

99 

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 ] 

116 

117 import re 

118 

119 try: 

120 content = path.read_text() 

121 lines = content.split("\n") 

122 findings = [] 

123 

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 

129 

130 if findings: 

131 return f"AI/Placeholder patterns detected in {file_path}:\n" + "\n".join(findings) 

132 return "No AI patterns detected" 

133 

134 except Exception as e: 

135 return f"Error reading file: {str(e)}" 

136 

137 

138async def ast_grep_search(pattern: str, directory: str = ".", language: str = "") -> str: 

139 """ 

140 Search codebase using ast-grep for structural patterns. 

141 

142 ast-grep uses AST-aware pattern matching, finding code by structure 

143 rather than just text. More precise than regex for code search. 

144 

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.) 

149 

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) 

157 

158 try: 

159 cmd = ["sg", "run", "-p", pattern, directory] 

160 if language: 

161 cmd.extend(["--lang", language]) 

162 cmd.append("--json") 

163 

164 result = subprocess.run( 

165 cmd, 

166 capture_output=True, 

167 text=True, 

168 timeout=60, 

169 ) 

170 

171 if result.returncode != 0 and not result.stdout: 

172 return result.stderr or "No matches found" 

173 

174 # Parse and format JSON output 

175 try: 

176 matches = json.loads(result.stdout) 

177 if not matches: 

178 return "No matches found" 

179 

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]}") 

186 

187 return "\n".join(lines) 

188 except json.JSONDecodeError: 

189 return result.stdout or "No matches found" 

190 

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)}" 

197 

198 

199async def grep_search(pattern: str, directory: str = ".", file_pattern: str = "") -> str: 

200 """ 

201 Fast text search using ripgrep. 

202 

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") 

207 

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) 

215 

216 try: 

217 cmd = ["rg", "--line-number", "--max-count=50", pattern, directory] 

218 if file_pattern: 

219 cmd.extend(["--glob", file_pattern]) 

220 

221 result = subprocess.run( 

222 cmd, 

223 capture_output=True, 

224 text=True, 

225 timeout=30, 

226 ) 

227 

228 output = result.stdout 

229 if not output.strip(): 

230 return "No matches found" 

231 

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)") 

237 

238 return "\n".join(lines) 

239 

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)}" 

246 

247 

248async def glob_files(pattern: str, directory: str = ".") -> str: 

249 """ 

250 Find files matching a glob pattern. 

251 

252 Args: 

253 pattern: Glob pattern (e.g., "**/*.py", "src/**/*.ts") 

254 directory: Base directory for search 

255 

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) 

262 

263 try: 

264 cmd = ["fd", "--type", "f", "--glob", pattern, directory] 

265 

266 result = subprocess.run( 

267 cmd, 

268 capture_output=True, 

269 text=True, 

270 timeout=30, 

271 ) 

272 

273 output = result.stdout 

274 if not output.strip(): 

275 return "No files found" 

276 

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") 

282 

283 return "\n".join(lines) 

284 

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)}" 

291 

292 

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. 

302  

303 ast-grep uses structural pattern matching for precise code transformations. 

304 More reliable than text-based search/replace for refactoring. 

305  

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 

312  

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) 

321 

322 try: 

323 # Build command 

324 cmd = ["sg", "run", "-p", pattern, "-r", replacement, directory] 

325 if language: 

326 cmd.extend(["--lang", language]) 

327 

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 ) 

337 

338 if result.returncode != 0 and not result.stdout: 

339 return result.stderr or "No matches found" 

340 

341 try: 

342 matches = json.loads(result.stdout) 

343 if not matches: 

344 return "No matches found for pattern" 

345 

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```") 

353 

354 if len(matches) > 15: 

355 lines.append(f"\n... and {len(matches) - 15} more matches") 

356 

357 lines.append("\n**To apply changes**, call with `dry_run=False`") 

358 return "\n".join(lines) 

359 

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]) 

367 

368 result = subprocess.run( 

369 cmd_apply, 

370 capture_output=True, 

371 text=True, 

372 timeout=60, 

373 ) 

374 

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}" 

379 

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)}" 

386