eslint-remote-tester
Version:
Tool for running ESLint on multiple repositories
203 lines (202 loc) • 8.04 kB
JavaScript
import chalk from 'chalk';
import { ESLint } from 'eslint';
import { LogLevels as LOG_LEVELS, ResultParsers as RESULT_PARSERS, } from './types.js';
const DEFAULT_RESULT_PARSER_CLI = 'markdown';
const DEFAULT_RESULT_PARSER_CI = 'plaintext';
const DEFAULT_LOG_LEVEL = 'verbose';
const DEFAULT_CONCURRENT_TASKS = 5;
const DEFAULT_MAX_FILE_SIZE_BYTES = 2000000;
const DEFAULT_TIME_LIMIT_SECONDS = 5.5 * 60 * 60;
const DEFAULT_CI = process.env.CI === 'true';
const DEFAULT_CACHE = true;
const DEFAULT_COMPARE = false;
const DEFAULT_UPDATE_COMPARISON_REFERENCE = true;
const UNKNOWN_RULE_REGEXP = /^Definition for rule (.*) was not found.$/;
/**
* Validate array of strings:
* 1. Object is required
* 2. Object is array
* 3. Object contains only strings
* 4. Object contains no duplicates
*/
function validateStringArray(name, array) {
if (!array || !array.length) {
return `Missing ${name}.`;
}
else if (!Array.isArray(array)) {
return `${name} should be an array.`;
}
else if (!array.every(item => typeof item === 'string')) {
return `${name} should contain only strings`;
}
else {
const duplicateItems = array.filter((item, index, items) => items.indexOf(item) !== index);
if (duplicateItems.length) {
return `${name} contains duplicate entries: [${duplicateItems.join(', ')}]`;
}
}
}
/**
* Validate optional positive number:
* 1. Value is optional
* 2. Type is number
* 3. Value is positive
*/
function validateOptionalPositiveNumber(name, value) {
if (value != null && typeof value !== 'number') {
return `${name} (${value}) should be a number.`;
}
else if (value != null && value <= 0) {
return `${name} (${value}) should be a positive number.`;
}
}
/**
* Validate optional boolean:
* 1. Value is optional
* 2. Type is boolean
*/
function validateOptionalBoolean(name, value) {
if (value != null && typeof value !== 'boolean') {
return `${name} (${value}) should be a boolean.`;
}
}
/**
* Validate given configuration
*/
export default async function validate(configToValidate, exitWhenError = true) {
const { repositories, extensions, pathIgnorePattern, maxFileSizeBytes, rulesUnderTesting, resultParser, concurrentTasks, eslintConfig, CI, logLevel, slowLintTimeLimit, cache, timeLimit, compare, updateComparisonReference, onComplete, ...unknownKeys } = configToValidate;
const errors = [];
// Validate no unknown options were given
const unsupportedOptions = Object.keys(unknownKeys);
if (unsupportedOptions.length) {
errors.push(`Options [${unsupportedOptions.join(', ')}] are not supported`);
}
// Required fields
errors.push(validateStringArray('repositories', repositories));
errors.push(validateStringArray('extensions', extensions));
if (!eslintConfig) {
errors.push(`Missing eslintConfig.`);
}
else {
try {
// This will throw when eslintConfig is invalid
const linter = new ESLint({
overrideConfigFile: true,
overrideConfig: typeof eslintConfig === 'function'
? await eslintConfig()
: eslintConfig,
});
errors.push(await validateEslintRules(linter));
}
catch (e) {
errors.push(`eslintConfig: ${e.message}`);
if (typeof eslintConfig === 'function') {
errors.push('Note that "config.eslintConfig" is called with empty options during configuration validation.');
}
}
}
// Optional fields
// TODO nice-to-have: Validate rules match eslintConfig config
// https://eslint.org/docs/developer-guide/nodejs-api#lintergetrules
if (rulesUnderTesting) {
if (Array.isArray(rulesUnderTesting)) {
// Empty rulesUnderTesting is valid
if (rulesUnderTesting.length) {
errors.push(validateStringArray('rulesUnderTesting', rulesUnderTesting));
}
}
else if (typeof rulesUnderTesting !== 'function') {
errors.push(`rulesUnderTesting should be either an array or function.`);
}
}
if (pathIgnorePattern) {
try {
new RegExp(pathIgnorePattern);
}
catch (e) {
errors.push(`pathIgnorePattern (${pathIgnorePattern}) is not valid regex: ${e.message}`);
}
}
errors.push(validateOptionalPositiveNumber('maxFileSizeBytes', maxFileSizeBytes));
errors.push(validateOptionalBoolean('CI', CI));
if (logLevel && !LOG_LEVELS.includes(logLevel)) {
errors.push(`logLevel (${logLevel}) is not valid value. Known values are ${LOG_LEVELS.join(', ')}`);
}
errors.push(validateOptionalBoolean('cache', cache));
if (resultParser && !RESULT_PARSERS.includes(resultParser)) {
errors.push(`resultParser (${resultParser}) is not valid value. Known values are ${RESULT_PARSERS.join(', ')}`);
}
errors.push(validateOptionalPositiveNumber('concurrentTasks', concurrentTasks));
errors.push(validateOptionalPositiveNumber('timeLimit', timeLimit));
if (slowLintTimeLimit) {
errors.push(validateOptionalPositiveNumber('slowLintTimeLimit', slowLintTimeLimit));
}
errors.push(validateOptionalBoolean('compare', compare));
errors.push(validateOptionalBoolean('updateComparisonReference', updateComparisonReference));
if (onComplete && typeof onComplete !== 'function') {
errors.push(`onComplete (${onComplete}) should be a function`);
}
const validationErrors = errors.filter(Boolean).join('\n- ');
if (validationErrors.length) {
const errorMessage = `Configuration validation errors:\n- ${validationErrors}`;
console.log(chalk.red(errorMessage));
if (exitWhenError) {
process.exit(1);
}
else {
throw new Error(errorMessage);
}
}
}
/**
* Get configuration with default values on optional fields
*/
export function getConfigWithDefaults(config) {
const CI = config.CI != null ? config.CI : DEFAULT_CI;
let pathIgnorePattern = undefined;
if (config.pathIgnorePattern) {
try {
pathIgnorePattern = new RegExp(config.pathIgnorePattern);
}
catch {
// Faulty patterns are validated separately by validateConfig
}
}
return {
...config,
rulesUnderTesting: config.rulesUnderTesting || [],
pathIgnorePattern,
maxFileSizeBytes: config.maxFileSizeBytes || DEFAULT_MAX_FILE_SIZE_BYTES,
CI,
logLevel: config.logLevel || DEFAULT_LOG_LEVEL,
cache: config.cache != null ? config.cache : DEFAULT_CACHE,
resultParser: config.resultParser ||
(CI ? DEFAULT_RESULT_PARSER_CI : DEFAULT_RESULT_PARSER_CLI),
concurrentTasks: config.concurrentTasks || DEFAULT_CONCURRENT_TASKS,
timeLimit: config.timeLimit || DEFAULT_TIME_LIMIT_SECONDS,
slowLintTimeLimit: config.slowLintTimeLimit || null,
compare: config.compare != null ? config.compare : DEFAULT_COMPARE,
updateComparisonReference: config.updateComparisonReference != null
? config.updateComparisonReference
: DEFAULT_UPDATE_COMPARISON_REFERENCE,
};
}
/**
* Validate given rules of `config.eslintConfig.rules`
* - When unknown rules are defined, or known ones are misspelled they are not
* reported during linting. We need to specifically look for them.
*/
async function validateEslintRules(linter) {
const results = await linter.lintText('');
const errors = [];
for (const result of results) {
for (const resultMessage of result.messages) {
if (UNKNOWN_RULE_REGEXP.test(resultMessage.message)) {
errors.push(resultMessage.message);
}
}
}
if (errors.length) {
return `Configuration validation errors at eslintConfig.rules: \n - ${errors.join('\n - ')}`;
}
}