Update checked-in dependencies

This commit is contained in:
github-actions[bot] 2021-07-27 16:54:26 +00:00
parent 6b0d45a5c6
commit cc1adb825a
4247 changed files with 144820 additions and 149530 deletions

View file

@ -1,502 +0,0 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview `CascadingConfigArrayFactory` class.
*
* `CascadingConfigArrayFactory` class has a responsibility:
*
* 1. Handles cascading of config files.
*
* It provides two methods:
*
* - `getConfigArrayForFile(filePath)`
* Get the corresponded configuration of a given file. This method doesn't
* throw even if the given file didn't exist.
* - `clearCache()`
* Clear the internal cache. You have to call this method when
* `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends
* on the additional plugins. (`CLIEngine#addPlugin()` method calls this.)
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const os = require("os");
const path = require("path");
const { validateConfigArray } = require("../shared/config-validator");
const { emitDeprecationWarning } = require("../shared/deprecation-warnings");
const { ConfigArrayFactory } = require("./config-array-factory");
const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array");
const loadRules = require("./load-rules");
const debug = require("debug")("eslint:cascading-config-array-factory");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
// Define types for VSCode IntelliSense.
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").Parser} Parser */
/** @typedef {import("../shared/types").Plugin} Plugin */
/** @typedef {ReturnType<ConfigArrayFactory["create"]>} ConfigArray */
/**
* @typedef {Object} CascadingConfigArrayFactoryOptions
* @property {Map<string,Plugin>} [additionalPluginPool] The map for additional plugins.
* @property {ConfigData} [baseConfig] The config by `baseConfig` option.
* @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--ignore-pattern`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files.
* @property {string} [cwd] The base directory to start lookup.
* @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
* @property {string[]} [rulePaths] The value of `--rulesdir` option.
* @property {string} [specificConfigPath] The value of `--config` option.
* @property {boolean} [useEslintrc] if `false` then it doesn't load config files.
*/
/**
* @typedef {Object} CascadingConfigArrayFactoryInternalSlots
* @property {ConfigArray} baseConfigArray The config array of `baseConfig` option.
* @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`.
* @property {ConfigArray} cliConfigArray The config array of CLI options.
* @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`.
* @property {ConfigArrayFactory} configArrayFactory The factory for config arrays.
* @property {Map<string, ConfigArray>} configCache The cache from directory paths to config arrays.
* @property {string} cwd The base directory to start lookup.
* @property {WeakMap<ConfigArray, ConfigArray>} finalizeCache The cache from config arrays to finalized config arrays.
* @property {string} [ignorePath] The path to the alternative file of `.eslintignore`.
* @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`.
* @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`.
* @property {boolean} useEslintrc if `false` then it doesn't load config files.
*/
/** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* Create the config array from `baseConfig` and `rulePaths`.
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
* @returns {ConfigArray} The config array of the base configs.
*/
function createBaseConfigArray({
configArrayFactory,
baseConfigData,
rulePaths,
cwd
}) {
const baseConfigArray = configArrayFactory.create(
baseConfigData,
{ name: "BaseConfig" }
);
/*
* Create the config array element for the default ignore patterns.
* This element has `ignorePattern` property that ignores the default
* patterns in the current working directory.
*/
baseConfigArray.unshift(configArrayFactory.create(
{ ignorePatterns: IgnorePattern.DefaultPatterns },
{ name: "DefaultIgnorePattern" }
)[0]);
/*
* Load rules `--rulesdir` option as a pseudo plugin.
* Use a pseudo plugin to define rules of `--rulesdir`, so we can validate
* the rule's options with only information in the config array.
*/
if (rulePaths && rulePaths.length > 0) {
baseConfigArray.push({
type: "config",
name: "--rulesdir",
filePath: "",
plugins: {
"": new ConfigDependency({
definition: {
rules: rulePaths.reduce(
(map, rulesPath) => Object.assign(
map,
loadRules(rulesPath, cwd)
),
{}
)
},
filePath: "",
id: "",
importerName: "--rulesdir",
importerPath: ""
})
}
});
}
return baseConfigArray;
}
/**
* Create the config array from CLI options.
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
* @returns {ConfigArray} The config array of the base configs.
*/
function createCLIConfigArray({
cliConfigData,
configArrayFactory,
cwd,
ignorePath,
specificConfigPath
}) {
const cliConfigArray = configArrayFactory.create(
cliConfigData,
{ name: "CLIOptions" }
);
cliConfigArray.unshift(
...(ignorePath
? configArrayFactory.loadESLintIgnore(ignorePath)
: configArrayFactory.loadDefaultESLintIgnore())
);
if (specificConfigPath) {
cliConfigArray.unshift(
...configArrayFactory.loadFile(
specificConfigPath,
{ name: "--config", basePath: cwd }
)
);
}
return cliConfigArray;
}
/**
* The error type when there are files matched by a glob, but all of them have been ignored.
*/
class ConfigurationNotFoundError extends Error {
// eslint-disable-next-line jsdoc/require-description
/**
* @param {string} directoryPath The directory path.
*/
constructor(directoryPath) {
super(`No ESLint configuration found in ${directoryPath}.`);
this.messageTemplate = "no-config-found";
this.messageData = { directoryPath };
}
}
/**
* This class provides the functionality that enumerates every file which is
* matched by given glob patterns and that configuration.
*/
class CascadingConfigArrayFactory {
/**
* Initialize this enumerator.
* @param {CascadingConfigArrayFactoryOptions} options The options.
*/
constructor({
additionalPluginPool = new Map(),
baseConfig: baseConfigData = null,
cliConfig: cliConfigData = null,
cwd = process.cwd(),
ignorePath,
resolvePluginsRelativeTo,
rulePaths = [],
specificConfigPath = null,
useEslintrc = true
} = {}) {
const configArrayFactory = new ConfigArrayFactory({
additionalPluginPool,
cwd,
resolvePluginsRelativeTo
});
internalSlotsMap.set(this, {
baseConfigArray: createBaseConfigArray({
baseConfigData,
configArrayFactory,
cwd,
rulePaths
}),
baseConfigData,
cliConfigArray: createCLIConfigArray({
cliConfigData,
configArrayFactory,
cwd,
ignorePath,
specificConfigPath
}),
cliConfigData,
configArrayFactory,
configCache: new Map(),
cwd,
finalizeCache: new WeakMap(),
ignorePath,
rulePaths,
specificConfigPath,
useEslintrc
});
}
/**
* The path to the current working directory.
* This is used by tests.
* @type {string}
*/
get cwd() {
const { cwd } = internalSlotsMap.get(this);
return cwd;
}
/**
* Get the config array of a given file.
* If `filePath` was not given, it returns the config which contains only
* `baseConfigData` and `cliConfigData`.
* @param {string} [filePath] The file path to a file.
* @param {Object} [options] The options.
* @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
* @returns {ConfigArray} The config array of the file.
*/
getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) {
const {
baseConfigArray,
cliConfigArray,
cwd
} = internalSlotsMap.get(this);
if (!filePath) {
return new ConfigArray(...baseConfigArray, ...cliConfigArray);
}
const directoryPath = path.dirname(path.resolve(cwd, filePath));
debug(`Load config files for ${directoryPath}.`);
return this._finalizeConfigArray(
this._loadConfigInAncestors(directoryPath),
directoryPath,
ignoreNotFoundError
);
}
/**
* Set the config data to override all configs.
* Require to call `clearCache()` method after this method is called.
* @param {ConfigData} configData The config data to override all configs.
* @returns {void}
*/
setOverrideConfig(configData) {
const slots = internalSlotsMap.get(this);
slots.cliConfigData = configData;
}
/**
* Clear config cache.
* @returns {void}
*/
clearCache() {
const slots = internalSlotsMap.get(this);
slots.baseConfigArray = createBaseConfigArray(slots);
slots.cliConfigArray = createCLIConfigArray(slots);
slots.configCache.clear();
}
/**
* Load and normalize config files from the ancestor directories.
* @param {string} directoryPath The path to a leaf directory.
* @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories.
* @returns {ConfigArray} The loaded config.
* @private
*/
_loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) {
const {
baseConfigArray,
configArrayFactory,
configCache,
cwd,
useEslintrc
} = internalSlotsMap.get(this);
if (!useEslintrc) {
return baseConfigArray;
}
let configArray = configCache.get(directoryPath);
// Hit cache.
if (configArray) {
debug(`Cache hit: ${directoryPath}.`);
return configArray;
}
debug(`No cache found: ${directoryPath}.`);
const homePath = os.homedir();
// Consider this is root.
if (directoryPath === homePath && cwd !== homePath) {
debug("Stop traversing because of considered root.");
if (configsExistInSubdirs) {
const filePath = ConfigArrayFactory.getPathToConfigFileInDirectory(directoryPath);
if (filePath) {
emitDeprecationWarning(
filePath,
"ESLINT_PERSONAL_CONFIG_SUPPRESS"
);
}
}
return this._cacheConfig(directoryPath, baseConfigArray);
}
// Load the config on this directory.
try {
configArray = configArrayFactory.loadInDirectory(directoryPath);
} catch (error) {
/* istanbul ignore next */
if (error.code === "EACCES") {
debug("Stop traversing because of 'EACCES' error.");
return this._cacheConfig(directoryPath, baseConfigArray);
}
throw error;
}
if (configArray.length > 0 && configArray.isRoot()) {
debug("Stop traversing because of 'root:true'.");
configArray.unshift(...baseConfigArray);
return this._cacheConfig(directoryPath, configArray);
}
// Load from the ancestors and merge it.
const parentPath = path.dirname(directoryPath);
const parentConfigArray = parentPath && parentPath !== directoryPath
? this._loadConfigInAncestors(
parentPath,
configsExistInSubdirs || configArray.length > 0
)
: baseConfigArray;
if (configArray.length > 0) {
configArray.unshift(...parentConfigArray);
} else {
configArray = parentConfigArray;
}
// Cache and return.
return this._cacheConfig(directoryPath, configArray);
}
/**
* Freeze and cache a given config.
* @param {string} directoryPath The path to a directory as a cache key.
* @param {ConfigArray} configArray The config array as a cache value.
* @returns {ConfigArray} The `configArray` (frozen).
*/
_cacheConfig(directoryPath, configArray) {
const { configCache } = internalSlotsMap.get(this);
Object.freeze(configArray);
configCache.set(directoryPath, configArray);
return configArray;
}
/**
* Finalize a given config array.
* Concatenate `--config` and other CLI options.
* @param {ConfigArray} configArray The parent config array.
* @param {string} directoryPath The path to the leaf directory to find config files.
* @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
* @returns {ConfigArray} The loaded config.
* @private
*/
_finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) {
const {
cliConfigArray,
configArrayFactory,
finalizeCache,
useEslintrc
} = internalSlotsMap.get(this);
let finalConfigArray = finalizeCache.get(configArray);
if (!finalConfigArray) {
finalConfigArray = configArray;
// Load the personal config if there are no regular config files.
if (
useEslintrc &&
configArray.every(c => !c.filePath) &&
cliConfigArray.every(c => !c.filePath) // `--config` option can be a file.
) {
const homePath = os.homedir();
debug("Loading the config file of the home directory:", homePath);
const personalConfigArray = configArrayFactory.loadInDirectory(
homePath,
{ name: "PersonalConfig" }
);
if (
personalConfigArray.length > 0 &&
!directoryPath.startsWith(homePath)
) {
const lastElement =
personalConfigArray[personalConfigArray.length - 1];
emitDeprecationWarning(
lastElement.filePath,
"ESLINT_PERSONAL_CONFIG_LOAD"
);
}
finalConfigArray = finalConfigArray.concat(personalConfigArray);
}
// Apply CLI options.
if (cliConfigArray.length > 0) {
finalConfigArray = finalConfigArray.concat(cliConfigArray);
}
// Validate rule settings and environments.
validateConfigArray(finalConfigArray);
// Cache it.
Object.freeze(finalConfigArray);
finalizeCache.set(configArray, finalConfigArray);
debug(
"Configuration was determined: %o on %s",
finalConfigArray,
directoryPath
);
}
// At least one element (the default ignore patterns) exists.
if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) {
throw new ConfigurationNotFoundError(directoryPath);
}
return finalConfigArray;
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = { CascadingConfigArrayFactory };

View file

