318 lines
7.5 KiB
JavaScript
Executable File
318 lines
7.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* WordPress Ability Category Validation Script (JavaScript)
|
|
*
|
|
* Validates JavaScript category registration code using acorn AST parser.
|
|
* For PHP validation, use validate-category.php instead.
|
|
*
|
|
* Usage:
|
|
* node validate-category.js path/to/category-file.js
|
|
*
|
|
* Requirements:
|
|
* - Node.js
|
|
* - acorn parser: npm install acorn
|
|
*
|
|
* Arguments:
|
|
* file_path Path to the JavaScript file containing category registration
|
|
*
|
|
* Exit Codes:
|
|
* 0 - Validation passed
|
|
* 1 - Validation failed
|
|
* 2 - File not found, invalid usage, or missing dependencies
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Check for acorn dependency
|
|
let acorn;
|
|
try {
|
|
acorn = require('acorn');
|
|
} catch (e) {
|
|
console.error(
|
|
'Error: acorn parser not found. Install with: npm install acorn'
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
// Check for file argument
|
|
if (process.argv.length < 3) {
|
|
console.error('Usage: node validate-category.js path/to/category-file.js');
|
|
process.exit(2);
|
|
}
|
|
|
|
const filePath = process.argv[2];
|
|
|
|
// Check if file exists
|
|
if (!fs.existsSync(filePath)) {
|
|
console.error(`Error: File not found: ${filePath}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
// Check if file is JavaScript
|
|
const ext = path.extname(filePath);
|
|
if (!['.js', '.jsx', '.ts', '.tsx', '.mjs'].includes(ext)) {
|
|
console.error(
|
|
'Error: This validator only supports JavaScript files. For PHP validation, use validate-category.php'
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
// Read file contents
|
|
let content;
|
|
try {
|
|
content = fs.readFileSync(filePath, 'utf8');
|
|
} catch (e) {
|
|
console.error(`Error: Unable to read file: ${filePath}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
// Initialize validation results
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
// Parse and validate JavaScript categories
|
|
try {
|
|
const ast = acorn.parse(content, {
|
|
sourceType: 'module',
|
|
ecmaVersion: 'latest',
|
|
allowAwaitOutsideFunction: true,
|
|
});
|
|
|
|
const categories = extractCategories(ast);
|
|
|
|
if (categories.length === 0) {
|
|
errors.push('No registerAbilityCategory() calls found in file');
|
|
} else {
|
|
categories.forEach((category) => {
|
|
console.log(`Validating category: ${category.name}`);
|
|
validateCategory(category, errors, warnings);
|
|
console.log('');
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof SyntaxError) {
|
|
errors.push(`JavaScript syntax error: ${e.message}`);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// Output final results
|
|
outputResults(filePath, errors, warnings);
|
|
|
|
// Exit with appropriate code
|
|
process.exit(errors.length > 0 ? 1 : 0);
|
|
|
|
/**
|
|
* Extract category registrations from AST.
|
|
*
|
|
* @param {Object} ast - Acorn AST
|
|
* @returns {Array} Array of category objects
|
|
*/
|
|
function extractCategories(ast) {
|
|
const categories = [];
|
|
const processed = new Set(); // Track processed nodes to avoid duplicates
|
|
|
|
// Walk the AST to find registerAbilityCategory calls
|
|
walk(ast, (node) => {
|
|
let callNode = null;
|
|
|
|
// Direct call: registerAbilityCategory()
|
|
if (
|
|
node.type === 'CallExpression' &&
|
|
node.callee.type === 'Identifier' &&
|
|
node.callee.name === 'registerAbilityCategory' &&
|
|
!processed.has(node)
|
|
) {
|
|
callNode = node;
|
|
}
|
|
|
|
// Await call: await registerAbilityCategory()
|
|
if (
|
|
node.type === 'AwaitExpression' &&
|
|
node.argument.type === 'CallExpression' &&
|
|
node.argument.callee.type === 'Identifier' &&
|
|
node.argument.callee.name === 'registerAbilityCategory' &&
|
|
!processed.has(node.argument)
|
|
) {
|
|
callNode = node.argument;
|
|
}
|
|
|
|
if (callNode) {
|
|
processed.add(callNode);
|
|
const category = extractCategoryFromCall(callNode);
|
|
if (category) {
|
|
categories.push(category);
|
|
}
|
|
}
|
|
});
|
|
|
|
return categories;
|
|
}
|
|
|
|
/**
|
|
* Extract category data from a CallExpression node.
|
|
*
|
|
* @param {Object} node - CallExpression AST node
|
|
* @returns {Object|null} Category object or null
|
|
*/
|
|
function extractCategoryFromCall(node) {
|
|
if (node.arguments.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
const category = {
|
|
name: null,
|
|
label: null,
|
|
description: null,
|
|
};
|
|
|
|
// First argument: category name (string literal)
|
|
const nameArg = node.arguments[0];
|
|
if (nameArg.type === 'Literal' && typeof nameArg.value === 'string') {
|
|
category.name = nameArg.value;
|
|
} else {
|
|
return null; // Name must be a string literal
|
|
}
|
|
|
|
// Second argument: config object
|
|
const configArg = node.arguments[1];
|
|
if (configArg.type === 'ObjectExpression') {
|
|
configArg.properties.forEach((prop) => {
|
|
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
const key = prop.key.name;
|
|
if (
|
|
(key === 'label' || key === 'description') &&
|
|
prop.value.type === 'Literal' &&
|
|
typeof prop.value.value === 'string'
|
|
) {
|
|
category[key] = prop.value.value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return category;
|
|
}
|
|
|
|
/**
|
|
* Validate category data.
|
|
*
|
|
* @param {Object} category - Category data
|
|
* @param {Array} errors - Error messages array
|
|
* @param {Array} warnings - Warning messages array
|
|
*/
|
|
function validateCategory(category, errors, warnings) {
|
|
const { name, label, description } = category;
|
|
|
|
// Validate category name format (kebab-case)
|
|
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
errors.push(
|
|
`Invalid category name format: '${name}'. Must be kebab-case (lowercase, hyphens allowed)`
|
|
);
|
|
}
|
|
|
|
// Check required fields
|
|
if (!label) {
|
|
errors.push(`Missing required field 'label' in category '${name}'`);
|
|
} else {
|
|
// Validate label quality
|
|
if (label.trim().length < 2) {
|
|
warnings.push(
|
|
`Label for category '${name}' is too short. Provide a meaningful label.`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!description) {
|
|
errors.push(
|
|
`Missing required field 'description' in category '${name}'`
|
|
);
|
|
} else {
|
|
// Validate description quality
|
|
const descLength = description.trim().length;
|
|
|
|
if (descLength < 15) {
|
|
warnings.push(
|
|
`Description for category '${name}' is too short (${descLength} chars). Provide detailed information about what abilities belong in this category.`
|
|
);
|
|
}
|
|
|
|
if (/TODO/i.test(description)) {
|
|
warnings.push(
|
|
`Description for category '${name}' contains TODO placeholder. Replace with actual description.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple AST walker.
|
|
*
|
|
* @param {Object} node - AST node
|
|
* @param {Function} callback - Function to call for each node
|
|
*/
|
|
function walk(node, callback) {
|
|
if (!node || typeof node !== 'object') {
|
|
return;
|
|
}
|
|
|
|
callback(node);
|
|
|
|
for (const key in node) {
|
|
if (
|
|
key === 'loc' ||
|
|
key === 'range' ||
|
|
key === 'start' ||
|
|
key === 'end'
|
|
) {
|
|
continue; // Skip position metadata
|
|
}
|
|
|
|
const value = node[key];
|
|
if (Array.isArray(value)) {
|
|
value.forEach((child) => walk(child, callback));
|
|
} else if (value && typeof value === 'object') {
|
|
walk(value, callback);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Output validation results.
|
|
*
|
|
* @param {string} filePath - File being validated
|
|
* @param {Array} errors - Error messages
|
|
* @param {Array} warnings - Warning messages
|
|
*/
|
|
function outputResults(filePath, errors, warnings) {
|
|
console.log('='.repeat(70));
|
|
console.log(`Validation Results: ${filePath}`);
|
|
console.log('='.repeat(70));
|
|
console.log('');
|
|
|
|
if (errors.length > 0) {
|
|
console.log(`ERRORS (${errors.length}):`);
|
|
errors.forEach((error) => console.log(` ✗ ${error}`));
|
|
console.log('');
|
|
}
|
|
|
|
if (warnings.length > 0) {
|
|
console.log(`WARNINGS (${warnings.length}):`);
|
|
warnings.forEach((warning) => console.log(` ⚠ ${warning}`));
|
|
console.log('');
|
|
}
|
|
|
|
if (errors.length === 0 && warnings.length === 0) {
|
|
console.log('✓ All validations passed! No issues found.');
|
|
console.log('');
|
|
} else if (errors.length === 0) {
|
|
console.log('✓ Validation passed with warnings.');
|
|
console.log('');
|
|
} else {
|
|
console.log('✗ Validation failed. Please fix the errors above.');
|
|
console.log('');
|
|
}
|
|
}
|