UNPKG

@maniascript/mslint

Version:
230 lines (229 loc) 8.29 kB
import fs from 'node:fs'; import path from 'node:path'; import { ConfigArray, ConfigArraySymbol } from '@eslint/config-array'; import { MSLintError, MSLintConfigValidationError } from './error.js'; import { getSeverity, getSettings, Severity } from './rule.js'; import { ConfigName, configs } from '../../configs/index.js'; const DEFAULT_LINTER_CONFIG = [ // Default ignores { ignores: [ '**/node_modules/*', '.git/' ] }, // Defaut matches { files: ['**/*.Script.txt'] } ]; class MSLintConfigArray extends ConfigArray { [ConfigArraySymbol.preprocessConfig](config) { if (config === ConfigName.Recommended) { return configs.recommended; } else if (config === ConfigName.All) { return configs.all; } return config; } } function getRuleSeverity(source) { return getSeverity(Array.isArray(source) ? source[0] : source); } function getRuleSettings(source) { return getSettings(Array.isArray(source) ? source[1] : {}); } function normalizeRuleConfig(ruleConfig) { const result = Array.isArray(ruleConfig) ? ruleConfig.slice(0) : [ruleConfig]; result[0] = getRuleSeverity(result); return result; } // Schema for the linter ArrayConfig const LINTER_CONFIG_SCHEMA = { rules: { merge(first = {}, second = {}) { // Merge rules const result = { ...first, ...second }; // Validate each rule configuration for (const ruleId of Object.keys(result)) { result[ruleId] = normalizeRuleConfig(result[ruleId]); // If the rule was present in only one of the two config there's nothing more to do if (!(ruleId in first) || !(ruleId in second)) continue; // If the rule was present in both config and the second one only contains the severity // we want to merge the severity of the second one // over the severity of the first one while keeping its options // eg: first = ['warn', { option: value }] and second = ['error'] // then result = ['error', { option: value }] const secondRuleConfig = normalizeRuleConfig(second[ruleId]); if (secondRuleConfig.length === 1) { result[ruleId] = [secondRuleConfig[0], ...normalizeRuleConfig(first[ruleId]).slice(1)]; } } return result; }, validate(rules) { for (const ruleId of Object.keys(rules)) { const ruleConfig = rules[ruleId]; if (typeof ruleConfig !== 'string' && typeof ruleConfig !== 'number' && !Array.isArray(ruleConfig)) { throw new TypeError(`Config for rule "${ruleId}" must be a string, number or array`); } const severity = getRuleSeverity(ruleConfig); if (severity === undefined) { throw new TypeError(`Severity for rule "${ruleId}" must be "error", 2, "warn", 1, "off" or 0`); } } } }, msApiPath: { merge: 'replace', validate: 'string' } }; function validateConfig(unvalidatedGlobalConfig) { const errors = []; const unknownOptionKeys = []; let cwd = process.cwd(); let patterns = []; let verbose = false; let displayStats = false; let reportUnusedDisableDirective = false; let reportDisableDirectiveWithoutDescription = false; let linter = null; if (typeof unvalidatedGlobalConfig === 'object') { for (const property in unvalidatedGlobalConfig) { const value = unvalidatedGlobalConfig[property]; switch (property) { case 'cwd': { if (typeof value === 'string') { cwd = value; } break; } case 'patterns': { if (Array.isArray(value)) { for (const item of value) { if (typeof item === 'string') { patterns.push(item); } } } else if (typeof value === 'string') { patterns = [value]; } break; } case 'verbose': { if (typeof value === 'boolean') { verbose = value; } break; } case 'displayStats': { if (typeof value === 'boolean') { displayStats = value; } break; } case 'reportUnusedDisableDirective': { if (typeof value === 'boolean') { reportUnusedDisableDirective = value; } break; } case 'reportDisableDirectiveWithoutDescription': { if (typeof value === 'boolean') { reportDisableDirectiveWithoutDescription = value; } break; } case 'linter': { linter = value; break; } default: { unknownOptionKeys.push(property); break; } } } } if (unknownOptionKeys.length > 0) { errors.push(`Unknown options: ${unknownOptionKeys.join(', ')}`); } if (typeof cwd !== 'string' || cwd.trim() === '' || !path.isAbsolute(cwd)) { errors.push('\'cwd\' must be an absolute path'); } if (typeof verbose !== 'boolean') { errors.push('\'verbose\' must be a boolean'); } if (typeof displayStats !== 'boolean') { errors.push('\'displayStats\' must be a boolean'); } if (typeof reportUnusedDisableDirective !== 'boolean') { errors.push('\'reportUnusedDisableDirective\' must be a boolean'); } if (typeof reportDisableDirectiveWithoutDescription !== 'boolean') { errors.push('\'reportDisableDirectiveWithoutDescription\' must be a boolean'); } const linterConfig = new MSLintConfigArray(DEFAULT_LINTER_CONFIG, { basePath: cwd, schema: LINTER_CONFIG_SCHEMA }); if (!Array.isArray(linter) && linter !== null) { errors.push('\'linter\' must be an array of config or null'); } if (Array.isArray(linter)) { linterConfig.push(...linter); } if (errors.length > 0) { throw new MSLintConfigValidationError(errors); } if (!Array.isArray(patterns)) { patterns = [patterns]; } return { cwd, patterns, verbose, displayStats, reportUnusedDisableDirective, reportDisableDirectiveWithoutDescription, linter: linterConfig }; } function createConfig({ cwd = process.cwd(), patterns = [], verbose = false, displayStats = false, reportUnusedDisableDirective = false, reportDisableDirectiveWithoutDescription = false, rules = {}, msApiPath = '' }) { return validateConfig({ cwd, patterns, verbose, displayStats, reportUnusedDisableDirective, reportDisableDirectiveWithoutDescription, linter: [{ rules, msApiPath }] }); } function loadConfig(configPath) { // Load file let configJson; try { configJson = fs.readFileSync(configPath, 'utf-8'); } catch { throw new MSLintError(`Config file '${configPath}' not found`); } // Parse file let configParsed = {}; if (configJson !== '') { try { configParsed = JSON.parse(configJson); } catch { throw new MSLintError(`Failed to parse config file '${configPath}'`); } } return validateConfig(configParsed); } export { Severity, getRuleSeverity, getRuleSettings, validateConfig, createConfig, loadConfig };