@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
204 lines (178 loc) • 6.27 kB
JavaScript
/**
* Rule Selection Service
* Following Rule C005: Single responsibility - only handle rule selection
* REFACTORED: Now uses SunlintRuleAdapter instead of direct registry access
* UPDATED: Load rules from released-rules.json for consistency
*/
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const RuleMappingService = require('./rule-mapping-service');
const SunlintRuleAdapter = require('./adapters/sunlint-rule-adapter');
class RuleSelectionService {
constructor() {
this.ruleAdapter = SunlintRuleAdapter.getInstance();
this.ruleMappingService = new RuleMappingService();
this.initialized = false;
// Path works both in dev (from pages/) and npm package (from config/)
this.releasedRulesPath = path.join(__dirname, '../config/released-rules.json');
}
async initialize() {
if (!this.initialized) {
await this.ruleAdapter.initialize();
this.initialized = true;
}
}
/**
* Load released rules from released-rules.json
* @param {string} [version] - Version to load (default: latest)
* @returns {Object} Object with Common, Security arrays
*/
loadReleasedRules(version = null) {
try {
if (!fs.existsSync(this.releasedRulesPath)) {
return null;
}
const data = JSON.parse(fs.readFileSync(this.releasedRulesPath, 'utf8'));
const versions = data.versions || [];
if (versions.length === 0) {
return null;
}
// Get specified version or latest
const targetVersion = version
? versions.find(v => v.version === version)
: versions[versions.length - 1]; // Latest version
if (!targetVersion) {
return null;
}
return targetVersion.rulesByCategory;
} catch (error) {
return null;
}
}
async selectRules(config, options) {
// Ensure adapter is initialized
await this.initialize();
const allRules = config.rules || {};
let selectedRules = [];
// Try to load from released-rules.json first
const releasedRules = this.loadReleasedRules();
// Determine rule selection strategy
if (options.rule) {
selectedRules = [options.rule];
} else if (options.rules) {
selectedRules = options.rules.split(',').map(r => r.trim());
} else if (options.all) {
// Load all rules from released-rules.json
if (releasedRules) {
selectedRules = [
...(releasedRules.Common || []),
...(releasedRules.Security || []),
...(releasedRules.Frontend || []),
...(releasedRules.Backend || []),
...(releasedRules.Mobile || [])
];
} else {
// Fallback to preset file
selectedRules = this.loadPresetRules('all');
}
} else if (options.quality) {
// Load Common rules from released-rules.json
if (releasedRules && releasedRules.Common) {
selectedRules = releasedRules.Common;
} else {
selectedRules = this.loadPresetRules('quality');
}
} else if (options.security) {
// Load Security rules from released-rules.json
if (releasedRules && releasedRules.Security) {
selectedRules = releasedRules.Security;
} else {
selectedRules = this.loadPresetRules('security');
}
} else if (options.category) {
// Handle --category shortcut (standardized approach)
const categoryRules = this.ruleAdapter.getStandardCategoryRules(options.category);
selectedRules = categoryRules.map(rule => rule.id);
} else {
// Default: load all from released-rules.json
if (releasedRules) {
selectedRules = [
...(releasedRules.Common || []),
...(releasedRules.Security || [])
];
} else {
// Fallback to config rules or minimal set
selectedRules = Object.keys(allRules).filter(ruleId =>
allRules[ruleId] !== 'off' && allRules[ruleId] !== false
);
if (selectedRules.length === 0) {
selectedRules = ['C006', 'C019']; // Default minimal set
}
}
}
// Convert to rule objects
return selectedRules.map(ruleId => {
const adapterRule = this.ruleAdapter.getRuleById(ruleId);
return {
id: ruleId,
name: this.getRuleName(ruleId),
severity: 'warning',
...(adapterRule || {})
};
}).filter(rule => rule.id);
}
getMinimalRuleSet() {
return {
rules: {
'C006': {
name: 'Function Naming Convention',
description: 'Function names should follow verb-noun pattern',
category: 'naming',
severity: 'warning'
},
'C019': {
name: 'Log Level Usage',
description: 'Use appropriate log levels',
category: 'logging',
severity: 'warning'
}
}
};
}
getRulesByCategory(category) {
// Use adapter to get rules by category
return this.ruleAdapter.getRulesByCategory(category).map(rule => rule.id);
}
getRuleName(ruleId) {
// Use adapter to get rule name
const rule = this.ruleAdapter.getRuleById(ruleId);
return rule ? rule.name : `Rule ${ruleId}`;
}
/**
* Load rules from preset configuration files
* @param {string} presetName - Name of preset (quality, security, all)
* @returns {Array} Array of rule IDs
*/
loadPresetRules(presetName) {
try {
const presetPath = path.join(__dirname, '../config/presets', `${presetName}.json`);
if (!fs.existsSync(presetPath)) {
console.warn(chalk.yellow(`⚠️ Preset file not found: ${presetPath}`));
return [];
}
const presetConfig = JSON.parse(fs.readFileSync(presetPath, 'utf8'));
const ruleIds = Object.keys(presetConfig.rules || {});
if (ruleIds.length === 0) {
console.warn(chalk.yellow(`⚠️ No rules found in preset: ${presetName}`));
return [];
}
console.log(chalk.green(`✅ Loaded ${ruleIds.length} rules from ${presetName} preset`));
return ruleIds;
} catch (error) {
console.error(chalk.red(`❌ Failed to load preset ${presetName}:`, error.message));
return [];
}
}
}
module.exports = RuleSelectionService;