Coverage for mcp_bridge/tools/code_search.py: 5%

1"""

2 LSP Tools - Language Server Protocol Operations

3

4 These tools provide LSP functionality for Claude Code via subprocess calls

5 to language servers. Claude Code has native LSP support, so these serve as

6 supplementary utilities for advanced operations.

7"""

8

9 import json

10 import subprocess

11 from pathlib import Path

12

13

14 async 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

76 async 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

138 async 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

199 async 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

248 async 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

293 async 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