@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
JavaScript
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