61 lines
2.6 KiB
JavaScript
61 lines
2.6 KiB
JavaScript
// ast-grep CLI wrapper.
|
|
import { execFile } from 'node:child_process'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
let sgPath
|
|
export function findAstGrep(extra = []) {
|
|
if (sgPath !== undefined) return sgPath
|
|
const dirs = [...extra, ...(process.env.PATH || '').split(path.delimiter), path.join(process.env.HOME || '', '.local/bin'), '/usr/local/bin']
|
|
sgPath = null
|
|
for (const d of dirs) {
|
|
if (!d) continue
|
|
const p = path.join(d, 'ast-grep') // `sg` collides with util-linux's setgroups on many distros
|
|
try { fs.accessSync(p, fs.constants.X_OK); sgPath = p; break } catch {}
|
|
}
|
|
return sgPath
|
|
}
|
|
|
|
export function run(bin, args, { cwd, signal, timeoutMs = 120_000, maxBuffer = 64 * 1024 * 1024 } = {}) {
|
|
return new Promise((resolve) => {
|
|
execFile(bin, args, { cwd, signal, timeout: timeoutMs, maxBuffer, encoding: 'utf8' }, (error, stdout, stderr) => {
|
|
resolve({ code: error ? (typeof error.code === 'number' ? error.code : 1) : 0, stdout: stdout ?? '', stderr: stderr ?? '', error })
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Run a pattern (optionally with rewrite) and return parsed matches.
|
|
* @returns {{matches: object[], warnings: string[]}}
|
|
*/
|
|
export async function search({ bin, pattern, rewrite, lang, paths, cwd, globs, strictness, selector, signal }) {
|
|
const args = ['run', '--pattern', pattern, '--json=compact', '--color=never']
|
|
if (rewrite !== undefined) args.push('--rewrite', rewrite)
|
|
if (lang) args.push('--lang', lang)
|
|
if (strictness) args.push('--strictness', strictness)
|
|
if (selector) args.push('--selector', selector)
|
|
for (const g of globs ?? []) args.push('--globs', g)
|
|
args.push('--', ...(paths?.length ? paths : ['.']))
|
|
const r = await run(bin, args, { cwd, signal })
|
|
const warnings = r.stderr.split('\n').map((l) => l.trim()).filter((l) => l && !/^See also|^Help: .*playground/i.test(l))
|
|
if (r.code !== 0 && !r.stdout.trim().startsWith('[')) {
|
|
throw new Error(`ast-grep failed: ${warnings.join(' ') || r.error?.message || `exit ${r.code}`}`)
|
|
}
|
|
let matches = []
|
|
try { matches = JSON.parse(r.stdout || '[]') } catch (e) { throw new Error(`ast-grep returned malformed JSON: ${e.message}`) }
|
|
return { matches, warnings }
|
|
}
|
|
|
|
/** Apply replacements (byte offsets) to file contents; returns new text. */
|
|
export function applyReplacements(buf, reps) {
|
|
const sorted = [...reps].sort((a, b) => b.start - a.start)
|
|
let out = buf
|
|
let lastStart = Infinity
|
|
for (const r of sorted) {
|
|
if (r.end > lastStart) continue // overlapping: keep the outermost-first earlier one
|
|
out = Buffer.concat([out.subarray(0, r.start), Buffer.from(r.text, 'utf8'), out.subarray(r.end)])
|
|
lastStart = r.start
|
|
}
|
|
return out
|
|
}
|