UNPKG

gulp-eslint-new

Version:

A gulp plugin to lint code with ESLint 8 and 9.

480 lines (439 loc) 15.2 kB
'use strict'; /** * @typedef {import('eslint').ESLint} ESLint * @typedef {import('./eslint').FormatterFunction} FormatterFunction * @typedef {import('./eslint').LintMessage} LintMessage * @typedef {import('./eslint').LintResult} LintResult * @typedef {import('./eslint').LoadedFormatter} LoadedFormatter * @typedef {import('./gulp-eslint-new').Writer} Writer */ const { normalize, relative } = require('path'); const { Transform } = require('stream'); const ESLINT_PKG = Symbol('ESLint package name'); const GULP_DEST_KEY = Symbol('require("vinyl-fs").dest'); const GULP_WARN_KEY = Symbol('require("fancy-log").warn'); function compareResultsByFilePath({ filePath: filePath1 }, { filePath: filePath2 }) { if (filePath1 > filePath2) return 1; if (filePath1 < filePath2) return -1; return 0; } const PLUGIN_ERROR_OPTIONS = { showStack: true }; function createPluginError(error) { const PluginError = require('plugin-error'); if (error instanceof PluginError) return error; if (error == null) error = 'Unknown Error'; const pluginError = PluginError('gulp-eslint-new', error, PLUGIN_ERROR_OPTIONS); return pluginError; } const { defineProperty } = Object; /** Determine if the specified object has the indicated property as its own property. */ const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty); /** * Determine if a message is an error. * * @param {LintMessage} { severity } - An ESLint message. * @returns {boolean} Whether the message is an error message. */ const isErrorMessage = ({ severity }) => severity > 1; function isEslintrcESLintConstructor({ name, version }) { const { satisfies } = require('semver'); return name === 'ESLint' === satisfies(version, '8'); } /** * Determine if a message is a fatal error. * * @param {LintMessage} { fatal, severity } - An ESLint message. * @returns {boolean} Whether the message is a fatal error message. */ const isFatalErrorMessage = ({ fatal, severity }) => !!fatal && severity > 1; /** * Determine if a message is a fixable error. * * @param {LintMessage} { fix, severity } - An ESLint message. * @returns {boolean} Whether the message is a fixable error message. */ const isFixableErrorMessage = ({ fix, severity }) => fix !== undefined && severity > 1; /** * Determine if a message is a fixable warning. * * @param {LintMessage} { fix, severity } - An ESLint message. * @returns {boolean} Whether the message is a fixable warning message. */ const isFixableWarningMessage = ({ fix, severity }) => fix !== undefined && severity === 1; const isObject = value => Object(value) === value; /** * Determine if a message is a warning. * * @param {LintMessage} { severity } - An ESLint message. * @returns {boolean} Whether the message is a warning message. */ const isWarningMessage = ({ severity }) => severity === 1; exports.ESLINT_PKG = ESLINT_PKG; exports.GULP_DEST_KEY = GULP_DEST_KEY; exports.GULP_WARN_KEY = GULP_WARN_KEY; exports.compareResultsByFilePath = compareResultsByFilePath; const isHiddenRegExp = /(?<![^/\\])\.(?!\.)/u; const isInNodeModulesRegExp = /(?<![^/\\])node_modules[/\\]/u; /** * This is a remake of the CLI engine `createIgnoreResult` function with no reference to ESLint CLI * options and with a better detection of the ignore reason in some edge cases. * * @param {string} filePath - Absolute path of checked code file. * @param {string} baseDir - Absolute path of base directory. * @param {{ name: string, version: string }} eslintConstructor - ESLint constructor. * @returns {LintResult} Result with warning by ignore settings. */ exports.createIgnoreResult = (filePath, baseDir, eslintConstructor) => { const { ltr } = require('semver'); let message; const relativePath = relative(baseDir, filePath); if (isEslintrcESLintConstructor(eslintConstructor)) { if (isHiddenRegExp.test(relativePath)) { message = 'File ignored by default. Use a negated ignore pattern (like ' + '"!<relative/path/to/filename>") to override.'; } else if (isInNodeModulesRegExp.test(relativePath)) { message = 'File ignored by default. Use a negated ignore pattern like "!**/node_modules/*" to ' + 'override.'; } else { message = 'File ignored because of a matching ignore pattern. Set "ignore" option to false to ' + 'override.'; } } else { if (relativePath.startsWith('..')) message = 'File ignored because outside of base path.'; else if (isInNodeModulesRegExp.test(relativePath)) { message = 'File ignored by default because it is located under the node_modules directory. Use ' + 'ignore pattern "!**/node_modules/**" to override.'; } else { message = 'File ignored. If this file is matched by a global ignore pattern, it can be ' + 'unignored by setting the "ignore" option to false.'; } } const result = { filePath, messages: [{ fatal: false, severity: 1, message }], errorCount: 0, warningCount: 1, fixableErrorCount: 0, fixableWarningCount: 0, fatalErrorCount: 0, }; if (!ltr(eslintConstructor.version, '8.8')) result.suppressedMessages = []; return result; }; exports.createPluginError = createPluginError; async function awaitHandler(handler, data, done) { try { await handler(); } catch (error) { const pluginError = createPluginError(error); done(pluginError); return; } done(null, data); } /** * Create a transform stream in object mode from synchronous or asynchronous handler functions. * All files are passed through the stream. * Errors thrown by the handlers will be wrapped in a `PluginError` and emitted from the stream. * * @param {Function} handleFile * A function that is called for each file, with the file object as the only parameter. * If the function returns a promise, the file will be passed through the stream after the promise * is resolved. * * @param {Function} [handleFinal] * A function that is called with no parameters before closing the stream. * If the function returns a promise, the stream will be closed after the promise is resolved. * * @returns {Transform} A transform stream. */ exports.createTransform = (handleFile, handleFinal) => { const transform = (file, enc, done) => void awaitHandler(() => handleFile(file), file, done); const final = handleFinal ? done => void awaitHandler(handleFinal, null, done) : undefined; return new Transform({ autoDestroy: false, objectMode: true, transform, final }); }; /** * @callback CountMessagePredicate * @param {LintMessage} message - ESLint message. * @returns {boolean} `true` or `false`, depending on the input. */ /** * Count the number of messages for which a predicate function returns `true`. * * @param {LintMessage[]} messages - ESLint messages to count. * @param {CountMessagePredicate} predicate - Function to call for each message. * @returns {number} The number of messages for which the predicate function returns `true`. */ function countMessages(messages, predicate) { let count = 0; for (const message of messages) { if (predicate(message)) ++count; } return count; } /** * Filter result messages, update error and warning counts. * * @param {LintResult} result - An ESLint result. * @param {Function} filter - A function that evaluates what messages to keep. * @returns {LintResult} A filtered ESLint result. */ exports.filterResult = (result, filter) => { const { messages, ...newResult } = result; const newMessages = messages.filter(filter, result); newResult.messages = newMessages; newResult.errorCount = countMessages(newMessages, isErrorMessage); newResult.warningCount = countMessages(newMessages, isWarningMessage); newResult.fixableErrorCount = countMessages(newMessages, isFixableErrorMessage); newResult.fixableWarningCount = countMessages(newMessages, isFixableWarningMessage); newResult.fatalErrorCount = countMessages(newMessages, isFatalErrorMessage); return newResult; }; exports.hasOwn = hasOwn; exports.isErrorMessage = isErrorMessage; exports.isWarningMessage = isWarningMessage; const makeNPMLink = anchor => { const { version } = require('gulp-eslint-new/package.json'); const npmLink = `https://www.npmjs.com/package/gulp-eslint-new/v/${version}#${anchor}`; return npmLink; }; exports.makeNPMLink = makeNPMLink; const FORBIDDEN_OPTIONS = [ 'cache', 'cacheFile', 'cacheLocation', 'cacheStrategy', 'errorOnUnmatchedPattern', 'extensions', 'globInputPaths', 'passOnNoPatterns', ]; const requireESLint = eslintPkg => require(eslintPkg).ESLint; function requireFlatESLint(eslintPkg) { const { FlatESLint } = require(`${eslintPkg}/use-at-your-own-risk`); if (FlatESLint == null) { const message = 'The version of ESLint you are using does not support flat config. ' + 'To use flat config, upgrade to ESLint 8.21 or later.'; throw Error(message); } return FlatESLint; } const requireEslintrcESLint = eslintPkg => require(`${eslintPkg}/use-at-your-own-risk`).LegacyESLint || require(eslintPkg).ESLint; /** * Throws an error about invalid options passed to gulp-eslint-new. * * @param {string} message - The error message. * @throws An error with code `"ESLINT_INVALID_OPTIONS"` and the specified message. */ function throwInvalidOptionError(message) { const error = Error(message); Error.captureStackTrace(error, throwInvalidOptionError); error.code = 'ESLINT_INVALID_OPTIONS'; throw error; } /** * Organize and partially validate the options passed to gulp-eslint-new. * * @param {Object.<string | symbol, unknown>} [options] - Options to organize. * @returns {OrganizedOptions} Organized options. * * @typedef {Object} OrganizedOptions * @property {Function} [ESLint] * @property {Object.<string, unknown>} eslintOptions * @property {boolean | Function} [quiet] * @property {boolean} [warnIgnored] */ exports.organizeOptions = (options = { }) => { if (typeof options === 'string') { const organizedOptions = { ESLint: requireESLint('eslint'), eslintOptions: { cwd: process.cwd(), overrideConfigFile: options }, }; return organizedOptions; } { const invalidOptions = FORBIDDEN_OPTIONS.filter(option => hasOwn(options, option)); if (invalidOptions.length) throwInvalidOptionError(`Invalid options: ${invalidOptions.join(', ')}`); } const { [ESLINT_PKG]: eslintPkg = 'eslint', configType, quiet, warnIgnored, ...eslintOptions } = options; if (configType != null && configType !== 'eslintrc' && configType !== 'flat') throw Error('Option configType must be one of "eslintrc", "flat", null, or undefined'); { const type = typeof quiet; if (type !== 'boolean' && type !== 'function' && type !== 'undefined') throw Error('Option quiet must be a boolean, a function, or undefined'); } { const type = typeof warnIgnored; if (type !== 'boolean' && type !== 'undefined') throw Error('Option warnIgnored must be a boolean or undefined'); } let requireFn; switch (configType) { default: requireFn = requireESLint; break; case 'eslintrc': requireFn = requireEslintrcESLint; break; case 'flat': requireFn = requireFlatESLint; break; } const ESLint = requireFn(eslintPkg); const organizedOptions = { ESLint, eslintOptions, quiet, warnIgnored }; { const { cwd } = eslintOptions; eslintOptions.cwd = cwd === undefined ? process.cwd() : typeof cwd === 'string' ? normalize(cwd) : cwd; } return organizedOptions; }; /** * Resolve a formatter from a string. * If a function is specified, it will be treated as a formatter function and wrapped in an object * appropriately. * * @param {{ cwd: string, eslint: ESLint }} eslintInfo * Current directory and instance of ESLint used to load and configure the formatter. * * @param {string | LoadedFormatter | FormatterFunction} [formatter] * A name or path of a formatter, a formatter object or a formatter function. * * @returns {Promise<LoadedFormatter>} An ESLint formatter. */ exports.resolveFormatter = async ({ cwd, eslint }, formatter) => { if (formatter === undefined) { const { format } = await eslint.loadFormatter(); return { format: results => format(results) .replace ( / with the `--fix` option\.(?=(\u001b\[\d+m|\n)+$)/, ` - see ${makeNPMLink('autofix')}`, ), }; } if (isObject(formatter) && typeof formatter.format === 'function') return formatter; if (typeof formatter === 'function') { return { format: results => { results.sort(compareResultsByFilePath); return formatter ( results, { cwd, get rulesMeta() { const rulesMeta = eslint.getRulesMetaForResults(results); defineProperty(this, 'rulesMeta', { value: rulesMeta }); return rulesMeta; }, }, ); }, }; } // Use ESLint to look up formatter references. return eslint.loadFormatter(formatter); }; /** * Resolve a writer function used to write formatted ESLint messages. * * @param {Writer | NodeJS.WritableStream} [writer=require('fancy-log')] * A stream or function to resolve as a format writer. * @returns {Writer} A function that writes formatted messages. */ exports.resolveWriter = (writer = require('fancy-log')) => { if (isObject(writer)) { const { write } = writer; if (typeof write === 'function') writer = write.bind(writer); } return writer; }; /** * Write formatted ESLint messages. * * @param {LintResult[]} results * A list of ESLint results. * * @param {LoadedFormatter} formatterObj * A formatter object. * * @param {Writer} [writer] * A function used to write formatted ESLint messages. */ exports.writeResults = async (results, formatterObj, writer) => { const message = await formatterObj.format(results, { }); if (writer && message != null && message !== '') await writer(message); };