utils.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { groupByFile } from "../../shared"
  2. import type { SgResult, CliLanguage } from "./types"
  3. /**
  4. * Formats an ast-grep search result into a human-readable summary.
  5. * @param result - The SgResult containing matches or error.
  6. * @returns A formatted string summary.
  7. */
  8. export function formatSearchResult(result: SgResult): string {
  9. if (result.error) {
  10. return `Error: ${result.error}`
  11. }
  12. if (result.matches.length === 0) {
  13. return "No matches found."
  14. }
  15. const lines: string[] = []
  16. // Group matches by file
  17. const byFile = groupByFile(result.matches)
  18. for (const [file, matches] of byFile) {
  19. lines.push(`\n${file}:`)
  20. for (const match of matches) {
  21. const startLine = match.range.start.line + 1
  22. const text = match.text.length > 100 ? match.text.substring(0, 100) + "..." : match.text
  23. lines.push(` ${startLine}: ${text.replace(/\n/g, "\\n")}`)
  24. }
  25. }
  26. const fileCount = byFile.size
  27. const summary = `Found ${result.totalMatches} matches in ${fileCount} files`
  28. if (result.truncated) {
  29. lines.push(`\n${summary} (output truncated: ${result.truncatedReason})`)
  30. } else {
  31. lines.push(`\n${summary}`)
  32. }
  33. return lines.join("\n")
  34. }
  35. /**
  36. * Formats an ast-grep replacement result into a human-readable summary.
  37. * @param result - The SgResult containing matches/replacements.
  38. * @param isDryRun - Whether this was a dry run.
  39. * @returns A formatted string summary.
  40. */
  41. export function formatReplaceResult(result: SgResult, isDryRun: boolean): string {
  42. if (result.error) {
  43. return `Error: ${result.error}`
  44. }
  45. if (result.matches.length === 0) {
  46. return "No matches found for replacement."
  47. }
  48. const lines: string[] = []
  49. const mode = isDryRun ? "[DRY RUN]" : "[APPLIED]"
  50. // Group by file
  51. const byFile = new Map<string, typeof result.matches>()
  52. for (const match of result.matches) {
  53. const existing = byFile.get(match.file) || []
  54. existing.push(match)
  55. byFile.set(match.file, existing)
  56. }
  57. for (const [file, matches] of byFile) {
  58. lines.push(`\n${file}:`)
  59. for (const match of matches) {
  60. const startLine = match.range.start.line + 1
  61. const original = match.text.length > 60 ? match.text.substring(0, 60) + "..." : match.text
  62. const replacement = match.replacement
  63. ? match.replacement.length > 60
  64. ? match.replacement.substring(0, 60) + "..."
  65. : match.replacement
  66. : "[no replacement]"
  67. lines.push(` ${startLine}: "${original.replace(/\n/g, "\\n")}" → "${replacement.replace(/\n/g, "\\n")}"`)
  68. }
  69. }
  70. const fileCount = byFile.size
  71. lines.push(`\n${mode} ${result.totalMatches} replacements in ${fileCount} files`)
  72. if (isDryRun) {
  73. lines.push("\nTo apply changes, run with dryRun=false")
  74. }
  75. return lines.join("\n")
  76. }
  77. /**
  78. * Provides helpful hints for common mistakes when an ast-grep search returns no results.
  79. * @param pattern - The pattern that was searched.
  80. * @param lang - The language used for the search.
  81. * @returns A hint string if a common mistake is detected, otherwise null.
  82. */
  83. export function getEmptyResultHint(pattern: string, lang: CliLanguage): string | null {
  84. const src = pattern.trim()
  85. if (lang === "python") {
  86. if (src.startsWith("class ") && src.endsWith(":")) {
  87. const withoutColon = src.slice(0, -1)
  88. return `Hint: Remove trailing colon. Try: "${withoutColon}"`
  89. }
  90. if ((src.startsWith("def ") || src.startsWith("async def ")) && src.endsWith(":")) {
  91. const withoutColon = src.slice(0, -1)
  92. return `Hint: Remove trailing colon. Try: "${withoutColon}"`
  93. }
  94. }
  95. if (["javascript", "typescript", "tsx"].includes(lang)) {
  96. if (/^(export\s+)?(async\s+)?function\s+\$[A-Z_]+\s*$/i.test(src)) {
  97. return `Hint: Function patterns need params and body. Try "function $NAME($$$) { $$$ }"`
  98. }
  99. }
  100. return null
  101. }