@maniascript/mslint
Version:
ManiaScript linter
238 lines (237 loc) • 8.57 kB
JavaScript
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`);
}
}
}
},
msApiGame: {
merge: 'replace',
validate: 'string'
},
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');
}
// Throw early to avoid triggering an error in the MSLintConfigArray constructor
if (errors.length > 0) {
throw new MSLintConfigValidationError(errors);
}
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 = {}, msApiGame = '', msApiPath = '' }) {
return validateConfig({
cwd,
patterns,
verbose,
displayStats,
reportUnusedDisableDirective,
reportDisableDirectiveWithoutDescription,
linter: [{ rules, msApiGame, 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 };