267 lines
7.4 KiB
JavaScript
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
|
|
};
|