UNPKG

@felixgeelhaar/cclint

Version:

Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.

148 lines 5.48 kB
import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, isAbsolute, join, relative } from 'path'; import { LintingResult } from '../../domain/LintingResult.js'; import { Severity } from '../../domain/Severity.js'; import { getRuleMetadata } from '../../infrastructure/RuleMetadata.js'; /** * Canonical SARIF 2.1.0 schema URL. GitHub code scanning validates uploads * against this schema, so keep it stable. */ const SARIF_SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json'; const SARIF_VERSION = '2.1.0'; const FALLBACK_INFORMATION_URI = 'https://github.com/felixgeelhaar/cclint'; /** * Convert a cclint {@link LintingResult} into a SARIF 2.1.0 document. * * Output is deterministic: rules are sorted by id and results preserve the * (already deterministic) violation order, so snapshot tests are reliable. */ export function formatSarifResult(result, options = {}) { return formatSarifResults([result], options); } /** * Convert one or more cclint {@link LintingResult}s into a single SARIF 2.1.0 * document. SARIF represents multiple files natively — each result carries its * own artifact URI — so this is how project-wide (`cclint lint <dir>`) output * is emitted. The single-file {@link formatSarifResult} is a one-element case * of this and produces byte-identical output. * * Output is deterministic: rule descriptors are sorted by id and results * preserve file order then per-file violation order. */ export function formatSarifResults(results, options = {}) { const pkg = readPackageMetadata(); const version = options.toolVersion ?? pkg.version; const informationUri = options.informationUri ?? pkg.informationUri; const sarifResults = results.flatMap(result => { const uri = toSarifUri(result.file.path); return result.violations.map(violation => ({ ruleId: violation.ruleId, level: toSarifLevel(violation.severity), message: { text: violation.message }, locations: [ { physicalLocation: { artifactLocation: { uri }, region: { startLine: violation.location.line, // SARIF columns are 1-based; a 0 column means "whole line". startColumn: Math.max(1, violation.location.column), }, }, }, ], })); }); const rules = buildRules(results); const log = { $schema: SARIF_SCHEMA, version: SARIF_VERSION, runs: [ { tool: { driver: { name: 'cclint', informationUri, version, rules, }, }, results: sarifResults, }, ], }; return JSON.stringify(log, null, 2); } /** * Map a domain severity to a SARIF level. `error`/`warning` map directly; * anything else (info, suggestion) becomes `note`. */ function toSarifLevel(severity) { if (severity === Severity.ERROR) return 'error'; if (severity === Severity.WARNING) return 'warning'; return 'note'; } /** * Build the deduplicated, alphabetically-sorted rule descriptors for every * rule id that produced a result across all files, each with a helpful * shortDescription. */ function buildRules(results) { const ids = new Set(); for (const result of results) { for (const violation of result.violations) { ids.add(violation.ruleId); } } return [...ids].sort().map(id => { const metadata = getRuleMetadata(id); return { id, name: metadata?.name ?? id, shortDescription: { text: metadata?.description ?? `Violations reported by rule "${id}"`, }, }; }); } /** * Normalize a file path into a SARIF artifact URI: relative to the current * working directory (when absolute) and using forward slashes. */ function toSarifUri(filePath) { const relativePath = isAbsolute(filePath) ? relative(process.cwd(), filePath) : filePath; return relativePath.split('\\').join('/'); } let cachedPackageMetadata = null; /** * Read the tool version and homepage from package.json. Resolves relative to * this module so it works from both `src` (tsx) and the compiled `dist` tree. */ function readPackageMetadata() { if (cachedPackageMetadata) { return cachedPackageMetadata; } let version = '0.0.0'; let informationUri = FALLBACK_INFORMATION_URI; try { const here = dirname(fileURLToPath(import.meta.url)); // src/cli/formatters -> repo root, and dist/cli/formatters -> repo root. const pkgPath = join(here, '..', '..', '..', 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); if (typeof pkg.version === 'string') version = pkg.version; if (typeof pkg.homepage === 'string') informationUri = pkg.homepage; } catch { // Fall back to defaults when package.json cannot be read. } cachedPackageMetadata = { version, informationUri }; return cachedPackageMetadata; } //# sourceMappingURL=sarifFormatter.js.map