@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
306 lines (266 loc) • 9.61 kB
JavaScript
const { RuleParser } = require("./rule-parser")
const fs = require("fs")
const path = require("path")
/**
* Simple Rule Parser - Lightweight wrapper around RuleParser
* Provides simplified interface for basic rule information extraction
* Focused on English rules (*-en.md) with filtering capabilities
*/
class SimpleRuleParser {
constructor() {
// Use existing RuleParser as core engine, but disable migrate mode
this.coreParser = new RuleParser(false)
this.rulesCache = new Map() // Cache parsed rules by file
}
/**
* Parse a specific markdown file and extract basic rule information
* @param {string} filePath - Path to the markdown file
* @returns {Array} Array of simplified rule objects
*/
parseRuleFile(filePath) {
try {
// Check cache first
if (this.rulesCache.has(filePath)) {
return this.rulesCache.get(filePath)
}
// Use default config for parsing
const defaultConfig = {
category: "Common",
language: "All languages",
framework: "All"
}
// Parse using core parser
const fullRules = this.coreParser.parseMarkdownFile(filePath, defaultConfig)
// Convert to simplified format
const simplifiedRules = fullRules.map(rule => this.simplifyRule(rule))
// Cache the result
this.rulesCache.set(filePath, simplifiedRules)
return simplifiedRules
} catch (error) {
console.error(`Error parsing file ${filePath}:`, error.message)
return []
}
}
/**
* Parse all English rule files from rules directory
* @param {string} rulesDir - Path to rules directory (default: relative to this script)
* @returns {Array} Array of all simplified rule objects
*/
parseAllRules(rulesDir = null) {
try {
if (!rulesDir) {
rulesDir = path.join(__dirname, "..", "..", "origin-rules")
}
if (!fs.existsSync(rulesDir)) {
throw new Error(`Rules directory not found: ${rulesDir}`)
}
const allRules = []
const files = fs.readdirSync(rulesDir)
// Find all English rule files (*-en.md)
const englishRuleFiles = files.filter(file =>
file.endsWith('-en.md') && file !== 'README-en.md'
)
console.log(`Found ${englishRuleFiles.length} English rule files: ${englishRuleFiles.join(', ')}`)
for (const file of englishRuleFiles) {
const filePath = path.join(rulesDir, file)
const rules = this.parseRuleFile(filePath)
allRules.push(...rules)
}
return allRules
} catch (error) {
console.error("Error parsing all rules:", error.message)
return []
}
}
/**
* Filter rules by various criteria
* @param {Array} rules - Array of rules to filter
* @param {Object} filters - Filter criteria
* @param {string} filters.ruleId - Specific rule ID (e.g., "C001")
* @param {string|Array} filters.principles - Principle(s) to match (e.g., "Quality", ["Security", "Performance"])
* @param {string} filters.framework - Framework to match (e.g., "Android")
* @param {string} filters.language - Language to match (e.g., "Java")
* @param {string} filters.minVersion - Minimum version (e.g., "1.0")
* @param {string} filters.status - Status to match (default: "activated")
* @returns {Array} Filtered rules
*/
filterRules(rules, filters = {}) {
try {
let filteredRules = [...rules]
// Default filters: version >= 1.0 and status = activated
const {
ruleId,
principles,
framework,
language,
minVersion = "1.0",
status // No default for status when looking for specific ruleId
} = filters
// Filter by specific rule ID first (no status filter needed)
if (ruleId) {
filteredRules = filteredRules.filter(rule =>
rule.id && rule.id.toLowerCase() === ruleId.toLowerCase()
)
// If searching by ID, return early (no other filters needed)
return filteredRules
}
// Filter by status (default: activated) only when not searching by ID
const defaultStatus = status || "activated"
filteredRules = filteredRules.filter(rule =>
rule.status && rule.status.toLowerCase() === defaultStatus.toLowerCase()
)
// Filter by minimum version
if (minVersion) {
filteredRules = filteredRules.filter(rule =>
this.compareVersions(rule.version || "1.0", minVersion) >= 0
)
}
// Filter by principles
if (principles) {
const principlesArray = Array.isArray(principles) ? principles : [principles]
filteredRules = filteredRules.filter(rule => {
if (!rule.principles || rule.principles.length === 0) return false
return principlesArray.some(principle =>
rule.principles.some(rulePrinciple =>
rulePrinciple.toLowerCase().includes(principle.toLowerCase())
)
)
})
}
// Filter by framework
if (framework) {
filteredRules = filteredRules.filter(rule =>
rule.framework && rule.framework.toLowerCase().includes(framework.toLowerCase())
)
}
// Filter by language
if (language) {
filteredRules = filteredRules.filter(rule =>
rule.language && (
rule.language.toLowerCase().includes(language.toLowerCase()) ||
rule.language.toLowerCase().includes('all')
)
)
}
return filteredRules
} catch (error) {
console.error("Error filtering rules:", error.message)
return rules
}
}
/**
* Get rules by principles (common use case)
* @param {string|Array} principles - Principle(s) to search for
* @param {string} rulesDir - Rules directory path
* @returns {Array} Rules matching the principles
*/
getRulesByPrinciples(principles, rulesDir = null) {
const allRules = this.parseAllRules(rulesDir)
return this.filterRules(allRules, { principles })
}
/**
* Get rules for specific framework
* @param {string} framework - Framework name (e.g., "Android", "React")
* @param {string} rulesDir - Rules directory path
* @returns {Array} Rules for the framework
*/
getRulesByFramework(framework, rulesDir = null) {
const allRules = this.parseAllRules(rulesDir)
return this.filterRules(allRules, { framework })
}
/**
* Get single rule by ID
* @param {string} ruleId - Rule ID (e.g., "C001")
* @param {string} rulesDir - Rules directory path
* @returns {Object|null} Rule object or null if not found
*/
getRuleById(ruleId, rulesDir = null) {
try {
const allRules = this.parseAllRules(rulesDir)
const filtered = this.filterRules(allRules, { ruleId })
return filtered.length > 0 ? filtered[0] : null
} catch (error) {
console.error("Error in getRuleById:", error.message)
return null
}
}
/**
* Compare version strings (supports x.y format)
* @param {string} version1 - First version
* @param {string} version2 - Second version
* @returns {number} -1 if version1 < version2, 0 if equal, 1 if version1 > version2
*/
compareVersions(version1, version2) {
try {
const v1Parts = version1.split('.').map(Number)
const v2Parts = version2.split('.').map(Number)
const maxLength = Math.max(v1Parts.length, v2Parts.length)
for (let i = 0; i < maxLength; i++) {
const v1Part = v1Parts[i] || 0
const v2Part = v2Parts[i] || 0
if (v1Part < v2Part) return -1
if (v1Part > v2Part) return 1
}
return 0
} catch (error) {
console.warn(`Error comparing versions ${version1} and ${version2}:`, error.message)
return 0
}
}
/**
* Convert full rule object to simplified format
* @param {Object} fullRule - Full rule object from RuleParser
* @returns {Object} Simplified rule object
*/
simplifyRule(fullRule) {
return {
id: fullRule.id,
title: fullRule.title,
description: fullRule.description,
details: fullRule.details || [],
tools: fullRule.tools || [],
principles: fullRule.principles || [],
version: fullRule.version,
status: fullRule.status,
severity: fullRule.severity,
language: fullRule.language,
framework: fullRule.framework,
category: fullRule.category
// Exclude examples and configs for lightweight usage
}
}
}
// Factory functions for easy use (maintaining backward compatibility)
function parseRuleFile(filePath) {
const parser = new SimpleRuleParser()
return parser.parseRuleFile(filePath)
}
function parseAllRules(rulesDir = null) {
const parser = new SimpleRuleParser()
return parser.parseAllRules(rulesDir)
}
function filterRules(rules, filters = {}) {
const parser = new SimpleRuleParser()
return parser.filterRules(rules, filters)
}
function getRulesByPrinciples(principles, rulesDir = null) {
const parser = new SimpleRuleParser()
return parser.getRulesByPrinciples(principles, rulesDir)
}
function getRulesByFramework(framework, rulesDir = null) {
const parser = new SimpleRuleParser()
return parser.getRulesByFramework(framework, rulesDir)
}
function getRuleById(ruleId, rulesDir = null) {
const parser = new SimpleRuleParser()
return parser.getRuleById(ruleId, rulesDir)
}
module.exports = {
SimpleRuleParser,
parseRuleFile,
parseAllRules,
filterRules,
getRulesByPrinciples,
getRulesByFramework,
getRuleById
}