@ -19,14 +19,24 @@ const fs = require("fs");
const path = require("path");
const defaultOptions = require("../../conf/default-cli-options");
const pkg = require("../../package.json");
const ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops");
const naming = require("@eslint/eslintrc/lib/shared/naming");
const ModuleResolver = require("../shared/relative-module-resolver");
const {
Legacy: {
ConfigOps,
naming,
CascadingConfigArrayFactory,
IgnorePattern,
getUsedExtractedConfigs,
ModuleResolver
}
} = require("@eslint/eslintrc");
const { FileEnumerator } = require("./file-enumerator");
const { Linter } = require("../linter");
const builtInRules = require("../rules");
const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory");
const { IgnorePattern, getUsedExtractedConfigs } = require("./config-array");
const { FileEnumerator } = require("./file-enumerator");
const loadRules = require("./load-rules");
const hash = require("./hash");
const LintResultCache = require("./lint-result-cache");
@ -516,7 +526,7 @@ function directoryExists(resolvedPath) {
try {
return fs.statSync(resolvedPath).isDirectory();
} catch (error) {
if (error && error.code === "ENOENT") {
if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
return false;
}
throw error;
@ -559,7 +569,11 @@ class CLIEngine {
resolvePluginsRelativeTo: options.resolvePluginsRelativeTo,
rulePaths: options.rulePaths,
specificConfigPath: options.configFile,
useEslintrc: options.useEslintrc
useEslintrc: options.useEslintrc,
builtInRules,
loadRules,
eslintRecommendedPath: path.resolve(__dirname, "../../conf/eslint-recommended.js"),
eslintAllPath: path.resolve(__dirname, "../../conf/eslint-all.js")
});
const fileEnumerator = new FileEnumerator({
configArrayFactory,
@ -570,7 +584,7 @@ class CLIEngine {
ignore: options.ignore
});
const lintResultCache =
options.cache ? new LintResultCache(cacheFilePath) : null;
options.cache ? new LintResultCache(cacheFilePath, options.cacheStrategy) : null;
const linter = new Linter({ cwd: options.cwd });
/** @type {ConfigArray[]} */

File diff suppressed because it is too large Load diff

View file

@ -1,536 +0,0 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview `ConfigArray` class.
*
* `ConfigArray` class expresses the full of a configuration. It has the entry
* config file, base config files that were extended, loaded parsers, and loaded
* plugins.
*
* `ConfigArray` class provides three properties and two methods.
*
* - `pluginEnvironments`
* - `pluginProcessors`
* - `pluginRules`
* The `Map` objects that contain the members of all plugins that this
* config array contains. Those map objects don't have mutation methods.
* Those keys are the member ID such as `pluginId/memberName`.
* - `isRoot()`
* If `true` then this configuration has `root:true` property.
* - `extractConfig(filePath)`
* Extract the final configuration for a given file. This means merging
* every config array element which that `criteria` property matched. The
* `filePath` argument must be an absolute path.
*
* `ConfigArrayFactory` provides the loading logic of config files.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const { ExtractedConfig } = require("./extracted-config");
const { IgnorePattern } = require("./ignore-pattern");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
// Define types for VSCode IntelliSense.
/** @typedef {import("../../shared/types").Environment} Environment */
/** @typedef {import("../../shared/types").GlobalConf} GlobalConf */
/** @typedef {import("../../shared/types").RuleConf} RuleConf */
/** @typedef {import("../../shared/types").Rule} Rule */
/** @typedef {import("../../shared/types").Plugin} Plugin */
/** @typedef {import("../../shared/types").Processor} Processor */
/** @typedef {import("./config-dependency").DependentParser} DependentParser */
/** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */
/** @typedef {import("./override-tester")["OverrideTester"]} OverrideTester */
/**
* @typedef {Object} ConfigArrayElement
* @property {string} name The name of this config element.
* @property {string} filePath The path to the source file of this config element.
* @property {InstanceType<OverrideTester>|null} criteria The tester for the `files` and `excludedFiles` of this config element.
* @property {Record<string, boolean>|undefined} env The environment settings.
* @property {Record<string, GlobalConf>|undefined} globals The global variable settings.
* @property {IgnorePattern|undefined} ignorePattern The ignore patterns.
* @property {boolean|undefined} noInlineConfig The flag that disables directive comments.
* @property {DependentParser|undefined} parser The parser loader.
* @property {Object|undefined} parserOptions The parser options.
* @property {Record<string, DependentPlugin>|undefined} plugins The plugin loaders.
* @property {string|undefined} processor The processor name to refer plugin's processor.
* @property {boolean|undefined} reportUnusedDisableDirectives The flag to report unused `eslint-disable` comments.
* @property {boolean|undefined} root The flag to express root.
* @property {Record<string, RuleConf>|undefined} rules The rule settings
* @property {Object|undefined} settings The shared settings.
* @property {"config" | "ignore" | "implicit-processor"} type The element type.
*/
/**
* @typedef {Object} ConfigArrayInternalSlots
* @property {Map<string, ExtractedConfig>} cache The cache to extract configs.
* @property {ReadonlyMap<string, Environment>|null} envMap The map from environment ID to environment definition.
* @property {ReadonlyMap<string, Processor>|null} processorMap The map from processor ID to environment definition.
* @property {ReadonlyMap<string, Rule>|null} ruleMap The map from rule ID to rule definition.
*/
/** @type {WeakMap<ConfigArray, ConfigArrayInternalSlots>} */
const internalSlotsMap = new class extends WeakMap {
get(key) {
let value = super.get(key);
if (!value) {
value = {
cache: new Map(),
envMap: null,
processorMap: null,
ruleMap: null
};
super.set(key, value);
}
return value;
}
}();
/**
* Get the indices which are matched to a given file.
* @param {ConfigArrayElement[]} elements The elements.
* @param {string} filePath The path to a target file.
* @returns {number[]} The indices.
*/
function getMatchedIndices(elements, filePath) {
const indices = [];
for (let i = elements.length - 1; i >= 0; --i) {
const element = elements[i];
if (!element.criteria || (filePath && element.criteria.test(filePath))) {
indices.push(i);
}
}
return indices;
}
/**
* Check if a value is a non-null object.
* @param {any} x The value to check.
* @returns {boolean} `true` if the value is a non-null object.
*/
function isNonNullObject(x) {
return typeof x === "object" && x !== null;
}
/**
* Merge two objects.
*
* Assign every property values of `y` to `x` if `x` doesn't have the property.
* If `x`'s property value is an object, it does recursive.
* @param {Object} target The destination to merge
* @param {Object|undefined} source The source to merge.
* @returns {void}
*/
function mergeWithoutOverwrite(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
if (isNonNullObject(target[key])) {
mergeWithoutOverwrite(target[key], source[key]);
} else if (target[key] === void 0) {
if (isNonNullObject(source[key])) {
target[key] = Array.isArray(source[key]) ? [] : {};
mergeWithoutOverwrite(target[key], source[key]);
} else if (source[key] !== void 0) {
target[key] = source[key];
}
}
}
}
/**
* The error for plugin conflicts.
*/
class PluginConflictError extends Error {
/**
* Initialize this error object.
* @param {string} pluginId The plugin ID.
* @param {{filePath:string, importerName:string}[]} plugins The resolved plugins.
*/
constructor(pluginId, plugins) {
super(`Plugin "${pluginId}" was conflicted between ${plugins.map(p => `"${p.importerName}"`).join(" and ")}.`);
this.messageTemplate = "plugin-conflict";
this.messageData = { pluginId, plugins };
}
}
/**
* Merge plugins.
* `target`'s definition is prior to `source`'s.
* @param {Record<string, DependentPlugin>} target The destination to merge
* @param {Record<string, DependentPlugin>|undefined} source The source to merge.
* @returns {void}
*/
function mergePlugins(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
const targetValue = target[key];
const sourceValue = source[key];
// Adopt the plugin which was found at first.
if (targetValue === void 0) {
if (sourceValue.error) {
throw sourceValue.error;
}
target[key] = sourceValue;
} else if (sourceValue.filePath !== targetValue.filePath) {
throw new PluginConflictError(key, [
{
filePath: targetValue.filePath,
importerName: targetValue.importerName
},
{
filePath: sourceValue.filePath,
importerName: sourceValue.importerName
}
]);
}
}
}
/**
* Merge rule configs.
* `target`'s definition is prior to `source`'s.
* @param {Record<string, Array>} target The destination to merge
* @param {Record<string, RuleConf>|undefined} source The source to merge.
* @returns {void}
*/
function mergeRuleConfigs(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
const targetDef = target[key];
const sourceDef = source[key];
// Adopt the rule config which was found at first.
if (targetDef === void 0) {
if (Array.isArray(sourceDef)) {
target[key] = [...sourceDef];
} else {
target[key] = [sourceDef];
}
/*
* If the first found rule config is severity only and the current rule
* config has options, merge the severity and the options.
*/
} else if (
targetDef.length === 1 &&
Array.isArray(sourceDef) &&
sourceDef.length >= 2
) {
targetDef.push(...sourceDef.slice(1));
}
}
}
/**
* Create the extracted config.
* @param {ConfigArray} instance The config elements.
* @param {number[]} indices The indices to use.
* @returns {ExtractedConfig} The extracted config.
*/
function createConfig(instance, indices) {
const config = new ExtractedConfig();
const ignorePatterns = [];
// Merge elements.
for (const index of indices) {
const element = instance[index];
// Adopt the parser which was found at first.
if (!config.parser && element.parser) {
if (element.parser.error) {
throw element.parser.error;
}
config.parser = element.parser;
}
// Adopt the processor which was found at first.
if (!config.processor && element.processor) {
config.processor = element.processor;
}
// Adopt the noInlineConfig which was found at first.
if (config.noInlineConfig === void 0 && element.noInlineConfig !== void 0) {
config.noInlineConfig = element.noInlineConfig;
config.configNameOfNoInlineConfig = element.name;
}
// Adopt the reportUnusedDisableDirectives which was found at first.
if (config.reportUnusedDisableDirectives === void 0 && element.reportUnusedDisableDirectives !== void 0) {
config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives;
}
// Collect ignorePatterns
if (element.ignorePattern) {
ignorePatterns.push(element.ignorePattern);
}
// Merge others.
mergeWithoutOverwrite(config.env, element.env);
mergeWithoutOverwrite(config.globals, element.globals);
mergeWithoutOverwrite(config.parserOptions, element.parserOptions);
mergeWithoutOverwrite(config.settings, element.settings);
mergePlugins(config.plugins, element.plugins);
mergeRuleConfigs(config.rules, element.rules);
}
// Create the predicate function for ignore patterns.
if (ignorePatterns.length > 0) {
config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse());
}
return config;
}
/**
* Collect definitions.
* @template T, U
* @param {string} pluginId The plugin ID for prefix.
* @param {Record<string,T>} defs The definitions to collect.
* @param {Map<string, U>} map The map to output.
* @param {function(T): U} [normalize] The normalize function for each value.
* @returns {void}
*/
function collect(pluginId, defs, map, normalize) {
if (defs) {
const prefix = pluginId && `${pluginId}/`;
for (const [key, value] of Object.entries(defs)) {
map.set(
`${prefix}${key}`,
normalize ? normalize(value) : value
);
}
}
}
/**
* Normalize a rule definition.
* @param {Function|Rule} rule The rule definition to normalize.
* @returns {Rule} The normalized rule definition.
*/
function normalizePluginRule(rule) {
return typeof rule === "function" ? { create: rule } : rule;
}
/**
* Delete the mutation methods from a given map.
* @param {Map<any, any>} map The map object to delete.
* @returns {void}
*/
function deleteMutationMethods(map) {
Object.defineProperties(map, {
clear: { configurable: true, value: void 0 },
delete: { configurable: true, value: void 0 },
set: { configurable: true, value: void 0 }
});
}
/**
* Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
* @param {ConfigArrayElement[]} elements The config elements.
* @param {ConfigArrayInternalSlots} slots The internal slots.
* @returns {void}
*/
function initPluginMemberMaps(elements, slots) {
const processed = new Set();
slots.envMap = new Map();
slots.processorMap = new Map();
slots.ruleMap = new Map();
for (const element of elements) {
if (!element.plugins) {
continue;
}
for (const [pluginId, value] of Object.entries(element.plugins)) {
const plugin = value.definition;
if (!plugin || processed.has(pluginId)) {
continue;
}
processed.add(pluginId);
collect(pluginId, plugin.environments, slots.envMap);
collect(pluginId, plugin.processors, slots.processorMap);
collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule);
}
}
deleteMutationMethods(slots.envMap);
deleteMutationMethods(slots.processorMap);
deleteMutationMethods(slots.ruleMap);
}
/**
* Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array.
* @param {ConfigArray} instance The config elements.
* @returns {ConfigArrayInternalSlots} The extracted config.
*/
function ensurePluginMemberMaps(instance) {
const slots = internalSlotsMap.get(instance);
if (!slots.ruleMap) {
initPluginMemberMaps(instance, slots);
}
return slots;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* The Config Array.
*
* `ConfigArray` instance contains all settings, parsers, and plugins.
* You need to call `ConfigArray#extractConfig(filePath)` method in order to
* extract, merge and get only the config data which is related to an arbitrary
* file.
* @extends {Array<ConfigArrayElement>}
*/
class ConfigArray extends Array {
/**
* Get the plugin environments.
* The returned map cannot be mutated.
* @type {ReadonlyMap<string, Environment>} The plugin environments.
*/
get pluginEnvironments() {
return ensurePluginMemberMaps(this).envMap;
}
/**
* Get the plugin processors.
* The returned map cannot be mutated.
* @type {ReadonlyMap<string, Processor>} The plugin processors.
*/
get pluginProcessors() {
return ensurePluginMemberMaps(this).processorMap;
}
/**
* Get the plugin rules.
* The returned map cannot be mutated.
* @returns {ReadonlyMap<string, Rule>} The plugin rules.
*/
get pluginRules() {
return ensurePluginMemberMaps(this).ruleMap;
}
/**
* Check if this config has `root` flag.
* @returns {boolean} `true` if this config array is root.
*/
isRoot() {
for (let i = this.length - 1; i >= 0; --i) {
const root = this[i].root;
if (typeof root === "boolean") {
return root;
}
}
return false;
}
/**
* Extract the config data which is related to a given file.
* @param {string} filePath The absolute path to the target file.
* @returns {ExtractedConfig} The extracted config data.
*/
extractConfig(filePath) {
const { cache } = internalSlotsMap.get(this);
const indices = getMatchedIndices(this, filePath);
const cacheKey = indices.join(",");
if (!cache.has(cacheKey)) {
cache.set(cacheKey, createConfig(this, indices));
}
return cache.get(cacheKey);
}
/**
* Check if a given path is an additional lint target.
* @param {string} filePath The absolute path to the target file.
* @returns {boolean} `true` if the file is an additional lint target.
*/
isAdditionalTargetPath(filePath) {
for (const { criteria, type } of this) {
if (
type === "config" &&
criteria &&
!criteria.endsWithWildcard &&
criteria.test(filePath)
) {
return true;
}
}
return false;
}
}
const exportObject = {
ConfigArray,
/**
* Get the used extracted configs.
* CLIEngine will use this method to collect used deprecated rules.
* @param {ConfigArray} instance The config array object to get.
* @returns {ExtractedConfig[]} The used extracted configs.
* @private
*/
getUsedExtractedConfigs(instance) {
const { cache } = internalSlotsMap.get(instance);
return Array.from(cache.values());
}
};
module.exports = exportObject;

View file

@ -1,128 +0,0 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview `ConfigDependency` class.
*
* `ConfigDependency` class expresses a loaded parser or plugin.
*
* If the parser or plugin was loaded successfully, it has `definition` property
* and `filePath` property. Otherwise, it has `error` property.
*
* When `JSON.stringify()` converted a `ConfigDependency` object to a JSON, it
* omits `definition` property.
*
* `ConfigArrayFactory` creates `ConfigDependency` objects when it loads parsers
* or plugins.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
const util = require("util");
/**
* The class is to store parsers or plugins.
* This class hides the loaded object from `JSON.stringify()` and `console.log`.
* @template T
*/
class ConfigDependency {
/**
* Initialize this instance.
* @param {Object} data The dependency data.
* @param {T} [data.definition] The dependency if the loading succeeded.
* @param {Error} [data.error] The error object if the loading failed.
* @param {string} [data.filePath] The actual path to the dependency if the loading succeeded.
* @param {string} data.id The ID of this dependency.
* @param {string} data.importerName The name of the config file which loads this dependency.
* @param {string} data.importerPath The path to the config file which loads this dependency.
*/
constructor({
definition = null,
error = null,
filePath = null,
id,
importerName,
importerPath
}) {
/**
* The loaded dependency if the loading succeeded.
* @type {T|null}
*/
this.definition = definition;
/**
* The error object if the loading failed.
* @type {Error|null}
*/
this.error = error;
/**
* The loaded dependency if the loading succeeded.
* @type {string|null}
*/
this.filePath = filePath;
/**
* The ID of this dependency.
* @type {string}
*/
this.id = id;
/**
* The name of the config file which loads this dependency.
* @type {string}
*/
this.importerName = importerName;
/**
* The path to the config file which loads this dependency.
* @type {string}
*/
this.importerPath = importerPath;
}
// eslint-disable-next-line jsdoc/require-description
/**
* @returns {Object} a JSON compatible object.
*/
toJSON() {
const obj = this[util.inspect.custom]();
// Display `error.message` (`Error#message` is unenumerable).
if (obj.error instanceof Error) {
obj.error = { ...obj.error, message: obj.error.message };
}
return obj;
}
// eslint-disable-next-line jsdoc/require-description
/**
* @returns {Object} an object to display by `console.log()`.
*/
[util.inspect.custom]() {
const {
definition: _ignore, // eslint-disable-line no-unused-vars
...obj
} = this;
return obj;
}
}
/** @typedef {ConfigDependency<import("../../shared/types").Parser>} DependentParser */
/** @typedef {ConfigDependency<import("../../shared/types").Plugin>} DependentPlugin */
module.exports = { ConfigDependency };

View file

@ -1,158 +0,0 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview `ExtractedConfig` class.
*
* `ExtractedConfig` class expresses a final configuration for a specific file.
*
* It provides one method.
*
* - `toCompatibleObjectAsConfigFileContent()`
* Convert this configuration to the compatible object as the content of
* config files. It converts the loaded parser and plugins to strings.
* `CLIEngine#getConfigForFile(filePath)` method uses this method.
*
* `ConfigArray#extractConfig(filePath)` creates a `ExtractedConfig` instance.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
const { IgnorePattern } = require("./ignore-pattern");
// For VSCode intellisense
/** @typedef {import("../../shared/types").ConfigData} ConfigData */
/** @typedef {import("../../shared/types").GlobalConf} GlobalConf */
/** @typedef {import("../../shared/types").SeverityConf} SeverityConf */
/** @typedef {import("./config-dependency").DependentParser} DependentParser */
/** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */
/**
* Check if `xs` starts with `ys`.
* @template T
* @param {T[]} xs The array to check.
* @param {T[]} ys The array that may be the first part of `xs`.
* @returns {boolean} `true` if `xs` starts with `ys`.
*/
function startsWith(xs, ys) {
return xs.length >= ys.length && ys.every((y, i) => y === xs[i]);
}
/**
* The class for extracted config data.
*/
class ExtractedConfig {
constructor() {
/**
* The config name what `noInlineConfig` setting came from.
* @type {string}
*/
this.configNameOfNoInlineConfig = "";
/**
* Environments.
* @type {Record<string, boolean>}
*/
this.env = {};
/**
* Global variables.
* @type {Record<string, GlobalConf>}
*/
this.globals = {};
/**
* The glob patterns that ignore to lint.
* @type {(((filePath:string, dot?:boolean) => boolean) & { basePath:string; patterns:string[] }) | undefined}
*/
this.ignores = void 0;
/**
* The flag that disables directive comments.
* @type {boolean|undefined}
*/
this.noInlineConfig = void 0;
/**
* Parser definition.
* @type {DependentParser|null}
*/
this.parser = null;
/**
* Options for the parser.
* @type {Object}
*/
this.parserOptions = {};
/**
* Plugin definitions.
* @type {Record<string, DependentPlugin>}
*/
this.plugins = {};
/**
* Processor ID.
* @type {string|null}
*/
this.processor = null;
/**
* The flag that reports unused `eslint-disable` directive comments.
* @type {boolean|undefined}
*/
this.reportUnusedDisableDirectives = void 0;
/**
* Rule settings.
* @type {Record<string, [SeverityConf, ...any[]]>}
*/
this.rules = {};
/**
* Shared settings.
* @type {Object}
*/
this.settings = {};
}
/**
* Convert this config to the compatible object as a config file content.
* @returns {ConfigData} The converted object.
*/
toCompatibleObjectAsConfigFileContent() {
const {
/* eslint-disable no-unused-vars */
configNameOfNoInlineConfig: _ignore1,
processor: _ignore2,
/* eslint-enable no-unused-vars */
ignores,
...config
} = this;
config.parser = config.parser && config.parser.filePath;
config.plugins = Object.keys(config.plugins).filter(Boolean).reverse();
config.ignorePatterns = ignores ? ignores.patterns : [];
// Strip the default patterns from `ignorePatterns`.
if (startsWith(config.ignorePatterns, IgnorePattern.DefaultPatterns)) {
config.ignorePatterns =
config.ignorePatterns.slice(IgnorePattern.DefaultPatterns.length);
}
return config;
}
}
module.exports = { ExtractedConfig };

View file

@ -1,249 +0,0 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview `IgnorePattern` class.
*
* `IgnorePattern` class has the set of glob patterns and the base path.
*
* It provides two static methods.
*
* - `IgnorePattern.createDefaultIgnore(cwd)`
* Create the default predicate function.
* - `IgnorePattern.createIgnore(ignorePatterns)`
* Create the predicate function from multiple `IgnorePattern` objects.
*
* It provides two properties and a method.
*
* - `patterns`
* The glob patterns that ignore to lint.
* - `basePath`
* The base path of the glob patterns. If absolute paths existed in the
* glob patterns, those are handled as relative paths to the base path.
* - `getPatternsRelativeTo(basePath)`
* Get `patterns` as modified for a given base path. It modifies the
* absolute paths in the patterns as prepending the difference of two base
* paths.
*
* `ConfigArrayFactory` creates `IgnorePattern` objects when it processes
* `ignorePatterns` properties.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("assert");
const path = require("path");
const ignore = require("ignore");
const debug = require("debug")("eslint:ignore-pattern");
/** @typedef {ReturnType<import("ignore").default>} Ignore */
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Get the path to the common ancestor directory of given paths.
* @param {string[]} sourcePaths The paths to calculate the common ancestor.
* @returns {string} The path to the common ancestor directory.
*/
function getCommonAncestorPath(sourcePaths) {
let result = sourcePaths[0];
for (let i = 1; i < sourcePaths.length; ++i) {
const a = result;
const b = sourcePaths[i];
// Set the shorter one (it's the common ancestor if one includes the other).
result = a.length < b.length ? a : b;
// Set the common ancestor.
for (let j = 0, lastSepPos = 0; j < a.length && j < b.length; ++j) {
if (a[j] !== b[j]) {
result = a.slice(0, lastSepPos);
break;
}
if (a[j] === path.sep) {
lastSepPos = j;
}
}
}
let resolvedResult = result || path.sep;
// if Windows common ancestor is root of drive must have trailing slash to be absolute.
if (resolvedResult && resolvedResult.endsWith(":") && process.platform === "win32") {
resolvedResult += path.sep;
}
return resolvedResult;
}
/**
* Make relative path.
* @param {string} from The source path to get relative path.
* @param {string} to The destination path to get relative path.
* @returns {string} The relative path.
*/
function relative(from, to) {
const relPath = path.relative(from, to);
if (path.sep === "/") {
return relPath;
}
return relPath.split(path.sep).join("/");
}
/**
* Get the trailing slash if existed.
* @param {string} filePath The path to check.
* @returns {string} The trailing slash if existed.
*/
function dirSuffix(filePath) {
const isDir = (
filePath.endsWith(path.sep) ||
(process.platform === "win32" && filePath.endsWith("/"))
);
return isDir ? "/" : "";
}
const DefaultPatterns = Object.freeze(["/**/node_modules/*"]);
const DotPatterns = Object.freeze([".*", "!.eslintrc.*", "!../"]);
//------------------------------------------------------------------------------
// Public
//------------------------------------------------------------------------------
class IgnorePattern {
/**
* The default patterns.
* @type {string[]}
*/
static get DefaultPatterns() {
return DefaultPatterns;
}
/**
* Create the default predicate function.
* @param {string} cwd The current working directory.
* @returns {((filePath:string, dot:boolean) => boolean) & {basePath:string; patterns:string[]}}
* The preficate function.
* The first argument is an absolute path that is checked.
* The second argument is the flag to not ignore dotfiles.
* If the predicate function returned `true`, it means the path should be ignored.
*/
static createDefaultIgnore(cwd) {
return this.createIgnore([new IgnorePattern(DefaultPatterns, cwd)]);
}
/**
* Create the predicate function from multiple `IgnorePattern` objects.
* @param {IgnorePattern[]} ignorePatterns The list of ignore patterns.
* @returns {((filePath:string, dot?:boolean) => boolean) & {basePath:string; patterns:string[]}}
* The preficate function.
* The first argument is an absolute path that is checked.
* The second argument is the flag to not ignore dotfiles.
* If the predicate function returned `true`, it means the path should be ignored.
*/
static createIgnore(ignorePatterns) {
debug("Create with: %o", ignorePatterns);
const basePath = getCommonAncestorPath(ignorePatterns.map(p => p.basePath));
const patterns = [].concat(
...ignorePatterns.map(p => p.getPatternsRelativeTo(basePath))
);
const ig = ignore().add([...DotPatterns, ...patterns]);
const dotIg = ignore().add(patterns);
debug(" processed: %o", { basePath, patterns });
return Object.assign(
(filePath, dot = false) => {
assert(path.isAbsolute(filePath), "'filePath' should be an absolute path.");
const relPathRaw = relative(basePath, filePath);
const relPath = relPathRaw && (relPathRaw + dirSuffix(filePath));
const adoptedIg = dot ? dotIg : ig;
const result = relPath !== "" && adoptedIg.ignores(relPath);
debug("Check", { filePath, dot, relativePath: relPath, result });
return result;
},
{ basePath, patterns }
);
}
/**
* Initialize a new `IgnorePattern` instance.
* @param {string[]} patterns The glob patterns that ignore to lint.
* @param {string} basePath The base path of `patterns`.
*/
constructor(patterns, basePath) {
assert(path.isAbsolute(basePath), "'basePath' should be an absolute path.");
/**
* The glob patterns that ignore to lint.
* @type {string[]}
*/
this.patterns = patterns;
/**
* The base path of `patterns`.
* @type {string}
*/
this.basePath = basePath;
/**
* If `true` then patterns which don't start with `/` will match the paths to the outside of `basePath`. Defaults to `false`.
*
* It's set `true` for `.eslintignore`, `package.json`, and `--ignore-path` for backward compatibility.
* It's `false` as-is for `ignorePatterns` property in config files.
* @type {boolean}
*/
this.loose = false;
}
/**
* Get `patterns` as modified for a given base path. It modifies the
* absolute paths in the patterns as prepending the difference of two base
* paths.
* @param {string} newBasePath The base path.
* @returns {string[]} Modifired patterns.
*/
getPatternsRelativeTo(newBasePath) {
assert(path.isAbsolute(newBasePath), "'newBasePath' should be an absolute path.");
const { basePath, loose, patterns } = this;
if (newBasePath === basePath) {
return patterns;
}
const prefix = `/${relative(newBasePath, basePath)}`;
return patterns.map(pattern => {
const negative = pattern.startsWith("!");
const head = negative ? "!" : "";
const body = negative ? pattern.slice(1) : pattern;
if (body.startsWith("/") || body.startsWith("../")) {
return `${head}${prefix}${body}`;
}
return loose ? pattern : `${head}${prefix}/**/${body}`;
});
}
}
module.exports = { IgnorePattern };

View file

@ -1,32 +0,0 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview `ConfigArray` class.
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
const { ConfigArray, getUsedExtractedConfigs } = require("./config-array");
const { ConfigDependency } = require("./config-dependency");
const { ExtractedConfig } = require("./extracted-config");
const { IgnorePattern } = require("./ignore-pattern");
const { OverrideTester } = require("./override-tester");
module.exports = {
ConfigArray,
ConfigDependency,
ExtractedConfig,
IgnorePattern,
OverrideTester,
getUsedExtractedConfigs
};

View file

@ -1,235 +0,0 @@
/*
* STOP!!! DO NOT MODIFY.
*
* This file is part of the ongoing work to move the eslintrc-style config
* system into the @eslint/eslintrc package. This file needs to remain
* unchanged in order for this work to proceed.
*
* If you think you need to change this file, please contact @nzakas first.
*
* Thanks in advance for your cooperation.
*/
/**
* @fileoverview `OverrideTester` class.
*
* `OverrideTester` class handles `files` property and `excludedFiles` property
* of `overrides` config.
*
* It provides one method.
*
* - `test(filePath)`
* Test if a file path matches the pair of `files` property and
* `excludedFiles` property. The `filePath` argument must be an absolute
* path.
*
* `ConfigArrayFactory` creates `OverrideTester` objects when it processes
* `overrides` properties.
*
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"use strict";
const assert = require("assert");
const path = require("path");
const util = require("util");
const { Minimatch } = require("minimatch");
const minimatchOpts = { dot: true, matchBase: true };
/**
* @typedef {Object} Pattern
* @property {InstanceType<Minimatch>[] | null} includes The positive matchers.
* @property {InstanceType<Minimatch>[] | null} excludes The negative matchers.
*/
/**
* Normalize a given pattern to an array.
* @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns.
* @returns {string[]|null} Normalized patterns.
* @private
*/
function normalizePatterns(patterns) {
if (Array.isArray(patterns)) {
return patterns.filter(Boolean);
}
if (typeof patterns === "string" && patterns) {
return [patterns];
}
return [];
}
/**
* Create the matchers of given patterns.
* @param {string[]} patterns The patterns.
* @returns {InstanceType<Minimatch>[] | null} The matchers.
*/
function toMatcher(patterns) {
if (patterns.length === 0) {
return null;
}
return patterns.map(pattern => {
if (/^\.[/\\]/u.test(pattern)) {
return new Minimatch(
pattern.slice(2),
// `./*.js` should not match with `subdir/foo.js`
{ ...minimatchOpts, matchBase: false }
);
}
return new Minimatch(pattern, minimatchOpts);
});
}
/**
* Convert a given matcher to string.
* @param {Pattern} matchers The matchers.
* @returns {string} The string expression of the matcher.
*/
function patternToJson({ includes, excludes }) {
return {
includes: includes && includes.map(m => m.pattern),
excludes: excludes && excludes.map(m => m.pattern)
};
}
/**
* The class to test given paths are matched by the patterns.
*/
class OverrideTester {
/**
* Create a tester with given criteria.
* If there are no criteria, returns `null`.
* @param {string|string[]} files The glob patterns for included files.
* @param {string|string[]} excludedFiles The glob patterns for excluded files.
* @param {string} basePath The path to the base directory to test paths.
* @returns {OverrideTester|null} The created instance or `null`.
*/
static create(files, excludedFiles, basePath) {
const includePatterns = normalizePatterns(files);
const excludePatterns = normalizePatterns(excludedFiles);
let endsWithWildcard = false;
if (includePatterns.length === 0) {
return null;
}
// Rejects absolute paths or relative paths to parents.
for (const pattern of includePatterns) {
if (path.isAbsolute(pattern) || pattern.includes("..")) {
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
}
if (pattern.endsWith("*")) {
endsWithWildcard = true;
}
}
for (const pattern of excludePatterns) {
if (path.isAbsolute(pattern) || pattern.includes("..")) {
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
}
}
const includes = toMatcher(includePatterns);
const excludes = toMatcher(excludePatterns);
return new OverrideTester(
[{ includes, excludes }],
basePath,
endsWithWildcard
);
}
/**
* Combine two testers by logical and.
* If either of the testers was `null`, returns the other tester.
* The `basePath` property of the two must be the same value.
* @param {OverrideTester|null} a A tester.
* @param {OverrideTester|null} b Another tester.
* @returns {OverrideTester|null} Combined tester.
*/
static and(a, b) {
if (!b) {
return a && new OverrideTester(
a.patterns,
a.basePath,
a.endsWithWildcard
);
}
if (!a) {
return new OverrideTester(
b.patterns,
b.basePath,
b.endsWithWildcard
);
}
assert.strictEqual(a.basePath, b.basePath);
return new OverrideTester(
a.patterns.concat(b.patterns),
a.basePath,
a.endsWithWildcard || b.endsWithWildcard
);
}
/**
* Initialize this instance.
* @param {Pattern[]} patterns The matchers.
* @param {string} basePath The base path.
* @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`.
*/
constructor(patterns, basePath, endsWithWildcard = false) {
/** @type {Pattern[]} */
this.patterns = patterns;
/** @type {string} */
this.basePath = basePath;
/** @type {boolean} */
this.endsWithWildcard = endsWithWildcard;
}
/**
* Test if a given path is matched or not.
* @param {string} filePath The absolute path to the target file.
* @returns {boolean} `true` if the path was matched.
*/
test(filePath) {
if (typeof filePath !== "string" || !path.isAbsolute(filePath)) {
throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`);
}
const relativePath = path.relative(this.basePath, filePath);
return this.patterns.every(({ includes, excludes }) => (
(!includes || includes.some(m => m.match(relativePath))) &&
(!excludes || !excludes.some(m => m.match(relativePath)))
));
}
// eslint-disable-next-line jsdoc/require-description
/**
* @returns {Object} a JSON compatible object.
*/
toJSON() {
if (this.patterns.length === 1) {
return {
...patternToJson(this.patterns[0]),
basePath: this.basePath
};
}
return {
AND: this.patterns.map(patternToJson),
basePath: this.basePath
};
}
// eslint-disable-next-line jsdoc/require-description
/**
* @returns {Object} an object to display by `console.log()`.
*/
[util.inspect.custom]() {
return this.toJSON();
}
}
module.exports = { OverrideTester };

View file

@ -38,10 +38,15 @@ const fs = require("fs");
const path = require("path");
const getGlobParent = require("glob-parent");
const isGlob = require("is-glob");
const { escapeRegExp } = require("lodash");
const escapeRegExp = require("escape-string-regexp");
const { Minimatch } = require("minimatch");
const { IgnorePattern } = require("./config-array");
const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory");
const {
Legacy: {
IgnorePattern,
CascadingConfigArrayFactory
}
} = require("@eslint/eslintrc");
const debug = require("debug")("eslint:file-enumerator");
//------------------------------------------------------------------------------
@ -208,7 +213,11 @@ class FileEnumerator {
*/
constructor({
cwd = process.cwd(),
configArrayFactory = new CascadingConfigArrayFactory({ cwd }),
configArrayFactory = new CascadingConfigArrayFactory({
cwd,
eslintRecommendedPath: path.resolve(__dirname, "../../conf/eslint-recommended.js"),
eslintAllPath: path.resolve(__dirname, "../../conf/eslint-all.js")
}),
extensions = null,
globInputPaths = true,
errorOnUnmatchedPattern = true,
@ -424,9 +433,14 @@ class FileEnumerator {
// Enumerate the files of this directory.
for (const entry of readdirSafeSync(directoryPath)) {
const filePath = path.join(directoryPath, entry.name);
const fileInfo = entry.isSymbolicLink() ? statSafeSync(filePath) : entry;
if (!fileInfo) {
continue;
}
// Check if the file is matched.
if (entry.isFile()) {
if (fileInfo.isFile()) {
if (!config) {
config = configArrayFactory.getConfigArrayForFile(
filePath,
@ -462,7 +476,7 @@ class FileEnumerator {
}
// Dive into the sub directory.
} else if (options.recursive && entry.isDirectory()) {
} else if (options.recursive && fileInfo.isDirectory()) {
if (!config) {
config = configArrayFactory.getConfigArrayForFile(
filePath,

View file

@ -1,8 +0,0 @@
<tr style="display:none" class="f-<%= parentIndex %>">
<td><%= lineNumber %>:<%= columnNumber %></td>
<td class="clr-<%= severityNumber %>"><%= severityName %></td>
<td><%- message %></td>
<td>
<a href="<%= ruleUrl %>" target="_blank" rel="noopener noreferrer"><%= ruleId %></a>
</td>
</tr>

View file

@ -1,115 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESLint Report</title>
<style>
body {
font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size:16px;
font-weight:normal;
margin:0;
padding:0;
color:#333
}
#overview {
padding:20px 30px
}
td, th {
padding:5px 10px
}
h1 {
margin:0
}
table {
margin:30px;
width:calc(100% - 60px);
max-width:1000px;
border-radius:5px;
border:1px solid #ddd;
border-spacing:0px;
}
th {
font-weight:400;
font-size:medium;
text-align:left;
cursor:pointer
}
td.clr-1, td.clr-2, th span {
font-weight:700
}
th span {
float:right;
margin-left:20px
}
th span:after {
content:"";
clear:both;
display:block
}
tr:last-child td {
border-bottom:none
}
tr td:first-child, tr td:last-child {
color:#9da0a4
}
#overview.bg-0, tr.bg-0 th {
color:#468847;
background:#dff0d8;
border-bottom:1px solid #d6e9c6
}
#overview.bg-1, tr.bg-1 th {
color:#f0ad4e;
background:#fcf8e3;
border-bottom:1px solid #fbeed5
}
#overview.bg-2, tr.bg-2 th {
color:#b94a48;
background:#f2dede;
border-bottom:1px solid #eed3d7
}
td {
border-bottom:1px solid #ddd
}
td.clr-1 {
color:#f0ad4e
}
td.clr-2 {
color:#b94a48
}
td a {
color:#3a33d1;
text-decoration:none
}
td a:hover {
color:#272296;
text-decoration:underline
}
</style>
</head>
<body>
<div id="overview" class="bg-<%= reportColor %>">
<h1>ESLint Report</h1>
<div>
<span><%= reportSummary %></span> - Generated on <%= date %>
</div>
</div>
<table>
<tbody>
<%= results %>
</tbody>
</table>
<script type="text/javascript">
var groups = document.querySelectorAll("tr[data-group]");
for (i = 0; i < groups.length; i++) {
groups[i].addEventListener("click", function() {
var inGroup = document.getElementsByClassName(this.getAttribute("data-group"));
this.innerHTML = (this.innerHTML.indexOf("+") > -1) ? this.innerHTML.replace("+", "-") : this.innerHTML.replace("-", "+");
for (var j = 0; j < inGroup.length; j++) {
inGroup[j].style.display = (inGroup[j].style.display !== "none") ? "none" : "table-row";
}
});
}
</script>
</body>
</html>

View file

@ -1,6 +0,0 @@
<tr class="bg-<%- color %>" data-group="f-<%- index %>">
<th colspan="4">
[+] <%- filePath %>
<span><%- summary %></span>
</th>
</tr>

View file

@ -4,17 +4,153 @@
*/
"use strict";
const lodash = require("lodash");
const fs = require("fs");
const path = require("path");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const pageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-page.html"), "utf-8"));
const messageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-message.html"), "utf-8"));
const resultTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-result.html"), "utf-8"));
const encodeHTML = (function() {
const encodeHTMLRules = {
"&": "&#38;",
"<": "&#60;",
">": "&#62;",
'"': "&#34;",
"'": "&#39;"
};
const matchHTML = /[&<>"']/ug;
return function(code) {
return code
? code.toString().replace(matchHTML, m => encodeHTMLRules[m] || m)
: "";
};
}());
/**
* Get the final HTML document.
* @param {Object} it data for the document.
* @returns {string} HTML document.
*/
function pageTemplate(it) {
const { reportColor, reportSummary, date, results } = it;
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESLint Report</title>
<style>
body {
font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size:16px;
font-weight:normal;
margin:0;
padding:0;
color:#333
}
#overview {
padding:20px 30px
}
td, th {
padding:5px 10px
}
h1 {
margin:0
}
table {
margin:30px;
width:calc(100% - 60px);
max-width:1000px;
border-radius:5px;
border:1px solid #ddd;
border-spacing:0px;
}
th {
font-weight:400;
font-size:medium;
text-align:left;
cursor:pointer
}
td.clr-1, td.clr-2, th span {
font-weight:700
}
th span {
float:right;
margin-left:20px
}
th span:after {
content:"";
clear:both;
display:block
}
tr:last-child td {
border-bottom:none
}
tr td:first-child, tr td:last-child {
color:#9da0a4
}
#overview.bg-0, tr.bg-0 th {
color:#468847;
background:#dff0d8;
border-bottom:1px solid #d6e9c6
}
#overview.bg-1, tr.bg-1 th {
color:#f0ad4e;
background:#fcf8e3;
border-bottom:1px solid #fbeed5
}
#overview.bg-2, tr.bg-2 th {
color:#b94a48;
background:#f2dede;
border-bottom:1px solid #eed3d7
}
td {
border-bottom:1px solid #ddd
}
td.clr-1 {
color:#f0ad4e
}
td.clr-2 {
color:#b94a48
}
td a {
color:#3a33d1;
text-decoration:none
}
td a:hover {
color:#272296;
text-decoration:underline
}
</style>
</head>
<body>
<div id="overview" class="bg-${reportColor}">
<h1>ESLint Report</h1>
<div>
<span>${reportSummary}</span> - Generated on ${date}
</div>
</div>
<table>
<tbody>
${results}
</tbody>
</table>
<script type="text/javascript">
var groups = document.querySelectorAll("tr[data-group]");
for (i = 0; i < groups.length; i++) {
groups[i].addEventListener("click", function() {
var inGroup = document.getElementsByClassName(this.getAttribute("data-group"));
this.innerHTML = (this.innerHTML.indexOf("+") > -1) ? this.innerHTML.replace("+", "-") : this.innerHTML.replace("-", "+");
for (var j = 0; j < inGroup.length; j++) {
inGroup[j].style.display = (inGroup[j].style.display !== "none") ? "none" : "table-row";
}
});
}
</script>
</body>
</html>
`.trimLeft();
}
/**
* Given a word and a count, append an s if count is not one.
@ -58,6 +194,35 @@ function renderColor(totalErrors, totalWarnings) {
return 0;
}
/**
* Get HTML (table row) describing a single message.
* @param {Object} it data for the message.
* @returns {string} HTML (table row) describing the message.
*/
function messageTemplate(it) {
const {
parentIndex,
lineNumber,
columnNumber,
severityNumber,
severityName,
message,
ruleUrl,
ruleId
} = it;
return `
<tr style="display:none" class="f-${parentIndex}">
<td>${lineNumber}:${columnNumber}</td>
<td class="clr-${severityNumber}">${severityName}</td>
<td>${encodeHTML(message)}</td>
<td>
<a href="${ruleUrl ? ruleUrl : ""}" target="_blank" rel="noopener noreferrer">${ruleId ? ruleId : ""}</a>
</td>
</tr>
`.trimLeft();
}
/**
* Get HTML (table rows) describing the messages.
* @param {Array} messages Messages.
@ -72,7 +237,7 @@ function renderMessages(messages, parentIndex, rulesMeta) {
* @param {Object} message Message.
* @returns {string} HTML (table row) describing a message.
*/
return lodash.map(messages, message => {
return messages.map(message => {
const lineNumber = message.line || 0;
const columnNumber = message.column || 0;
let ruleUrl;
@ -80,7 +245,9 @@ function renderMessages(messages, parentIndex, rulesMeta) {
if (rulesMeta) {
const meta = rulesMeta[message.ruleId];
ruleUrl = lodash.get(meta, "docs.url", null);
if (meta && meta.docs && meta.docs.url) {
ruleUrl = meta.docs.url;
}
}
return messageTemplate({
@ -96,6 +263,24 @@ function renderMessages(messages, parentIndex, rulesMeta) {
}).join("\n");
}
/**
* Get HTML (table row) describing the result for a single file.
* @param {Object} it data for the file.
* @returns {string} HTML (table row) describing the result for the file.
*/
function resultTemplate(it) {
const { color, index, filePath, summary } = it;
return `
<tr class="bg-${color}" data-group="f-${index}">
<th colspan="4">
[+] ${encodeHTML(filePath)}
<span>${encodeHTML(summary)}</span>
</th>
</tr>
`.trimLeft();
}
// eslint-disable-next-line jsdoc/require-description
/**
* @param {Array} results Test results.
@ -103,12 +288,11 @@ function renderMessages(messages, parentIndex, rulesMeta) {
* @returns {string} HTML string describing the results.
*/
function renderResults(results, rulesMeta) {
return lodash.map(results, (result, index) => resultTemplate({
return results.map((result, index) => resultTemplate({
index,
color: renderColor(result.errorCount, result.warningCount),
filePath: result.filePath,
summary: renderSummary(result.errorCount, result.warningCount)
}) + renderMessages(result.messages, index, rulesMeta)).join("\n");
}

View file

@ -15,6 +15,8 @@ const stringify = require("json-stable-stringify-without-jsonify");
const pkg = require("../../package.json");
const hash = require("./hash");
const debug = require("debug")("eslint:lint-result-cache");
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
@ -22,6 +24,22 @@ const hash = require("./hash");
const configHashCache = new WeakMap();
const nodeVersion = process && process.version;
const validCacheStrategies = ["metadata", "content"];
const invalidCacheStrategyErrorMessage = `Cache strategy must be one of: ${validCacheStrategies
.map(strategy => `"${strategy}"`)
.join(", ")}`;
/**
* Tests whether a provided cacheStrategy is valid
* @param {string} cacheStrategy The cache strategy to use
* @returns {boolean} true if `cacheStrategy` is one of `validCacheStrategies`; false otherwise
*/
function isValidCacheStrategy(cacheStrategy) {
return (
validCacheStrategies.indexOf(cacheStrategy) !== -1
);
}
/**
* Calculates the hash of the config
* @param {ConfigArray} config The config.
@ -49,12 +67,30 @@ class LintResultCache {
/**
* Creates a new LintResultCache instance.
* @param {string} cacheFileLocation The cache file location.
* configuration lookup by file path).
* @param {"metadata" | "content"} cacheStrategy The cache strategy to use.
*/
constructor(cacheFileLocation) {
constructor(cacheFileLocation, cacheStrategy) {
assert(cacheFileLocation, "Cache file location is required");
assert(cacheStrategy, "Cache strategy is required");
assert(
isValidCacheStrategy(cacheStrategy),
invalidCacheStrategyErrorMessage
);
this.fileEntryCache = fileEntryCache.create(cacheFileLocation);
debug(`Caching results to ${cacheFileLocation}`);
const useChecksum = cacheStrategy === "content";
debug(
`Using "${cacheStrategy}" strategy to detect changes`
);
this.fileEntryCache = fileEntryCache.create(
cacheFileLocation,
void 0,
useChecksum
);
this.cacheFileLocation = cacheFileLocation;
}
/**
@ -76,17 +112,28 @@ class LintResultCache {
* was previously linted
* If any of these are not true, we will not reuse the lint results.
*/
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
const hashOfConfig = hashOfConfigFor(config);
const changed = fileDescriptor.changed || fileDescriptor.meta.hashOfConfig !== hashOfConfig;
const changed =
fileDescriptor.changed ||
fileDescriptor.meta.hashOfConfig !== hashOfConfig;
if (fileDescriptor.notFound || changed) {
if (fileDescriptor.notFound) {
debug(`File not found on the file system: ${filePath}`);
return null;
}
if (changed) {
debug(`Cache entry not found or no longer valid: ${filePath}`);
return null;
}
// If source is present but null, need to reread the file from the filesystem.
if (fileDescriptor.meta.results && fileDescriptor.meta.results.source === null) {
if (
fileDescriptor.meta.results &&
fileDescriptor.meta.results.source === null
) {
debug(`Rereading cached result source from filesystem: ${filePath}`);
fileDescriptor.meta.results.source = fs.readFileSync(filePath, "utf-8");
}
@ -112,6 +159,7 @@ class LintResultCache {
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
if (fileDescriptor && !fileDescriptor.notFound) {
debug(`Updating cached result: ${filePath}`);
// Serialize the result, except that we want to remove the file source if present.
const resultToSerialize = Object.assign({}, result);
@ -135,6 +183,7 @@ class LintResultCache {
* @returns {void}
*/
reconcile() {
debug(`Persisting cached results: ${this.cacheFileLocation}`);
this.fileEntryCache.reconcile();
}
}

15
node_modules/eslint/lib/cli.js generated vendored
View file

@ -32,6 +32,7 @@ const debug = require("debug")("eslint:cli");
/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */
/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
/** @typedef {import("./eslint/eslint").LintResult} LintResult */
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
//------------------------------------------------------------------------------
// Helpers
@ -54,7 +55,7 @@ function quietFixPredicate(message) {
/**
* Translates the CLI options into the options expected by the CLIEngine.
* @param {Object} cliOptions The CLI options to translate.
* @param {ParsedCLIOptions} cliOptions The CLI options to translate.
* @returns {ESLintOptions} The options object for the CLIEngine.
* @private
*/
@ -62,6 +63,7 @@ function translateOptions({
cache,
cacheFile,
cacheLocation,
cacheStrategy,
config,
env,
errorOnUnmatchedPattern,
@ -88,6 +90,7 @@ function translateOptions({
allowInlineConfig: inlineConfig,
cache,
cacheLocation: cacheLocation || cacheFile,
cacheStrategy,
errorOnUnmatchedPattern,
extensions: ext,
fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true),
@ -219,6 +222,8 @@ const cli = {
if (Array.isArray(args)) {
debug("CLI args: %o", args.slice(2));
}
/** @type {ParsedCLIOptions} */
let options;
try {
@ -299,12 +304,16 @@ const cli = {
await ESLint.outputFixes(results);
}
let resultsToPrint = results;
if (options.quiet) {
debug("Quiet mode enabled - filtering out warnings");
results = ESLint.getErrorResults(results);
resultsToPrint = ESLint.getErrorResults(resultsToPrint);
}
if (await printResults(engine, results, options.format, options.outputFile)) {
if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {
// Errors and warnings from the original unfiltered results should determine the exit code
const { errorCount, warningCount } = countErrors(results);
const tooManyWarnings =
options.maxWarnings >= 0 && warningCount > options.maxWarnings;

52
node_modules/eslint/lib/config/default-config.js generated vendored Normal file
View file

@ -0,0 +1,52 @@
/**
* @fileoverview Default configuration
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const Rules = require("../rules");
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
exports.defaultConfig = [
{
plugins: {
"@": {
parsers: {
espree: require("espree")
},
/*
* Because we try to delay loading rules until absolutely
* necessary, a proxy allows us to hook into the lazy-loading
* aspect of the rules map while still keeping all of the
* relevant configuration inside of the config array.
*/
rules: new Proxy({}, {
get(target, property) {
return Rules.get(property);
},
has(target, property) {
return Rules.has(property);
}
})
}
},
ignores: [
"**/node_modules/**",
".git/**"
],
languageOptions: {
parser: "@/espree"
}
}
];

125
node_modules/eslint/lib/config/flat-config-array.js generated vendored Normal file
View file

@ -0,0 +1,125 @@
/**
* @fileoverview Flat Config Array
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const { ConfigArray, ConfigArraySymbol } = require("@humanwhocodes/config-array");
const { flatConfigSchema } = require("./flat-config-schema");
const { RuleValidator } = require("./rule-validator");
const { defaultConfig } = require("./default-config");
const recommendedConfig = require("../../conf/eslint-recommended");
const allConfig = require("../../conf/eslint-all");
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const ruleValidator = new RuleValidator();
/**
* Splits a plugin identifier in the form a/b/c into two parts: a/b and c.
* @param {string} identifier The identifier to parse.
* @returns {{objectName: string, pluginName: string}} The parts of the plugin
* name.
*/
function splitPluginIdentifier(identifier) {
const parts = identifier.split("/");
return {
objectName: parts.pop(),
pluginName: parts.join("/")
};
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* Represents an array containing configuration information for ESLint.
*/
class FlatConfigArray extends ConfigArray {
/**
* Creates a new instance.
* @param {*[]} configs An array of configuration information.
* @param {{basePath: string, baseConfig: FlatConfig}} options The options
* to use for the config array instance.
*/
constructor(configs, { basePath, baseConfig = defaultConfig }) {
super(configs, {
basePath,
schema: flatConfigSchema
});
this.unshift(baseConfig);
}
/* eslint-disable class-methods-use-this */
/**
* Replaces a config with another config to allow us to put strings
* in the config array that will be replaced by objects before
* normalization.
* @param {Object} config The config to preprocess.
* @returns {Object} The preprocessed config.
*/
[ConfigArraySymbol.preprocessConfig](config) {
if (config === "eslint:recommended") {
return recommendedConfig;
}
if (config === "eslint:all") {
return allConfig;
}
return config;
}
/**
* Finalizes the config by replacing plugin references with their objects
* and validating rule option schemas.
* @param {Object} config The config to finalize.
* @returns {Object} The finalized config.
* @throws {TypeError} If the config is invalid.
*/
[ConfigArraySymbol.finalizeConfig](config) {
const { plugins, languageOptions, processor } = config;
// Check parser value
if (languageOptions && languageOptions.parser && typeof languageOptions.parser === "string") {
const { pluginName, objectName: parserName } = splitPluginIdentifier(languageOptions.parser);
if (!plugins || !plugins[pluginName] || !plugins[pluginName].parsers || !plugins[pluginName].parsers[parserName]) {
throw new TypeError(`Key "parser": Could not find "${parserName}" in plugin "${pluginName}".`);
}
languageOptions.parser = plugins[pluginName].parsers[parserName];
}
// Check processor value
if (processor && typeof processor === "string") {
const { pluginName, objectName: processorName } = splitPluginIdentifier(processor);
if (!plugins || !plugins[pluginName] || !plugins[pluginName].processors || !plugins[pluginName].processors[processorName]) {
throw new TypeError(`Key "processor": Could not find "${processorName}" in plugin "${pluginName}".`);
}
config.processor = plugins[pluginName].processors[processorName];
}
ruleValidator.validate(config);
return config;
}
/* eslint-enable class-methods-use-this */
}
exports.FlatConfigArray = FlatConfigArray;

452
node_modules/eslint/lib/config/flat-config-schema.js generated vendored Normal file
View file

@ -0,0 +1,452 @@
/**
* @fileoverview Flat config schema
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @typedef ObjectPropertySchema
* @property {Function|string} merge The function or name of the function to call
* to merge multiple objects with this property.
* @property {Function|string} validate The function or name of the function to call
* to validate the value of this property.
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const ruleSeverities = new Map([
[0, 0], ["off", 0],
[1, 1], ["warn", 1],
[2, 2], ["error", 2]
]);
const globalVariablesValues = new Set([
true, "true", "writable", "writeable",
false, "false", "readonly", "readable", null,
"off"
]);
/**
* Check if a value is a non-null object.
* @param {any} value The value to check.
* @returns {boolean} `true` if the value is a non-null object.
*/
function isNonNullObject(value) {
return typeof value === "object" && value !== null;
}
/**
* Check if a value is undefined.
* @param {any} value The value to check.
* @returns {boolean} `true` if the value is undefined.
*/
function isUndefined(value) {
return typeof value === "undefined";
}
/**
* Deeply merges two objects.
* @param {Object} first The base object.
* @param {Object} second The overrides object.
* @returns {Object} An object with properties from both first and second.
*/
function deepMerge(first = {}, second = {}) {
/*
* If the second value is an array, just return it. We don't merge
* arrays because order matters and we can't know the correct order.
*/
if (Array.isArray(second)) {
return second;
}
/*
* First create a result object where properties from the second object
* overwrite properties from the first. This sets up a baseline to use
* later rather than needing to inspect and change every property
* individually.
*/
const result = {
...first,
...second
};
for (const key of Object.keys(second)) {
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
const firstValue = first[key];
const secondValue = second[key];
if (isNonNullObject(firstValue)) {
result[key] = deepMerge(firstValue, secondValue);
} else if (isUndefined(firstValue)) {
if (isNonNullObject(secondValue)) {
result[key] = deepMerge(
Array.isArray(secondValue) ? [] : {},
secondValue
);
} else if (!isUndefined(secondValue)) {
result[key] = secondValue;
}
}
}
return result;
}
/**
* Normalizes the rule options config for a given rule by ensuring that
* it is an array and that the first item is 0, 1, or 2.
* @param {Array|string|number} ruleOptions The rule options config.
* @returns {Array} An array of rule options.
*/
function normalizeRuleOptions(ruleOptions) {
const finalOptions = Array.isArray(ruleOptions)
? ruleOptions.slice(0)
: [ruleOptions];
finalOptions[0] = ruleSeverities.get(finalOptions[0]);
return finalOptions;
}
//-----------------------------------------------------------------------------
// Assertions
//-----------------------------------------------------------------------------
/**
* Validates that a value is a valid rule options entry.
* @param {any} value The value to check.
* @returns {void}
* @throws {TypeError} If the value isn't a valid rule options.
*/
function assertIsRuleOptions(value) {
if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
throw new TypeError("Expected a string, number, or array.");
}
}
/**
* Validates that a value is valid rule severity.
* @param {any} value The value to check.
* @returns {void}
* @throws {TypeError} If the value isn't a valid rule severity.
*/
function assertIsRuleSeverity(value) {
const severity = typeof value === "string"
? ruleSeverities.get(value.toLowerCase())
: ruleSeverities.get(value);
if (typeof severity === "undefined") {
throw new TypeError("Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2.");
}
}
/**
* Validates that a given string is the form pluginName/objectName.
* @param {string} value The string to check.
* @returns {void}
* @throws {TypeError} If the string isn't in the correct format.
*/
function assertIsPluginMemberName(value) {
if (!/[@a-z0-9-_$]+(?:\/(?:[a-z0-9-_$]+))+$/iu.test(value)) {
throw new TypeError(`Expected string in the form "pluginName/objectName" but found "${value}".`);
}
}
/**
* Validates that a value is an object.
* @param {any} value The value to check.
* @returns {void}
* @throws {TypeError} If the value isn't an object.
*/
function assertIsObject(value) {
if (!isNonNullObject(value)) {
throw new TypeError("Expected an object.");
}
}
/**
* Validates that a value is an object or a string.
* @param {any} value The value to check.
* @returns {void}
* @throws {TypeError} If the value isn't an object or a string.
*/
function assertIsObjectOrString(value) {
if ((!value || typeof value !== "object") && typeof value !== "string") {
throw new TypeError("Expected an object or string.");
}
}
//-----------------------------------------------------------------------------
// Low-Level Schemas
//-----------------------------------------------------------------------------
/** @type {ObjectPropertySchema} */
const numberSchema = {
merge: "replace",
validate: "number"
};
/** @type {ObjectPropertySchema} */
const booleanSchema = {
merge: "replace",
validate: "boolean"
};
/** @type {ObjectPropertySchema} */
const deepObjectAssignSchema = {
merge(first = {}, second = {}) {
return deepMerge(first, second);
},
validate: "object"
};
//-----------------------------------------------------------------------------
// High-Level Schemas
//-----------------------------------------------------------------------------
/** @type {ObjectPropertySchema} */
const globalsSchema = {
merge: "assign",
validate(value) {
assertIsObject(value);
for (const key of Object.keys(value)) {
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
if (key !== key.trim()) {
throw new TypeError(`Global "${key}" has leading or trailing whitespace.`);
}
if (!globalVariablesValues.has(value[key])) {
throw new TypeError(`Key "${key}": Expected "readonly", "writable", or "off".`);
}
}
}
};
/** @type {ObjectPropertySchema} */
const parserSchema = {
merge: "replace",
validate(value) {
assertIsObjectOrString(value);
if (typeof value === "object" && typeof value.parse !== "function" && typeof value.parseForESLint !== "function") {
throw new TypeError("Expected object to have a parse() or parseForESLint() method.");
}
if (typeof value === "string") {
assertIsPluginMemberName(value);
}
}
};
/** @type {ObjectPropertySchema} */
const pluginsSchema = {
merge(first = {}, second = {}) {
const keys = new Set([...Object.keys(first), ...Object.keys(second)]);
const result = {};
// manually validate that plugins are not redefined
for (const key of keys) {
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
if (key in first && key in second && first[key] !== second[key]) {
throw new TypeError(`Cannot redefine plugin "${key}".`);
}
result[key] = second[key] || first[key];
}
return result;
},
validate(value) {
// first check the value to be sure it's an object
if (value === null || typeof value !== "object") {
throw new TypeError("Expected an object.");
}
// second check the keys to make sure they are objects
for (const key of Object.keys(value)) {
// avoid hairy edge case
if (key === "__proto__") {
continue;
}
if (value[key] === null || typeof value[key] !== "object") {
throw new TypeError(`Key "${key}": Expected an object.`);
}
}
}
};
/** @type {ObjectPropertySchema} */
const processorSchema = {
merge: "replace",
validate(value) {
if (typeof value === "string") {
assertIsPluginMemberName(value);
} else if (value && typeof value === "object") {
if (typeof value.preprocess !== "function" || typeof value.postprocess !== "function") {
throw new TypeError("Object must have a preprocess() and a postprocess() method.");
}
} else {
throw new TypeError("Expected an object or a string.");
}
}
};
/** @type {ObjectPropertySchema} */
const rulesSchema = {
merge(first = {}, second = {}) {
const result = {
...first,
...second
};
for (const ruleId of Object.keys(result)) {
// avoid hairy edge case
if (ruleId === "__proto__") {
/* eslint-disable-next-line no-proto */
delete result.__proto__;
continue;
}
result[ruleId] = normalizeRuleOptions(result[ruleId]);
/*
* If either rule config is missing, then the correct
* config is already present and we just need to normalize
* the severity.
*/
if (!(ruleId in first) || !(ruleId in second)) {
continue;
}
const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
/*
* If the second rule config only has a severity (length of 1),
* then use that severity and keep the rest of the options from
* the first rule config.
*/
if (secondRuleOptions.length === 1) {
result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
continue;
}
/*
* In any other situation, then the second rule config takes
* precedence. That means the value at `result[ruleId]` is
* already correct and no further work is necessary.
*/
}
return result;
},
validate(value) {
assertIsObject(value);
let lastRuleId;
// Performance: One try-catch has less overhead than one per loop iteration
try {
/*
* We are not checking the rule schema here because there is no
* guarantee that the rule definition is present at this point. Instead
* we wait and check the rule schema during the finalization step
* of calculating a config.
*/
for (const ruleId of Object.keys(value)) {
// avoid hairy edge case
if (ruleId === "__proto__") {
continue;
}
lastRuleId = ruleId;
const ruleOptions = value[ruleId];
assertIsRuleOptions(ruleOptions);
if (Array.isArray(ruleOptions)) {
assertIsRuleSeverity(ruleOptions[0]);
} else {
assertIsRuleSeverity(ruleOptions);
}
}
} catch (error) {
error.message = `Key "${lastRuleId}": ${error.message}`;
throw error;
}
}
};
/** @type {ObjectPropertySchema} */
const sourceTypeSchema = {
merge: "replace",
validate(value) {
if (typeof value !== "string" || !/^(?:script|module|commonjs)$/u.test(value)) {
throw new TypeError("Expected \"script\", \"module\", or \"commonjs\".");
}
}
};
//-----------------------------------------------------------------------------
// Full schema
//-----------------------------------------------------------------------------
exports.flatConfigSchema = {
settings: deepObjectAssignSchema,
linterOptions: {
schema: {
noInlineConfig: booleanSchema,
reportUnusedDisableDirectives: booleanSchema
}
},
languageOptions: {
schema: {
ecmaVersion: numberSchema,
sourceType: sourceTypeSchema,
globals: globalsSchema,
parser: parserSchema,
parserOptions: deepObjectAssignSchema
}
},
processor: processorSchema,
plugins: pluginsSchema,
rules: rulesSchema
};

169
node_modules/eslint/lib/config/rule-validator.js generated vendored Normal file
View file

@ -0,0 +1,169 @@
/**
* @fileoverview Rule Validator
* @author Nicholas C. Zakas
*/
"use strict";
//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------
const ajv = require("../shared/ajv")();
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Finds a rule with the given ID in the given config.
* @param {string} ruleId The ID of the rule to find.
* @param {Object} config The config to search in.
* @returns {{create: Function, schema: (Array|null)}} THe rule object.
*/
function findRuleDefinition(ruleId, config) {
const ruleIdParts = ruleId.split("/");
let pluginName, ruleName;
// built-in rule
if (ruleIdParts.length === 1) {
pluginName = "@";
ruleName = ruleIdParts[0];
} else {
ruleName = ruleIdParts.pop();
pluginName = ruleIdParts.join("/");
}
if (!config.plugins || !config.plugins[pluginName]) {
throw new TypeError(`Key "rules": Key "${ruleId}": Could not find plugin "${pluginName}".`);
}
if (!config.plugins[pluginName].rules || !config.plugins[pluginName].rules[ruleName]) {
throw new TypeError(`Key "rules": Key "${ruleId}": Could not find "${ruleName}" in plugin "${pluginName}".`);
}
return config.plugins[pluginName].rules[ruleName];
}
/**
* Gets a complete options schema for a rule.
* @param {{create: Function, schema: (Array|null)}} rule A new-style rule object
* @returns {Object} JSON Schema for the rule's options.
*/
function getRuleOptionsSchema(rule) {
if (!rule) {
return null;
}
const schema = rule.schema || rule.meta && rule.meta.schema;
if (Array.isArray(schema)) {
if (schema.length) {
return {
type: "array",
items: schema,
minItems: 0,
maxItems: schema.length
};
}
return {
type: "array",
minItems: 0,
maxItems: 0
};
}
// Given a full schema, leave it alone
return schema || null;
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* Implements validation functionality for the rules portion of a config.
*/
class RuleValidator {
/**
* Creates a new instance.
*/
constructor() {
/**
* A collection of compiled validators for rules that have already
* been validated.
* @type {WeakMap}
* @property validators
*/
this.validators = new WeakMap();
}
/**
* Validates all of the rule configurations in a config against each
* rule's schema.
* @param {Object} config The full config to validate. This object must
* contain both the rules section and the plugins section.
* @returns {void}
* @throws {Error} If a rule's configuration does not match its schema.
*/
validate(config) {
if (!config.rules) {
return;
}
for (const [ruleId, ruleOptions] of Object.entries(config.rules)) {
// check for edge case
if (ruleId === "__proto__") {
continue;
}
/*
* If a rule is disabled, we don't do any validation. This allows
* users to safely set any value to 0 or "off" without worrying
* that it will cause a validation error.
*
* Note: ruleOptions is always an array at this point because
* this validation occurs after FlatConfigArray has merged and
* normalized values.
*/
if (ruleOptions[0] === 0) {
continue;
}
const rule = findRuleDefinition(ruleId, config);
// Precompile and cache validator the first time
if (!this.validators.has(rule)) {
const schema = getRuleOptionsSchema(rule);
if (schema) {
this.validators.set(rule, ajv.compile(schema));
}
}
const validateRule = this.validators.get(rule);
if (validateRule) {
validateRule(ruleOptions.slice(1));
if (validateRule.errors) {
throw new Error(`Key "rules": Key "${ruleId}": ${
validateRule.errors.map(
error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
).join("")
}`);
}
}
}
}
}
exports.RuleValidator = RuleValidator;

View file

@ -43,6 +43,7 @@ const { version } = require("../../package.json");
* @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this instance
* @property {boolean} [cache] Enable result caching.
* @property {string} [cacheLocation] The cache file to use instead of .eslintcache.
* @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files.
* @property {string} [cwd] The value to use for the current working directory.
* @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`.
* @property {string[]} [extensions] An array of file extensions to check.
@ -157,6 +158,7 @@ function processOptions({
baseConfig = null,
cache = false,
cacheLocation = ".eslintcache",
cacheStrategy = "metadata",
cwd = process.cwd(),
errorOnUnmatchedPattern = true,
extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature.
@ -216,6 +218,12 @@ function processOptions({
if (!isNonEmptyString(cacheLocation)) {
errors.push("'cacheLocation' must be a non-empty string.");
}
if (
cacheStrategy !== "metadata" &&
cacheStrategy !== "content"
) {
errors.push("'cacheStrategy' must be any of \"metadata\", \"content\".");
}
if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) {
errors.push("'cwd' must be an absolute path.");
}
@ -272,7 +280,7 @@ function processOptions({
errors.push("'rulePaths' must be an array of non-empty strings.");
}
if (typeof useEslintrc !== "boolean") {
errors.push("'useElintrc' must be a boolean.");
errors.push("'useEslintrc' must be a boolean.");
}
if (errors.length > 0) {
@ -284,6 +292,7 @@ function processOptions({
baseConfig,
cache,
cacheLocation,
cacheStrategy,
configFile: overrideConfigFile,
cwd,
errorOnUnmatchedPattern,
@ -505,6 +514,39 @@ class ESLint {
return CLIEngine.getErrorResults(results);
}
/**
* Returns meta objects for each rule represented in the lint results.
* @param {LintResult[]} results The results to fetch rules meta for.
* @returns {Object} A mapping of ruleIds to rule meta objects.
*/
getRulesMetaForResults(results) {
const resultRuleIds = new Set();
// first gather all ruleIds from all results
for (const result of results) {
for (const { ruleId } of result.messages) {
resultRuleIds.add(ruleId);
}
}
// create a map of all rules in the results
const { cliEngine } = privateMembersMap.get(this);
const rules = cliEngine.getRules();
const resultRules = new Map();
for (const [ruleId, rule] of rules) {
if (resultRuleIds.has(ruleId)) {
resultRules.set(ruleId, rule);
}
}
return createRulesMeta(resultRules);
}
/**
* Executes the current configuration on an array of file and directory names.
* @param {string[]} patterns An array of file and directory names.
@ -543,9 +585,12 @@ class ESLint {
...unknownOptions
} = options || {};
for (const key of Object.keys(unknownOptions)) {
throw new Error(`'options' must not include the unknown option '${key}'`);
const unknownOptionKeys = Object.keys(unknownOptions);
if (unknownOptionKeys.length > 0) {
throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`);
}
if (filePath !== void 0 && !isNonEmptyString(filePath)) {
throw new Error("'options.filePath' must be a non-empty string or undefined");
}
@ -563,7 +608,7 @@ class ESLint {
/**
* Returns the formatter representing the given formatter name.
* @param {string} [name] The name of the formattter to load.
* @param {string} [name] The name of the formatter to load.
* The following values are allowed:
* - `undefined` ... Load `stylish` builtin formatter.
* - A builtin formatter name ... Load the builtin formatter.

View file

@ -9,7 +9,7 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash"),
const equal = require("fast-deep-equal"),
recConfig = require("../../conf/eslint-recommended"),
ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"),
{ Linter } = require("../linter"),
@ -85,7 +85,7 @@ class Registry {
* @returns {void}
*/
populateFromCoreRules() {
const rulesConfig = configRule.createCoreRuleConfigs();
const rulesConfig = configRule.createCoreRuleConfigs(/* noDeprecated = */ true);
this.rules = makeRegistryItems(rulesConfig);
}
@ -329,7 +329,7 @@ function extendFromRecommended(config) {
const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
recRules.forEach(ruleId => {
if (lodash.isEqual(recConfig.rules[ruleId], newConfig.rules[ruleId])) {
if (equal(recConfig.rules[ruleId], newConfig.rules[ruleId])) {
delete newConfig.rules[ruleId];
}
});

View file

@ -117,6 +117,7 @@ function writeJSConfigFile(config, filePath) {
function write(config, filePath) {
switch (path.extname(filePath)) {
case ".js":
case ".cjs":
writeJSConfigFile(config, filePath);
break;

View file

@ -12,6 +12,7 @@
const util = require("util"),
path = require("path"),
fs = require("fs"),
enquirer = require("enquirer"),
ProgressBar = require("progress"),
semver = require("semver"),
@ -48,6 +49,16 @@ function writeFile(config, format) {
extname = ".yml";
} else if (format === "JSON") {
extname = ".json";
} else if (format === "JavaScript") {
const pkgJSONPath = npmUtils.findPackageJson();
if (pkgJSONPath) {
const pkgJSONContents = JSON.parse(fs.readFileSync(pkgJSONPath, "utf8"));
if (pkgJSONContents.type === "module") {
extname = ".cjs";
}
}
}
const installedESLint = config.installedESLint;
@ -531,7 +542,8 @@ function promptUser() {
choices: [
{ message: "Airbnb: https://github.com/airbnb/javascript", name: "airbnb" },
{ message: "Standard: https://github.com/standard/standard", name: "standard" },
{ message: "Google: https://github.com/google/eslint-config-google", name: "google" }
{ message: "Google: https://github.com/google/eslint-config-google", name: "google" },
{ message: "XO: https://github.com/xojs/eslint-config-xo", name: "xo" }
],
skip() {
this.state.answers.packageJsonExists = npmUtils.checkPackageJson();
@ -565,7 +577,8 @@ function promptUser() {
{
type: "toggle",
name: "installESLint",
message(answers) {
message() {
const { answers } = this.state;
const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange)
? "upgrade"
: "downgrade";
@ -682,6 +695,7 @@ const init = {
hasESLintVersionConflict,
installModules,
processAnswers,
writeFile,
/* istanbul ignore next */initializeConfig() {
return promptUser();
}

View file

@ -50,8 +50,7 @@ function findPackageJson(startDir) {
*/
function installSyncSaveDev(packages) {
const packageList = Array.isArray(packages) ? packages : [packages];
const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packageList),
{ stdio: "inherit" });
const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packageList), { stdio: "inherit" });
const error = npmProcess.error;
if (error && error.code === "ENOENT") {
@ -172,6 +171,7 @@ function checkPackageJson(startDir) {
module.exports = {
installSyncSaveDev,
fetchPeerDependencies,
findPackageJson,
checkDeps,
checkDevDeps,
checkPackageJson

View file

@ -5,8 +5,6 @@
"use strict";
const lodash = require("lodash");
/**
* Compares the locations of two objects in a source file
* @param {{line: number, column: number}} itemA The first object
@ -124,7 +122,21 @@ module.exports = ({ directives, problems, reportUnusedDisableDirectives = "off"
.map(directive => Object.assign({}, directive, { unprocessedDirective: directive }))
.sort(compareLocations);
const lineDirectives = lodash.flatMap(directives, directive => {
/**
* Returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
* TODO(stephenwade): Replace this with array.flatMap when we drop support for Node v10
* @param {any[]} array The array to process
* @param {Function} fn The function to use
* @returns {any[]} The result array
*/
function flatMap(array, fn) {
const mapped = array.map(fn);
const flattened = [].concat(...mapped);
return flattened;
}
const lineDirectives = flatMap(directives, directive => {
switch (directive.type) {
case "disable":
case "enable":

View file

@ -15,7 +15,7 @@ const
eslintScope = require("eslint-scope"),
evk = require("eslint-visitor-keys"),
espree = require("espree"),
lodash = require("lodash"),
merge = require("lodash.merge"),
BuiltInEnvironments = require("@eslint/eslintrc/conf/environments"),
pkg = require("../../package.json"),
astUtils = require("../shared/ast-utils"),
@ -37,8 +37,10 @@ const
const debug = require("debug")("eslint:linter");
const MAX_AUTOFIX_PASSES = 10;
const DEFAULT_PARSER_NAME = "espree";
const DEFAULT_ECMA_VERSION = 5;
const commentParser = new ConfigCommentParser();
const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } };
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
//------------------------------------------------------------------------------
// Typedefs
@ -432,10 +434,16 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
/**
* Normalize ECMAScript version from the initial config
* @param {number} ecmaVersion ECMAScript version from the initial config
* @param {Parser} parser The parser which uses this options.
* @param {number} ecmaVersion ECMAScript version from the initial config
* @returns {number} normalized ECMAScript version
*/
function normalizeEcmaVersion(ecmaVersion) {
function normalizeEcmaVersion(parser, ecmaVersion) {
if ((parser[parserSymbol] || parser) === espree) {
if (ecmaVersion === "latest") {
return espree.latestEcmaVersion;
}
}
/*
* Calculate ECMAScript edition number from official year version starting with
@ -444,7 +452,7 @@ function normalizeEcmaVersion(ecmaVersion) {
return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion;
}
const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gu;
const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gsu;
/**
* Checks whether or not there is a comment which has "eslint-env *" in a given text.
@ -521,16 +529,17 @@ function normalizeVerifyOptions(providedOptions, config) {
/**
* Combines the provided parserOptions with the options from environments
* @param {string} parserName The parser name which uses this options.
* @param {Parser} parser The parser which uses this options.
* @param {ParserOptions} providedOptions The provided 'parserOptions' key in a config
* @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments
* @returns {ParserOptions} Resulting parser options after merge
*/
function resolveParserOptions(parserName, providedOptions, enabledEnvironments) {
function resolveParserOptions(parser, providedOptions, enabledEnvironments) {
const parserOptionsFromEnv = enabledEnvironments
.filter(env => env.parserOptions)
.reduce((parserOptions, env) => lodash.merge(parserOptions, env.parserOptions), {});
const mergedParserOptions = lodash.merge(parserOptionsFromEnv, providedOptions || {});
.reduce((parserOptions, env) => merge(parserOptions, env.parserOptions), {});
const mergedParserOptions = merge(parserOptionsFromEnv, providedOptions || {});
const isModule = mergedParserOptions.sourceType === "module";
if (isModule) {
@ -542,12 +551,7 @@ function resolveParserOptions(parserName, providedOptions, enabledEnvironments)
mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false });
}
/*
* TODO: @aladdin-add
* 1. for a 3rd-party parser, do not normalize parserOptions
* 2. for espree, no need to do this (espree will do it)
*/
mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion);
mergedParserOptions.ecmaVersion = normalizeEcmaVersion(parser, mergedParserOptions.ecmaVersion);
return mergedParserOptions;
}
@ -606,7 +610,7 @@ function getRuleOptions(ruleConfig) {
*/
function analyzeScope(ast, parserOptions, visitorKeys) {
const ecmaFeatures = parserOptions.ecmaFeatures || {};
const ecmaVersion = parserOptions.ecmaVersion || 5;
const ecmaVersion = parserOptions.ecmaVersion || DEFAULT_ECMA_VERSION;
return eslintScope.analyze(ast, {
ignoreEval: true,
@ -828,9 +832,10 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze(
* @param {string} filename The reported filename of the code
* @param {boolean} disableFixes If true, it doesn't make `fix` properties.
* @param {string | undefined} cwd cwd of the cli
* @param {string} physicalFilename The full path of the file on disk without any code block information
* @returns {Problem[]} An array of reported problems
*/
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd) {
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd, physicalFilename) {
const emitter = createEmitter();
const nodeQueue = [];
let currentNode = sourceCode.ast;
@ -859,6 +864,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser
getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
getCwd: () => cwd,
getFilename: () => filename,
getPhysicalFilename: () => physicalFilename || filename,
getScope: () => getScope(sourceCode.scopeManager, currentNode),
getSourceCode: () => sourceCode,
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name),
@ -942,7 +948,9 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser
});
// only run code path analyzer if the top level node is "Program", skip otherwise
const eventGenerator = nodeQueue[0].node.type === "Program" ? new CodePathAnalyzer(new NodeEventGenerator(emitter)) : new NodeEventGenerator(emitter);
const eventGenerator = nodeQueue[0].node.type === "Program"
? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
: new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
@ -1119,7 +1127,7 @@ class Linter {
.map(envName => getEnv(slots, envName))
.filter(env => env);
const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs);
const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs);
const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs);
const settings = config.settings || {};
@ -1179,7 +1187,8 @@ class Linter {
settings,
options.filename,
options.disableFixes,
slots.cwd
slots.cwd,
providedOptions.physicalFilename
);
} catch (err) {
err.message += `\nOccurred while linting ${options.filename}`;
@ -1282,9 +1291,12 @@ class Linter {
_verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) {
const filename = options.filename || "<input>";
const filenameToExpose = normalizeFilename(filename);
const physicalFilename = options.physicalFilename || filenameToExpose;
const text = ensureText(textOrSourceCode);
const preprocess = options.preprocess || (rawText => [rawText]);
const postprocess = options.postprocess || lodash.flatten;
// TODO(stephenwade): Replace this with array.flat() when we drop support for Node v10
const postprocess = options.postprocess || (array => [].concat(...array));
const filterCodeBlock =
options.filterCodeBlock ||
(blockFilename => blockFilename.endsWith(".js"));
@ -1306,13 +1318,13 @@ class Linter {
return [];
}
// Resolve configuration again if the file extension was changed.
if (configForRecursive && path.extname(blockName) !== originalExtname) {
debug("Resolving configuration again because the file extension was changed.");
// Resolve configuration again if the file content or extension was changed.
if (configForRecursive && (text !== blockText || path.extname(blockName) !== originalExtname)) {
debug("Resolving configuration again because the file content or extension was changed.");
return this._verifyWithConfigArray(
blockText,
configForRecursive,
{ ...options, filename: blockName }
{ ...options, filename: blockName, physicalFilename }
);
}
@ -1320,7 +1332,7 @@ class Linter {
return this._verifyWithoutProcessors(
blockText,
config,
{ ...options, filename: blockName }
{ ...options, filename: blockName, physicalFilename }
);
});

View file

@ -10,7 +10,6 @@
//------------------------------------------------------------------------------
const esquery = require("esquery");
const lodash = require("lodash");
//------------------------------------------------------------------------------
// Typedefs
@ -32,6 +31,35 @@ const lodash = require("lodash");
// Helpers
//------------------------------------------------------------------------------
/**
* Computes the union of one or more arrays
* @param {...any[]} arrays One or more arrays to union
* @returns {any[]} The union of the input arrays
*/
function union(...arrays) {
// TODO(stephenwade): Replace this with arrays.flat() when we drop support for Node v10
return [...new Set([].concat(...arrays))];
}
/**
* Computes the intersection of one or more arrays
* @param {...any[]} arrays One or more arrays to intersect
* @returns {any[]} The intersection of the input arrays
*/
function intersection(...arrays) {
if (arrays.length === 0) {
return [];
}
let result = [...new Set(arrays[0])];
for (const array of arrays.slice(1)) {
result = result.filter(x => array.includes(x));
}
return result;
}
/**
* Gets the possible types of a selector
* @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
@ -46,7 +74,7 @@ function getPossibleTypes(parsedSelector) {
const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
if (typesForComponents.every(Boolean)) {
return lodash.union(...typesForComponents);
return union(...typesForComponents);
}
return null;
}
@ -63,7 +91,7 @@ function getPossibleTypes(parsedSelector) {
* If at least one of the components could only match a particular type, the compound could only match
* the intersection of those types.
*/
return lodash.intersection(...typesForComponents);
return intersection(...typesForComponents);
}
case "child":
@ -166,15 +194,21 @@ function tryParseSelector(rawSelector) {
}
}
const selectorCache = new Map();
/**
* Parses a raw selector string, and returns the parsed selector along with specificity and type information.
* @param {string} rawSelector A raw AST selector
* @returns {ASTSelector} A selector descriptor
*/
const parseSelector = lodash.memoize(rawSelector => {
function parseSelector(rawSelector) {
if (selectorCache.has(rawSelector)) {
return selectorCache.get(rawSelector);
}
const parsedSelector = tryParseSelector(rawSelector);
return {
const result = {
rawSelector,
isExit: rawSelector.endsWith(":exit"),
parsedSelector,
@ -182,7 +216,10 @@ const parseSelector = lodash.memoize(rawSelector => {
attributeCount: countClassAttributes(parsedSelector),
identifierCount: countIdentifiers(parsedSelector)
};
});
selectorCache.set(rawSelector, result);
return result;
}
//------------------------------------------------------------------------------
// Public Interface
@ -208,10 +245,12 @@ class NodeEventGenerator {
* An SafeEmitter which is the destination of events. This emitter must already
* have registered listeners for all of the events that it needs to listen for.
* (See lib/linter/safe-emitter.js for more details on `SafeEmitter`.)
* @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
* @returns {NodeEventGenerator} new instance
*/
constructor(emitter) {
constructor(emitter, esqueryOptions) {
this.emitter = emitter;
this.esqueryOptions = esqueryOptions;
this.currentAncestry = [];
this.enterSelectorsByNodeType = new Map();
this.exitSelectorsByNodeType = new Map();
@ -250,7 +289,7 @@ class NodeEventGenerator {
* @returns {void}
*/
applySelector(node, selector) {
if (esquery.matches(node, selector.parsedSelector, this.currentAncestry)) {
if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) {
this.emitter.emit(selector.rawSelector, node);
}
}

View file

@ -115,6 +115,17 @@ function normalizeReportLoc(descriptor) {
return descriptor.node.loc;
}
/**
* Check that a fix has a valid range.
* @param {Fix|null} fix The fix to validate.
* @returns {void}
*/
function assertValidFix(fix) {
if (fix) {
assert(fix.range && typeof fix.range[0] === "number" && typeof fix.range[1] === "number", `Fix has invalid range: ${JSON.stringify(fix, null, 2)}`);
}
}
/**
* Compares items in a fixes array by range.
* @param {Fix} a The first message.
@ -133,6 +144,10 @@ function compareFixesByRange(a, b) {
* @returns {{text: string, range: number[]}} The merged fixes
*/
function mergeFixes(fixes, sourceCode) {
for (const fix of fixes) {
assertValidFix(fix);
}
if (fixes.length === 0) {
return null;
}
@ -181,6 +196,8 @@ function normalizeFixes(descriptor, sourceCode) {
if (fix && Symbol.iterator in fix) {
return mergeFixes(Array.from(fix), sourceCode);
}
assertValidFix(fix);
return fix;
}
@ -196,15 +213,19 @@ function mapSuggestions(descriptor, sourceCode, messages) {
return [];
}
return descriptor.suggest.map(suggestInfo => {
const computedDesc = suggestInfo.desc || messages[suggestInfo.messageId];
return descriptor.suggest
.map(suggestInfo => {
const computedDesc = suggestInfo.desc || messages[suggestInfo.messageId];
return {
...suggestInfo,
desc: interpolate(computedDesc, suggestInfo.data),
fix: normalizeFixes(suggestInfo, sourceCode)
};
});
return {
...suggestInfo,
desc: interpolate(computedDesc, suggestInfo.data),
fix: normalizeFixes(suggestInfo, sourceCode)
};
})
// Remove suggestions that didn't provide a fix
.filter(({ fix }) => fix);
}
/**

View file

@ -44,6 +44,26 @@ const enabled = !!process.env.TIMING;
const HEADERS = ["Rule", "Time (ms)", "Relative"];
const ALIGN = [alignLeft, alignRight, alignRight];
/**
* Decide how many rules to show in the output list.
* @returns {number} the number of rules to show
*/
function getListSize() {
const MINIMUM_SIZE = 10;
if (typeof process.env.TIMING !== "string") {
return MINIMUM_SIZE;
}
if (process.env.TIMING.toLowerCase() === "all") {
return Number.POSITIVE_INFINITY;
}
const TIMING_ENV_VAR_AS_INTEGER = Number.parseInt(process.env.TIMING, 10);
return TIMING_ENV_VAR_AS_INTEGER > 10 ? TIMING_ENV_VAR_AS_INTEGER : MINIMUM_SIZE;
}
/* istanbul ignore next */
/**
* display the data
@ -61,7 +81,7 @@ function display(data) {
return [key, time];
})
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
.slice(0, getListSize());
rows.forEach(row => {
row.push(`${(row[1] * 100 / total).toFixed(1)}%`);
@ -133,7 +153,8 @@ module.exports = (function() {
return {
time,
enabled
enabled,
getListSize
};
}());

55
node_modules/eslint/lib/options.js generated vendored
View file

@ -11,6 +11,53 @@
const optionator = require("optionator");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/**
* The options object parsed by Optionator.
* @typedef {Object} ParsedCLIOptions
* @property {boolean} cache Only check changed files
* @property {string} cacheFile Path to the cache file. Deprecated: use --cache-location
* @property {string} [cacheLocation] Path to the cache file or directory
* @property {"metadata" | "content"} cacheStrategy Strategy to use for detecting changed files in the cache
* @property {boolean} [color] Force enabling/disabling of color
* @property {string} [config] Use this configuration, overriding .eslintrc.* config options if present
* @property {boolean} debug Output debugging information
* @property {string[]} [env] Specify environments
* @property {boolean} envInfo Output execution environment information
* @property {boolean} errorOnUnmatchedPattern Prevent errors when pattern is unmatched
* @property {boolean} eslintrc Disable use of configuration from .eslintrc.*
* @property {string[]} [ext] Specify JavaScript file extensions
* @property {boolean} fix Automatically fix problems
* @property {boolean} fixDryRun Automatically fix problems without saving the changes to the file system
* @property {("problem" | "suggestion" | "layout")[]} [fixType] Specify the types of fixes to apply (problem, suggestion, layout)
* @property {string} format Use a specific output format
* @property {string[]} [global] Define global variables
* @property {boolean} [help] Show help
* @property {boolean} ignore Disable use of ignore files and patterns
* @property {string} [ignorePath] Specify path of ignore file
* @property {string[]} [ignorePattern] Pattern of files to ignore (in addition to those in .eslintignore)
* @property {boolean} init Run config initialization wizard
* @property {boolean} inlineConfig Prevent comments from changing config or rules
* @property {number} maxWarnings Number of warnings to trigger nonzero exit code
* @property {string} [outputFile] Specify file to write report to
* @property {string} [parser] Specify the parser to be used
* @property {Object} [parserOptions] Specify parser options
* @property {string[]} [plugin] Specify plugins
* @property {string} [printConfig] Print the configuration for the given file
* @property {boolean | undefined} reportUnusedDisableDirectives Adds reported errors for unused eslint-disable directives
* @property {string} [resolvePluginsRelativeTo] A folder where plugins should be resolved from, CWD by default
* @property {Object} [rule] Specify rules
* @property {string[]} [rulesdir] Use additional rules from this directory
* @property {boolean} stdin Lint code provided on <STDIN>
* @property {string} [stdinFilename] Specify filename to process STDIN as
* @property {boolean} quiet Report errors only
* @property {boolean} [version] Output the version number
* @property {string[]} _ Positional filenames or patterns
*/
//------------------------------------------------------------------------------
// Initialization and Public Interface
//------------------------------------------------------------------------------
@ -214,6 +261,14 @@ module.exports = optionator({
type: "path::String",
description: "Path to the cache file or directory"
},
{
option: "cache-strategy",
dependsOn: ["cache"],
type: "String",
default: "metadata",
enum: ["metadata", "content"],
description: "Strategy to use for detecting changed files in the cache"
},
{
heading: "Miscellaneous"
},

View file

@ -44,7 +44,8 @@ const
assert = require("assert"),
path = require("path"),
util = require("util"),
lodash = require("lodash"),
merge = require("lodash.merge"),
equal = require("fast-deep-equal"),
Traverser = require("../../lib/shared/traverser"),
{ getRuleOptionsSchema, validate } = require("../shared/config-validator"),
{ Linter, SourceCodeFixer, interpolate } = require("../linter");
@ -52,6 +53,7 @@ const
const ajv = require("../shared/ajv")({ strictDefaults: true });
const espreePath = require.resolve("espree");
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
//------------------------------------------------------------------------------
// Typedefs
@ -70,6 +72,7 @@ const espreePath = require.resolve("espree");
* @property {{ [name: string]: any }} [parserOptions] Options for the parser.
* @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
* @property {{ [name: string]: boolean }} [env] Environments for the test case.
* @property {boolean} [only] Run only this test case or the subset of test cases with this property.
*/
/**
@ -85,6 +88,7 @@ const espreePath = require.resolve("espree");
* @property {{ [name: string]: any }} [parserOptions] Options for the parser.
* @property {{ [name: string]: "readonly" | "writable" | "off" }} [globals] The additional global variables.
* @property {{ [name: string]: boolean }} [env] Environments for the test case.
* @property {boolean} [only] Run only this test case or the subset of test cases with this property.
*/
/**
@ -120,7 +124,8 @@ const RuleTesterParameters = [
"filename",
"options",
"errors",
"output"
"output",
"only"
];
/*
@ -235,6 +240,7 @@ function defineStartEndAsError(objName, node) {
});
}
/**
* Define `start`/`end` properties of all nodes of the given AST as throwing error.
* @param {ASTNode} ast The root node to errorize `start`/`end` properties.
@ -254,8 +260,10 @@ function defineStartEndAsErrorInTree(ast, visitorKeys) {
* @returns {Parser} Wrapped parser object.
*/
function wrapParser(parser) {
if (typeof parser.parseForESLint === "function") {
return {
[parserSymbol]: parser,
parseForESLint(...args) {
const ret = parser.parseForESLint(...args);
@ -264,7 +272,9 @@ function wrapParser(parser) {
}
};
}
return {
[parserSymbol]: parser,
parse(...args) {
const ast = parser.parse(...args);
@ -281,6 +291,7 @@ function wrapParser(parser) {
// default separators for testing
const DESCRIBE = Symbol("describe");
const IT = Symbol("it");
const IT_ONLY = Symbol("itOnly");
/**
* This is `it` default handler if `it` don't exist.
@ -324,10 +335,9 @@ class RuleTester {
* configuration and the default configuration.
* @type {Object}
*/
this.testerConfig = lodash.merge(
// we have to clone because merge uses the first argument for recipient
lodash.cloneDeep(defaultConfig),
this.testerConfig = merge(
{},
defaultConfig,
testerConfig,
{ rules: { "rule-tester/validate-ast": "error" } }
);
@ -369,7 +379,7 @@ class RuleTester {
* @returns {void}
*/
static resetDefaultConfig() {
defaultConfig = lodash.cloneDeep(testerDefaultConfig);
defaultConfig = merge({}, testerDefaultConfig);
}
@ -400,6 +410,46 @@ class RuleTester {
this[IT] = value;
}
/**
* Adds the `only` property to a test to run it in isolation.
* @param {string | ValidTestCase | InvalidTestCase} item A single test to run by itself.
* @returns {ValidTestCase | InvalidTestCase} The test with `only` set.
*/
static only(item) {
if (typeof item === "string") {
return { code: item, only: true };
}
return { ...item, only: true };
}
static get itOnly() {
if (typeof this[IT_ONLY] === "function") {
return this[IT_ONLY];
}
if (typeof this[IT] === "function" && typeof this[IT].only === "function") {
return Function.bind.call(this[IT].only, this[IT]);
}
if (typeof it === "function" && typeof it.only === "function") {
return Function.bind.call(it.only, it);
}
if (typeof this[DESCRIBE] === "function" || typeof this[IT] === "function") {
throw new Error(
"Set `RuleTester.itOnly` to use `only` with a custom test framework.\n" +
"See https://eslint.org/docs/developer-guide/nodejs-api#customizing-ruletester for more."
);
}
if (typeof it === "function") {
throw new Error("The current test framework does not support exclusive tests with `only`.");
}
throw new Error("To use `only`, use RuleTester with a test framework that provides `it.only()` like Mocha.");
}
static set itOnly(value) {
this[IT_ONLY] = value;
}
/**
* Define a rule for one particular run of tests.
* @param {string} name The name of the rule to define.
@ -427,12 +477,12 @@ class RuleTester {
scenarioErrors = [],
linter = this.linter;
if (lodash.isNil(test) || typeof test !== "object") {
if (!test || typeof test !== "object") {
throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`);
}
requiredScenarios.forEach(scenarioType => {
if (lodash.isNil(test[scenarioType])) {
if (!test[scenarioType]) {
scenarioErrors.push(`Could not find any ${scenarioType} test scenarios`);
}
});
@ -465,7 +515,7 @@ class RuleTester {
* @private
*/
function runRuleForItem(item) {
let config = lodash.cloneDeep(testerConfig),
let config = merge({}, testerConfig),
code, filename, output, beforeAST, afterAST;
if (typeof item === "string") {
@ -477,13 +527,17 @@ class RuleTester {
* Assumes everything on the item is a config except for the
* parameters used by this tester
*/
const itemConfig = lodash.omit(item, RuleTesterParameters);
const itemConfig = { ...item };
for (const parameter of RuleTesterParameters) {
delete itemConfig[parameter];
}
/*
* Create the config object from the tester config and this item
* specific configurations.
*/
config = lodash.merge(
config = merge(
config,
itemConfig
);
@ -589,7 +643,7 @@ class RuleTester {
* @private
*/
function assertASTDidntChange(beforeAST, afterAST) {
if (!lodash.isEqual(beforeAST, afterAST)) {
if (!equal(beforeAST, afterAST)) {
assert.fail("Rule should not modify AST.");
}
}
@ -606,7 +660,8 @@ class RuleTester {
const messages = result.messages;
assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s",
messages.length, util.inspect(messages)));
messages.length,
util.inspect(messages)));
assertASTDidntChange(result.beforeAST, result.afterAST);
}
@ -661,13 +716,18 @@ class RuleTester {
}
assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s",
item.errors, item.errors === 1 ? "" : "s", messages.length, util.inspect(messages)));
item.errors,
item.errors === 1 ? "" : "s",
messages.length,
util.inspect(messages)));
} else {
assert.strictEqual(
messages.length, item.errors.length,
util.format(
messages.length, item.errors.length, util.format(
"Should have %d error%s but had %d: %s",
item.errors.length, item.errors.length === 1 ? "" : "s", messages.length, util.inspect(messages)
item.errors.length,
item.errors.length === 1 ? "" : "s",
messages.length,
util.inspect(messages)
)
);
@ -881,23 +941,29 @@ class RuleTester {
RuleTester.describe(ruleName, () => {
RuleTester.describe("valid", () => {
test.valid.forEach(valid => {
RuleTester.it(sanitize(typeof valid === "object" ? valid.code : valid), () => {
testValidTemplate(valid);
});
RuleTester[valid.only ? "itOnly" : "it"](
sanitize(typeof valid === "object" ? valid.code : valid),
() => {
testValidTemplate(valid);
}
);
});
});
RuleTester.describe("invalid", () => {
test.invalid.forEach(invalid => {
RuleTester.it(sanitize(invalid.code), () => {
testInvalidTemplate(invalid);
});
RuleTester[invalid.only ? "itOnly" : "it"](
sanitize(invalid.code),
() => {
testInvalidTemplate(invalid);
}
);
});
});
});
}
}
RuleTester[DESCRIBE] = RuleTester[IT] = null;
RuleTester[DESCRIBE] = RuleTester[IT] = RuleTester[IT_ONLY] = null;
module.exports = RuleTester;

View file

@ -87,17 +87,17 @@ module.exports = {
}
/**
* Gets the closing parenthesis which is the pair of the given opening parenthesis.
* @param {Token} token The opening parenthesis token to get.
* Gets the closing parenthesis by the given node.
* @param {ASTNode} node first node after an opening parenthesis.
* @returns {Token} The found closing parenthesis token.
*/
function findClosingParen(token) {
let node = sourceCode.getNodeByRangeIndex(token.range[0]);
function findClosingParen(node) {
let nodeToCheck = node;
while (!astUtils.isParenthesised(sourceCode, node)) {
node = node.parent;
while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) {
nodeToCheck = nodeToCheck.parent;
}
return sourceCode.getTokenAfter(node);
return sourceCode.getTokenAfter(nodeToCheck);
}
/**
@ -191,7 +191,7 @@ module.exports = {
}
/*
* If the first token of the reutrn value is `{` or the return value is a sequence expression,
* If the first token of the return value is `{` or the return value is a sequence expression,
* enclose the return value by parentheses to avoid syntax error.
*/
if (astUtils.isOpeningBraceToken(firstValueToken) || blockBody[0].argument.type === "SequenceExpression" || (funcInfo.hasInOperator && isInsideForLoopInitializer(node))) {
@ -226,12 +226,22 @@ module.exports = {
const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken);
const [firstTokenAfterArrow, secondTokenAfterArrow] = sourceCode.getTokensAfter(arrowToken, { count: 2 });
const lastToken = sourceCode.getLastToken(node);
const isParenthesisedObjectLiteral =
let parenthesisedObjectLiteral = null;
if (
astUtils.isOpeningParenToken(firstTokenAfterArrow) &&
astUtils.isOpeningBraceToken(secondTokenAfterArrow);
astUtils.isOpeningBraceToken(secondTokenAfterArrow)
) {
const braceNode = sourceCode.getNodeByRangeIndex(secondTokenAfterArrow.range[0]);
if (braceNode.type === "ObjectExpression") {
parenthesisedObjectLiteral = braceNode;
}
}
// If the value is object literal, remove parentheses which were forced by syntax.
if (isParenthesisedObjectLiteral) {
if (parenthesisedObjectLiteral) {
const openingParenToken = firstTokenAfterArrow;
const openingBraceToken = secondTokenAfterArrow;
@ -247,7 +257,7 @@ module.exports = {
}
// Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo()
fixes.push(fixer.remove(findClosingParen(openingBraceToken)));
fixes.push(fixer.remove(findClosingParen(parenthesisedObjectLiteral)));
fixes.push(fixer.insertTextAfter(lastToken, "}"));
} else {

View file

@ -59,9 +59,9 @@ module.exports = {
}
/**
* Check to see if a node contains only identifers
* Check to see if a node contains only identifiers
* @param {ASTNode} node The node to check
* @returns {boolean} Whether or not the node contains only identifers
* @returns {boolean} Whether or not the node contains only identifiers
*/
function containsOnlyIdentifiers(node) {
if (node.type === "Identifier") {

View file

@ -9,7 +9,6 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
@ -144,23 +143,33 @@ module.exports = {
* @returns {ASTNode|null} The last node or null.
*/
function getLastItem(node) {
/**
* Returns the last element of an array
* @param {any[]} array The input array
* @returns {any} The last element
*/
function last(array) {
return array[array.length - 1];
}
switch (node.type) {
case "ObjectExpression":
case "ObjectPattern":
return lodash.last(node.properties);
return last(node.properties);
case "ArrayExpression":
case "ArrayPattern":
return lodash.last(node.elements);
return last(node.elements);
case "ImportDeclaration":
case "ExportNamedDeclaration":
return lodash.last(node.specifiers);
return last(node.specifiers);
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
return lodash.last(node.params);
return last(node.params);
case "CallExpression":
case "NewExpression":
return lodash.last(node.arguments);
return last(node.arguments);
default:
return null;
}
@ -316,7 +325,7 @@ module.exports = {
"always-multiline": forceTrailingCommaIfMultiline,
"only-multiline": allowTrailingCommaIfMultiline,
never: forbidTrailingComma,
ignore: lodash.noop
ignore: () => {}
};
return {

View file

@ -181,7 +181,7 @@ module.exports = {
validateCommaItemSpacing({
comma: token,
left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken,
left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.includes(token) ? null : previousToken,
right: astUtils.isCommaToken(nextToken) ? null : nextToken
}, token);
});

View file

@ -207,8 +207,7 @@ module.exports = {
* they are always valid regardless of an undefined item.
*/
if (astUtils.isCommaToken(commaToken)) {
validateCommaItemSpacing(previousItemToken, commaToken,
currentItemToken, reportItem);
validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem);
}
if (item) {

View file

@ -10,9 +10,8 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
const { upperCaseFirst } = require("../shared/string-utils");
//------------------------------------------------------------------------------
// Rule Definition
@ -95,7 +94,7 @@ module.exports = {
* @private
*/
function endFunction(node) {
const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node));
const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node));
const complexity = fns.pop();
if (complexity > THRESHOLD) {
@ -153,7 +152,13 @@ module.exports = {
IfStatement: increaseComplexity,
SwitchCase: increaseSwitchComplexity,
WhileStatement: increaseComplexity,
DoWhileStatement: increaseComplexity
DoWhileStatement: increaseComplexity,
AssignmentExpression(node) {
if (astUtils.isLogicalAssignmentOperator(node.operator)) {
increaseComplexity();
}
}
};
}

View file

@ -8,8 +8,8 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
const { upperCaseFirst } = require("../shared/string-utils");
//------------------------------------------------------------------------------
// Helpers
@ -104,18 +104,18 @@ module.exports = {
} else if (node.type === "ArrowFunctionExpression") {
// `=>` token
loc = context.getSourceCode().getTokenBefore(node.body, astUtils.isArrowToken).loc.start;
loc = context.getSourceCode().getTokenBefore(node.body, astUtils.isArrowToken).loc;
} else if (
node.parent.type === "MethodDefinition" ||
(node.parent.type === "Property" && node.parent.method)
) {
// Method name.
loc = node.parent.key.loc.start;
loc = node.parent.key.loc;
} else {
// Function name or `function` keyword.
loc = (node.id || node).loc.start;
loc = (node.id || context.getSourceCode().getFirstToken(node)).loc;
}
if (!name) {
@ -164,7 +164,7 @@ module.exports = {
funcInfo.data = {
name: funcInfo.node.type === "Program"
? "Program"
: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
: upperCaseFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
};
} else if (funcInfo.hasReturnValue !== hasReturnValue) {
context.report({

View file

@ -79,6 +79,17 @@ function isPossibleConstructor(node) {
return false;
case "LogicalExpression":
/*
* If the && operator short-circuits, the left side was falsy and therefore not a constructor, and if
* it doesn't short-circuit, it takes the value from the right side, so the right side must always be a
* possible constructor. A future improvement could verify that the left side could be truthy by
* excluding falsy literals.
*/
if (node.operator === "&&") {
return isPossibleConstructor(node.right);
}
return (
isPossibleConstructor(node.left) ||
isPossibleConstructor(node.right)

View file

@ -46,7 +46,7 @@ module.exports = {
const sourceCode = context.getSourceCode();
/**
* Reports if the dot between object and property is on the correct loccation.
* Reports if the dot between object and property is on the correct location.
* @param {ASTNode} node The `MemberExpression` node.
* @returns {void}
*/

View file

@ -94,7 +94,7 @@ module.exports = {
// Don't perform any fixes if there are comments inside the brackets.
if (sourceCode.commentsExistBetween(leftBracket, rightBracket)) {
return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
return;
}
// Replace the brackets by an identifier.
@ -154,12 +154,12 @@ module.exports = {
// A statement that starts with `let[` is parsed as a destructuring variable declaration, not a MemberExpression.
if (node.object.type === "Identifier" && node.object.name === "let" && !node.optional) {
return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
return;
}
// Don't perform any fixes if there are comments between the dot and the property name.
if (sourceCode.commentsExistBetween(dotToken, node.property)) {
return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
return;
}
// Replace the identifier to brackets.

View file

@ -4,12 +4,6 @@
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -48,13 +42,14 @@ module.exports = {
Program: function checkBadEOF(node) {
const sourceCode = context.getSourceCode(),
src = sourceCode.getText(),
lastLine = sourceCode.lines[sourceCode.lines.length - 1],
location = {
column: lodash.last(sourceCode.lines).length,
column: lastLine.length,
line: sourceCode.lines.length
},
LF = "\n",
CRLF = `\r${LF}`,
endsWithNewline = lodash.endsWith(src, LF);
endsWithNewline = src.endsWith(LF);
/*
* Empty source is always valid: No content in file so we don't

View file

@ -131,7 +131,7 @@ module.exports = {
return null;
}
// If `?.` exsits, it doesn't hide no-undexpected-multiline errors
// If `?.` exists, it doesn't hide no-unexpected-multiline errors
if (node.optional) {
return fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], "?.");
}
@ -177,7 +177,7 @@ module.exports = {
/*
* Only autofix if there is no newline
* https://github.com/eslint/eslint/issues/7787
* But if `?.` exsits, it doesn't hide no-undexpected-multiline errors
* But if `?.` exists, it doesn't hide no-unexpected-multiline errors
*/
if (!node.optional) {
return null;

View file

@ -218,7 +218,7 @@ module.exports = {
}
case "ArrowFunctionExpression": {
const firstToken = sourceCode.getFirstToken(node);
const firstToken = sourceCode.getFirstToken(node, { skip: (node.async ? 1 : 0) });
if (!astUtils.isOpeningParenToken(firstToken)) {

View file

@ -954,7 +954,7 @@ module.exports = {
}
/**
* Checks wether a return statement is wrapped in ()
* Checks whether a return statement is wrapped in ()
* @param {ASTNode} node node to examine
* @returns {boolean} the result
*/

View file

@ -12,10 +12,10 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
const createTree = require("functional-red-black-tree");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -1068,7 +1068,7 @@ module.exports = {
const baseOffsetListeners = {
"ArrayExpression, ArrayPattern"(node) {
const openingBracket = sourceCode.getFirstToken(node);
const closingBracket = sourceCode.getTokenAfter(lodash.findLast(node.elements) || openingBracket, astUtils.isClosingBracketToken);
const closingBracket = sourceCode.getTokenAfter([...node.elements].reverse().find(_ => _) || openingBracket, astUtils.isClosingBracketToken);
addElementListIndent(node.elements, openingBracket, closingBracket, options.ArrayExpression);
},
@ -1177,7 +1177,7 @@ module.exports = {
offsets.setDesiredOffset(questionMarkToken, firstToken, 1);
offsets.setDesiredOffset(colonToken, firstToken, 1);
offsets.setDesiredOffset(firstConsequentToken, firstToken,
offsets.setDesiredOffset(firstConsequentToken, firstToken, firstConsequentToken.type === "Punctuator" &&
options.offsetTernaryExpressions ? 2 : 1);
/*
@ -1203,8 +1203,7 @@ module.exports = {
* If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up
* having no expected indentation.
*/
offsets.setDesiredOffset(firstAlternateToken, firstToken,
firstAlternateToken.type === "Punctuator" &&
offsets.setDesiredOffset(firstAlternateToken, firstToken, firstAlternateToken.type === "Punctuator" &&
options.offsetTernaryExpressions ? 2 : 1);
}
}
@ -1559,8 +1558,9 @@ module.exports = {
* 2. Don't set any offsets against the first token of the node.
* 3. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
*/
const offsetListeners = lodash.mapValues(
baseOffsetListeners,
const offsetListeners = {};
for (const [selector, listener] of Object.entries(baseOffsetListeners)) {
/*
* Offset listener calls are deferred until traversal is finished, and are called as
@ -1578,10 +1578,8 @@ module.exports = {
* To avoid this, the `Identifier` listener isn't called until traversal finishes and all
* ignored nodes are known.
*/
listener =>
node =>
listenerCallQueue.push({ listener, node })
);
offsetListeners[selector] = node => listenerCallQueue.push({ listener, node });
}
// For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set.
const ignoredNodes = new Set();

View file

@ -169,6 +169,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
"no-new-require": () => require("./no-new-require"),
"no-new-symbol": () => require("./no-new-symbol"),
"no-new-wrappers": () => require("./no-new-wrappers"),
"no-nonoctal-decimal-escape": () => require("./no-nonoctal-decimal-escape"),
"no-obj-calls": () => require("./no-obj-calls"),
"no-octal": () => require("./no-octal"),
"no-octal-escape": () => require("./no-octal-escape"),
@ -217,6 +218,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
"no-unreachable-loop": () => require("./no-unreachable-loop"),
"no-unsafe-finally": () => require("./no-unsafe-finally"),
"no-unsafe-negation": () => require("./no-unsafe-negation"),
"no-unsafe-optional-chaining": () => require("./no-unsafe-optional-chaining"),
"no-unused-expressions": () => require("./no-unused-expressions"),
"no-unused-labels": () => require("./no-unused-labels"),
"no-unused-vars": () => require("./no-unused-vars"),

View file

@ -8,8 +8,7 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash"),
astUtils = require("./utils/ast-utils");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
@ -347,7 +346,7 @@ module.exports = {
const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
// check for newline before
if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
if (!exceptionStartAllowed && before && !commentAndEmptyLines.includes(prevLineNum) &&
!(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
const lineStart = token.range[0] - token.loc.start.column;
const range = [lineStart, lineStart];
@ -362,7 +361,7 @@ module.exports = {
}
// check for newline after
if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
if (!exceptionEndAllowed && after && !commentAndEmptyLines.includes(nextLineNum) &&
!(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
context.report({
node: token,

View file

@ -9,8 +9,7 @@
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
const lodash = require("lodash");
const { upperCaseFirst } = require("../shared/string-utils");
//------------------------------------------------------------------------------
// Constants
@ -71,7 +70,7 @@ module.exports = {
type: "suggestion",
docs: {
description: "enforce a maximum number of line of code in a function",
description: "enforce a maximum number of lines of code in a function",
category: "Stylistic Issues",
recommended: false,
url: "https://eslint.org/docs/rules/max-lines-per-function"
@ -191,7 +190,7 @@ module.exports = {
}
if (lineCount > maxLines) {
const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(funcNode));
const name = upperCaseFirst(astUtils.getFunctionNameWithKind(funcNode));
context.report({
node,

View file

@ -8,9 +8,22 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Creates an array of numbers from `start` up to, but not including, `end`
* @param {number} start The start of the range
* @param {number} end The end of the range
* @returns {number[]} The range of numbers
*/
function range(start, end) {
return [...Array(end - start).keys()].map(x => x + start);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -119,11 +132,25 @@ module.exports = {
}
if (start <= end) {
return lodash.range(start, end + 1);
return range(start, end + 1);
}
return [];
}
/**
* Returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
* TODO(stephenwade): Replace this with array.flatMap when we drop support for Node v10
* @param {any[]} array The array to process
* @param {Function} fn The function to use
* @returns {any[]} The result array
*/
function flatMap(array, fn) {
const mapped = array.map(fn);
const flattened = [].concat(...mapped);
return flattened;
}
return {
"Program:exit"() {
let lines = sourceCode.lines.map((text, i) => ({
@ -131,6 +158,14 @@ module.exports = {
text
}));
/*
* If file ends with a linebreak, `sourceCode.lines` will have one extra empty line at the end.
* That isn't a real line, so we shouldn't count it.
*/
if (lines.length > 1 && lines[lines.length - 1].text === "") {
lines.pop();
}
if (skipBlankLines) {
lines = lines.filter(l => l.text.trim() !== "");
}
@ -138,12 +173,10 @@ module.exports = {
if (skipComments) {
const comments = sourceCode.getAllComments();
const commentLines = lodash.flatten(
comments.map(comment => getLinesWithoutCode(comment))
);
const commentLines = flatMap(comments, comment => getLinesWithoutCode(comment));
lines = lines.filter(
l => !lodash.includes(commentLines, l.lineNumber)
l => !commentLines.includes(l.lineNumber)
);
}
@ -155,7 +188,7 @@ module.exports = {
},
end: {
line: sourceCode.lines.length,
column: lodash.last(sourceCode.lines).length
column: sourceCode.lines[sourceCode.lines.length - 1].length
}
};

View file

@ -9,9 +9,8 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
const { upperCaseFirst } = require("../shared/string-utils");
//------------------------------------------------------------------------------
// Rule Definition
@ -85,7 +84,7 @@ module.exports = {
node,
messageId: "exceed",
data: {
name: lodash.upperFirst(astUtils.getFunctionNameWithKind(node)),
name: upperCaseFirst(astUtils.getFunctionNameWithKind(node)),
count: node.params.length,
max: numParams
}

View file

@ -9,9 +9,8 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
const { upperCaseFirst } = require("../shared/string-utils");
//------------------------------------------------------------------------------
// Rule Definition
@ -97,7 +96,7 @@ module.exports = {
*/
function reportIfTooManyStatements(node, count, max) {
if (count > max) {
const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node));
const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node));
context.report({
node,

View file

@ -27,19 +27,22 @@ module.exports = {
enum: ["always", "always-multiline", "never"]
}
],
messages: {
expectedTestCons: "Expected newline between test and consequent of ternary expression.",
expectedConsAlt: "Expected newline between consequent and alternate of ternary expression.",
unexpectedTestCons: "Unexpected newline between test and consequent of ternary expression.",
unexpectedConsAlt: "Unexpected newline between consequent and alternate of ternary expression."
}
},
fixable: "whitespace"
},
create(context) {
const sourceCode = context.getSourceCode();
const option = context.options[0];
const multiline = option !== "never";
const allowSingleLine = option === "always-multiline";
const sourceCode = context.getSourceCode();
//--------------------------------------------------------------------------
// Public
@ -59,6 +62,8 @@ module.exports = {
const areTestAndConsequentOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfTest, firstTokenOfConsequent);
const areConsequentAndAlternateOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfConsequent, firstTokenOfAlternate);
const hasComments = !!sourceCode.getCommentsInside(node).length;
if (!multiline) {
if (!areTestAndConsequentOnSameLine) {
context.report({
@ -67,7 +72,24 @@ module.exports = {
start: firstTokenOfTest.loc.start,
end: lastTokenOfTest.loc.end
},
messageId: "unexpectedTestCons"
messageId: "unexpectedTestCons",
fix: fixer => {
if (hasComments) {
return null;
}
const fixers = [];
const areTestAndQuestionOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfTest, questionToken);
const areQuestionAndConsOnSameLine = astUtils.isTokenOnSameLine(questionToken, firstTokenOfConsequent);
if (!areTestAndQuestionOnSameLine) {
fixers.push(fixer.removeRange([lastTokenOfTest.range[1], questionToken.range[0]]));
}
if (!areQuestionAndConsOnSameLine) {
fixers.push(fixer.removeRange([questionToken.range[1], firstTokenOfConsequent.range[0]]));
}
return fixers;
}
});
}
@ -78,7 +100,24 @@ module.exports = {
start: firstTokenOfConsequent.loc.start,
end: lastTokenOfConsequent.loc.end
},
messageId: "unexpectedConsAlt"
messageId: "unexpectedConsAlt",
fix: fixer => {
if (hasComments) {
return null;
}
const fixers = [];
const areConsAndColonOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfConsequent, colonToken);
const areColonAndAltOnSameLine = astUtils.isTokenOnSameLine(colonToken, firstTokenOfAlternate);
if (!areConsAndColonOnSameLine) {
fixers.push(fixer.removeRange([lastTokenOfConsequent.range[1], colonToken.range[0]]));
}
if (!areColonAndAltOnSameLine) {
fixers.push(fixer.removeRange([colonToken.range[1], firstTokenOfAlternate.range[0]]));
}
return fixers;
}
});
}
} else {
@ -93,7 +132,16 @@ module.exports = {
start: firstTokenOfTest.loc.start,
end: lastTokenOfTest.loc.end
},
messageId: "expectedTestCons"
messageId: "expectedTestCons",
fix: fixer => (hasComments ? null : (
fixer.replaceTextRange(
[
lastTokenOfTest.range[1],
questionToken.range[0]
],
"\n"
)
))
});
}
@ -104,7 +152,16 @@ module.exports = {
start: firstTokenOfConsequent.loc.start,
end: lastTokenOfConsequent.loc.end
},
messageId: "expectedConsAlt"
messageId: "expectedConsAlt",
fix: (fixer => (hasComments ? null : (
fixer.replaceTextRange(
[
lastTokenOfConsequent.range[1],
colonToken.range[0]
],
"\n"
)
)))
});
}
}

View file

@ -9,9 +9,6 @@
// Helpers
//------------------------------------------------------------------------------
const EQUALITY_OPERATORS = ["===", "!==", "==", "!="];
const RELATIONAL_OPERATORS = [">", "<", ">=", "<=", "in", "instanceof"];
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -56,6 +53,35 @@ module.exports = {
// Helpers
//--------------------------------------------------------------------------
/**
* Returns literal's value converted to the Boolean type
* @param {ASTNode} node any `Literal` node
* @returns {boolean | null} `true` when node is truthy, `false` when node is falsy,
* `null` when it cannot be determined.
*/
function getBooleanValue(node) {
if (node.value === null) {
/*
* it might be a null literal or bigint/regex literal in unsupported environments .
* https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es5.md#regexpliteral
* https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es2020.md#bigintliteral
*/
if (node.raw === "null") {
return false;
}
// regex is always truthy
if (typeof node.regex === "object") {
return true;
}
return null;
}
return !!node.value;
}
/**
* Checks if a branch node of LogicalExpression short circuits the whole condition
@ -66,15 +92,28 @@ module.exports = {
function isLogicalIdentity(node, operator) {
switch (node.type) {
case "Literal":
return (operator === "||" && node.value === true) ||
(operator === "&&" && node.value === false);
return (operator === "||" && getBooleanValue(node) === true) ||
(operator === "&&" && getBooleanValue(node) === false);
case "UnaryExpression":
return (operator === "&&" && node.operator === "void");
case "LogicalExpression":
return isLogicalIdentity(node.left, node.operator) ||
isLogicalIdentity(node.right, node.operator);
/*
* handles `a && false || b`
* `false` is an identity element of `&&` but not `||`
*/
return operator === node.operator &&
(
isLogicalIdentity(node.left, operator) ||
isLogicalIdentity(node.right, operator)
);
case "AssignmentExpression":
return ["||=", "&&="].includes(node.operator) &&
operator === node.operator.slice(0, -1) &&
isLogicalIdentity(node.right, operator);
// no default
}
@ -113,12 +152,18 @@ module.exports = {
}
case "UnaryExpression":
if (node.operator === "void") {
if (
node.operator === "void" ||
node.operator === "typeof" && inBooleanPosition
) {
return true;
}
return (node.operator === "typeof" && inBooleanPosition) ||
isConstant(node.argument, true);
if (node.operator === "!") {
return isConstant(node.argument, true);
}
return isConstant(node.argument, false);
case "BinaryExpression":
return isConstant(node.left, false) &&
@ -129,27 +174,23 @@ module.exports = {
const isLeftConstant = isConstant(node.left, inBooleanPosition);
const isRightConstant = isConstant(node.right, inBooleanPosition);
const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
const isRightShortCircuit = (isRightConstant && isLogicalIdentity(node.right, node.operator));
const isRightShortCircuit = (inBooleanPosition && isRightConstant && isLogicalIdentity(node.right, node.operator));
return (isLeftConstant && isRightConstant) ||
(
// in the case of an "OR", we need to know if the right constant value is truthy
node.operator === "||" &&
isRightConstant &&
node.right.value &&
(
!node.parent ||
node.parent.type !== "BinaryExpression" ||
!(EQUALITY_OPERATORS.includes(node.parent.operator) || RELATIONAL_OPERATORS.includes(node.parent.operator))
)
) ||
isLeftShortCircuit ||
isRightShortCircuit;
}
case "AssignmentExpression":
return (node.operator === "=") && isConstant(node.right, inBooleanPosition);
if (node.operator === "=") {
return isConstant(node.right, inBooleanPosition);
}
if (["||=", "&&="].includes(node.operator) && inBooleanPosition) {
return isLogicalIdentity(node.right, node.operator.slice(0, -1));
}
return false;
case "SequenceExpression":
return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);

View file

@ -8,7 +8,6 @@
const RegExpValidator = require("regexpp").RegExpValidator;
const collector = new (class {
constructor() {
this.ecmaVersion = 2018;
this._source = "";
this._controlChars = [];
this._validator = new RegExpValidator(this);

View file

@ -4,42 +4,183 @@
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const NAMED_TYPES = ["ImportSpecifier", "ExportSpecifier"];
const NAMESPACE_TYPES = [
"ImportNamespaceSpecifier",
"ExportNamespaceSpecifier"
];
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/**
* Check if an import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier).
* @param {string} importExportType An import/export type to check.
* @param {string} type Can be "named" or "namespace"
* @returns {boolean} True if import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier) and false if it doesn't.
*/
function isImportExportSpecifier(importExportType, type) {
const arrayToCheck = type === "named" ? NAMED_TYPES : NAMESPACE_TYPES;
return arrayToCheck.includes(importExportType);
}
/**
* Return the type of (import|export).
* @param {ASTNode} node A node to get.
* @returns {string} The type of the (import|export).
*/
function getImportExportType(node) {
if (node.specifiers && node.specifiers.length > 0) {
const nodeSpecifiers = node.specifiers;
const index = nodeSpecifiers.findIndex(
({ type }) =>
isImportExportSpecifier(type, "named") ||
isImportExportSpecifier(type, "namespace")
);
const i = index > -1 ? index : 0;
return nodeSpecifiers[i].type;
}
if (node.type === "ExportAllDeclaration") {
if (node.exported) {
return "ExportNamespaceSpecifier";
}
return "ExportAll";
}
return "SideEffectImport";
}
/**
* Returns a boolean indicates if two (import|export) can be merged
* @param {ASTNode} node1 A node to check.
* @param {ASTNode} node2 A node to check.
* @returns {boolean} True if two (import|export) can be merged, false if they can't.
*/
function isImportExportCanBeMerged(node1, node2) {
const importExportType1 = getImportExportType(node1);
const importExportType2 = getImportExportType(node2);
if (
(importExportType1 === "ExportAll" &&
importExportType2 !== "ExportAll" &&
importExportType2 !== "SideEffectImport") ||
(importExportType1 !== "ExportAll" &&
importExportType1 !== "SideEffectImport" &&
importExportType2 === "ExportAll")
) {
return false;
}
if (
(isImportExportSpecifier(importExportType1, "namespace") &&
isImportExportSpecifier(importExportType2, "named")) ||
(isImportExportSpecifier(importExportType2, "namespace") &&
isImportExportSpecifier(importExportType1, "named"))
) {
return false;
}
return true;
}
/**
* Returns a boolean if we should report (import|export).
* @param {ASTNode} node A node to be reported or not.
* @param {[ASTNode]} previousNodes An array contains previous nodes of the module imported or exported.
* @returns {boolean} True if the (import|export) should be reported.
*/
function shouldReportImportExport(node, previousNodes) {
let i = 0;
while (i < previousNodes.length) {
if (isImportExportCanBeMerged(node, previousNodes[i])) {
return true;
}
i++;
}
return false;
}
/**
* Returns array contains only nodes with declarations types equal to type.
* @param {[{node: ASTNode, declarationType: string}]} nodes An array contains objects, each object contains a node and a declaration type.
* @param {string} type Declaration type.
* @returns {[ASTNode]} An array contains only nodes with declarations types equal to type.
*/
function getNodesByDeclarationType(nodes, type) {
return nodes
.filter(({ declarationType }) => declarationType === type)
.map(({ node }) => node);
}
/**
* Returns the name of the module imported or re-exported.
* @param {ASTNode} node A node to get.
* @returns {string} the name of the module, or empty string if no name.
* @returns {string} The name of the module, or empty string if no name.
*/
function getValue(node) {
function getModule(node) {
if (node && node.source && node.source.value) {
return node.source.value.trim();
}
return "";
}
/**
* Checks if the name of the import or export exists in the given array, and reports if so.
* Checks if the (import|export) can be merged with at least one import or one export, and reports if so.
* @param {RuleContext} context The ESLint rule context object.
* @param {ASTNode} node A node to get.
* @param {string} value The name of the imported or exported module.
* @param {string[]} array The array containing other imports or exports in the file.
* @param {string} messageId A messageId to be reported after the name of the module
*
* @returns {void} No return value
* @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
* @param {string} declarationType A declaration type can be an import or export.
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
* @returns {void} No return value.
*/
function checkAndReport(context, node, value, array, messageId) {
if (array.indexOf(value) !== -1) {
context.report({
node,
messageId,
data: {
module: value
function checkAndReport(
context,
node,
modules,
declarationType,
includeExports
) {
const module = getModule(node);
if (modules.has(module)) {
const previousNodes = modules.get(module);
const messagesIds = [];
const importNodes = getNodesByDeclarationType(previousNodes, "import");
let exportNodes;
if (includeExports) {
exportNodes = getNodesByDeclarationType(previousNodes, "export");
}
if (declarationType === "import") {
if (shouldReportImportExport(node, importNodes)) {
messagesIds.push("import");
}
});
if (includeExports) {
if (shouldReportImportExport(node, exportNodes)) {
messagesIds.push("importAs");
}
}
} else if (declarationType === "export") {
if (shouldReportImportExport(node, exportNodes)) {
messagesIds.push("export");
}
if (shouldReportImportExport(node, importNodes)) {
messagesIds.push("exportAs");
}
}
messagesIds.forEach(messageId =>
context.report({
node,
messageId,
data: {
module
}
}));
}
}
@ -49,47 +190,39 @@ function checkAndReport(context, node, value, array, messageId) {
*/
/**
* Returns a function handling the imports of a given file
* Returns a function handling the (imports|exports) of a given file
* @param {RuleContext} context The ESLint rule context object.
* @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
* @param {string} declarationType A declaration type can be an import or export.
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
* @param {string[]} importsInFile The array containing other imports in the file.
* @param {string[]} exportsInFile The array containing other exports in the file.
*
* @returns {nodeCallback} A function passed to ESLint to handle the statement.
*/
function handleImports(context, includeExports, importsInFile, exportsInFile) {
function handleImportsExports(
context,
modules,
declarationType,
includeExports
) {
return function(node) {
const value = getValue(node);
const module = getModule(node);
if (value) {
checkAndReport(context, node, value, importsInFile, "import");
if (module) {
checkAndReport(
context,
node,
modules,
declarationType,
includeExports
);
const currentNode = { node, declarationType };
let nodes = [currentNode];
if (includeExports) {
checkAndReport(context, node, value, exportsInFile, "importAs");
if (modules.has(module)) {
const previousNodes = modules.get(module);
nodes = [...previousNodes, currentNode];
}
importsInFile.push(value);
}
};
}
/**
* Returns a function handling the exports of a given file
* @param {RuleContext} context The ESLint rule context object.
* @param {string[]} importsInFile The array containing other imports in the file.
* @param {string[]} exportsInFile The array containing other exports in the file.
*
* @returns {nodeCallback} A function passed to ESLint to handle the statement.
*/
function handleExports(context, importsInFile, exportsInFile) {
return function(node) {
const value = getValue(node);
if (value) {
checkAndReport(context, node, value, exportsInFile, "export");
checkAndReport(context, node, value, importsInFile, "exportAs");
exportsInFile.push(value);
modules.set(module, nodes);
}
};
}
@ -105,16 +238,19 @@ module.exports = {
url: "https://eslint.org/docs/rules/no-duplicate-imports"
},
schema: [{
type: "object",
properties: {
includeExports: {
type: "boolean",
default: false
}
},
additionalProperties: false
}],
schema: [
{
type: "object",
properties: {
includeExports: {
type: "boolean",
default: false
}
},
additionalProperties: false
}
],
messages: {
import: "'{{module}}' import is duplicated.",
importAs: "'{{module}}' import is duplicated as export.",
@ -125,18 +261,30 @@ module.exports = {
create(context) {
const includeExports = (context.options[0] || {}).includeExports,
importsInFile = [],
exportsInFile = [];
modules = new Map();
const handlers = {
ImportDeclaration: handleImports(context, includeExports, importsInFile, exportsInFile)
ImportDeclaration: handleImportsExports(
context,
modules,
"import",
includeExports
)
};
if (includeExports) {
handlers.ExportNamedDeclaration = handleExports(context, importsInFile, exportsInFile);
handlers.ExportAllDeclaration = handleExports(context, importsInFile, exportsInFile);
handlers.ExportNamedDeclaration = handleImportsExports(
context,
modules,
"export",
includeExports
);
handlers.ExportAllDeclaration = handleImportsExports(
context,
modules,
"export",
includeExports
);
}
return handlers;
}
};

View file

@ -138,7 +138,7 @@ module.exports = {
}
/*
* `identifierNode.parent` is a MamberExpression `*.prototype`.
* `identifierNode.parent` is a MemberExpression `*.prototype`.
* If it's an optional member access, it may be wrapped by a `ChainExpression` node.
*/
const prototypeNode =

View file

@ -472,20 +472,34 @@ module.exports = {
const callee = node.callee;
if (hasExcessParensWithPrecedence(callee, precedence(node))) {
const hasNewParensException = callee.type === "NewExpression" && !isNewExpressionWithParens(callee);
if (
hasDoubleExcessParens(callee) ||
!isIIFE(node) &&
!hasNewParensException &&
!(
isIIFE(node) ||
// Allow extra parens around a new expression if they are intervening parentheses.
node.type === "NewExpression" &&
callee.type === "MemberExpression" &&
doesMemberExpressionContainCallExpression(callee)
) &&
!(!node.optional && callee.type === "ChainExpression")
// (new A)(); new (new A)();
(
callee.type === "NewExpression" &&
!isNewExpressionWithParens(callee) &&
!(
node.type === "NewExpression" &&
!isNewExpressionWithParens(node)
)
) ||
// new (a().b)(); new (a.b().c);
(
node.type === "NewExpression" &&
callee.type === "MemberExpression" &&
doesMemberExpressionContainCallExpression(callee)
) ||
// (a?.b)(); (a?.())();
(
!node.optional &&
callee.type === "ChainExpression"
)
)
) {
report(node.callee);
}
@ -511,7 +525,7 @@ module.exports = {
if (!shouldSkipLeft && hasExcessParens(node.left)) {
if (
!(node.left.type === "UnaryExpression" && isExponentiation) &&
!(["AwaitExpression", "UnaryExpression"].includes(node.left.type) && isExponentiation) &&
!astUtils.isMixedLogicalAndCoalesceExpressions(node.left, node) &&
(leftPrecedence > prec || (leftPrecedence === prec && !isExponentiation)) ||
isParenthesisedTwice(node.left)
@ -830,45 +844,49 @@ module.exports = {
ExportDefaultDeclaration: node => checkExpressionOrExportStatement(node.declaration),
ExpressionStatement: node => checkExpressionOrExportStatement(node.expression),
"ForInStatement, ForOfStatement"(node) {
if (node.left.type !== "VariableDeclarator") {
ForInStatement(node) {
if (node.left.type !== "VariableDeclaration") {
const firstLeftToken = sourceCode.getFirstToken(node.left, astUtils.isNotOpeningParenToken);
if (
firstLeftToken.value === "let" && (
/*
* If `let` is the only thing on the left side of the loop, it's the loop variable: `for ((let) of foo);`
* Removing it will cause a syntax error, because it will be parsed as the start of a VariableDeclarator.
*/
(firstLeftToken.range[1] === node.left.range[1] || /*
* If `let` is followed by a `[` token, it's a property access on the `let` value: `for ((let[foo]) of bar);`
* Removing it will cause the property access to be parsed as a destructuring declaration of `foo` instead.
*/
astUtils.isOpeningBracketToken(
sourceCode.getTokenAfter(firstLeftToken, astUtils.isNotClosingParenToken)
))
firstLeftToken.value === "let" &&
astUtils.isOpeningBracketToken(
sourceCode.getTokenAfter(firstLeftToken, astUtils.isNotClosingParenToken)
)
) {
// ForInStatement#left expression cannot start with `let[`.
tokensToIgnore.add(firstLeftToken);
}
}
if (node.type === "ForOfStatement") {
const hasExtraParens = node.right.type === "SequenceExpression"
? hasDoubleExcessParens(node.right)
: hasExcessParens(node.right);
if (hasExcessParens(node.left)) {
report(node.left);
}
if (hasExtraParens) {
report(node.right);
}
} else if (hasExcessParens(node.right)) {
if (hasExcessParens(node.right)) {
report(node.right);
}
},
ForOfStatement(node) {
if (node.left.type !== "VariableDeclaration") {
const firstLeftToken = sourceCode.getFirstToken(node.left, astUtils.isNotOpeningParenToken);
if (firstLeftToken.value === "let") {
// ForOfStatement#left expression cannot start with `let`.
tokensToIgnore.add(firstLeftToken);
}
}
if (hasExcessParens(node.left)) {
report(node.left);
}
if (hasExcessParensWithPrecedence(node.right, PRECEDENCE_OF_ASSIGNMENT_EXPR)) {
report(node.right);
}
},
ForStatement(node) {
@ -881,6 +899,22 @@ module.exports = {
}
if (node.init) {
if (node.init.type !== "VariableDeclaration") {
const firstToken = sourceCode.getFirstToken(node.init, astUtils.isNotOpeningParenToken);
if (
firstToken.value === "let" &&
astUtils.isOpeningBracketToken(
sourceCode.getTokenAfter(firstToken, astUtils.isNotClosingParenToken)
)
) {
// ForStatement#init expression cannot start with `let[`.
tokensToIgnore.add(firstToken);
}
}
startNewReportsBuffering();
if (hasExcessParens(node.init)) {
@ -1109,7 +1143,22 @@ module.exports = {
},
UnaryExpression: checkArgumentWithPrecedence,
UpdateExpression: checkArgumentWithPrecedence,
UpdateExpression(node) {
if (node.prefix) {
checkArgumentWithPrecedence(node);
} else {
const { argument } = node;
const operatorToken = sourceCode.getLastToken(node);
if (argument.loc.end.line === operatorToken.loc.start.line) {
checkArgumentWithPrecedence(node);
} else {
if (hasDoubleExcessParens(argument)) {
report(argument);
}
}
}
},
AwaitExpression: checkArgumentWithPrecedence,
VariableDeclarator(node) {

View file

@ -4,12 +4,6 @@
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
@ -17,15 +11,26 @@ const lodash = require("lodash");
const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu;
/**
* Checks whether or not a given node has a fallthrough comment.
* @param {ASTNode} node A SwitchCase node to get comments.
* Checks whether or not a given case has a fallthrough comment.
* @param {ASTNode} caseWhichFallsThrough SwitchCase node which falls through.
* @param {ASTNode} subsequentCase The case after caseWhichFallsThrough.
* @param {RuleContext} context A rule context which stores comments.
* @param {RegExp} fallthroughCommentPattern A pattern to match comment to.
* @returns {boolean} `true` if the node has a valid fallthrough comment.
* @returns {boolean} `true` if the case has a valid fallthrough comment.
*/
function hasFallthroughComment(node, context, fallthroughCommentPattern) {
function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
const sourceCode = context.getSourceCode();
const comment = lodash.last(sourceCode.getCommentsBefore(node));
if (caseWhichFallsThrough.consequent.length === 1 && caseWhichFallsThrough.consequent[0].type === "BlockStatement") {
const trailingCloseBrace = sourceCode.getLastToken(caseWhichFallsThrough.consequent[0]);
const commentInBlock = sourceCode.getCommentsBefore(trailingCloseBrace).pop();
if (commentInBlock && fallthroughCommentPattern.test(commentInBlock.value)) {
return true;
}
}
const comment = sourceCode.getCommentsBefore(subsequentCase).pop();
return Boolean(comment && fallthroughCommentPattern.test(comment.value));
}
@ -114,7 +119,7 @@ module.exports = {
* Checks whether or not there is a fallthrough comment.
* And reports the previous fallthrough node if that does not exist.
*/
if (fallthroughCase && !hasFallthroughComment(node, context, fallthroughCommentPattern)) {
if (fallthroughCase && !hasFallthroughComment(fallthroughCase, node, context, fallthroughCommentPattern)) {
context.report({
messageId: node.test ? "case" : "default",
node
@ -133,7 +138,7 @@ module.exports = {
*/
if (currentCodePath.currentSegments.some(isReachable) &&
(node.consequent.length > 0 || hasBlankLinesBetween(node, nextToken)) &&
lodash.last(node.parent.cases) !== node) {
node.parent.cases[node.parent.cases.length - 1] !== node) {
fallthroughCase = node;
}
}

View file

@ -24,6 +24,7 @@ function parseOptions(options) {
boolean: "boolean" in options ? options.boolean : true,
number: "number" in options ? options.number : true,
string: "string" in options ? options.string : true,
disallowTemplateShorthand: "disallowTemplateShorthand" in options ? options.disallowTemplateShorthand : false,
allow: options.allow || []
};
}
@ -108,6 +109,20 @@ function getNonNumericOperand(node) {
return null;
}
/**
* Checks whether an expression evaluates to a string.
* @param {ASTNode} node node that represents the expression to check.
* @returns {boolean} Whether or not the expression evaluates to a string.
*/
function isStringType(node) {
return astUtils.isStringLiteral(node) ||
(
node.type === "CallExpression" &&
node.callee.type === "Identifier" &&
node.callee.name === "String"
);
}
/**
* Checks whether a node is an empty string literal or not.
* @param {ASTNode} node The node to check.
@ -125,8 +140,8 @@ function isEmptyString(node) {
*/
function isConcatWithEmptyString(node) {
return node.operator === "+" && (
(isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) ||
(isEmptyString(node.right) && !astUtils.isStringLiteral(node.left))
(isEmptyString(node.left) && !isStringType(node.right)) ||
(isEmptyString(node.right) && !isStringType(node.left))
);
}
@ -180,6 +195,10 @@ module.exports = {
type: "boolean",
default: true
},
disallowTemplateShorthand: {
type: "boolean",
default: false
},
allow: {
type: "array",
items: {
@ -299,6 +318,43 @@ module.exports = {
report(node, recommendation, true);
}
},
TemplateLiteral(node) {
if (!options.disallowTemplateShorthand) {
return;
}
// tag`${foo}`
if (node.parent.type === "TaggedTemplateExpression") {
return;
}
// `` or `${foo}${bar}`
if (node.expressions.length !== 1) {
return;
}
// `prefix${foo}`
if (node.quasis[0].value.cooked !== "") {
return;
}
// `${foo}postfix`
if (node.quasis[1].value.cooked !== "") {
return;
}
// if the expression is already a string, then this isn't a coercion
if (isStringType(node.expressions[0])) {
return;
}
const code = sourceCode.getText(node.expressions[0]);
const recommendation = `String(${code})`;
report(node, recommendation, true);
}
};
}

View file

@ -97,10 +97,10 @@ function isIterationVariable(node) {
* - `Object.defineProperties`
* - `Object.freeze`
* - `Object.setPrototypeOf`
* - `Refrect.defineProperty`
* - `Refrect.deleteProperty`
* - `Refrect.set`
* - `Refrect.setPrototypeOf`
* - `Reflect.defineProperty`
* - `Reflect.deleteProperty`
* - `Reflect.set`
* - `Reflect.setPrototypeOf`
* @param {ASTNode} node The node to check.
* @param {Scope} scope A `escope.Scope` object to find variable (whichever).
* @returns {boolean} `true` if the node is at the first argument of a well-known mutation function.

View file

@ -21,7 +21,17 @@ module.exports = {
url: "https://eslint.org/docs/rules/no-inline-comments"
},
schema: [],
schema: [
{
type: "object",
properties: {
ignorePattern: {
type: "string"
}
},
additionalProperties: false
}
],
messages: {
unexpectedInlineComment: "Unexpected comment inline with code."
@ -30,6 +40,12 @@ module.exports = {
create(context) {
const sourceCode = context.getSourceCode();
const options = context.options[0];
let customIgnoreRegExp;
if (options && options.ignorePattern) {
customIgnoreRegExp = new RegExp(options.ignorePattern, "u");
}
/**
* Will check that comments are not on lines starting with or ending with code
@ -51,6 +67,11 @@ module.exports = {
return;
}
// Matches the ignore pattern
if (customIgnoreRegExp && customIgnoreRegExp.test(node.value)) {
return;
}
// JSX Exception
if (
(isPreambleEmpty || preamble === "{") &&
@ -80,9 +101,9 @@ module.exports = {
return {
Program() {
const comments = sourceCode.getAllComments();
comments.filter(token => token.type !== "Shebang").forEach(testCodeAroundComment);
sourceCode.getAllComments()
.filter(token => token.type !== "Shebang")
.forEach(testCodeAroundComment);
}
};
}

View file

@ -9,7 +9,7 @@
//------------------------------------------------------------------------------
const RegExpValidator = require("regexpp").RegExpValidator;
const validator = new RegExpValidator({ ecmaVersion: 2018 });
const validator = new RegExpValidator();
const validFlags = /[gimuys]/gu;
const undefined1 = void 0;
@ -69,6 +69,28 @@ module.exports = {
return node && node.type === "Literal" && typeof node.value === "string";
}
/**
* Gets flags of a regular expression created by the given `RegExp()` or `new RegExp()` call
* Examples:
* new RegExp(".") // => ""
* new RegExp(".", "gu") // => "gu"
* new RegExp(".", flags) // => null
* @param {ASTNode} node `CallExpression` or `NewExpression` node
* @returns {string|null} flags if they can be determined, `null` otherwise
* @private
*/
function getFlags(node) {
if (node.arguments.length < 2) {
return "";
}
if (isString(node.arguments[1])) {
return node.arguments[1].value;
}
return null;
}
/**
* Check syntax error in a given pattern.
* @param {string} pattern The RegExp pattern to validate.
@ -104,18 +126,23 @@ module.exports = {
return;
}
const pattern = node.arguments[0].value;
let flags = isString(node.arguments[1]) ? node.arguments[1].value : "";
let flags = getFlags(node);
if (allowedFlags) {
if (flags && allowedFlags) {
flags = flags.replace(allowedFlags, "");
}
// If flags are unknown, check both are errored or not.
const message = validateRegExpFlags(flags) || (
flags
? validateRegExpPattern(pattern, flags.indexOf("u") !== -1)
: validateRegExpPattern(pattern, true) && validateRegExpPattern(pattern, false)
);
const message =
(
flags && validateRegExpFlags(flags)
) ||
(
// If flags are unknown, report the regex only if its pattern is invalid both with and without the "u" flag
flags === null
? validateRegExpPattern(pattern, true) && validateRegExpPattern(pattern, false)
: validateRegExpPattern(pattern, flags.includes("u"))
);
if (message) {
context.report({

View file

@ -82,7 +82,7 @@ module.exports = {
const commentNodes = sourceCode.getAllComments();
/**
* Removes errors that occur inside a string node
* Removes errors that occur inside the given node
* @param {ASTNode} node to check for matching errors.
* @returns {void}
* @private
@ -91,14 +91,12 @@ module.exports = {
const locStart = node.loc.start;
const locEnd = node.loc.end;
errors = errors.filter(({ loc: { start: errorLoc } }) => {
if (errorLoc.line >= locStart.line && errorLoc.line <= locEnd.line) {
if (errorLoc.column >= locStart.column && (errorLoc.column <= locEnd.column || errorLoc.line < locEnd.line)) {
return false;
}
}
return true;
});
errors = errors.filter(({ loc: { start: errorLocStart } }) => (
errorLocStart.line < locStart.line ||
errorLocStart.line === locStart.line && errorLocStart.column < locStart.column ||
errorLocStart.line === locEnd.line && errorLocStart.column >= locEnd.column ||
errorLocStart.line > locEnd.line
));
}
/**

View file

@ -105,9 +105,9 @@ module.exports = {
}
/**
* Converts an integer to to an object containing the the integer's coefficient and order of magnitude
* Converts an integer to to an object containing the integer's coefficient and order of magnitude
* @param {string} stringInteger the string representation of the integer being converted
* @returns {Object} the object containing the the integer's coefficient and order of magnitude
* @returns {Object} the object containing the integer's coefficient and order of magnitude
*/
function normalizeInteger(stringInteger) {
const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger));
@ -120,9 +120,9 @@ module.exports = {
/**
*
* Converts a float to to an object containing the the floats's coefficient and order of magnitude
* Converts a float to to an object containing the floats's coefficient and order of magnitude
* @param {string} stringFloat the string representation of the float being converted
* @returns {Object} the object containing the the integer's coefficient and order of magnitude
* @returns {Object} the object containing the integer's coefficient and order of magnitude
*/
function normalizeFloat(stringFloat) {
const trimmedFloat = removeLeadingZeros(stringFloat);

View file

@ -117,7 +117,7 @@ module.exports = {
],
messages: {
unexpectedMixedOperator: "Unexpected mix of '{{leftOperator}}' and '{{rightOperator}}'."
unexpectedMixedOperator: "Unexpected mix of '{{leftOperator}}' and '{{rightOperator}}'. Use parentheses to clarify the intended order of operations."
}
},
@ -161,17 +161,6 @@ module.exports = {
);
}
/**
* Checks whether the operator of a given node is mixed with a
* conditional expression.
* @param {ASTNode} node A node to check. This is a conditional
* expression node
* @returns {boolean} `true` if the node was mixed.
*/
function isMixedWithConditionalParent(node) {
return !astUtils.isParenthesised(sourceCode, node) && !astUtils.isParenthesised(sourceCode, node.test);
}
/**
* Gets the operator token of a given node.
* @param {ASTNode} node A node to check. This is a BinaryExpression
@ -220,19 +209,13 @@ module.exports = {
* @returns {void}
*/
function check(node) {
if (TARGET_NODE_TYPE.test(node.parent.type)) {
if (node.parent.type === "ConditionalExpression" && !shouldIgnore(node) && isMixedWithConditionalParent(node.parent)) {
reportBothOperators(node);
} else {
if (TARGET_NODE_TYPE.test(node.parent.type) &&
isMixedWithParent(node) &&
!shouldIgnore(node)
) {
reportBothOperators(node);
}
}
if (
TARGET_NODE_TYPE.test(node.parent.type) &&
isMixedWithParent(node) &&
!shouldIgnore(node)
) {
reportBothOperators(node);
}
}
return {

View file

@ -21,7 +21,16 @@ module.exports = {
url: "https://eslint.org/docs/rules/no-multi-assign"
},
schema: [],
schema: [{
type: "object",
properties: {
ignoreNonDeclaration: {
type: "boolean",
default: false
}
},
additionalProperties: false
}],
messages: {
unexpectedChain: "Unexpected chained assignment."
@ -33,10 +42,14 @@ module.exports = {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
const options = context.options[0] || {
ignoreNonDeclaration: false
};
const targetParent = options.ignoreNonDeclaration ? ["VariableDeclarator"] : ["AssignmentExpression", "VariableDeclarator"];
return {
AssignmentExpression(node) {
if (["AssignmentExpression", "VariableDeclarator"].indexOf(node.parent.type) !== -1) {
if (targetParent.indexOf(node.parent.type) !== -1) {
context.report({
node,
messageId: "unexpectedChain"

View file

@ -0,0 +1,147 @@
/**
* @fileoverview Rule to disallow `\8` and `\9` escape sequences in string literals.
* @author Milos Djermanovic
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const QUICK_TEST_REGEX = /\\[89]/u;
/**
* Returns unicode escape sequence that represents the given character.
* @param {string} character A single code unit.
* @returns {string} "\uXXXX" sequence.
*/
function getUnicodeEscape(character) {
return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow `\\8` and `\\9` escape sequences in string literals",
category: "Best Practices",
recommended: false,
url: "https://eslint.org/docs/rules/no-nonoctal-decimal-escape",
suggestion: true
},
schema: [],
messages: {
decimalEscape: "Don't use '{{decimalEscape}}' escape sequence.",
// suggestions
refactor: "Replace '{{original}}' with '{{replacement}}'. This maintains the current functionality.",
escapeBackslash: "Replace '{{original}}' with '{{replacement}}' to include the actual backslash character."
}
},
create(context) {
const sourceCode = context.getSourceCode();
/**
* Creates a new Suggestion object.
* @param {string} messageId "refactor" or "escapeBackslash".
* @param {int[]} range The range to replace.
* @param {string} replacement New text for the range.
* @returns {Object} Suggestion
*/
function createSuggestion(messageId, range, replacement) {
return {
messageId,
data: {
original: sourceCode.getText().slice(...range),
replacement
},
fix(fixer) {
return fixer.replaceTextRange(range, replacement);
}
};
}
return {
Literal(node) {
if (typeof node.value !== "string") {
return;
}
if (!QUICK_TEST_REGEX.test(node.raw)) {
return;
}
const regex = /(?:[^\\]|(?<previousEscape>\\.))*?(?<decimalEscape>\\[89])/suy;
let match;
while ((match = regex.exec(node.raw))) {
const { previousEscape, decimalEscape } = match.groups;
const decimalEscapeRangeEnd = node.range[0] + match.index + match[0].length;
const decimalEscapeRangeStart = decimalEscapeRangeEnd - decimalEscape.length;
const decimalEscapeRange = [decimalEscapeRangeStart, decimalEscapeRangeEnd];
const suggest = [];
// When `regex` is matched, `previousEscape` can only capture characters adjacent to `decimalEscape`
if (previousEscape === "\\0") {
/*
* Now we have a NULL escape "\0" immediately followed by a decimal escape, e.g.: "\0\8".
* Fixing this to "\08" would turn "\0" into a legacy octal escape. To avoid producing
* an octal escape while fixing a decimal escape, we provide different suggestions.
*/
suggest.push(
createSuggestion( // "\0\8" -> "\u00008"
"refactor",
[decimalEscapeRangeStart - previousEscape.length, decimalEscapeRangeEnd],
`${getUnicodeEscape("\0")}${decimalEscape[1]}`
),
createSuggestion( // "\8" -> "\u0038"
"refactor",
decimalEscapeRange,
getUnicodeEscape(decimalEscape[1])
)
);
} else {
suggest.push(
createSuggestion( // "\8" -> "8"
"refactor",
decimalEscapeRange,
decimalEscape[1]
)
);
}
suggest.push(
createSuggestion( // "\8" -> "\\8"
"escapeBackslash",
decimalEscapeRange,
`\\${decimalEscape}`
)
);
context.report({
node,
loc: {
start: sourceCode.getLocFromIndex(decimalEscapeRangeStart),
end: sourceCode.getLocFromIndex(decimalEscapeRangeEnd)
},
messageId: "decimalEscape",
data: {
decimalEscape
},
suggest
});
}
}
};
}
};

View file

@ -46,15 +46,15 @@ module.exports = {
*/
function disallowBuiltIns(node) {
// TODO: just use `astUtils.getStaticPropertyName(node.callee)`
const callee = astUtils.skipChainExpression(node.callee);
if (callee.type !== "MemberExpression" || callee.computed) {
if (callee.type !== "MemberExpression") {
return;
}
const propName = callee.property.name;
if (DISALLOWED_PROPS.indexOf(propName) > -1) {
const propName = astUtils.getStaticPropertyName(callee);
if (propName !== null && DISALLOWED_PROPS.indexOf(propName) > -1) {
context.report({
messageId: "prototypeBuildIn",
loc: callee.property.loc,

View file

@ -45,7 +45,7 @@ module.exports = {
/**
* Checks and reports given exported identifier.
* @param {ASTNode} node exported `Identifer` node to check.
* @param {ASTNode} node exported `Identifier` node to check.
* @returns {void}
*/
function checkExportedName(node) {

View file

@ -10,12 +10,6 @@
const ignore = require("ignore");
const arrayOfStrings = {
type: "array",
items: { type: "string" },
uniqueItems: true
};
const arrayOfStringsOrObjects = {
type: "array",
items: {
@ -44,6 +38,41 @@ const arrayOfStringsOrObjects = {
uniqueItems: true
};
const arrayOfStringsOrObjectPatterns = {
anyOf: [
{
type: "array",
items: {
type: "string"
},
uniqueItems: true
},
{
type: "array",
items: {
type: "object",
properties: {
group: {
type: "array",
items: {
type: "string"
},
minItems: 1,
uniqueItems: true
},
message: {
type: "string",
minLength: 1
}
},
additionalProperties: false,
required: ["group"]
},
uniqueItems: true
}
]
};
module.exports = {
meta: {
type: "suggestion",
@ -61,6 +90,8 @@ module.exports = {
pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}",
patterns: "'{{importSource}}' import is restricted from being used by a pattern.",
// eslint-disable-next-line eslint-plugin/report-message-format
patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}",
everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
// eslint-disable-next-line eslint-plugin/report-message-format
@ -80,7 +111,7 @@ module.exports = {
type: "object",
properties: {
paths: arrayOfStringsOrObjects,
patterns: arrayOfStrings
patterns: arrayOfStringsOrObjectPatterns
},
additionalProperties: false
}],
@ -98,13 +129,6 @@ module.exports = {
(Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns"));
const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
// if no imports are restricted we don"t need to check
if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) {
return {};
}
const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
if (typeof importSource === "string") {
memo[importSource] = { message: null };
@ -117,7 +141,16 @@ module.exports = {
return memo;
}, {});
const restrictedPatternsMatcher = ignore().add(restrictedPatterns);
// Handle patterns too, either as strings or groups
const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
const restrictedPatternGroups = restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string"
? [{ matcher: ignore().add(restrictedPatterns) }]
: restrictedPatterns.map(({ group, message }) => ({ matcher: ignore().add(group), customMessage: message }));
// if no imports are restricted we don"t need to check
if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) {
return {};
}
/**
* Report a restricted path.
@ -184,17 +217,19 @@ module.exports = {
/**
* Report a restricted path specifically for patterns.
* @param {node} node representing the restricted path reference
* @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails
* @returns {void}
* @private
*/
function reportPathForPatterns(node) {
function reportPathForPatterns(node, group) {
const importSource = node.source.value.trim();
context.report({
node,
messageId: "patterns",
messageId: group.customMessage ? "patternWithCustomMessage" : "patterns",
data: {
importSource
importSource,
customMessage: group.customMessage
}
});
}
@ -202,11 +237,12 @@ module.exports = {
/**
* Check if the given importSource is restricted by a pattern.
* @param {string} importSource path of the import
* @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails
* @returns {boolean} whether the variable is a restricted pattern or not
* @private
*/
function isRestrictedPattern(importSource) {
return restrictedPatterns.length > 0 && restrictedPatternsMatcher.ignores(importSource);
function isRestrictedPattern(importSource, group) {
return group.matcher.ignores(importSource);
}
/**
@ -249,10 +285,11 @@ module.exports = {
}
checkRestrictedPathAndReport(importSource, importNames, node);
if (isRestrictedPattern(importSource)) {
reportPathForPatterns(node);
}
restrictedPatternGroups.forEach(group => {
if (isRestrictedPattern(importSource, group)) {
reportPathForPatterns(node, group);
}
});
}
return {

View file

@ -7,6 +7,8 @@
"use strict";
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -31,18 +33,30 @@ module.exports = {
create(context) {
return {
/**
* Check whether a node's static value starts with "javascript:" or not.
* And report an error for unexpected script URL.
* @param {ASTNode} node node to check
* @returns {void}
*/
function check(node) {
const value = astUtils.getStaticStringValue(node);
if (typeof value === "string" && value.toLowerCase().indexOf("javascript:") === 0) {
context.report({ node, messageId: "unexpectedScriptURL" });
}
}
return {
Literal(node) {
if (node.value && typeof node.value === "string") {
const value = node.value.toLowerCase();
if (value.indexOf("javascript:") === 0) {
context.report({ node, messageId: "unexpectedScriptURL" });
}
check(node);
}
},
TemplateLiteral(node) {
if (!(node.parent && node.parent.type === "TaggedTemplateExpression")) {
check(node);
}
}
};
}
};

View file

@ -11,6 +11,14 @@
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const DEFAULT_OPTIONS = {
allowInParentheses: true
};
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -26,7 +34,15 @@ module.exports = {
url: "https://eslint.org/docs/rules/no-sequences"
},
schema: [],
schema: [{
properties: {
allowInParentheses: {
type: "boolean",
default: true
}
},
additionalProperties: false
}],
messages: {
unexpectedCommaExpression: "Unexpected use of comma operator."
@ -34,6 +50,7 @@ module.exports = {
},
create(context) {
const options = Object.assign({}, DEFAULT_OPTIONS, context.options[0]);
const sourceCode = context.getSourceCode();
/**
@ -99,13 +116,15 @@ module.exports = {
}
// Wrapping a sequence in extra parens indicates intent
if (requiresExtraParens(node)) {
if (isParenthesisedTwice(node)) {
return;
}
} else {
if (isParenthesised(node)) {
return;
if (options.allowInParentheses) {
if (requiresExtraParens(node)) {
if (isParenthesisedTwice(node)) {
return;
}
} else {
if (isParenthesised(node)) {
return;
}
}
}

View file

@ -44,7 +44,8 @@ module.exports = {
],
messages: {
noShadow: "'{{name}}' is already declared in the upper scope."
noShadow: "'{{name}}' is already declared in the upper scope on line {{shadowedLine}} column {{shadowedColumn}}.",
noShadowGlobal: "'{{name}}' is already a global variable."
}
},
@ -117,6 +118,29 @@ module.exports = {
return def && def.name.range;
}
/**
* Get declared line and column of a variable.
* @param {eslint-scope.Variable} variable The variable to get.
* @returns {Object} The declared line and column of the variable.
*/
function getDeclaredLocation(variable) {
const identifier = variable.identifiers[0];
let obj;
if (identifier) {
obj = {
global: false,
line: identifier.loc.start.line,
column: identifier.loc.start.column + 1
};
} else {
obj = {
global: true
};
}
return obj;
}
/**
* Checks if a variable is in TDZ of scopeVar.
* @param {Object} variable The variable to check.
@ -165,10 +189,18 @@ module.exports = {
!isOnInitializer(variable, shadowed) &&
!(options.hoist !== "all" && isInTdz(variable, shadowed))
) {
const location = getDeclaredLocation(shadowed);
const messageId = location.global ? "noShadowGlobal" : "noShadow";
const data = { name: variable.name };
if (!location.global) {
data.shadowedLine = location.line;
data.shadowedColumn = location.column;
}
context.report({
node: variable.identifiers[0],
messageId: "noShadow",
data: variable
messageId,
data
});
}
}

View file

@ -171,7 +171,7 @@ module.exports = {
/**
* Removes the top of stack item.
*
* And this treverses all segments of this code path then reports every
* And this traverses all segments of this code path then reports every
* invalid node.
* @param {CodePath} codePath A code path which was ended.
* @returns {void}

View file

@ -0,0 +1,205 @@
/**
* @fileoverview Rule to disallow unsafe optional chaining
* @author Yeon JuAn
*/
"use strict";
const UNSAFE_ARITHMETIC_OPERATORS = new Set(["+", "-", "/", "*", "%", "**"]);
const UNSAFE_ASSIGNMENT_OPERATORS = new Set(["+=", "-=", "/=", "*=", "%=", "**="]);
const UNSAFE_RELATIONAL_OPERATORS = new Set(["in", "instanceof"]);
/**
* Checks whether a node is a destructuring pattern or not
* @param {ASTNode} node node to check
* @returns {boolean} `true` if a node is a destructuring pattern, otherwise `false`
*/
function isDestructuringPattern(node) {
return node.type === "ObjectPattern" || node.type === "ArrayPattern";
}
module.exports = {
meta: {
type: "problem",
docs: {
description: "disallow use of optional chaining in contexts where the `undefined` value is not allowed",
category: "Possible Errors",
recommended: false,
url: "https://eslint.org/docs/rules/no-unsafe-optional-chaining"
},
schema: [{
type: "object",
properties: {
disallowArithmeticOperators: {
type: "boolean",
default: false
}
},
additionalProperties: false
}],
fixable: null,
messages: {
unsafeOptionalChain: "Unsafe usage of optional chaining. If it short-circuits with 'undefined' the evaluation will throw TypeError.",
unsafeArithmetic: "Unsafe arithmetic operation on optional chaining. It can result in NaN."
}
},
create(context) {
const options = context.options[0] || {};
const disallowArithmeticOperators = (options.disallowArithmeticOperators) || false;
/**
* Reports unsafe usage of optional chaining
* @param {ASTNode} node node to report
* @returns {void}
*/
function reportUnsafeUsage(node) {
context.report({
messageId: "unsafeOptionalChain",
node
});
}
/**
* Reports unsafe arithmetic operation on optional chaining
* @param {ASTNode} node node to report
* @returns {void}
*/
function reportUnsafeArithmetic(node) {
context.report({
messageId: "unsafeArithmetic",
node
});
}
/**
* Checks and reports if a node can short-circuit with `undefined` by optional chaining.
* @param {ASTNode} [node] node to check
* @param {Function} reportFunc report function
* @returns {void}
*/
function checkUndefinedShortCircuit(node, reportFunc) {
if (!node) {
return;
}
switch (node.type) {
case "LogicalExpression":
if (node.operator === "||" || node.operator === "??") {
checkUndefinedShortCircuit(node.right, reportFunc);
} else if (node.operator === "&&") {
checkUndefinedShortCircuit(node.left, reportFunc);
checkUndefinedShortCircuit(node.right, reportFunc);
}
break;
case "SequenceExpression":
checkUndefinedShortCircuit(
node.expressions[node.expressions.length - 1],
reportFunc
);
break;
case "ConditionalExpression":
checkUndefinedShortCircuit(node.consequent, reportFunc);
checkUndefinedShortCircuit(node.alternate, reportFunc);
break;
case "AwaitExpression":
checkUndefinedShortCircuit(node.argument, reportFunc);
break;
case "ChainExpression":
reportFunc(node);
break;
default:
break;
}
}
/**
* Checks unsafe usage of optional chaining
* @param {ASTNode} node node to check
* @returns {void}
*/
function checkUnsafeUsage(node) {
checkUndefinedShortCircuit(node, reportUnsafeUsage);
}
/**
* Checks unsafe arithmetic operations on optional chaining
* @param {ASTNode} node node to check
* @returns {void}
*/
function checkUnsafeArithmetic(node) {
checkUndefinedShortCircuit(node, reportUnsafeArithmetic);
}
return {
"AssignmentExpression, AssignmentPattern"(node) {
if (isDestructuringPattern(node.left)) {
checkUnsafeUsage(node.right);
}
},
"ClassDeclaration, ClassExpression"(node) {
checkUnsafeUsage(node.superClass);
},
CallExpression(node) {
if (!node.optional) {
checkUnsafeUsage(node.callee);
}
},
NewExpression(node) {
checkUnsafeUsage(node.callee);
},
VariableDeclarator(node) {
if (isDestructuringPattern(node.id)) {
checkUnsafeUsage(node.init);
}
},
MemberExpression(node) {
if (!node.optional) {
checkUnsafeUsage(node.object);
}
},
TaggedTemplateExpression(node) {
checkUnsafeUsage(node.tag);
},
ForOfStatement(node) {
checkUnsafeUsage(node.right);
},
SpreadElement(node) {
if (node.parent && node.parent.type !== "ObjectExpression") {
checkUnsafeUsage(node.argument);
}
},
BinaryExpression(node) {
if (UNSAFE_RELATIONAL_OPERATORS.has(node.operator)) {
checkUnsafeUsage(node.right);
}
if (
disallowArithmeticOperators &&
UNSAFE_ARITHMETIC_OPERATORS.has(node.operator)
) {
checkUnsafeArithmetic(node.right);
checkUnsafeArithmetic(node.left);
}
},
WithStatement(node) {
checkUnsafeUsage(node.object);
},
UnaryExpression(node) {
if (
disallowArithmeticOperators &&
UNSAFE_ARITHMETIC_OPERATORS.has(node.operator)
) {
checkUnsafeArithmetic(node.argument);
}
},
AssignmentExpression(node) {
if (
disallowArithmeticOperators &&
UNSAFE_ASSIGNMENT_OPERATORS.has(node.operator)
) {
checkUnsafeArithmetic(node.right);
}
}
};
}
};

View file

@ -50,6 +50,10 @@ module.exports = {
allowTaggedTemplates: {
type: "boolean",
default: false
},
enforceForJSX: {
type: "boolean",
default: false
}
},
additionalProperties: false
@ -65,7 +69,8 @@ module.exports = {
const config = context.options[0] || {},
allowShortCircuit = config.allowShortCircuit || false,
allowTernary = config.allowTernary || false,
allowTaggedTemplates = config.allowTaggedTemplates || false;
allowTaggedTemplates = config.allowTaggedTemplates || false,
enforceForJSX = config.enforceForJSX || false;
// eslint-disable-next-line jsdoc/require-description
/**
@ -140,6 +145,12 @@ module.exports = {
},
FunctionExpression: alwaysTrue,
Identifier: alwaysTrue,
JSXElement() {
return enforceForJSX;
},
JSXFragment() {
return enforceForJSX;
},
Literal: alwaysTrue,
LogicalExpression(node) {
if (allowShortCircuit) {

View file

@ -196,6 +196,17 @@ module.exports = {
}
/**
* Checks whether a node is a sibling of the rest property or not.
* @param {ASTNode} node a node to check
* @returns {boolean} True if the node is a sibling of the rest property, otherwise false.
*/
function hasRestSibling(node) {
return node.type === "Property" &&
node.parent.type === "ObjectPattern" &&
REST_PROPERTY_TYPE.test(node.parent.properties[node.parent.properties.length - 1].type);
}
/**
* Determines if a variable has a sibling rest property
* @param {Variable} variable eslint-scope variable object.
@ -204,16 +215,10 @@ module.exports = {
*/
function hasRestSpreadSibling(variable) {
if (config.ignoreRestSiblings) {
return variable.defs.some(def => {
const propertyNode = def.name.parent;
const patternNode = propertyNode.parent;
const hasRestSiblingDefinition = variable.defs.some(def => hasRestSibling(def.name.parent));
const hasRestSiblingReference = variable.references.some(ref => hasRestSibling(ref.identifier.parent));
return (
propertyNode.type === "Property" &&
patternNode.type === "ObjectPattern" &&
REST_PROPERTY_TYPE.test(patternNode.properties[patternNode.properties.length - 1].type)
);
});
return hasRestSiblingDefinition || hasRestSiblingReference;
}
return false;
@ -405,6 +410,31 @@ module.exports = {
);
}
/**
* Checks whether a given node is unused expression or not.
* @param {ASTNode} node The node itself
* @returns {boolean} The node is an unused expression.
* @private
*/
function isUnusedExpression(node) {
const parent = node.parent;
if (parent.type === "ExpressionStatement") {
return true;
}
if (parent.type === "SequenceExpression") {
const isLastExpression = parent.expressions[parent.expressions.length - 1] === node;
if (!isLastExpression) {
return true;
}
return isUnusedExpression(parent);
}
return false;
}
/**
* Checks whether a given reference is a read to update itself or not.
* @param {eslint-scope.Reference} ref A reference to check.
@ -415,23 +445,28 @@ module.exports = {
function isReadForItself(ref, rhsNode) {
const id = ref.identifier;
const parent = id.parent;
const grandparent = parent.parent;
return ref.isRead() && (
// self update. e.g. `a += 1`, `a++`
(// in RHS of an assignment for itself. e.g. `a = a + 1`
((
parent.type === "AssignmentExpression" &&
grandparent.type === "ExpressionStatement" &&
parent.left === id
) ||
(
parent.type === "UpdateExpression" &&
grandparent.type === "ExpressionStatement"
) || rhsNode &&
isInside(id, rhsNode) &&
!isInsideOfStorableFunction(id, rhsNode)))
(
parent.type === "AssignmentExpression" &&
parent.left === id &&
isUnusedExpression(parent)
) ||
(
parent.type === "UpdateExpression" &&
isUnusedExpression(parent)
)
) ||
// in RHS of an assignment for itself. e.g. `a = a + 1`
(
rhsNode &&
isInside(id, rhsNode) &&
!isInsideOfStorableFunction(id, rhsNode)
)
);
}
@ -619,10 +654,18 @@ module.exports = {
// Report the first declaration.
if (unusedVar.defs.length > 0) {
// report last write reference, https://github.com/eslint/eslint/issues/14324
const writeReferences = unusedVar.references.filter(ref => ref.isWrite() && ref.from.variableScope === unusedVar.scope.variableScope);
let referenceToReport;
if (writeReferences.length > 0) {
referenceToReport = writeReferences[writeReferences.length - 1];
}
context.report({
node: unusedVar.references.length ? unusedVar.references[
unusedVar.references.length - 1
].identifier : unusedVar.identifiers[0],
node: referenceToReport ? referenceToReport.identifier : unusedVar.identifiers[0],
messageId: "unusedVar",
data: unusedVar.references.some(ref => ref.isWrite())
? getAssignedMessageData(unusedVar)

View file

@ -11,7 +11,6 @@
const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("eslint-utils");
const { RegExpParser, visitRegExpAST } = require("regexpp");
const lodash = require("lodash");
//------------------------------------------------------------------------------
// Helpers
@ -137,7 +136,7 @@ module.exports = {
// the opposite of the previous when the regex is matching backward in a lookbehind context.
messageId = "backward";
} else if (lodash.last(groupCut).type === "Alternative") {
} else if (groupCut[groupCut.length - 1].type === "Alternative") {
// group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
messageId = "disjunctive";

View file

@ -8,7 +8,6 @@
// Requirements
//------------------------------------------------------------------------------
const lodash = require("lodash");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
@ -95,9 +94,16 @@ module.exports = {
}
}
/**
* A no-op function to act as placeholder for checking a node when the `enforceForClassMembers` option is `false`.
* @returns {void}
* @private
*/
function noop() {}
return {
Property: check,
MethodDefinition: enforceForClassMembers ? check : lodash.noop
MethodDefinition: enforceForClassMembers ? check : noop
};
}
};

View file

@ -162,6 +162,14 @@ module.exports = {
return;
}
/*
* Prevent crashing on parsers which do not require class constructor
* to have a body, e.g. typescript and flow
*/
if (!node.value.body) {
return;
}
const body = node.value.body.body;
const ctorParams = node.value.params;
const superClass = node.parent.parent.superClass;

View file

@ -109,9 +109,9 @@ module.exports = {
* @returns {void}
*/
function report(node, startOffset, character) {
const start = sourceCode.getLocFromIndex(sourceCode.getIndexFromLoc(node.loc.start) + startOffset);
const rangeStart = sourceCode.getIndexFromLoc(node.loc.start) + startOffset;
const rangeStart = node.range[0] + startOffset;
const range = [rangeStart, rangeStart + 1];
const start = sourceCode.getLocFromIndex(rangeStart);
context.report({
node,
@ -172,7 +172,7 @@ module.exports = {
}
if (isUnnecessaryEscape && !isQuoteEscape) {
report(node, match.index + 1, match[0].slice(1));
report(node, match.index, match[0].slice(1));
}
}
@ -206,7 +206,7 @@ module.exports = {
return;
}
const value = isTemplateElement ? node.value.raw : node.raw.slice(1, -1);
const value = isTemplateElement ? sourceCode.getText(node) : node.raw;
const pattern = /\\[^\d]/gu;
let match;

View file

@ -5,6 +5,12 @@
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -54,11 +60,10 @@ module.exports = {
* Reports error for unnecessarily renamed assignments
* @param {ASTNode} node node to report
* @param {ASTNode} initial node with initial name value
* @param {ASTNode} result node with new name value
* @param {string} type the type of the offending node
* @returns {void}
*/
function reportError(node, initial, result, type) {
function reportError(node, initial, type) {
const name = initial.type === "Identifier" ? initial.name : initial.value;
return context.report({
@ -69,18 +74,21 @@ module.exports = {
type
},
fix(fixer) {
if (sourceCode.commentsExistBetween(initial, result)) {
const replacementNode = node.type === "Property" ? node.value : node.local;
if (sourceCode.getCommentsInside(node).length > sourceCode.getCommentsInside(replacementNode).length) {
return null;
}
const replacementText = result.type === "AssignmentPattern"
? sourceCode.getText(result)
: name;
// Don't autofix code such as `({foo: (foo) = a} = obj);`, parens are not allowed in shorthand properties.
if (
replacementNode.type === "AssignmentPattern" &&
astUtils.isParenthesised(sourceCode, replacementNode.left)
) {
return null;
}
return fixer.replaceTextRange([
initial.range[0],
result.range[1]
], replacementText);
return fixer.replaceText(node, sourceCode.getText(replacementNode));
}
});
}
@ -97,19 +105,11 @@ module.exports = {
for (const property of node.properties) {
/*
* TODO: Remove after babel-eslint removes ExperimentalRestProperty
* https://github.com/eslint/eslint/issues/12335
*/
if (property.type === "ExperimentalRestProperty") {
continue;
}
/**
* Properties using shorthand syntax and rest elements can not be renamed.
* If the property is computed, we have no idea if a rename is useless or not.
*/
if (property.shorthand || property.type === "RestElement" || property.computed) {
if (property.type !== "Property" || property.shorthand || property.computed) {
continue;
}
@ -117,7 +117,7 @@ module.exports = {
const renamedKey = property.value.type === "AssignmentPattern" ? property.value.left.name : property.value.name;
if (key === renamedKey) {
reportError(property, property.key, property.value, "Destructuring assignment");
reportError(property, property.key, "Destructuring assignment");
}
}
}
@ -134,7 +134,7 @@ module.exports = {
if (node.imported.name === node.local.name &&
node.imported.range[0] !== node.local.range[0]) {
reportError(node, node.imported, node.local, "Import");
reportError(node, node.imported, "Import");
}
}
@ -150,7 +150,7 @@ module.exports = {
if (node.local.name === node.exported.name &&
node.local.range[0] !== node.exported.range[0]) {
reportError(node, node.local, node.exported, "Export");
reportError(node, node.local, "Export");
}
}

View file

@ -5,7 +5,7 @@
"use strict";
const { escapeRegExp } = require("lodash");
const escapeRegExp = require("escape-string-regexp");
const astUtils = require("./utils/ast-utils");
const CHAR_LIMIT = 40;

View file

@ -10,7 +10,6 @@
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
const lodash = require("lodash");
//------------------------------------------------------------------------------
// Helpers
@ -69,6 +68,24 @@ function normalizeOptionValue(value) {
return { multiline, minProperties, consistent };
}
/**
* Checks if a value is an object.
* @param {any} value The value to check
* @returns {boolean} `true` if the value is an object, otherwise `false`
*/
function isObject(value) {
return typeof value === "object" && value !== null;
}
/**
* Checks if an option is a node-specific option
* @param {any} option The option to check
* @returns {boolean} `true` if the option is node-specific, otherwise `false`
*/
function isNodeSpecificOption(option) {
return isObject(option) || typeof option === "string";
}
/**
* Normalizes a given option value.
* @param {string|Object|undefined} options An option value to parse.
@ -80,9 +97,7 @@ function normalizeOptionValue(value) {
* }} Normalized option object.
*/
function normalizeOptions(options) {
const isNodeSpecificOption = lodash.overSome([lodash.isPlainObject, lodash.isString]);
if (lodash.isPlainObject(options) && lodash.some(options, isNodeSpecificOption)) {
if (isObject(options) && Object.values(options).some(isNodeSpecificOption)) {
return {
ObjectExpression: normalizeOptionValue(options.ObjectExpression),
ObjectPattern: normalizeOptionValue(options.ObjectPattern),
@ -134,7 +149,7 @@ module.exports = {
type: "layout",
docs: {
description: "enforce consistent line breaks inside braces",
description: "enforce consistent line breaks after opening and before closing braces",
category: "Stylistic Issues",
recommended: false,
url: "https://eslint.org/docs/rules/object-curly-newline"

View file

@ -5,6 +5,25 @@
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines whether the given node is in a statement list.
* @param {ASTNode} node node to check
* @returns {boolean} `true` if the given node is in a statement list
*/
function isInStatementList(node) {
return astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -268,8 +287,8 @@ module.exports = {
/**
* Fixer to join VariableDeclaration's into a single declaration
* @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join
* @returns {Function} The fixer function
* @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join
* @returns {Function} The fixer function
*/
function joinDeclarations(declarations) {
const declaration = declarations[0];
@ -297,10 +316,17 @@ module.exports = {
/**
* Fixer to split a VariableDeclaration into individual declarations
* @param {VariableDeclaration} declaration The `VariableDeclaration` to split
* @returns {Function} The fixer function
* @param {VariableDeclaration} declaration The `VariableDeclaration` to split
* @returns {Function|null} The fixer function
*/
function splitDeclarations(declaration) {
const { parent } = declaration;
// don't autofix code such as: if (foo) var x, y;
if (!isInStatementList(parent.type === "ExportNamedDeclaration" ? parent : declaration)) {
return null;
}
return fixer => declaration.declarations.map(declarator => {
const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator);
@ -314,12 +340,14 @@ module.exports = {
return null;
}
const exportPlacement = declaration.parent.type === "ExportNamedDeclaration" ? "export " : "";
/*
* `var x,y`
* tokenAfterDeclarator ^^ afterComma
*/
if (afterComma.range[0] === tokenAfterDeclarator.range[1]) {
return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind} `);
return fixer.replaceText(tokenAfterDeclarator, `; ${exportPlacement}${declaration.kind} `);
}
/*
@ -341,11 +369,11 @@ module.exports = {
return fixer.replaceTextRange(
[tokenAfterDeclarator.range[0], lastComment.range[0]],
`;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${declaration.kind} `
`;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${exportPlacement}${declaration.kind} `
);
}
return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind}`);
return fixer.replaceText(tokenAfterDeclarator, `; ${exportPlacement}${declaration.kind}`);
}).filter(x => x);
}

View file

@ -76,8 +76,8 @@ module.exports = {
fixable: "code",
messages: {
replaced: "Assignment can be replaced with operator assignment.",
unexpected: "Unexpected operator assignment shorthand."
replaced: "Assignment (=) can be replaced with operator assignment ({{operator}}=).",
unexpected: "Unexpected operator assignment ({{operator}}=) shorthand."
}
},
@ -113,6 +113,7 @@ module.exports = {
context.report({
node,
messageId: "replaced",
data: { operator },
fix(fixer) {
if (canBeFixed(left) && canBeFixed(expr.left)) {
const equalsToken = getOperatorToken(node);
@ -139,7 +140,8 @@ module.exports = {
*/
context.report({
node,
messageId: "replaced"
messageId: "replaced",
data: { operator }
});
}
}
@ -155,6 +157,7 @@ module.exports = {
context.report({
node,
messageId: "unexpected",
data: { operator: node.operator },
fix(fixer) {
if (canBeFixed(node.left)) {
const firstToken = sourceCode.getFirstToken(node);

View file

@ -295,7 +295,7 @@ module.exports = {
* If the callback function has duplicates in its list of parameters (possible in sloppy mode),
* don't replace it with an arrow function, because this is a SyntaxError with arrow functions.
*/
return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
return;
}
// Remove `.bind(this)` if exists.
@ -307,7 +307,7 @@ module.exports = {
* E.g. `(foo || function(){}).bind(this)`
*/
if (memberNode.type !== "MemberExpression") {
return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
return;
}
const callNode = memberNode.parent;
@ -320,12 +320,12 @@ module.exports = {
* ^^^^^^^^^^^^
*/
if (astUtils.isParenthesised(sourceCode, memberNode)) {
return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
return;
}
// If comments exist in the `.bind(this)`, don't remove those.
if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) {
return; // eslint-disable-line eslint-plugin/fixer-return -- false positive
return;
}
yield fixer.removeRange([firstTokenToRemove.range[0], lastTokenToRemove.range[1]]);

View file

@ -5,6 +5,11 @@
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const FixTracker = require("./utils/fix-tracker");
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
@ -451,10 +456,18 @@ module.exports = {
messageId: "useConst",
data: node,
fix: shouldFix
? fixer => fixer.replaceText(
sourceCode.getFirstToken(varDeclParent, t => t.value === varDeclParent.kind),
"const"
)
? fixer => {
const letKeywordToken = sourceCode.getFirstToken(varDeclParent, t => t.value === varDeclParent.kind);
/**
* Extend the replacement range to the whole declaration,
* in order to prevent other fixes in the same pass
* https://github.com/eslint/eslint/issues/13899
*/
return new FixTracker(fixer, sourceCode)
.retainRange(varDeclParent.range)
.replaceTextRange(letKeywordToken.range, "const");
}
: null
});
});

View file

@ -4,6 +4,18 @@
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const PRECEDENCE_OF_ASSIGNMENT_EXPR = astUtils.getPrecedence({ type: "AssignmentExpression" });
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -156,13 +168,15 @@ module.exports = {
* Assignment expression is not fixed.
* Array destructuring is not fixed.
* Renamed property is not fixed.
* @param {ASTNode} node the the node to evaluate
* @param {ASTNode} node the node to evaluate
* @returns {boolean} whether or not the node should be fixed
*/
function shouldFix(node) {
return node.type === "VariableDeclarator" &&
node.id.type === "Identifier" &&
node.init.type === "MemberExpression" &&
!node.init.computed &&
node.init.property.type === "Identifier" &&
node.id.name === node.init.property.name;
}
@ -183,9 +197,15 @@ module.exports = {
return null;
}
let objectText = sourceCode.getText(rightNode.object);
if (astUtils.getPrecedence(rightNode.object) < PRECEDENCE_OF_ASSIGNMENT_EXPR) {
objectText = `(${objectText})`;
}
return fixer.replaceText(
node,
`{${rightNode.property.name}} = ${sourceCode.getText(rightNode.object)}`
`{${rightNode.property.name}} = ${objectText}`
);
}
@ -259,7 +279,7 @@ module.exports = {
* @param {ASTNode} node the AssignmentExpression node
* @returns {void}
*/
function checkAssigmentExpression(node) {
function checkAssignmentExpression(node) {
if (node.operator === "=") {
performCheck(node.left, node.right, node);
}
@ -271,7 +291,7 @@ module.exports = {
return {
VariableDeclarator: checkVariableDeclarator,
AssignmentExpression: checkAssigmentExpression
AssignmentExpression: checkAssignmentExpression
};
}
};

View file

@ -30,6 +30,7 @@ function doesBaseNeedParens(base) {
astUtils.getPrecedence(base) <= PRECEDENCE_OF_EXPONENTIATION_EXPR ||
// An unary operator cannot be used immediately before an exponentiation expression
base.type === "AwaitExpression" ||
base.type === "UnaryExpression"
);
}

View file

@ -105,10 +105,10 @@ module.exports = {
CallExpression(node) {
const methodName = (node.callee.property || {}).name;
const isReflectCall = (node.callee.object || {}).name === "Reflect";
const hasReflectSubsitute = Object.prototype.hasOwnProperty.call(reflectSubstitutes, methodName);
const hasReflectSubstitute = Object.prototype.hasOwnProperty.call(reflectSubstitutes, methodName);
const userConfiguredException = exceptions.indexOf(methodName) !== -1;
if (hasReflectSubsitute && !isReflectCall && !userConfiguredException) {
if (hasReflectSubstitute && !isReflectCall && !userConfiguredException) {
report(node, existingNames[methodName], reflectSubstitutes[methodName]);
}
},

View file

@ -39,33 +39,25 @@ function getTopConcatBinaryExpression(node) {
}
/**
* Determines whether a given node is a octal escape sequence
* Checks whether or not a node contains a string literal with an octal or non-octal decimal escape sequence
* @param {ASTNode} node A node to check
* @returns {boolean} `true` if the node is an octal escape sequence
* @returns {boolean} `true` if at least one string literal within the node contains
* an octal or non-octal decimal escape sequence
*/
function isOctalEscapeSequence(node) {
// No need to check TemplateLiterals would throw error with octal escape
const isStringLiteral = node.type === "Literal" && typeof node.value === "string";
if (!isStringLiteral) {
return false;
}
return astUtils.hasOctalEscapeSequence(node.raw);
}
/**
* Checks whether or not a node contains a octal escape sequence
* @param {ASTNode} node A node to check
* @returns {boolean} `true` if the node contains an octal escape sequence
*/
function hasOctalEscapeSequence(node) {
function hasOctalOrNonOctalDecimalEscapeSequence(node) {
if (isConcatenation(node)) {
return hasOctalEscapeSequence(node.left) || hasOctalEscapeSequence(node.right);
return (
hasOctalOrNonOctalDecimalEscapeSequence(node.left) ||
hasOctalOrNonOctalDecimalEscapeSequence(node.right)
);
}
return isOctalEscapeSequence(node);
// No need to check TemplateLiterals would throw parsing error
if (node.type === "Literal" && typeof node.value === "string") {
return astUtils.hasOctalOrNonOctalDecimalEscapeSequence(node.raw);
}
return false;
}
/**
@ -237,7 +229,7 @@ module.exports = {
function fixNonStringBinaryExpression(fixer, node) {
const topBinaryExpr = getTopConcatBinaryExpression(node.parent);
if (hasOctalEscapeSequence(topBinaryExpr)) {
if (hasOctalOrNonOctalDecimalEscapeSequence(topBinaryExpr)) {
return null;
}

View file

@ -282,9 +282,12 @@ module.exports = {
description: settings.description
},
fix(fixer) {
if (quoteOption === "backtick" && astUtils.hasOctalEscapeSequence(rawVal)) {
if (quoteOption === "backtick" && astUtils.hasOctalOrNonOctalDecimalEscapeSequence(rawVal)) {
// An octal escape sequence in a template literal would produce syntax error, even in non-strict mode.
/*
* An octal or non-octal decimal escape sequence in a template literal would
* produce syntax error, even in non-strict mode.
*/
return null;
}

View file

@ -82,7 +82,8 @@ module.exports = {
description: "enforce the consistent use of the radix argument when using `parseInt()`",
category: "Best Practices",
recommended: false,
url: "https://eslint.org/docs/rules/radix"
url: "https://eslint.org/docs/rules/radix",
suggestion: true
},
schema: [
@ -95,7 +96,8 @@ module.exports = {
missingParameters: "Missing parameters.",
redundantRadix: "Redundant radix parameter.",
missingRadix: "Missing radix parameter.",
invalidRadix: "Invalid radix parameter, must be an integer between 2 and 36."
invalidRadix: "Invalid radix parameter, must be an integer between 2 and 36.",
addRadixParameter10: "Add radix parameter `10` for parsing decimal numbers."
}
},
@ -123,7 +125,21 @@ module.exports = {
if (mode === MODE_ALWAYS) {
context.report({
node,
messageId: "missingRadix"
messageId: "missingRadix",
suggest: [
{
messageId: "addRadixParameter10",
fix(fixer) {
const sourceCode = context.getSourceCode();
const tokens = sourceCode.getTokens(node);
const lastToken = tokens[tokens.length - 1]; // Parenthesis.
const secondToLastToken = tokens[tokens.length - 2]; // May or may not be a comma.
const hasTrailingComma = secondToLastToken.type === "Punctuator" && secondToLastToken.value === ",";
return fixer.insertTextBefore(lastToken, hasTrailingComma ? " 10," : ", 10");
}
}
]
});
}
break;

View file

@ -13,6 +13,10 @@
*/
function createReferenceMap(scope, outReferenceMap = new Map()) {
for (const reference of scope.references) {
if (reference.resolved === null) {
continue;
}
outReferenceMap.set(reference.identifier, reference);
}
for (const childScope of scope.childScopes) {
@ -86,39 +90,42 @@ class SegmentInfo {
* @returns {void}
*/
initialize(segment) {
const outdatedReadVariableNames = new Set();
const freshReadVariableNames = new Set();
const outdatedReadVariables = new Set();
const freshReadVariables = new Set();
for (const prevSegment of segment.prevSegments) {
const info = this.info.get(prevSegment);
if (info) {
info.outdatedReadVariableNames.forEach(Set.prototype.add, outdatedReadVariableNames);
info.freshReadVariableNames.forEach(Set.prototype.add, freshReadVariableNames);
info.outdatedReadVariables.forEach(Set.prototype.add, outdatedReadVariables);
info.freshReadVariables.forEach(Set.prototype.add, freshReadVariables);
}
}
this.info.set(segment, { outdatedReadVariableNames, freshReadVariableNames });
this.info.set(segment, { outdatedReadVariables, freshReadVariables });
}
/**
* Mark a given variable as read on given segments.
* @param {PathSegment[]} segments The segments that it read the variable on.
* @param {string} variableName The variable name to be read.
* @param {Variable} variable The variable to be read.
* @returns {void}
*/
markAsRead(segments, variableName) {
markAsRead(segments, variable) {
for (const segment of segments) {
const info = this.info.get(segment);
if (info) {
info.freshReadVariableNames.add(variableName);
info.freshReadVariables.add(variable);
// If a variable is freshly read again, then it's no more out-dated.
info.outdatedReadVariables.delete(variable);
}
}
}
/**
* Move `freshReadVariableNames` to `outdatedReadVariableNames`.
* Move `freshReadVariables` to `outdatedReadVariables`.
* @param {PathSegment[]} segments The segments to process.
* @returns {void}
*/
@ -127,8 +134,8 @@ class SegmentInfo {
const info = this.info.get(segment);
if (info) {
info.freshReadVariableNames.forEach(Set.prototype.add, info.outdatedReadVariableNames);
info.freshReadVariableNames.clear();
info.freshReadVariables.forEach(Set.prototype.add, info.outdatedReadVariables);
info.freshReadVariables.clear();
}
}
}
@ -136,14 +143,14 @@ class SegmentInfo {
/**
* Check if a given variable is outdated on the current segments.
* @param {PathSegment[]} segments The current segments.
* @param {string} variableName The variable name to check.
* @param {Variable} variable The variable to check.
* @returns {boolean} `true` if the variable is outdated on the segments.
*/
isOutdated(segments, variableName) {
isOutdated(segments, variable) {
for (const segment of segments) {
const info = this.info.get(segment);
if (info && info.outdatedReadVariableNames.has(variableName)) {
if (info && info.outdatedReadVariables.has(variable)) {
return true;
}
}
@ -211,14 +218,13 @@ module.exports = {
if (!reference) {
return;
}
const name = reference.identifier.name;
const variable = reference.resolved;
const writeExpr = getWriteExpr(reference);
const isMemberAccess = reference.identifier.parent.type === "MemberExpression";
// Add a fresh read variable.
if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) {
segmentInfo.markAsRead(codePath.currentSegments, name);
segmentInfo.markAsRead(codePath.currentSegments, variable);
}
/*
@ -242,7 +248,7 @@ module.exports = {
/*
* Verify assignments.
* If the reference exists in `outdatedReadVariableNames` list, report it.
* If the reference exists in `outdatedReadVariables` list, report it.
*/
":expression:exit"(node) {
const { codePath, referenceMap } = stack;
@ -264,9 +270,9 @@ module.exports = {
assignmentReferences.delete(node);
for (const reference of references) {
const name = reference.identifier.name;
const variable = reference.resolved;
if (segmentInfo.isOutdated(codePath.currentSegments, name)) {
if (segmentInfo.isOutdated(codePath.currentSegments, variable)) {
context.report({
node: node.parent,
messageId: "nonAtomicUpdate",

View file

@ -5,8 +5,31 @@
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether the given node represents the body of a function.
* @param {ASTNode} node the node to check.
* @returns {boolean} `true` if the node is function body.
*/
function isFunctionBody(node) {
const parent = node.parent;
return (
node.type === "BlockStatement" &&
astUtils.isFunction(parent) &&
parent.body === node
);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
@ -82,13 +105,16 @@ module.exports = {
}
/**
* Checks whether or not a given token is an arrow operator (=>) or a keyword
* in order to avoid to conflict with `arrow-spacing` and `keyword-spacing`.
* @param {Token} token A token to check.
* @returns {boolean} `true` if the token is an arrow operator.
* Checks whether the spacing before the given block is already controlled by another rule:
* - `arrow-spacing` checks spaces after `=>`.
* - `keyword-spacing` checks spaces after keywords in certain contexts.
* @param {Token} precedingToken first token before the block.
* @param {ASTNode|Token} node `BlockStatement` node or `{` token of a `SwitchStatement` node.
* @returns {boolean} `true` if requiring or disallowing spaces before the given block could produce conflicts with other rules.
*/
function isConflicted(token) {
return (token.type === "Punctuator" && token.value === "=>") || token.type === "Keyword";
function isConflicted(precedingToken, node) {
return astUtils.isArrowToken(precedingToken) ||
astUtils.isKeywordToken(precedingToken) && !isFunctionBody(node);
}
/**
@ -99,13 +125,12 @@ module.exports = {
function checkPrecedingSpace(node) {
const precedingToken = sourceCode.getTokenBefore(node);
if (precedingToken && !isConflicted(precedingToken) && astUtils.isTokenOnSameLine(precedingToken, node)) {
if (precedingToken && !isConflicted(precedingToken, node) && astUtils.isTokenOnSameLine(precedingToken, node)) {
const hasSpace = sourceCode.isSpaceBetweenTokens(precedingToken, node);
const parent = context.getAncestors().pop();
let requireSpace;
let requireNoSpace;
if (parent.type === "FunctionExpression" || parent.type === "FunctionDeclaration") {
if (isFunctionBody(node)) {
requireSpace = alwaysFunctions;
requireNoSpace = neverFunctions;
} else if (node.type === "ClassBody") {

View file

@ -132,7 +132,9 @@ module.exports = {
if (nonSpacedConsequentNode) {
report(node, nonSpacedConsequentNode);
} else if (nonSpacedAlternateNode) {
}
if (nonSpacedAlternateNode) {
report(node, nonSpacedAlternateNode);
}
}

View file

@ -1,5 +1,5 @@
/**
* @fileoverview This rule shoud require or disallow spaces before or after unary operations.
* @fileoverview This rule should require or disallow spaces before or after unary operations.
* @author Marcin Kumorek
*/
"use strict";

Some files were not shown because too many files have changed in this diff Show more