import type { RuleOwner } from './rules/catalog.js' export type DiagnosticSeverity = 'error' | 'warning' export type DiagnosticLocation = { path: string line: number column: number offset?: number length?: number } export type LintDiagnostic = { id: string severity: DiagnosticSeverity message: string help?: string url?: string owner: RuleOwner | 'generic' location?: DiagnosticLocation } export type LintResult = { root: string diagnostics: LintDiagnostic[] files: number durationMs: number exitCode: 0 | 1 } export type LinterToolErrorCode = | 'LINTER_PREFLIGHT_FAILED' | 'LINTER_TOOL_FAILED' | 'OXLINT_CANCELLED' | 'OXLINT_FAILED' | 'OXLINT_INVALID_OUTPUT' | 'OXLINT_NOT_INSTALLED' | 'OXLINT_OUTPUT_LIMIT' | 'OXLINT_START_FAILED' | 'OXLINT_TIMEOUT' type LinterToolErrorOptions = ErrorOptions & { code?: LinterToolErrorCode } export class LinterToolError extends Error { readonly exitCode = 2 readonly code: LinterToolErrorCode constructor(message: string, options: LinterToolErrorOptions = {}) { super(message, options) this.name = 'LinterToolError' this.code = options.code ?? 'LINTER_TOOL_FAILED' } } export function compareDiagnostics(a: LintDiagnostic, b: LintDiagnostic): number { return ( (a.location?.path ?? '').localeCompare(b.location?.path ?? '') || (a.location?.line ?? 0) - (b.location?.line ?? 0) || (a.location?.column ?? 0) - (b.location?.column ?? 0) || a.id.localeCompare(b.id) || a.message.localeCompare(b.message) ) } export function dedupeDiagnostics(diagnostics: readonly LintDiagnostic[]): LintDiagnostic[] { const seen = new Set() const result: LintDiagnostic[] = [] for (const diagnostic of diagnostics) { const location = diagnostic.location const key = [ diagnostic.id, location?.path ?? '', location?.line ?? 0, location?.column ?? 0, location?.length ?? 0, diagnostic.message, ].join(':') if (seen.has(key)) continue seen.add(key) result.push(diagnostic) } return result.sort(compareDiagnostics) }