///
/**
* `estiva-gates name-escapes`: write into every escape marker the rules it keeps
* off, once (Katerina's ruling B3, 25 September).
*
* A marker used to keep off every rule on its element; now it keeps off only the
* rules it names, `// @estiva-escape(no-raw-element): `. So that nothing
* that was escaped breaks, this asks the gate itself which rules each unnamed
* marker stands for: linted with the new reader, an unnamed marker is reported
* once per rule that finds its element ("Name the rule … `@estiva-escape()`"),
* and those names are written into the marker. Nothing is guessed, and a marker
* no rule reaches is left as it is and listed.
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { join, relative } from 'node:path'
import { pathToFileURL } from 'node:url'
import type { ESLint as ESLintClass } from 'eslint'
export interface NameEscapesOptions {
/** The folder the gate lints from. */
root?: string
/** The gate's config file, relative to `root`. */
config?: string
}
export interface NameEscapesResult {
/** One line per marker rewritten: `file:line rule, rule`. */
named: string[]
/** Markers no rule reached, left unnamed. */
unreached: string[]
}
const UNNAMED = /@estiva-escape(?!\s*\()/
const NAMED_IN_MESSAGE = /@estiva-escape\(([^)]+)\)/
export async function nameEscapes({ root = process.cwd(), config = 'eslint.gates.config.js' }: NameEscapesOptions = {}): Promise {
const resolved = createRequire(join(root, 'package.json')).resolve('eslint')
const { ESLint } = (await import(pathToFileURL(resolved).href)) as { ESLint: typeof ESLintClass }
const eslint = new ESLint({ cwd: root, overrideConfigFile: join(root, config) })
const results = await eslint.lintFiles(['.'])
const named: string[] = []
const unreached: string[] = []
for (const result of results) {
// The rules each unnamed marker stands for, by the line its comment starts on.
const byLine = new Map>()
for (const m of result.messages) {
if (m.messageId !== 'escapeWithoutRule') continue
const rule = NAMED_IN_MESSAGE.exec(m.message)?.[1]
if (!rule) continue
byLine.set(m.line, (byLine.get(m.line) ?? new Set()).add(rule))
}
const source = result.source ?? readFileSync(result.filePath, 'utf8')
const eol = source.includes('\r\n') ? '\r\n' : '\n'
const lines = source.split(/\r?\n/)
let changed = false
const file = relative(root, result.filePath).split('\\').join('/')
for (const [line, rules] of byLine) {
// The marker is on the comment's first line, or within the next few (a block comment).
for (let i = line - 1; i < Math.min(lines.length, line + 3); i++) {
if (!UNNAMED.test(lines[i])) continue
const names = [...rules].sort().join(', ')
lines[i] = lines[i].replace(UNNAMED, `@estiva-escape(${names})`)
named.push(`${file}:${i + 1} ${names}`)
changed = true
break
}
}
if (changed) writeFileSync(result.filePath, lines.join(eol), 'utf8')
// Unnamed markers the gate did not report: nothing of the gate's reaches that element.
lines.forEach((text, i) => {
if (UNNAMED.test(text) && !/eslint-disable/.test(text) && /\/\/|\/\*/.test(text)) unreached.push(`${file}:${i + 1}`)
})
}
return { named, unreached }
}