Files
dss/packages/dss-rules/lib/validate.js
2025-12-11 08:03:47 -03:00

267 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
/**
* DSS Rules Validator
*
* CLI tool for validating files against DSS rules.
* Used by CI pipelines and pre-commit hooks.
*/
const fs = require('fs');
const path = require('path');
const rules = require('./index');
// ANSI colors for output
const colors = {
red: '\x1b[31m',
yellow: '\x1b[33m',
green: '\x1b[32m',
blue: '\x1b[34m',
reset: '\x1b[0m',
bold: '\x1b[1m'
};
/**
* Validate a single file against all applicable rules
* @param {string} filePath - Path to file to validate
* @param {Object} options - Validation options
* @returns {Object} Validation results
*/
function validateFile(filePath, options = {}) {
const results = {
file: filePath,
errors: [],
warnings: [],
info: [],
passed: true
};
if (!fs.existsSync(filePath)) {
results.errors.push({ message: `File not found: ${filePath}` });
results.passed = false;
return results;
}
const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath).toLowerCase();
// Determine which rule categories apply based on file type
const applicableCategories = getApplicableCategories(ext);
for (const category of applicableCategories) {
const ruleSet = rules.getRulesByCategory(category);
if (!ruleSet?.rules) continue;
for (const rule of ruleSet.rules) {
// Skip if file matches exception patterns
if (rule.exceptions?.some(exc => filePath.includes(exc.replace('*', '')))) {
continue;
}
// Check forbidden patterns
if (rule.patterns?.forbidden) {
for (const pattern of rule.patterns.forbidden) {
try {
const regex = new RegExp(pattern, 'gm');
let match;
while ((match = regex.exec(content)) !== null) {
const lineNumber = content.substring(0, match.index).split('\n').length;
const violation = {
rule: `${category}/${rule.id}`,
name: rule.name,
line: lineNumber,
match: match[0],
message: rule.description || `Violation of ${rule.name}`
};
const severity = rule.severity || ruleSet.severity || 'warning';
if (severity === 'error') {
results.errors.push(violation);
results.passed = false;
} else if (severity === 'warning') {
results.warnings.push(violation);
} else {
results.info.push(violation);
}
}
} catch (e) {
// Invalid regex, skip
}
}
}
}
}
return results;
}
/**
* Determine which rule categories apply to a file type
* @param {string} ext - File extension
* @returns {string[]} Applicable category names
*/
function getApplicableCategories(ext) {
const cssTypes = ['.css', '.scss', '.sass', '.less', '.styl'];
const jsTypes = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'];
const htmlTypes = ['.html', '.htm', '.vue', '.svelte', '.jsx', '.tsx'];
const categories = [];
if (cssTypes.includes(ext)) {
categories.push('colors', 'spacing', 'typography');
}
if (jsTypes.includes(ext)) {
categories.push('colors', 'spacing', 'components');
}
if (htmlTypes.includes(ext)) {
categories.push('accessibility', 'components');
}
return categories;
}
/**
* Validate multiple files and aggregate results
* @param {string[]} files - Array of file paths
* @param {Object} options - Validation options
* @returns {Object} Aggregated results
*/
function validateFiles(files, options = {}) {
const results = {
totalFiles: files.length,
passedFiles: 0,
failedFiles: 0,
totalErrors: 0,
totalWarnings: 0,
fileResults: []
};
for (const file of files) {
const fileResult = validateFile(file, options);
results.fileResults.push(fileResult);
if (fileResult.passed) {
results.passedFiles++;
} else {
results.failedFiles++;
}
results.totalErrors += fileResult.errors.length;
results.totalWarnings += fileResult.warnings.length;
}
return results;
}
/**
* Print validation results to console
* @param {Object} results - Validation results
*/
function printResults(results) {
console.log('\n' + colors.bold + '=== DSS Rules Validation ===' + colors.reset);
console.log(`Rules version: ${rules.getVersion()}\n`);
for (const fileResult of results.fileResults) {
const statusIcon = fileResult.passed ? colors.green + '✓' : colors.red + '✗';
console.log(`${statusIcon}${colors.reset} ${fileResult.file}`);
for (const error of fileResult.errors) {
console.log(` ${colors.red}ERROR${colors.reset} [${error.rule}] Line ${error.line}: ${error.message}`);
console.log(` Found: ${colors.yellow}${error.match}${colors.reset}`);
}
for (const warning of fileResult.warnings) {
console.log(` ${colors.yellow}WARN${colors.reset} [${warning.rule}] Line ${warning.line}: ${warning.message}`);
}
}
console.log('\n' + colors.bold + '=== Summary ===' + colors.reset);
console.log(`Files: ${results.passedFiles}/${results.totalFiles} passed`);
console.log(`Errors: ${colors.red}${results.totalErrors}${colors.reset}`);
console.log(`Warnings: ${colors.yellow}${results.totalWarnings}${colors.reset}`);
if (results.totalErrors > 0) {
console.log(`\n${colors.red}${colors.bold}Validation failed!${colors.reset}`);
} else {
console.log(`\n${colors.green}${colors.bold}Validation passed!${colors.reset}`);
}
}
/**
* Self-test to verify rules package is correctly structured
*/
function selfTest() {
console.log('Running @dss/rules self-test...\n');
const allRules = rules.loadRules();
let passed = true;
for (const [category, ruleSet] of Object.entries(allRules)) {
if (!ruleSet) {
console.log(`${colors.red}${colors.reset} ${category}: Failed to load`);
passed = false;
continue;
}
const ruleCount = ruleSet.rules?.length || 0;
console.log(`${colors.green}${colors.reset} ${category}: ${ruleCount} rules, version ${ruleSet.version}`);
}
const ciConfig = rules.getCIConfig();
console.log(`\nCI Config: ${ciConfig.blockingRules.length} blocking rules, ${ciConfig.advisoryRules.length} advisory rules`);
if (passed) {
console.log(`\n${colors.green}Self-test passed!${colors.reset}`);
process.exit(0);
} else {
console.log(`\n${colors.red}Self-test failed!${colors.reset}`);
process.exit(1);
}
}
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
if (args.includes('--self-test')) {
selfTest();
} else if (args.includes('--help') || args.length === 0) {
console.log(`
@dss/rules validator
Usage:
node validate.js [options] <files...>
Options:
--self-test Run self-test to verify rules package
--json Output results as JSON
--help Show this help message
Examples:
node validate.js src/**/*.tsx
node validate.js --self-test
node validate.js --json src/components/*.tsx
`);
} else {
const jsonOutput = args.includes('--json');
const files = args.filter(a => !a.startsWith('--'));
const results = validateFiles(files);
if (jsonOutput) {
console.log(JSON.stringify(results, null, 2));
} else {
printResults(results);
}
process.exit(results.totalErrors > 0 ? 1 : 0);
}
}
module.exports = {
validateFile,
validateFiles,
getApplicableCategories
};