Fix EsLint Brackets (error markup 'const').
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @fileoverview Expose out ESLint and CLI to require.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Linter = require("./linter");
|
||||
|
||||
module.exports = {
|
||||
Linter,
|
||||
CLIEngine: require("./cli-engine"),
|
||||
RuleTester: require("./testers/rule-tester"),
|
||||
SourceCode: require("./util/source-code")
|
||||
};
|
||||
|
||||
let deprecatedLinterInstance = null;
|
||||
|
||||
Object.defineProperty(module.exports, "linter", {
|
||||
enumerable: false,
|
||||
get() {
|
||||
if (!deprecatedLinterInstance) {
|
||||
deprecatedLinterInstance = new Linter();
|
||||
}
|
||||
|
||||
return deprecatedLinterInstance;
|
||||
}
|
||||
});
|
||||
+792
@@ -0,0 +1,792 @@
|
||||
/**
|
||||
* @fileoverview Main CLI object.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
* The CLI object should *not* call process.exit() directly. It should only return
|
||||
* exit codes. This allows other programs to use the CLI object and still control
|
||||
* when the program exits.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const fs = require("fs"),
|
||||
path = require("path"),
|
||||
defaultOptions = require("../conf/default-cli-options"),
|
||||
Linter = require("./linter"),
|
||||
lodash = require("lodash"),
|
||||
IgnoredPaths = require("./ignored-paths"),
|
||||
Config = require("./config"),
|
||||
ConfigOps = require("./config/config-ops"),
|
||||
LintResultCache = require("./util/lint-result-cache"),
|
||||
globUtils = require("./util/glob-utils"),
|
||||
validator = require("./config/config-validator"),
|
||||
hash = require("./util/hash"),
|
||||
ModuleResolver = require("./util/module-resolver"),
|
||||
naming = require("./util/naming"),
|
||||
pkg = require("../package.json");
|
||||
|
||||
const debug = require("debug")("eslint:cli-engine");
|
||||
const resolver = new ModuleResolver();
|
||||
const validFixTypes = new Set(["problem", "suggestion", "layout"]);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The options to configure a CLI engine with.
|
||||
* @typedef {Object} CLIEngineOptions
|
||||
* @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
|
||||
* @property {Object} baseConfig Base config object, extended by all configs used with this CLIEngine instance
|
||||
* @property {boolean} cache Enable result caching.
|
||||
* @property {string} cacheLocation The cache file to use instead of .eslintcache.
|
||||
* @property {string} configFile The configuration file to use.
|
||||
* @property {string} cwd The value to use for the current working directory.
|
||||
* @property {string[]} envs An array of environments to load.
|
||||
* @property {string[]} extensions An array of file extensions to check.
|
||||
* @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean.
|
||||
* @property {string[]} fixTypes Array of rule types to apply fixes for.
|
||||
* @property {string[]} globals An array of global variables to declare.
|
||||
* @property {boolean} ignore False disables use of .eslintignore.
|
||||
* @property {string} ignorePath The ignore file to use instead of .eslintignore.
|
||||
* @property {string} ignorePattern A glob pattern of files to ignore.
|
||||
* @property {boolean} useEslintrc False disables looking for .eslintrc
|
||||
* @property {string} parser The name of the parser to use.
|
||||
* @property {Object} parserOptions An object of parserOption settings to use.
|
||||
* @property {string[]} plugins An array of plugins to load.
|
||||
* @property {Object<string,*>} rules An object of rules to use.
|
||||
* @property {string[]} rulePaths An array of directories to load custom rules from.
|
||||
* @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives
|
||||
*/
|
||||
|
||||
/**
|
||||
* A linting warning or error.
|
||||
* @typedef {Object} LintMessage
|
||||
* @property {string} message The message to display to the user.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A linting result.
|
||||
* @typedef {Object} LintResult
|
||||
* @property {string} filePath The path to the file that was linted.
|
||||
* @property {LintMessage[]} messages All of the messages for the result.
|
||||
* @property {number} errorCount Number of errors for the result.
|
||||
* @property {number} warningCount Number of warnings for the result.
|
||||
* @property {number} fixableErrorCount Number of fixable errors for the result.
|
||||
* @property {number} fixableWarningCount Number of fixable warnings for the result.
|
||||
* @property {string=} [source] The source code of the file that was linted.
|
||||
* @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines if each fix type in an array is supported by ESLint and throws
|
||||
* an error if not.
|
||||
* @param {string[]} fixTypes An array of fix types to check.
|
||||
* @returns {void}
|
||||
* @throws {Error} If an invalid fix type is found.
|
||||
*/
|
||||
function validateFixTypes(fixTypes) {
|
||||
for (const fixType of fixTypes) {
|
||||
if (!validFixTypes.has(fixType)) {
|
||||
throw new Error(`Invalid fix type "${fixType}" found.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It will calculate the error and warning count for collection of messages per file
|
||||
* @param {Object[]} messages - Collection of messages
|
||||
* @returns {Object} Contains the stats
|
||||
* @private
|
||||
*/
|
||||
function calculateStatsPerFile(messages) {
|
||||
return messages.reduce((stat, message) => {
|
||||
if (message.fatal || message.severity === 2) {
|
||||
stat.errorCount++;
|
||||
if (message.fix) {
|
||||
stat.fixableErrorCount++;
|
||||
}
|
||||
} else {
|
||||
stat.warningCount++;
|
||||
if (message.fix) {
|
||||
stat.fixableWarningCount++;
|
||||
}
|
||||
}
|
||||
return stat;
|
||||
}, {
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
fixableErrorCount: 0,
|
||||
fixableWarningCount: 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* It will calculate the error and warning count for collection of results from all files
|
||||
* @param {Object[]} results - Collection of messages from all the files
|
||||
* @returns {Object} Contains the stats
|
||||
* @private
|
||||
*/
|
||||
function calculateStatsPerRun(results) {
|
||||
return results.reduce((stat, result) => {
|
||||
stat.errorCount += result.errorCount;
|
||||
stat.warningCount += result.warningCount;
|
||||
stat.fixableErrorCount += result.fixableErrorCount;
|
||||
stat.fixableWarningCount += result.fixableWarningCount;
|
||||
return stat;
|
||||
}, {
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
fixableErrorCount: 0,
|
||||
fixableWarningCount: 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an source code using ESLint.
|
||||
* @param {string} text The source code to check.
|
||||
* @param {Object} configHelper The configuration options for ESLint.
|
||||
* @param {string} filename An optional string representing the texts filename.
|
||||
* @param {boolean|Function} fix Indicates if fixes should be processed.
|
||||
* @param {boolean} allowInlineConfig Allow/ignore comments that change config.
|
||||
* @param {boolean} reportUnusedDisableDirectives Allow/ignore comments that change config.
|
||||
* @param {Linter} linter Linter context
|
||||
* @returns {{rules: LintResult, config: Object}} The results for linting on this text and the fully-resolved config for it.
|
||||
* @private
|
||||
*/
|
||||
function processText(text, configHelper, filename, fix, allowInlineConfig, reportUnusedDisableDirectives, linter) {
|
||||
let filePath,
|
||||
fileExtension,
|
||||
processor;
|
||||
|
||||
if (filename) {
|
||||
filePath = path.resolve(filename);
|
||||
fileExtension = path.extname(filename);
|
||||
}
|
||||
|
||||
const effectiveFilename = filename || "<text>";
|
||||
|
||||
debug(`Linting ${effectiveFilename}`);
|
||||
const config = configHelper.getConfig(filePath);
|
||||
|
||||
if (config.plugins) {
|
||||
configHelper.plugins.loadAll(config.plugins);
|
||||
}
|
||||
|
||||
const loadedPlugins = configHelper.plugins.getAll();
|
||||
|
||||
for (const plugin in loadedPlugins) {
|
||||
if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) {
|
||||
processor = loadedPlugins[plugin].processors[fileExtension];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix);
|
||||
const fixedResult = linter.verifyAndFix(text, config, {
|
||||
filename: effectiveFilename,
|
||||
allowInlineConfig,
|
||||
reportUnusedDisableDirectives,
|
||||
fix: !!autofixingEnabled && fix,
|
||||
preprocess: processor && (rawText => processor.preprocess(rawText, effectiveFilename)),
|
||||
postprocess: processor && (problemLists => processor.postprocess(problemLists, effectiveFilename))
|
||||
});
|
||||
const stats = calculateStatsPerFile(fixedResult.messages);
|
||||
|
||||
const result = {
|
||||
filePath: effectiveFilename,
|
||||
messages: fixedResult.messages,
|
||||
errorCount: stats.errorCount,
|
||||
warningCount: stats.warningCount,
|
||||
fixableErrorCount: stats.fixableErrorCount,
|
||||
fixableWarningCount: stats.fixableWarningCount
|
||||
};
|
||||
|
||||
if (fixedResult.fixed) {
|
||||
result.output = fixedResult.output;
|
||||
}
|
||||
|
||||
if (result.errorCount + result.warningCount > 0 && typeof result.output === "undefined") {
|
||||
result.source = text;
|
||||
}
|
||||
|
||||
return { result, config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an individual file using ESLint. Files used here are known to
|
||||
* exist, so no need to check that here.
|
||||
* @param {string} filename The filename of the file being checked.
|
||||
* @param {Object} configHelper The configuration options for ESLint.
|
||||
* @param {Object} options The CLIEngine options object.
|
||||
* @param {Linter} linter Linter context
|
||||
* @returns {{rules: LintResult, config: Object}} The results for linting on this text and the fully-resolved config for it.
|
||||
* @private
|
||||
*/
|
||||
function processFile(filename, configHelper, options, linter) {
|
||||
|
||||
const text = fs.readFileSync(path.resolve(filename), "utf8");
|
||||
|
||||
return processText(
|
||||
text,
|
||||
configHelper,
|
||||
filename,
|
||||
options.fix,
|
||||
options.allowInlineConfig,
|
||||
options.reportUnusedDisableDirectives,
|
||||
linter
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns result with warning by ignore settings
|
||||
* @param {string} filePath - File path of checked code
|
||||
* @param {string} baseDir - Absolute path of base directory
|
||||
* @returns {LintResult} Result with single warning
|
||||
* @private
|
||||
*/
|
||||
function createIgnoreResult(filePath, baseDir) {
|
||||
let message;
|
||||
const isHidden = /^\./.test(path.basename(filePath));
|
||||
const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules");
|
||||
const isInBowerComponents = baseDir && path.relative(baseDir, filePath).startsWith("bower_components");
|
||||
|
||||
if (isHidden) {
|
||||
message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
|
||||
} else if (isInNodeModules) {
|
||||
message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
|
||||
} else if (isInBowerComponents) {
|
||||
message = "File ignored by default. Use \"--ignore-pattern '!bower_components/*'\" to override.";
|
||||
} else {
|
||||
message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
|
||||
}
|
||||
|
||||
return {
|
||||
filePath: path.resolve(filePath),
|
||||
messages: [
|
||||
{
|
||||
fatal: false,
|
||||
severity: 1,
|
||||
message
|
||||
}
|
||||
],
|
||||
errorCount: 0,
|
||||
warningCount: 1,
|
||||
fixableErrorCount: 0,
|
||||
fixableWarningCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces rule warnings (i.e. deprecation) from configured rules
|
||||
* @param {(Array<string>|Set<string>)} usedRules - Rules configured
|
||||
* @param {Map} loadedRules - Map of loaded rules
|
||||
* @returns {Array<Object>} Contains rule warnings
|
||||
* @private
|
||||
*/
|
||||
function createRuleDeprecationWarnings(usedRules, loadedRules) {
|
||||
const usedDeprecatedRules = [];
|
||||
|
||||
usedRules.forEach(name => {
|
||||
const loadedRule = loadedRules.get(name);
|
||||
|
||||
if (loadedRule && loadedRule.meta && loadedRule.meta.deprecated) {
|
||||
const deprecatedRule = { ruleId: name };
|
||||
const replacedBy = lodash.get(loadedRule, "meta.replacedBy", []);
|
||||
|
||||
if (replacedBy.every(newRule => lodash.isString(newRule))) {
|
||||
deprecatedRule.replacedBy = replacedBy;
|
||||
}
|
||||
|
||||
usedDeprecatedRules.push(deprecatedRule);
|
||||
}
|
||||
});
|
||||
|
||||
return usedDeprecatedRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given message is an error message.
|
||||
* @param {Object} message The message to check.
|
||||
* @returns {boolean} Whether or not the message is an error message.
|
||||
* @private
|
||||
*/
|
||||
function isErrorMessage(message) {
|
||||
return message.severity === 2;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* return the cacheFile to be used by eslint, based on whether the provided parameter is
|
||||
* a directory or looks like a directory (ends in `path.sep`), in which case the file
|
||||
* name will be the `cacheFile/.cache_hashOfCWD`
|
||||
*
|
||||
* if cacheFile points to a file or looks like a file then in will just use that file
|
||||
*
|
||||
* @param {string} cacheFile The name of file to be used to store the cache
|
||||
* @param {string} cwd Current working directory
|
||||
* @returns {string} the resolved path to the cache file
|
||||
*/
|
||||
function getCacheFile(cacheFile, cwd) {
|
||||
|
||||
/*
|
||||
* make sure the path separators are normalized for the environment/os
|
||||
* keeping the trailing path separator if present
|
||||
*/
|
||||
const normalizedCacheFile = path.normalize(cacheFile);
|
||||
|
||||
const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile);
|
||||
const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep;
|
||||
|
||||
/**
|
||||
* return the name for the cache file in case the provided parameter is a directory
|
||||
* @returns {string} the resolved path to the cacheFile
|
||||
*/
|
||||
function getCacheFileForDirectory() {
|
||||
return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
|
||||
}
|
||||
|
||||
let fileStats;
|
||||
|
||||
try {
|
||||
fileStats = fs.lstatSync(resolvedCacheFile);
|
||||
} catch (ex) {
|
||||
fileStats = null;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* in case the file exists we need to verify if the provided path
|
||||
* is a directory or a file. If it is a directory we want to create a file
|
||||
* inside that directory
|
||||
*/
|
||||
if (fileStats) {
|
||||
|
||||
/*
|
||||
* is a directory or is a file, but the original file the user provided
|
||||
* looks like a directory but `path.resolve` removed the `last path.sep`
|
||||
* so we need to still treat this like a directory
|
||||
*/
|
||||
if (fileStats.isDirectory() || looksLikeADirectory) {
|
||||
return getCacheFileForDirectory();
|
||||
}
|
||||
|
||||
// is file so just use that file
|
||||
return resolvedCacheFile;
|
||||
}
|
||||
|
||||
/*
|
||||
* here we known the file or directory doesn't exist,
|
||||
* so we will try to infer if its a directory if it looks like a directory
|
||||
* for the current operating system.
|
||||
*/
|
||||
|
||||
// if the last character passed is a path separator we assume is a directory
|
||||
if (looksLikeADirectory) {
|
||||
return getCacheFileForDirectory();
|
||||
}
|
||||
|
||||
return resolvedCacheFile;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class CLIEngine {
|
||||
|
||||
/**
|
||||
* Creates a new instance of the core CLI engine.
|
||||
* @param {CLIEngineOptions} providedOptions The options for this instance.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(providedOptions) {
|
||||
|
||||
const options = Object.assign(
|
||||
Object.create(null),
|
||||
defaultOptions,
|
||||
{ cwd: process.cwd() },
|
||||
providedOptions
|
||||
);
|
||||
|
||||
/*
|
||||
* if an --ignore-path option is provided, ensure that the ignore
|
||||
* file exists and is not a directory
|
||||
*/
|
||||
if (options.ignore && options.ignorePath) {
|
||||
try {
|
||||
if (!fs.statSync(options.ignorePath).isFile()) {
|
||||
throw new Error(`${options.ignorePath} is not a file`);
|
||||
}
|
||||
} catch (e) {
|
||||
e.message = `Error: Could not load file ${options.ignorePath}\nError: ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored options for this instance
|
||||
* @type {Object}
|
||||
*/
|
||||
this.options = options;
|
||||
this.linter = new Linter();
|
||||
|
||||
// load in additional rules
|
||||
if (this.options.rulePaths) {
|
||||
const cwd = this.options.cwd;
|
||||
|
||||
this.options.rulePaths.forEach(rulesdir => {
|
||||
debug(`Loading rules from ${rulesdir}`);
|
||||
this.linter.rules.load(rulesdir, cwd);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.rules && Object.keys(this.options.rules).length) {
|
||||
const loadedRules = this.linter.getRules();
|
||||
|
||||
Object.keys(this.options.rules).forEach(name => {
|
||||
validator.validateRuleOptions(loadedRules.get(name), name, this.options.rules[name], "CLI");
|
||||
});
|
||||
}
|
||||
|
||||
this.config = new Config(this.options, this.linter);
|
||||
|
||||
if (this.options.cache) {
|
||||
const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
|
||||
|
||||
/**
|
||||
* Cache used to avoid operating on files that haven't changed since the
|
||||
* last successful execution.
|
||||
* @type {Object}
|
||||
*/
|
||||
this._lintResultCache = new LintResultCache(cacheFile, this.config);
|
||||
}
|
||||
|
||||
// setup special filter for fixes
|
||||
if (this.options.fix && this.options.fixTypes && this.options.fixTypes.length > 0) {
|
||||
|
||||
debug(`Using fix types ${this.options.fixTypes}`);
|
||||
|
||||
// throw an error if any invalid fix types are found
|
||||
validateFixTypes(this.options.fixTypes);
|
||||
|
||||
// convert to Set for faster lookup
|
||||
const fixTypes = new Set(this.options.fixTypes);
|
||||
|
||||
// save original value of options.fix in case it's a function
|
||||
const originalFix = (typeof this.options.fix === "function")
|
||||
? this.options.fix : () => this.options.fix;
|
||||
|
||||
// create a cache of rules (but don't populate until needed)
|
||||
this._rulesCache = null;
|
||||
|
||||
this.options.fix = lintResult => {
|
||||
const rule = this._rulesCache.get(lintResult.ruleId);
|
||||
const matches = rule.meta && fixTypes.has(rule.meta.type);
|
||||
|
||||
return matches && originalFix(lintResult);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getRules() {
|
||||
return this.linter.getRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns results that only contains errors.
|
||||
* @param {LintResult[]} results The results to filter.
|
||||
* @returns {LintResult[]} The filtered results.
|
||||
*/
|
||||
static getErrorResults(results) {
|
||||
const filtered = [];
|
||||
|
||||
results.forEach(result => {
|
||||
const filteredMessages = result.messages.filter(isErrorMessage);
|
||||
|
||||
if (filteredMessages.length > 0) {
|
||||
filtered.push(
|
||||
Object.assign(result, {
|
||||
messages: filteredMessages,
|
||||
errorCount: filteredMessages.length,
|
||||
warningCount: 0,
|
||||
fixableErrorCount: result.fixableErrorCount,
|
||||
fixableWarningCount: 0
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs fixes from the given results to files.
|
||||
* @param {Object} report The report object created by CLIEngine.
|
||||
* @returns {void}
|
||||
*/
|
||||
static outputFixes(report) {
|
||||
report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => {
|
||||
fs.writeFileSync(result.filePath, result.output);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a plugin by passing its configuration
|
||||
* @param {string} name Name of the plugin.
|
||||
* @param {Object} pluginobject Plugin configuration object.
|
||||
* @returns {void}
|
||||
*/
|
||||
addPlugin(name, pluginobject) {
|
||||
this.config.plugins.define(name, pluginobject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the patterns passed into executeOnFiles() into glob-based patterns
|
||||
* for easier handling.
|
||||
* @param {string[]} patterns The file patterns passed on the command line.
|
||||
* @returns {string[]} The equivalent glob patterns.
|
||||
*/
|
||||
resolveFileGlobPatterns(patterns) {
|
||||
return globUtils.resolveFileGlobPatterns(patterns.filter(Boolean), this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the current configuration on an array of file and directory names.
|
||||
* @param {string[]} patterns An array of file and directory names.
|
||||
* @returns {Object} The results for all files that were linted.
|
||||
*/
|
||||
executeOnFiles(patterns) {
|
||||
const options = this.options,
|
||||
lintResultCache = this._lintResultCache,
|
||||
configHelper = this.config;
|
||||
const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
|
||||
|
||||
if (!options.cache && fs.existsSync(cacheFile)) {
|
||||
fs.unlinkSync(cacheFile);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const fileList = globUtils.listFilesToProcess(patterns, options);
|
||||
const allUsedRules = new Set();
|
||||
const results = fileList.map(fileInfo => {
|
||||
if (fileInfo.ignored) {
|
||||
return createIgnoreResult(fileInfo.filename, options.cwd);
|
||||
}
|
||||
|
||||
if (options.cache) {
|
||||
const cachedLintResults = lintResultCache.getCachedLintResults(fileInfo.filename);
|
||||
|
||||
if (cachedLintResults) {
|
||||
const resultHadMessages = cachedLintResults.messages && cachedLintResults.messages.length;
|
||||
|
||||
if (resultHadMessages && options.fix) {
|
||||
debug(`Reprocessing cached file to allow autofix: ${fileInfo.filename}`);
|
||||
} else {
|
||||
debug(`Skipping file since it hasn't changed: ${fileInfo.filename}`);
|
||||
|
||||
return cachedLintResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there's a cache, populate it
|
||||
if ("_rulesCache" in this) {
|
||||
this._rulesCache = this.getRules();
|
||||
}
|
||||
|
||||
debug(`Processing ${fileInfo.filename}`);
|
||||
|
||||
const { result, config } = processFile(fileInfo.filename, configHelper, options, this.linter);
|
||||
|
||||
Object.keys(config.rules)
|
||||
.filter(ruleId => ConfigOps.getRuleSeverity(config.rules[ruleId]))
|
||||
.forEach(ruleId => allUsedRules.add(ruleId));
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
if (options.cache) {
|
||||
results.forEach(result => {
|
||||
|
||||
/*
|
||||
* Store the lint result in the LintResultCache.
|
||||
* NOTE: The LintResultCache will remove the file source and any
|
||||
* other properties that are difficult to serialize, and will
|
||||
* hydrate those properties back in on future lint runs.
|
||||
*/
|
||||
lintResultCache.setCachedLintResults(result.filePath, result);
|
||||
});
|
||||
|
||||
// persist the cache to disk
|
||||
lintResultCache.reconcile();
|
||||
}
|
||||
|
||||
const stats = calculateStatsPerRun(results);
|
||||
|
||||
const usedDeprecatedRules = createRuleDeprecationWarnings(allUsedRules, this.getRules());
|
||||
|
||||
debug(`Linting complete in: ${Date.now() - startTime}ms`);
|
||||
|
||||
return {
|
||||
results,
|
||||
errorCount: stats.errorCount,
|
||||
warningCount: stats.warningCount,
|
||||
fixableErrorCount: stats.fixableErrorCount,
|
||||
fixableWarningCount: stats.fixableWarningCount,
|
||||
usedDeprecatedRules
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the current configuration on text.
|
||||
* @param {string} text A string of JavaScript code to lint.
|
||||
* @param {string} filename An optional string representing the texts filename.
|
||||
* @param {boolean} warnIgnored Always warn when a file is ignored
|
||||
* @returns {Object} The results for the linting.
|
||||
*/
|
||||
executeOnText(text, filename, warnIgnored) {
|
||||
|
||||
const results = [],
|
||||
options = this.options,
|
||||
configHelper = this.config,
|
||||
ignoredPaths = new IgnoredPaths(options);
|
||||
|
||||
// resolve filename based on options.cwd (for reporting, ignoredPaths also resolves)
|
||||
|
||||
const resolvedFilename = filename && !path.isAbsolute(filename)
|
||||
? path.resolve(options.cwd, filename)
|
||||
: filename;
|
||||
let usedDeprecatedRules;
|
||||
|
||||
if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) {
|
||||
if (warnIgnored) {
|
||||
results.push(createIgnoreResult(resolvedFilename, options.cwd));
|
||||
}
|
||||
usedDeprecatedRules = [];
|
||||
} else {
|
||||
|
||||
// if there's a cache, populate it
|
||||
if ("_rulesCache" in this) {
|
||||
this._rulesCache = this.getRules();
|
||||
}
|
||||
|
||||
const { result, config } = processText(
|
||||
text,
|
||||
configHelper,
|
||||
resolvedFilename,
|
||||
options.fix,
|
||||
options.allowInlineConfig,
|
||||
options.reportUnusedDisableDirectives,
|
||||
this.linter
|
||||
);
|
||||
|
||||
results.push(result);
|
||||
usedDeprecatedRules = createRuleDeprecationWarnings(
|
||||
Object.keys(config.rules).filter(rule => ConfigOps.getRuleSeverity(config.rules[rule])),
|
||||
this.getRules()
|
||||
);
|
||||
}
|
||||
|
||||
const stats = calculateStatsPerRun(results);
|
||||
|
||||
return {
|
||||
results,
|
||||
errorCount: stats.errorCount,
|
||||
warningCount: stats.warningCount,
|
||||
fixableErrorCount: stats.fixableErrorCount,
|
||||
fixableWarningCount: stats.fixableWarningCount,
|
||||
usedDeprecatedRules
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a configuration object for the given file based on the CLI options.
|
||||
* This is the same logic used by the ESLint CLI executable to determine
|
||||
* configuration for each file it processes.
|
||||
* @param {string} filePath The path of the file to retrieve a config object for.
|
||||
* @returns {Object} A configuration object for the file.
|
||||
*/
|
||||
getConfigForFile(filePath) {
|
||||
const configHelper = this.config;
|
||||
|
||||
return configHelper.getConfig(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given path is ignored by ESLint.
|
||||
* @param {string} filePath The path of the file to check.
|
||||
* @returns {boolean} Whether or not the given path is ignored.
|
||||
*/
|
||||
isPathIgnored(filePath) {
|
||||
const resolvedPath = path.resolve(this.options.cwd, filePath);
|
||||
const ignoredPaths = new IgnoredPaths(this.options);
|
||||
|
||||
return ignoredPaths.contains(resolvedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the formatter representing the given format or null if no formatter
|
||||
* with the given name can be found.
|
||||
* @param {string} [format] The name of the format to load or the path to a
|
||||
* custom formatter.
|
||||
* @returns {Function} The formatter function or null if not found.
|
||||
*/
|
||||
getFormatter(format) {
|
||||
|
||||
// default is stylish
|
||||
const resolvedFormatName = format || "stylish";
|
||||
|
||||
// only strings are valid formatters
|
||||
if (typeof resolvedFormatName === "string") {
|
||||
|
||||
// replace \ with / for Windows compatibility
|
||||
const normalizedFormatName = resolvedFormatName.replace(/\\/g, "/");
|
||||
|
||||
const cwd = this.options ? this.options.cwd : process.cwd();
|
||||
const namespace = naming.getNamespaceFromTerm(normalizedFormatName);
|
||||
|
||||
let formatterPath;
|
||||
|
||||
// if there's a slash, then it's a file
|
||||
if (!namespace && normalizedFormatName.indexOf("/") > -1) {
|
||||
formatterPath = path.resolve(cwd, normalizedFormatName);
|
||||
} else {
|
||||
try {
|
||||
const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter");
|
||||
|
||||
formatterPath = resolver.resolve(npmFormat, `${cwd}/node_modules`);
|
||||
} catch (e) {
|
||||
formatterPath = `./formatters/${normalizedFormatName}`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return require(formatterPath);
|
||||
} catch (ex) {
|
||||
ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
|
||||
throw ex;
|
||||
}
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CLIEngine.version = pkg.version;
|
||||
CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
|
||||
|
||||
module.exports = CLIEngine;
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @fileoverview Main CLI object.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
* The CLI object should *not* call process.exit() directly. It should only return
|
||||
* exit codes. This allows other programs to use the CLI object and still control
|
||||
* when the program exits.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const fs = require("fs"),
|
||||
path = require("path"),
|
||||
options = require("./options"),
|
||||
CLIEngine = require("./cli-engine"),
|
||||
mkdirp = require("mkdirp"),
|
||||
log = require("./util/logging");
|
||||
|
||||
const debug = require("debug")("eslint:cli");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Predicate function for whether or not to apply fixes in quiet mode.
|
||||
* If a message is a warning, do not apply a fix.
|
||||
* @param {LintResult} lintResult The lint result.
|
||||
* @returns {boolean} True if the lint message is an error (and thus should be
|
||||
* autofixed), false otherwise.
|
||||
*/
|
||||
function quietFixPredicate(lintResult) {
|
||||
return lintResult.severity === 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the CLI options into the options expected by the CLIEngine.
|
||||
* @param {Object} cliOptions The CLI options to translate.
|
||||
* @returns {CLIEngineOptions} The options object for the CLIEngine.
|
||||
* @private
|
||||
*/
|
||||
function translateOptions(cliOptions) {
|
||||
return {
|
||||
envs: cliOptions.env,
|
||||
extensions: cliOptions.ext,
|
||||
rules: cliOptions.rule,
|
||||
plugins: cliOptions.plugin,
|
||||
globals: cliOptions.global,
|
||||
ignore: cliOptions.ignore,
|
||||
ignorePath: cliOptions.ignorePath,
|
||||
ignorePattern: cliOptions.ignorePattern,
|
||||
configFile: cliOptions.config,
|
||||
rulePaths: cliOptions.rulesdir,
|
||||
useEslintrc: cliOptions.eslintrc,
|
||||
parser: cliOptions.parser,
|
||||
parserOptions: cliOptions.parserOptions,
|
||||
cache: cliOptions.cache,
|
||||
cacheFile: cliOptions.cacheFile,
|
||||
cacheLocation: cliOptions.cacheLocation,
|
||||
fix: (cliOptions.fix || cliOptions.fixDryRun) && (cliOptions.quiet ? quietFixPredicate : true),
|
||||
fixTypes: cliOptions.fixType,
|
||||
allowInlineConfig: cliOptions.inlineConfig,
|
||||
reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the results of the linting.
|
||||
* @param {CLIEngine} engine The CLIEngine to use.
|
||||
* @param {LintResult[]} results The results to print.
|
||||
* @param {string} format The name of the formatter to use or the path to the formatter.
|
||||
* @param {string} outputFile The path for the output file.
|
||||
* @returns {boolean} True if the printing succeeds, false if not.
|
||||
* @private
|
||||
*/
|
||||
function printResults(engine, results, format, outputFile) {
|
||||
let formatter;
|
||||
|
||||
try {
|
||||
formatter = engine.getFormatter(format);
|
||||
} catch (e) {
|
||||
log.error(e.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
const output = formatter(results);
|
||||
|
||||
if (output) {
|
||||
if (outputFile) {
|
||||
const filePath = path.resolve(process.cwd(), outputFile);
|
||||
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
||||
log.error("Cannot write to output file path, it is a directory: %s", outputFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirp.sync(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, output);
|
||||
} catch (ex) {
|
||||
log.error("There was a problem writing the output file:\n%s", ex);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
log.info(output);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Encapsulates all CLI behavior for eslint. Makes it easier to test as well as
|
||||
* for other Node.js programs to effectively run the CLI.
|
||||
*/
|
||||
const cli = {
|
||||
|
||||
/**
|
||||
* Executes the CLI based on an array of arguments that is passed in.
|
||||
* @param {string|Array|Object} args The arguments to process.
|
||||
* @param {string} [text] The text to lint (used for TTY).
|
||||
* @returns {int} The exit code for the operation.
|
||||
*/
|
||||
execute(args, text) {
|
||||
if (Array.isArray(args)) {
|
||||
debug("CLI args: %o", args.slice(2));
|
||||
}
|
||||
|
||||
let currentOptions;
|
||||
|
||||
try {
|
||||
currentOptions = options.parse(args);
|
||||
} catch (error) {
|
||||
log.error(error.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const files = currentOptions._;
|
||||
|
||||
const useStdin = typeof text === "string";
|
||||
|
||||
if (currentOptions.version) { // version from package.json
|
||||
|
||||
log.info(`v${require("../package.json").version}`);
|
||||
|
||||
} else if (currentOptions.printConfig) {
|
||||
if (files.length) {
|
||||
log.error("The --print-config option must be used with exactly one file name.");
|
||||
return 2;
|
||||
}
|
||||
if (useStdin) {
|
||||
log.error("The --print-config option is not available for piped-in code.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const engine = new CLIEngine(translateOptions(currentOptions));
|
||||
|
||||
const fileConfig = engine.getConfigForFile(currentOptions.printConfig);
|
||||
|
||||
log.info(JSON.stringify(fileConfig, null, " "));
|
||||
return 0;
|
||||
} else if (currentOptions.help || (!files.length && !useStdin)) {
|
||||
|
||||
log.info(options.generateHelp());
|
||||
|
||||
} else {
|
||||
|
||||
debug(`Running on ${useStdin ? "text" : "files"}`);
|
||||
|
||||
if (currentOptions.fix && currentOptions.fixDryRun) {
|
||||
log.error("The --fix option and the --fix-dry-run option cannot be used together.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (useStdin && currentOptions.fix) {
|
||||
log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (currentOptions.fixType && !currentOptions.fix && !currentOptions.fixDryRun) {
|
||||
log.error("The --fix-type option requires either --fix or --fix-dry-run.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const engine = new CLIEngine(translateOptions(currentOptions));
|
||||
const report = useStdin ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files);
|
||||
|
||||
if (currentOptions.fix) {
|
||||
debug("Fix mode enabled - applying fixes");
|
||||
CLIEngine.outputFixes(report);
|
||||
}
|
||||
|
||||
if (currentOptions.quiet) {
|
||||
debug("Quiet mode enabled - filtering out warnings");
|
||||
report.results = CLIEngine.getErrorResults(report.results);
|
||||
}
|
||||
|
||||
if (printResults(engine, report.results, currentOptions.format, currentOptions.outputFile)) {
|
||||
const tooManyWarnings = currentOptions.maxWarnings >= 0 && report.warningCount > currentOptions.maxWarnings;
|
||||
|
||||
if (!report.errorCount && tooManyWarnings) {
|
||||
log.error("ESLint found too many warnings (maximum: %s).", currentOptions.maxWarnings);
|
||||
}
|
||||
|
||||
return (report.errorCount || tooManyWarnings) ? 1 : 0;
|
||||
}
|
||||
return 2;
|
||||
|
||||
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = cli;
|
||||
+683
@@ -0,0 +1,683 @@
|
||||
/**
|
||||
* @fileoverview A class of the code path analyzer.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const assert = require("assert"),
|
||||
CodePath = require("./code-path"),
|
||||
CodePathSegment = require("./code-path-segment"),
|
||||
IdGenerator = require("./id-generator"),
|
||||
debug = require("./debug-helpers"),
|
||||
astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is a `case` node (not `default` node).
|
||||
*
|
||||
* @param {ASTNode} node - A `SwitchCase` node to check.
|
||||
* @returns {boolean} `true` if the node is a `case` node (not `default` node).
|
||||
*/
|
||||
function isCaseNode(node) {
|
||||
return Boolean(node.test);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given logical operator is taken into account for the code
|
||||
* path analysis.
|
||||
*
|
||||
* @param {string} operator - The operator found in the LogicalExpression node
|
||||
* @returns {boolean} `true` if the operator is "&&" or "||"
|
||||
*/
|
||||
function isHandledLogicalOperator(operator) {
|
||||
return operator === "&&" || operator === "||";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given logical expression node goes different path
|
||||
* between the `true` case and the `false` case.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {boolean} `true` if the node is a test of a choice statement.
|
||||
*/
|
||||
function isForkingByTrueOrFalse(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "ConditionalExpression":
|
||||
case "IfStatement":
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
case "ForStatement":
|
||||
return parent.test === node;
|
||||
|
||||
case "LogicalExpression":
|
||||
return isHandledLogicalOperator(parent.operator);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the boolean value of a given literal node.
|
||||
*
|
||||
* This is used to detect infinity loops (e.g. `while (true) {}`).
|
||||
* Statements preceded by an infinity loop are unreachable if the loop didn't
|
||||
* have any `break` statement.
|
||||
*
|
||||
* @param {ASTNode} node - A node to get.
|
||||
* @returns {boolean|undefined} a boolean value if the node is a Literal node,
|
||||
* otherwise `undefined`.
|
||||
*/
|
||||
function getBooleanValueIfSimpleConstant(node) {
|
||||
if (node.type === "Literal") {
|
||||
return Boolean(node.value);
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a given identifier node is a reference or not.
|
||||
*
|
||||
* This is used to detect the first throwable node in a `try` block.
|
||||
*
|
||||
* @param {ASTNode} node - An Identifier node to check.
|
||||
* @returns {boolean} `true` if the node is a reference.
|
||||
*/
|
||||
function isIdentifierReference(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "LabeledStatement":
|
||||
case "BreakStatement":
|
||||
case "ContinueStatement":
|
||||
case "ArrayPattern":
|
||||
case "RestElement":
|
||||
case "ImportSpecifier":
|
||||
case "ImportDefaultSpecifier":
|
||||
case "ImportNamespaceSpecifier":
|
||||
case "CatchClause":
|
||||
return false;
|
||||
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "ClassDeclaration":
|
||||
case "ClassExpression":
|
||||
case "VariableDeclarator":
|
||||
return parent.id !== node;
|
||||
|
||||
case "Property":
|
||||
case "MethodDefinition":
|
||||
return (
|
||||
parent.key !== node ||
|
||||
parent.computed ||
|
||||
parent.shorthand
|
||||
);
|
||||
|
||||
case "AssignmentPattern":
|
||||
return parent.key !== node;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current segment with the head segment.
|
||||
* This is similar to local branches and tracking branches of git.
|
||||
*
|
||||
* To separate the current and the head is in order to not make useless segments.
|
||||
*
|
||||
* In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
|
||||
* events are fired.
|
||||
*
|
||||
* @param {CodePathAnalyzer} analyzer - The instance.
|
||||
* @param {ASTNode} node - The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function forwardCurrentToHead(analyzer, node) {
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
const currentSegments = state.currentSegments;
|
||||
const headSegments = state.headSegments;
|
||||
const end = Math.max(currentSegments.length, headSegments.length);
|
||||
let i, currentSegment, headSegment;
|
||||
|
||||
// Fires leaving events.
|
||||
for (i = 0; i < end; ++i) {
|
||||
currentSegment = currentSegments[i];
|
||||
headSegment = headSegments[i];
|
||||
|
||||
if (currentSegment !== headSegment && currentSegment) {
|
||||
debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
|
||||
|
||||
if (currentSegment.reachable) {
|
||||
analyzer.emitter.emit(
|
||||
"onCodePathSegmentEnd",
|
||||
currentSegment,
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update state.
|
||||
state.currentSegments = headSegments;
|
||||
|
||||
// Fires entering events.
|
||||
for (i = 0; i < end; ++i) {
|
||||
currentSegment = currentSegments[i];
|
||||
headSegment = headSegments[i];
|
||||
|
||||
if (currentSegment !== headSegment && headSegment) {
|
||||
debug.dump(`onCodePathSegmentStart ${headSegment.id}`);
|
||||
|
||||
CodePathSegment.markUsed(headSegment);
|
||||
if (headSegment.reachable) {
|
||||
analyzer.emitter.emit(
|
||||
"onCodePathSegmentStart",
|
||||
headSegment,
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current segment with empty.
|
||||
* This is called at the last of functions or the program.
|
||||
*
|
||||
* @param {CodePathAnalyzer} analyzer - The instance.
|
||||
* @param {ASTNode} node - The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function leaveFromCurrentSegment(analyzer, node) {
|
||||
const state = CodePath.getState(analyzer.codePath);
|
||||
const currentSegments = state.currentSegments;
|
||||
|
||||
for (let i = 0; i < currentSegments.length; ++i) {
|
||||
const currentSegment = currentSegments[i];
|
||||
|
||||
debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
|
||||
if (currentSegment.reachable) {
|
||||
analyzer.emitter.emit(
|
||||
"onCodePathSegmentEnd",
|
||||
currentSegment,
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
state.currentSegments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the position of a given node in the parent node
|
||||
* thereof.
|
||||
*
|
||||
* For example, if the node is `parent.consequent`, this creates a fork from the
|
||||
* current path.
|
||||
*
|
||||
* @param {CodePathAnalyzer} analyzer - The instance.
|
||||
* @param {ASTNode} node - The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function preprocess(analyzer, node) {
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case "LogicalExpression":
|
||||
if (
|
||||
parent.right === node &&
|
||||
isHandledLogicalOperator(parent.operator)
|
||||
) {
|
||||
state.makeLogicalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
case "ConditionalExpression":
|
||||
case "IfStatement":
|
||||
|
||||
/*
|
||||
* Fork if this node is at `consequent`/`alternate`.
|
||||
* `popForkContext()` exists at `IfStatement:exit` and
|
||||
* `ConditionalExpression:exit`.
|
||||
*/
|
||||
if (parent.consequent === node) {
|
||||
state.makeIfConsequent();
|
||||
} else if (parent.alternate === node) {
|
||||
state.makeIfAlternate();
|
||||
}
|
||||
break;
|
||||
|
||||
case "SwitchCase":
|
||||
if (parent.consequent[0] === node) {
|
||||
state.makeSwitchCaseBody(false, !parent.test);
|
||||
}
|
||||
break;
|
||||
|
||||
case "TryStatement":
|
||||
if (parent.handler === node) {
|
||||
state.makeCatchBlock();
|
||||
} else if (parent.finalizer === node) {
|
||||
state.makeFinallyBlock();
|
||||
}
|
||||
break;
|
||||
|
||||
case "WhileStatement":
|
||||
if (parent.test === node) {
|
||||
state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
|
||||
} else {
|
||||
assert(parent.body === node);
|
||||
state.makeWhileBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case "DoWhileStatement":
|
||||
if (parent.body === node) {
|
||||
state.makeDoWhileBody();
|
||||
} else {
|
||||
assert(parent.test === node);
|
||||
state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
|
||||
}
|
||||
break;
|
||||
|
||||
case "ForStatement":
|
||||
if (parent.test === node) {
|
||||
state.makeForTest(getBooleanValueIfSimpleConstant(node));
|
||||
} else if (parent.update === node) {
|
||||
state.makeForUpdate();
|
||||
} else if (parent.body === node) {
|
||||
state.makeForBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
if (parent.left === node) {
|
||||
state.makeForInOfLeft();
|
||||
} else if (parent.right === node) {
|
||||
state.makeForInOfRight();
|
||||
} else {
|
||||
assert(parent.body === node);
|
||||
state.makeForInOfBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case "AssignmentPattern":
|
||||
|
||||
/*
|
||||
* Fork if this node is at `right`.
|
||||
* `left` is executed always, so it uses the current path.
|
||||
* `popForkContext()` exists at `AssignmentPattern:exit`.
|
||||
*/
|
||||
if (parent.right === node) {
|
||||
state.pushForkContext();
|
||||
state.forkBypassPath();
|
||||
state.forkPath();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the type of a given node in entering.
|
||||
*
|
||||
* @param {CodePathAnalyzer} analyzer - The instance.
|
||||
* @param {ASTNode} node - The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processCodePathToEnter(analyzer, node) {
|
||||
let codePath = analyzer.codePath;
|
||||
let state = codePath && CodePath.getState(codePath);
|
||||
const parent = node.parent;
|
||||
|
||||
switch (node.type) {
|
||||
case "Program":
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
if (codePath) {
|
||||
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
debug.dumpState(node, state, false);
|
||||
}
|
||||
|
||||
// Create the code path of this scope.
|
||||
codePath = analyzer.codePath = new CodePath(
|
||||
analyzer.idGenerator.next(),
|
||||
codePath,
|
||||
analyzer.onLooped
|
||||
);
|
||||
state = CodePath.getState(codePath);
|
||||
|
||||
// Emits onCodePathStart events.
|
||||
debug.dump(`onCodePathStart ${codePath.id}`);
|
||||
analyzer.emitter.emit("onCodePathStart", codePath, node);
|
||||
break;
|
||||
|
||||
case "LogicalExpression":
|
||||
if (isHandledLogicalOperator(node.operator)) {
|
||||
state.pushChoiceContext(
|
||||
node.operator,
|
||||
isForkingByTrueOrFalse(node)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "ConditionalExpression":
|
||||
case "IfStatement":
|
||||
state.pushChoiceContext("test", false);
|
||||
break;
|
||||
|
||||
case "SwitchStatement":
|
||||
state.pushSwitchContext(
|
||||
node.cases.some(isCaseNode),
|
||||
astUtils.getLabel(node)
|
||||
);
|
||||
break;
|
||||
|
||||
case "TryStatement":
|
||||
state.pushTryContext(Boolean(node.finalizer));
|
||||
break;
|
||||
|
||||
case "SwitchCase":
|
||||
|
||||
/*
|
||||
* Fork if this node is after the 2st node in `cases`.
|
||||
* It's similar to `else` blocks.
|
||||
* The next `test` node is processed in this path.
|
||||
*/
|
||||
if (parent.discriminant !== node && parent.cases[0] !== node) {
|
||||
state.forkPath();
|
||||
}
|
||||
break;
|
||||
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
case "ForStatement":
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
state.pushLoopContext(node.type, astUtils.getLabel(node));
|
||||
break;
|
||||
|
||||
case "LabeledStatement":
|
||||
if (!astUtils.isBreakableStatement(node.body)) {
|
||||
state.pushBreakContext(false, node.label.name);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
debug.dumpState(node, state, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the type of a given node in leaving.
|
||||
*
|
||||
* @param {CodePathAnalyzer} analyzer - The instance.
|
||||
* @param {ASTNode} node - The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processCodePathToExit(analyzer, node) {
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
let dontForward = false;
|
||||
|
||||
switch (node.type) {
|
||||
case "IfStatement":
|
||||
case "ConditionalExpression":
|
||||
state.popChoiceContext();
|
||||
break;
|
||||
|
||||
case "LogicalExpression":
|
||||
if (isHandledLogicalOperator(node.operator)) {
|
||||
state.popChoiceContext();
|
||||
}
|
||||
break;
|
||||
|
||||
case "SwitchStatement":
|
||||
state.popSwitchContext();
|
||||
break;
|
||||
|
||||
case "SwitchCase":
|
||||
|
||||
/*
|
||||
* This is the same as the process at the 1st `consequent` node in
|
||||
* `preprocess` function.
|
||||
* Must do if this `consequent` is empty.
|
||||
*/
|
||||
if (node.consequent.length === 0) {
|
||||
state.makeSwitchCaseBody(true, !node.test);
|
||||
}
|
||||
if (state.forkContext.reachable) {
|
||||
dontForward = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "TryStatement":
|
||||
state.popTryContext();
|
||||
break;
|
||||
|
||||
case "BreakStatement":
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeBreak(node.label && node.label.name);
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case "ContinueStatement":
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeContinue(node.label && node.label.name);
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case "ReturnStatement":
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeReturn();
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case "ThrowStatement":
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeThrow();
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case "Identifier":
|
||||
if (isIdentifierReference(node)) {
|
||||
state.makeFirstThrowablePathInTryBlock();
|
||||
dontForward = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "CallExpression":
|
||||
case "MemberExpression":
|
||||
case "NewExpression":
|
||||
state.makeFirstThrowablePathInTryBlock();
|
||||
break;
|
||||
|
||||
case "WhileStatement":
|
||||
case "DoWhileStatement":
|
||||
case "ForStatement":
|
||||
case "ForInStatement":
|
||||
case "ForOfStatement":
|
||||
state.popLoopContext();
|
||||
break;
|
||||
|
||||
case "AssignmentPattern":
|
||||
state.popForkContext();
|
||||
break;
|
||||
|
||||
case "LabeledStatement":
|
||||
if (!astUtils.isBreakableStatement(node.body)) {
|
||||
state.popBreakContext();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
if (!dontForward) {
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
}
|
||||
debug.dumpState(node, state, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path to finalize the current code path.
|
||||
*
|
||||
* @param {CodePathAnalyzer} analyzer - The instance.
|
||||
* @param {ASTNode} node - The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function postprocess(analyzer, node) {
|
||||
switch (node.type) {
|
||||
case "Program":
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression": {
|
||||
let codePath = analyzer.codePath;
|
||||
|
||||
// Mark the current path as the final node.
|
||||
CodePath.getState(codePath).makeFinal();
|
||||
|
||||
// Emits onCodePathSegmentEnd event of the current segments.
|
||||
leaveFromCurrentSegment(analyzer, node);
|
||||
|
||||
// Emits onCodePathEnd event of this code path.
|
||||
debug.dump(`onCodePathEnd ${codePath.id}`);
|
||||
analyzer.emitter.emit("onCodePathEnd", codePath, node);
|
||||
debug.dumpDot(codePath);
|
||||
|
||||
codePath = analyzer.codePath = analyzer.codePath.upper;
|
||||
if (codePath) {
|
||||
debug.dumpState(node, CodePath.getState(codePath), true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The class to analyze code paths.
|
||||
* This class implements the EventGenerator interface.
|
||||
*/
|
||||
class CodePathAnalyzer {
|
||||
|
||||
/**
|
||||
* @param {EventGenerator} eventGenerator - An event generator to wrap.
|
||||
*/
|
||||
constructor(eventGenerator) {
|
||||
this.original = eventGenerator;
|
||||
this.emitter = eventGenerator.emitter;
|
||||
this.codePath = null;
|
||||
this.idGenerator = new IdGenerator("s");
|
||||
this.currentNode = null;
|
||||
this.onLooped = this.onLooped.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the process to enter a given AST node.
|
||||
* This updates state of analysis and calls `enterNode` of the wrapped.
|
||||
*
|
||||
* @param {ASTNode} node - A node which is entering.
|
||||
* @returns {void}
|
||||
*/
|
||||
enterNode(node) {
|
||||
this.currentNode = node;
|
||||
|
||||
// Updates the code path due to node's position in its parent node.
|
||||
if (node.parent) {
|
||||
preprocess(this, node);
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates the code path.
|
||||
* And emits onCodePathStart/onCodePathSegmentStart events.
|
||||
*/
|
||||
processCodePathToEnter(this, node);
|
||||
|
||||
// Emits node events.
|
||||
this.original.enterNode(node);
|
||||
|
||||
this.currentNode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the process to leave a given AST node.
|
||||
* This updates state of analysis and calls `leaveNode` of the wrapped.
|
||||
*
|
||||
* @param {ASTNode} node - A node which is leaving.
|
||||
* @returns {void}
|
||||
*/
|
||||
leaveNode(node) {
|
||||
this.currentNode = node;
|
||||
|
||||
/*
|
||||
* Updates the code path.
|
||||
* And emits onCodePathStart/onCodePathSegmentStart events.
|
||||
*/
|
||||
processCodePathToExit(this, node);
|
||||
|
||||
// Emits node events.
|
||||
this.original.leaveNode(node);
|
||||
|
||||
// Emits the last onCodePathStart/onCodePathSegmentStart events.
|
||||
postprocess(this, node);
|
||||
|
||||
this.currentNode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called on a code path looped.
|
||||
* Then this raises a looped event.
|
||||
*
|
||||
* @param {CodePathSegment} fromSegment - A segment of prev.
|
||||
* @param {CodePathSegment} toSegment - A segment of next.
|
||||
* @returns {void}
|
||||
*/
|
||||
onLooped(fromSegment, toSegment) {
|
||||
if (fromSegment.reachable && toSegment.reachable) {
|
||||
debug.dump(`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`);
|
||||
this.emitter.emit(
|
||||
"onCodePathSegmentLoop",
|
||||
fromSegment,
|
||||
toSegment,
|
||||
this.currentNode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePathAnalyzer;
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* @fileoverview A class of the code path segment.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const debug = require("./debug-helpers");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given segment is reachable.
|
||||
*
|
||||
* @param {CodePathSegment} segment - A segment to check.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A code path segment.
|
||||
*/
|
||||
class CodePathSegment {
|
||||
|
||||
/**
|
||||
* @param {string} id - An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments - An array of the previous segments.
|
||||
* This array includes unreachable segments.
|
||||
* @param {boolean} reachable - A flag which shows this is reachable.
|
||||
*/
|
||||
constructor(id, allPrevSegments, reachable) {
|
||||
|
||||
/**
|
||||
* The identifier of this code path.
|
||||
* Rules use it to store additional information of each rule.
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* An array of the next segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.nextSegments = [];
|
||||
|
||||
/**
|
||||
* An array of the previous segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.prevSegments = allPrevSegments.filter(isReachable);
|
||||
|
||||
/**
|
||||
* An array of the next segments.
|
||||
* This array includes unreachable segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.allNextSegments = [];
|
||||
|
||||
/**
|
||||
* An array of the previous segments.
|
||||
* This array includes unreachable segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.allPrevSegments = allPrevSegments;
|
||||
|
||||
/**
|
||||
* A flag which shows this is reachable.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.reachable = reachable;
|
||||
|
||||
// Internal data.
|
||||
Object.defineProperty(this, "internal", {
|
||||
value: {
|
||||
used: false,
|
||||
loopedPrevSegments: []
|
||||
}
|
||||
});
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (debug.enabled) {
|
||||
this.internal.nodes = [];
|
||||
this.internal.exitNodes = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a given previous segment is coming from the end of a loop.
|
||||
*
|
||||
* @param {CodePathSegment} segment - A previous segment to check.
|
||||
* @returns {boolean} `true` if the segment is coming from the end of a loop.
|
||||
*/
|
||||
isLoopedPrevSegment(segment) {
|
||||
return this.internal.loopedPrevSegments.indexOf(segment) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the root segment.
|
||||
*
|
||||
* @param {string} id - An identifier.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newRoot(id) {
|
||||
return new CodePathSegment(id, [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a segment that follows given segments.
|
||||
*
|
||||
* @param {string} id - An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments - An array of the previous segments.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newNext(id, allPrevSegments) {
|
||||
return new CodePathSegment(
|
||||
id,
|
||||
CodePathSegment.flattenUnusedSegments(allPrevSegments),
|
||||
allPrevSegments.some(isReachable)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unreachable segment that follows given segments.
|
||||
*
|
||||
* @param {string} id - An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments - An array of the previous segments.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newUnreachable(id, allPrevSegments) {
|
||||
const segment = new CodePathSegment(id, CodePathSegment.flattenUnusedSegments(allPrevSegments), false);
|
||||
|
||||
/*
|
||||
* In `if (a) return a; foo();` case, the unreachable segment preceded by
|
||||
* the return statement is not used but must not be remove.
|
||||
*/
|
||||
CodePathSegment.markUsed(segment);
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a segment that follows given segments.
|
||||
* This factory method does not connect with `allPrevSegments`.
|
||||
* But this inherits `reachable` flag.
|
||||
*
|
||||
* @param {string} id - An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments - An array of the previous segments.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newDisconnected(id, allPrevSegments) {
|
||||
return new CodePathSegment(id, [], allPrevSegments.some(isReachable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a given segment being used.
|
||||
*
|
||||
* And this function registers the segment into the previous segments as a next.
|
||||
*
|
||||
* @param {CodePathSegment} segment - A segment to mark.
|
||||
* @returns {void}
|
||||
*/
|
||||
static markUsed(segment) {
|
||||
if (segment.internal.used) {
|
||||
return;
|
||||
}
|
||||
segment.internal.used = true;
|
||||
|
||||
let i;
|
||||
|
||||
if (segment.reachable) {
|
||||
for (i = 0; i < segment.allPrevSegments.length; ++i) {
|
||||
const prevSegment = segment.allPrevSegments[i];
|
||||
|
||||
prevSegment.allNextSegments.push(segment);
|
||||
prevSegment.nextSegments.push(segment);
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < segment.allPrevSegments.length; ++i) {
|
||||
segment.allPrevSegments[i].allNextSegments.push(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a previous segment as looped.
|
||||
*
|
||||
* @param {CodePathSegment} segment - A segment.
|
||||
* @param {CodePathSegment} prevSegment - A previous segment to mark.
|
||||
* @returns {void}
|
||||
*/
|
||||
static markPrevSegmentAsLooped(segment, prevSegment) {
|
||||
segment.internal.loopedPrevSegments.push(prevSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces unused segments with the previous segments of each unused segment.
|
||||
*
|
||||
* @param {CodePathSegment[]} segments - An array of segments to replace.
|
||||
* @returns {CodePathSegment[]} The replaced array.
|
||||
*/
|
||||
static flattenUnusedSegments(segments) {
|
||||
const done = Object.create(null);
|
||||
const retv = [];
|
||||
|
||||
for (let i = 0; i < segments.length; ++i) {
|
||||
const segment = segments[i];
|
||||
|
||||
// Ignores duplicated.
|
||||
if (done[segment.id]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use previous segments if unused.
|
||||
if (!segment.internal.used) {
|
||||
for (let j = 0; j < segment.allPrevSegments.length; ++j) {
|
||||
const prevSegment = segment.allPrevSegments[j];
|
||||
|
||||
if (!done[prevSegment.id]) {
|
||||
done[prevSegment.id] = true;
|
||||
retv.push(prevSegment);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
done[segment.id] = true;
|
||||
retv.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
return retv;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePathSegment;
|
||||
+1440
File diff suppressed because it is too large
Load Diff
+239
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @fileoverview A class of the code path.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const CodePathState = require("./code-path-state");
|
||||
const IdGenerator = require("./id-generator");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A code path.
|
||||
*/
|
||||
class CodePath {
|
||||
|
||||
/**
|
||||
* @param {string} id - An identifier.
|
||||
* @param {CodePath|null} upper - The code path of the upper function scope.
|
||||
* @param {Function} onLooped - A callback function to notify looping.
|
||||
*/
|
||||
constructor(id, upper, onLooped) {
|
||||
|
||||
/**
|
||||
* The identifier of this code path.
|
||||
* Rules use it to store additional information of each rule.
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* The code path of the upper function scope.
|
||||
* @type {CodePath|null}
|
||||
*/
|
||||
this.upper = upper;
|
||||
|
||||
/**
|
||||
* The code paths of nested function scopes.
|
||||
* @type {CodePath[]}
|
||||
*/
|
||||
this.childCodePaths = [];
|
||||
|
||||
// Initializes internal state.
|
||||
Object.defineProperty(
|
||||
this,
|
||||
"internal",
|
||||
{ value: new CodePathState(new IdGenerator(`${id}_`), onLooped) }
|
||||
);
|
||||
|
||||
// Adds this into `childCodePaths` of `upper`.
|
||||
if (upper) {
|
||||
upper.childCodePaths.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the state of a given code path.
|
||||
*
|
||||
* @param {CodePath} codePath - A code path to get.
|
||||
* @returns {CodePathState} The state of the code path.
|
||||
*/
|
||||
static getState(codePath) {
|
||||
return codePath.internal;
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial code path segment.
|
||||
* @type {CodePathSegment}
|
||||
*/
|
||||
get initialSegment() {
|
||||
return this.internal.initialSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Final code path segments.
|
||||
* This array is a mix of `returnedSegments` and `thrownSegments`.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get finalSegments() {
|
||||
return this.internal.finalSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Final code path segments which is with `return` statements.
|
||||
* This array contains the last path segment if it's reachable.
|
||||
* Since the reachable last path returns `undefined`.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get returnedSegments() {
|
||||
return this.internal.returnedForkContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Final code path segments which is with `throw` statements.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get thrownSegments() {
|
||||
return this.internal.thrownForkContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current code path segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get currentSegments() {
|
||||
return this.internal.currentSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses all segments in this code path.
|
||||
*
|
||||
* codePath.traverseSegments(function(segment, controller) {
|
||||
* // do something.
|
||||
* });
|
||||
*
|
||||
* This method enumerates segments in order from the head.
|
||||
*
|
||||
* The `controller` object has two methods.
|
||||
*
|
||||
* - `controller.skip()` - Skip the following segments in this branch.
|
||||
* - `controller.break()` - Skip all following segments.
|
||||
*
|
||||
* @param {Object} [options] - Omittable.
|
||||
* @param {CodePathSegment} [options.first] - The first segment to traverse.
|
||||
* @param {CodePathSegment} [options.last] - The last segment to traverse.
|
||||
* @param {Function} callback - A callback function.
|
||||
* @returns {void}
|
||||
*/
|
||||
traverseSegments(options, callback) {
|
||||
let resolvedOptions;
|
||||
let resolvedCallback;
|
||||
|
||||
if (typeof options === "function") {
|
||||
resolvedCallback = options;
|
||||
resolvedOptions = {};
|
||||
} else {
|
||||
resolvedOptions = options || {};
|
||||
resolvedCallback = callback;
|
||||
}
|
||||
|
||||
const startSegment = resolvedOptions.first || this.internal.initialSegment;
|
||||
const lastSegment = resolvedOptions.last;
|
||||
|
||||
let item = null;
|
||||
let index = 0;
|
||||
let end = 0;
|
||||
let segment = null;
|
||||
const visited = Object.create(null);
|
||||
const stack = [[startSegment, 0]];
|
||||
let skippedSegment = null;
|
||||
let broken = false;
|
||||
const controller = {
|
||||
skip() {
|
||||
if (stack.length <= 1) {
|
||||
broken = true;
|
||||
} else {
|
||||
skippedSegment = stack[stack.length - 2][0];
|
||||
}
|
||||
},
|
||||
break() {
|
||||
broken = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks a given previous segment has been visited.
|
||||
* @param {CodePathSegment} prevSegment - A previous segment to check.
|
||||
* @returns {boolean} `true` if the segment has been visited.
|
||||
*/
|
||||
function isVisited(prevSegment) {
|
||||
return (
|
||||
visited[prevSegment.id] ||
|
||||
segment.isLoopedPrevSegment(prevSegment)
|
||||
);
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
item = stack[stack.length - 1];
|
||||
segment = item[0];
|
||||
index = item[1];
|
||||
|
||||
if (index === 0) {
|
||||
|
||||
// Skip if this segment has been visited already.
|
||||
if (visited[segment.id]) {
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if all previous segments have not been visited.
|
||||
if (segment !== startSegment &&
|
||||
segment.prevSegments.length > 0 &&
|
||||
!segment.prevSegments.every(isVisited)
|
||||
) {
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset the flag of skipping if all branches have been skipped.
|
||||
if (skippedSegment && segment.prevSegments.indexOf(skippedSegment) !== -1) {
|
||||
skippedSegment = null;
|
||||
}
|
||||
visited[segment.id] = true;
|
||||
|
||||
// Call the callback when the first time.
|
||||
if (!skippedSegment) {
|
||||
resolvedCallback.call(this, segment, controller);
|
||||
if (segment === lastSegment) {
|
||||
controller.skip();
|
||||
}
|
||||
if (broken) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the stack.
|
||||
end = segment.nextSegments.length - 1;
|
||||
if (index < end) {
|
||||
item[1] += 1;
|
||||
stack.push([segment.nextSegments[index], 0]);
|
||||
} else if (index === end) {
|
||||
item[0] = segment.nextSegments[index];
|
||||
item[1] = 0;
|
||||
} else {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePath;
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @fileoverview Helpers to debug for code path analysis.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const debug = require("debug")("eslint:code-path");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets id of a given segment.
|
||||
* @param {CodePathSegment} segment - A segment to get.
|
||||
* @returns {string} Id of the segment.
|
||||
*/
|
||||
/* istanbul ignore next */
|
||||
function getId(segment) { // eslint-disable-line require-jsdoc
|
||||
return segment.id + (segment.reachable ? "" : "!");
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* A flag that debug dumping is enabled or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
enabled: debug.enabled,
|
||||
|
||||
/**
|
||||
* Dumps given objects.
|
||||
*
|
||||
* @param {...any} args - objects to dump.
|
||||
* @returns {void}
|
||||
*/
|
||||
dump: debug,
|
||||
|
||||
/**
|
||||
* Dumps the current analyzing state.
|
||||
*
|
||||
* @param {ASTNode} node - A node to dump.
|
||||
* @param {CodePathState} state - A state to dump.
|
||||
* @param {boolean} leaving - A flag whether or not it's leaving
|
||||
* @returns {void}
|
||||
*/
|
||||
dumpState: !debug.enabled ? debug : /* istanbul ignore next */ function(node, state, leaving) {
|
||||
for (let i = 0; i < state.currentSegments.length; ++i) {
|
||||
const segInternal = state.currentSegments[i].internal;
|
||||
|
||||
if (leaving) {
|
||||
segInternal.exitNodes.push(node);
|
||||
} else {
|
||||
segInternal.nodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
debug([
|
||||
`${state.currentSegments.map(getId).join(",")})`,
|
||||
`${node.type}${leaving ? ":exit" : ""}`
|
||||
].join(" "));
|
||||
},
|
||||
|
||||
/**
|
||||
* Dumps a DOT code of a given code path.
|
||||
* The DOT code can be visialized with Graphvis.
|
||||
*
|
||||
* @param {CodePath} codePath - A code path to dump.
|
||||
* @returns {void}
|
||||
* @see http://www.graphviz.org
|
||||
* @see http://www.webgraphviz.com
|
||||
*/
|
||||
dumpDot: !debug.enabled ? debug : /* istanbul ignore next */ function(codePath) {
|
||||
let text =
|
||||
"\n" +
|
||||
"digraph {\n" +
|
||||
"node[shape=box,style=\"rounded,filled\",fillcolor=white];\n" +
|
||||
"initial[label=\"\",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];\n";
|
||||
|
||||
if (codePath.returnedSegments.length > 0) {
|
||||
text += "final[label=\"\",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];\n";
|
||||
}
|
||||
if (codePath.thrownSegments.length > 0) {
|
||||
text += "thrown[label=\"✘\",shape=circle,width=0.3,height=0.3,fixedsize];\n";
|
||||
}
|
||||
|
||||
const traceMap = Object.create(null);
|
||||
const arrows = this.makeDotArrows(codePath, traceMap);
|
||||
|
||||
for (const id in traceMap) { // eslint-disable-line guard-for-in
|
||||
const segment = traceMap[id];
|
||||
|
||||
text += `${id}[`;
|
||||
|
||||
if (segment.reachable) {
|
||||
text += "label=\"";
|
||||
} else {
|
||||
text += "style=\"rounded,dashed,filled\",fillcolor=\"#FF9800\",label=\"<<unreachable>>\\n";
|
||||
}
|
||||
|
||||
if (segment.internal.nodes.length > 0 || segment.internal.exitNodes.length > 0) {
|
||||
text += [].concat(
|
||||
segment.internal.nodes.map(node => {
|
||||
switch (node.type) {
|
||||
case "Identifier": return `${node.type} (${node.name})`;
|
||||
case "Literal": return `${node.type} (${node.value})`;
|
||||
default: return node.type;
|
||||
}
|
||||
}),
|
||||
segment.internal.exitNodes.map(node => {
|
||||
switch (node.type) {
|
||||
case "Identifier": return `${node.type}:exit (${node.name})`;
|
||||
case "Literal": return `${node.type}:exit (${node.value})`;
|
||||
default: return `${node.type}:exit`;
|
||||
}
|
||||
})
|
||||
).join("\\n");
|
||||
} else {
|
||||
text += "????";
|
||||
}
|
||||
|
||||
text += "\"];\n";
|
||||
}
|
||||
|
||||
text += `${arrows}\n`;
|
||||
text += "}";
|
||||
debug("DOT", text);
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes a DOT code of a given code path.
|
||||
* The DOT code can be visialized with Graphvis.
|
||||
*
|
||||
* @param {CodePath} codePath - A code path to make DOT.
|
||||
* @param {Object} traceMap - Optional. A map to check whether or not segments had been done.
|
||||
* @returns {string} A DOT code of the code path.
|
||||
*/
|
||||
makeDotArrows(codePath, traceMap) {
|
||||
const stack = [[codePath.initialSegment, 0]];
|
||||
const done = traceMap || Object.create(null);
|
||||
let lastId = codePath.initialSegment.id;
|
||||
let text = `initial->${codePath.initialSegment.id}`;
|
||||
|
||||
while (stack.length > 0) {
|
||||
const item = stack.pop();
|
||||
const segment = item[0];
|
||||
const index = item[1];
|
||||
|
||||
if (done[segment.id] && index === 0) {
|
||||
continue;
|
||||
}
|
||||
done[segment.id] = segment;
|
||||
|
||||
const nextSegment = segment.allNextSegments[index];
|
||||
|
||||
if (!nextSegment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastId === segment.id) {
|
||||
text += `->${nextSegment.id}`;
|
||||
} else {
|
||||
text += `;\n${segment.id}->${nextSegment.id}`;
|
||||
}
|
||||
lastId = nextSegment.id;
|
||||
|
||||
stack.unshift([segment, 1 + index]);
|
||||
stack.push([nextSegment, 0]);
|
||||
}
|
||||
|
||||
codePath.returnedSegments.forEach(finalSegment => {
|
||||
if (lastId === finalSegment.id) {
|
||||
text += "->final";
|
||||
} else {
|
||||
text += `;\n${finalSegment.id}->final`;
|
||||
}
|
||||
lastId = null;
|
||||
});
|
||||
|
||||
codePath.thrownSegments.forEach(finalSegment => {
|
||||
if (lastId === finalSegment.id) {
|
||||
text += "->thrown";
|
||||
} else {
|
||||
text += `;\n${finalSegment.id}->thrown`;
|
||||
}
|
||||
lastId = null;
|
||||
});
|
||||
|
||||
return `${text};`;
|
||||
}
|
||||
};
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* @fileoverview A class to operate forking.
|
||||
*
|
||||
* This is state of forking.
|
||||
* This has a fork list and manages it.
|
||||
*
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const assert = require("assert"),
|
||||
CodePathSegment = require("./code-path-segment");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets whether or not a given segment is reachable.
|
||||
*
|
||||
* @param {CodePathSegment} segment - A segment to get.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new segments from the specific range of `context.segmentsList`.
|
||||
*
|
||||
* When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and
|
||||
* `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`.
|
||||
* This `h` is from `b`, `d`, and `f`.
|
||||
*
|
||||
* @param {ForkContext} context - An instance.
|
||||
* @param {number} begin - The first index of the previous segments.
|
||||
* @param {number} end - The last index of the previous segments.
|
||||
* @param {Function} create - A factory function of new segments.
|
||||
* @returns {CodePathSegment[]} New segments.
|
||||
*/
|
||||
function makeSegments(context, begin, end, create) {
|
||||
const list = context.segmentsList;
|
||||
|
||||
const normalizedBegin = begin >= 0 ? begin : list.length + begin;
|
||||
const normalizedEnd = end >= 0 ? end : list.length + end;
|
||||
|
||||
const segments = [];
|
||||
|
||||
for (let i = 0; i < context.count; ++i) {
|
||||
const allPrevSegments = [];
|
||||
|
||||
for (let j = normalizedBegin; j <= normalizedEnd; ++j) {
|
||||
allPrevSegments.push(list[j][i]);
|
||||
}
|
||||
|
||||
segments.push(create(context.idGenerator.next(), allPrevSegments));
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* `segments` becomes doubly in a `finally` block. Then if a code path exits by a
|
||||
* control statement (such as `break`, `continue`) from the `finally` block, the
|
||||
* destination's segments may be half of the source segments. In that case, this
|
||||
* merges segments.
|
||||
*
|
||||
* @param {ForkContext} context - An instance.
|
||||
* @param {CodePathSegment[]} segments - Segments to merge.
|
||||
* @returns {CodePathSegment[]} The merged segments.
|
||||
*/
|
||||
function mergeExtraSegments(context, segments) {
|
||||
let currentSegments = segments;
|
||||
|
||||
while (currentSegments.length > context.count) {
|
||||
const merged = [];
|
||||
|
||||
for (let i = 0, length = currentSegments.length / 2 | 0; i < length; ++i) {
|
||||
merged.push(CodePathSegment.newNext(
|
||||
context.idGenerator.next(),
|
||||
[currentSegments[i], currentSegments[i + length]]
|
||||
));
|
||||
}
|
||||
currentSegments = merged;
|
||||
}
|
||||
return currentSegments;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A class to manage forking.
|
||||
*/
|
||||
class ForkContext {
|
||||
|
||||
/**
|
||||
* @param {IdGenerator} idGenerator - An identifier generator for segments.
|
||||
* @param {ForkContext|null} upper - An upper fork context.
|
||||
* @param {number} count - A number of parallel segments.
|
||||
*/
|
||||
constructor(idGenerator, upper, count) {
|
||||
this.idGenerator = idGenerator;
|
||||
this.upper = upper;
|
||||
this.count = count;
|
||||
this.segmentsList = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The head segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get head() {
|
||||
const list = this.segmentsList;
|
||||
|
||||
return list.length === 0 ? [] : list[list.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag which shows empty.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get empty() {
|
||||
return this.segmentsList.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag which shows reachable.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get reachable() {
|
||||
const segments = this.head;
|
||||
|
||||
return segments.length > 0 && segments.some(isReachable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new segments from this context.
|
||||
*
|
||||
* @param {number} begin - The first index of previous segments.
|
||||
* @param {number} end - The last index of previous segments.
|
||||
* @returns {CodePathSegment[]} New segments.
|
||||
*/
|
||||
makeNext(begin, end) {
|
||||
return makeSegments(this, begin, end, CodePathSegment.newNext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new segments from this context.
|
||||
* The new segments is always unreachable.
|
||||
*
|
||||
* @param {number} begin - The first index of previous segments.
|
||||
* @param {number} end - The last index of previous segments.
|
||||
* @returns {CodePathSegment[]} New segments.
|
||||
*/
|
||||
makeUnreachable(begin, end) {
|
||||
return makeSegments(this, begin, end, CodePathSegment.newUnreachable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new segments from this context.
|
||||
* The new segments don't have connections for previous segments.
|
||||
* But these inherit the reachable flag from this context.
|
||||
*
|
||||
* @param {number} begin - The first index of previous segments.
|
||||
* @param {number} end - The last index of previous segments.
|
||||
* @returns {CodePathSegment[]} New segments.
|
||||
*/
|
||||
makeDisconnected(begin, end) {
|
||||
return makeSegments(this, begin, end, CodePathSegment.newDisconnected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds segments into this context.
|
||||
* The added segments become the head.
|
||||
*
|
||||
* @param {CodePathSegment[]} segments - Segments to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
add(segments) {
|
||||
assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
|
||||
|
||||
this.segmentsList.push(mergeExtraSegments(this, segments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the head segments with given segments.
|
||||
* The current head segments are removed.
|
||||
*
|
||||
* @param {CodePathSegment[]} segments - Segments to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
replaceHead(segments) {
|
||||
assert(segments.length >= this.count, `${segments.length} >= ${this.count}`);
|
||||
|
||||
this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all segments of a given fork context into this context.
|
||||
*
|
||||
* @param {ForkContext} context - A fork context to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addAll(context) {
|
||||
assert(context.count === this.count);
|
||||
|
||||
const source = context.segmentsList;
|
||||
|
||||
for (let i = 0; i < source.length; ++i) {
|
||||
this.segmentsList.push(source[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all secments in this context.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
this.segmentsList = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the root fork context.
|
||||
*
|
||||
* @param {IdGenerator} idGenerator - An identifier generator for segments.
|
||||
* @returns {ForkContext} New fork context.
|
||||
*/
|
||||
static newRoot(idGenerator) {
|
||||
const context = new ForkContext(idGenerator, null, 1);
|
||||
|
||||
context.add([CodePathSegment.newRoot(idGenerator.next())]);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty fork context preceded by a given context.
|
||||
*
|
||||
* @param {ForkContext} parentContext - The parent fork context.
|
||||
* @param {boolean} forkLeavingPath - A flag which shows inside of `finally` block.
|
||||
* @returns {ForkContext} New fork context.
|
||||
*/
|
||||
static newEmpty(parentContext, forkLeavingPath) {
|
||||
return new ForkContext(
|
||||
parentContext.idGenerator,
|
||||
parentContext,
|
||||
(forkLeavingPath ? 2 : 1) * parentContext.count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ForkContext;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @fileoverview A class of identifiers generator for code path segments.
|
||||
*
|
||||
* Each rule uses the identifier of code path segments to store additional
|
||||
* information of the code path.
|
||||
*
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A generator for unique ids.
|
||||
*/
|
||||
class IdGenerator {
|
||||
|
||||
/**
|
||||
* @param {string} prefix - Optional. A prefix of generated ids.
|
||||
*/
|
||||
constructor(prefix) {
|
||||
this.prefix = String(prefix);
|
||||
this.n = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates id.
|
||||
*
|
||||
* @returns {string} A generated id.
|
||||
*/
|
||||
next() {
|
||||
this.n = 1 + this.n | 0;
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (this.n < 0) {
|
||||
this.n = 1;
|
||||
}
|
||||
|
||||
return this.prefix + this.n;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IdGenerator;
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* @fileoverview Responsible for loading config files
|
||||
* @author Seth McLaughlin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const path = require("path"),
|
||||
os = require("os"),
|
||||
ConfigOps = require("./config/config-ops"),
|
||||
ConfigFile = require("./config/config-file"),
|
||||
ConfigCache = require("./config/config-cache"),
|
||||
Plugins = require("./config/plugins"),
|
||||
FileFinder = require("./util/file-finder"),
|
||||
isResolvable = require("is-resolvable");
|
||||
|
||||
const debug = require("debug")("eslint:config");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const PERSONAL_CONFIG_DIR = os.homedir();
|
||||
const SUBCONFIG_SEP = ":";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines if any rules were explicitly passed in as options.
|
||||
* @param {Object} options The options used to create our configuration.
|
||||
* @returns {boolean} True if rules were passed in as options, false otherwise.
|
||||
* @private
|
||||
*/
|
||||
function hasRules(options) {
|
||||
return options.rules && Object.keys(options.rules).length > 0;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// API
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Configuration class
|
||||
*/
|
||||
class Config {
|
||||
|
||||
/**
|
||||
* @param {Object} providedOptions Options to be passed in
|
||||
* @param {Linter} linterContext Linter instance object
|
||||
*/
|
||||
constructor(providedOptions, linterContext) {
|
||||
const options = providedOptions || {};
|
||||
|
||||
this.linterContext = linterContext;
|
||||
this.plugins = new Plugins(linterContext.environments, linterContext.rules);
|
||||
|
||||
this.options = options;
|
||||
this.ignore = options.ignore;
|
||||
this.ignorePath = options.ignorePath;
|
||||
this.parser = options.parser;
|
||||
this.parserOptions = options.parserOptions || {};
|
||||
|
||||
this.configCache = new ConfigCache();
|
||||
|
||||
this.baseConfig = options.baseConfig
|
||||
? ConfigOps.merge({}, ConfigFile.loadObject(options.baseConfig, this))
|
||||
: { rules: {} };
|
||||
this.baseConfig.filePath = "";
|
||||
this.baseConfig.baseDirectory = this.options.cwd;
|
||||
|
||||
this.configCache.setConfig(this.baseConfig.filePath, this.baseConfig);
|
||||
this.configCache.setMergedVectorConfig(this.baseConfig.filePath, this.baseConfig);
|
||||
|
||||
this.useEslintrc = (options.useEslintrc !== false);
|
||||
|
||||
this.env = (options.envs || []).reduce((envs, name) => {
|
||||
envs[name] = true;
|
||||
return envs;
|
||||
}, {});
|
||||
|
||||
/*
|
||||
* Handle declared globals.
|
||||
* For global variable foo, handle "foo:false" and "foo:true" to set
|
||||
* whether global is writable.
|
||||
* If user declares "foo", convert to "foo:false".
|
||||
*/
|
||||
this.globals = (options.globals || []).reduce((globals, def) => {
|
||||
const parts = def.split(SUBCONFIG_SEP);
|
||||
|
||||
globals[parts[0]] = (parts.length > 1 && parts[1] === "true");
|
||||
|
||||
return globals;
|
||||
}, {});
|
||||
|
||||
this.loadSpecificConfig(options.configFile);
|
||||
|
||||
// Empty values in configs don't merge properly
|
||||
const cliConfigOptions = {
|
||||
env: this.env,
|
||||
rules: this.options.rules,
|
||||
globals: this.globals,
|
||||
parserOptions: this.parserOptions,
|
||||
plugins: this.options.plugins
|
||||
};
|
||||
|
||||
this.cliConfig = {};
|
||||
Object.keys(cliConfigOptions).forEach(configKey => {
|
||||
const value = cliConfigOptions[configKey];
|
||||
|
||||
if (value) {
|
||||
this.cliConfig[configKey] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the config options from a config specified on the command line.
|
||||
* @param {string} [config] A shareable named config or path to a config file.
|
||||
* @returns {void}
|
||||
*/
|
||||
loadSpecificConfig(config) {
|
||||
if (config) {
|
||||
debug(`Using command line config ${config}`);
|
||||
const isNamedConfig =
|
||||
isResolvable(config) ||
|
||||
isResolvable(`eslint-config-${config}`) ||
|
||||
config.charAt(0) === "@";
|
||||
|
||||
this.specificConfig = ConfigFile.load(
|
||||
isNamedConfig ? config : path.resolve(this.options.cwd, config),
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the personal config object from user's home directory.
|
||||
* @returns {Object} the personal config object (null if there is no personal config)
|
||||
* @private
|
||||
*/
|
||||
getPersonalConfig() {
|
||||
if (typeof this.personalConfig === "undefined") {
|
||||
let config;
|
||||
const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR);
|
||||
|
||||
if (filename) {
|
||||
debug("Using personal config");
|
||||
config = ConfigFile.load(filename, this);
|
||||
}
|
||||
|
||||
this.personalConfig = config || null;
|
||||
}
|
||||
|
||||
return this.personalConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a hierarchy of config objects, including the base config, all local configs from the directory tree,
|
||||
* and a config file specified on the command line, if applicable.
|
||||
* @param {string} directory a file in whose directory we start looking for a local config
|
||||
* @returns {Object[]} The config objects, in ascending order of precedence
|
||||
* @private
|
||||
*/
|
||||
getConfigHierarchy(directory) {
|
||||
debug(`Constructing config file hierarchy for ${directory}`);
|
||||
|
||||
// Step 1: Always include baseConfig
|
||||
let configs = [this.baseConfig];
|
||||
|
||||
// Step 2: Add user-specified config from .eslintrc.* and package.json files
|
||||
if (this.useEslintrc) {
|
||||
debug("Using .eslintrc and package.json files");
|
||||
configs = configs.concat(this.getLocalConfigHierarchy(directory));
|
||||
} else {
|
||||
debug("Not using .eslintrc or package.json files");
|
||||
}
|
||||
|
||||
// Step 3: Merge in command line config file
|
||||
if (this.specificConfig) {
|
||||
debug("Using command line config file");
|
||||
configs.push(this.specificConfig);
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of config objects extracted from local config files that apply to the current directory, in
|
||||
* descending order, beginning with the config that is highest in the directory tree.
|
||||
* @param {string} directory The directory to start looking in for local config files.
|
||||
* @returns {Object[]} The shallow local config objects, in ascending order of precedence (closest to the current
|
||||
* directory at the end), or an empty array if there are no local configs.
|
||||
* @private
|
||||
*/
|
||||
getLocalConfigHierarchy(directory) {
|
||||
const localConfigFiles = this.findLocalConfigFiles(directory),
|
||||
projectConfigPath = ConfigFile.getFilenameForDirectory(this.options.cwd),
|
||||
searched = [],
|
||||
configs = [];
|
||||
|
||||
for (const localConfigFile of localConfigFiles) {
|
||||
const localConfigDirectory = path.dirname(localConfigFile);
|
||||
const localConfigHierarchyCache = this.configCache.getHierarchyLocalConfigs(localConfigDirectory);
|
||||
|
||||
if (localConfigHierarchyCache) {
|
||||
const localConfigHierarchy = localConfigHierarchyCache.concat(configs);
|
||||
|
||||
this.configCache.setHierarchyLocalConfigs(searched, localConfigHierarchy);
|
||||
return localConfigHierarchy;
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't consider the personal config file in the home directory,
|
||||
* except if the home directory is the same as the current working directory
|
||||
*/
|
||||
if (localConfigDirectory === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug(`Loading ${localConfigFile}`);
|
||||
const localConfig = ConfigFile.load(localConfigFile, this);
|
||||
|
||||
// Ignore empty config files
|
||||
if (!localConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug(`Using ${localConfigFile}`);
|
||||
configs.unshift(localConfig);
|
||||
searched.push(localConfigDirectory);
|
||||
|
||||
// Stop traversing if a config is found with the root flag set
|
||||
if (localConfig.root) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!configs.length && !this.specificConfig) {
|
||||
|
||||
// Fall back on the personal config from ~/.eslintrc
|
||||
debug("Using personal config file");
|
||||
const personalConfig = this.getPersonalConfig();
|
||||
|
||||
if (personalConfig) {
|
||||
configs.unshift(personalConfig);
|
||||
} else if (!hasRules(this.options) && !this.options.baseConfig) {
|
||||
|
||||
// No config file, no manual configuration, and no rules, so error.
|
||||
const noConfigError = new Error("No ESLint configuration found.");
|
||||
|
||||
noConfigError.messageTemplate = "no-config-found";
|
||||
noConfigError.messageData = {
|
||||
directory,
|
||||
filesExamined: localConfigFiles
|
||||
};
|
||||
|
||||
throw noConfigError;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the caches for the parent directories
|
||||
this.configCache.setHierarchyLocalConfigs(searched, configs);
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vector of applicable configs and subconfigs from the hierarchy for a given file. A vector is an array of
|
||||
* entries, each of which in an object specifying a config file path and an array of override indices corresponding
|
||||
* to entries in the config file's overrides section whose glob patterns match the specified file path; e.g., the
|
||||
* vector entry { configFile: '/home/john/app/.eslintrc', matchingOverrides: [0, 2] } would indicate that the main
|
||||
* project .eslintrc file and its first and third override blocks apply to the current file.
|
||||
* @param {string} filePath The file path for which to build the hierarchy and config vector.
|
||||
* @returns {Array<Object>} config vector applicable to the specified path
|
||||
* @private
|
||||
*/
|
||||
getConfigVector(filePath) {
|
||||
const directory = filePath ? path.dirname(filePath) : this.options.cwd;
|
||||
|
||||
return this.getConfigHierarchy(directory).map(config => {
|
||||
const vectorEntry = {
|
||||
filePath: config.filePath,
|
||||
matchingOverrides: []
|
||||
};
|
||||
|
||||
if (config.overrides) {
|
||||
const relativePath = path.relative(config.baseDirectory, filePath || directory);
|
||||
|
||||
config.overrides.forEach((override, i) => {
|
||||
if (ConfigOps.pathMatchesGlobs(relativePath, override.files, override.excludedFiles)) {
|
||||
vectorEntry.matchingOverrides.push(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return vectorEntry;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds local config files from the specified directory and its parent directories.
|
||||
* @param {string} directory The directory to start searching from.
|
||||
* @returns {GeneratorFunction} The paths of local config files found.
|
||||
*/
|
||||
findLocalConfigFiles(directory) {
|
||||
if (!this.localConfigFinder) {
|
||||
this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, this.options.cwd);
|
||||
}
|
||||
|
||||
return this.localConfigFinder.findAllInDirectoryAndParents(directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the authoritative config object for the specified file path by merging the hierarchy of config objects
|
||||
* that apply to the current file, including the base config (conf/eslint-recommended), the user's personal config
|
||||
* from their homedir, all local configs from the directory tree, any specific config file passed on the command
|
||||
* line, any configuration overrides set directly on the command line, and finally the environment configs
|
||||
* (conf/environments).
|
||||
* @param {string} filePath a file in whose directory we start looking for a local config
|
||||
* @returns {Object} config object
|
||||
*/
|
||||
getConfig(filePath) {
|
||||
const vector = this.getConfigVector(filePath);
|
||||
let config = this.configCache.getMergedConfig(vector);
|
||||
|
||||
if (config) {
|
||||
debug("Using config from cache");
|
||||
return config;
|
||||
}
|
||||
|
||||
// Step 1: Merge in the filesystem configurations (base, local, and personal)
|
||||
config = ConfigOps.getConfigFromVector(vector, this.configCache);
|
||||
|
||||
// Step 2: Merge in command line configurations
|
||||
config = ConfigOps.merge(config, this.cliConfig);
|
||||
|
||||
if (this.cliConfig.plugins) {
|
||||
this.plugins.loadAll(this.cliConfig.plugins);
|
||||
}
|
||||
|
||||
/*
|
||||
* Step 3: Override parser only if it is passed explicitly through the command line
|
||||
* or if it's not defined yet (because the final object will at least have the parser key)
|
||||
*/
|
||||
if (this.parser || !config.parser) {
|
||||
config = ConfigOps.merge(config, { parser: this.parser });
|
||||
}
|
||||
|
||||
// Step 4: Apply environments to the config
|
||||
config = ConfigOps.applyEnvironments(config, this.linterContext.environments);
|
||||
|
||||
this.configCache.setMergedConfig(vector, config);
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Config;
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* @fileoverview Used for creating a suggested configuration based on project code.
|
||||
* @author Ian VanSchooten
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const lodash = require("lodash"),
|
||||
Linter = require("../linter"),
|
||||
configRule = require("./config-rule"),
|
||||
ConfigOps = require("./config-ops"),
|
||||
recConfig = require("../../conf/eslint-recommended");
|
||||
|
||||
const debug = require("debug")("eslint:autoconfig");
|
||||
const linter = new Linter();
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Data
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const MAX_CONFIG_COMBINATIONS = 17, // 16 combinations + 1 for severity only
|
||||
RECOMMENDED_CONFIG_NAME = "eslint:recommended";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Information about a rule configuration, in the context of a Registry.
|
||||
*
|
||||
* @typedef {Object} registryItem
|
||||
* @param {ruleConfig} config A valid configuration for the rule
|
||||
* @param {number} specificity The number of elements in the ruleConfig array
|
||||
* @param {number} errorCount The number of errors encountered when linting with the config
|
||||
*/
|
||||
|
||||
/**
|
||||
* This callback is used to measure execution status in a progress bar
|
||||
* @callback progressCallback
|
||||
* @param {number} The total number of times the callback will be called.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create registryItems for rules
|
||||
* @param {rulesConfig} rulesConfig Hash of rule names and arrays of ruleConfig items
|
||||
* @returns {Object} registryItems for each rule in provided rulesConfig
|
||||
*/
|
||||
function makeRegistryItems(rulesConfig) {
|
||||
return Object.keys(rulesConfig).reduce((accumulator, ruleId) => {
|
||||
accumulator[ruleId] = rulesConfig[ruleId].map(config => ({
|
||||
config,
|
||||
specificity: config.length || 1,
|
||||
errorCount: void 0
|
||||
}));
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an object in which to store rule configs and error counts
|
||||
*
|
||||
* Unless a rulesConfig is provided at construction, the registry will not contain
|
||||
* any rules, only methods. This will be useful for building up registries manually.
|
||||
*
|
||||
* Registry class
|
||||
*/
|
||||
class Registry {
|
||||
|
||||
/**
|
||||
* @param {rulesConfig} [rulesConfig] Hash of rule names and arrays of possible configurations
|
||||
*/
|
||||
constructor(rulesConfig) {
|
||||
this.rules = (rulesConfig) ? makeRegistryItems(rulesConfig) : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the registry with core rule configs.
|
||||
*
|
||||
* It will set the registry's `rule` property to an object having rule names
|
||||
* as keys and an array of registryItems as values.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
populateFromCoreRules() {
|
||||
const rulesConfig = configRule.createCoreRuleConfigs();
|
||||
|
||||
this.rules = makeRegistryItems(rulesConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates sets of rule configurations which can be used for linting
|
||||
* and initializes registry errors to zero for those configurations (side effect).
|
||||
*
|
||||
* This combines as many rules together as possible, such that the first sets
|
||||
* in the array will have the highest number of rules configured, and later sets
|
||||
* will have fewer and fewer, as not all rules have the same number of possible
|
||||
* configurations.
|
||||
*
|
||||
* The length of the returned array will be <= MAX_CONFIG_COMBINATIONS.
|
||||
*
|
||||
* @param {Object} registry The autoconfig registry
|
||||
* @returns {Object[]} "rules" configurations to use for linting
|
||||
*/
|
||||
buildRuleSets() {
|
||||
let idx = 0;
|
||||
const ruleIds = Object.keys(this.rules),
|
||||
ruleSets = [];
|
||||
|
||||
/**
|
||||
* Add a rule configuration from the registry to the ruleSets
|
||||
*
|
||||
* This is broken out into its own function so that it doesn't need to be
|
||||
* created inside of the while loop.
|
||||
*
|
||||
* @param {string} rule The ruleId to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
const addRuleToRuleSet = function(rule) {
|
||||
|
||||
/*
|
||||
* This check ensures that there is a rule configuration and that
|
||||
* it has fewer than the max combinations allowed.
|
||||
* If it has too many configs, we will only use the most basic of
|
||||
* the possible configurations.
|
||||
*/
|
||||
const hasFewCombos = (this.rules[rule].length <= MAX_CONFIG_COMBINATIONS);
|
||||
|
||||
if (this.rules[rule][idx] && (hasFewCombos || this.rules[rule][idx].specificity <= 2)) {
|
||||
|
||||
/*
|
||||
* If the rule has too many possible combinations, only take
|
||||
* simple ones, avoiding objects.
|
||||
*/
|
||||
if (!hasFewCombos && typeof this.rules[rule][idx].config[1] === "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
ruleSets[idx] = ruleSets[idx] || {};
|
||||
ruleSets[idx][rule] = this.rules[rule][idx].config;
|
||||
|
||||
/*
|
||||
* Initialize errorCount to zero, since this is a config which
|
||||
* will be linted.
|
||||
*/
|
||||
this.rules[rule][idx].errorCount = 0;
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
while (ruleSets.length === idx) {
|
||||
ruleIds.forEach(addRuleToRuleSet);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
return ruleSets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all items from the registry with a non-zero number of errors
|
||||
*
|
||||
* Note: this also removes rule configurations which were not linted
|
||||
* (meaning, they have an undefined errorCount).
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
stripFailingConfigs() {
|
||||
const ruleIds = Object.keys(this.rules),
|
||||
newRegistry = new Registry();
|
||||
|
||||
newRegistry.rules = Object.assign({}, this.rules);
|
||||
ruleIds.forEach(ruleId => {
|
||||
const errorFreeItems = newRegistry.rules[ruleId].filter(registryItem => (registryItem.errorCount === 0));
|
||||
|
||||
if (errorFreeItems.length > 0) {
|
||||
newRegistry.rules[ruleId] = errorFreeItems;
|
||||
} else {
|
||||
delete newRegistry.rules[ruleId];
|
||||
}
|
||||
});
|
||||
|
||||
return newRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes rule configurations which were not included in a ruleSet
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
stripExtraConfigs() {
|
||||
const ruleIds = Object.keys(this.rules),
|
||||
newRegistry = new Registry();
|
||||
|
||||
newRegistry.rules = Object.assign({}, this.rules);
|
||||
ruleIds.forEach(ruleId => {
|
||||
newRegistry.rules[ruleId] = newRegistry.rules[ruleId].filter(registryItem => (typeof registryItem.errorCount !== "undefined"));
|
||||
});
|
||||
|
||||
return newRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a registry of rules which had no error-free configs.
|
||||
* The new registry is intended to be analyzed to determine whether its rules
|
||||
* should be disabled or set to warning.
|
||||
*
|
||||
* @returns {Registry} A registry of failing rules.
|
||||
*/
|
||||
getFailingRulesRegistry() {
|
||||
const ruleIds = Object.keys(this.rules),
|
||||
failingRegistry = new Registry();
|
||||
|
||||
ruleIds.forEach(ruleId => {
|
||||
const failingConfigs = this.rules[ruleId].filter(registryItem => (registryItem.errorCount > 0));
|
||||
|
||||
if (failingConfigs && failingConfigs.length === this.rules[ruleId].length) {
|
||||
failingRegistry.rules[ruleId] = failingConfigs;
|
||||
}
|
||||
});
|
||||
|
||||
return failingRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an eslint config for any rules which only have one configuration
|
||||
* in the registry.
|
||||
*
|
||||
* @returns {Object} An eslint config with rules section populated
|
||||
*/
|
||||
createConfig() {
|
||||
const ruleIds = Object.keys(this.rules),
|
||||
config = { rules: {} };
|
||||
|
||||
ruleIds.forEach(ruleId => {
|
||||
if (this.rules[ruleId].length === 1) {
|
||||
config.rules[ruleId] = this.rules[ruleId][0].config;
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a cloned registry containing only configs with a desired specificity
|
||||
*
|
||||
* @param {number} specificity Only keep configs with this specificity
|
||||
* @returns {Registry} A registry of rules
|
||||
*/
|
||||
filterBySpecificity(specificity) {
|
||||
const ruleIds = Object.keys(this.rules),
|
||||
newRegistry = new Registry();
|
||||
|
||||
newRegistry.rules = Object.assign({}, this.rules);
|
||||
ruleIds.forEach(ruleId => {
|
||||
newRegistry.rules[ruleId] = this.rules[ruleId].filter(registryItem => (registryItem.specificity === specificity));
|
||||
});
|
||||
|
||||
return newRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lint SourceCodes against all configurations in the registry, and record results
|
||||
*
|
||||
* @param {Object[]} sourceCodes SourceCode objects for each filename
|
||||
* @param {Object} config ESLint config object
|
||||
* @param {progressCallback} [cb] Optional callback for reporting execution status
|
||||
* @returns {Registry} New registry with errorCount populated
|
||||
*/
|
||||
lintSourceCode(sourceCodes, config, cb) {
|
||||
let lintedRegistry = new Registry();
|
||||
|
||||
lintedRegistry.rules = Object.assign({}, this.rules);
|
||||
|
||||
const ruleSets = lintedRegistry.buildRuleSets();
|
||||
|
||||
lintedRegistry = lintedRegistry.stripExtraConfigs();
|
||||
|
||||
debug("Linting with all possible rule combinations");
|
||||
|
||||
const filenames = Object.keys(sourceCodes);
|
||||
const totalFilesLinting = filenames.length * ruleSets.length;
|
||||
|
||||
filenames.forEach(filename => {
|
||||
debug(`Linting file: ${filename}`);
|
||||
|
||||
let ruleSetIdx = 0;
|
||||
|
||||
ruleSets.forEach(ruleSet => {
|
||||
const lintConfig = Object.assign({}, config, { rules: ruleSet });
|
||||
const lintResults = linter.verify(sourceCodes[filename], lintConfig);
|
||||
|
||||
lintResults.forEach(result => {
|
||||
|
||||
/*
|
||||
* It is possible that the error is from a configuration comment
|
||||
* in a linted file, in which case there may not be a config
|
||||
* set in this ruleSetIdx.
|
||||
* (https://github.com/eslint/eslint/issues/5992)
|
||||
* (https://github.com/eslint/eslint/issues/7860)
|
||||
*/
|
||||
if (
|
||||
lintedRegistry.rules[result.ruleId] &&
|
||||
lintedRegistry.rules[result.ruleId][ruleSetIdx]
|
||||
) {
|
||||
lintedRegistry.rules[result.ruleId][ruleSetIdx].errorCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
ruleSetIdx += 1;
|
||||
|
||||
if (cb) {
|
||||
cb(totalFilesLinting); // eslint-disable-line callback-return
|
||||
}
|
||||
});
|
||||
|
||||
// Deallocate for GC
|
||||
sourceCodes[filename] = null;
|
||||
});
|
||||
|
||||
return lintedRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract rule configuration into eslint:recommended where possible.
|
||||
*
|
||||
* This will return a new config with `"extends": "eslint:recommended"` and
|
||||
* only the rules which have configurations different from the recommended config.
|
||||
*
|
||||
* @param {Object} config config object
|
||||
* @returns {Object} config object using `"extends": "eslint:recommended"`
|
||||
*/
|
||||
function extendFromRecommended(config) {
|
||||
const newConfig = Object.assign({}, config);
|
||||
|
||||
ConfigOps.normalizeToStrings(newConfig);
|
||||
|
||||
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])) {
|
||||
delete newConfig.rules[ruleId];
|
||||
}
|
||||
});
|
||||
newConfig.extends = RECOMMENDED_CONFIG_NAME;
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
Registry,
|
||||
extendFromRecommended
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @fileoverview Responsible for caching config files
|
||||
* @author Sylvan Mably
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get a string hash for a config vector
|
||||
* @param {Array<Object>} vector config vector to hash
|
||||
* @returns {string} hash of the vector values
|
||||
* @private
|
||||
*/
|
||||
function hash(vector) {
|
||||
return JSON.stringify(vector);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// API
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Configuration caching class
|
||||
*/
|
||||
module.exports = class ConfigCache {
|
||||
|
||||
constructor() {
|
||||
this.configFullNameCache = new Map();
|
||||
this.localHierarchyCache = new Map();
|
||||
this.mergedVectorCache = new Map();
|
||||
this.mergedCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a config object from the cache for the specified config file path.
|
||||
* @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
|
||||
* or the absolute path to a config file. This should uniquely identify a config.
|
||||
* @returns {Object|null} config object, if found in the cache, otherwise null
|
||||
* @private
|
||||
*/
|
||||
getConfig(configFullName) {
|
||||
return this.configFullNameCache.get(configFullName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a config object in the cache for the specified config file path.
|
||||
* @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
|
||||
* or the absolute path to a config file. This should uniquely identify a config.
|
||||
* @param {Object} config the config object to add to the cache
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
setConfig(configFullName, config) {
|
||||
this.configFullNameCache.set(configFullName, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of hierarchy-local config objects that apply to the specified directory.
|
||||
* @param {string} directory the path to the directory
|
||||
* @returns {Object[]|null} a list of config objects, if found in the cache, otherwise null
|
||||
* @private
|
||||
*/
|
||||
getHierarchyLocalConfigs(directory) {
|
||||
return this.localHierarchyCache.get(directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each of the supplied parent directories, sets the list of config objects for that directory to the
|
||||
* appropriate subset of the supplied parent config objects.
|
||||
* @param {string[]} parentDirectories a list of parent directories to add to the config cache
|
||||
* @param {Object[]} parentConfigs a list of config objects that apply to the lowest directory in parentDirectories
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
setHierarchyLocalConfigs(parentDirectories, parentConfigs) {
|
||||
parentDirectories.forEach((localConfigDirectory, i) => {
|
||||
const directoryParentConfigs = parentConfigs.slice(0, parentConfigs.length - i);
|
||||
|
||||
this.localHierarchyCache.set(localConfigDirectory, directoryParentConfigs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a merged config object corresponding to the supplied vector.
|
||||
* @param {Array<Object>} vector the vector to find a merged config for
|
||||
* @returns {Object|null} a merged config object, if found in the cache, otherwise null
|
||||
* @private
|
||||
*/
|
||||
getMergedVectorConfig(vector) {
|
||||
return this.mergedVectorCache.get(hash(vector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a merged config object in the cache for the supplied vector.
|
||||
* @param {Array<Object>} vector the vector to save a merged config for
|
||||
* @param {Object} config the merged config object to add to the cache
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
setMergedVectorConfig(vector, config) {
|
||||
this.mergedVectorCache.set(hash(vector), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a merged config object corresponding to the supplied vector, including configuration options from outside
|
||||
* the vector.
|
||||
* @param {Array<Object>} vector the vector to find a merged config for
|
||||
* @returns {Object|null} a merged config object, if found in the cache, otherwise null
|
||||
* @private
|
||||
*/
|
||||
getMergedConfig(vector) {
|
||||
return this.mergedCache.get(hash(vector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a merged config object in the cache for the supplied vector, including configuration options from outside
|
||||
* the vector.
|
||||
* @param {Array<Object>} vector the vector to save a merged config for
|
||||
* @param {Object} config the merged config object to add to the cache
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
setMergedConfig(vector, config) {
|
||||
this.mergedCache.set(hash(vector), config);
|
||||
}
|
||||
};
|
||||
+617
@@ -0,0 +1,617 @@
|
||||
/**
|
||||
* @fileoverview Helper to locate and load configuration files.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const fs = require("fs"),
|
||||
path = require("path"),
|
||||
ConfigOps = require("./config-ops"),
|
||||
validator = require("./config-validator"),
|
||||
ModuleResolver = require("../util/module-resolver"),
|
||||
naming = require("../util/naming"),
|
||||
pathIsInside = require("path-is-inside"),
|
||||
stripComments = require("strip-json-comments"),
|
||||
stringify = require("json-stable-stringify-without-jsonify"),
|
||||
requireUncached = require("require-uncached");
|
||||
|
||||
const debug = require("debug")("eslint:config-file");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines sort order for object keys for json-stable-stringify
|
||||
*
|
||||
* see: https://github.com/samn/json-stable-stringify#cmp
|
||||
*
|
||||
* @param {Object} a The first comparison object ({key: akey, value: avalue})
|
||||
* @param {Object} b The second comparison object ({key: bkey, value: bvalue})
|
||||
* @returns {number} 1 or -1, used in stringify cmp method
|
||||
*/
|
||||
function sortByKey(a, b) {
|
||||
return a.key > b.key ? 1 : -1;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const CONFIG_FILES = [
|
||||
".eslintrc.js",
|
||||
".eslintrc.yaml",
|
||||
".eslintrc.yml",
|
||||
".eslintrc.json",
|
||||
".eslintrc",
|
||||
"package.json"
|
||||
];
|
||||
|
||||
const resolver = new ModuleResolver();
|
||||
|
||||
/**
|
||||
* Convenience wrapper for synchronously reading file contents.
|
||||
* @param {string} filePath The filename to read.
|
||||
* @returns {string} The file contents, with the BOM removed.
|
||||
* @private
|
||||
*/
|
||||
function readFile(filePath) {
|
||||
return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given string represents a filepath or not using the same
|
||||
* conventions as require(), meaning that the first character must be nonalphanumeric
|
||||
* and not the @ sign which is used for scoped packages to be considered a file path.
|
||||
* @param {string} filePath The string to check.
|
||||
* @returns {boolean} True if it's a filepath, false if not.
|
||||
* @private
|
||||
*/
|
||||
function isFilePath(filePath) {
|
||||
return path.isAbsolute(filePath) || !/\w|@/.test(filePath.charAt(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a YAML configuration from a file.
|
||||
* @param {string} filePath The filename to load.
|
||||
* @returns {Object} The configuration object from the file.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
* @private
|
||||
*/
|
||||
function loadYAMLConfigFile(filePath) {
|
||||
debug(`Loading YAML config file: ${filePath}`);
|
||||
|
||||
// lazy load YAML to improve performance when not used
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
try {
|
||||
|
||||
// empty YAML file can be null, so always use
|
||||
return yaml.safeLoad(readFile(filePath)) || {};
|
||||
} catch (e) {
|
||||
debug(`Error reading YAML file: ${filePath}`);
|
||||
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a JSON configuration from a file.
|
||||
* @param {string} filePath The filename to load.
|
||||
* @returns {Object} The configuration object from the file.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
* @private
|
||||
*/
|
||||
function loadJSONConfigFile(filePath) {
|
||||
debug(`Loading JSON config file: ${filePath}`);
|
||||
|
||||
try {
|
||||
return JSON.parse(stripComments(readFile(filePath)));
|
||||
} catch (e) {
|
||||
debug(`Error reading JSON file: ${filePath}`);
|
||||
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
||||
e.messageTemplate = "failed-to-read-json";
|
||||
e.messageData = {
|
||||
path: filePath,
|
||||
message: e.message
|
||||
};
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a legacy (.eslintrc) configuration from a file.
|
||||
* @param {string} filePath The filename to load.
|
||||
* @returns {Object} The configuration object from the file.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
* @private
|
||||
*/
|
||||
function loadLegacyConfigFile(filePath) {
|
||||
debug(`Loading config file: ${filePath}`);
|
||||
|
||||
// lazy load YAML to improve performance when not used
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
try {
|
||||
return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {};
|
||||
} catch (e) {
|
||||
debug(`Error reading YAML file: ${filePath}`);
|
||||
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a JavaScript configuration from a file.
|
||||
* @param {string} filePath The filename to load.
|
||||
* @returns {Object} The configuration object from the file.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
* @private
|
||||
*/
|
||||
function loadJSConfigFile(filePath) {
|
||||
debug(`Loading JS config file: ${filePath}`);
|
||||
try {
|
||||
return requireUncached(filePath);
|
||||
} catch (e) {
|
||||
debug(`Error reading JavaScript file: ${filePath}`);
|
||||
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a configuration from a package.json file.
|
||||
* @param {string} filePath The filename to load.
|
||||
* @returns {Object} The configuration object from the file.
|
||||
* @throws {Error} If the file cannot be read.
|
||||
* @private
|
||||
*/
|
||||
function loadPackageJSONConfigFile(filePath) {
|
||||
debug(`Loading package.json config file: ${filePath}`);
|
||||
try {
|
||||
return loadJSONConfigFile(filePath).eslintConfig || null;
|
||||
} catch (e) {
|
||||
debug(`Error reading package.json file: ${filePath}`);
|
||||
e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error to notify about a missing config to extend from.
|
||||
* @param {string} configName The name of the missing config.
|
||||
* @returns {Error} The error object to throw
|
||||
* @private
|
||||
*/
|
||||
function configMissingError(configName) {
|
||||
const error = new Error(`Failed to load config "${configName}" to extend from.`);
|
||||
|
||||
error.messageTemplate = "extend-config-missing";
|
||||
error.messageData = {
|
||||
configName
|
||||
};
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a configuration file regardless of the source. Inspects the file path
|
||||
* to determine the correctly way to load the config file.
|
||||
* @param {Object} file The path to the configuration.
|
||||
* @returns {Object} The configuration information.
|
||||
* @private
|
||||
*/
|
||||
function loadConfigFile(file) {
|
||||
const filePath = file.filePath;
|
||||
let config;
|
||||
|
||||
switch (path.extname(filePath)) {
|
||||
case ".js":
|
||||
config = loadJSConfigFile(filePath);
|
||||
if (file.configName) {
|
||||
config = config.configs[file.configName];
|
||||
if (!config) {
|
||||
throw configMissingError(file.configFullName);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ".json":
|
||||
if (path.basename(filePath) === "package.json") {
|
||||
config = loadPackageJSONConfigFile(filePath);
|
||||
if (config === null) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
config = loadJSONConfigFile(filePath);
|
||||
}
|
||||
break;
|
||||
|
||||
case ".yaml":
|
||||
case ".yml":
|
||||
config = loadYAMLConfigFile(filePath);
|
||||
break;
|
||||
|
||||
default:
|
||||
config = loadLegacyConfigFile(filePath);
|
||||
}
|
||||
|
||||
return ConfigOps.merge(ConfigOps.createEmptyConfig(), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a configuration file in JSON format.
|
||||
* @param {Object} config The configuration object to write.
|
||||
* @param {string} filePath The filename to write to.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function writeJSONConfigFile(config, filePath) {
|
||||
debug(`Writing JSON config file: ${filePath}`);
|
||||
|
||||
const content = stringify(config, { cmp: sortByKey, space: 4 });
|
||||
|
||||
fs.writeFileSync(filePath, content, "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a configuration file in YAML format.
|
||||
* @param {Object} config The configuration object to write.
|
||||
* @param {string} filePath The filename to write to.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function writeYAMLConfigFile(config, filePath) {
|
||||
debug(`Writing YAML config file: ${filePath}`);
|
||||
|
||||
// lazy load YAML to improve performance when not used
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const content = yaml.safeDump(config, { sortKeys: true });
|
||||
|
||||
fs.writeFileSync(filePath, content, "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a configuration file in JavaScript format.
|
||||
* @param {Object} config The configuration object to write.
|
||||
* @param {string} filePath The filename to write to.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function writeJSConfigFile(config, filePath) {
|
||||
debug(`Writing JS config file: ${filePath}`);
|
||||
|
||||
const content = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`;
|
||||
|
||||
fs.writeFileSync(filePath, content, "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a configuration file.
|
||||
* @param {Object} config The configuration object to write.
|
||||
* @param {string} filePath The filename to write to.
|
||||
* @returns {void}
|
||||
* @throws {Error} When an unknown file type is specified.
|
||||
* @private
|
||||
*/
|
||||
function write(config, filePath) {
|
||||
switch (path.extname(filePath)) {
|
||||
case ".js":
|
||||
writeJSConfigFile(config, filePath);
|
||||
break;
|
||||
|
||||
case ".json":
|
||||
writeJSONConfigFile(config, filePath);
|
||||
break;
|
||||
|
||||
case ".yaml":
|
||||
case ".yml":
|
||||
writeYAMLConfigFile(config, filePath);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Can't write to unknown file type.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the base directory for node packages referenced in a config file.
|
||||
* This does not include node_modules in the path so it can be used for all
|
||||
* references relative to a config file.
|
||||
* @param {string} configFilePath The config file referencing the file.
|
||||
* @returns {string} The base directory for the file path.
|
||||
* @private
|
||||
*/
|
||||
function getBaseDir(configFilePath) {
|
||||
|
||||
// calculates the path of the project including ESLint as dependency
|
||||
const projectPath = path.resolve(__dirname, "../../../");
|
||||
|
||||
if (configFilePath && pathIsInside(configFilePath, projectPath)) {
|
||||
|
||||
// be careful of https://github.com/substack/node-resolve/issues/78
|
||||
return path.join(path.resolve(configFilePath));
|
||||
}
|
||||
|
||||
/*
|
||||
* default to ESLint project path since it's unlikely that plugins will be
|
||||
* in this directory
|
||||
*/
|
||||
return path.join(projectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the lookup path, including node_modules, for package
|
||||
* references relative to a config file.
|
||||
* @param {string} configFilePath The config file referencing the file.
|
||||
* @returns {string} The lookup path for the file path.
|
||||
* @private
|
||||
*/
|
||||
function getLookupPath(configFilePath) {
|
||||
const basedir = getBaseDir(configFilePath);
|
||||
|
||||
return path.join(basedir, "node_modules");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a eslint core config path
|
||||
* @param {string} name The eslint config name.
|
||||
* @returns {string} The resolved path of the config.
|
||||
* @private
|
||||
*/
|
||||
function getEslintCoreConfigPath(name) {
|
||||
if (name === "eslint:recommended") {
|
||||
|
||||
/*
|
||||
* Add an explicit substitution for eslint:recommended to
|
||||
* conf/eslint-recommended.js.
|
||||
*/
|
||||
return path.resolve(__dirname, "../../conf/eslint-recommended.js");
|
||||
}
|
||||
|
||||
if (name === "eslint:all") {
|
||||
|
||||
/*
|
||||
* Add an explicit substitution for eslint:all to conf/eslint-all.js
|
||||
*/
|
||||
return path.resolve(__dirname, "../../conf/eslint-all.js");
|
||||
}
|
||||
|
||||
throw configMissingError(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies values from the "extends" field in a configuration file.
|
||||
* @param {Object} config The configuration information.
|
||||
* @param {Config} configContext Plugin context for the config instance
|
||||
* @param {string} filePath The file path from which the configuration information
|
||||
* was loaded.
|
||||
* @param {string} [relativeTo] The path to resolve relative to.
|
||||
* @returns {Object} A new configuration object with all of the "extends" fields
|
||||
* loaded and merged.
|
||||
* @private
|
||||
*/
|
||||
function applyExtends(config, configContext, filePath, relativeTo) {
|
||||
let configExtends = config.extends;
|
||||
|
||||
// normalize into an array for easier handling
|
||||
if (!Array.isArray(config.extends)) {
|
||||
configExtends = [config.extends];
|
||||
}
|
||||
|
||||
// Make the last element in an array take the highest precedence
|
||||
return configExtends.reduceRight((previousValue, parentPath) => {
|
||||
try {
|
||||
let extensionPath;
|
||||
|
||||
if (parentPath.startsWith("eslint:")) {
|
||||
extensionPath = getEslintCoreConfigPath(parentPath);
|
||||
} else if (isFilePath(parentPath)) {
|
||||
|
||||
/*
|
||||
* If the `extends` path is relative, use the directory of the current configuration
|
||||
* file as the reference point. Otherwise, use as-is.
|
||||
*/
|
||||
extensionPath = (path.isAbsolute(parentPath)
|
||||
? parentPath
|
||||
: path.join(relativeTo || path.dirname(filePath), parentPath)
|
||||
);
|
||||
} else {
|
||||
extensionPath = parentPath;
|
||||
}
|
||||
debug(`Loading ${extensionPath}`);
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
return ConfigOps.merge(load(extensionPath, configContext, relativeTo), previousValue);
|
||||
} catch (e) {
|
||||
|
||||
/*
|
||||
* If the file referenced by `extends` failed to load, add the path
|
||||
* to the configuration file that referenced it to the error
|
||||
* message so the user is able to see where it was referenced from,
|
||||
* then re-throw.
|
||||
*/
|
||||
e.message += `\nReferenced from: ${filePath}`;
|
||||
throw e;
|
||||
}
|
||||
|
||||
}, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a configuration file path into the fully-formed path, whether filename
|
||||
* or package name.
|
||||
* @param {string} filePath The filepath to resolve.
|
||||
* @param {string} [relativeTo] The path to resolve relative to.
|
||||
* @returns {Object} An object containing 3 properties:
|
||||
* - 'filePath' (required) the resolved path that can be used directly to load the configuration.
|
||||
* - 'configName' the name of the configuration inside the plugin.
|
||||
* - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'),
|
||||
* or the absolute path to a config file. This should uniquely identify a config.
|
||||
* @private
|
||||
*/
|
||||
function resolve(filePath, relativeTo) {
|
||||
if (isFilePath(filePath)) {
|
||||
const fullPath = path.resolve(relativeTo || "", filePath);
|
||||
|
||||
return { filePath: fullPath, configFullName: fullPath };
|
||||
}
|
||||
let normalizedPackageName;
|
||||
|
||||
if (filePath.startsWith("plugin:")) {
|
||||
const configFullName = filePath;
|
||||
const pluginName = filePath.slice(7, filePath.lastIndexOf("/"));
|
||||
const configName = filePath.slice(filePath.lastIndexOf("/") + 1);
|
||||
|
||||
normalizedPackageName = naming.normalizePackageName(pluginName, "eslint-plugin");
|
||||
debug(`Attempting to resolve ${normalizedPackageName}`);
|
||||
|
||||
return {
|
||||
filePath: require.resolve(normalizedPackageName),
|
||||
configName,
|
||||
configFullName
|
||||
};
|
||||
}
|
||||
normalizedPackageName = naming.normalizePackageName(filePath, "eslint-config");
|
||||
debug(`Attempting to resolve ${normalizedPackageName}`);
|
||||
|
||||
return {
|
||||
filePath: resolver.resolve(normalizedPackageName, getLookupPath(relativeTo)),
|
||||
configFullName: filePath
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a configuration file from the given file path.
|
||||
* @param {Object} resolvedPath The value from calling resolve() on a filename or package name.
|
||||
* @param {Config} configContext Plugins context
|
||||
* @returns {Object} The configuration information.
|
||||
*/
|
||||
function loadFromDisk(resolvedPath, configContext) {
|
||||
const dirname = path.dirname(resolvedPath.filePath),
|
||||
lookupPath = getLookupPath(dirname);
|
||||
let config = loadConfigFile(resolvedPath);
|
||||
|
||||
if (config) {
|
||||
|
||||
// ensure plugins are properly loaded first
|
||||
if (config.plugins) {
|
||||
configContext.plugins.loadAll(config.plugins);
|
||||
}
|
||||
|
||||
// include full path of parser if present
|
||||
if (config.parser) {
|
||||
if (isFilePath(config.parser)) {
|
||||
config.parser = path.resolve(dirname || "", config.parser);
|
||||
} else {
|
||||
config.parser = resolver.resolve(config.parser, lookupPath);
|
||||
}
|
||||
}
|
||||
|
||||
const ruleMap = configContext.linterContext.getRules();
|
||||
|
||||
// validate the configuration before continuing
|
||||
validator.validate(config, resolvedPath.configFullName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments);
|
||||
|
||||
/*
|
||||
* If an `extends` property is defined, it represents a configuration file to use as
|
||||
* a "parent". Load the referenced file and merge the configuration recursively.
|
||||
*/
|
||||
if (config.extends) {
|
||||
config = applyExtends(config, configContext, resolvedPath.filePath, dirname);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a config object, applying extends if present.
|
||||
* @param {Object} configObject a config object to load
|
||||
* @param {Config} configContext Context for the config instance
|
||||
* @returns {Object} the config object with extends applied if present, or the passed config if not
|
||||
* @private
|
||||
*/
|
||||
function loadObject(configObject, configContext) {
|
||||
return configObject.extends ? applyExtends(configObject, configContext, "") : configObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet
|
||||
* cached.
|
||||
* @param {string} filePath the path to the config file
|
||||
* @param {Config} configContext Context for the config instance
|
||||
* @param {string} [relativeTo] The path to resolve relative to.
|
||||
* @returns {Object} the parsed config object (empty object if there was a parse error)
|
||||
* @private
|
||||
*/
|
||||
function load(filePath, configContext, relativeTo) {
|
||||
const resolvedPath = resolve(filePath, relativeTo);
|
||||
|
||||
const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName);
|
||||
|
||||
if (cachedConfig) {
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
const config = loadFromDisk(resolvedPath, configContext);
|
||||
|
||||
if (config) {
|
||||
config.filePath = resolvedPath.filePath;
|
||||
config.baseDirectory = path.dirname(resolvedPath.filePath);
|
||||
configContext.configCache.setConfig(resolvedPath.configFullName, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given filename points to a file
|
||||
* @param {string} filename A path to a file
|
||||
* @returns {boolean} `true` if a file exists at the given location
|
||||
*/
|
||||
function isExistingFile(filename) {
|
||||
try {
|
||||
return fs.statSync(filename).isFile();
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
|
||||
getBaseDir,
|
||||
getLookupPath,
|
||||
load,
|
||||
loadObject,
|
||||
resolve,
|
||||
write,
|
||||
applyExtends,
|
||||
CONFIG_FILES,
|
||||
|
||||
/**
|
||||
* Retrieves the configuration filename for a given directory. It loops over all
|
||||
* of the valid configuration filenames in order to find the first one that exists.
|
||||
* @param {string} directory The directory to check for a config file.
|
||||
* @returns {?string} The filename of the configuration file for the directory
|
||||
* or null if there is no configuration file in the directory.
|
||||
*/
|
||||
getFilenameForDirectory(directory) {
|
||||
return CONFIG_FILES.map(filename => path.join(directory, filename)).find(isExistingFile) || null;
|
||||
}
|
||||
};
|
||||
+647
@@ -0,0 +1,647 @@
|
||||
/**
|
||||
* @fileoverview Config initialization wizard.
|
||||
* @author Ilya Volodin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const util = require("util"),
|
||||
inquirer = require("inquirer"),
|
||||
ProgressBar = require("progress"),
|
||||
semver = require("semver"),
|
||||
autoconfig = require("./autoconfig.js"),
|
||||
ConfigFile = require("./config-file"),
|
||||
ConfigOps = require("./config-ops"),
|
||||
getSourceCodeOfFiles = require("../util/source-code-utils").getSourceCodeOfFiles,
|
||||
ModuleResolver = require("../util/module-resolver"),
|
||||
npmUtils = require("../util/npm-utils"),
|
||||
recConfig = require("../../conf/eslint-recommended"),
|
||||
log = require("../util/logging");
|
||||
|
||||
const debug = require("debug")("eslint:config-initializer");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/* istanbul ignore next: hard to test fs function */
|
||||
/**
|
||||
* Create .eslintrc file in the current working directory
|
||||
* @param {Object} config object that contains user's answers
|
||||
* @param {string} format The file format to write to.
|
||||
* @returns {void}
|
||||
*/
|
||||
function writeFile(config, format) {
|
||||
|
||||
// default is .js
|
||||
let extname = ".js";
|
||||
|
||||
if (format === "YAML") {
|
||||
extname = ".yml";
|
||||
} else if (format === "JSON") {
|
||||
extname = ".json";
|
||||
}
|
||||
|
||||
const installedESLint = config.installedESLint;
|
||||
|
||||
delete config.installedESLint;
|
||||
|
||||
ConfigFile.write(config, `./.eslintrc${extname}`);
|
||||
log.info(`Successfully created .eslintrc${extname} file in ${process.cwd()}`);
|
||||
|
||||
if (installedESLint) {
|
||||
log.info("ESLint was installed locally. We recommend using this local copy instead of your globally-installed copy.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the peer dependencies of the given module.
|
||||
* This adds the gotten value to cache at the first time, then reuses it.
|
||||
* In a process, this function is called twice, but `npmUtils.fetchPeerDependencies` needs to access network which is relatively slow.
|
||||
* @param {string} moduleName The module name to get.
|
||||
* @returns {Object} The peer dependencies of the given module.
|
||||
* This object is the object of `peerDependencies` field of `package.json`.
|
||||
* Returns null if npm was not found.
|
||||
*/
|
||||
function getPeerDependencies(moduleName) {
|
||||
let result = getPeerDependencies.cache.get(moduleName);
|
||||
|
||||
if (!result) {
|
||||
log.info(`Checking peerDependencies of ${moduleName}`);
|
||||
|
||||
result = npmUtils.fetchPeerDependencies(moduleName);
|
||||
getPeerDependencies.cache.set(moduleName, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
getPeerDependencies.cache = new Map();
|
||||
|
||||
/**
|
||||
* Return necessary plugins, configs, parsers, etc. based on the config
|
||||
* @param {Object} config config object
|
||||
* @param {boolean} [installESLint=true] If `false` is given, it does not install eslint.
|
||||
* @returns {string[]} An array of modules to be installed.
|
||||
*/
|
||||
function getModulesList(config, installESLint) {
|
||||
const modules = {};
|
||||
|
||||
// Create a list of modules which should be installed based on config
|
||||
if (config.plugins) {
|
||||
for (const plugin of config.plugins) {
|
||||
modules[`eslint-plugin-${plugin}`] = "latest";
|
||||
}
|
||||
}
|
||||
if (config.extends && config.extends.indexOf("eslint:") === -1) {
|
||||
const moduleName = `eslint-config-${config.extends}`;
|
||||
|
||||
modules[moduleName] = "latest";
|
||||
Object.assign(
|
||||
modules,
|
||||
getPeerDependencies(`${moduleName}@latest`)
|
||||
);
|
||||
}
|
||||
|
||||
if (installESLint === false) {
|
||||
delete modules.eslint;
|
||||
} else {
|
||||
const installStatus = npmUtils.checkDevDeps(["eslint"]);
|
||||
|
||||
// Mark to show messages if it's new installation of eslint.
|
||||
if (installStatus.eslint === false) {
|
||||
log.info("Local ESLint installation not found.");
|
||||
modules.eslint = modules.eslint || "latest";
|
||||
config.installedESLint = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(modules).map(name => `${name}@${modules[name]}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the `rules` of a config by examining a user's source code
|
||||
*
|
||||
* Note: This clones the config object and returns a new config to avoid mutating
|
||||
* the original config parameter.
|
||||
*
|
||||
* @param {Object} answers answers received from inquirer
|
||||
* @param {Object} config config object
|
||||
* @returns {Object} config object with configured rules
|
||||
*/
|
||||
function configureRules(answers, config) {
|
||||
const BAR_TOTAL = 20,
|
||||
BAR_SOURCE_CODE_TOTAL = 4,
|
||||
newConfig = Object.assign({}, config),
|
||||
disabledConfigs = {};
|
||||
let sourceCodes,
|
||||
registry;
|
||||
|
||||
// Set up a progress bar, as this process can take a long time
|
||||
const bar = new ProgressBar("Determining Config: :percent [:bar] :elapseds elapsed, eta :etas ", {
|
||||
width: 30,
|
||||
total: BAR_TOTAL
|
||||
});
|
||||
|
||||
bar.tick(0); // Shows the progress bar
|
||||
|
||||
// Get the SourceCode of all chosen files
|
||||
const patterns = answers.patterns.split(/[\s]+/);
|
||||
|
||||
try {
|
||||
sourceCodes = getSourceCodeOfFiles(patterns, { baseConfig: newConfig, useEslintrc: false }, total => {
|
||||
bar.tick((BAR_SOURCE_CODE_TOTAL / total));
|
||||
});
|
||||
} catch (e) {
|
||||
log.info("\n");
|
||||
throw e;
|
||||
}
|
||||
const fileQty = Object.keys(sourceCodes).length;
|
||||
|
||||
if (fileQty === 0) {
|
||||
log.info("\n");
|
||||
throw new Error("Automatic Configuration failed. No files were able to be parsed.");
|
||||
}
|
||||
|
||||
// Create a registry of rule configs
|
||||
registry = new autoconfig.Registry();
|
||||
registry.populateFromCoreRules();
|
||||
|
||||
// Lint all files with each rule config in the registry
|
||||
registry = registry.lintSourceCode(sourceCodes, newConfig, total => {
|
||||
bar.tick((BAR_TOTAL - BAR_SOURCE_CODE_TOTAL) / total); // Subtract out ticks used at beginning
|
||||
});
|
||||
debug(`\nRegistry: ${util.inspect(registry.rules, { depth: null })}`);
|
||||
|
||||
// Create a list of recommended rules, because we don't want to disable them
|
||||
const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
|
||||
|
||||
// Find and disable rules which had no error-free configuration
|
||||
const failingRegistry = registry.getFailingRulesRegistry();
|
||||
|
||||
Object.keys(failingRegistry.rules).forEach(ruleId => {
|
||||
|
||||
// If the rule is recommended, set it to error, otherwise disable it
|
||||
disabledConfigs[ruleId] = (recRules.indexOf(ruleId) !== -1) ? 2 : 0;
|
||||
});
|
||||
|
||||
// Now that we know which rules to disable, strip out configs with errors
|
||||
registry = registry.stripFailingConfigs();
|
||||
|
||||
/*
|
||||
* If there is only one config that results in no errors for a rule, we should use it.
|
||||
* createConfig will only add rules that have one configuration in the registry.
|
||||
*/
|
||||
const singleConfigs = registry.createConfig().rules;
|
||||
|
||||
/*
|
||||
* The "sweet spot" for number of options in a config seems to be two (severity plus one option).
|
||||
* Very often, a third option (usually an object) is available to address
|
||||
* edge cases, exceptions, or unique situations. We will prefer to use a config with
|
||||
* specificity of two.
|
||||
*/
|
||||
const specTwoConfigs = registry.filterBySpecificity(2).createConfig().rules;
|
||||
|
||||
// Maybe a specific combination using all three options works
|
||||
const specThreeConfigs = registry.filterBySpecificity(3).createConfig().rules;
|
||||
|
||||
// If all else fails, try to use the default (severity only)
|
||||
const defaultConfigs = registry.filterBySpecificity(1).createConfig().rules;
|
||||
|
||||
// Combine configs in reverse priority order (later take precedence)
|
||||
newConfig.rules = Object.assign({}, disabledConfigs, defaultConfigs, specThreeConfigs, specTwoConfigs, singleConfigs);
|
||||
|
||||
// Make sure progress bar has finished (floating point rounding)
|
||||
bar.update(BAR_TOTAL);
|
||||
|
||||
// Log out some stats to let the user know what happened
|
||||
const finalRuleIds = Object.keys(newConfig.rules);
|
||||
const totalRules = finalRuleIds.length;
|
||||
const enabledRules = finalRuleIds.filter(ruleId => (newConfig.rules[ruleId] !== 0)).length;
|
||||
const resultMessage = [
|
||||
`\nEnabled ${enabledRules} out of ${totalRules}`,
|
||||
`rules based on ${fileQty}`,
|
||||
`file${(fileQty === 1) ? "." : "s."}`
|
||||
].join(" ");
|
||||
|
||||
log.info(resultMessage);
|
||||
|
||||
ConfigOps.normalizeToStrings(newConfig);
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* process user's answers and create config object
|
||||
* @param {Object} answers answers received from inquirer
|
||||
* @returns {Object} config object
|
||||
*/
|
||||
function processAnswers(answers) {
|
||||
let config = { rules: {}, env: {}, parserOptions: {} };
|
||||
|
||||
config.parserOptions.ecmaVersion = answers.ecmaVersion;
|
||||
if (answers.ecmaVersion >= 2015) {
|
||||
if (answers.modules) {
|
||||
config.parserOptions.sourceType = "module";
|
||||
}
|
||||
config.env.es6 = true;
|
||||
}
|
||||
|
||||
if (answers.commonjs) {
|
||||
config.env.commonjs = true;
|
||||
}
|
||||
answers.env.forEach(env => {
|
||||
config.env[env] = true;
|
||||
});
|
||||
if (answers.jsx) {
|
||||
config.parserOptions = config.parserOptions || {};
|
||||
config.parserOptions.ecmaFeatures = config.parserOptions.ecmaFeatures || {};
|
||||
config.parserOptions.ecmaFeatures.jsx = true;
|
||||
if (answers.react) {
|
||||
config.plugins = ["react"];
|
||||
config.parserOptions.ecmaVersion = 2018;
|
||||
}
|
||||
}
|
||||
|
||||
if (answers.source === "prompt") {
|
||||
config.extends = "eslint:recommended";
|
||||
config.rules.indent = ["error", answers.indent];
|
||||
config.rules.quotes = ["error", answers.quotes];
|
||||
config.rules["linebreak-style"] = ["error", answers.linebreak];
|
||||
config.rules.semi = ["error", answers.semi ? "always" : "never"];
|
||||
}
|
||||
|
||||
if (answers.source === "auto") {
|
||||
config = configureRules(answers, config);
|
||||
config = autoconfig.extendFromRecommended(config);
|
||||
}
|
||||
|
||||
ConfigOps.normalizeToStrings(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* process user's style guide of choice and return an appropriate config object.
|
||||
* @param {string} guide name of the chosen style guide
|
||||
* @returns {Object} config object
|
||||
*/
|
||||
function getConfigForStyleGuide(guide) {
|
||||
const guides = {
|
||||
google: { extends: "google" },
|
||||
airbnb: { extends: "airbnb" },
|
||||
"airbnb-base": { extends: "airbnb-base" },
|
||||
standard: { extends: "standard" }
|
||||
};
|
||||
|
||||
if (!guides[guide]) {
|
||||
throw new Error("You referenced an unsupported guide.");
|
||||
}
|
||||
|
||||
return guides[guide];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the local ESLint.
|
||||
* @returns {string|null} The version. If the local ESLint was not found, returns null.
|
||||
*/
|
||||
function getLocalESLintVersion() {
|
||||
try {
|
||||
const resolver = new ModuleResolver();
|
||||
const eslintPath = resolver.resolve("eslint", process.cwd());
|
||||
const eslint = require(eslintPath);
|
||||
|
||||
return eslint.linter.version || null;
|
||||
} catch (_err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shareable config name of the chosen style guide.
|
||||
* @param {Object} answers The answers object.
|
||||
* @returns {string} The shareable config name.
|
||||
*/
|
||||
function getStyleGuideName(answers) {
|
||||
if (answers.styleguide === "airbnb" && !answers.airbnbReact) {
|
||||
return "airbnb-base";
|
||||
}
|
||||
return answers.styleguide;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the local ESLint version conflicts with the required version of the chosen shareable config.
|
||||
* @param {Object} answers The answers object.
|
||||
* @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config.
|
||||
*/
|
||||
function hasESLintVersionConflict(answers) {
|
||||
|
||||
// Get the local ESLint version.
|
||||
const localESLintVersion = getLocalESLintVersion();
|
||||
|
||||
if (!localESLintVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the required range of ESLint version.
|
||||
const configName = getStyleGuideName(answers);
|
||||
const moduleName = `eslint-config-${configName}@latest`;
|
||||
const peerDependencies = getPeerDependencies(moduleName) || {};
|
||||
const requiredESLintVersionRange = peerDependencies.eslint;
|
||||
|
||||
if (!requiredESLintVersionRange) {
|
||||
return false;
|
||||
}
|
||||
|
||||
answers.localESLintVersion = localESLintVersion;
|
||||
answers.requiredESLintVersionRange = requiredESLintVersionRange;
|
||||
|
||||
// Check the version.
|
||||
if (semver.satisfies(localESLintVersion, requiredESLintVersionRange)) {
|
||||
answers.installESLint = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install modules.
|
||||
* @param {string[]} modules Modules to be installed.
|
||||
* @returns {void}
|
||||
*/
|
||||
function installModules(modules) {
|
||||
log.info(`Installing ${modules.join(", ")}`);
|
||||
npmUtils.installSyncSaveDev(modules);
|
||||
}
|
||||
|
||||
/* istanbul ignore next: no need to test inquirer */
|
||||
/**
|
||||
* Ask user to install modules.
|
||||
* @param {string[]} modules Array of modules to be installed.
|
||||
* @param {boolean} packageJsonExists Indicates if package.json is existed.
|
||||
* @returns {Promise} Answer that indicates if user wants to install.
|
||||
*/
|
||||
function askInstallModules(modules, packageJsonExists) {
|
||||
|
||||
// If no modules, do nothing.
|
||||
if (modules.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
log.info("The config that you've selected requires the following dependencies:\n");
|
||||
log.info(modules.join(" "));
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: "confirm",
|
||||
name: "executeInstallation",
|
||||
message: "Would you like to install them now with npm?",
|
||||
default: true,
|
||||
when() {
|
||||
return modules.length && packageJsonExists;
|
||||
}
|
||||
}
|
||||
]).then(({ executeInstallation }) => {
|
||||
if (executeInstallation) {
|
||||
installModules(modules);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* istanbul ignore next: no need to test inquirer */
|
||||
/**
|
||||
* Ask use a few questions on command prompt
|
||||
* @returns {Promise} The promise with the result of the prompt
|
||||
*/
|
||||
function promptUser() {
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "source",
|
||||
message: "How would you like to configure ESLint?",
|
||||
default: "prompt",
|
||||
choices: [
|
||||
{ name: "Use a popular style guide", value: "guide" },
|
||||
{ name: "Answer questions about your style", value: "prompt" },
|
||||
{ name: "Inspect your JavaScript file(s)", value: "auto" }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
name: "styleguide",
|
||||
message: "Which style guide do you want to follow?",
|
||||
choices: [
|
||||
{ name: "Airbnb (https://github.com/airbnb/javascript)", value: "airbnb" },
|
||||
{ name: "Standard (https://github.com/standard/standard)", value: "standard" },
|
||||
{ name: "Google (https://github.com/google/eslint-config-google)", value: "google" }
|
||||
],
|
||||
when(answers) {
|
||||
answers.packageJsonExists = npmUtils.checkPackageJson();
|
||||
return answers.source === "guide" && answers.packageJsonExists;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: "airbnbReact",
|
||||
message: "Do you use React?",
|
||||
default: false,
|
||||
when(answers) {
|
||||
return answers.styleguide === "airbnb";
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "patterns",
|
||||
message: "Which file(s), path(s), or glob(s) should be examined?",
|
||||
when(answers) {
|
||||
return (answers.source === "auto");
|
||||
},
|
||||
validate(input) {
|
||||
if (input.trim().length === 0 && input.trim() !== ",") {
|
||||
return "You must tell us what code to examine. Try again.";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
name: "format",
|
||||
message: "What format do you want your config file to be in?",
|
||||
default: "JavaScript",
|
||||
choices: ["JavaScript", "YAML", "JSON"],
|
||||
when(answers) {
|
||||
return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto");
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: "installESLint",
|
||||
message(answers) {
|
||||
const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange)
|
||||
? "upgrade"
|
||||
: "downgrade";
|
||||
|
||||
return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`;
|
||||
},
|
||||
default: true,
|
||||
when(answers) {
|
||||
return answers.source === "guide" && answers.packageJsonExists && hasESLintVersionConflict(answers);
|
||||
}
|
||||
}
|
||||
]).then(earlyAnswers => {
|
||||
|
||||
// early exit if you are using a style guide
|
||||
if (earlyAnswers.source === "guide") {
|
||||
if (!earlyAnswers.packageJsonExists) {
|
||||
log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again.");
|
||||
return void 0;
|
||||
}
|
||||
if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) {
|
||||
log.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`);
|
||||
}
|
||||
if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) {
|
||||
earlyAnswers.styleguide = "airbnb-base";
|
||||
}
|
||||
|
||||
const config = getConfigForStyleGuide(earlyAnswers.styleguide);
|
||||
const modules = getModulesList(config);
|
||||
|
||||
return askInstallModules(modules, earlyAnswers.packageJsonExists)
|
||||
.then(() => writeFile(config, earlyAnswers.format));
|
||||
}
|
||||
|
||||
// continue with the questions otherwise...
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "ecmaVersion",
|
||||
message: "Which version of ECMAScript do you use?",
|
||||
choices: [
|
||||
{ name: "ES3", value: 3 },
|
||||
{ name: "ES5", value: 5 },
|
||||
{ name: "ES2015", value: 2015 },
|
||||
{ name: "ES2016", value: 2016 },
|
||||
{ name: "ES2017", value: 2017 },
|
||||
{ name: "ES2018", value: 2018 }
|
||||
],
|
||||
default: 1 // This is the index in the choices list
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: "modules",
|
||||
message: "Are you using ES6 modules?",
|
||||
default: false,
|
||||
when(answers) {
|
||||
return answers.ecmaVersion >= 2015;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "env",
|
||||
message: "Where will your code run?",
|
||||
default: ["browser"],
|
||||
choices: [{ name: "Browser", value: "browser" }, { name: "Node", value: "node" }]
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: "commonjs",
|
||||
message: "Do you use CommonJS?",
|
||||
default: false,
|
||||
when(answers) {
|
||||
return answers.env.some(env => env === "browser");
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: "jsx",
|
||||
message: "Do you use JSX?",
|
||||
default: false
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: "react",
|
||||
message: "Do you use React?",
|
||||
default: false,
|
||||
when(answers) {
|
||||
return answers.jsx;
|
||||
}
|
||||
}
|
||||
]).then(secondAnswers => {
|
||||
|
||||
// early exit if you are using automatic style generation
|
||||
if (earlyAnswers.source === "auto") {
|
||||
const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers);
|
||||
|
||||
const config = processAnswers(combinedAnswers);
|
||||
|
||||
const modules = getModulesList(config);
|
||||
|
||||
return askInstallModules(modules).then(() => writeFile(config, earlyAnswers.format));
|
||||
}
|
||||
|
||||
// continue with the style questions otherwise...
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "indent",
|
||||
message: "What style of indentation do you use?",
|
||||
default: "tab",
|
||||
choices: [{ name: "Tabs", value: "tab" }, { name: "Spaces", value: 4 }]
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
name: "quotes",
|
||||
message: "What quotes do you use for strings?",
|
||||
default: "double",
|
||||
choices: [{ name: "Double", value: "double" }, { name: "Single", value: "single" }]
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
name: "linebreak",
|
||||
message: "What line endings do you use?",
|
||||
default: "unix",
|
||||
choices: [{ name: "Unix", value: "unix" }, { name: "Windows", value: "windows" }]
|
||||
},
|
||||
{
|
||||
type: "confirm",
|
||||
name: "semi",
|
||||
message: "Do you require semicolons?",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
name: "format",
|
||||
message: "What format do you want your config file to be in?",
|
||||
default: "JavaScript",
|
||||
choices: ["JavaScript", "YAML", "JSON"]
|
||||
}
|
||||
]).then(answers => {
|
||||
const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers);
|
||||
|
||||
const config = processAnswers(totalAnswers);
|
||||
const modules = getModulesList(config);
|
||||
|
||||
return askInstallModules(modules).then(() => writeFile(config, answers.format));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const init = {
|
||||
getConfigForStyleGuide,
|
||||
getModulesList,
|
||||
hasESLintVersionConflict,
|
||||
installModules,
|
||||
processAnswers,
|
||||
/* istanbul ignore next */initializeConfig() {
|
||||
return promptUser();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = init;
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* @fileoverview Config file operations. This file must be usable in the browser,
|
||||
* so no Node-specific code can be here.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const minimatch = require("minimatch"),
|
||||
path = require("path");
|
||||
|
||||
const debug = require("debug")("eslint:config-ops");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const RULE_SEVERITY_STRINGS = ["off", "warn", "error"],
|
||||
RULE_SEVERITY = RULE_SEVERITY_STRINGS.reduce((map, value, index) => {
|
||||
map[value] = index;
|
||||
return map;
|
||||
}, {}),
|
||||
VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"];
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* Creates an empty configuration object suitable for merging as a base.
|
||||
* @returns {Object} A configuration object.
|
||||
*/
|
||||
createEmptyConfig() {
|
||||
return {
|
||||
globals: {},
|
||||
env: {},
|
||||
rules: {},
|
||||
parserOptions: {}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an environment config based on the specified environments.
|
||||
* @param {Object<string,boolean>} env The environment settings.
|
||||
* @param {Environments} envContext The environment context.
|
||||
* @returns {Object} A configuration object with the appropriate rules and globals
|
||||
* set.
|
||||
*/
|
||||
createEnvironmentConfig(env, envContext) {
|
||||
|
||||
const envConfig = this.createEmptyConfig();
|
||||
|
||||
if (env) {
|
||||
|
||||
envConfig.env = env;
|
||||
|
||||
Object.keys(env).filter(name => env[name]).forEach(name => {
|
||||
const environment = envContext.get(name);
|
||||
|
||||
if (environment) {
|
||||
debug(`Creating config for environment ${name}`);
|
||||
if (environment.globals) {
|
||||
Object.assign(envConfig.globals, environment.globals);
|
||||
}
|
||||
|
||||
if (environment.parserOptions) {
|
||||
Object.assign(envConfig.parserOptions, environment.parserOptions);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return envConfig;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a config with environment settings, applies the globals and
|
||||
* ecmaFeatures to the configuration and returns the result.
|
||||
* @param {Object} config The configuration information.
|
||||
* @param {Environments} envContent env context.
|
||||
* @returns {Object} The updated configuration information.
|
||||
*/
|
||||
applyEnvironments(config, envContent) {
|
||||
if (config.env && typeof config.env === "object") {
|
||||
debug("Apply environment settings to config");
|
||||
return this.merge(this.createEnvironmentConfig(config.env, envContent), config);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
/**
|
||||
* Merges two config objects. This will not only add missing keys, but will also modify values to match.
|
||||
* @param {Object} target config object
|
||||
* @param {Object} src config object. Overrides in this config object will take priority over base.
|
||||
* @param {boolean} [combine] Whether to combine arrays or not
|
||||
* @param {boolean} [isRule] Whether its a rule
|
||||
* @returns {Object} merged config object.
|
||||
*/
|
||||
merge: function deepmerge(target, src, combine, isRule) {
|
||||
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2012 Nicholas Fisher
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This code is taken from deepmerge repo
|
||||
* (https://github.com/KyleAMathews/deepmerge)
|
||||
* and modified to meet our needs.
|
||||
*/
|
||||
const array = Array.isArray(src) || Array.isArray(target);
|
||||
let dst = array && [] || {};
|
||||
|
||||
if (array) {
|
||||
const resolvedTarget = target || [];
|
||||
|
||||
// src could be a string, so check for array
|
||||
if (isRule && Array.isArray(src) && src.length > 1) {
|
||||
dst = dst.concat(src);
|
||||
} else {
|
||||
dst = dst.concat(resolvedTarget);
|
||||
}
|
||||
const resolvedSrc = typeof src === "object" ? src : [src];
|
||||
|
||||
Object.keys(resolvedSrc).forEach((_, i) => {
|
||||
const e = resolvedSrc[i];
|
||||
|
||||
if (typeof dst[i] === "undefined") {
|
||||
dst[i] = e;
|
||||
} else if (typeof e === "object") {
|
||||
if (isRule) {
|
||||
dst[i] = e;
|
||||
} else {
|
||||
dst[i] = deepmerge(resolvedTarget[i], e, combine, isRule);
|
||||
}
|
||||
} else {
|
||||
if (!combine) {
|
||||
dst[i] = e;
|
||||
} else {
|
||||
if (dst.indexOf(e) === -1) {
|
||||
dst.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (target && typeof target === "object") {
|
||||
Object.keys(target).forEach(key => {
|
||||
dst[key] = target[key];
|
||||
});
|
||||
}
|
||||
Object.keys(src).forEach(key => {
|
||||
if (key === "overrides") {
|
||||
dst[key] = (target[key] || []).concat(src[key] || []);
|
||||
} else if (Array.isArray(src[key]) || Array.isArray(target[key])) {
|
||||
dst[key] = deepmerge(target[key], src[key], key === "plugins" || key === "extends", isRule);
|
||||
} else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") {
|
||||
dst[key] = src[key];
|
||||
} else {
|
||||
dst[key] = deepmerge(target[key] || {}, src[key], combine, key === "rules");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return dst;
|
||||
},
|
||||
|
||||
/**
|
||||
* Normalizes the severity value of a rule's configuration to a number
|
||||
* @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally
|
||||
* received from the user. A valid config value is either 0, 1, 2, the string "off" (treated the same as 0),
|
||||
* the string "warn" (treated the same as 1), the string "error" (treated the same as 2), or an array
|
||||
* whose first element is one of the above values. Strings are matched case-insensitively.
|
||||
* @returns {(0|1|2)} The numeric severity value if the config value was valid, otherwise 0.
|
||||
*/
|
||||
getRuleSeverity(ruleConfig) {
|
||||
const severityValue = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
|
||||
|
||||
if (severityValue === 0 || severityValue === 1 || severityValue === 2) {
|
||||
return severityValue;
|
||||
}
|
||||
|
||||
if (typeof severityValue === "string") {
|
||||
return RULE_SEVERITY[severityValue.toLowerCase()] || 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts old-style severity settings (0, 1, 2) into new-style
|
||||
* severity settings (off, warn, error) for all rules. Assumption is that severity
|
||||
* values have already been validated as correct.
|
||||
* @param {Object} config The config object to normalize.
|
||||
* @returns {void}
|
||||
*/
|
||||
normalizeToStrings(config) {
|
||||
|
||||
if (config.rules) {
|
||||
Object.keys(config.rules).forEach(ruleId => {
|
||||
const ruleConfig = config.rules[ruleId];
|
||||
|
||||
if (typeof ruleConfig === "number") {
|
||||
config.rules[ruleId] = RULE_SEVERITY_STRINGS[ruleConfig] || RULE_SEVERITY_STRINGS[0];
|
||||
} else if (Array.isArray(ruleConfig) && typeof ruleConfig[0] === "number") {
|
||||
ruleConfig[0] = RULE_SEVERITY_STRINGS[ruleConfig[0]] || RULE_SEVERITY_STRINGS[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the severity for the given rule configuration represents an error.
|
||||
* @param {int|string|Array} ruleConfig The configuration for an individual rule.
|
||||
* @returns {boolean} True if the rule represents an error, false if not.
|
||||
*/
|
||||
isErrorSeverity(ruleConfig) {
|
||||
return module.exports.getRuleSeverity(ruleConfig) === 2;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether a given config has valid severity or not.
|
||||
* @param {number|string|Array} ruleConfig - The configuration for an individual rule.
|
||||
* @returns {boolean} `true` if the configuration has valid severity.
|
||||
*/
|
||||
isValidSeverity(ruleConfig) {
|
||||
let severity = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;
|
||||
|
||||
if (typeof severity === "string") {
|
||||
severity = severity.toLowerCase();
|
||||
}
|
||||
return VALID_SEVERITIES.indexOf(severity) !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether every rule of a given config has valid severity or not.
|
||||
* @param {Object} config - The configuration for rules.
|
||||
* @returns {boolean} `true` if the configuration has valid severity.
|
||||
*/
|
||||
isEverySeverityValid(config) {
|
||||
return Object.keys(config).every(ruleId => this.isValidSeverity(config[ruleId]));
|
||||
},
|
||||
|
||||
/**
|
||||
* Merges all configurations in a given config vector. A vector is an array of objects, each containing a config
|
||||
* file path and a list of subconfig indices that match the current file path. All config data is assumed to be
|
||||
* cached.
|
||||
* @param {Array<Object>} vector list of config files and their subconfig indices that match the current file path
|
||||
* @param {Object} configCache the config cache
|
||||
* @returns {Object} config object
|
||||
*/
|
||||
getConfigFromVector(vector, configCache) {
|
||||
|
||||
const cachedConfig = configCache.getMergedVectorConfig(vector);
|
||||
|
||||
if (cachedConfig) {
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
debug("Using config from partial cache");
|
||||
|
||||
const subvector = Array.from(vector);
|
||||
let nearestCacheIndex = subvector.length - 1,
|
||||
partialCachedConfig;
|
||||
|
||||
while (nearestCacheIndex >= 0) {
|
||||
partialCachedConfig = configCache.getMergedVectorConfig(subvector);
|
||||
if (partialCachedConfig) {
|
||||
break;
|
||||
}
|
||||
subvector.pop();
|
||||
nearestCacheIndex--;
|
||||
}
|
||||
|
||||
if (!partialCachedConfig) {
|
||||
partialCachedConfig = {};
|
||||
}
|
||||
|
||||
let finalConfig = partialCachedConfig;
|
||||
|
||||
// Start from entry immediately following nearest cached config (first uncached entry)
|
||||
for (let i = nearestCacheIndex + 1; i < vector.length; i++) {
|
||||
finalConfig = this.mergeVectorEntry(finalConfig, vector[i], configCache);
|
||||
configCache.setMergedVectorConfig(vector.slice(0, i + 1), finalConfig);
|
||||
}
|
||||
|
||||
return finalConfig;
|
||||
},
|
||||
|
||||
/**
|
||||
* Merges the config options from a single vector entry into the supplied config.
|
||||
* @param {Object} config the base config to merge the vector entry's options into
|
||||
* @param {Object} vectorEntry a single entry from a vector, consisting of a config file path and an array of
|
||||
* matching override indices
|
||||
* @param {Object} configCache the config cache
|
||||
* @returns {Object} merged config object
|
||||
*/
|
||||
mergeVectorEntry(config, vectorEntry, configCache) {
|
||||
const vectorEntryConfig = Object.assign({}, configCache.getConfig(vectorEntry.filePath));
|
||||
let mergedConfig = Object.assign({}, config),
|
||||
overrides;
|
||||
|
||||
if (vectorEntryConfig.overrides) {
|
||||
overrides = vectorEntryConfig.overrides.filter(
|
||||
(override, overrideIndex) => vectorEntry.matchingOverrides.indexOf(overrideIndex) !== -1
|
||||
);
|
||||
} else {
|
||||
overrides = [];
|
||||
}
|
||||
|
||||
mergedConfig = this.merge(mergedConfig, vectorEntryConfig);
|
||||
|
||||
delete mergedConfig.overrides;
|
||||
|
||||
mergedConfig = overrides.reduce((lastConfig, override) => this.merge(lastConfig, override), mergedConfig);
|
||||
|
||||
if (mergedConfig.filePath) {
|
||||
delete mergedConfig.filePath;
|
||||
delete mergedConfig.baseDirectory;
|
||||
} else if (mergedConfig.files) {
|
||||
delete mergedConfig.files;
|
||||
}
|
||||
|
||||
return mergedConfig;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the specified file path matches all of the supplied glob patterns.
|
||||
* @param {string} filePath The file path to test patterns against
|
||||
* @param {string|string[]} patterns One or more glob patterns, of which at least one should match the file path
|
||||
* @param {string|string[]} [excludedPatterns] One or more glob patterns, of which none should match the file path
|
||||
* @returns {boolean} True if all the supplied patterns match the file path, false otherwise
|
||||
*/
|
||||
pathMatchesGlobs(filePath, patterns, excludedPatterns) {
|
||||
const patternList = [].concat(patterns);
|
||||
const excludedPatternList = [].concat(excludedPatterns || []);
|
||||
|
||||
patternList.concat(excludedPatternList).forEach(pattern => {
|
||||
if (path.isAbsolute(pattern) || pattern.includes("..")) {
|
||||
throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
|
||||
}
|
||||
});
|
||||
|
||||
const opts = { matchBase: true };
|
||||
|
||||
return patternList.some(pattern => minimatch(filePath, pattern, opts)) &&
|
||||
!excludedPatternList.some(excludedPattern => minimatch(filePath, excludedPattern, opts));
|
||||
}
|
||||
};
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* @fileoverview Create configurations for a rule
|
||||
* @author Ian VanSchooten
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const Rules = require("../rules"),
|
||||
loadRules = require("../load-rules");
|
||||
|
||||
const rules = new Rules();
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wrap all of the elements of an array into arrays.
|
||||
* @param {*[]} xs Any array.
|
||||
* @returns {Array[]} An array of arrays.
|
||||
*/
|
||||
function explodeArray(xs) {
|
||||
return xs.reduce((accumulator, x) => {
|
||||
accumulator.push([x]);
|
||||
return accumulator;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix two arrays such that each element of the second array is concatenated
|
||||
* onto each element of the first array.
|
||||
*
|
||||
* For example:
|
||||
* combineArrays([a, [b, c]], [x, y]); // -> [[a, x], [a, y], [b, c, x], [b, c, y]]
|
||||
*
|
||||
* @param {array} arr1 The first array to combine.
|
||||
* @param {array} arr2 The second array to combine.
|
||||
* @returns {array} A mixture of the elements of the first and second arrays.
|
||||
*/
|
||||
function combineArrays(arr1, arr2) {
|
||||
const res = [];
|
||||
|
||||
if (arr1.length === 0) {
|
||||
return explodeArray(arr2);
|
||||
}
|
||||
if (arr2.length === 0) {
|
||||
return explodeArray(arr1);
|
||||
}
|
||||
arr1.forEach(x1 => {
|
||||
arr2.forEach(x2 => {
|
||||
res.push([].concat(x1, x2));
|
||||
});
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group together valid rule configurations based on object properties
|
||||
*
|
||||
* e.g.:
|
||||
* groupByProperty([
|
||||
* {before: true},
|
||||
* {before: false},
|
||||
* {after: true},
|
||||
* {after: false}
|
||||
* ]);
|
||||
*
|
||||
* will return:
|
||||
* [
|
||||
* [{before: true}, {before: false}],
|
||||
* [{after: true}, {after: false}]
|
||||
* ]
|
||||
*
|
||||
* @param {Object[]} objects Array of objects, each with one property/value pair
|
||||
* @returns {Array[]} Array of arrays of objects grouped by property
|
||||
*/
|
||||
function groupByProperty(objects) {
|
||||
const groupedObj = objects.reduce((accumulator, obj) => {
|
||||
const prop = Object.keys(obj)[0];
|
||||
|
||||
accumulator[prop] = accumulator[prop] ? accumulator[prop].concat(obj) : [obj];
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
return Object.keys(groupedObj).map(prop => groupedObj[prop]);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Configuration settings for a rule.
|
||||
*
|
||||
* A configuration can be a single number (severity), or an array where the first
|
||||
* element in the array is the severity, and is the only required element.
|
||||
* Configs may also have one or more additional elements to specify rule
|
||||
* configuration or options.
|
||||
*
|
||||
* @typedef {array|number} ruleConfig
|
||||
* @param {number} 0 The rule's severity (0, 1, 2).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Object whose keys are rule names and values are arrays of valid ruleConfig items
|
||||
* which should be linted against the target source code to determine error counts.
|
||||
* (a ruleConfigSet.ruleConfigs).
|
||||
*
|
||||
* e.g. rulesConfig = {
|
||||
* "comma-dangle": [2, [2, "always"], [2, "always-multiline"], [2, "never"]],
|
||||
* "no-console": [2]
|
||||
* }
|
||||
* @typedef rulesConfig
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Create valid rule configurations by combining two arrays,
|
||||
* with each array containing multiple objects each with a
|
||||
* single property/value pair and matching properties.
|
||||
*
|
||||
* e.g.:
|
||||
* combinePropertyObjects(
|
||||
* [{before: true}, {before: false}],
|
||||
* [{after: true}, {after: false}]
|
||||
* );
|
||||
*
|
||||
* will return:
|
||||
* [
|
||||
* {before: true, after: true},
|
||||
* {before: true, after: false},
|
||||
* {before: false, after: true},
|
||||
* {before: false, after: false}
|
||||
* ]
|
||||
*
|
||||
* @param {Object[]} objArr1 Single key/value objects, all with the same key
|
||||
* @param {Object[]} objArr2 Single key/value objects, all with another key
|
||||
* @returns {Object[]} Combined objects for each combination of input properties and values
|
||||
*/
|
||||
function combinePropertyObjects(objArr1, objArr2) {
|
||||
const res = [];
|
||||
|
||||
if (objArr1.length === 0) {
|
||||
return objArr2;
|
||||
}
|
||||
if (objArr2.length === 0) {
|
||||
return objArr1;
|
||||
}
|
||||
objArr1.forEach(obj1 => {
|
||||
objArr2.forEach(obj2 => {
|
||||
const combinedObj = {};
|
||||
const obj1Props = Object.keys(obj1);
|
||||
const obj2Props = Object.keys(obj2);
|
||||
|
||||
obj1Props.forEach(prop1 => {
|
||||
combinedObj[prop1] = obj1[prop1];
|
||||
});
|
||||
obj2Props.forEach(prop2 => {
|
||||
combinedObj[prop2] = obj2[prop2];
|
||||
});
|
||||
res.push(combinedObj);
|
||||
});
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of a rule configuration set
|
||||
*
|
||||
* A rule configuration set is an array of configurations that are valid for a
|
||||
* given rule. For example, the configuration set for the "semi" rule could be:
|
||||
*
|
||||
* ruleConfigSet.ruleConfigs // -> [[2], [2, "always"], [2, "never"]]
|
||||
*
|
||||
* Rule configuration set class
|
||||
*/
|
||||
class RuleConfigSet {
|
||||
|
||||
/**
|
||||
* @param {ruleConfig[]} configs Valid rule configurations
|
||||
*/
|
||||
constructor(configs) {
|
||||
|
||||
/**
|
||||
* Stored valid rule configurations for this instance
|
||||
* @type {array}
|
||||
*/
|
||||
this.ruleConfigs = configs || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a severity level to the front of all configs in the instance.
|
||||
* This should only be called after all configs have been added to the instance.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
addErrorSeverity() {
|
||||
const severity = 2;
|
||||
|
||||
this.ruleConfigs = this.ruleConfigs.map(config => {
|
||||
config.unshift(severity);
|
||||
return config;
|
||||
});
|
||||
|
||||
// Add a single config at the beginning consisting of only the severity
|
||||
this.ruleConfigs.unshift(severity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rule configs from an array of strings (schema enums)
|
||||
* @param {string[]} enums Array of valid rule options (e.g. ["always", "never"])
|
||||
* @returns {void}
|
||||
*/
|
||||
addEnums(enums) {
|
||||
this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, enums));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rule configurations from a schema object
|
||||
* @param {Object} obj Schema item with type === "object"
|
||||
* @returns {boolean} true if at least one schema for the object could be generated, false otherwise
|
||||
*/
|
||||
addObject(obj) {
|
||||
const objectConfigSet = {
|
||||
objectConfigs: [],
|
||||
add(property, values) {
|
||||
for (let idx = 0; idx < values.length; idx++) {
|
||||
const optionObj = {};
|
||||
|
||||
optionObj[property] = values[idx];
|
||||
this.objectConfigs.push(optionObj);
|
||||
}
|
||||
},
|
||||
|
||||
combine() {
|
||||
this.objectConfigs = groupByProperty(this.objectConfigs).reduce((accumulator, objArr) => combinePropertyObjects(accumulator, objArr), []);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* The object schema could have multiple independent properties.
|
||||
* If any contain enums or booleans, they can be added and then combined
|
||||
*/
|
||||
Object.keys(obj.properties).forEach(prop => {
|
||||
if (obj.properties[prop].enum) {
|
||||
objectConfigSet.add(prop, obj.properties[prop].enum);
|
||||
}
|
||||
if (obj.properties[prop].type && obj.properties[prop].type === "boolean") {
|
||||
objectConfigSet.add(prop, [true, false]);
|
||||
}
|
||||
});
|
||||
objectConfigSet.combine();
|
||||
|
||||
if (objectConfigSet.objectConfigs.length > 0) {
|
||||
this.ruleConfigs = this.ruleConfigs.concat(combineArrays(this.ruleConfigs, objectConfigSet.objectConfigs));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate valid rule configurations based on a schema object
|
||||
* @param {Object} schema A rule's schema object
|
||||
* @returns {array[]} Valid rule configurations
|
||||
*/
|
||||
function generateConfigsFromSchema(schema) {
|
||||
const configSet = new RuleConfigSet();
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
for (const opt of schema) {
|
||||
if (opt.enum) {
|
||||
configSet.addEnums(opt.enum);
|
||||
} else if (opt.type && opt.type === "object") {
|
||||
if (!configSet.addObject(opt)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO (IanVS): support oneOf
|
||||
} else {
|
||||
|
||||
// If we don't know how to fill in this option, don't fill in any of the following options.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
configSet.addErrorSeverity();
|
||||
return configSet.ruleConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate possible rule configurations for all of the core rules
|
||||
* @returns {rulesConfig} Hash of rule names and arrays of possible configurations
|
||||
*/
|
||||
function createCoreRuleConfigs() {
|
||||
const ruleList = loadRules();
|
||||
|
||||
return Object.keys(ruleList).reduce((accumulator, id) => {
|
||||
const rule = rules.get(id);
|
||||
const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema;
|
||||
|
||||
accumulator[id] = generateConfigsFromSchema(schema);
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
generateConfigsFromSchema,
|
||||
createCoreRuleConfigs
|
||||
};
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* @fileoverview Validates configs.
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const path = require("path"),
|
||||
ajv = require("../util/ajv"),
|
||||
lodash = require("lodash"),
|
||||
configSchema = require("../../conf/config-schema.js"),
|
||||
util = require("util");
|
||||
|
||||
const ruleValidators = new WeakMap();
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------------------------
|
||||
let validateSchema;
|
||||
|
||||
// Defitions for deprecation warnings.
|
||||
const deprecationWarningMessages = {
|
||||
ESLINT_LEGACY_ECMAFEATURES: "The 'ecmaFeatures' config file property is deprecated, and has no effect.",
|
||||
ESLINT_LEGACY_OBJECT_REST_SPREAD: "The 'parserOptions.ecmaFeatures.experimentalObjectRestSpread' option is deprecated. Use 'parserOptions.ecmaVersion' instead."
|
||||
};
|
||||
const severityMap = {
|
||||
error: 2,
|
||||
warn: 1,
|
||||
off: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const schema = rule.schema || rule.meta && rule.meta.schema;
|
||||
|
||||
// Given a tuple of schemas, insert warning level at the beginning
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid.
|
||||
* @param {options} options The given options for the rule.
|
||||
* @returns {number|string} The rule's severity value
|
||||
*/
|
||||
function validateRuleSeverity(options) {
|
||||
const severity = Array.isArray(options) ? options[0] : options;
|
||||
const normSeverity = typeof severity === "string" ? severityMap[severity.toLowerCase()] : severity;
|
||||
|
||||
if (normSeverity === 0 || normSeverity === 1 || normSeverity === 2) {
|
||||
return normSeverity;
|
||||
}
|
||||
|
||||
throw new Error(`\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '${util.inspect(severity).replace(/'/g, "\"").replace(/\n/g, "")}').\n`);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the non-severity options passed to a rule, based on its schema.
|
||||
* @param {{create: Function}} rule The rule to validate
|
||||
* @param {array} localOptions The options for the rule, excluding severity
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateRuleSchema(rule, localOptions) {
|
||||
if (!ruleValidators.has(rule)) {
|
||||
const schema = getRuleOptionsSchema(rule);
|
||||
|
||||
if (schema) {
|
||||
ruleValidators.set(rule, ajv.compile(schema));
|
||||
}
|
||||
}
|
||||
|
||||
const validateRule = ruleValidators.get(rule);
|
||||
|
||||
if (validateRule) {
|
||||
validateRule(localOptions);
|
||||
if (validateRule.errors) {
|
||||
throw new Error(validateRule.errors.map(
|
||||
error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
|
||||
).join(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a rule's options against its schema.
|
||||
* @param {{create: Function}|null} rule The rule that the config is being validated for
|
||||
* @param {string} ruleId The rule's unique name.
|
||||
* @param {array|number} options The given options for the rule.
|
||||
* @param {string|null} source The name of the configuration source to report in any errors. If null or undefined,
|
||||
* no source is prepended to the message.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateRuleOptions(rule, ruleId, options, source) {
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const severity = validateRuleSeverity(options);
|
||||
|
||||
if (severity !== 0) {
|
||||
validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
|
||||
}
|
||||
} catch (err) {
|
||||
const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${err.message}`;
|
||||
|
||||
if (typeof source === "string") {
|
||||
throw new Error(`${source}:\n\t${enhancedMessage}`);
|
||||
} else {
|
||||
throw new Error(enhancedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an environment object
|
||||
* @param {Object} environment The environment config object to validate.
|
||||
* @param {string} source The name of the configuration source to report in any errors.
|
||||
* @param {Environments} envContext Env context
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateEnvironment(environment, source, envContext) {
|
||||
|
||||
// not having an environment is ok
|
||||
if (!environment) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(environment).forEach(env => {
|
||||
if (!envContext.get(env)) {
|
||||
const message = `${source}:\n\tEnvironment key "${env}" is unknown\n`;
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a rules config object
|
||||
* @param {Object} rulesConfig The rules config object to validate.
|
||||
* @param {string} source The name of the configuration source to report in any errors.
|
||||
* @param {function(string): {create: Function}} ruleMapper A mapper function from strings to loaded rules
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateRules(rulesConfig, source, ruleMapper) {
|
||||
if (!rulesConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(rulesConfig).forEach(id => {
|
||||
validateRuleOptions(ruleMapper(id), id, rulesConfig[id], source);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an array of schema validation errors.
|
||||
* @param {Array} errors An array of error messages to format.
|
||||
* @returns {string} Formatted error message
|
||||
*/
|
||||
function formatErrors(errors) {
|
||||
return errors.map(error => {
|
||||
if (error.keyword === "additionalProperties") {
|
||||
const formattedPropertyPath = error.dataPath.length ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` : error.params.additionalProperty;
|
||||
|
||||
return `Unexpected top-level property "${formattedPropertyPath}"`;
|
||||
}
|
||||
if (error.keyword === "type") {
|
||||
const formattedField = error.dataPath.slice(1);
|
||||
const formattedExpectedType = Array.isArray(error.schema) ? error.schema.join("/") : error.schema;
|
||||
const formattedValue = JSON.stringify(error.data);
|
||||
|
||||
return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
|
||||
}
|
||||
|
||||
const field = error.dataPath[0] === "." ? error.dataPath.slice(1) : error.dataPath;
|
||||
|
||||
return `"${field}" ${error.message}. Value: ${JSON.stringify(error.data)}`;
|
||||
}).map(message => `\t- ${message}.\n`).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted
|
||||
* for each unique file path, but repeated invocations with the same file path have no effect.
|
||||
* No warnings are emitted if the `--no-deprecation` or `--no-warnings` Node runtime flags are active.
|
||||
* @param {string} source The name of the configuration source to report the warning for.
|
||||
* @param {string} errorCode The warning message to show.
|
||||
* @returns {void}
|
||||
*/
|
||||
const emitDeprecationWarning = lodash.memoize((source, errorCode) => {
|
||||
const rel = path.relative(process.cwd(), source);
|
||||
const message = deprecationWarningMessages[errorCode];
|
||||
|
||||
process.emitWarning(
|
||||
`${message} (found in "${rel}")`,
|
||||
"DeprecationWarning",
|
||||
errorCode
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates the top level properties of the config object.
|
||||
* @param {Object} config The config object to validate.
|
||||
* @param {string} source The name of the configuration source to report in any errors.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateConfigSchema(config, source) {
|
||||
validateSchema = validateSchema || ajv.compile(configSchema);
|
||||
|
||||
if (!validateSchema(config)) {
|
||||
throw new Error(`ESLint configuration in ${source} is invalid:\n${formatErrors(validateSchema.errors)}`);
|
||||
}
|
||||
|
||||
if (Object.hasOwnProperty.call(config, "ecmaFeatures")) {
|
||||
emitDeprecationWarning(source, "ESLINT_LEGACY_ECMAFEATURES");
|
||||
}
|
||||
|
||||
if (
|
||||
(config.parser || "espree") === "espree" &&
|
||||
config.parserOptions &&
|
||||
config.parserOptions.ecmaFeatures &&
|
||||
config.parserOptions.ecmaFeatures.experimentalObjectRestSpread
|
||||
) {
|
||||
emitDeprecationWarning(source, "ESLINT_LEGACY_OBJECT_REST_SPREAD");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an entire config object.
|
||||
* @param {Object} config The config object to validate.
|
||||
* @param {string} source The name of the configuration source to report in any errors.
|
||||
* @param {function(string): {create: Function}} ruleMapper A mapper function from rule IDs to defined rules
|
||||
* @param {Environments} envContext The env context
|
||||
* @returns {void}
|
||||
*/
|
||||
function validate(config, source, ruleMapper, envContext) {
|
||||
validateConfigSchema(config, source);
|
||||
validateRules(config.rules, source, ruleMapper);
|
||||
validateEnvironment(config.env, source, envContext);
|
||||
|
||||
for (const override of config.overrides || []) {
|
||||
validateRules(override.rules, source, ruleMapper);
|
||||
validateEnvironment(override.env, source, envContext);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
getRuleOptionsSchema,
|
||||
validate,
|
||||
validateRuleOptions
|
||||
};
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @fileoverview Environments manager
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const envs = require("../../conf/environments");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class Environments {
|
||||
|
||||
/**
|
||||
* create env context
|
||||
*/
|
||||
constructor() {
|
||||
this._environments = new Map();
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the default environments.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
load() {
|
||||
Object.keys(envs).forEach(envName => {
|
||||
this._environments.set(envName, envs[envName]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the environment with the given name.
|
||||
* @param {string} name The name of the environment to retrieve.
|
||||
* @returns {Object?} The environment object or null if not found.
|
||||
*/
|
||||
get(name) {
|
||||
return this._environments.get(name) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the environment present
|
||||
* @returns {Object} The environment object for each env name
|
||||
*/
|
||||
getAll() {
|
||||
return Array.from(this._environments).reduce((coll, env) => {
|
||||
coll[env[0]] = env[1];
|
||||
return coll;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an environment.
|
||||
* @param {string} name The name of the environment.
|
||||
* @param {Object} env The environment settings.
|
||||
* @returns {void}
|
||||
*/
|
||||
define(name, env) {
|
||||
this._environments.set(name, env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports all environments from a plugin.
|
||||
* @param {Object} plugin The plugin object.
|
||||
* @param {string} pluginName The name of the plugin.
|
||||
* @returns {void}
|
||||
*/
|
||||
importPlugin(plugin, pluginName) {
|
||||
if (plugin.environments) {
|
||||
Object.keys(plugin.environments).forEach(envName => {
|
||||
this.define(`${pluginName}/${envName}`, plugin.environments[envName]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Environments;
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @fileoverview Plugins manager
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const debug = require("debug")("eslint:plugins");
|
||||
const naming = require("../util/naming");
|
||||
const path = require("path");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Plugin class
|
||||
*/
|
||||
class Plugins {
|
||||
|
||||
/**
|
||||
* Creates the plugins context
|
||||
* @param {Environments} envContext - env context
|
||||
* @param {Rules} rulesContext - rules context
|
||||
*/
|
||||
constructor(envContext, rulesContext) {
|
||||
this._plugins = Object.create(null);
|
||||
this._environments = envContext;
|
||||
this._rules = rulesContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a plugin with a given name rather than loading from disk.
|
||||
* @param {string} pluginName The name of the plugin to load.
|
||||
* @param {Object} plugin The plugin object.
|
||||
* @returns {void}
|
||||
*/
|
||||
define(pluginName, plugin) {
|
||||
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
|
||||
const shortName = naming.getShorthandName(longName, "eslint-plugin");
|
||||
|
||||
// load up environments and rules
|
||||
this._plugins[shortName] = plugin;
|
||||
this._environments.importPlugin(plugin, shortName);
|
||||
this._rules.importPlugin(plugin, shortName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a plugin with the given name.
|
||||
* @param {string} pluginName The name of the plugin to retrieve.
|
||||
* @returns {Object} The plugin or null if not loaded.
|
||||
*/
|
||||
get(pluginName) {
|
||||
return this._plugins[pluginName] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all plugins that are loaded.
|
||||
* @returns {Object} The plugins cache.
|
||||
*/
|
||||
getAll() {
|
||||
return this._plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a plugin with the given name.
|
||||
* @param {string} pluginName The name of the plugin to load.
|
||||
* @returns {void}
|
||||
* @throws {Error} If the plugin cannot be loaded.
|
||||
*/
|
||||
load(pluginName) {
|
||||
const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
|
||||
const shortName = naming.getShorthandName(longName, "eslint-plugin");
|
||||
let plugin = null;
|
||||
|
||||
if (pluginName.match(/\s+/)) {
|
||||
const whitespaceError = new Error(`Whitespace found in plugin name '${pluginName}'`);
|
||||
|
||||
whitespaceError.messageTemplate = "whitespace-found";
|
||||
whitespaceError.messageData = {
|
||||
pluginName: longName
|
||||
};
|
||||
throw whitespaceError;
|
||||
}
|
||||
|
||||
if (!this._plugins[shortName]) {
|
||||
try {
|
||||
plugin = require(longName);
|
||||
} catch (pluginLoadErr) {
|
||||
try {
|
||||
|
||||
// Check whether the plugin exists
|
||||
require.resolve(longName);
|
||||
} catch (missingPluginErr) {
|
||||
|
||||
// If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
|
||||
debug(`Failed to load plugin ${longName}.`);
|
||||
missingPluginErr.message = `Failed to load plugin ${pluginName}: ${missingPluginErr.message}`;
|
||||
missingPluginErr.messageTemplate = "plugin-missing";
|
||||
missingPluginErr.messageData = {
|
||||
pluginName: longName,
|
||||
eslintPath: path.resolve(__dirname, "../..")
|
||||
};
|
||||
throw missingPluginErr;
|
||||
}
|
||||
|
||||
// Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace.
|
||||
throw pluginLoadErr;
|
||||
}
|
||||
|
||||
// This step is costly, so skip if debug is disabled
|
||||
if (debug.enabled) {
|
||||
const resolvedPath = require.resolve(longName);
|
||||
|
||||
let version = null;
|
||||
|
||||
try {
|
||||
version = require(`${longName}/package.json`).version;
|
||||
} catch (e) {
|
||||
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
const loadedPluginAndVersion = version
|
||||
? `${longName}@${version}`
|
||||
: `${longName}, version unknown`;
|
||||
|
||||
debug(`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${resolvedPath})`);
|
||||
}
|
||||
|
||||
this.define(pluginName, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all plugins from an array.
|
||||
* @param {string[]} pluginNames An array of plugins names.
|
||||
* @returns {void}
|
||||
* @throws {Error} If a plugin cannot be loaded.
|
||||
* @throws {Error} If "plugins" in config is not an array
|
||||
*/
|
||||
loadAll(pluginNames) {
|
||||
|
||||
// if "plugins" in config is not an array, throw an error so user can fix their config.
|
||||
if (!Array.isArray(pluginNames)) {
|
||||
const pluginNotArrayMessage = "ESLint configuration error: \"plugins\" value must be an array";
|
||||
|
||||
debug(`${pluginNotArrayMessage}: ${JSON.stringify(pluginNames)}`);
|
||||
|
||||
throw new Error(pluginNotArrayMessage);
|
||||
}
|
||||
|
||||
// load each plugin by name
|
||||
pluginNames.forEach(this.load, this);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Plugins;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview CheckStyle XML reporter
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const xmlEscape = require("../util/xml-escape");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helper Functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the severity of warning or error
|
||||
* @param {Object} message message object to examine
|
||||
* @returns {string} severity level
|
||||
* @private
|
||||
*/
|
||||
function getMessageType(message) {
|
||||
if (message.fatal || message.severity === 2) {
|
||||
return "error";
|
||||
}
|
||||
return "warning";
|
||||
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
|
||||
let output = "";
|
||||
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
|
||||
output += "<checkstyle version=\"4.3\">";
|
||||
|
||||
results.forEach(result => {
|
||||
const messages = result.messages;
|
||||
|
||||
output += `<file name="${xmlEscape(result.filePath)}">`;
|
||||
|
||||
messages.forEach(message => {
|
||||
output += [
|
||||
`<error line="${xmlEscape(message.line)}"`,
|
||||
`column="${xmlEscape(message.column)}"`,
|
||||
`severity="${xmlEscape(getMessageType(message))}"`,
|
||||
`message="${xmlEscape(message.message)}${message.ruleId ? ` (${message.ruleId})` : ""}"`,
|
||||
`source="${message.ruleId ? xmlEscape(`eslint.rules.${message.ruleId}`) : ""}" />`
|
||||
].join(" ");
|
||||
});
|
||||
|
||||
output += "</file>";
|
||||
|
||||
});
|
||||
|
||||
output += "</checkstyle>";
|
||||
|
||||
return output;
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @fileoverview Codeframe reporter
|
||||
* @author Vitor Balocco
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const chalk = require("chalk");
|
||||
const { codeFrameColumns } = require("@babel/code-frame");
|
||||
const path = require("path");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Given a word and a count, append an s if count is not one.
|
||||
* @param {string} word A word in its singular form.
|
||||
* @param {number} count A number controlling whether word should be pluralized.
|
||||
* @returns {string} The original word with an s on the end if count is not one.
|
||||
*/
|
||||
function pluralize(word, count) {
|
||||
return (count === 1 ? word : `${word}s`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a formatted relative file path from an absolute path and a line/column in the file.
|
||||
* @param {string} filePath The absolute file path to format.
|
||||
* @param {number} line The line from the file to use for formatting.
|
||||
* @param {number} column The column from the file to use for formatting.
|
||||
* @returns {string} The formatted file path.
|
||||
*/
|
||||
function formatFilePath(filePath, line, column) {
|
||||
let relPath = path.relative(process.cwd(), filePath);
|
||||
|
||||
if (line && column) {
|
||||
relPath += `:${line}:${column}`;
|
||||
}
|
||||
|
||||
return chalk.green(relPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the formatted output for a given message.
|
||||
* @param {Object} message The object that represents this message.
|
||||
* @param {Object} parentResult The result object that this message belongs to.
|
||||
* @returns {string} The formatted output.
|
||||
*/
|
||||
function formatMessage(message, parentResult) {
|
||||
const type = (message.fatal || message.severity === 2) ? chalk.red("error") : chalk.yellow("warning");
|
||||
const msg = `${chalk.bold(message.message.replace(/([^ ])\.$/, "$1"))}`;
|
||||
const ruleId = message.fatal ? "" : chalk.dim(`(${message.ruleId})`);
|
||||
const filePath = formatFilePath(parentResult.filePath, message.line, message.column);
|
||||
const sourceCode = parentResult.output ? parentResult.output : parentResult.source;
|
||||
|
||||
const firstLine = [
|
||||
`${type}:`,
|
||||
`${msg}`,
|
||||
ruleId ? `${ruleId}` : "",
|
||||
sourceCode ? `at ${filePath}:` : `at ${filePath}`
|
||||
].filter(String).join(" ");
|
||||
|
||||
const result = [firstLine];
|
||||
|
||||
if (sourceCode) {
|
||||
result.push(
|
||||
codeFrameColumns(sourceCode, { start: { line: message.line, column: message.column } }, { highlightCode: false })
|
||||
);
|
||||
}
|
||||
|
||||
return result.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the formatted output summary for a given number of errors and warnings.
|
||||
* @param {number} errors The number of errors.
|
||||
* @param {number} warnings The number of warnings.
|
||||
* @param {number} fixableErrors The number of fixable errors.
|
||||
* @param {number} fixableWarnings The number of fixable warnings.
|
||||
* @returns {string} The formatted output summary.
|
||||
*/
|
||||
function formatSummary(errors, warnings, fixableErrors, fixableWarnings) {
|
||||
const summaryColor = errors > 0 ? "red" : "yellow";
|
||||
const summary = [];
|
||||
const fixablesSummary = [];
|
||||
|
||||
if (errors > 0) {
|
||||
summary.push(`${errors} ${pluralize("error", errors)}`);
|
||||
}
|
||||
|
||||
if (warnings > 0) {
|
||||
summary.push(`${warnings} ${pluralize("warning", warnings)}`);
|
||||
}
|
||||
|
||||
if (fixableErrors > 0) {
|
||||
fixablesSummary.push(`${fixableErrors} ${pluralize("error", fixableErrors)}`);
|
||||
}
|
||||
|
||||
if (fixableWarnings > 0) {
|
||||
fixablesSummary.push(`${fixableWarnings} ${pluralize("warning", fixableWarnings)}`);
|
||||
}
|
||||
|
||||
let output = chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
|
||||
|
||||
if (fixableErrors || fixableWarnings) {
|
||||
output += chalk[summaryColor].bold(`\n${fixablesSummary.join(" and ")} potentially fixable with the \`--fix\` option.`);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
let errors = 0;
|
||||
let warnings = 0;
|
||||
let fixableErrors = 0;
|
||||
let fixableWarnings = 0;
|
||||
|
||||
const resultsWithMessages = results.filter(result => result.messages.length > 0);
|
||||
|
||||
let output = resultsWithMessages.reduce((resultsOutput, result) => {
|
||||
const messages = result.messages.map(message => `${formatMessage(message, result)}\n\n`);
|
||||
|
||||
errors += result.errorCount;
|
||||
warnings += result.warningCount;
|
||||
fixableErrors += result.fixableErrorCount;
|
||||
fixableWarnings += result.fixableWarningCount;
|
||||
|
||||
return resultsOutput.concat(messages);
|
||||
}, []).join("\n");
|
||||
|
||||
output += "\n";
|
||||
output += formatSummary(errors, warnings, fixableErrors, fixableWarnings);
|
||||
|
||||
return (errors + warnings) > 0 ? output : "";
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview Compact reporter
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helper Functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the severity of warning or error
|
||||
* @param {Object} message message object to examine
|
||||
* @returns {string} severity level
|
||||
* @private
|
||||
*/
|
||||
function getMessageType(message) {
|
||||
if (message.fatal || message.severity === 2) {
|
||||
return "Error";
|
||||
}
|
||||
return "Warning";
|
||||
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
|
||||
let output = "",
|
||||
total = 0;
|
||||
|
||||
results.forEach(result => {
|
||||
|
||||
const messages = result.messages;
|
||||
|
||||
total += messages.length;
|
||||
|
||||
messages.forEach(message => {
|
||||
|
||||
output += `${result.filePath}: `;
|
||||
output += `line ${message.line || 0}`;
|
||||
output += `, col ${message.column || 0}`;
|
||||
output += `, ${getMessageType(message)}`;
|
||||
output += ` - ${message.message}`;
|
||||
output += message.ruleId ? ` (${message.ruleId})` : "";
|
||||
output += "\n";
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if (total > 0) {
|
||||
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<tr style="display:none" class="f-<%= parentIndex %>">
|
||||
<td><%= lineNumber %>:<%= columnNumber %></td>
|
||||
<td class="clr-<%= severityNumber %>"><%= severityName %></td>
|
||||
<td><%- message %></td>
|
||||
<td>
|
||||
<a href="https://eslint.org/docs/rules/<%= ruleId %>" target="_blank" rel="noopener noreferrer"><%= ruleId %></a>
|
||||
</td>
|
||||
</tr>
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
<!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>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<tr class="bg-<%- color %>" data-group="f-<%- index %>">
|
||||
<th colspan="4">
|
||||
[+] <%- filePath %>
|
||||
<span><%- summary %></span>
|
||||
</th>
|
||||
</tr>
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @fileoverview HTML reporter
|
||||
* @author Julian Laval
|
||||
*/
|
||||
"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"));
|
||||
|
||||
/**
|
||||
* Given a word and a count, append an s if count is not one.
|
||||
* @param {string} word A word in its singular form.
|
||||
* @param {int} count A number controlling whether word should be pluralized.
|
||||
* @returns {string} The original word with an s on the end if count is not one.
|
||||
*/
|
||||
function pluralize(word, count) {
|
||||
return (count === 1 ? word : `${word}s`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders text along the template of x problems (x errors, x warnings)
|
||||
* @param {string} totalErrors Total errors
|
||||
* @param {string} totalWarnings Total warnings
|
||||
* @returns {string} The formatted string, pluralized where necessary
|
||||
*/
|
||||
function renderSummary(totalErrors, totalWarnings) {
|
||||
const totalProblems = totalErrors + totalWarnings;
|
||||
let renderedText = `${totalProblems} ${pluralize("problem", totalProblems)}`;
|
||||
|
||||
if (totalProblems !== 0) {
|
||||
renderedText += ` (${totalErrors} ${pluralize("error", totalErrors)}, ${totalWarnings} ${pluralize("warning", totalWarnings)})`;
|
||||
}
|
||||
return renderedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color based on whether there are errors/warnings...
|
||||
* @param {string} totalErrors Total errors
|
||||
* @param {string} totalWarnings Total warnings
|
||||
* @returns {int} The color code (0 = green, 1 = yellow, 2 = red)
|
||||
*/
|
||||
function renderColor(totalErrors, totalWarnings) {
|
||||
if (totalErrors !== 0) {
|
||||
return 2;
|
||||
}
|
||||
if (totalWarnings !== 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTML (table rows) describing the messages.
|
||||
* @param {Array} messages Messages.
|
||||
* @param {int} parentIndex Index of the parent HTML row.
|
||||
* @returns {string} HTML (table rows) describing the messages.
|
||||
*/
|
||||
function renderMessages(messages, parentIndex) {
|
||||
|
||||
/**
|
||||
* Get HTML (table row) describing a message.
|
||||
* @param {Object} message Message.
|
||||
* @returns {string} HTML (table row) describing a message.
|
||||
*/
|
||||
return lodash.map(messages, message => {
|
||||
const lineNumber = message.line || 0;
|
||||
const columnNumber = message.column || 0;
|
||||
|
||||
return messageTemplate({
|
||||
parentIndex,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
severityNumber: message.severity,
|
||||
severityName: message.severity === 1 ? "Warning" : "Error",
|
||||
message: message.message,
|
||||
ruleId: message.ruleId
|
||||
});
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} results Test results.
|
||||
* @returns {string} HTML string describing the results.
|
||||
*/
|
||||
function renderResults(results) {
|
||||
return lodash.map(results, (result, index) => resultTemplate({
|
||||
index,
|
||||
color: renderColor(result.errorCount, result.warningCount),
|
||||
filePath: result.filePath,
|
||||
summary: renderSummary(result.errorCount, result.warningCount)
|
||||
|
||||
}) + renderMessages(result.messages, index)).join("\n");
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
let totalErrors,
|
||||
totalWarnings;
|
||||
|
||||
totalErrors = 0;
|
||||
totalWarnings = 0;
|
||||
|
||||
// Iterate over results to get totals
|
||||
results.forEach(result => {
|
||||
totalErrors += result.errorCount;
|
||||
totalWarnings += result.warningCount;
|
||||
});
|
||||
|
||||
return pageTemplate({
|
||||
date: new Date(),
|
||||
reportColor: renderColor(totalErrors, totalWarnings),
|
||||
reportSummary: renderSummary(totalErrors, totalWarnings),
|
||||
results: renderResults(results)
|
||||
});
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @fileoverview JSLint XML reporter
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const xmlEscape = require("../util/xml-escape");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
|
||||
let output = "";
|
||||
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
|
||||
output += "<jslint>";
|
||||
|
||||
results.forEach(result => {
|
||||
const messages = result.messages;
|
||||
|
||||
output += `<file name="${result.filePath}">`;
|
||||
|
||||
messages.forEach(message => {
|
||||
output += [
|
||||
`<issue line="${message.line}"`,
|
||||
`char="${message.column}"`,
|
||||
`evidence="${xmlEscape(message.source || "")}"`,
|
||||
`reason="${xmlEscape(message.message || "")}${message.ruleId ? ` (${message.ruleId})` : ""}" />`
|
||||
].join(" ");
|
||||
});
|
||||
|
||||
output += "</file>";
|
||||
|
||||
});
|
||||
|
||||
output += "</jslint>";
|
||||
|
||||
return output;
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @fileoverview JSON reporter
|
||||
* @author Burak Yigit Kaya aka BYK
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
return JSON.stringify(results);
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @fileoverview jUnit Reporter
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const xmlEscape = require("../util/xml-escape");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helper Functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the severity of warning or error
|
||||
* @param {Object} message message object to examine
|
||||
* @returns {string} severity level
|
||||
* @private
|
||||
*/
|
||||
function getMessageType(message) {
|
||||
if (message.fatal || message.severity === 2) {
|
||||
return "Error";
|
||||
}
|
||||
return "Warning";
|
||||
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
|
||||
let output = "";
|
||||
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
||||
output += "<testsuites>\n";
|
||||
|
||||
results.forEach(result => {
|
||||
|
||||
const messages = result.messages;
|
||||
|
||||
if (messages.length > 0) {
|
||||
output += `<testsuite package="org.eslint" time="0" tests="${messages.length}" errors="${messages.length}" name="${result.filePath}">\n`;
|
||||
messages.forEach(message => {
|
||||
const type = message.fatal ? "error" : "failure";
|
||||
|
||||
output += `<testcase time="0" name="org.eslint.${message.ruleId || "unknown"}">`;
|
||||
output += `<${type} message="${xmlEscape(message.message || "")}">`;
|
||||
output += "<![CDATA[";
|
||||
output += `line ${message.line || 0}, col `;
|
||||
output += `${message.column || 0}, ${getMessageType(message)}`;
|
||||
output += ` - ${xmlEscape(message.message || "")}`;
|
||||
output += (message.ruleId ? ` (${message.ruleId})` : "");
|
||||
output += "]]>";
|
||||
output += `</${type}>`;
|
||||
output += "</testcase>\n";
|
||||
});
|
||||
output += "</testsuite>\n";
|
||||
} else {
|
||||
output += `<testsuite package="org.eslint" time="0" tests="1" errors="0" name="${result.filePath}">\n`;
|
||||
output += `<testcase time="0" name="${result.filePath}" />\n`;
|
||||
output += "</testsuite>\n";
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
output += "</testsuites>\n";
|
||||
|
||||
return output;
|
||||
};
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @fileoverview Stylish reporter
|
||||
* @author Sindre Sorhus
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const chalk = require("chalk"),
|
||||
stripAnsi = require("strip-ansi"),
|
||||
table = require("text-table");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Given a word and a count, append an s if count is not one.
|
||||
* @param {string} word A word in its singular form.
|
||||
* @param {int} count A number controlling whether word should be pluralized.
|
||||
* @returns {string} The original word with an s on the end if count is not one.
|
||||
*/
|
||||
function pluralize(word, count) {
|
||||
return (count === 1 ? word : `${word}s`);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
|
||||
let output = "\n",
|
||||
errorCount = 0,
|
||||
warningCount = 0,
|
||||
fixableErrorCount = 0,
|
||||
fixableWarningCount = 0,
|
||||
summaryColor = "yellow";
|
||||
|
||||
results.forEach(result => {
|
||||
const messages = result.messages;
|
||||
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
errorCount += result.errorCount;
|
||||
warningCount += result.warningCount;
|
||||
fixableErrorCount += result.fixableErrorCount;
|
||||
fixableWarningCount += result.fixableWarningCount;
|
||||
|
||||
output += `${chalk.underline(result.filePath)}\n`;
|
||||
|
||||
output += `${table(
|
||||
messages.map(message => {
|
||||
let messageType;
|
||||
|
||||
if (message.fatal || message.severity === 2) {
|
||||
messageType = chalk.red("error");
|
||||
summaryColor = "red";
|
||||
} else {
|
||||
messageType = chalk.yellow("warning");
|
||||
}
|
||||
|
||||
return [
|
||||
"",
|
||||
message.line || 0,
|
||||
message.column || 0,
|
||||
messageType,
|
||||
message.message.replace(/([^ ])\.$/, "$1"),
|
||||
chalk.dim(message.ruleId || "")
|
||||
];
|
||||
}),
|
||||
{
|
||||
align: ["", "r", "l"],
|
||||
stringLength(str) {
|
||||
return stripAnsi(str).length;
|
||||
}
|
||||
}
|
||||
).split("\n").map(el => el.replace(/(\d+)\s+(\d+)/, (m, p1, p2) => chalk.dim(`${p1}:${p2}`))).join("\n")}\n\n`;
|
||||
});
|
||||
|
||||
const total = errorCount + warningCount;
|
||||
|
||||
if (total > 0) {
|
||||
output += chalk[summaryColor].bold([
|
||||
"\u2716 ", total, pluralize(" problem", total),
|
||||
" (", errorCount, pluralize(" error", errorCount), ", ",
|
||||
warningCount, pluralize(" warning", warningCount), ")\n"
|
||||
].join(""));
|
||||
|
||||
if (fixableErrorCount > 0 || fixableWarningCount > 0) {
|
||||
output += chalk[summaryColor].bold([
|
||||
" ", fixableErrorCount, pluralize(" error", fixableErrorCount), " and ",
|
||||
fixableWarningCount, pluralize(" warning", fixableWarningCount),
|
||||
" potentially fixable with the `--fix` option.\n"
|
||||
].join(""));
|
||||
}
|
||||
}
|
||||
|
||||
return total > 0 ? output : "";
|
||||
};
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @fileoverview "table reporter.
|
||||
* @author Gajus Kuizinas <gajus@gajus.com>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const chalk = require("chalk"),
|
||||
table = require("table").table,
|
||||
pluralize = require("pluralize");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Draws text table.
|
||||
* @param {Array<Object>} messages Error messages relating to a specific file.
|
||||
* @returns {string} A text table.
|
||||
*/
|
||||
function drawTable(messages) {
|
||||
const rows = [];
|
||||
|
||||
if (messages.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
rows.push([
|
||||
chalk.bold("Line"),
|
||||
chalk.bold("Column"),
|
||||
chalk.bold("Type"),
|
||||
chalk.bold("Message"),
|
||||
chalk.bold("Rule ID")
|
||||
]);
|
||||
|
||||
messages.forEach(message => {
|
||||
let messageType;
|
||||
|
||||
if (message.fatal || message.severity === 2) {
|
||||
messageType = chalk.red("error");
|
||||
} else {
|
||||
messageType = chalk.yellow("warning");
|
||||
}
|
||||
|
||||
rows.push([
|
||||
message.line || 0,
|
||||
message.column || 0,
|
||||
messageType,
|
||||
message.message,
|
||||
message.ruleId || ""
|
||||
]);
|
||||
});
|
||||
|
||||
return table(rows, {
|
||||
columns: {
|
||||
0: {
|
||||
width: 8,
|
||||
wrapWord: true
|
||||
},
|
||||
1: {
|
||||
width: 8,
|
||||
wrapWord: true
|
||||
},
|
||||
2: {
|
||||
width: 8,
|
||||
wrapWord: true
|
||||
},
|
||||
3: {
|
||||
paddingRight: 5,
|
||||
width: 50,
|
||||
wrapWord: true
|
||||
},
|
||||
4: {
|
||||
width: 20,
|
||||
wrapWord: true
|
||||
}
|
||||
},
|
||||
drawHorizontalLine(index) {
|
||||
return index === 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a report (multiple tables).
|
||||
* @param {Array} results Report results for every file.
|
||||
* @returns {string} A column of text tables.
|
||||
*/
|
||||
function drawReport(results) {
|
||||
let files;
|
||||
|
||||
files = results.map(result => {
|
||||
if (!result.messages.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `\n${result.filePath}\n\n${drawTable(result.messages)}`;
|
||||
});
|
||||
|
||||
files = files.filter(content => content.trim());
|
||||
|
||||
return files.join("");
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(report) {
|
||||
let result,
|
||||
errorCount,
|
||||
warningCount;
|
||||
|
||||
result = "";
|
||||
errorCount = 0;
|
||||
warningCount = 0;
|
||||
|
||||
report.forEach(fileReport => {
|
||||
errorCount += fileReport.errorCount;
|
||||
warningCount += fileReport.warningCount;
|
||||
});
|
||||
|
||||
if (errorCount || warningCount) {
|
||||
result = drawReport(report);
|
||||
}
|
||||
|
||||
result += `\n${table([
|
||||
[
|
||||
chalk.red(pluralize("Error", errorCount, true))
|
||||
],
|
||||
[
|
||||
chalk.yellow(pluralize("Warning", warningCount, true))
|
||||
]
|
||||
], {
|
||||
columns: {
|
||||
0: {
|
||||
width: 110,
|
||||
wrapWord: true
|
||||
}
|
||||
},
|
||||
drawHorizontalLine() {
|
||||
return true;
|
||||
}
|
||||
})}`;
|
||||
|
||||
return result;
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @fileoverview TAP reporter
|
||||
* @author Jonathan Kingston
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helper Functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a canonical error level string based upon the error message passed in.
|
||||
* @param {Object} message Individual error message provided by eslint
|
||||
* @returns {string} Error level string
|
||||
*/
|
||||
function getMessageType(message) {
|
||||
if (message.fatal || message.severity === 2) {
|
||||
return "error";
|
||||
}
|
||||
return "warning";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in a JavaScript object and outputs a TAP diagnostics string
|
||||
* @param {Object} diagnostic JavaScript object to be embedded as YAML into output.
|
||||
* @returns {string} diagnostics string with YAML embedded - TAP version 13 compliant
|
||||
*/
|
||||
function outputDiagnostics(diagnostic) {
|
||||
const prefix = " ";
|
||||
let output = `${prefix}---\n`;
|
||||
|
||||
output += prefix + yaml.safeDump(diagnostic).split("\n").join(`\n${prefix}`);
|
||||
output += "...\n";
|
||||
return output;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
let output = `TAP version 13\n1..${results.length}\n`;
|
||||
|
||||
results.forEach((result, id) => {
|
||||
const messages = result.messages;
|
||||
let testResult = "ok";
|
||||
let diagnostics = {};
|
||||
|
||||
if (messages.length > 0) {
|
||||
testResult = "not ok";
|
||||
|
||||
messages.forEach(message => {
|
||||
const diagnostic = {
|
||||
message: message.message,
|
||||
severity: getMessageType(message),
|
||||
data: {
|
||||
line: message.line || 0,
|
||||
column: message.column || 0,
|
||||
ruleId: message.ruleId || ""
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* If we have multiple messages place them under a messages key
|
||||
* The first error will be logged as message key
|
||||
* This is to adhere to TAP 13 loosely defined specification of having a message key
|
||||
*/
|
||||
if ("message" in diagnostics) {
|
||||
if (typeof diagnostics.messages === "undefined") {
|
||||
diagnostics.messages = [];
|
||||
}
|
||||
diagnostics.messages.push(diagnostic);
|
||||
} else {
|
||||
diagnostics = diagnostic;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
output += `${testResult} ${id + 1} - ${result.filePath}\n`;
|
||||
|
||||
// If we have an error include diagnostics
|
||||
if (messages.length > 0) {
|
||||
output += outputDiagnostics(diagnostics);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return output;
|
||||
};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @fileoverview unix-style formatter.
|
||||
* @author oshi-shinobu
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helper Functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a canonical error level string based upon the error message passed in.
|
||||
* @param {Object} message Individual error message provided by eslint
|
||||
* @returns {string} Error level string
|
||||
*/
|
||||
function getMessageType(message) {
|
||||
if (message.fatal || message.severity === 2) {
|
||||
return "Error";
|
||||
}
|
||||
return "Warning";
|
||||
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
|
||||
let output = "",
|
||||
total = 0;
|
||||
|
||||
results.forEach(result => {
|
||||
|
||||
const messages = result.messages;
|
||||
|
||||
total += messages.length;
|
||||
|
||||
messages.forEach(message => {
|
||||
|
||||
output += `${result.filePath}:`;
|
||||
output += `${message.line || 0}:`;
|
||||
output += `${message.column || 0}:`;
|
||||
output += ` ${message.message} `;
|
||||
output += `[${getMessageType(message)}${message.ruleId ? `/${message.ruleId}` : ""}]`;
|
||||
output += "\n";
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if (total > 0) {
|
||||
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @fileoverview Visual Studio compatible formatter
|
||||
* @author Ronald Pijnacker
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helper Functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the severity of warning or error
|
||||
* @param {Object} message message object to examine
|
||||
* @returns {string} severity level
|
||||
* @private
|
||||
*/
|
||||
function getMessageType(message) {
|
||||
if (message.fatal || message.severity === 2) {
|
||||
return "error";
|
||||
}
|
||||
return "warning";
|
||||
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = function(results) {
|
||||
|
||||
let output = "",
|
||||
total = 0;
|
||||
|
||||
results.forEach(result => {
|
||||
|
||||
const messages = result.messages;
|
||||
|
||||
total += messages.length;
|
||||
|
||||
messages.forEach(message => {
|
||||
|
||||
output += result.filePath;
|
||||
output += `(${message.line || 0}`;
|
||||
output += message.column ? `,${message.column}` : "";
|
||||
output += `): ${getMessageType(message)}`;
|
||||
output += message.ruleId ? ` ${message.ruleId}` : "";
|
||||
output += ` : ${message.message}`;
|
||||
output += "\n";
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if (total === 0) {
|
||||
output += "no problems";
|
||||
} else {
|
||||
output += `\n${total} problem${total !== 1 ? "s" : ""}`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
+381
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* @fileoverview Responsible for loading ignore config files and managing ignore patterns
|
||||
* @author Jonathan Rajavuori
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const fs = require("fs"),
|
||||
path = require("path"),
|
||||
ignore = require("ignore"),
|
||||
pathUtils = require("./util/path-utils");
|
||||
|
||||
const debug = require("debug")("eslint:ignored-paths");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const ESLINT_IGNORE_FILENAME = ".eslintignore";
|
||||
|
||||
/**
|
||||
* Adds `"*"` at the end of `"node_modules/"`,
|
||||
* so that subtle directories could be re-included by .gitignore patterns
|
||||
* such as `"!node_modules/should_not_ignored"`
|
||||
*/
|
||||
const DEFAULT_IGNORE_DIRS = [
|
||||
"/node_modules/*",
|
||||
"/bower_components/*"
|
||||
];
|
||||
const DEFAULT_OPTIONS = {
|
||||
dotfiles: false,
|
||||
cwd: process.cwd()
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find a file in the current directory.
|
||||
* @param {string} cwd Current working directory
|
||||
* @param {string} name File name
|
||||
* @returns {string} Path of ignore file or an empty string.
|
||||
*/
|
||||
function findFile(cwd, name) {
|
||||
const ignoreFilePath = path.resolve(cwd, name);
|
||||
|
||||
return fs.existsSync(ignoreFilePath) && fs.statSync(ignoreFilePath).isFile() ? ignoreFilePath : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an ignore file in the current directory.
|
||||
* @param {string} cwd Current working directory
|
||||
* @returns {string} Path of ignore file or an empty string.
|
||||
*/
|
||||
function findIgnoreFile(cwd) {
|
||||
return findFile(cwd, ESLINT_IGNORE_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an package.json file in the current directory.
|
||||
* @param {string} cwd Current working directory
|
||||
* @returns {string} Path of package.json file or an empty string.
|
||||
*/
|
||||
function findPackageJSONFile(cwd) {
|
||||
return findFile(cwd, "package.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge options with defaults
|
||||
* @param {Object} options Options to merge with DEFAULT_OPTIONS constant
|
||||
* @returns {Object} Merged options
|
||||
*/
|
||||
function mergeDefaultOptions(options) {
|
||||
return Object.assign({}, DEFAULT_OPTIONS, options);
|
||||
}
|
||||
|
||||
/* eslint-disable valid-jsdoc */
|
||||
/**
|
||||
* Normalize the path separators in a given string.
|
||||
* On Windows environment, this replaces `\` by `/`.
|
||||
* Otherwrise, this does nothing.
|
||||
* @param {string} str The path string to normalize.
|
||||
* @returns {string} The normalized path.
|
||||
*/
|
||||
const normalizePathSeps = path.sep === "/"
|
||||
? (str => str)
|
||||
: ((seps, str) => str.replace(seps, "/")).bind(null, new RegExp(`\\${path.sep}`, "g"));
|
||||
/* eslint-enable valid-jsdoc */
|
||||
|
||||
/**
|
||||
* Converts a glob pattern to a new glob pattern relative to a different directory
|
||||
* @param {string} globPattern The glob pattern, relative the the old base directory
|
||||
* @param {string} relativePathToOldBaseDir A relative path from the new base directory to the old one
|
||||
* @returns {string} A glob pattern relative to the new base directory
|
||||
*/
|
||||
function relativize(globPattern, relativePathToOldBaseDir) {
|
||||
if (relativePathToOldBaseDir === "") {
|
||||
return globPattern;
|
||||
}
|
||||
|
||||
const prefix = globPattern.startsWith("!") ? "!" : "";
|
||||
const globWithoutPrefix = globPattern.replace(/^!/, "");
|
||||
|
||||
if (globWithoutPrefix.startsWith("/")) {
|
||||
return `${prefix}/${normalizePathSeps(relativePathToOldBaseDir)}${globWithoutPrefix}`;
|
||||
}
|
||||
|
||||
return globPattern;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* IgnoredPaths class
|
||||
*/
|
||||
class IgnoredPaths {
|
||||
|
||||
/**
|
||||
* @param {Object} providedOptions object containing 'ignore', 'ignorePath' and 'patterns' properties
|
||||
*/
|
||||
constructor(providedOptions) {
|
||||
const options = mergeDefaultOptions(providedOptions);
|
||||
|
||||
this.cache = {};
|
||||
|
||||
this.defaultPatterns = [].concat(DEFAULT_IGNORE_DIRS, options.patterns || []);
|
||||
|
||||
this.ignoreFileDir = options.ignore !== false && options.ignorePath
|
||||
? path.dirname(path.resolve(options.cwd, options.ignorePath))
|
||||
: options.cwd;
|
||||
this.options = options;
|
||||
this._baseDir = null;
|
||||
|
||||
this.ig = {
|
||||
custom: ignore(),
|
||||
default: ignore()
|
||||
};
|
||||
|
||||
this.defaultPatterns.forEach(pattern => this.addPatternRelativeToCwd(this.ig.default, pattern));
|
||||
if (options.dotfiles !== true) {
|
||||
|
||||
/*
|
||||
* ignore files beginning with a dot, but not files in a parent or
|
||||
* ancestor directory (which in relative format will begin with `../`).
|
||||
*/
|
||||
this.addPatternRelativeToCwd(this.ig.default, ".*");
|
||||
this.addPatternRelativeToCwd(this.ig.default, "!../");
|
||||
}
|
||||
|
||||
/*
|
||||
* Add a way to keep track of ignored files. This was present in node-ignore
|
||||
* 2.x, but dropped for now as of 3.0.10.
|
||||
*/
|
||||
this.ig.custom.ignoreFiles = [];
|
||||
this.ig.default.ignoreFiles = [];
|
||||
|
||||
if (options.ignore !== false) {
|
||||
let ignorePath;
|
||||
|
||||
if (options.ignorePath) {
|
||||
debug("Using specific ignore file");
|
||||
|
||||
try {
|
||||
fs.statSync(options.ignorePath);
|
||||
ignorePath = options.ignorePath;
|
||||
} catch (e) {
|
||||
e.message = `Cannot read ignore file: ${options.ignorePath}\nError: ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
debug(`Looking for ignore file in ${options.cwd}`);
|
||||
ignorePath = findIgnoreFile(options.cwd);
|
||||
|
||||
try {
|
||||
fs.statSync(ignorePath);
|
||||
debug(`Loaded ignore file ${ignorePath}`);
|
||||
} catch (e) {
|
||||
debug("Could not find ignore file in cwd");
|
||||
}
|
||||
}
|
||||
|
||||
if (ignorePath) {
|
||||
debug(`Adding ${ignorePath}`);
|
||||
this.addIgnoreFile(this.ig.custom, ignorePath);
|
||||
this.addIgnoreFile(this.ig.default, ignorePath);
|
||||
} else {
|
||||
try {
|
||||
|
||||
// if the ignoreFile does not exist, check package.json for eslintIgnore
|
||||
const packageJSONPath = findPackageJSONFile(options.cwd);
|
||||
|
||||
if (packageJSONPath) {
|
||||
let packageJSONOptions;
|
||||
|
||||
try {
|
||||
packageJSONOptions = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"));
|
||||
} catch (e) {
|
||||
debug("Could not read package.json file to check eslintIgnore property");
|
||||
e.messageTemplate = "failed-to-read-json";
|
||||
e.messageData = {
|
||||
path: packageJSONPath,
|
||||
message: e.message
|
||||
};
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (packageJSONOptions.eslintIgnore) {
|
||||
if (Array.isArray(packageJSONOptions.eslintIgnore)) {
|
||||
packageJSONOptions.eslintIgnore.forEach(pattern => {
|
||||
this.addPatternRelativeToIgnoreFile(this.ig.custom, pattern);
|
||||
this.addPatternRelativeToIgnoreFile(this.ig.default, pattern);
|
||||
});
|
||||
} else {
|
||||
throw new TypeError("Package.json eslintIgnore property requires an array of paths");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debug("Could not find package.json to check eslintIgnore property");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ignorePattern) {
|
||||
this.addPatternRelativeToCwd(this.ig.custom, options.ignorePattern);
|
||||
this.addPatternRelativeToCwd(this.ig.default, options.ignorePattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If `ignoreFileDir` is a subdirectory of `cwd`, all paths will be normalized to be relative to `cwd`.
|
||||
* Otherwise, all paths will be normalized to be relative to `ignoreFileDir`.
|
||||
* This ensures that the final normalized ignore rule will not contain `..`, which is forbidden in
|
||||
* ignore rules.
|
||||
*/
|
||||
|
||||
addPatternRelativeToCwd(ig, pattern) {
|
||||
const baseDir = this.getBaseDir();
|
||||
const cookedPattern = baseDir === this.options.cwd
|
||||
? pattern
|
||||
: relativize(pattern, path.relative(baseDir, this.options.cwd));
|
||||
|
||||
ig.addPattern(cookedPattern);
|
||||
debug("addPatternRelativeToCwd:\n original = %j\n cooked = %j", pattern, cookedPattern);
|
||||
}
|
||||
|
||||
addPatternRelativeToIgnoreFile(ig, pattern) {
|
||||
const baseDir = this.getBaseDir();
|
||||
const cookedPattern = baseDir === this.ignoreFileDir
|
||||
? pattern
|
||||
: relativize(pattern, path.relative(baseDir, this.ignoreFileDir));
|
||||
|
||||
ig.addPattern(cookedPattern);
|
||||
debug("addPatternRelativeToIgnoreFile:\n original = %j\n cooked = %j", pattern, cookedPattern);
|
||||
}
|
||||
|
||||
// Detect the common ancestor
|
||||
getBaseDir() {
|
||||
if (!this._baseDir) {
|
||||
const a = path.resolve(this.options.cwd);
|
||||
const b = path.resolve(this.ignoreFileDir);
|
||||
let lastSepPos = 0;
|
||||
|
||||
// Set the shorter one (it's the common ancestor if one includes the other).
|
||||
this._baseDir = a.length < b.length ? a : b;
|
||||
|
||||
// Set the common ancestor.
|
||||
for (let i = 0; i < a.length && i < b.length; ++i) {
|
||||
if (a[i] !== b[i]) {
|
||||
this._baseDir = a.slice(0, lastSepPos);
|
||||
break;
|
||||
}
|
||||
if (a[i] === path.sep) {
|
||||
lastSepPos = i;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's only Windows drive letter, it needs \
|
||||
if (/^[A-Z]:$/.test(this._baseDir)) {
|
||||
this._baseDir += "\\";
|
||||
}
|
||||
|
||||
debug("baseDir = %j", this._baseDir);
|
||||
}
|
||||
return this._baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* read ignore filepath
|
||||
* @param {string} filePath, file to add to ig
|
||||
* @returns {array} raw ignore rules
|
||||
*/
|
||||
readIgnoreFile(filePath) {
|
||||
if (typeof this.cache[filePath] === "undefined") {
|
||||
this.cache[filePath] = fs.readFileSync(filePath, "utf8").split(/\r?\n/g).filter(Boolean);
|
||||
}
|
||||
return this.cache[filePath];
|
||||
}
|
||||
|
||||
/**
|
||||
* add ignore file to node-ignore instance
|
||||
* @param {Object} ig, instance of node-ignore
|
||||
* @param {string} filePath, file to add to ig
|
||||
* @returns {void}
|
||||
*/
|
||||
addIgnoreFile(ig, filePath) {
|
||||
ig.ignoreFiles.push(filePath);
|
||||
this
|
||||
.readIgnoreFile(filePath)
|
||||
.forEach(ignoreRule => this.addPatternRelativeToIgnoreFile(ig, ignoreRule));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a file path is included in the default or custom ignore patterns
|
||||
* @param {string} filepath Path to check
|
||||
* @param {string} [category=undefined] check 'default', 'custom' or both (undefined)
|
||||
* @returns {boolean} true if the file path matches one or more patterns, false otherwise
|
||||
*/
|
||||
contains(filepath, category) {
|
||||
|
||||
let result = false;
|
||||
const absolutePath = path.resolve(this.options.cwd, filepath);
|
||||
const relativePath = pathUtils.getRelativePath(absolutePath, this.getBaseDir());
|
||||
|
||||
if (typeof category === "undefined") {
|
||||
result = (this.ig.default.filter([relativePath]).length === 0) ||
|
||||
(this.ig.custom.filter([relativePath]).length === 0);
|
||||
} else {
|
||||
result = (this.ig[category].filter([relativePath]).length === 0);
|
||||
}
|
||||
debug("contains:");
|
||||
debug(" target = %j", filepath);
|
||||
debug(" result = %j", result);
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of dir patterns for glob to ignore
|
||||
* @returns {function()} method to check whether a folder should be ignored by glob.
|
||||
*/
|
||||
getIgnoredFoldersGlobChecker() {
|
||||
const baseDir = this.getBaseDir();
|
||||
const ig = ignore();
|
||||
|
||||
DEFAULT_IGNORE_DIRS.forEach(ignoreDir => this.addPatternRelativeToCwd(ig, ignoreDir));
|
||||
|
||||
if (this.options.dotfiles !== true) {
|
||||
|
||||
// Ignore hidden folders. (This cannot be ".*", or else it's not possible to unignore hidden files)
|
||||
ig.add([".*/*", "!../*"]);
|
||||
}
|
||||
|
||||
if (this.options.ignore) {
|
||||
ig.add(this.ig.custom);
|
||||
}
|
||||
|
||||
const filter = ig.createFilter();
|
||||
|
||||
return function(absolutePath) {
|
||||
const relative = pathUtils.getRelativePath(absolutePath, baseDir);
|
||||
|
||||
if (!relative) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !filter(relative);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IgnoredPaths;
|
||||
+1182
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @fileoverview Module for loading rules from files and directories.
|
||||
* @author Michael Ficarra
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const fs = require("fs"),
|
||||
path = require("path");
|
||||
|
||||
const rulesDirCache = {};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load all rule modules from specified directory.
|
||||
* @param {string} [relativeRulesDir] Path to rules directory, may be relative. Defaults to `lib/rules`.
|
||||
* @param {string} cwd Current working directory
|
||||
* @returns {Object} Loaded rule modules by rule ids (file names).
|
||||
*/
|
||||
module.exports = function(relativeRulesDir, cwd) {
|
||||
|
||||
const rulesDir = relativeRulesDir
|
||||
? path.resolve(cwd, relativeRulesDir)
|
||||
: path.join(__dirname, "rules");
|
||||
|
||||
// cache will help performance as IO operation are expensive
|
||||
if (rulesDirCache[rulesDir]) {
|
||||
return rulesDirCache[rulesDir];
|
||||
}
|
||||
|
||||
const rules = Object.create(null);
|
||||
|
||||
fs.readdirSync(rulesDir).forEach(file => {
|
||||
if (path.extname(file) !== ".js") {
|
||||
return;
|
||||
}
|
||||
rules[file.slice(0, -3)] = path.join(rulesDir, file);
|
||||
});
|
||||
rulesDirCache[rulesDir] = rules;
|
||||
|
||||
return rules;
|
||||
};
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* @fileoverview Options configuration for optionator.
|
||||
* @author George Zahariev
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const optionator = require("optionator");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Initialization and Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// exports "parse(args)", "generateHelp()", and "generateHelpForOption(optionName)"
|
||||
module.exports = optionator({
|
||||
prepend: "eslint [options] file.js [file.js] [dir]",
|
||||
defaults: {
|
||||
concatRepeatedArrays: true,
|
||||
mergeRepeatedObjects: true
|
||||
},
|
||||
options: [
|
||||
{
|
||||
heading: "Basic configuration"
|
||||
},
|
||||
{
|
||||
option: "eslintrc",
|
||||
type: "Boolean",
|
||||
default: "true",
|
||||
description: "Disable use of configuration from .eslintrc.*"
|
||||
},
|
||||
{
|
||||
option: "config",
|
||||
alias: "c",
|
||||
type: "path::String",
|
||||
description: "Use this configuration, overriding .eslintrc.* config options if present"
|
||||
},
|
||||
{
|
||||
option: "env",
|
||||
type: "[String]",
|
||||
description: "Specify environments"
|
||||
},
|
||||
{
|
||||
option: "ext",
|
||||
type: "[String]",
|
||||
default: ".js",
|
||||
description: "Specify JavaScript file extensions"
|
||||
},
|
||||
{
|
||||
option: "global",
|
||||
type: "[String]",
|
||||
description: "Define global variables"
|
||||
},
|
||||
{
|
||||
option: "parser",
|
||||
type: "String",
|
||||
description: "Specify the parser to be used"
|
||||
},
|
||||
{
|
||||
option: "parser-options",
|
||||
type: "Object",
|
||||
description: "Specify parser options"
|
||||
},
|
||||
{
|
||||
heading: "Specifying rules and plugins"
|
||||
},
|
||||
{
|
||||
option: "rulesdir",
|
||||
type: "[path::String]",
|
||||
description: "Use additional rules from this directory"
|
||||
},
|
||||
{
|
||||
option: "plugin",
|
||||
type: "[String]",
|
||||
description: "Specify plugins"
|
||||
},
|
||||
{
|
||||
option: "rule",
|
||||
type: "Object",
|
||||
description: "Specify rules"
|
||||
},
|
||||
{
|
||||
heading: "Fixing problems"
|
||||
},
|
||||
{
|
||||
option: "fix",
|
||||
type: "Boolean",
|
||||
default: false,
|
||||
description: "Automatically fix problems"
|
||||
},
|
||||
{
|
||||
option: "fix-dry-run",
|
||||
type: "Boolean",
|
||||
default: false,
|
||||
description: "Automatically fix problems without saving the changes to the file system"
|
||||
},
|
||||
{
|
||||
option: "fix-type",
|
||||
type: "Array",
|
||||
description: "Specify the types of fixes to apply (problem, suggestion, layout)"
|
||||
},
|
||||
{
|
||||
heading: "Ignoring files"
|
||||
},
|
||||
{
|
||||
option: "ignore-path",
|
||||
type: "path::String",
|
||||
description: "Specify path of ignore file"
|
||||
},
|
||||
{
|
||||
option: "ignore",
|
||||
type: "Boolean",
|
||||
default: "true",
|
||||
description: "Disable use of ignore files and patterns"
|
||||
},
|
||||
{
|
||||
option: "ignore-pattern",
|
||||
type: "[String]",
|
||||
description: "Pattern of files to ignore (in addition to those in .eslintignore)",
|
||||
concatRepeatedArrays: [true, {
|
||||
oneValuePerFlag: true
|
||||
}]
|
||||
},
|
||||
{
|
||||
heading: "Using stdin"
|
||||
},
|
||||
{
|
||||
option: "stdin",
|
||||
type: "Boolean",
|
||||
default: "false",
|
||||
description: "Lint code provided on <STDIN>"
|
||||
},
|
||||
{
|
||||
option: "stdin-filename",
|
||||
type: "String",
|
||||
description: "Specify filename to process STDIN as"
|
||||
},
|
||||
{
|
||||
heading: "Handling warnings"
|
||||
},
|
||||
{
|
||||
option: "quiet",
|
||||
type: "Boolean",
|
||||
default: "false",
|
||||
description: "Report errors only"
|
||||
},
|
||||
{
|
||||
option: "max-warnings",
|
||||
type: "Int",
|
||||
default: "-1",
|
||||
description: "Number of warnings to trigger nonzero exit code"
|
||||
},
|
||||
{
|
||||
heading: "Output"
|
||||
},
|
||||
{
|
||||
option: "output-file",
|
||||
alias: "o",
|
||||
type: "path::String",
|
||||
description: "Specify file to write report to"
|
||||
},
|
||||
{
|
||||
option: "format",
|
||||
alias: "f",
|
||||
type: "String",
|
||||
default: "stylish",
|
||||
description: "Use a specific output format"
|
||||
},
|
||||
{
|
||||
option: "color",
|
||||
type: "Boolean",
|
||||
alias: "no-color",
|
||||
description: "Force enabling/disabling of color"
|
||||
},
|
||||
{
|
||||
heading: "Inline configuration comments"
|
||||
},
|
||||
{
|
||||
option: "inline-config",
|
||||
type: "Boolean",
|
||||
default: "true",
|
||||
description: "Prevent comments from changing config or rules"
|
||||
},
|
||||
{
|
||||
option: "report-unused-disable-directives",
|
||||
type: "Boolean",
|
||||
default: false,
|
||||
description: "Adds reported errors for unused eslint-disable directives"
|
||||
},
|
||||
{
|
||||
heading: "Caching"
|
||||
},
|
||||
{
|
||||
option: "cache",
|
||||
type: "Boolean",
|
||||
default: "false",
|
||||
description: "Only check changed files"
|
||||
},
|
||||
{
|
||||
option: "cache-file",
|
||||
type: "path::String",
|
||||
default: ".eslintcache",
|
||||
description: "Path to the cache file. Deprecated: use --cache-location"
|
||||
},
|
||||
{
|
||||
option: "cache-location",
|
||||
type: "path::String",
|
||||
description: "Path to the cache file or directory"
|
||||
},
|
||||
{
|
||||
heading: "Miscellaneous"
|
||||
},
|
||||
{
|
||||
option: "init",
|
||||
type: "Boolean",
|
||||
default: "false",
|
||||
description: "Run config initialization wizard"
|
||||
},
|
||||
{
|
||||
option: "debug",
|
||||
type: "Boolean",
|
||||
default: false,
|
||||
description: "Output debugging information"
|
||||
},
|
||||
{
|
||||
option: "help",
|
||||
alias: "h",
|
||||
type: "Boolean",
|
||||
description: "Show help"
|
||||
},
|
||||
{
|
||||
option: "version",
|
||||
alias: "v",
|
||||
type: "Boolean",
|
||||
description: "Output the version number"
|
||||
},
|
||||
{
|
||||
option: "print-config",
|
||||
type: "path::String",
|
||||
description: "Print the configuration for the given file"
|
||||
}
|
||||
]
|
||||
});
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* @fileoverview A helper that translates context.report() calls from the rule API into generic problem objects
|
||||
* @author Teddy Katz
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const assert = require("assert");
|
||||
const ruleFixer = require("./util/rule-fixer");
|
||||
const interpolate = require("./util/interpolate");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Typedefs
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* An error message description
|
||||
* @typedef {Object} MessageDescriptor
|
||||
* @property {ASTNode} [node] The reported node
|
||||
* @property {Location} loc The location of the problem.
|
||||
* @property {string} message The problem message.
|
||||
* @property {Object} [data] Optional data to use to fill in placeholders in the
|
||||
* message.
|
||||
* @property {Function} [fix] The function to call that creates a fix command.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Information about the report
|
||||
* @typedef {Object} ReportInfo
|
||||
* @property {string} ruleId
|
||||
* @property {(0|1|2)} severity
|
||||
* @property {(string|undefined)} message
|
||||
* @property {(string|undefined)} messageId
|
||||
* @property {number} line
|
||||
* @property {number} column
|
||||
* @property {(number|undefined)} endLine
|
||||
* @property {(number|undefined)} endColumn
|
||||
* @property {(string|null)} nodeType
|
||||
* @property {string} source
|
||||
* @property {({text: string, range: (number[]|null)}|null)} fix
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Module Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Translates a multi-argument context.report() call into a single object argument call
|
||||
* @param {...*} args A list of arguments passed to `context.report`
|
||||
* @returns {MessageDescriptor} A normalized object containing report information
|
||||
*/
|
||||
function normalizeMultiArgReportCall(...args) {
|
||||
|
||||
// If there is one argument, it is considered to be a new-style call already.
|
||||
if (args.length === 1) {
|
||||
|
||||
// Shallow clone the object to avoid surprises if reusing the descriptor
|
||||
return Object.assign({}, args[0]);
|
||||
}
|
||||
|
||||
// If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
|
||||
if (typeof args[1] === "string") {
|
||||
return {
|
||||
node: args[0],
|
||||
message: args[1],
|
||||
data: args[2],
|
||||
fix: args[3]
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
|
||||
return {
|
||||
node: args[0],
|
||||
loc: args[1],
|
||||
message: args[2],
|
||||
data: args[3],
|
||||
fix: args[4]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that either a loc or a node was provided, and the node is valid if it was provided.
|
||||
* @param {MessageDescriptor} descriptor A descriptor to validate
|
||||
* @returns {void}
|
||||
* @throws AssertionError if neither a node nor a loc was provided, or if the node is not an object
|
||||
*/
|
||||
function assertValidNodeInfo(descriptor) {
|
||||
if (descriptor.node) {
|
||||
assert(typeof descriptor.node === "object", "Node must be an object");
|
||||
} else {
|
||||
assert(descriptor.loc, "Node must be provided when reporting error if location is not provided");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
|
||||
* @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
|
||||
* @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
|
||||
* from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
|
||||
*/
|
||||
function normalizeReportLoc(descriptor) {
|
||||
if (descriptor.loc) {
|
||||
if (descriptor.loc.start) {
|
||||
return descriptor.loc;
|
||||
}
|
||||
return { start: descriptor.loc, end: null };
|
||||
}
|
||||
return descriptor.node.loc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares items in a fixes array by range.
|
||||
* @param {Fix} a The first message.
|
||||
* @param {Fix} b The second message.
|
||||
* @returns {int} -1 if a comes before b, 1 if a comes after b, 0 if equal.
|
||||
* @private
|
||||
*/
|
||||
function compareFixesByRange(a, b) {
|
||||
return a.range[0] - b.range[0] || a.range[1] - b.range[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the given fixes array into one.
|
||||
* @param {Fix[]} fixes The fixes to merge.
|
||||
* @param {SourceCode} sourceCode The source code object to get the text between fixes.
|
||||
* @returns {{text: string, range: number[]}} The merged fixes
|
||||
*/
|
||||
function mergeFixes(fixes, sourceCode) {
|
||||
if (fixes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (fixes.length === 1) {
|
||||
return fixes[0];
|
||||
}
|
||||
|
||||
fixes.sort(compareFixesByRange);
|
||||
|
||||
const originalText = sourceCode.text;
|
||||
const start = fixes[0].range[0];
|
||||
const end = fixes[fixes.length - 1].range[1];
|
||||
let text = "";
|
||||
let lastPos = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
for (const fix of fixes) {
|
||||
assert(fix.range[0] >= lastPos, "Fix objects must not be overlapped in a report.");
|
||||
|
||||
if (fix.range[0] >= 0) {
|
||||
text += originalText.slice(Math.max(0, start, lastPos), fix.range[0]);
|
||||
}
|
||||
text += fix.text;
|
||||
lastPos = fix.range[1];
|
||||
}
|
||||
text += originalText.slice(Math.max(0, start, lastPos), end);
|
||||
|
||||
return { range: [start, end], text };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets one fix object from the given descriptor.
|
||||
* If the descriptor retrieves multiple fixes, this merges those to one.
|
||||
* @param {MessageDescriptor} descriptor The report descriptor.
|
||||
* @param {SourceCode} sourceCode The source code object to get text between fixes.
|
||||
* @returns {({text: string, range: number[]}|null)} The fix for the descriptor
|
||||
*/
|
||||
function normalizeFixes(descriptor, sourceCode) {
|
||||
if (typeof descriptor.fix !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @type {null | Fix | Fix[] | IterableIterator<Fix>}
|
||||
const fix = descriptor.fix(ruleFixer);
|
||||
|
||||
// Merge to one.
|
||||
if (fix && Symbol.iterator in fix) {
|
||||
return mergeFixes(Array.from(fix), sourceCode);
|
||||
}
|
||||
return fix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates information about the report from a descriptor
|
||||
* @param {Object} options Information about the problem
|
||||
* @param {string} options.ruleId Rule ID
|
||||
* @param {(0|1|2)} options.severity Rule severity
|
||||
* @param {(ASTNode|null)} options.node Node
|
||||
* @param {string} options.message Error message
|
||||
* @param {string} [options.messageId] The error message ID.
|
||||
* @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location
|
||||
* @param {{text: string, range: (number[]|null)}} options.fix The fix object
|
||||
* @returns {function(...args): ReportInfo} Function that returns information about the report
|
||||
*/
|
||||
function createProblem(options) {
|
||||
const problem = {
|
||||
ruleId: options.ruleId,
|
||||
severity: options.severity,
|
||||
message: options.message,
|
||||
line: options.loc.start.line,
|
||||
column: options.loc.start.column + 1,
|
||||
nodeType: options.node && options.node.type || null
|
||||
};
|
||||
|
||||
/*
|
||||
* If this isn’t in the conditional, some of the tests fail
|
||||
* because `messageId` is present in the problem object
|
||||
*/
|
||||
if (options.messageId) {
|
||||
problem.messageId = options.messageId;
|
||||
}
|
||||
|
||||
if (options.loc.end) {
|
||||
problem.endLine = options.loc.end.line;
|
||||
problem.endColumn = options.loc.end.column + 1;
|
||||
}
|
||||
|
||||
if (options.fix) {
|
||||
problem.fix = options.fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that converts the arguments of a `context.report` call from a rule into a reported
|
||||
* problem for the Node.js API.
|
||||
* @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object}} metadata Metadata for the reported problem
|
||||
* @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted
|
||||
* @returns {function(...args): ReportInfo} Function that returns information about the report
|
||||
*/
|
||||
|
||||
module.exports = function createReportTranslator(metadata) {
|
||||
|
||||
/*
|
||||
* `createReportTranslator` gets called once per enabled rule per file. It needs to be very performant.
|
||||
* The report translator itself (i.e. the function that `createReportTranslator` returns) gets
|
||||
* called every time a rule reports a problem, which happens much less frequently (usually, the vast
|
||||
* majority of rules don't report any problems for a given file).
|
||||
*/
|
||||
return (...args) => {
|
||||
const descriptor = normalizeMultiArgReportCall(...args);
|
||||
|
||||
assertValidNodeInfo(descriptor);
|
||||
|
||||
let computedMessage;
|
||||
|
||||
if (descriptor.messageId) {
|
||||
if (!metadata.messageIds) {
|
||||
throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata.");
|
||||
}
|
||||
const id = descriptor.messageId;
|
||||
const messages = metadata.messageIds;
|
||||
|
||||
if (descriptor.message) {
|
||||
throw new TypeError("context.report() called with a message and a messageId. Please only pass one.");
|
||||
}
|
||||
if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) {
|
||||
throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
|
||||
}
|
||||
computedMessage = messages[id];
|
||||
} else if (descriptor.message) {
|
||||
computedMessage = descriptor.message;
|
||||
} else {
|
||||
throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem.");
|
||||
}
|
||||
|
||||
|
||||
return createProblem({
|
||||
ruleId: metadata.ruleId,
|
||||
severity: metadata.severity,
|
||||
node: descriptor.node,
|
||||
message: interpolate(computedMessage, descriptor.data),
|
||||
messageId: descriptor.messageId,
|
||||
loc: normalizeReportLoc(descriptor),
|
||||
fix: normalizeFixes(descriptor, metadata.sourceCode)
|
||||
});
|
||||
};
|
||||
};
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @fileoverview Defines a storage for rules.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const lodash = require("lodash");
|
||||
const loadRules = require("./load-rules");
|
||||
const ruleReplacements = require("../conf/replacements").rules;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a stub rule that gets used when a rule with a given ID is not found.
|
||||
* @param {string} ruleId The ID of the missing rule
|
||||
* @returns {{create: function(RuleContext): Object}} A rule that reports an error at the first location
|
||||
* in the program. The report has the message `Definition for rule '${ruleId}' was not found` if the rule is unknown,
|
||||
* or `Rule '${ruleId}' was removed and replaced by: ${replacements.join(", ")}` if the rule is known to have been
|
||||
* replaced.
|
||||
*/
|
||||
const createMissingRule = lodash.memoize(ruleId => {
|
||||
const message = Object.prototype.hasOwnProperty.call(ruleReplacements, ruleId)
|
||||
? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements[ruleId].join(", ")}`
|
||||
: `Definition for rule '${ruleId}' was not found`;
|
||||
|
||||
return {
|
||||
create: context => ({
|
||||
Program() {
|
||||
context.report({
|
||||
loc: { line: 1, column: 0 },
|
||||
message
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalizes a rule module to the new-style API
|
||||
* @param {(Function|{create: Function})} rule A rule object, which can either be a function
|
||||
* ("old-style") or an object with a `create` method ("new-style")
|
||||
* @returns {{create: Function}} A new-style rule.
|
||||
*/
|
||||
function normalizeRule(rule) {
|
||||
return typeof rule === "function" ? Object.assign({ create: rule }, rule) : rule;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class Rules {
|
||||
constructor() {
|
||||
this._rules = Object.create(null);
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a rule module for rule id in storage.
|
||||
* @param {string} ruleId Rule id (file name).
|
||||
* @param {Function} ruleModule Rule handler.
|
||||
* @returns {void}
|
||||
*/
|
||||
define(ruleId, ruleModule) {
|
||||
this._rules[ruleId] = normalizeRule(ruleModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and registers all rules from passed rules directory.
|
||||
* @param {string} [rulesDir] Path to rules directory, may be relative. Defaults to `lib/rules`.
|
||||
* @param {string} cwd Current working directory
|
||||
* @returns {void}
|
||||
*/
|
||||
load(rulesDir, cwd) {
|
||||
const newRules = loadRules(rulesDir, cwd);
|
||||
|
||||
Object.keys(newRules).forEach(ruleId => {
|
||||
this.define(ruleId, newRules[ruleId]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all given rules of a plugin.
|
||||
* @param {Object} plugin The plugin object to import.
|
||||
* @param {string} pluginName The name of the plugin without prefix (`eslint-plugin-`).
|
||||
* @returns {void}
|
||||
*/
|
||||
importPlugin(plugin, pluginName) {
|
||||
if (plugin.rules) {
|
||||
Object.keys(plugin.rules).forEach(ruleId => {
|
||||
const qualifiedRuleId = `${pluginName}/${ruleId}`,
|
||||
rule = plugin.rules[ruleId];
|
||||
|
||||
this.define(qualifiedRuleId, rule);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access rule handler by id (file name).
|
||||
* @param {string} ruleId Rule id (file name).
|
||||
* @returns {{create: Function, schema: JsonSchema[]}}
|
||||
* A rule. This is normalized to always have the new-style shape with a `create` method.
|
||||
*/
|
||||
get(ruleId) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this._rules, ruleId)) {
|
||||
return createMissingRule(ruleId);
|
||||
}
|
||||
if (typeof this._rules[ruleId] === "string") {
|
||||
return normalizeRule(require(this._rules[ruleId]));
|
||||
}
|
||||
return this._rules[ruleId];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object with all currently loaded rules
|
||||
* @returns {Map} All loaded rules
|
||||
*/
|
||||
getAllLoadedRules() {
|
||||
const allRules = new Map();
|
||||
|
||||
Object.keys(this._rules).forEach(name => {
|
||||
const rule = this.get(name);
|
||||
|
||||
allRules.set(name, rule);
|
||||
});
|
||||
return allRules;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Rules;
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag wrapping non-iife in parens
|
||||
* @author Gyandeep Singh
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is an `Identifier` node which was named a given name.
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @param {string} name - An expected name of the node.
|
||||
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
|
||||
*/
|
||||
function isIdentifier(node, name) {
|
||||
return node.type === "Identifier" && node.name === name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is an argument of a specified method call.
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @param {number} index - An expected index of the node in arguments.
|
||||
* @param {string} object - An expected name of the object of the method.
|
||||
* @param {string} property - An expected name of the method.
|
||||
* @returns {boolean} `true` if the node is an argument of the specified method call.
|
||||
*/
|
||||
function isArgumentOfMethodCall(node, index, object, property) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
parent.type === "CallExpression" &&
|
||||
parent.callee.type === "MemberExpression" &&
|
||||
parent.callee.computed === false &&
|
||||
isIdentifier(parent.callee.object, object) &&
|
||||
isIdentifier(parent.callee.property, property) &&
|
||||
parent.arguments[index] === node
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is a property descriptor.
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {boolean} `true` if the node is a property descriptor.
|
||||
*/
|
||||
function isPropertyDescriptor(node) {
|
||||
|
||||
// Object.defineProperty(obj, "foo", {set: ...})
|
||||
if (isArgumentOfMethodCall(node, 2, "Object", "defineProperty") ||
|
||||
isArgumentOfMethodCall(node, 2, "Reflect", "defineProperty")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Object.defineProperties(obj, {foo: {set: ...}})
|
||||
* Object.create(proto, {foo: {set: ...}})
|
||||
*/
|
||||
const grandparent = node.parent.parent;
|
||||
|
||||
return grandparent.type === "ObjectExpression" && (
|
||||
isArgumentOfMethodCall(grandparent, 1, "Object", "create") ||
|
||||
isArgumentOfMethodCall(grandparent, 1, "Object", "defineProperties")
|
||||
);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce getter and setter pairs in objects",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/accessor-pairs"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
getWithoutSet: {
|
||||
type: "boolean"
|
||||
},
|
||||
setWithoutGet: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
getter: "Getter is not present.",
|
||||
setter: "Setter is not present."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const config = context.options[0] || {};
|
||||
const checkGetWithoutSet = config.getWithoutSet === true;
|
||||
const checkSetWithoutGet = config.setWithoutGet !== false;
|
||||
|
||||
/**
|
||||
* Checks a object expression to see if it has setter and getter both present or none.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkLonelySetGet(node) {
|
||||
let isSetPresent = false;
|
||||
let isGetPresent = false;
|
||||
const isDescriptor = isPropertyDescriptor(node);
|
||||
|
||||
for (let i = 0, end = node.properties.length; i < end; i++) {
|
||||
const property = node.properties[i];
|
||||
|
||||
let propToCheck = "";
|
||||
|
||||
if (property.kind === "init") {
|
||||
if (isDescriptor && !property.computed) {
|
||||
propToCheck = property.key.name;
|
||||
}
|
||||
} else {
|
||||
propToCheck = property.kind;
|
||||
}
|
||||
|
||||
switch (propToCheck) {
|
||||
case "set":
|
||||
isSetPresent = true;
|
||||
break;
|
||||
|
||||
case "get":
|
||||
isGetPresent = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (isSetPresent && isGetPresent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkSetWithoutGet && isSetPresent && !isGetPresent) {
|
||||
context.report({ node, messageId: "getter" });
|
||||
} else if (checkGetWithoutSet && isGetPresent && !isSetPresent) {
|
||||
context.report({ node, messageId: "setter" });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ObjectExpression(node) {
|
||||
if (checkSetWithoutGet || checkGetWithoutSet) {
|
||||
checkLonelySetGet(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce linebreaks after open and before close array brackets
|
||||
* @author Jan Peer Stöcklmair <https://github.com/JPeer264>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce linebreaks after opening and before closing array brackets",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/array-bracket-newline"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["always", "never", "consistent"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
multiline: {
|
||||
type: "boolean"
|
||||
},
|
||||
minItems: {
|
||||
type: ["integer", "null"],
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedOpeningLinebreak: "There should be no linebreak after '['.",
|
||||
unexpectedClosingLinebreak: "There should be no linebreak before ']'.",
|
||||
missingOpeningLinebreak: "A linebreak is required after '['.",
|
||||
missingClosingLinebreak: "A linebreak is required before ']'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Normalizes a given option value.
|
||||
*
|
||||
* @param {string|Object|undefined} option - An option value to parse.
|
||||
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
|
||||
*/
|
||||
function normalizeOptionValue(option) {
|
||||
let consistent = false;
|
||||
let multiline = false;
|
||||
let minItems = 0;
|
||||
|
||||
if (option) {
|
||||
if (option === "consistent") {
|
||||
consistent = true;
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
} else if (option === "always" || option.minItems === 0) {
|
||||
minItems = 0;
|
||||
} else if (option === "never") {
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
} else {
|
||||
multiline = Boolean(option.multiline);
|
||||
minItems = option.minItems || Number.POSITIVE_INFINITY;
|
||||
}
|
||||
} else {
|
||||
consistent = false;
|
||||
multiline = true;
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return { consistent, multiline, minItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a given option value.
|
||||
*
|
||||
* @param {string|Object|undefined} options - An option value to parse.
|
||||
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
|
||||
*/
|
||||
function normalizeOptions(options) {
|
||||
const value = normalizeOptionValue(options);
|
||||
|
||||
return { ArrayExpression: value, ArrayPattern: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a linebreak after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoBeginningLinebreak(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc,
|
||||
messageId: "unexpectedOpeningLinebreak",
|
||||
fix(fixer) {
|
||||
const nextToken = sourceCode.getTokenAfter(token, { includeComments: true });
|
||||
|
||||
if (astUtils.isCommentToken(nextToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.removeRange([token.range[1], nextToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a linebreak before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoEndingLinebreak(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc,
|
||||
messageId: "unexpectedClosingLinebreak",
|
||||
fix(fixer) {
|
||||
const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
|
||||
|
||||
if (astUtils.isCommentToken(previousToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.removeRange([previousToken.range[1], token.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a linebreak after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredBeginningLinebreak(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc,
|
||||
messageId: "missingOpeningLinebreak",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(token, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a linebreak before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredEndingLinebreak(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc,
|
||||
messageId: "missingClosingLinebreak",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(token, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given node if it violated this rule.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check. This is an ArrayExpression node or an ArrayPattern node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function check(node) {
|
||||
const elements = node.elements;
|
||||
const normalizedOptions = normalizeOptions(context.options[0]);
|
||||
const options = normalizedOptions[node.type];
|
||||
const openBracket = sourceCode.getFirstToken(node);
|
||||
const closeBracket = sourceCode.getLastToken(node);
|
||||
const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true });
|
||||
const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true });
|
||||
const first = sourceCode.getTokenAfter(openBracket);
|
||||
const last = sourceCode.getTokenBefore(closeBracket);
|
||||
|
||||
const needsLinebreaks = (
|
||||
elements.length >= options.minItems ||
|
||||
(
|
||||
options.multiline &&
|
||||
elements.length > 0 &&
|
||||
firstIncComment.loc.start.line !== lastIncComment.loc.end.line
|
||||
) ||
|
||||
(
|
||||
elements.length === 0 &&
|
||||
firstIncComment.type === "Block" &&
|
||||
firstIncComment.loc.start.line !== lastIncComment.loc.end.line &&
|
||||
firstIncComment === lastIncComment
|
||||
) ||
|
||||
(
|
||||
options.consistent &&
|
||||
firstIncComment.loc.start.line !== openBracket.loc.end.line
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Use tokens or comments to check multiline or not.
|
||||
* But use only tokens to check whether linebreaks are needed.
|
||||
* This allows:
|
||||
* var arr = [ // eslint-disable-line foo
|
||||
* 'a'
|
||||
* ]
|
||||
*/
|
||||
|
||||
if (needsLinebreaks) {
|
||||
if (astUtils.isTokenOnSameLine(openBracket, first)) {
|
||||
reportRequiredBeginningLinebreak(node, openBracket);
|
||||
}
|
||||
if (astUtils.isTokenOnSameLine(last, closeBracket)) {
|
||||
reportRequiredEndingLinebreak(node, closeBracket);
|
||||
}
|
||||
} else {
|
||||
if (!astUtils.isTokenOnSameLine(openBracket, first)) {
|
||||
reportNoBeginningLinebreak(node, openBracket);
|
||||
}
|
||||
if (!astUtils.isTokenOnSameLine(last, closeBracket)) {
|
||||
reportNoEndingLinebreak(node, closeBracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ArrayPattern: check,
|
||||
ArrayExpression: check
|
||||
};
|
||||
}
|
||||
};
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* @fileoverview Disallows or enforces spaces inside of array brackets.
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent spacing inside array brackets",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/array-bracket-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
singleValue: {
|
||||
type: "boolean"
|
||||
},
|
||||
objectsInArrays: {
|
||||
type: "boolean"
|
||||
},
|
||||
arraysInArrays: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.",
|
||||
unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.",
|
||||
missingSpaceAfter: "A space is required after '{{tokenValue}}'.",
|
||||
missingSpaceBefore: "A space is required before '{{tokenValue}}'."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const spaced = context.options[0] === "always",
|
||||
sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Determines whether an option is set, relative to the spacing option.
|
||||
* If spaced is "always", then check whether option is set to false.
|
||||
* If spaced is "never", then check whether option is set to true.
|
||||
* @param {Object} option - The option to exclude.
|
||||
* @returns {boolean} Whether or not the property is excluded.
|
||||
*/
|
||||
function isOptionSet(option) {
|
||||
return context.options[1] ? context.options[1][option] === !spaced : false;
|
||||
}
|
||||
|
||||
const options = {
|
||||
spaced,
|
||||
singleElementException: isOptionSet("singleValue"),
|
||||
objectsInArraysException: isOptionSet("objectsInArrays"),
|
||||
arraysInArraysException: isOptionSet("arraysInArrays")
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a space after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoBeginningSpace(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
messageId: "unexpectedSpaceAfter",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
const nextToken = sourceCode.getTokenAfter(token);
|
||||
|
||||
return fixer.removeRange([token.range[1], nextToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a space before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoEndingSpace(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
messageId: "unexpectedSpaceBefore",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
const previousToken = sourceCode.getTokenBefore(token);
|
||||
|
||||
return fixer.removeRange([previousToken.range[1], token.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a space after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredBeginningSpace(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
messageId: "missingSpaceAfter",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(token, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a space before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredEndingSpace(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
messageId: "missingSpaceBefore",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(token, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a node is an object type
|
||||
* @param {ASTNode} node - The node to check.
|
||||
* @returns {boolean} Whether or not the node is an object type.
|
||||
*/
|
||||
function isObjectType(node) {
|
||||
return node && (node.type === "ObjectExpression" || node.type === "ObjectPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a node is an array type
|
||||
* @param {ASTNode} node - The node to check.
|
||||
* @returns {boolean} Whether or not the node is an array type.
|
||||
*/
|
||||
function isArrayType(node) {
|
||||
return node && (node.type === "ArrayExpression" || node.type === "ArrayPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the spacing around array brackets
|
||||
* @param {ASTNode} node - The node we're checking for spacing
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateArraySpacing(node) {
|
||||
if (options.spaced && node.elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const first = sourceCode.getFirstToken(node),
|
||||
second = sourceCode.getFirstToken(node, 1),
|
||||
last = node.typeAnnotation
|
||||
? sourceCode.getTokenBefore(node.typeAnnotation)
|
||||
: sourceCode.getLastToken(node),
|
||||
penultimate = sourceCode.getTokenBefore(last),
|
||||
firstElement = node.elements[0],
|
||||
lastElement = node.elements[node.elements.length - 1];
|
||||
|
||||
const openingBracketMustBeSpaced =
|
||||
options.objectsInArraysException && isObjectType(firstElement) ||
|
||||
options.arraysInArraysException && isArrayType(firstElement) ||
|
||||
options.singleElementException && node.elements.length === 1
|
||||
? !options.spaced : options.spaced;
|
||||
|
||||
const closingBracketMustBeSpaced =
|
||||
options.objectsInArraysException && isObjectType(lastElement) ||
|
||||
options.arraysInArraysException && isArrayType(lastElement) ||
|
||||
options.singleElementException && node.elements.length === 1
|
||||
? !options.spaced : options.spaced;
|
||||
|
||||
if (astUtils.isTokenOnSameLine(first, second)) {
|
||||
if (openingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(first, second)) {
|
||||
reportRequiredBeginningSpace(node, first);
|
||||
}
|
||||
if (!openingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(first, second)) {
|
||||
reportNoBeginningSpace(node, first);
|
||||
}
|
||||
}
|
||||
|
||||
if (first !== penultimate && astUtils.isTokenOnSameLine(penultimate, last)) {
|
||||
if (closingBracketMustBeSpaced && !sourceCode.isSpaceBetweenTokens(penultimate, last)) {
|
||||
reportRequiredEndingSpace(node, last);
|
||||
}
|
||||
if (!closingBracketMustBeSpaced && sourceCode.isSpaceBetweenTokens(penultimate, last)) {
|
||||
reportNoEndingSpace(node, last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ArrayPattern: validateArraySpacing,
|
||||
ArrayExpression: validateArraySpacing
|
||||
};
|
||||
}
|
||||
};
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce return statements in callbacks of array's methods
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const lodash = require("lodash");
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/;
|
||||
const TARGET_METHODS = /^(?:every|filter|find(?:Index)?|map|reduce(?:Right)?|some|sort)$/;
|
||||
|
||||
/**
|
||||
* Checks a given code path segment is reachable.
|
||||
*
|
||||
* @param {CodePathSegment} segment - A segment to check.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a readable location.
|
||||
*
|
||||
* - FunctionExpression -> the function name or `function` keyword.
|
||||
* - ArrowFunctionExpression -> `=>` token.
|
||||
*
|
||||
* @param {ASTNode} node - A function node to get.
|
||||
* @param {SourceCode} sourceCode - A source code to get tokens.
|
||||
* @returns {ASTNode|Token} The node or the token of a location.
|
||||
*/
|
||||
function getLocation(node, sourceCode) {
|
||||
if (node.type === "ArrowFunctionExpression") {
|
||||
return sourceCode.getTokenBefore(node.body);
|
||||
}
|
||||
return node.id || node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a given node is a MemberExpression node which has the specified name's
|
||||
* property.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {boolean} `true` if the node is a MemberExpression node which has
|
||||
* the specified name's property
|
||||
*/
|
||||
function isTargetMethod(node) {
|
||||
return (
|
||||
node.type === "MemberExpression" &&
|
||||
TARGET_METHODS.test(astUtils.getStaticPropertyName(node) || "")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is a function expression which is the
|
||||
* callback of an array method.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check. This is one of
|
||||
* FunctionExpression or ArrowFunctionExpression.
|
||||
* @returns {boolean} `true` if the node is the callback of an array method.
|
||||
*/
|
||||
function isCallbackOfArrayMethod(node) {
|
||||
let currentNode = node;
|
||||
|
||||
while (currentNode) {
|
||||
const parent = currentNode.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
|
||||
/*
|
||||
* Looks up the destination. e.g.,
|
||||
* foo.every(nativeFoo || function foo() { ... });
|
||||
*/
|
||||
case "LogicalExpression":
|
||||
case "ConditionalExpression":
|
||||
currentNode = parent;
|
||||
break;
|
||||
|
||||
/*
|
||||
* If the upper function is IIFE, checks the destination of the return value.
|
||||
* e.g.
|
||||
* foo.every((function() {
|
||||
* // setup...
|
||||
* return function callback() { ... };
|
||||
* })());
|
||||
*/
|
||||
case "ReturnStatement": {
|
||||
const func = astUtils.getUpperFunction(parent);
|
||||
|
||||
if (func === null || !astUtils.isCallee(func)) {
|
||||
return false;
|
||||
}
|
||||
currentNode = func.parent;
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* e.g.
|
||||
* Array.from([], function() {});
|
||||
* list.every(function() {});
|
||||
*/
|
||||
case "CallExpression":
|
||||
if (astUtils.isArrayFromMethod(parent.callee)) {
|
||||
return (
|
||||
parent.arguments.length >= 2 &&
|
||||
parent.arguments[1] === currentNode
|
||||
);
|
||||
}
|
||||
if (isTargetMethod(parent.callee)) {
|
||||
return (
|
||||
parent.arguments.length >= 1 &&
|
||||
parent.arguments[0] === currentNode
|
||||
);
|
||||
}
|
||||
return false;
|
||||
|
||||
// Otherwise this node is not target.
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next: unreachable */
|
||||
return false;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "enforce `return` statements in callbacks of array methods",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/array-callback-return"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowImplicit: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expectedAtEnd: "Expected to return a value at the end of {{name}}.",
|
||||
expectedInside: "Expected to return a value in {{name}}.",
|
||||
expectedReturnValue: "{{name}} expected a return value."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0] || { allowImplicit: false };
|
||||
|
||||
let funcInfo = {
|
||||
upper: null,
|
||||
codePath: null,
|
||||
hasReturn: false,
|
||||
shouldCheck: false,
|
||||
node: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether or not the last code path segment is reachable.
|
||||
* Then reports this function if the segment is reachable.
|
||||
*
|
||||
* If the last code path segment is reachable, there are paths which are not
|
||||
* returned or thrown.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkLastSegment(node) {
|
||||
if (funcInfo.shouldCheck &&
|
||||
funcInfo.codePath.currentSegments.some(isReachable)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
loc: getLocation(node, context.getSourceCode()).loc.start,
|
||||
messageId: funcInfo.hasReturn
|
||||
? "expectedAtEnd"
|
||||
: "expectedInside",
|
||||
data: {
|
||||
name: astUtils.getFunctionNameWithKind(funcInfo.node)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// Stacks this function's information.
|
||||
onCodePathStart(codePath, node) {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
codePath,
|
||||
hasReturn: false,
|
||||
shouldCheck:
|
||||
TARGET_NODE_TYPE.test(node.type) &&
|
||||
node.body.type === "BlockStatement" &&
|
||||
isCallbackOfArrayMethod(node) &&
|
||||
!node.async &&
|
||||
!node.generator,
|
||||
node
|
||||
};
|
||||
},
|
||||
|
||||
// Pops this function's information.
|
||||
onCodePathEnd() {
|
||||
funcInfo = funcInfo.upper;
|
||||
},
|
||||
|
||||
// Checks the return statement is valid.
|
||||
ReturnStatement(node) {
|
||||
if (funcInfo.shouldCheck) {
|
||||
funcInfo.hasReturn = true;
|
||||
|
||||
// if allowImplicit: false, should also check node.argument
|
||||
if (!options.allowImplicit && !node.argument) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "expectedReturnValue",
|
||||
data: {
|
||||
name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Reports a given function if the last path is reachable.
|
||||
"FunctionExpression:exit": checkLastSegment,
|
||||
"ArrowFunctionExpression:exit": checkLastSegment
|
||||
};
|
||||
}
|
||||
};
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce line breaks after each array element
|
||||
* @author Jan Peer Stöcklmair <https://github.com/JPeer264>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce line breaks after each array element",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/array-element-newline"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["always", "never", "consistent"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
multiline: {
|
||||
type: "boolean"
|
||||
},
|
||||
minItems: {
|
||||
type: ["integer", "null"],
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedLineBreak: "There should be no linebreak here.",
|
||||
missingLineBreak: "There should be a linebreak after this element."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Normalizes a given option value.
|
||||
*
|
||||
* @param {string|Object|undefined} providedOption - An option value to parse.
|
||||
* @returns {{multiline: boolean, minItems: number}} Normalized option object.
|
||||
*/
|
||||
function normalizeOptionValue(providedOption) {
|
||||
let consistent = false;
|
||||
let multiline = false;
|
||||
let minItems;
|
||||
|
||||
const option = providedOption || "always";
|
||||
|
||||
if (!option || option === "always" || option.minItems === 0) {
|
||||
minItems = 0;
|
||||
} else if (option === "never") {
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
} else if (option === "consistent") {
|
||||
consistent = true;
|
||||
minItems = Number.POSITIVE_INFINITY;
|
||||
} else {
|
||||
multiline = Boolean(option.multiline);
|
||||
minItems = option.minItems || Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return { consistent, multiline, minItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a given option value.
|
||||
*
|
||||
* @param {string|Object|undefined} options - An option value to parse.
|
||||
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
|
||||
*/
|
||||
function normalizeOptions(options) {
|
||||
const value = normalizeOptionValue(options);
|
||||
|
||||
return { ArrayExpression: value, ArrayPattern: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a line break after the first token
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoLineBreak(token) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
|
||||
|
||||
context.report({
|
||||
loc: {
|
||||
start: tokenBefore.loc.end,
|
||||
end: token.loc.start
|
||||
},
|
||||
messageId: "unexpectedLineBreak",
|
||||
fix(fixer) {
|
||||
if (astUtils.isCommentToken(tokenBefore)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!astUtils.isTokenOnSameLine(tokenBefore, token)) {
|
||||
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " ");
|
||||
}
|
||||
|
||||
/*
|
||||
* This will check if the comma is on the same line as the next element
|
||||
* Following array:
|
||||
* [
|
||||
* 1
|
||||
* , 2
|
||||
* , 3
|
||||
* ]
|
||||
*
|
||||
* will be fixed to:
|
||||
* [
|
||||
* 1, 2, 3
|
||||
* ]
|
||||
*/
|
||||
const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true });
|
||||
|
||||
if (astUtils.isCommentToken(twoTokensBefore)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], "");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a line break after the first token
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredLineBreak(token) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
|
||||
|
||||
context.report({
|
||||
loc: {
|
||||
start: tokenBefore.loc.end,
|
||||
end: token.loc.start
|
||||
},
|
||||
messageId: "missingLineBreak",
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given node if it violated this rule.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check. This is an ObjectExpression node or an ObjectPattern node.
|
||||
* @param {{multiline: boolean, minItems: number}} options - An option object.
|
||||
* @returns {void}
|
||||
*/
|
||||
function check(node) {
|
||||
const elements = node.elements;
|
||||
const normalizedOptions = normalizeOptions(context.options[0]);
|
||||
const options = normalizedOptions[node.type];
|
||||
|
||||
let elementBreak = false;
|
||||
|
||||
/*
|
||||
* MULTILINE: true
|
||||
* loop through every element and check
|
||||
* if at least one element has linebreaks inside
|
||||
* this ensures that following is not valid (due to elements are on the same line):
|
||||
*
|
||||
* [
|
||||
* 1,
|
||||
* 2,
|
||||
* 3
|
||||
* ]
|
||||
*/
|
||||
if (options.multiline) {
|
||||
elementBreak = elements
|
||||
.filter(element => element !== null)
|
||||
.some(element => element.loc.start.line !== element.loc.end.line);
|
||||
}
|
||||
|
||||
const linebreaksCount = node.elements.map((element, i) => {
|
||||
const previousElement = elements[i - 1];
|
||||
|
||||
if (i === 0 || element === null || previousElement === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
|
||||
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
|
||||
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
|
||||
|
||||
return !astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement);
|
||||
}).filter(isBreak => isBreak === true).length;
|
||||
|
||||
const needsLinebreaks = (
|
||||
elements.length >= options.minItems ||
|
||||
(
|
||||
options.multiline &&
|
||||
elementBreak
|
||||
) ||
|
||||
(
|
||||
options.consistent &&
|
||||
linebreaksCount > 0 &&
|
||||
linebreaksCount < node.elements.length
|
||||
)
|
||||
);
|
||||
|
||||
elements.forEach((element, i) => {
|
||||
const previousElement = elements[i - 1];
|
||||
|
||||
if (i === 0 || element === null || previousElement === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
|
||||
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
|
||||
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
|
||||
|
||||
if (needsLinebreaks) {
|
||||
if (astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
|
||||
reportRequiredLineBreak(firstTokenOfCurrentElement);
|
||||
}
|
||||
} else {
|
||||
if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
|
||||
reportNoLineBreak(firstTokenOfCurrentElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ArrayPattern: check,
|
||||
ArrayExpression: check
|
||||
};
|
||||
}
|
||||
};
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* @fileoverview Rule to require braces in arrow function body.
|
||||
* @author Alberto Rodríguez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require braces around arrow function bodies",
|
||||
category: "ECMAScript 6",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/arrow-body-style"
|
||||
},
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 1
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["as-needed"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
requireReturnForObjectLiteral: { type: "boolean" }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
unexpectedOtherBlock: "Unexpected block statement surrounding arrow body.",
|
||||
unexpectedEmptyBlock: "Unexpected block statement surrounding arrow body; put a value of `undefined` immediately after the `=>`.",
|
||||
unexpectedObjectBlock: "Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the `=>`.",
|
||||
unexpectedSingleBlock: "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.",
|
||||
expectedBlock: "Expected block statement surrounding arrow body."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options;
|
||||
const always = options[0] === "always";
|
||||
const asNeeded = !options[0] || options[0] === "as-needed";
|
||||
const never = options[0] === "never";
|
||||
const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral;
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Checks whether the given node has ASI problem or not.
|
||||
* @param {Token} token The token to check.
|
||||
* @returns {boolean} `true` if it changes semantics if `;` or `}` followed by the token are removed.
|
||||
*/
|
||||
function hasASIProblem(token) {
|
||||
return token && token.type === "Punctuator" && /^[([/`+-]/.test(token.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the closing parenthesis which is the pair of the given opening parenthesis.
|
||||
* @param {Token} token The opening parenthesis token to get.
|
||||
* @returns {Token} The found closing parenthesis token.
|
||||
*/
|
||||
function findClosingParen(token) {
|
||||
let node = sourceCode.getNodeByRangeIndex(token.range[1]);
|
||||
|
||||
while (!astUtils.isParenthesised(sourceCode, node)) {
|
||||
node = node.parent;
|
||||
}
|
||||
return sourceCode.getTokenAfter(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a arrow function body needs braces
|
||||
* @param {ASTNode} node The arrow function node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validate(node) {
|
||||
const arrowBody = node.body;
|
||||
|
||||
if (arrowBody.type === "BlockStatement") {
|
||||
const blockBody = arrowBody.body;
|
||||
|
||||
if (blockBody.length !== 1 && !never) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (asNeeded && requireReturnForObjectLiteral && blockBody[0].type === "ReturnStatement" &&
|
||||
blockBody[0].argument && blockBody[0].argument.type === "ObjectExpression") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (never || asNeeded && blockBody[0].type === "ReturnStatement") {
|
||||
let messageId;
|
||||
|
||||
if (blockBody.length === 0) {
|
||||
messageId = "unexpectedEmptyBlock";
|
||||
} else if (blockBody.length > 1) {
|
||||
messageId = "unexpectedOtherBlock";
|
||||
} else if (blockBody[0].argument === null) {
|
||||
messageId = "unexpectedSingleBlock";
|
||||
} else if (astUtils.isOpeningBraceToken(sourceCode.getFirstToken(blockBody[0], { skip: 1 }))) {
|
||||
messageId = "unexpectedObjectBlock";
|
||||
} else {
|
||||
messageId = "unexpectedSingleBlock";
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: arrowBody.loc.start,
|
||||
messageId,
|
||||
fix(fixer) {
|
||||
const fixes = [];
|
||||
|
||||
if (blockBody.length !== 1 ||
|
||||
blockBody[0].type !== "ReturnStatement" ||
|
||||
!blockBody[0].argument ||
|
||||
hasASIProblem(sourceCode.getTokenAfter(arrowBody))
|
||||
) {
|
||||
return fixes;
|
||||
}
|
||||
|
||||
const openingBrace = sourceCode.getFirstToken(arrowBody);
|
||||
const closingBrace = sourceCode.getLastToken(arrowBody);
|
||||
const firstValueToken = sourceCode.getFirstToken(blockBody[0], 1);
|
||||
const lastValueToken = sourceCode.getLastToken(blockBody[0]);
|
||||
const commentsExist =
|
||||
sourceCode.commentsExistBetween(openingBrace, firstValueToken) ||
|
||||
sourceCode.commentsExistBetween(lastValueToken, closingBrace);
|
||||
|
||||
/*
|
||||
* Remove tokens around the return value.
|
||||
* If comments don't exist, remove extra spaces as well.
|
||||
*/
|
||||
if (commentsExist) {
|
||||
fixes.push(
|
||||
fixer.remove(openingBrace),
|
||||
fixer.remove(closingBrace),
|
||||
fixer.remove(sourceCode.getTokenAfter(openingBrace)) // return keyword
|
||||
);
|
||||
} else {
|
||||
fixes.push(
|
||||
fixer.removeRange([openingBrace.range[0], firstValueToken.range[0]]),
|
||||
fixer.removeRange([lastValueToken.range[1], closingBrace.range[1]])
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* If the first token of the reutrn value is `{`,
|
||||
* enclose the return value by parentheses to avoid syntax error.
|
||||
*/
|
||||
if (astUtils.isOpeningBraceToken(firstValueToken)) {
|
||||
fixes.push(
|
||||
fixer.insertTextBefore(firstValueToken, "("),
|
||||
fixer.insertTextAfter(lastValueToken, ")")
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* If the last token of the return statement is semicolon, remove it.
|
||||
* Non-block arrow body is an expression, not a statement.
|
||||
*/
|
||||
if (astUtils.isSemicolonToken(lastValueToken)) {
|
||||
fixes.push(fixer.remove(lastValueToken));
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) {
|
||||
context.report({
|
||||
node,
|
||||
loc: arrowBody.loc.start,
|
||||
messageId: "expectedBlock",
|
||||
fix(fixer) {
|
||||
const fixes = [];
|
||||
const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken);
|
||||
const firstBodyToken = sourceCode.getTokenAfter(arrowToken);
|
||||
const lastBodyToken = sourceCode.getLastToken(node);
|
||||
const isParenthesisedObjectLiteral =
|
||||
astUtils.isOpeningParenToken(firstBodyToken) &&
|
||||
astUtils.isOpeningBraceToken(sourceCode.getTokenAfter(firstBodyToken));
|
||||
|
||||
// Wrap the value by a block and a return statement.
|
||||
fixes.push(
|
||||
fixer.insertTextBefore(firstBodyToken, "{return "),
|
||||
fixer.insertTextAfter(lastBodyToken, "}")
|
||||
);
|
||||
|
||||
// If the value is object literal, remove parentheses which were forced by syntax.
|
||||
if (isParenthesisedObjectLiteral) {
|
||||
fixes.push(
|
||||
fixer.remove(firstBodyToken),
|
||||
fixer.remove(findClosingParen(firstBodyToken))
|
||||
);
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"ArrowFunctionExpression:exit": validate
|
||||
};
|
||||
}
|
||||
};
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @fileoverview Rule to require parens in arrow function arguments.
|
||||
* @author Jxck
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "require parentheses around arrow function arguments",
|
||||
category: "ECMAScript 6",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/arrow-parens"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "as-needed"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
requireForBlockBody: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedParens: "Unexpected parentheses around single function argument.",
|
||||
expectedParens: "Expected parentheses around arrow function argument.",
|
||||
|
||||
unexpectedParensInline: "Unexpected parentheses around single function argument having a body with no curly braces.",
|
||||
expectedParensBlock: "Expected parentheses around arrow function argument having a body with curly braces."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const asNeeded = context.options[0] === "as-needed";
|
||||
const requireForBlockBody = asNeeded && context.options[1] && context.options[1].requireForBlockBody === true;
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Determines whether a arrow function argument end with `)`
|
||||
* @param {ASTNode} node The arrow function node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function parens(node) {
|
||||
const isAsync = node.async;
|
||||
const firstTokenOfParam = sourceCode.getFirstToken(node, isAsync ? 1 : 0);
|
||||
|
||||
/**
|
||||
* Remove the parenthesis around a parameter
|
||||
* @param {Fixer} fixer Fixer
|
||||
* @returns {string} fixed parameter
|
||||
*/
|
||||
function fixParamsWithParenthesis(fixer) {
|
||||
const paramToken = sourceCode.getTokenAfter(firstTokenOfParam);
|
||||
|
||||
/*
|
||||
* ES8 allows Trailing commas in function parameter lists and calls
|
||||
* https://github.com/eslint/eslint/issues/8834
|
||||
*/
|
||||
const closingParenToken = sourceCode.getTokenAfter(paramToken, astUtils.isClosingParenToken);
|
||||
const asyncToken = isAsync ? sourceCode.getTokenBefore(firstTokenOfParam) : null;
|
||||
const shouldAddSpaceForAsync = asyncToken && (asyncToken.range[1] === firstTokenOfParam.range[0]);
|
||||
|
||||
return fixer.replaceTextRange([
|
||||
firstTokenOfParam.range[0],
|
||||
closingParenToken.range[1]
|
||||
], `${shouldAddSpaceForAsync ? " " : ""}${paramToken.value}`);
|
||||
}
|
||||
|
||||
// "as-needed", { "requireForBlockBody": true }: x => x
|
||||
if (
|
||||
requireForBlockBody &&
|
||||
node.params.length === 1 &&
|
||||
node.params[0].type === "Identifier" &&
|
||||
!node.params[0].typeAnnotation &&
|
||||
node.body.type !== "BlockStatement" &&
|
||||
!node.returnType
|
||||
) {
|
||||
if (astUtils.isOpeningParenToken(firstTokenOfParam)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedParensInline",
|
||||
fix: fixParamsWithParenthesis
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
requireForBlockBody &&
|
||||
node.body.type === "BlockStatement"
|
||||
) {
|
||||
if (!astUtils.isOpeningParenToken(firstTokenOfParam)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "expectedParensBlock",
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// "as-needed": x => x
|
||||
if (asNeeded &&
|
||||
node.params.length === 1 &&
|
||||
node.params[0].type === "Identifier" &&
|
||||
!node.params[0].typeAnnotation &&
|
||||
!node.returnType
|
||||
) {
|
||||
if (astUtils.isOpeningParenToken(firstTokenOfParam)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unexpectedParens",
|
||||
fix: fixParamsWithParenthesis
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstTokenOfParam.type === "Identifier") {
|
||||
const after = sourceCode.getTokenAfter(firstTokenOfParam);
|
||||
|
||||
// (x) => x
|
||||
if (after.value !== ")") {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "expectedParens",
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(firstTokenOfParam, `(${firstTokenOfParam.value})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: parens
|
||||
};
|
||||
}
|
||||
};
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* @fileoverview Rule to define spacing before/after arrow function's arrow.
|
||||
* @author Jxck
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent spacing before and after the arrow in arrow functions",
|
||||
category: "ECMAScript 6",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/arrow-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: {
|
||||
type: "boolean"
|
||||
},
|
||||
after: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expectedBefore: "Missing space before =>.",
|
||||
unexpectedBefore: "Unexpected space before =>.",
|
||||
|
||||
expectedAfter: "Missing space after =>.",
|
||||
unexpectedAfter: "Unexpected space after =>."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
// merge rules with default
|
||||
const rule = { before: true, after: true },
|
||||
option = context.options[0] || {};
|
||||
|
||||
rule.before = option.before !== false;
|
||||
rule.after = option.after !== false;
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Get tokens of arrow(`=>`) and before/after arrow.
|
||||
* @param {ASTNode} node The arrow function node.
|
||||
* @returns {Object} Tokens of arrow and before/after arrow.
|
||||
*/
|
||||
function getTokens(node) {
|
||||
const arrow = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
|
||||
|
||||
return {
|
||||
before: sourceCode.getTokenBefore(arrow),
|
||||
arrow,
|
||||
after: sourceCode.getTokenAfter(arrow)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Count spaces before/after arrow(`=>`) token.
|
||||
* @param {Object} tokens Tokens before/after arrow.
|
||||
* @returns {Object} count of space before/after arrow.
|
||||
*/
|
||||
function countSpaces(tokens) {
|
||||
const before = tokens.arrow.range[0] - tokens.before.range[1];
|
||||
const after = tokens.after.range[0] - tokens.arrow.range[1];
|
||||
|
||||
return { before, after };
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether space(s) before after arrow(`=>`) is satisfy rule.
|
||||
* if before/after value is `true`, there should be space(s).
|
||||
* if before/after value is `false`, there should be no space.
|
||||
* @param {ASTNode} node The arrow function node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function spaces(node) {
|
||||
const tokens = getTokens(node);
|
||||
const countSpace = countSpaces(tokens);
|
||||
|
||||
if (rule.before) {
|
||||
|
||||
// should be space(s) before arrow
|
||||
if (countSpace.before === 0) {
|
||||
context.report({
|
||||
node: tokens.before,
|
||||
messageId: "expectedBefore",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(tokens.arrow, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
// should be no space before arrow
|
||||
if (countSpace.before > 0) {
|
||||
context.report({
|
||||
node: tokens.before,
|
||||
messageId: "unexpectedBefore",
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([tokens.before.range[1], tokens.arrow.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.after) {
|
||||
|
||||
// should be space(s) after arrow
|
||||
if (countSpace.after === 0) {
|
||||
context.report({
|
||||
node: tokens.after,
|
||||
messageId: "expectedAfter",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(tokens.arrow, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
// should be no space after arrow
|
||||
if (countSpace.after > 0) {
|
||||
context.report({
|
||||
node: tokens.after,
|
||||
messageId: "unexpectedAfter",
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([tokens.arrow.range[1], tokens.after.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: spaces
|
||||
};
|
||||
}
|
||||
};
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @fileoverview Rule to check for "block scoped" variables by binding context
|
||||
* @author Matt DuVall <http://www.mattduvall.com>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce the use of variables within the scope they are defined",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/block-scoped-var"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
outOfScope: "'{{name}}' used outside of binding context."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
let stack = [];
|
||||
|
||||
/**
|
||||
* Makes a block scope.
|
||||
* @param {ASTNode} node - A node of a scope.
|
||||
* @returns {void}
|
||||
*/
|
||||
function enterScope(node) {
|
||||
stack.push(node.range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the last block scope.
|
||||
* @returns {void}
|
||||
*/
|
||||
function exitScope() {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given reference.
|
||||
* @param {eslint-scope.Reference} reference - A reference to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(reference) {
|
||||
const identifier = reference.identifier;
|
||||
|
||||
context.report({ node: identifier, messageId: "outOfScope", data: { name: identifier.name } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and reports references which are outside of valid scopes.
|
||||
* @param {ASTNode} node - A node to get variables.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkForVariables(node) {
|
||||
if (node.kind !== "var") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defines a predicate to check whether or not a given reference is outside of valid scope.
|
||||
const scopeRange = stack[stack.length - 1];
|
||||
|
||||
/**
|
||||
* Check if a reference is out of scope
|
||||
* @param {ASTNode} reference node to examine
|
||||
* @returns {boolean} True is its outside the scope
|
||||
* @private
|
||||
*/
|
||||
function isOutsideOfScope(reference) {
|
||||
const idRange = reference.identifier.range;
|
||||
|
||||
return idRange[0] < scopeRange[0] || idRange[1] > scopeRange[1];
|
||||
}
|
||||
|
||||
// Gets declared variables, and checks its references.
|
||||
const variables = context.getDeclaredVariables(node);
|
||||
|
||||
for (let i = 0; i < variables.length; ++i) {
|
||||
|
||||
// Reports.
|
||||
variables[i]
|
||||
.references
|
||||
.filter(isOutsideOfScope)
|
||||
.forEach(report);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
stack = [node.range];
|
||||
},
|
||||
|
||||
// Manages scopes.
|
||||
BlockStatement: enterScope,
|
||||
"BlockStatement:exit": exitScope,
|
||||
ForStatement: enterScope,
|
||||
"ForStatement:exit": exitScope,
|
||||
ForInStatement: enterScope,
|
||||
"ForInStatement:exit": exitScope,
|
||||
ForOfStatement: enterScope,
|
||||
"ForOfStatement:exit": exitScope,
|
||||
SwitchStatement: enterScope,
|
||||
"SwitchStatement:exit": exitScope,
|
||||
CatchClause: enterScope,
|
||||
"CatchClause:exit": exitScope,
|
||||
|
||||
// Finds and reports references which are outside of valid scope.
|
||||
VariableDeclaration: checkForVariables
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @fileoverview A rule to disallow or enforce spaces inside of single line blocks.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const util = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "disallow or enforce spaces inside of blocks after opening block and before closing block",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/block-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{ enum: ["always", "never"] }
|
||||
],
|
||||
|
||||
messages: {
|
||||
missing: "Requires a space {{location}} '{{token}}'",
|
||||
extra: "Unexpected space(s) {{location}} '{{token}}'"
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const always = (context.options[0] !== "never"),
|
||||
messageId = always ? "missing" : "extra",
|
||||
sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Gets the open brace token from a given node.
|
||||
* @param {ASTNode} node - A BlockStatement/SwitchStatement node to get.
|
||||
* @returns {Token} The token of the open brace.
|
||||
*/
|
||||
function getOpenBrace(node) {
|
||||
if (node.type === "SwitchStatement") {
|
||||
if (node.cases.length > 0) {
|
||||
return sourceCode.getTokenBefore(node.cases[0]);
|
||||
}
|
||||
return sourceCode.getLastToken(node, 1);
|
||||
}
|
||||
return sourceCode.getFirstToken(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not:
|
||||
* - given tokens are on same line.
|
||||
* - there is/isn't a space between given tokens.
|
||||
* @param {Token} left - A token to check.
|
||||
* @param {Token} right - The token which is next to `left`.
|
||||
* @returns {boolean}
|
||||
* When the option is `"always"`, `true` if there are one or more spaces between given tokens.
|
||||
* When the option is `"never"`, `true` if there are not any spaces between given tokens.
|
||||
* If given tokens are not on same line, it's always `true`.
|
||||
*/
|
||||
function isValid(left, right) {
|
||||
return (
|
||||
!util.isTokenOnSameLine(left, right) ||
|
||||
sourceCode.isSpaceBetweenTokens(left, right) === always
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports invalid spacing style inside braces.
|
||||
* @param {ASTNode} node - A BlockStatement/SwitchStatement node to get.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingInsideBraces(node) {
|
||||
|
||||
// Gets braces and the first/last token of content.
|
||||
const openBrace = getOpenBrace(node);
|
||||
const closeBrace = sourceCode.getLastToken(node);
|
||||
const firstToken = sourceCode.getTokenAfter(openBrace, { includeComments: true });
|
||||
const lastToken = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
|
||||
|
||||
// Skip if the node is invalid or empty.
|
||||
if (openBrace.type !== "Punctuator" ||
|
||||
openBrace.value !== "{" ||
|
||||
closeBrace.type !== "Punctuator" ||
|
||||
closeBrace.value !== "}" ||
|
||||
firstToken === closeBrace
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip line comments for option never
|
||||
if (!always && firstToken.type === "Line") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check.
|
||||
if (!isValid(openBrace, firstToken)) {
|
||||
context.report({
|
||||
node,
|
||||
loc: openBrace.loc.start,
|
||||
messageId,
|
||||
data: {
|
||||
location: "after",
|
||||
token: openBrace.value
|
||||
},
|
||||
fix(fixer) {
|
||||
if (always) {
|
||||
return fixer.insertTextBefore(firstToken, " ");
|
||||
}
|
||||
|
||||
return fixer.removeRange([openBrace.range[1], firstToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!isValid(lastToken, closeBrace)) {
|
||||
context.report({
|
||||
node,
|
||||
loc: closeBrace.loc.start,
|
||||
messageId,
|
||||
data: {
|
||||
location: "before",
|
||||
token: closeBrace.value
|
||||
},
|
||||
fix(fixer) {
|
||||
if (always) {
|
||||
return fixer.insertTextAfter(lastToken, " ");
|
||||
}
|
||||
|
||||
return fixer.removeRange([lastToken.range[1], closeBrace.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
BlockStatement: checkSpacingInsideBraces,
|
||||
SwitchStatement: checkSpacingInsideBraces
|
||||
};
|
||||
}
|
||||
};
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag block statements that do not use the one true brace style
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent brace style for blocks",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/brace-style"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["1tbs", "stroustrup", "allman"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowSingleLine: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
messages: {
|
||||
nextLineOpen: "Opening curly brace does not appear on the same line as controlling statement.",
|
||||
sameLineOpen: "Opening curly brace appears on the same line as controlling statement.",
|
||||
blockSameLine: "Statement inside of curly braces should be on next line.",
|
||||
nextLineClose: "Closing curly brace does not appear on the same line as the subsequent block.",
|
||||
singleLineClose: "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.",
|
||||
sameLineClose: "Closing curly brace appears on the same line as the subsequent block."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const style = context.options[0] || "1tbs",
|
||||
params = context.options[1] || {},
|
||||
sourceCode = context.getSourceCode();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fixes a place where a newline unexpectedly appears
|
||||
* @param {Token} firstToken The token before the unexpected newline
|
||||
* @param {Token} secondToken The token after the unexpected newline
|
||||
* @returns {Function} A fixer function to remove the newlines between the tokens
|
||||
*/
|
||||
function removeNewlineBetween(firstToken, secondToken) {
|
||||
const textRange = [firstToken.range[1], secondToken.range[0]];
|
||||
const textBetween = sourceCode.text.slice(textRange[0], textRange[1]);
|
||||
|
||||
// Don't do a fix if there is a comment between the tokens
|
||||
if (textBetween.trim()) {
|
||||
return null;
|
||||
}
|
||||
return fixer => fixer.replaceTextRange(textRange, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a pair of curly brackets based on the user's config
|
||||
* @param {Token} openingCurly The opening curly bracket
|
||||
* @param {Token} closingCurly The closing curly bracket
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateCurlyPair(openingCurly, closingCurly) {
|
||||
const tokenBeforeOpeningCurly = sourceCode.getTokenBefore(openingCurly);
|
||||
const tokenAfterOpeningCurly = sourceCode.getTokenAfter(openingCurly);
|
||||
const tokenBeforeClosingCurly = sourceCode.getTokenBefore(closingCurly);
|
||||
const singleLineException = params.allowSingleLine && astUtils.isTokenOnSameLine(openingCurly, closingCurly);
|
||||
|
||||
if (style !== "allman" && !astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly)) {
|
||||
context.report({
|
||||
node: openingCurly,
|
||||
messageId: "nextLineOpen",
|
||||
fix: removeNewlineBetween(tokenBeforeOpeningCurly, openingCurly)
|
||||
});
|
||||
}
|
||||
|
||||
if (style === "allman" && astUtils.isTokenOnSameLine(tokenBeforeOpeningCurly, openingCurly) && !singleLineException) {
|
||||
context.report({
|
||||
node: openingCurly,
|
||||
messageId: "sameLineOpen",
|
||||
fix: fixer => fixer.insertTextBefore(openingCurly, "\n")
|
||||
});
|
||||
}
|
||||
|
||||
if (astUtils.isTokenOnSameLine(openingCurly, tokenAfterOpeningCurly) && tokenAfterOpeningCurly !== closingCurly && !singleLineException) {
|
||||
context.report({
|
||||
node: openingCurly,
|
||||
messageId: "blockSameLine",
|
||||
fix: fixer => fixer.insertTextAfter(openingCurly, "\n")
|
||||
});
|
||||
}
|
||||
|
||||
if (tokenBeforeClosingCurly !== openingCurly && !singleLineException && astUtils.isTokenOnSameLine(tokenBeforeClosingCurly, closingCurly)) {
|
||||
context.report({
|
||||
node: closingCurly,
|
||||
messageId: "singleLineClose",
|
||||
fix: fixer => fixer.insertTextBefore(closingCurly, "\n")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the location of a token that appears before a keyword (e.g. a newline before `else`)
|
||||
* @param {Token} curlyToken The closing curly token. This is assumed to precede a keyword token (such as `else` or `finally`).
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateCurlyBeforeKeyword(curlyToken) {
|
||||
const keywordToken = sourceCode.getTokenAfter(curlyToken);
|
||||
|
||||
if (style === "1tbs" && !astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
|
||||
context.report({
|
||||
node: curlyToken,
|
||||
messageId: "nextLineClose",
|
||||
fix: removeNewlineBetween(curlyToken, keywordToken)
|
||||
});
|
||||
}
|
||||
|
||||
if (style !== "1tbs" && astUtils.isTokenOnSameLine(curlyToken, keywordToken)) {
|
||||
context.report({
|
||||
node: curlyToken,
|
||||
messageId: "sameLineClose",
|
||||
fix: fixer => fixer.insertTextAfter(curlyToken, "\n")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
BlockStatement(node) {
|
||||
if (!astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)) {
|
||||
validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
|
||||
}
|
||||
},
|
||||
ClassBody(node) {
|
||||
validateCurlyPair(sourceCode.getFirstToken(node), sourceCode.getLastToken(node));
|
||||
},
|
||||
SwitchStatement(node) {
|
||||
const closingCurly = sourceCode.getLastToken(node);
|
||||
const openingCurly = sourceCode.getTokenBefore(node.cases.length ? node.cases[0] : closingCurly);
|
||||
|
||||
validateCurlyPair(openingCurly, closingCurly);
|
||||
},
|
||||
IfStatement(node) {
|
||||
if (node.consequent.type === "BlockStatement" && node.alternate) {
|
||||
|
||||
// Handle the keyword after the `if` block (before `else`)
|
||||
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.consequent));
|
||||
}
|
||||
},
|
||||
TryStatement(node) {
|
||||
|
||||
// Handle the keyword after the `try` block (before `catch` or `finally`)
|
||||
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.block));
|
||||
|
||||
if (node.handler && node.finalizer) {
|
||||
|
||||
// Handle the keyword after the `catch` block (before `finally`)
|
||||
validateCurlyBeforeKeyword(sourceCode.getLastToken(node.handler.body));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* @fileoverview Enforce return after a callback.
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require `return` statements after callbacks",
|
||||
category: "Node.js and CommonJS",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/callback-return"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "array",
|
||||
items: { type: "string" }
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingReturn: "Expected return with your callback function."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const callbacks = context.options[0] || ["callback", "cb", "next"],
|
||||
sourceCode = context.getSourceCode();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find the closest parent matching a list of types.
|
||||
* @param {ASTNode} node The node whose parents we are searching
|
||||
* @param {Array} types The node types to match
|
||||
* @returns {ASTNode} The matched node or undefined.
|
||||
*/
|
||||
function findClosestParentOfType(node, types) {
|
||||
if (!node.parent) {
|
||||
return null;
|
||||
}
|
||||
if (types.indexOf(node.parent.type) === -1) {
|
||||
return findClosestParentOfType(node.parent, types);
|
||||
}
|
||||
return node.parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if a node contains only identifers
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} Whether or not the node contains only identifers
|
||||
*/
|
||||
function containsOnlyIdentifiers(node) {
|
||||
if (node.type === "Identifier") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.type === "MemberExpression") {
|
||||
if (node.object.type === "Identifier") {
|
||||
return true;
|
||||
}
|
||||
if (node.object.type === "MemberExpression") {
|
||||
return containsOnlyIdentifiers(node.object);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if a CallExpression is in our callback list.
|
||||
* @param {ASTNode} node The node to check against our callback names list.
|
||||
* @returns {boolean} Whether or not this function matches our callback name.
|
||||
*/
|
||||
function isCallback(node) {
|
||||
return containsOnlyIdentifiers(node.callee) && callbacks.indexOf(sourceCode.getText(node.callee)) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not the callback is part of a callback expression.
|
||||
* @param {ASTNode} node The callback node
|
||||
* @param {ASTNode} parentNode The expression node
|
||||
* @returns {boolean} Whether or not this is part of a callback expression
|
||||
*/
|
||||
function isCallbackExpression(node, parentNode) {
|
||||
|
||||
// ensure the parent node exists and is an expression
|
||||
if (!parentNode || parentNode.type !== "ExpressionStatement") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// cb()
|
||||
if (parentNode.expression === node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// special case for cb && cb() and similar
|
||||
if (parentNode.expression.type === "BinaryExpression" || parentNode.expression.type === "LogicalExpression") {
|
||||
if (parentNode.expression.right === node) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
|
||||
// if we're not a callback we can return
|
||||
if (!isCallback(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find the closest block, return or loop
|
||||
const closestBlock = findClosestParentOfType(node, ["BlockStatement", "ReturnStatement", "ArrowFunctionExpression"]) || {};
|
||||
|
||||
// if our parent is a return we know we're ok
|
||||
if (closestBlock.type === "ReturnStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// arrow functions don't always have blocks and implicitly return
|
||||
if (closestBlock.type === "ArrowFunctionExpression") {
|
||||
return;
|
||||
}
|
||||
|
||||
// block statements are part of functions and most if statements
|
||||
if (closestBlock.type === "BlockStatement") {
|
||||
|
||||
// find the last item in the block
|
||||
const lastItem = closestBlock.body[closestBlock.body.length - 1];
|
||||
|
||||
// if the callback is the last thing in a block that might be ok
|
||||
if (isCallbackExpression(node, lastItem)) {
|
||||
|
||||
const parentType = closestBlock.parent.type;
|
||||
|
||||
// but only if the block is part of a function
|
||||
if (parentType === "FunctionExpression" ||
|
||||
parentType === "FunctionDeclaration" ||
|
||||
parentType === "ArrowFunctionExpression"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ending a block with a return is also ok
|
||||
if (lastItem.type === "ReturnStatement") {
|
||||
|
||||
// but only if the callback is immediately before
|
||||
if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// as long as you're the child of a function at this point you should be asked to return
|
||||
if (findClosestParentOfType(node, ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"])) {
|
||||
context.report({ node, messageId: "missingReturn" });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag non-camelcased identifiers
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce camelcase naming convention",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/camelcase"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
ignoreDestructuring: {
|
||||
type: "boolean"
|
||||
},
|
||||
properties: {
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
allow: {
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
type: "string"
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
notCamelCase: "Identifier '{{name}}' is not in camel case."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0] || {};
|
||||
let properties = options.properties || "";
|
||||
const ignoreDestructuring = options.ignoreDestructuring || false;
|
||||
const allow = options.allow || [];
|
||||
|
||||
if (properties !== "always" && properties !== "never") {
|
||||
properties = "always";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
|
||||
const reported = [];
|
||||
const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]);
|
||||
|
||||
/**
|
||||
* Checks if a string contains an underscore and isn't all upper-case
|
||||
* @param {string} name The string to check.
|
||||
* @returns {boolean} if the string is underscored
|
||||
* @private
|
||||
*/
|
||||
function isUnderscored(name) {
|
||||
|
||||
// if there's an underscore, it might be A_CONSTANT, which is okay
|
||||
return name.indexOf("_") > -1 && name !== name.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string match the ignore list
|
||||
* @param {string} name The string to check.
|
||||
* @returns {boolean} if the string is ignored
|
||||
* @private
|
||||
*/
|
||||
function isAllowed(name) {
|
||||
return allow.findIndex(
|
||||
entry => name === entry || name.match(new RegExp(entry))
|
||||
) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a parent of a node is an ObjectPattern.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} if the node is inside an ObjectPattern
|
||||
* @private
|
||||
*/
|
||||
function isInsideObjectPattern(node) {
|
||||
let { parent } = node;
|
||||
|
||||
while (parent) {
|
||||
if (parent.type === "ObjectPattern") {
|
||||
return true;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an AST node as a rule violation.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node) {
|
||||
if (reported.indexOf(node) < 0) {
|
||||
reported.push(node);
|
||||
context.report({ node, messageId: "notCamelCase", data: { name: node.name } });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Identifier(node) {
|
||||
|
||||
/*
|
||||
* Leading and trailing underscores are commonly used to flag
|
||||
* private/protected identifiers, strip them
|
||||
*/
|
||||
const name = node.name.replace(/^_+|_+$/g, ""),
|
||||
effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent;
|
||||
|
||||
// First, we ignore the node if it match the ignore list
|
||||
if (isAllowed(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// MemberExpressions get special rules
|
||||
if (node.parent.type === "MemberExpression") {
|
||||
|
||||
// "never" check properties
|
||||
if (properties === "never") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always report underscored object names
|
||||
if (node.parent.object.type === "Identifier" && node.parent.object.name === node.name && isUnderscored(name)) {
|
||||
report(node);
|
||||
|
||||
// Report AssignmentExpressions only if they are the left side of the assignment
|
||||
} else if (effectiveParent.type === "AssignmentExpression" && isUnderscored(name) && (effectiveParent.right.type !== "MemberExpression" || effectiveParent.left.type === "MemberExpression" && effectiveParent.left.property.name === node.name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
/*
|
||||
* Properties have their own rules, and
|
||||
* AssignmentPattern nodes can be treated like Properties:
|
||||
* e.g.: const { no_camelcased = false } = bar;
|
||||
*/
|
||||
} else if (node.parent.type === "Property" || node.parent.type === "AssignmentPattern") {
|
||||
|
||||
if (node.parent.parent && node.parent.parent.type === "ObjectPattern") {
|
||||
if (node.parent.shorthand && node.parent.value.left && isUnderscored(name)) {
|
||||
|
||||
report(node);
|
||||
}
|
||||
|
||||
const assignmentKeyEqualsValue = node.parent.key.name === node.parent.value.name;
|
||||
|
||||
// prevent checking righthand side of destructured object
|
||||
if (node.parent.key === node && node.parent.value !== node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueIsUnderscored = node.parent.value.name && isUnderscored(name);
|
||||
|
||||
// ignore destructuring if the option is set, unless a new identifier is created
|
||||
if (valueIsUnderscored && !(assignmentKeyEqualsValue && ignoreDestructuring)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
// "never" check properties or always ignore destructuring
|
||||
if (properties === "never" || (ignoreDestructuring && isInsideObjectPattern(node))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't check right hand side of AssignmentExpression to prevent duplicate warnings
|
||||
if (isUnderscored(name) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type) && !(node.parent.right === node)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Check if it's an import specifier
|
||||
} else if (["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"].indexOf(node.parent.type) >= 0) {
|
||||
|
||||
// Report only if the local imported identifier is underscored
|
||||
if (node.parent.local && node.parent.local.name === node.name && isUnderscored(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Report anything that is underscored that isn't a CallExpression
|
||||
} else if (isUnderscored(name) && !ALLOWED_PARENT_TYPES.has(effectiveParent.type)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* @fileoverview enforce or disallow capitalization of the first letter of a comment
|
||||
* @author Kevin Partington
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const LETTER_PATTERN = require("../util/patterns/letters");
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
|
||||
WHITESPACE = /\s/g,
|
||||
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern?
|
||||
DEFAULTS = {
|
||||
ignorePattern: null,
|
||||
ignoreInlineComments: false,
|
||||
ignoreConsecutiveComments: false
|
||||
};
|
||||
|
||||
/*
|
||||
* Base schema body for defining the basic capitalization rule, ignorePattern,
|
||||
* and ignoreInlineComments values.
|
||||
* This can be used in a few different ways in the actual schema.
|
||||
*/
|
||||
const SCHEMA_BODY = {
|
||||
type: "object",
|
||||
properties: {
|
||||
ignorePattern: {
|
||||
type: "string"
|
||||
},
|
||||
ignoreInlineComments: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignoreConsecutiveComments: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Get normalized options for either block or line comments from the given
|
||||
* user-provided options.
|
||||
* - If the user-provided options is just a string, returns a normalized
|
||||
* set of options using default values for all other options.
|
||||
* - If the user-provided options is an object, then a normalized option
|
||||
* set is returned. Options specified in overrides will take priority
|
||||
* over options specified in the main options object, which will in
|
||||
* turn take priority over the rule's defaults.
|
||||
*
|
||||
* @param {Object|string} rawOptions The user-provided options.
|
||||
* @param {string} which Either "line" or "block".
|
||||
* @returns {Object} The normalized options.
|
||||
*/
|
||||
function getNormalizedOptions(rawOptions, which) {
|
||||
if (!rawOptions) {
|
||||
return Object.assign({}, DEFAULTS);
|
||||
}
|
||||
|
||||
return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get normalized options for block and line comments.
|
||||
*
|
||||
* @param {Object|string} rawOptions The user-provided options.
|
||||
* @returns {Object} An object with "Line" and "Block" keys and corresponding
|
||||
* normalized options objects.
|
||||
*/
|
||||
function getAllNormalizedOptions(rawOptions) {
|
||||
return {
|
||||
Line: getNormalizedOptions(rawOptions, "line"),
|
||||
Block: getNormalizedOptions(rawOptions, "block")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a regular expression for each ignorePattern defined in the rule
|
||||
* options.
|
||||
*
|
||||
* This is done in order to avoid invoking the RegExp constructor repeatedly.
|
||||
*
|
||||
* @param {Object} normalizedOptions The normalized rule options.
|
||||
* @returns {void}
|
||||
*/
|
||||
function createRegExpForIgnorePatterns(normalizedOptions) {
|
||||
Object.keys(normalizedOptions).forEach(key => {
|
||||
const ignorePatternStr = normalizedOptions[key].ignorePattern;
|
||||
|
||||
if (ignorePatternStr) {
|
||||
const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`);
|
||||
|
||||
normalizedOptions[key].ignorePatternRegExp = regExp;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce or disallow capitalization of the first letter of a comment",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/capitalized-comments"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [
|
||||
{ enum: ["always", "never"] },
|
||||
{
|
||||
oneOf: [
|
||||
SCHEMA_BODY,
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
line: SCHEMA_BODY,
|
||||
block: SCHEMA_BODY
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedLowercaseComment: "Comments should not begin with a lowercase character",
|
||||
unexpectedUppercaseComment: "Comments should not begin with an uppercase character"
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const capitalize = context.options[0] || "always",
|
||||
normalizedOptions = getAllNormalizedOptions(context.options[1]),
|
||||
sourceCode = context.getSourceCode();
|
||||
|
||||
createRegExpForIgnorePatterns(normalizedOptions);
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether a comment is an inline comment.
|
||||
*
|
||||
* For the purpose of this rule, a comment is inline if:
|
||||
* 1. The comment is preceded by a token on the same line; and
|
||||
* 2. The command is followed by a token on the same line.
|
||||
*
|
||||
* Note that the comment itself need not be single-line!
|
||||
*
|
||||
* Also, it follows from this definition that only block comments can
|
||||
* be considered as possibly inline. This is because line comments
|
||||
* would consume any following tokens on the same line as the comment.
|
||||
*
|
||||
* @param {ASTNode} comment The comment node to check.
|
||||
* @returns {boolean} True if the comment is an inline comment, false
|
||||
* otherwise.
|
||||
*/
|
||||
function isInlineComment(comment) {
|
||||
const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),
|
||||
nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });
|
||||
|
||||
return Boolean(
|
||||
previousToken &&
|
||||
nextToken &&
|
||||
comment.loc.start.line === previousToken.loc.end.line &&
|
||||
comment.loc.end.line === nextToken.loc.start.line
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a comment follows another comment.
|
||||
*
|
||||
* @param {ASTNode} comment The comment to check.
|
||||
* @returns {boolean} True if the comment follows a valid comment.
|
||||
*/
|
||||
function isConsecutiveComment(comment) {
|
||||
const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });
|
||||
|
||||
return Boolean(
|
||||
previousTokenOrComment &&
|
||||
["Block", "Line"].indexOf(previousTokenOrComment.type) !== -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a comment to determine if it is valid for this rule.
|
||||
*
|
||||
* @param {ASTNode} comment The comment node to process.
|
||||
* @param {Object} options The options for checking this comment.
|
||||
* @returns {boolean} True if the comment is valid, false otherwise.
|
||||
*/
|
||||
function isCommentValid(comment, options) {
|
||||
|
||||
// 1. Check for default ignore pattern.
|
||||
if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Check for custom ignore pattern.
|
||||
const commentWithoutAsterisks = comment.value
|
||||
.replace(/\*/g, "");
|
||||
|
||||
if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Check for inline comments.
|
||||
if (options.ignoreInlineComments && isInlineComment(comment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. Is this a consecutive comment (and are we tolerating those)?
|
||||
if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. Does the comment start with a possible URL?
|
||||
if (MAYBE_URL.test(commentWithoutAsterisks)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 6. Is the initial word character a letter?
|
||||
const commentWordCharsOnly = commentWithoutAsterisks
|
||||
.replace(WHITESPACE, "");
|
||||
|
||||
if (commentWordCharsOnly.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const firstWordChar = commentWordCharsOnly[0];
|
||||
|
||||
if (!LETTER_PATTERN.test(firstWordChar)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 7. Check the case of the initial word character.
|
||||
const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),
|
||||
isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();
|
||||
|
||||
if (capitalize === "always" && isLowercase) {
|
||||
return false;
|
||||
}
|
||||
if (capitalize === "never" && isUppercase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a comment to determine if it needs to be reported.
|
||||
*
|
||||
* @param {ASTNode} comment The comment node to process.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processComment(comment) {
|
||||
const options = normalizedOptions[comment.type],
|
||||
commentValid = isCommentValid(comment, options);
|
||||
|
||||
if (!commentValid) {
|
||||
const messageId = capitalize === "always"
|
||||
? "unexpectedLowercaseComment"
|
||||
: "unexpectedUppercaseComment";
|
||||
|
||||
context.report({
|
||||
node: null, // Intentionally using loc instead
|
||||
loc: comment.loc,
|
||||
messageId,
|
||||
fix(fixer) {
|
||||
const match = comment.value.match(LETTER_PATTERN);
|
||||
|
||||
return fixer.replaceTextRange(
|
||||
|
||||
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
|
||||
[comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
|
||||
capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program() {
|
||||
const comments = sourceCode.getAllComments();
|
||||
|
||||
comments.filter(token => token.type !== "Shebang").forEach(processComment);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce that all class methods use 'this'.
|
||||
* @author Patrick Williams
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce that class methods utilize `this`",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/class-methods-use-this"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptMethods: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingThis: "Expected 'this' to be used by class method '{{name}}'."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const config = context.options[0] ? Object.assign({}, context.options[0]) : {};
|
||||
const exceptMethods = new Set(config.exceptMethods || []);
|
||||
|
||||
const stack = [];
|
||||
|
||||
/**
|
||||
* Initializes the current context to false and pushes it onto the stack.
|
||||
* These booleans represent whether 'this' has been used in the context.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function enterFunction() {
|
||||
stack.push(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node is an instance method
|
||||
* @param {ASTNode} node - node to check
|
||||
* @returns {boolean} True if its an instance method
|
||||
* @private
|
||||
*/
|
||||
function isInstanceMethod(node) {
|
||||
return !node.static && node.kind !== "constructor" && node.type === "MethodDefinition";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node is an instance method not excluded by config
|
||||
* @param {ASTNode} node - node to check
|
||||
* @returns {boolean} True if it is an instance method, and not excluded by config
|
||||
* @private
|
||||
*/
|
||||
function isIncludedInstanceMethod(node) {
|
||||
return isInstanceMethod(node) && !exceptMethods.has(node.key.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we are leaving a function that is a method, and reports if 'this' has not been used.
|
||||
* Static methods and the constructor are exempt.
|
||||
* Then pops the context off the stack.
|
||||
* @param {ASTNode} node - A function node that was entered.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function exitFunction(node) {
|
||||
const methodUsesThis = stack.pop();
|
||||
|
||||
if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "missingThis",
|
||||
data: {
|
||||
name: node.parent.key.name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current context as having used 'this'.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function markThisUsed() {
|
||||
if (stack.length) {
|
||||
stack[stack.length - 1] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: enterFunction,
|
||||
"FunctionDeclaration:exit": exitFunction,
|
||||
FunctionExpression: enterFunction,
|
||||
"FunctionExpression:exit": exitFunction,
|
||||
ThisExpression: markThisUsed,
|
||||
Super: markThisUsed
|
||||
};
|
||||
}
|
||||
};
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* @fileoverview Rule to forbid or enforce dangling commas.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const lodash = require("lodash");
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_OPTIONS = Object.freeze({
|
||||
arrays: "never",
|
||||
objects: "never",
|
||||
imports: "never",
|
||||
exports: "never",
|
||||
functions: "ignore"
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks whether or not a trailing comma is allowed in a given node.
|
||||
* If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas.
|
||||
*
|
||||
* @param {ASTNode} lastItem - The node of the last element in the given node.
|
||||
* @returns {boolean} `true` if a trailing comma is allowed.
|
||||
*/
|
||||
function isTrailingCommaAllowed(lastItem) {
|
||||
return !(
|
||||
lastItem.type === "RestElement" ||
|
||||
lastItem.type === "RestProperty" ||
|
||||
lastItem.type === "ExperimentalRestProperty"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize option value.
|
||||
*
|
||||
* @param {string|Object|undefined} optionValue - The 1st option value to normalize.
|
||||
* @returns {Object} The normalized option value.
|
||||
*/
|
||||
function normalizeOptions(optionValue) {
|
||||
if (typeof optionValue === "string") {
|
||||
return {
|
||||
arrays: optionValue,
|
||||
objects: optionValue,
|
||||
imports: optionValue,
|
||||
exports: optionValue,
|
||||
|
||||
// For backward compatibility, always ignore functions.
|
||||
functions: "ignore"
|
||||
};
|
||||
}
|
||||
if (typeof optionValue === "object" && optionValue !== null) {
|
||||
return {
|
||||
arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
|
||||
objects: optionValue.objects || DEFAULT_OPTIONS.objects,
|
||||
imports: optionValue.imports || DEFAULT_OPTIONS.imports,
|
||||
exports: optionValue.exports || DEFAULT_OPTIONS.exports,
|
||||
functions: optionValue.functions || DEFAULT_OPTIONS.functions
|
||||
};
|
||||
}
|
||||
|
||||
return DEFAULT_OPTIONS;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "require or disallow trailing commas",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/comma-dangle"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: {
|
||||
definitions: {
|
||||
value: {
|
||||
enum: [
|
||||
"always-multiline",
|
||||
"always",
|
||||
"never",
|
||||
"only-multiline"
|
||||
]
|
||||
},
|
||||
valueWithIgnore: {
|
||||
enum: [
|
||||
"always-multiline",
|
||||
"always",
|
||||
"ignore",
|
||||
"never",
|
||||
"only-multiline"
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
$ref: "#/definitions/value"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
arrays: { $ref: "#/definitions/valueWithIgnore" },
|
||||
objects: { $ref: "#/definitions/valueWithIgnore" },
|
||||
imports: { $ref: "#/definitions/valueWithIgnore" },
|
||||
exports: { $ref: "#/definitions/valueWithIgnore" },
|
||||
functions: { $ref: "#/definitions/valueWithIgnore" }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected trailing comma.",
|
||||
missing: "Missing trailing comma."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = normalizeOptions(context.options[0]);
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Gets the last item of the given node.
|
||||
* @param {ASTNode} node - The node to get.
|
||||
* @returns {ASTNode|null} The last node or null.
|
||||
*/
|
||||
function getLastItem(node) {
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ObjectPattern":
|
||||
return lodash.last(node.properties);
|
||||
case "ArrayExpression":
|
||||
case "ArrayPattern":
|
||||
return lodash.last(node.elements);
|
||||
case "ImportDeclaration":
|
||||
case "ExportNamedDeclaration":
|
||||
return lodash.last(node.specifiers);
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
return lodash.last(node.params);
|
||||
case "CallExpression":
|
||||
case "NewExpression":
|
||||
return lodash.last(node.arguments);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the trailing comma token of the given node.
|
||||
* If the trailing comma does not exist, this returns the token which is
|
||||
* the insertion point of the trailing comma token.
|
||||
*
|
||||
* @param {ASTNode} node - The node to get.
|
||||
* @param {ASTNode} lastItem - The last item of the node.
|
||||
* @returns {Token} The trailing comma token or the insertion point.
|
||||
*/
|
||||
function getTrailingToken(node, lastItem) {
|
||||
switch (node.type) {
|
||||
case "ObjectExpression":
|
||||
case "ArrayExpression":
|
||||
case "CallExpression":
|
||||
case "NewExpression":
|
||||
return sourceCode.getLastToken(node, 1);
|
||||
default: {
|
||||
const nextToken = sourceCode.getTokenAfter(lastItem);
|
||||
|
||||
if (astUtils.isCommaToken(nextToken)) {
|
||||
return nextToken;
|
||||
}
|
||||
return sourceCode.getLastToken(lastItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is multiline.
|
||||
* This rule handles a given node as multiline when the closing parenthesis
|
||||
* and the last element are not on the same line.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {boolean} `true` if the node is multiline.
|
||||
*/
|
||||
function isMultiline(node) {
|
||||
const lastItem = getLastItem(node);
|
||||
|
||||
if (!lastItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const penultimateToken = getTrailingToken(node, lastItem);
|
||||
const lastToken = sourceCode.getTokenAfter(penultimateToken);
|
||||
|
||||
return lastToken.loc.end.line !== penultimateToken.loc.end.line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a trailing comma if it exists.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check. Its type is one of
|
||||
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
||||
* ImportDeclaration, and ExportNamedDeclaration.
|
||||
* @returns {void}
|
||||
*/
|
||||
function forbidTrailingComma(node) {
|
||||
const lastItem = getLastItem(node);
|
||||
|
||||
if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trailingToken = getTrailingToken(node, lastItem);
|
||||
|
||||
if (astUtils.isCommaToken(trailingToken)) {
|
||||
context.report({
|
||||
node: lastItem,
|
||||
loc: trailingToken.loc.start,
|
||||
messageId: "unexpected",
|
||||
fix(fixer) {
|
||||
return fixer.remove(trailingToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the last element of a given node if it does not have a trailing
|
||||
* comma.
|
||||
*
|
||||
* If a given node is `ArrayPattern` which has `RestElement`, the trailing
|
||||
* comma is disallowed, so report if it exists.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check. Its type is one of
|
||||
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
||||
* ImportDeclaration, and ExportNamedDeclaration.
|
||||
* @returns {void}
|
||||
*/
|
||||
function forceTrailingComma(node) {
|
||||
const lastItem = getLastItem(node);
|
||||
|
||||
if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
||||
return;
|
||||
}
|
||||
if (!isTrailingCommaAllowed(lastItem)) {
|
||||
forbidTrailingComma(node);
|
||||
return;
|
||||
}
|
||||
|
||||
const trailingToken = getTrailingToken(node, lastItem);
|
||||
|
||||
if (trailingToken.value !== ",") {
|
||||
context.report({
|
||||
node: lastItem,
|
||||
loc: trailingToken.loc.end,
|
||||
messageId: "missing",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(trailingToken, ",");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a given node is multiline, reports the last element of a given node
|
||||
* when it does not have a trailing comma.
|
||||
* Otherwise, reports a trailing comma if it exists.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check. Its type is one of
|
||||
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
||||
* ImportDeclaration, and ExportNamedDeclaration.
|
||||
* @returns {void}
|
||||
*/
|
||||
function forceTrailingCommaIfMultiline(node) {
|
||||
if (isMultiline(node)) {
|
||||
forceTrailingComma(node);
|
||||
} else {
|
||||
forbidTrailingComma(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only if a given node is not multiline, reports the last element of a given node
|
||||
* when it does not have a trailing comma.
|
||||
* Otherwise, reports a trailing comma if it exists.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check. Its type is one of
|
||||
* ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
||||
* ImportDeclaration, and ExportNamedDeclaration.
|
||||
* @returns {void}
|
||||
*/
|
||||
function allowTrailingCommaIfMultiline(node) {
|
||||
if (!isMultiline(node)) {
|
||||
forbidTrailingComma(node);
|
||||
}
|
||||
}
|
||||
|
||||
const predicate = {
|
||||
always: forceTrailingComma,
|
||||
"always-multiline": forceTrailingCommaIfMultiline,
|
||||
"only-multiline": allowTrailingCommaIfMultiline,
|
||||
never: forbidTrailingComma,
|
||||
ignore: lodash.noop
|
||||
};
|
||||
|
||||
return {
|
||||
ObjectExpression: predicate[options.objects],
|
||||
ObjectPattern: predicate[options.objects],
|
||||
|
||||
ArrayExpression: predicate[options.arrays],
|
||||
ArrayPattern: predicate[options.arrays],
|
||||
|
||||
ImportDeclaration: predicate[options.imports],
|
||||
|
||||
ExportNamedDeclaration: predicate[options.exports],
|
||||
|
||||
FunctionDeclaration: predicate[options.functions],
|
||||
FunctionExpression: predicate[options.functions],
|
||||
ArrowFunctionExpression: predicate[options.functions],
|
||||
CallExpression: predicate[options.functions],
|
||||
NewExpression: predicate[options.functions]
|
||||
};
|
||||
}
|
||||
};
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @fileoverview Comma spacing - validates spacing before and after comma
|
||||
* @author Vignesh Anand aka vegetableman.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent spacing before and after commas",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/comma-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: {
|
||||
type: "boolean"
|
||||
},
|
||||
after: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
missing: "A space is required {{loc}} ','.",
|
||||
unexpected: "There should be no space {{loc}} ','."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
const tokensAndComments = sourceCode.tokensAndComments;
|
||||
|
||||
const options = {
|
||||
before: context.options[0] ? !!context.options[0].before : false,
|
||||
after: context.options[0] ? !!context.options[0].after : true
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// list of comma tokens to ignore for the check of leading whitespace
|
||||
const commaTokensToIgnore = [];
|
||||
|
||||
/**
|
||||
* Reports a spacing error with an appropriate message.
|
||||
* @param {ASTNode} node The binary expression node to report.
|
||||
* @param {string} loc Is the error "before" or "after" the comma?
|
||||
* @param {ASTNode} otherNode The node at the left or right of `node`
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node, loc, otherNode) {
|
||||
context.report({
|
||||
node,
|
||||
fix(fixer) {
|
||||
if (options[loc]) {
|
||||
if (loc === "before") {
|
||||
return fixer.insertTextBefore(node, " ");
|
||||
}
|
||||
return fixer.insertTextAfter(node, " ");
|
||||
|
||||
}
|
||||
let start, end;
|
||||
const newText = "";
|
||||
|
||||
if (loc === "before") {
|
||||
start = otherNode.range[1];
|
||||
end = node.range[0];
|
||||
} else {
|
||||
start = node.range[1];
|
||||
end = otherNode.range[0];
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange([start, end], newText);
|
||||
|
||||
},
|
||||
messageId: options[loc] ? "missing" : "unexpected",
|
||||
data: {
|
||||
loc
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the spacing around a comma token.
|
||||
* @param {Object} tokens - The tokens to be validated.
|
||||
* @param {Token} tokens.comma The token representing the comma.
|
||||
* @param {Token} [tokens.left] The last token before the comma.
|
||||
* @param {Token} [tokens.right] The first token after the comma.
|
||||
* @param {Token|ASTNode} reportItem The item to use when reporting an error.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function validateCommaItemSpacing(tokens, reportItem) {
|
||||
if (tokens.left && astUtils.isTokenOnSameLine(tokens.left, tokens.comma) &&
|
||||
(options.before !== sourceCode.isSpaceBetweenTokens(tokens.left, tokens.comma))
|
||||
) {
|
||||
report(reportItem, "before", tokens.left);
|
||||
}
|
||||
|
||||
if (tokens.right && !options.after && tokens.right.type === "Line") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) &&
|
||||
(options.after !== sourceCode.isSpaceBetweenTokens(tokens.comma, tokens.right))
|
||||
) {
|
||||
report(reportItem, "after", tokens.right);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds null elements of the given ArrayExpression or ArrayPattern node to the ignore list.
|
||||
* @param {ASTNode} node An ArrayExpression or ArrayPattern node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function addNullElementsToIgnoreList(node) {
|
||||
let previousToken = sourceCode.getFirstToken(node);
|
||||
|
||||
node.elements.forEach(element => {
|
||||
let token;
|
||||
|
||||
if (element === null) {
|
||||
token = sourceCode.getTokenAfter(previousToken);
|
||||
|
||||
if (astUtils.isCommaToken(token)) {
|
||||
commaTokensToIgnore.push(token);
|
||||
}
|
||||
} else {
|
||||
token = sourceCode.getTokenAfter(element);
|
||||
}
|
||||
|
||||
previousToken = token;
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
"Program:exit"() {
|
||||
tokensAndComments.forEach((token, i) => {
|
||||
|
||||
if (!astUtils.isCommaToken(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (token && token.type === "JSXText") {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousToken = tokensAndComments[i - 1];
|
||||
const nextToken = tokensAndComments[i + 1];
|
||||
|
||||
validateCommaItemSpacing({
|
||||
comma: token,
|
||||
left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken,
|
||||
right: astUtils.isCommaToken(nextToken) ? null : nextToken
|
||||
}, token);
|
||||
});
|
||||
},
|
||||
ArrayExpression: addNullElementsToIgnoreList,
|
||||
ArrayPattern: addNullElementsToIgnoreList
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* @fileoverview Comma style - enforces comma styles of two types: last and first
|
||||
* @author Vignesh Anand aka vegetableman
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent comma style",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/comma-style"
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["first", "last"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptions: {
|
||||
type: "object",
|
||||
additionalProperties: {
|
||||
type: "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedLineBeforeAndAfterComma: "Bad line breaking before and after ','.",
|
||||
expectedCommaFirst: "',' should be placed first.",
|
||||
expectedCommaLast: "',' should be placed last."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const style = context.options[0] || "last",
|
||||
sourceCode = context.getSourceCode();
|
||||
const exceptions = {
|
||||
ArrayPattern: true,
|
||||
ArrowFunctionExpression: true,
|
||||
CallExpression: true,
|
||||
FunctionDeclaration: true,
|
||||
FunctionExpression: true,
|
||||
ImportDeclaration: true,
|
||||
ObjectPattern: true,
|
||||
NewExpression: true
|
||||
};
|
||||
|
||||
if (context.options.length === 2 && Object.prototype.hasOwnProperty.call(context.options[1], "exceptions")) {
|
||||
const keys = Object.keys(context.options[1].exceptions);
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
exceptions[keys[i]] = context.options[1].exceptions[keys[i]];
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Modified text based on the style
|
||||
* @param {string} styleType Style type
|
||||
* @param {string} text Source code text
|
||||
* @returns {string} modified text
|
||||
* @private
|
||||
*/
|
||||
function getReplacedText(styleType, text) {
|
||||
switch (styleType) {
|
||||
case "between":
|
||||
return `,${text.replace("\n", "")}`;
|
||||
|
||||
case "first":
|
||||
return `${text},`;
|
||||
|
||||
case "last":
|
||||
return `,${text}`;
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the fixer function for a given style.
|
||||
* @param {string} styleType comma style
|
||||
* @param {ASTNode} previousItemToken The token to check.
|
||||
* @param {ASTNode} commaToken The token to check.
|
||||
* @param {ASTNode} currentItemToken The token to check.
|
||||
* @returns {Function} Fixer function
|
||||
* @private
|
||||
*/
|
||||
function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) {
|
||||
const text =
|
||||
sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) +
|
||||
sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]);
|
||||
const range = [previousItemToken.range[1], currentItemToken.range[0]];
|
||||
|
||||
return function(fixer) {
|
||||
return fixer.replaceTextRange(range, getReplacedText(styleType, text));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the spacing around single items in lists.
|
||||
* @param {Token} previousItemToken The last token from the previous item.
|
||||
* @param {Token} commaToken The token representing the comma.
|
||||
* @param {Token} currentItemToken The first token of the current item.
|
||||
* @param {Token} reportItem The item to use when reporting an error.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) {
|
||||
|
||||
// if single line
|
||||
if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
|
||||
astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
|
||||
|
||||
// do nothing.
|
||||
|
||||
} else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
|
||||
!astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
|
||||
|
||||
// lone comma
|
||||
context.report({
|
||||
node: reportItem,
|
||||
loc: {
|
||||
line: commaToken.loc.end.line,
|
||||
column: commaToken.loc.start.column
|
||||
},
|
||||
messageId: "unexpectedLineBeforeAndAfterComma",
|
||||
fix: getFixerFunction("between", previousItemToken, commaToken, currentItemToken)
|
||||
});
|
||||
|
||||
} else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
|
||||
|
||||
context.report({
|
||||
node: reportItem,
|
||||
messageId: "expectedCommaFirst",
|
||||
fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
|
||||
});
|
||||
|
||||
} else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
|
||||
|
||||
context.report({
|
||||
node: reportItem,
|
||||
loc: {
|
||||
line: commaToken.loc.end.line,
|
||||
column: commaToken.loc.end.column
|
||||
},
|
||||
messageId: "expectedCommaLast",
|
||||
fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the comma placement with regards to a declaration/property/element
|
||||
* @param {ASTNode} node The binary expression node to check
|
||||
* @param {string} property The property of the node containing child nodes.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateComma(node, property) {
|
||||
const items = node[property],
|
||||
arrayLiteral = (node.type === "ArrayExpression" || node.type === "ArrayPattern");
|
||||
|
||||
if (items.length > 1 || arrayLiteral) {
|
||||
|
||||
// seed as opening [
|
||||
let previousItemToken = sourceCode.getFirstToken(node);
|
||||
|
||||
items.forEach(item => {
|
||||
const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken,
|
||||
currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken),
|
||||
reportItem = item || currentItemToken;
|
||||
|
||||
/*
|
||||
* This works by comparing three token locations:
|
||||
* - previousItemToken is the last token of the previous item
|
||||
* - commaToken is the location of the comma before the current item
|
||||
* - currentItemToken is the first token of the current item
|
||||
*
|
||||
* These values get switched around if item is undefined.
|
||||
* previousItemToken will refer to the last token not belonging
|
||||
* to the current item, which could be a comma or an opening
|
||||
* square bracket. currentItemToken could be a comma.
|
||||
*
|
||||
* All comparisons are done based on these tokens directly, so
|
||||
* they are always valid regardless of an undefined item.
|
||||
*/
|
||||
if (astUtils.isCommaToken(commaToken)) {
|
||||
validateCommaItemSpacing(previousItemToken, commaToken,
|
||||
currentItemToken, reportItem);
|
||||
}
|
||||
|
||||
if (item) {
|
||||
const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken);
|
||||
|
||||
previousItemToken = tokenAfterItem
|
||||
? sourceCode.getTokenBefore(tokenAfterItem)
|
||||
: sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1];
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Special case for array literals that have empty last items, such
|
||||
* as [ 1, 2, ]. These arrays only have two items show up in the
|
||||
* AST, so we need to look at the token to verify that there's no
|
||||
* dangling comma.
|
||||
*/
|
||||
if (arrayLiteral) {
|
||||
|
||||
const lastToken = sourceCode.getLastToken(node),
|
||||
nextToLastToken = sourceCode.getTokenBefore(lastToken);
|
||||
|
||||
if (astUtils.isCommaToken(nextToLastToken)) {
|
||||
validateCommaItemSpacing(
|
||||
sourceCode.getTokenBefore(nextToLastToken),
|
||||
nextToLastToken,
|
||||
lastToken,
|
||||
lastToken
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const nodes = {};
|
||||
|
||||
if (!exceptions.VariableDeclaration) {
|
||||
nodes.VariableDeclaration = function(node) {
|
||||
validateComma(node, "declarations");
|
||||
};
|
||||
}
|
||||
if (!exceptions.ObjectExpression) {
|
||||
nodes.ObjectExpression = function(node) {
|
||||
validateComma(node, "properties");
|
||||
};
|
||||
}
|
||||
if (!exceptions.ObjectPattern) {
|
||||
nodes.ObjectPattern = function(node) {
|
||||
validateComma(node, "properties");
|
||||
};
|
||||
}
|
||||
if (!exceptions.ArrayExpression) {
|
||||
nodes.ArrayExpression = function(node) {
|
||||
validateComma(node, "elements");
|
||||
};
|
||||
}
|
||||
if (!exceptions.ArrayPattern) {
|
||||
nodes.ArrayPattern = function(node) {
|
||||
validateComma(node, "elements");
|
||||
};
|
||||
}
|
||||
if (!exceptions.FunctionDeclaration) {
|
||||
nodes.FunctionDeclaration = function(node) {
|
||||
validateComma(node, "params");
|
||||
};
|
||||
}
|
||||
if (!exceptions.FunctionExpression) {
|
||||
nodes.FunctionExpression = function(node) {
|
||||
validateComma(node, "params");
|
||||
};
|
||||
}
|
||||
if (!exceptions.ArrowFunctionExpression) {
|
||||
nodes.ArrowFunctionExpression = function(node) {
|
||||
validateComma(node, "params");
|
||||
};
|
||||
}
|
||||
if (!exceptions.CallExpression) {
|
||||
nodes.CallExpression = function(node) {
|
||||
validateComma(node, "arguments");
|
||||
};
|
||||
}
|
||||
if (!exceptions.ImportDeclaration) {
|
||||
nodes.ImportDeclaration = function(node) {
|
||||
validateComma(node, "specifiers");
|
||||
};
|
||||
}
|
||||
if (!exceptions.NewExpression) {
|
||||
nodes.NewExpression = function(node) {
|
||||
validateComma(node, "arguments");
|
||||
};
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
};
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @fileoverview Counts the cyclomatic complexity of each function of the script. See http://en.wikipedia.org/wiki/Cyclomatic_complexity.
|
||||
* Counts the number of if, conditional, for, whilte, try, switch/case,
|
||||
* @author Patrick Brosset
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const lodash = require("lodash");
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce a maximum cyclomatic complexity allowed in a program",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/complexity"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
maximum: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
complex: "{{name}} has a complexity of {{complexity}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const option = context.options[0];
|
||||
let THRESHOLD = 20;
|
||||
|
||||
if (typeof option === "object" && Object.prototype.hasOwnProperty.call(option, "maximum") && typeof option.maximum === "number") {
|
||||
THRESHOLD = option.maximum;
|
||||
}
|
||||
if (typeof option === "object" && Object.prototype.hasOwnProperty.call(option, "max") && typeof option.max === "number") {
|
||||
THRESHOLD = option.max;
|
||||
}
|
||||
if (typeof option === "number") {
|
||||
THRESHOLD = option;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Using a stack to store complexity (handling nested functions)
|
||||
const fns = [];
|
||||
|
||||
/**
|
||||
* When parsing a new function, store it in our function stack
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function startFunction() {
|
||||
fns.push(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the node at the end of function
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function endFunction(node) {
|
||||
const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node));
|
||||
const complexity = fns.pop();
|
||||
|
||||
if (complexity > THRESHOLD) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "complex",
|
||||
data: { name, complexity }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the complexity of the function in context
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function increaseComplexity() {
|
||||
if (fns.length) {
|
||||
fns[fns.length - 1]++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the switch complexity in context
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function increaseSwitchComplexity(node) {
|
||||
|
||||
// Avoiding `default`
|
||||
if (node.test) {
|
||||
increaseComplexity();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
FunctionDeclaration: startFunction,
|
||||
FunctionExpression: startFunction,
|
||||
ArrowFunctionExpression: startFunction,
|
||||
"FunctionDeclaration:exit": endFunction,
|
||||
"FunctionExpression:exit": endFunction,
|
||||
"ArrowFunctionExpression:exit": endFunction,
|
||||
|
||||
CatchClause: increaseComplexity,
|
||||
ConditionalExpression: increaseComplexity,
|
||||
LogicalExpression: increaseComplexity,
|
||||
ForStatement: increaseComplexity,
|
||||
ForInStatement: increaseComplexity,
|
||||
ForOfStatement: increaseComplexity,
|
||||
IfStatement: increaseComplexity,
|
||||
SwitchCase: increaseSwitchComplexity,
|
||||
WhileStatement: increaseComplexity,
|
||||
DoWhileStatement: increaseComplexity
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @fileoverview Disallows or enforces spaces inside computed properties.
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent spacing inside computed property brackets",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/computed-property-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.",
|
||||
unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.",
|
||||
|
||||
missingSpaceBefore: "A space is required before '{{tokenValue}}'.",
|
||||
missingSpaceAfter: "A space is required after '{{tokenValue}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const propertyNameMustBeSpaced = context.options[0] === "always"; // default is "never"
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a space after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @param {Token} tokenAfter - The token after `token`.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoBeginningSpace(node, token, tokenAfter) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
messageId: "unexpectedSpaceAfter",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([token.range[1], tokenAfter.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there shouldn't be a space before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @param {Token} tokenBefore - The token before `token`.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportNoEndingSpace(node, token, tokenBefore) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
messageId: "unexpectedSpaceBefore",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([tokenBefore.range[1], token.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a space after the first token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredBeginningSpace(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
messageId: "missingSpaceAfter",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(token, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that there should be a space before the last token
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @param {Token} token - The token to use for the report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportRequiredEndingSpace(node, token) {
|
||||
context.report({
|
||||
node,
|
||||
loc: token.loc.start,
|
||||
messageId: "missingSpaceBefore",
|
||||
data: {
|
||||
tokenValue: token.value
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(token, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that checks the spacing of a node on the property name
|
||||
* that was passed in.
|
||||
* @param {string} propertyName The property on the node to check for spacing
|
||||
* @returns {Function} A function that will check spacing on a node
|
||||
*/
|
||||
function checkSpacing(propertyName) {
|
||||
return function(node) {
|
||||
if (!node.computed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const property = node[propertyName];
|
||||
|
||||
const before = sourceCode.getTokenBefore(property),
|
||||
first = sourceCode.getFirstToken(property),
|
||||
last = sourceCode.getLastToken(property),
|
||||
after = sourceCode.getTokenAfter(property);
|
||||
|
||||
if (astUtils.isTokenOnSameLine(before, first)) {
|
||||
if (propertyNameMustBeSpaced) {
|
||||
if (!sourceCode.isSpaceBetweenTokens(before, first) && astUtils.isTokenOnSameLine(before, first)) {
|
||||
reportRequiredBeginningSpace(node, before);
|
||||
}
|
||||
} else {
|
||||
if (sourceCode.isSpaceBetweenTokens(before, first)) {
|
||||
reportNoBeginningSpace(node, before, first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (astUtils.isTokenOnSameLine(last, after)) {
|
||||
if (propertyNameMustBeSpaced) {
|
||||
if (!sourceCode.isSpaceBetweenTokens(last, after) && astUtils.isTokenOnSameLine(last, after)) {
|
||||
reportRequiredEndingSpace(node, after);
|
||||
}
|
||||
} else {
|
||||
if (sourceCode.isSpaceBetweenTokens(last, after)) {
|
||||
reportNoEndingSpace(node, after, last);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Property: checkSpacing("key"),
|
||||
MemberExpression: checkSpacing("property")
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag consistent return values
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const lodash = require("lodash");
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is an `Identifier` node which was named a given name.
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @param {string} name - An expected name of the node.
|
||||
* @returns {boolean} `true` if the node is an `Identifier` node which was named as expected.
|
||||
*/
|
||||
function isIdentifier(node, name) {
|
||||
return node.type === "Identifier" && node.name === name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given code path segment is unreachable.
|
||||
* @param {CodePathSegment} segment - A CodePathSegment to check.
|
||||
* @returns {boolean} `true` if the segment is unreachable.
|
||||
*/
|
||||
function isUnreachable(segment) {
|
||||
return !segment.reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given node is a `constructor` method in an ES6 class
|
||||
* @param {ASTNode} node A node to check
|
||||
* @returns {boolean} `true` if the node is a `constructor` method
|
||||
*/
|
||||
function isClassConstructor(node) {
|
||||
return node.type === "FunctionExpression" &&
|
||||
node.parent &&
|
||||
node.parent.type === "MethodDefinition" &&
|
||||
node.parent.kind === "constructor";
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require `return` statements to either always or never specify values",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/consistent-return"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
treatUndefinedAsUnspecified: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingReturn: "Expected to return a value at the end of {{name}}.",
|
||||
missingReturnValue: "{{name}} expected a return value.",
|
||||
unexpectedReturnValue: "{{name}} expected no return value."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const treatUndefinedAsUnspecified = options.treatUndefinedAsUnspecified === true;
|
||||
let funcInfo = null;
|
||||
|
||||
/**
|
||||
* Checks whether of not the implicit returning is consistent if the last
|
||||
* code path segment is reachable.
|
||||
*
|
||||
* @param {ASTNode} node - A program/function node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkLastSegment(node) {
|
||||
let loc, name;
|
||||
|
||||
/*
|
||||
* Skip if it expected no return value or unreachable.
|
||||
* When unreachable, all paths are returned or thrown.
|
||||
*/
|
||||
if (!funcInfo.hasReturnValue ||
|
||||
funcInfo.codePath.currentSegments.every(isUnreachable) ||
|
||||
astUtils.isES5Constructor(node) ||
|
||||
isClassConstructor(node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust a location and a message.
|
||||
if (node.type === "Program") {
|
||||
|
||||
// The head of program.
|
||||
loc = { line: 1, column: 0 };
|
||||
name = "program";
|
||||
} else if (node.type === "ArrowFunctionExpression") {
|
||||
|
||||
// `=>` token
|
||||
loc = context.getSourceCode().getTokenBefore(node.body, astUtils.isArrowToken).loc.start;
|
||||
} else if (
|
||||
node.parent.type === "MethodDefinition" ||
|
||||
(node.parent.type === "Property" && node.parent.method)
|
||||
) {
|
||||
|
||||
// Method name.
|
||||
loc = node.parent.key.loc.start;
|
||||
} else {
|
||||
|
||||
// Function name or `function` keyword.
|
||||
loc = (node.id || node).loc.start;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = astUtils.getFunctionNameWithKind(node);
|
||||
}
|
||||
|
||||
// Reports.
|
||||
context.report({
|
||||
node,
|
||||
loc,
|
||||
messageId: "missingReturn",
|
||||
data: { name }
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// Initializes/Disposes state of each code path.
|
||||
onCodePathStart(codePath, node) {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
codePath,
|
||||
hasReturn: false,
|
||||
hasReturnValue: false,
|
||||
messageId: "",
|
||||
node
|
||||
};
|
||||
},
|
||||
onCodePathEnd() {
|
||||
funcInfo = funcInfo.upper;
|
||||
},
|
||||
|
||||
// Reports a given return statement if it's inconsistent.
|
||||
ReturnStatement(node) {
|
||||
const argument = node.argument;
|
||||
let hasReturnValue = Boolean(argument);
|
||||
|
||||
if (treatUndefinedAsUnspecified && hasReturnValue) {
|
||||
hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void";
|
||||
}
|
||||
|
||||
if (!funcInfo.hasReturn) {
|
||||
funcInfo.hasReturn = true;
|
||||
funcInfo.hasReturnValue = hasReturnValue;
|
||||
funcInfo.messageId = hasReturnValue ? "missingReturnValue" : "unexpectedReturnValue";
|
||||
funcInfo.data = {
|
||||
name: funcInfo.node.type === "Program"
|
||||
? "Program"
|
||||
: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
|
||||
};
|
||||
} else if (funcInfo.hasReturnValue !== hasReturnValue) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: funcInfo.messageId,
|
||||
data: funcInfo.data
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Reports a given program/function if the implicit returning is not consistent.
|
||||
"Program:exit": checkLastSegment,
|
||||
"FunctionDeclaration:exit": checkLastSegment,
|
||||
"FunctionExpression:exit": checkLastSegment,
|
||||
"ArrowFunctionExpression:exit": checkLastSegment
|
||||
};
|
||||
}
|
||||
};
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce consistent naming of "this" context variables
|
||||
* @author Raphael Pigulla
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent naming when capturing the current execution context",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/consistent-this"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
|
||||
messages: {
|
||||
aliasNotAssignedToThis: "Designated alias '{{name}}' is not assigned to 'this'.",
|
||||
unexpectedAlias: "Unexpected alias '{{name}}' for 'this'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
let aliases = [];
|
||||
|
||||
if (context.options.length === 0) {
|
||||
aliases.push("that");
|
||||
} else {
|
||||
aliases = context.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that a variable declarator or assignment expression is assigning
|
||||
* a non-'this' value to the specified alias.
|
||||
* @param {ASTNode} node - The assigning node.
|
||||
* @param {string} name - the name of the alias that was incorrectly used.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportBadAssignment(node, name) {
|
||||
context.report({ node, messageId: "aliasNotAssignedToThis", data: { name } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that an assignment to an identifier only assigns 'this' to the
|
||||
* appropriate alias, and the alias is only assigned to 'this'.
|
||||
* @param {ASTNode} node - The assigning node.
|
||||
* @param {Identifier} name - The name of the variable assigned to.
|
||||
* @param {Expression} value - The value of the assignment.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkAssignment(node, name, value) {
|
||||
const isThis = value.type === "ThisExpression";
|
||||
|
||||
if (aliases.indexOf(name) !== -1) {
|
||||
if (!isThis || node.operator && node.operator !== "=") {
|
||||
reportBadAssignment(node, name);
|
||||
}
|
||||
} else if (isThis) {
|
||||
context.report({ node, messageId: "unexpectedAlias", data: { name } });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a variable declaration of the alias in a program or function
|
||||
* is assigned to the correct value.
|
||||
* @param {string} alias alias the check the assignment of.
|
||||
* @param {Object} scope scope of the current code we are checking.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkWasAssigned(alias, scope) {
|
||||
const variable = scope.set.get(alias);
|
||||
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (variable.defs.some(def => def.node.type === "VariableDeclarator" &&
|
||||
def.node.init !== null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* The alias has been declared and not assigned: check it was
|
||||
* assigned later in the same scope.
|
||||
*/
|
||||
if (!variable.references.some(reference => {
|
||||
const write = reference.writeExpr;
|
||||
|
||||
return (
|
||||
reference.from === scope &&
|
||||
write && write.type === "ThisExpression" &&
|
||||
write.parent.operator === "="
|
||||
);
|
||||
})) {
|
||||
variable.defs.map(def => def.node).forEach(node => {
|
||||
reportBadAssignment(node, alias);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check each alias to ensure that is was assinged to the correct value.
|
||||
* @returns {void}
|
||||
*/
|
||||
function ensureWasAssigned() {
|
||||
const scope = context.getScope();
|
||||
|
||||
aliases.forEach(alias => {
|
||||
checkWasAssigned(alias, scope);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
"Program:exit": ensureWasAssigned,
|
||||
"FunctionExpression:exit": ensureWasAssigned,
|
||||
"FunctionDeclaration:exit": ensureWasAssigned,
|
||||
|
||||
VariableDeclarator(node) {
|
||||
const id = node.id;
|
||||
const isDestructuring =
|
||||
id.type === "ArrayPattern" || id.type === "ObjectPattern";
|
||||
|
||||
if (node.init !== null && !isDestructuring) {
|
||||
checkAssignment(node, id.name, node.init);
|
||||
}
|
||||
},
|
||||
|
||||
AssignmentExpression(node) {
|
||||
if (node.left.type === "Identifier") {
|
||||
checkAssignment(node, node.left.name, node.right);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* @fileoverview A rule to verify `super()` callings in constructor.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether a given code path segment is reachable or not.
|
||||
*
|
||||
* @param {CodePathSegment} segment - A code path segment to check.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is a constructor.
|
||||
* @param {ASTNode} node - A node to check. This node type is one of
|
||||
* `Program`, `FunctionDeclaration`, `FunctionExpression`, and
|
||||
* `ArrowFunctionExpression`.
|
||||
* @returns {boolean} `true` if the node is a constructor.
|
||||
*/
|
||||
function isConstructorFunction(node) {
|
||||
return (
|
||||
node.type === "FunctionExpression" &&
|
||||
node.parent.type === "MethodDefinition" &&
|
||||
node.parent.kind === "constructor"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given node can be a constructor or not.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {boolean} `true` if the node can be a constructor.
|
||||
*/
|
||||
function isPossibleConstructor(node) {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case "ClassExpression":
|
||||
case "FunctionExpression":
|
||||
case "ThisExpression":
|
||||
case "MemberExpression":
|
||||
case "CallExpression":
|
||||
case "NewExpression":
|
||||
case "YieldExpression":
|
||||
case "TaggedTemplateExpression":
|
||||
case "MetaProperty":
|
||||
return true;
|
||||
|
||||
case "Identifier":
|
||||
return node.name !== "undefined";
|
||||
|
||||
case "AssignmentExpression":
|
||||
return isPossibleConstructor(node.right);
|
||||
|
||||
case "LogicalExpression":
|
||||
return (
|
||||
isPossibleConstructor(node.left) ||
|
||||
isPossibleConstructor(node.right)
|
||||
);
|
||||
|
||||
case "ConditionalExpression":
|
||||
return (
|
||||
isPossibleConstructor(node.alternate) ||
|
||||
isPossibleConstructor(node.consequent)
|
||||
);
|
||||
|
||||
case "SequenceExpression": {
|
||||
const lastExpression = node.expressions[node.expressions.length - 1];
|
||||
|
||||
return isPossibleConstructor(lastExpression);
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "require `super()` calls in constructors",
|
||||
category: "ECMAScript 6",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/rules/constructor-super"
|
||||
},
|
||||
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
missingSome: "Lacked a call of 'super()' in some code paths.",
|
||||
missingAll: "Expected to call 'super()'.",
|
||||
|
||||
duplicate: "Unexpected duplicate 'super()'.",
|
||||
badSuper: "Unexpected 'super()' because 'super' is not a constructor.",
|
||||
unexpected: "Unexpected 'super()'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/*
|
||||
* {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
|
||||
* Information for each constructor.
|
||||
* - upper: Information of the upper constructor.
|
||||
* - hasExtends: A flag which shows whether own class has a valid `extends`
|
||||
* part.
|
||||
* - scope: The scope of own class.
|
||||
* - codePath: The code path object of the constructor.
|
||||
*/
|
||||
let funcInfo = null;
|
||||
|
||||
/*
|
||||
* {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
|
||||
* Information for each code path segment.
|
||||
* - calledInSomePaths: A flag of be called `super()` in some code paths.
|
||||
* - calledInEveryPaths: A flag of be called `super()` in all code paths.
|
||||
* - validNodes:
|
||||
*/
|
||||
let segInfoMap = Object.create(null);
|
||||
|
||||
/**
|
||||
* Gets the flag which shows `super()` is called in some paths.
|
||||
* @param {CodePathSegment} segment - A code path segment to get.
|
||||
* @returns {boolean} The flag which shows `super()` is called in some paths
|
||||
*/
|
||||
function isCalledInSomePath(segment) {
|
||||
return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the flag which shows `super()` is called in all paths.
|
||||
* @param {CodePathSegment} segment - A code path segment to get.
|
||||
* @returns {boolean} The flag which shows `super()` is called in all paths.
|
||||
*/
|
||||
function isCalledInEveryPath(segment) {
|
||||
|
||||
/*
|
||||
* If specific segment is the looped segment of the current segment,
|
||||
* skip the segment.
|
||||
* If not skipped, this never becomes true after a loop.
|
||||
*/
|
||||
if (segment.nextSegments.length === 1 &&
|
||||
segment.nextSegments[0].isLoopedPrevSegment(segment)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Stacks a constructor information.
|
||||
* @param {CodePath} codePath - A code path which was started.
|
||||
* @param {ASTNode} node - The current node.
|
||||
* @returns {void}
|
||||
*/
|
||||
onCodePathStart(codePath, node) {
|
||||
if (isConstructorFunction(node)) {
|
||||
|
||||
// Class > ClassBody > MethodDefinition > FunctionExpression
|
||||
const classNode = node.parent.parent.parent;
|
||||
const superClass = classNode.superClass;
|
||||
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
isConstructor: true,
|
||||
hasExtends: Boolean(superClass),
|
||||
superIsConstructor: isPossibleConstructor(superClass),
|
||||
codePath
|
||||
};
|
||||
} else {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
isConstructor: false,
|
||||
hasExtends: false,
|
||||
superIsConstructor: false,
|
||||
codePath
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Pops a constructor information.
|
||||
* And reports if `super()` lacked.
|
||||
* @param {CodePath} codePath - A code path which was ended.
|
||||
* @param {ASTNode} node - The current node.
|
||||
* @returns {void}
|
||||
*/
|
||||
onCodePathEnd(codePath, node) {
|
||||
const hasExtends = funcInfo.hasExtends;
|
||||
|
||||
// Pop.
|
||||
funcInfo = funcInfo.upper;
|
||||
|
||||
if (!hasExtends) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reports if `super()` lacked.
|
||||
const segments = codePath.returnedSegments;
|
||||
const calledInEveryPaths = segments.every(isCalledInEveryPath);
|
||||
const calledInSomePaths = segments.some(isCalledInSomePath);
|
||||
|
||||
if (!calledInEveryPaths) {
|
||||
context.report({
|
||||
messageId: calledInSomePaths
|
||||
? "missingSome"
|
||||
: "missingAll",
|
||||
node: node.parent
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize information of a given code path segment.
|
||||
* @param {CodePathSegment} segment - A code path segment to initialize.
|
||||
* @returns {void}
|
||||
*/
|
||||
onCodePathSegmentStart(segment) {
|
||||
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize info.
|
||||
const info = segInfoMap[segment.id] = {
|
||||
calledInSomePaths: false,
|
||||
calledInEveryPaths: false,
|
||||
validNodes: []
|
||||
};
|
||||
|
||||
// When there are previous segments, aggregates these.
|
||||
const prevSegments = segment.prevSegments;
|
||||
|
||||
if (prevSegments.length > 0) {
|
||||
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
|
||||
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update information of the code path segment when a code path was
|
||||
* looped.
|
||||
* @param {CodePathSegment} fromSegment - The code path segment of the
|
||||
* end of a loop.
|
||||
* @param {CodePathSegment} toSegment - A code path segment of the head
|
||||
* of a loop.
|
||||
* @returns {void}
|
||||
*/
|
||||
onCodePathSegmentLoop(fromSegment, toSegment) {
|
||||
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update information inside of the loop.
|
||||
const isRealLoop = toSegment.prevSegments.length >= 2;
|
||||
|
||||
funcInfo.codePath.traverseSegments(
|
||||
{ first: toSegment, last: fromSegment },
|
||||
segment => {
|
||||
const info = segInfoMap[segment.id];
|
||||
const prevSegments = segment.prevSegments;
|
||||
|
||||
// Updates flags.
|
||||
info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
|
||||
info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
|
||||
|
||||
// If flags become true anew, reports the valid nodes.
|
||||
if (info.calledInSomePaths || isRealLoop) {
|
||||
const nodes = info.validNodes;
|
||||
|
||||
info.validNodes = [];
|
||||
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
const node = nodes[i];
|
||||
|
||||
context.report({
|
||||
messageId: "duplicate",
|
||||
node
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a call of `super()`.
|
||||
* @param {ASTNode} node - A CallExpression node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
"CallExpression:exit"(node) {
|
||||
if (!(funcInfo && funcInfo.isConstructor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skips except `super()`.
|
||||
if (node.callee.type !== "Super") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reports if needed.
|
||||
if (funcInfo.hasExtends) {
|
||||
const segments = funcInfo.codePath.currentSegments;
|
||||
let duplicate = false;
|
||||
let info = null;
|
||||
|
||||
for (let i = 0; i < segments.length; ++i) {
|
||||
const segment = segments[i];
|
||||
|
||||
if (segment.reachable) {
|
||||
info = segInfoMap[segment.id];
|
||||
|
||||
duplicate = duplicate || info.calledInSomePaths;
|
||||
info.calledInSomePaths = info.calledInEveryPaths = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info) {
|
||||
if (duplicate) {
|
||||
context.report({
|
||||
messageId: "duplicate",
|
||||
node
|
||||
});
|
||||
} else if (!funcInfo.superIsConstructor) {
|
||||
context.report({
|
||||
messageId: "badSuper",
|
||||
node
|
||||
});
|
||||
} else {
|
||||
info.validNodes.push(node);
|
||||
}
|
||||
}
|
||||
} else if (funcInfo.codePath.currentSegments.some(isReachable)) {
|
||||
context.report({
|
||||
messageId: "unexpected",
|
||||
node
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the mark to the returned path as `super()` was called.
|
||||
* @param {ASTNode} node - A ReturnStatement node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
ReturnStatement(node) {
|
||||
if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skips if no argument.
|
||||
if (!node.argument) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Returning argument is a substitute of 'super()'.
|
||||
const segments = funcInfo.codePath.currentSegments;
|
||||
|
||||
for (let i = 0; i < segments.length; ++i) {
|
||||
const segment = segments[i];
|
||||
|
||||
if (segment.reachable) {
|
||||
const info = segInfoMap[segment.id];
|
||||
|
||||
info.calledInSomePaths = info.calledInEveryPaths = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets state.
|
||||
* @returns {void}
|
||||
*/
|
||||
"Program:exit"() {
|
||||
segInfoMap = Object.create(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag statements without curly braces
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent brace style for all control statements",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/curly"
|
||||
},
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["all"]
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 1
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["multi", "multi-line", "multi-or-nest"]
|
||||
},
|
||||
{
|
||||
enum: ["consistent"]
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
missingCurlyAfter: "Expected { after '{{name}}'.",
|
||||
missingCurlyAfterCondition: "Expected { after '{{name}}' condition.",
|
||||
unexpectedCurlyAfter: "Unnecessary { after '{{name}}'.",
|
||||
unexpectedCurlyAfterCondition: "Unnecessary { after '{{name}}' condition."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const multiOnly = (context.options[0] === "multi");
|
||||
const multiLine = (context.options[0] === "multi-line");
|
||||
const multiOrNest = (context.options[0] === "multi-or-nest");
|
||||
const consistent = (context.options[1] === "consistent");
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines if a given node is a one-liner that's on the same line as it's preceding code.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code.
|
||||
* @private
|
||||
*/
|
||||
function isCollapsedOneLiner(node) {
|
||||
const before = sourceCode.getTokenBefore(node);
|
||||
const last = sourceCode.getLastToken(node);
|
||||
const lastExcludingSemicolon = astUtils.isSemicolonToken(last) ? sourceCode.getTokenBefore(last) : last;
|
||||
|
||||
return before.loc.start.line === lastExcludingSemicolon.loc.end.line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given node is a one-liner.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} True if the node is a one-liner.
|
||||
* @private
|
||||
*/
|
||||
function isOneLiner(node) {
|
||||
const first = sourceCode.getFirstToken(node),
|
||||
last = sourceCode.getLastToken(node);
|
||||
|
||||
return first.loc.start.line === last.loc.end.line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given token is an `else` token or not.
|
||||
*
|
||||
* @param {Token} token - The token to check.
|
||||
* @returns {boolean} `true` if the token is an `else` token.
|
||||
*/
|
||||
function isElseKeywordToken(token) {
|
||||
return token.value === "else" && token.type === "Keyword";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `else` keyword token of a given `IfStatement` node.
|
||||
* @param {ASTNode} node - A `IfStatement` node to get.
|
||||
* @returns {Token} The `else` keyword token.
|
||||
*/
|
||||
function getElseKeyword(node) {
|
||||
return node.alternate && sourceCode.getFirstTokenBetween(node.consequent, node.alternate, isElseKeywordToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a given IfStatement node requires braces of the consequent chunk.
|
||||
* This returns `true` when below:
|
||||
*
|
||||
* 1. The given node has the `alternate` node.
|
||||
* 2. There is a `IfStatement` which doesn't have `alternate` node in the
|
||||
* trailing statement chain of the `consequent` node.
|
||||
*
|
||||
* @param {ASTNode} node - A IfStatement node to check.
|
||||
* @returns {boolean} `true` if the node requires braces of the consequent chunk.
|
||||
*/
|
||||
function requiresBraceOfConsequent(node) {
|
||||
if (node.alternate && node.consequent.type === "BlockStatement") {
|
||||
if (node.consequent.body.length >= 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (
|
||||
let currentNode = node.consequent.body[0];
|
||||
currentNode;
|
||||
currentNode = astUtils.getTrailingStatement(currentNode)
|
||||
) {
|
||||
if (currentNode.type === "IfStatement" && !currentNode.alternate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError.
|
||||
* @param {Token} closingBracket The } token
|
||||
* @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block.
|
||||
*/
|
||||
function needsSemicolon(closingBracket) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(closingBracket);
|
||||
const tokenAfter = sourceCode.getTokenAfter(closingBracket);
|
||||
const lastBlockNode = sourceCode.getNodeByRangeIndex(tokenBefore.range[0]);
|
||||
|
||||
if (astUtils.isSemicolonToken(tokenBefore)) {
|
||||
|
||||
// If the last statement already has a semicolon, don't add another one.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tokenAfter) {
|
||||
|
||||
// If there are no statements after this block, there is no need to add a semicolon.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lastBlockNode.type === "BlockStatement" && lastBlockNode.parent.type !== "FunctionExpression" && lastBlockNode.parent.type !== "ArrowFunctionExpression") {
|
||||
|
||||
/*
|
||||
* If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression),
|
||||
* don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause
|
||||
* a SyntaxError if it was followed by `else`.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) {
|
||||
|
||||
// If the next token is on the same line, insert a semicolon.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^[([/`+-]/.test(tokenAfter.value)) {
|
||||
|
||||
// If the next token starts with a character that would disrupt ASI, insert a semicolon.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tokenBefore.type === "Punctuator" && (tokenBefore.value === "++" || tokenBefore.value === "--")) {
|
||||
|
||||
// If the last token is ++ or --, insert a semicolon to avoid disrupting ASI.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, do not insert a semicolon.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares to check the body of a node to see if it's a block statement.
|
||||
* @param {ASTNode} node The node to report if there's a problem.
|
||||
* @param {ASTNode} body The body node to check for blocks.
|
||||
* @param {string} name The name to report if there's a problem.
|
||||
* @param {{ condition: boolean }} opts Options to pass to the report functions
|
||||
* @returns {Object} a prepared check object, with "actual", "expected", "check" properties.
|
||||
* "actual" will be `true` or `false` whether the body is already a block statement.
|
||||
* "expected" will be `true` or `false` if the body should be a block statement or not, or
|
||||
* `null` if it doesn't matter, depending on the rule options. It can be modified to change
|
||||
* the final behavior of "check".
|
||||
* "check" will be a function reporting appropriate problems depending on the other
|
||||
* properties.
|
||||
*/
|
||||
function prepareCheck(node, body, name, opts) {
|
||||
const hasBlock = (body.type === "BlockStatement");
|
||||
let expected = null;
|
||||
|
||||
if (node.type === "IfStatement" && node.consequent === body && requiresBraceOfConsequent(node)) {
|
||||
expected = true;
|
||||
} else if (multiOnly) {
|
||||
if (hasBlock && body.body.length === 1) {
|
||||
expected = false;
|
||||
}
|
||||
} else if (multiLine) {
|
||||
if (!isCollapsedOneLiner(body)) {
|
||||
expected = true;
|
||||
}
|
||||
} else if (multiOrNest) {
|
||||
if (hasBlock && body.body.length === 1 && isOneLiner(body.body[0])) {
|
||||
const leadingComments = sourceCode.getCommentsBefore(body.body[0]);
|
||||
|
||||
expected = leadingComments.length > 0;
|
||||
} else if (!isOneLiner(body)) {
|
||||
expected = true;
|
||||
}
|
||||
} else {
|
||||
expected = true;
|
||||
}
|
||||
|
||||
return {
|
||||
actual: hasBlock,
|
||||
expected,
|
||||
check() {
|
||||
if (this.expected !== null && this.expected !== this.actual) {
|
||||
if (this.expected) {
|
||||
context.report({
|
||||
node,
|
||||
loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
|
||||
messageId: opts && opts.condition ? "missingCurlyAfterCondition" : "missingCurlyAfter",
|
||||
data: {
|
||||
name
|
||||
},
|
||||
fix: fixer => fixer.replaceText(body, `{${sourceCode.getText(body)}}`)
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
loc: (name !== "else" ? node : getElseKeyword(node)).loc.start,
|
||||
messageId: opts && opts.condition ? "unexpectedCurlyAfterCondition" : "unexpectedCurlyAfter",
|
||||
data: {
|
||||
name
|
||||
},
|
||||
fix(fixer) {
|
||||
|
||||
/*
|
||||
* `do while` expressions sometimes need a space to be inserted after `do`.
|
||||
* e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)`
|
||||
*/
|
||||
const needsPrecedingSpace = node.type === "DoWhileStatement" &&
|
||||
sourceCode.getTokenBefore(body).range[1] === body.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent("do", sourceCode.getFirstToken(body, { skip: 1 }));
|
||||
|
||||
const openingBracket = sourceCode.getFirstToken(body);
|
||||
const closingBracket = sourceCode.getLastToken(body);
|
||||
const lastTokenInBlock = sourceCode.getTokenBefore(closingBracket);
|
||||
|
||||
if (needsSemicolon(closingBracket)) {
|
||||
|
||||
/*
|
||||
* If removing braces would cause a SyntaxError due to multiple statements on the same line (or
|
||||
* change the semantics of the code due to ASI), don't perform a fix.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
const resultingBodyText = sourceCode.getText().slice(openingBracket.range[1], lastTokenInBlock.range[0]) +
|
||||
sourceCode.getText(lastTokenInBlock) +
|
||||
sourceCode.getText().slice(lastTokenInBlock.range[1], closingBracket.range[0]);
|
||||
|
||||
return fixer.replaceText(body, (needsPrecedingSpace ? " " : "") + resultingBodyText);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares to check the bodies of a "if", "else if" and "else" chain.
|
||||
* @param {ASTNode} node The first IfStatement node of the chain.
|
||||
* @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more
|
||||
* information.
|
||||
*/
|
||||
function prepareIfChecks(node) {
|
||||
const preparedChecks = [];
|
||||
|
||||
for (let currentNode = node; currentNode; currentNode = currentNode.alternate) {
|
||||
preparedChecks.push(prepareCheck(currentNode, currentNode.consequent, "if", { condition: true }));
|
||||
if (currentNode.alternate && currentNode.alternate.type !== "IfStatement") {
|
||||
preparedChecks.push(prepareCheck(currentNode, currentNode.alternate, "else"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consistent) {
|
||||
|
||||
/*
|
||||
* If any node should have or already have braces, make sure they
|
||||
* all have braces.
|
||||
* If all nodes shouldn't have braces, make sure they don't.
|
||||
*/
|
||||
const expected = preparedChecks.some(preparedCheck => {
|
||||
if (preparedCheck.expected !== null) {
|
||||
return preparedCheck.expected;
|
||||
}
|
||||
return preparedCheck.actual;
|
||||
});
|
||||
|
||||
preparedChecks.forEach(preparedCheck => {
|
||||
preparedCheck.expected = expected;
|
||||
});
|
||||
}
|
||||
|
||||
return preparedChecks;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
IfStatement(node) {
|
||||
if (node.parent.type !== "IfStatement") {
|
||||
prepareIfChecks(node).forEach(preparedCheck => {
|
||||
preparedCheck.check();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
WhileStatement(node) {
|
||||
prepareCheck(node, node.body, "while", { condition: true }).check();
|
||||
},
|
||||
|
||||
DoWhileStatement(node) {
|
||||
prepareCheck(node, node.body, "do").check();
|
||||
},
|
||||
|
||||
ForStatement(node) {
|
||||
prepareCheck(node, node.body, "for", { condition: true }).check();
|
||||
},
|
||||
|
||||
ForInStatement(node) {
|
||||
prepareCheck(node, node.body, "for-in").check();
|
||||
},
|
||||
|
||||
ForOfStatement(node) {
|
||||
prepareCheck(node, node.body, "for-of").check();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @fileoverview require default case in switch statements
|
||||
* @author Aliaksei Shytkin
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_COMMENT_PATTERN = /^no default$/i;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require `default` cases in `switch` statements",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/default-case"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
commentPattern: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
|
||||
messages: {
|
||||
missingDefaultCase: "Expected a default case."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const commentPattern = options.commentPattern
|
||||
? new RegExp(options.commentPattern)
|
||||
: DEFAULT_COMMENT_PATTERN;
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Shortcut to get last element of array
|
||||
* @param {*[]} collection Array
|
||||
* @returns {*} Last element
|
||||
*/
|
||||
function last(collection) {
|
||||
return collection[collection.length - 1];
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
SwitchStatement(node) {
|
||||
|
||||
if (!node.cases.length) {
|
||||
|
||||
/*
|
||||
* skip check of empty switch because there is no easy way
|
||||
* to extract comments inside it now
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDefault = node.cases.some(v => v.test === null);
|
||||
|
||||
if (!hasDefault) {
|
||||
|
||||
let comment;
|
||||
|
||||
const lastCase = last(node.cases);
|
||||
const comments = sourceCode.getCommentsAfter(lastCase);
|
||||
|
||||
if (comments.length) {
|
||||
comment = last(comments);
|
||||
}
|
||||
|
||||
if (!comment || !commentPattern.test(comment.value.trim())) {
|
||||
context.report({ node, messageId: "missingDefaultCase" });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @fileoverview Validates newlines before and after dots
|
||||
* @author Greg Cochard
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent newlines before and after dots",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/dot-location"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["object", "property"]
|
||||
}
|
||||
],
|
||||
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
expectedDotAfterObject: "Expected dot to be on same line as object.",
|
||||
expectedDotBeforeProperty: "Expected dot to be on same line as property."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const config = context.options[0];
|
||||
|
||||
// default to onObject if no preference is passed
|
||||
const onObject = config === "object" || !config;
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Reports if the dot between object and property is on the correct loccation.
|
||||
* @param {ASTNode} obj The object owning the property.
|
||||
* @param {ASTNode} prop The property of the object.
|
||||
* @param {ASTNode} node The corresponding node of the token.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkDotLocation(obj, prop, node) {
|
||||
const dot = sourceCode.getTokenBefore(prop);
|
||||
const textBeforeDot = sourceCode.getText().slice(obj.range[1], dot.range[0]);
|
||||
const textAfterDot = sourceCode.getText().slice(dot.range[1], prop.range[0]);
|
||||
|
||||
if (dot.type === "Punctuator" && dot.value === ".") {
|
||||
if (onObject) {
|
||||
if (!astUtils.isTokenOnSameLine(obj, dot)) {
|
||||
const neededTextAfterObj = astUtils.isDecimalInteger(obj) ? " " : "";
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: dot.loc.start,
|
||||
messageId: "expectedDotAfterObject",
|
||||
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${neededTextAfterObj}.${textBeforeDot}${textAfterDot}`)
|
||||
});
|
||||
}
|
||||
} else if (!astUtils.isTokenOnSameLine(dot, prop)) {
|
||||
context.report({
|
||||
node,
|
||||
loc: dot.loc.start,
|
||||
messageId: "expectedDotBeforeProperty",
|
||||
fix: fixer => fixer.replaceTextRange([obj.range[1], prop.range[0]], `${textBeforeDot}${textAfterDot}.`)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the spacing of the dot within a member expression.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkNode(node) {
|
||||
checkDotLocation(node.object, node.property, node);
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression: checkNode
|
||||
};
|
||||
}
|
||||
};
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @fileoverview Rule to warn about using dot notation instead of square bracket notation when possible.
|
||||
* @author Josh Perez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
||||
const keywords = require("../util/keywords");
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce dot notation whenever possible",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/dot-notation"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowKeywords: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowPattern: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
useDot: "[{{key}}] is better written in dot notation.",
|
||||
useBrackets: ".{{key}} is a syntax error."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const allowKeywords = options.allowKeywords === void 0 || !!options.allowKeywords;
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
let allowPattern;
|
||||
|
||||
if (options.allowPattern) {
|
||||
allowPattern = new RegExp(options.allowPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the property is valid dot notation
|
||||
* @param {ASTNode} node The dot notation node
|
||||
* @param {string} value Value which is to be checked
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkComputedProperty(node, value) {
|
||||
if (
|
||||
validIdentifier.test(value) &&
|
||||
(allowKeywords || keywords.indexOf(String(value)) === -1) &&
|
||||
!(allowPattern && allowPattern.test(value))
|
||||
) {
|
||||
const formattedValue = node.property.type === "Literal" ? JSON.stringify(value) : `\`${value}\``;
|
||||
|
||||
context.report({
|
||||
node: node.property,
|
||||
messageId: "useDot",
|
||||
data: {
|
||||
key: formattedValue
|
||||
},
|
||||
fix(fixer) {
|
||||
const leftBracket = sourceCode.getTokenAfter(node.object, astUtils.isOpeningBracketToken);
|
||||
const rightBracket = sourceCode.getLastToken(node);
|
||||
|
||||
if (sourceCode.getFirstTokenBetween(leftBracket, rightBracket, { includeComments: true, filter: astUtils.isCommentToken })) {
|
||||
|
||||
// Don't perform any fixes if there are comments inside the brackets.
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenAfterProperty = sourceCode.getTokenAfter(rightBracket);
|
||||
const needsSpaceAfterProperty = tokenAfterProperty &&
|
||||
rightBracket.range[1] === tokenAfterProperty.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(String(value), tokenAfterProperty);
|
||||
|
||||
const textBeforeDot = astUtils.isDecimalInteger(node.object) ? " " : "";
|
||||
const textAfterProperty = needsSpaceAfterProperty ? " " : "";
|
||||
|
||||
return fixer.replaceTextRange(
|
||||
[leftBracket.range[0], rightBracket.range[1]],
|
||||
`${textBeforeDot}.${value}${textAfterProperty}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (
|
||||
node.computed &&
|
||||
node.property.type === "Literal"
|
||||
) {
|
||||
checkComputedProperty(node, node.property.value);
|
||||
}
|
||||
if (
|
||||
node.computed &&
|
||||
node.property.type === "TemplateLiteral" &&
|
||||
node.property.expressions.length === 0
|
||||
) {
|
||||
checkComputedProperty(node, node.property.quasis[0].value.cooked);
|
||||
}
|
||||
if (
|
||||
!allowKeywords &&
|
||||
!node.computed &&
|
||||
keywords.indexOf(String(node.property.name)) !== -1
|
||||
) {
|
||||
context.report({
|
||||
node: node.property,
|
||||
messageId: "useBrackets",
|
||||
data: {
|
||||
key: node.property.name
|
||||
},
|
||||
fix(fixer) {
|
||||
const dot = sourceCode.getTokenBefore(node.property);
|
||||
const textAfterDot = sourceCode.text.slice(dot.range[1], node.property.range[0]);
|
||||
|
||||
if (textAfterDot.trim()) {
|
||||
|
||||
// Don't perform any fixes if there are comments between the dot and the property name.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.object.type === "Identifier" && node.object.name === "let") {
|
||||
|
||||
/*
|
||||
* A statement that starts with `let[` is parsed as a destructuring variable declaration, not
|
||||
* a MemberExpression.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
return fixer.replaceTextRange(
|
||||
[dot.range[0], node.property.range[1]],
|
||||
`[${textAfterDot}"${node.property.name}"]`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @fileoverview Require or disallow newline at the end of files
|
||||
* @author Nodeca Team <https://github.com/nodeca>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const lodash = require("lodash");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "require or disallow newline at the end of files",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/eol-last"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never", "unix", "windows"]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
missing: "Newline required at end of file but not found.",
|
||||
unexpected: "Newline not allowed at end of file."
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program: function checkBadEOF(node) {
|
||||
const sourceCode = context.getSourceCode(),
|
||||
src = sourceCode.getText(),
|
||||
location = {
|
||||
column: lodash.last(sourceCode.lines).length,
|
||||
line: sourceCode.lines.length
|
||||
},
|
||||
LF = "\n",
|
||||
CRLF = `\r${LF}`,
|
||||
endsWithNewline = lodash.endsWith(src, LF);
|
||||
|
||||
/*
|
||||
* Empty source is always valid: No content in file so we don't
|
||||
* need to lint for a newline on the last line of content.
|
||||
*/
|
||||
if (!src.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mode = context.options[0] || "always",
|
||||
appendCRLF = false;
|
||||
|
||||
if (mode === "unix") {
|
||||
|
||||
// `"unix"` should behave exactly as `"always"`
|
||||
mode = "always";
|
||||
}
|
||||
if (mode === "windows") {
|
||||
|
||||
// `"windows"` should behave exactly as `"always"`, but append CRLF in the fixer for backwards compatibility
|
||||
mode = "always";
|
||||
appendCRLF = true;
|
||||
}
|
||||
if (mode === "always" && !endsWithNewline) {
|
||||
|
||||
// File is not newline-terminated, but should be
|
||||
context.report({
|
||||
node,
|
||||
loc: location,
|
||||
messageId: "missing",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfterRange([0, src.length], appendCRLF ? CRLF : LF);
|
||||
}
|
||||
});
|
||||
} else if (mode === "never" && endsWithNewline) {
|
||||
|
||||
// File is newline-terminated, but shouldn't be
|
||||
context.report({
|
||||
node,
|
||||
loc: location,
|
||||
messageId: "unexpected",
|
||||
fix(fixer) {
|
||||
const finalEOLs = /(?:\r?\n)+$/,
|
||||
match = finalEOLs.exec(sourceCode.text),
|
||||
start = match.index,
|
||||
end = sourceCode.text.length;
|
||||
|
||||
return fixer.replaceTextRange([start, end], "");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag statements that use != and == instead of !== and ===
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require the use of `===` and `!==`",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/eqeqeq"
|
||||
},
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["always"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
null: {
|
||||
enum: ["always", "never", "ignore"]
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
additionalItems: false
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["smart", "allow-null"]
|
||||
}
|
||||
],
|
||||
additionalItems: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
fixable: "code",
|
||||
|
||||
messages: {
|
||||
unexpected: "Expected '{{expectedOperator}}' and instead saw '{{actualOperator}}'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const config = context.options[0] || "always";
|
||||
const options = context.options[1] || {};
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
const nullOption = (config === "always")
|
||||
? options.null || "always"
|
||||
: "ignore";
|
||||
const enforceRuleForNull = (nullOption === "always");
|
||||
const enforceInverseRuleForNull = (nullOption === "never");
|
||||
|
||||
/**
|
||||
* Checks if an expression is a typeof expression
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} if the node is a typeof expression
|
||||
*/
|
||||
function isTypeOf(node) {
|
||||
return node.type === "UnaryExpression" && node.operator === "typeof";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if either operand of a binary expression is a typeof operation
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} if one of the operands is typeof
|
||||
* @private
|
||||
*/
|
||||
function isTypeOfBinary(node) {
|
||||
return isTypeOf(node.left) || isTypeOf(node.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if operands are literals of the same type (via typeof)
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} if operands are of same type
|
||||
* @private
|
||||
*/
|
||||
function areLiteralsAndSameType(node) {
|
||||
return node.left.type === "Literal" && node.right.type === "Literal" &&
|
||||
typeof node.left.value === typeof node.right.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if one of the operands is a literal null
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} if operands are null
|
||||
* @private
|
||||
*/
|
||||
function isNullCheck(node) {
|
||||
return astUtils.isNullLiteral(node.right) || astUtils.isNullLiteral(node.left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the location (line and column) of the binary expression's operator
|
||||
* @param {ASTNode} node The binary expression node to check
|
||||
* @param {string} operator The operator to find
|
||||
* @returns {Object} { line, column } location of operator
|
||||
* @private
|
||||
*/
|
||||
function getOperatorLocation(node) {
|
||||
const opToken = sourceCode.getTokenAfter(node.left);
|
||||
|
||||
return { line: opToken.loc.start.line, column: opToken.loc.start.column };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a message for this rule.
|
||||
* @param {ASTNode} node The binary expression node that was checked
|
||||
* @param {string} expectedOperator The operator that was expected (either '==', '!=', '===', or '!==')
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node, expectedOperator) {
|
||||
context.report({
|
||||
node,
|
||||
loc: getOperatorLocation(node),
|
||||
messageId: "unexpected",
|
||||
data: { expectedOperator, actualOperator: node.operator },
|
||||
fix(fixer) {
|
||||
|
||||
// If the comparison is a `typeof` comparison or both sides are literals with the same type, then it's safe to fix.
|
||||
if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {
|
||||
const operatorToken = sourceCode.getFirstTokenBetween(
|
||||
node.left,
|
||||
node.right,
|
||||
token => token.value === node.operator
|
||||
);
|
||||
|
||||
return fixer.replaceText(operatorToken, expectedOperator);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
BinaryExpression(node) {
|
||||
const isNull = isNullCheck(node);
|
||||
|
||||
if (node.operator !== "==" && node.operator !== "!=") {
|
||||
if (enforceInverseRuleForNull && isNull) {
|
||||
report(node, node.operator.slice(0, -1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (config === "smart" && (isTypeOfBinary(node) ||
|
||||
areLiteralsAndSameType(node) || isNull)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enforceRuleForNull && isNull) {
|
||||
return;
|
||||
}
|
||||
|
||||
report(node, `${node.operator}=`);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @fileoverview enforce "for" loop update clause moving the counter in the right direction.(for-direction)
|
||||
* @author Aladdin-ADD<hh_2013@foxmail.com>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "enforce \"for\" loop update clause moving the counter in the right direction.",
|
||||
category: "Possible Errors",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/rules/for-direction"
|
||||
},
|
||||
|
||||
fixable: null,
|
||||
schema: [],
|
||||
|
||||
messages: {
|
||||
incorrectDirection: "The update clause in this loop moves the variable in the wrong direction."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* report an error.
|
||||
* @param {ASTNode} node the node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "incorrectDirection"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* check UpdateExpression add/sub the counter
|
||||
* @param {ASTNode} update UpdateExpression to check
|
||||
* @param {string} counter variable name to check
|
||||
* @returns {int} if add return 1, if sub return -1, if nochange, return 0
|
||||
*/
|
||||
function getUpdateDirection(update, counter) {
|
||||
if (update.argument.type === "Identifier" && update.argument.name === counter) {
|
||||
if (update.operator === "++") {
|
||||
return 1;
|
||||
}
|
||||
if (update.operator === "--") {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* check AssignmentExpression add/sub the counter
|
||||
* @param {ASTNode} update AssignmentExpression to check
|
||||
* @param {string} counter variable name to check
|
||||
* @returns {int} if add return 1, if sub return -1, if nochange, return 0
|
||||
*/
|
||||
function getAssignmentDirection(update, counter) {
|
||||
if (update.left.name === counter) {
|
||||
if (update.operator === "+=") {
|
||||
return 1;
|
||||
}
|
||||
if (update.operator === "-=") {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return {
|
||||
ForStatement(node) {
|
||||
|
||||
if (node.test && node.test.type === "BinaryExpression" && node.test.left.type === "Identifier" && node.update) {
|
||||
const counter = node.test.left.name;
|
||||
const operator = node.test.operator;
|
||||
const update = node.update;
|
||||
|
||||
if (operator === "<" || operator === "<=") {
|
||||
|
||||
// report error if update sub the counter (--, -=)
|
||||
if (update.type === "UpdateExpression" && getUpdateDirection(update, counter) < 0) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) < 0) {
|
||||
report(node);
|
||||
}
|
||||
} else if (operator === ">" || operator === ">=") {
|
||||
|
||||
// report error if update add the counter (++, +=)
|
||||
if (update.type === "UpdateExpression" && getUpdateDirection(update, counter) > 0) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
if (update.type === "AssignmentExpression" && getAssignmentDirection(update, counter) > 0) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @fileoverview Rule to control spacing within function calls
|
||||
* @author Matt DuVall <http://www.mattduvall.com>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "require or disallow spacing between function identifiers and their invocations",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/func-call-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["never"]
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 1
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["always"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowNewlines: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
messages: {
|
||||
unexpected: "Unexpected newline between function name and paren.",
|
||||
missing: "Missing space between function name and paren."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const never = context.options[0] !== "always";
|
||||
const allowNewlines = !never && context.options[1] && context.options[1].allowNewlines;
|
||||
const sourceCode = context.getSourceCode();
|
||||
const text = sourceCode.getText();
|
||||
|
||||
/**
|
||||
* Check if open space is present in a function name
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkSpacing(node) {
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
const lastCalleeToken = sourceCode.getLastToken(node.callee);
|
||||
const parenToken = sourceCode.getFirstTokenBetween(lastCalleeToken, lastToken, astUtils.isOpeningParenToken);
|
||||
const prevToken = parenToken && sourceCode.getTokenBefore(parenToken);
|
||||
|
||||
// Parens in NewExpression are optional
|
||||
if (!(parenToken && parenToken.range[1] < node.range[1])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textBetweenTokens = text.slice(prevToken.range[1], parenToken.range[0]).replace(/\/\*.*?\*\//g, "");
|
||||
const hasWhitespace = /\s/.test(textBetweenTokens);
|
||||
const hasNewline = hasWhitespace && astUtils.LINEBREAK_MATCHER.test(textBetweenTokens);
|
||||
|
||||
/*
|
||||
* never allowNewlines hasWhitespace hasNewline message
|
||||
* F F F F Missing space between function name and paren.
|
||||
* F F F T (Invalid `!hasWhitespace && hasNewline`)
|
||||
* F F T T Unexpected newline between function name and paren.
|
||||
* F F T F (OK)
|
||||
* F T T F (OK)
|
||||
* F T T T (OK)
|
||||
* F T F T (Invalid `!hasWhitespace && hasNewline`)
|
||||
* F T F F Missing space between function name and paren.
|
||||
* T T F F (Invalid `never && allowNewlines`)
|
||||
* T T F T (Invalid `!hasWhitespace && hasNewline`)
|
||||
* T T T T (Invalid `never && allowNewlines`)
|
||||
* T T T F (Invalid `never && allowNewlines`)
|
||||
* T F T F Unexpected space between function name and paren.
|
||||
* T F T T Unexpected space between function name and paren.
|
||||
* T F F T (Invalid `!hasWhitespace && hasNewline`)
|
||||
* T F F F (OK)
|
||||
*
|
||||
* T T Unexpected space between function name and paren.
|
||||
* F F Missing space between function name and paren.
|
||||
* F F T Unexpected newline between function name and paren.
|
||||
*/
|
||||
|
||||
if (never && hasWhitespace) {
|
||||
context.report({
|
||||
node,
|
||||
loc: lastCalleeToken.loc.start,
|
||||
messageId: "unexpected",
|
||||
fix(fixer) {
|
||||
|
||||
/*
|
||||
* Only autofix if there is no newline
|
||||
* https://github.com/eslint/eslint/issues/7787
|
||||
*/
|
||||
if (!hasNewline) {
|
||||
return fixer.removeRange([prevToken.range[1], parenToken.range[0]]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} else if (!never && !hasWhitespace) {
|
||||
context.report({
|
||||
node,
|
||||
loc: lastCalleeToken.loc.start,
|
||||
messageId: "missing",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(parenToken, " ");
|
||||
}
|
||||
});
|
||||
} else if (!never && !allowNewlines && hasNewline) {
|
||||
context.report({
|
||||
node,
|
||||
loc: lastCalleeToken.loc.start,
|
||||
messageId: "unexpected",
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([prevToken.range[1], parenToken.range[0]], " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CallExpression: checkSpacing,
|
||||
NewExpression: checkSpacing
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* @fileoverview Rule to require function names to match the name of the variable or property to which they are assigned.
|
||||
* @author Annie Zhang, Pavel Strashkin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
const esutils = require("esutils");
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines if a pattern is `module.exports` or `module["exports"]`
|
||||
* @param {ASTNode} pattern The left side of the AssignmentExpression
|
||||
* @returns {boolean} True if the pattern is `module.exports` or `module["exports"]`
|
||||
*/
|
||||
function isModuleExports(pattern) {
|
||||
if (pattern.type === "MemberExpression" && pattern.object.type === "Identifier" && pattern.object.name === "module") {
|
||||
|
||||
// module.exports
|
||||
if (pattern.property.type === "Identifier" && pattern.property.name === "exports") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// module["exports"]
|
||||
if (pattern.property.type === "Literal" && pattern.property.value === "exports") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a string name is a valid identifier
|
||||
* @param {string} name The string to be checked
|
||||
* @param {int} ecmaVersion The ECMAScript version if specified in the parserOptions config
|
||||
* @returns {boolean} True if the string is a valid identifier
|
||||
*/
|
||||
function isIdentifier(name, ecmaVersion) {
|
||||
if (ecmaVersion >= 6) {
|
||||
return esutils.keyword.isIdentifierES6(name);
|
||||
}
|
||||
return esutils.keyword.isIdentifierES5(name);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const alwaysOrNever = { enum: ["always", "never"] };
|
||||
const optionsObject = {
|
||||
type: "object",
|
||||
properties: {
|
||||
considerPropertyDescriptor: {
|
||||
type: "boolean"
|
||||
},
|
||||
includeCommonJSModuleExports: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require function names to match the name of the variable or property to which they are assigned",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/func-name-matching"
|
||||
},
|
||||
|
||||
schema: {
|
||||
anyOf: [{
|
||||
type: "array",
|
||||
additionalItems: false,
|
||||
items: [alwaysOrNever, optionsObject]
|
||||
}, {
|
||||
type: "array",
|
||||
additionalItems: false,
|
||||
items: [optionsObject]
|
||||
}]
|
||||
},
|
||||
|
||||
messages: {
|
||||
matchProperty: "Function name `{{funcName}}` should match property name `{{name}}`",
|
||||
matchVariable: "Function name `{{funcName}}` should match variable name `{{name}}`",
|
||||
notMatchProperty: "Function name `{{funcName}}` should not match property name `{{name}}`",
|
||||
notMatchVariable: "Function name `{{funcName}}` should not match variable name `{{name}}`"
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = (typeof context.options[0] === "object" ? context.options[0] : context.options[1]) || {};
|
||||
const nameMatches = typeof context.options[0] === "string" ? context.options[0] : "always";
|
||||
const considerPropertyDescriptor = options.considerPropertyDescriptor;
|
||||
const includeModuleExports = options.includeCommonJSModuleExports;
|
||||
const ecmaVersion = context.parserOptions && context.parserOptions.ecmaVersion ? context.parserOptions.ecmaVersion : 5;
|
||||
|
||||
/**
|
||||
* Check whether node is a certain CallExpression.
|
||||
* @param {string} objName object name
|
||||
* @param {string} funcName function name
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} `true` if node matches CallExpression
|
||||
*/
|
||||
function isPropertyCall(objName, funcName, node) {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
return node.type === "CallExpression" &&
|
||||
node.callee.object.name === objName &&
|
||||
node.callee.property.name === funcName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares identifiers based on the nameMatches option
|
||||
* @param {string} x the first identifier
|
||||
* @param {string} y the second identifier
|
||||
* @returns {boolean} whether the two identifiers should warn.
|
||||
*/
|
||||
function shouldWarn(x, y) {
|
||||
return (nameMatches === "always" && x !== y) || (nameMatches === "never" && x === y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports
|
||||
* @param {ASTNode} node The node to report
|
||||
* @param {string} name The variable or property name
|
||||
* @param {string} funcName The function name
|
||||
* @param {boolean} isProp True if the reported node is a property assignment
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(node, name, funcName, isProp) {
|
||||
let messageId;
|
||||
|
||||
if (nameMatches === "always" && isProp) {
|
||||
messageId = "matchProperty";
|
||||
} else if (nameMatches === "always") {
|
||||
messageId = "matchVariable";
|
||||
} else if (isProp) {
|
||||
messageId = "notMatchProperty";
|
||||
} else {
|
||||
messageId = "notMatchVariable";
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
data: {
|
||||
name,
|
||||
funcName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given node is a string literal
|
||||
* @param {ASTNode} node The node to check
|
||||
* @returns {boolean} `true` if the node is a string literal
|
||||
*/
|
||||
function isStringLiteral(node) {
|
||||
return node.type === "Literal" && typeof node.value === "string";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
VariableDeclarator(node) {
|
||||
if (!node.init || node.init.type !== "FunctionExpression" || node.id.type !== "Identifier") {
|
||||
return;
|
||||
}
|
||||
if (node.init.id && shouldWarn(node.id.name, node.init.id.name)) {
|
||||
report(node, node.id.name, node.init.id.name, false);
|
||||
}
|
||||
},
|
||||
|
||||
AssignmentExpression(node) {
|
||||
if (
|
||||
node.right.type !== "FunctionExpression" ||
|
||||
(node.left.computed && node.left.property.type !== "Literal") ||
|
||||
(!includeModuleExports && isModuleExports(node.left)) ||
|
||||
(node.left.type !== "Identifier" && node.left.type !== "MemberExpression")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isProp = node.left.type === "MemberExpression";
|
||||
const name = isProp ? astUtils.getStaticPropertyName(node.left) : node.left.name;
|
||||
|
||||
if (node.right.id && isIdentifier(name) && shouldWarn(name, node.right.id.name)) {
|
||||
report(node, name, node.right.id.name, isProp);
|
||||
}
|
||||
},
|
||||
|
||||
Property(node) {
|
||||
if (node.value.type !== "FunctionExpression" || !node.value.id || node.computed && !isStringLiteral(node.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.key.type === "Identifier") {
|
||||
const functionName = node.value.id.name;
|
||||
let propertyName = node.key.name;
|
||||
|
||||
if (considerPropertyDescriptor && propertyName === "value") {
|
||||
if (isPropertyCall("Object", "defineProperty", node.parent.parent) || isPropertyCall("Reflect", "defineProperty", node.parent.parent)) {
|
||||
const property = node.parent.parent.arguments[1];
|
||||
|
||||
if (isStringLiteral(property) && shouldWarn(property.value, functionName)) {
|
||||
report(node, property.value, functionName, true);
|
||||
}
|
||||
} else if (isPropertyCall("Object", "defineProperties", node.parent.parent.parent.parent)) {
|
||||
propertyName = node.parent.parent.key.name;
|
||||
if (!node.parent.parent.computed && shouldWarn(propertyName, functionName)) {
|
||||
report(node, propertyName, functionName, true);
|
||||
}
|
||||
} else if (isPropertyCall("Object", "create", node.parent.parent.parent.parent)) {
|
||||
propertyName = node.parent.parent.key.name;
|
||||
if (!node.parent.parent.computed && shouldWarn(propertyName, functionName)) {
|
||||
report(node, propertyName, functionName, true);
|
||||
}
|
||||
} else if (shouldWarn(propertyName, functionName)) {
|
||||
report(node, propertyName, functionName, true);
|
||||
}
|
||||
} else if (shouldWarn(propertyName, functionName)) {
|
||||
report(node, propertyName, functionName, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isStringLiteral(node.key) &&
|
||||
isIdentifier(node.key.value, ecmaVersion) &&
|
||||
shouldWarn(node.key.value, node.value.id.name)
|
||||
) {
|
||||
report(node, node.key.value, node.value.id.name, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* @fileoverview Rule to warn when a function expression does not have a name.
|
||||
* @author Kyle T. Nunery
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
/**
|
||||
* Checks whether or not a given variable is a function name.
|
||||
* @param {eslint-scope.Variable} variable - A variable to check.
|
||||
* @returns {boolean} `true` if the variable is a function name.
|
||||
*/
|
||||
function isFunctionName(variable) {
|
||||
return variable && variable.defs[0].type === "FunctionName";
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require or disallow named `function` expressions",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/func-names"
|
||||
},
|
||||
|
||||
schema: {
|
||||
definitions: {
|
||||
value: {
|
||||
enum: [
|
||||
"always",
|
||||
"as-needed",
|
||||
"never"
|
||||
]
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{
|
||||
$ref: "#/definitions/value"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
generators: {
|
||||
$ref: "#/definitions/value"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
messages: {
|
||||
unnamed: "Unexpected unnamed {{name}}.",
|
||||
named: "Unexpected named {{name}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* Returns the config option for the given node.
|
||||
* @param {ASTNode} node - A node to get the config for.
|
||||
* @returns {string} The config option.
|
||||
*/
|
||||
function getConfigForNode(node) {
|
||||
if (
|
||||
node.generator &&
|
||||
context.options.length > 1 &&
|
||||
context.options[1].generators
|
||||
) {
|
||||
return context.options[1].generators;
|
||||
}
|
||||
|
||||
return context.options[0] || "always";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current FunctionExpression node is a get, set, or
|
||||
* shorthand method in an object literal or a class.
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {boolean} True if the node is a get, set, or shorthand method.
|
||||
*/
|
||||
function isObjectOrClassMethod(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (parent.type === "MethodDefinition" || (
|
||||
parent.type === "Property" && (
|
||||
parent.method ||
|
||||
parent.kind === "get" ||
|
||||
parent.kind === "set"
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current FunctionExpression node has a name that would be
|
||||
* inferred from context in a conforming ES6 environment.
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {boolean} True if the node would have a name assigned automatically.
|
||||
*/
|
||||
function hasInferredName(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return isObjectOrClassMethod(node) ||
|
||||
(parent.type === "VariableDeclarator" && parent.id.type === "Identifier" && parent.init === node) ||
|
||||
(parent.type === "Property" && parent.value === node) ||
|
||||
(parent.type === "AssignmentExpression" && parent.left.type === "Identifier" && parent.right === node) ||
|
||||
(parent.type === "ExportDefaultDeclaration" && parent.declaration === node) ||
|
||||
(parent.type === "AssignmentPattern" && parent.right === node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that an unnamed function should be named
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportUnexpectedUnnamedFunction(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "unnamed",
|
||||
data: { name: astUtils.getFunctionNameWithKind(node) }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports that a named function should be unnamed
|
||||
* @param {ASTNode} node - The node to report in the event of an error.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportUnexpectedNamedFunction(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "named",
|
||||
data: { name: astUtils.getFunctionNameWithKind(node) }
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
"FunctionExpression:exit"(node) {
|
||||
|
||||
// Skip recursive functions.
|
||||
const nameVar = context.getDeclaredVariables(node)[0];
|
||||
|
||||
if (isFunctionName(nameVar) && nameVar.references.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasName = Boolean(node.id && node.id.name);
|
||||
const config = getConfigForNode(node);
|
||||
|
||||
if (config === "never") {
|
||||
if (hasName) {
|
||||
reportUnexpectedNamedFunction(node);
|
||||
}
|
||||
} else if (config === "as-needed") {
|
||||
if (!hasName && !hasInferredName(node)) {
|
||||
reportUnexpectedUnnamedFunction(node);
|
||||
}
|
||||
} else {
|
||||
if (!hasName && !isObjectOrClassMethod(node)) {
|
||||
reportUnexpectedUnnamedFunction(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce a particular function style
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce the consistent use of either `function` declarations or expressions",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/func-style"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["declaration", "expression"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowArrowFunctions: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expression: "Expected a function expression.",
|
||||
declaration: "Expected a function declaration."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const style = context.options[0],
|
||||
allowArrowFunctions = context.options[1] && context.options[1].allowArrowFunctions === true,
|
||||
enforceDeclarations = (style === "declaration"),
|
||||
stack = [];
|
||||
|
||||
const nodesToCheck = {
|
||||
FunctionDeclaration(node) {
|
||||
stack.push(false);
|
||||
|
||||
if (!enforceDeclarations && node.parent.type !== "ExportDefaultDeclaration") {
|
||||
context.report({ node, messageId: "expression" });
|
||||
}
|
||||
},
|
||||
"FunctionDeclaration:exit"() {
|
||||
stack.pop();
|
||||
},
|
||||
|
||||
FunctionExpression(node) {
|
||||
stack.push(false);
|
||||
|
||||
if (enforceDeclarations && node.parent.type === "VariableDeclarator") {
|
||||
context.report({ node: node.parent, messageId: "declaration" });
|
||||
}
|
||||
},
|
||||
"FunctionExpression:exit"() {
|
||||
stack.pop();
|
||||
},
|
||||
|
||||
ThisExpression() {
|
||||
if (stack.length > 0) {
|
||||
stack[stack.length - 1] = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!allowArrowFunctions) {
|
||||
nodesToCheck.ArrowFunctionExpression = function() {
|
||||
stack.push(false);
|
||||
};
|
||||
|
||||
nodesToCheck["ArrowFunctionExpression:exit"] = function(node) {
|
||||
const hasThisExpr = stack.pop();
|
||||
|
||||
if (enforceDeclarations && !hasThisExpr && node.parent.type === "VariableDeclarator") {
|
||||
context.report({ node: node.parent, messageId: "declaration" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return nodesToCheck;
|
||||
|
||||
}
|
||||
};
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* @fileoverview enforce consistent line breaks inside function parentheses
|
||||
* @author Teddy Katz
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent line breaks inside function parentheses",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/function-paren-newline"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["always", "never", "consistent", "multiline"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
minItems: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expectedBefore: "Expected newline before ')'.",
|
||||
expectedAfter: "Expected newline after '('.",
|
||||
unexpectedBefore: "Unexpected newline before '('.",
|
||||
unexpectedAfter: "Unexpected newline after ')'."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const rawOption = context.options[0] || "multiline";
|
||||
const multilineOption = rawOption === "multiline";
|
||||
const consistentOption = rawOption === "consistent";
|
||||
let minItems;
|
||||
|
||||
if (typeof rawOption === "object") {
|
||||
minItems = rawOption.minItems;
|
||||
} else if (rawOption === "always") {
|
||||
minItems = 0;
|
||||
} else if (rawOption === "never") {
|
||||
minItems = Infinity;
|
||||
} else {
|
||||
minItems = null;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether there should be newlines inside function parens
|
||||
* @param {ASTNode[]} elements The arguments or parameters in the list
|
||||
* @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
|
||||
* @returns {boolean} `true` if there should be newlines inside the function parens
|
||||
*/
|
||||
function shouldHaveNewlines(elements, hasLeftNewline) {
|
||||
if (multilineOption) {
|
||||
return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line);
|
||||
}
|
||||
if (consistentOption) {
|
||||
return hasLeftNewline;
|
||||
}
|
||||
return elements.length >= minItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a list of arguments or parameters
|
||||
* @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
|
||||
* @param {ASTNode[]} elements The arguments or parameters in the list
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateParens(parens, elements) {
|
||||
const leftParen = parens.leftParen;
|
||||
const rightParen = parens.rightParen;
|
||||
const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
|
||||
const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
|
||||
const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
|
||||
const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen);
|
||||
const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
|
||||
|
||||
if (hasLeftNewline && !needsNewlines) {
|
||||
context.report({
|
||||
node: leftParen,
|
||||
messageId: "unexpectedAfter",
|
||||
fix(fixer) {
|
||||
return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim()
|
||||
|
||||
// If there is a comment between the ( and the first element, don't do a fix.
|
||||
? null
|
||||
: fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]);
|
||||
}
|
||||
});
|
||||
} else if (!hasLeftNewline && needsNewlines) {
|
||||
context.report({
|
||||
node: leftParen,
|
||||
messageId: "expectedAfter",
|
||||
fix: fixer => fixer.insertTextAfter(leftParen, "\n")
|
||||
});
|
||||
}
|
||||
|
||||
if (hasRightNewline && !needsNewlines) {
|
||||
context.report({
|
||||
node: rightParen,
|
||||
messageId: "unexpectedBefore",
|
||||
fix(fixer) {
|
||||
return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim()
|
||||
|
||||
// If there is a comment between the last element and the ), don't do a fix.
|
||||
? null
|
||||
: fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]);
|
||||
}
|
||||
});
|
||||
} else if (!hasRightNewline && needsNewlines) {
|
||||
context.report({
|
||||
node: rightParen,
|
||||
messageId: "expectedBefore",
|
||||
fix: fixer => fixer.insertTextBefore(rightParen, "\n")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the left paren and right paren tokens of a node.
|
||||
* @param {ASTNode} node The node with parens
|
||||
* @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
|
||||
* Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
|
||||
* with a single parameter)
|
||||
*/
|
||||
function getParenTokens(node) {
|
||||
switch (node.type) {
|
||||
case "NewExpression":
|
||||
if (!node.arguments.length && !(
|
||||
astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) &&
|
||||
astUtils.isClosingParenToken(sourceCode.getLastToken(node))
|
||||
)) {
|
||||
|
||||
// If the NewExpression does not have parens (e.g. `new Foo`), return null.
|
||||
return null;
|
||||
}
|
||||
|
||||
// falls through
|
||||
|
||||
case "CallExpression":
|
||||
return {
|
||||
leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken),
|
||||
rightParen: sourceCode.getLastToken(node)
|
||||
};
|
||||
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression": {
|
||||
const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken);
|
||||
const rightParen = node.params.length
|
||||
? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)
|
||||
: sourceCode.getTokenAfter(leftParen);
|
||||
|
||||
return { leftParen, rightParen };
|
||||
}
|
||||
|
||||
case "ArrowFunctionExpression": {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
|
||||
if (!astUtils.isOpeningParenToken(firstToken)) {
|
||||
|
||||
// If the ArrowFunctionExpression has a single param without parens, return null.
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
leftParen: firstToken,
|
||||
rightParen: sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken)
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new TypeError(`unexpected node with type ${node.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the parentheses for a node
|
||||
* @param {ASTNode} node The node with parens
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateNode(node) {
|
||||
const parens = getParenTokens(node);
|
||||
|
||||
if (parens) {
|
||||
validateParens(parens, astUtils.isFunction(node) ? node.params : node.arguments);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
ArrowFunctionExpression: validateNode,
|
||||
CallExpression: validateNode,
|
||||
FunctionDeclaration: validateNode,
|
||||
FunctionExpression: validateNode,
|
||||
NewExpression: validateNode
|
||||
};
|
||||
}
|
||||
};
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* @fileoverview Rule to check the spacing around the * in generator functions.
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const OVERRIDE_SCHEMA = {
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["before", "after", "both", "neither"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: { type: "boolean" },
|
||||
after: { type: "boolean" }
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent spacing around `*` operators in generator functions",
|
||||
category: "ECMAScript 6",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/generator-star-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["before", "after", "both", "neither"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: { type: "boolean" },
|
||||
after: { type: "boolean" },
|
||||
named: OVERRIDE_SCHEMA,
|
||||
anonymous: OVERRIDE_SCHEMA,
|
||||
method: OVERRIDE_SCHEMA
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
missingBefore: "Missing space before *.",
|
||||
missingAfter: "Missing space after *.",
|
||||
unexpectedBefore: "Unexpected space before *.",
|
||||
unexpectedAfter: "Unexpected space after *."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const optionDefinitions = {
|
||||
before: { before: true, after: false },
|
||||
after: { before: false, after: true },
|
||||
both: { before: true, after: true },
|
||||
neither: { before: false, after: false }
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns resolved option definitions based on an option and defaults
|
||||
*
|
||||
* @param {any} option - The option object or string value
|
||||
* @param {Object} defaults - The defaults to use if options are not present
|
||||
* @returns {Object} the resolved object definition
|
||||
*/
|
||||
function optionToDefinition(option, defaults) {
|
||||
if (!option) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
return typeof option === "string"
|
||||
? optionDefinitions[option]
|
||||
: Object.assign({}, defaults, option);
|
||||
}
|
||||
|
||||
const modes = (function(option) {
|
||||
const defaults = optionToDefinition(option, optionDefinitions.before);
|
||||
|
||||
return {
|
||||
named: optionToDefinition(option.named, defaults),
|
||||
anonymous: optionToDefinition(option.anonymous, defaults),
|
||||
method: optionToDefinition(option.method, defaults)
|
||||
};
|
||||
}(context.options[0] || {}));
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Checks if the given token is a star token or not.
|
||||
*
|
||||
* @param {Token} token - The token to check.
|
||||
* @returns {boolean} `true` if the token is a star token.
|
||||
*/
|
||||
function isStarToken(token) {
|
||||
return token.value === "*" && token.type === "Punctuator";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the generator star token of the given function node.
|
||||
*
|
||||
* @param {ASTNode} node - The function node to get.
|
||||
* @returns {Token} Found star token.
|
||||
*/
|
||||
function getStarToken(node) {
|
||||
return sourceCode.getFirstToken(
|
||||
(node.parent.method || node.parent.type === "MethodDefinition") ? node.parent : node,
|
||||
isStarToken
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* capitalize a given string.
|
||||
* @param {string} str the given string.
|
||||
* @returns {string} the capitalized string.
|
||||
*/
|
||||
function capitalize(str) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the spacing between two tokens before or after the star token.
|
||||
*
|
||||
* @param {string} kind Either "named", "anonymous", or "method"
|
||||
* @param {string} side Either "before" or "after".
|
||||
* @param {Token} leftToken `function` keyword token if side is "before", or
|
||||
* star token if side is "after".
|
||||
* @param {Token} rightToken Star token if side is "before", or identifier
|
||||
* token if side is "after".
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacing(kind, side, leftToken, rightToken) {
|
||||
if (!!(rightToken.range[0] - leftToken.range[1]) !== modes[kind][side]) {
|
||||
const after = leftToken.value === "*";
|
||||
const spaceRequired = modes[kind][side];
|
||||
const node = after ? leftToken : rightToken;
|
||||
const messageId = `${spaceRequired ? "missing" : "unexpected"}${capitalize(side)}`;
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId,
|
||||
fix(fixer) {
|
||||
if (spaceRequired) {
|
||||
if (after) {
|
||||
return fixer.insertTextAfter(node, " ");
|
||||
}
|
||||
return fixer.insertTextBefore(node, " ");
|
||||
}
|
||||
return fixer.removeRange([leftToken.range[1], rightToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforces the spacing around the star if node is a generator function.
|
||||
*
|
||||
* @param {ASTNode} node A function expression or declaration node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkFunction(node) {
|
||||
if (!node.generator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const starToken = getStarToken(node);
|
||||
const prevToken = sourceCode.getTokenBefore(starToken);
|
||||
const nextToken = sourceCode.getTokenAfter(starToken);
|
||||
|
||||
let kind = "named";
|
||||
|
||||
if (node.parent.type === "MethodDefinition" || (node.parent.type === "Property" && node.parent.method)) {
|
||||
kind = "method";
|
||||
} else if (!node.id) {
|
||||
kind = "anonymous";
|
||||
}
|
||||
|
||||
// Only check before when preceded by `function`|`static` keyword
|
||||
if (!(kind === "method" && starToken === sourceCode.getFirstToken(node.parent))) {
|
||||
checkSpacing(kind, "before", prevToken, starToken);
|
||||
}
|
||||
|
||||
checkSpacing(kind, "after", starToken, nextToken);
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: checkFunction,
|
||||
FunctionExpression: checkFunction
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* @fileoverview Enforces that a return statement is present in property getters.
|
||||
* @author Aladdin-ADD(hh_2013@foxmail.com)
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/;
|
||||
|
||||
/**
|
||||
* Checks a given code path segment is reachable.
|
||||
*
|
||||
* @param {CodePathSegment} segment - A segment to check.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a readable location.
|
||||
*
|
||||
* - FunctionExpression -> the function name or `function` keyword.
|
||||
*
|
||||
* @param {ASTNode} node - A function node to get.
|
||||
* @returns {ASTNode|Token} The node or the token of a location.
|
||||
*/
|
||||
function getId(node) {
|
||||
return node.id || node;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
|
||||
docs: {
|
||||
description: "enforce `return` statements in getters",
|
||||
category: "Possible Errors",
|
||||
recommended: true,
|
||||
url: "https://eslint.org/docs/rules/getter-return"
|
||||
},
|
||||
|
||||
fixable: null,
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowImplicit: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
expected: "Expected to return a value in {{name}}.",
|
||||
expectedAlways: "Expected {{name}} to always return a value."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0] || { allowImplicit: false };
|
||||
|
||||
let funcInfo = {
|
||||
upper: null,
|
||||
codePath: null,
|
||||
hasReturn: false,
|
||||
shouldCheck: false,
|
||||
node: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether or not the last code path segment is reachable.
|
||||
* Then reports this function if the segment is reachable.
|
||||
*
|
||||
* If the last code path segment is reachable, there are paths which are not
|
||||
* returned or thrown.
|
||||
*
|
||||
* @param {ASTNode} node - A node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkLastSegment(node) {
|
||||
if (funcInfo.shouldCheck &&
|
||||
funcInfo.codePath.currentSegments.some(isReachable)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
loc: getId(node).loc.start,
|
||||
messageId: funcInfo.hasReturn ? "expectedAlways" : "expected",
|
||||
data: {
|
||||
name: astUtils.getFunctionNameWithKind(funcInfo.node)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node means a getter function.
|
||||
* @param {ASTNode} node - a node to check.
|
||||
* @returns {boolean} if node means a getter, return true; else return false.
|
||||
*/
|
||||
function isGetter(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
if (TARGET_NODE_TYPE.test(node.type) && node.body.type === "BlockStatement") {
|
||||
if (parent.kind === "get") {
|
||||
return true;
|
||||
}
|
||||
if (parent.type === "Property" && astUtils.getStaticPropertyName(parent) === "get" && parent.parent.type === "ObjectExpression") {
|
||||
|
||||
// Object.defineProperty()
|
||||
if (parent.parent.parent.type === "CallExpression" &&
|
||||
astUtils.getStaticPropertyName(parent.parent.parent.callee) === "defineProperty") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Object.defineProperties()
|
||||
if (parent.parent.parent.type === "Property" &&
|
||||
parent.parent.parent.parent.type === "ObjectExpression" &&
|
||||
parent.parent.parent.parent.parent.type === "CallExpression" &&
|
||||
astUtils.getStaticPropertyName(parent.parent.parent.parent.parent.callee) === "defineProperties") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
|
||||
// Stacks this function's information.
|
||||
onCodePathStart(codePath, node) {
|
||||
funcInfo = {
|
||||
upper: funcInfo,
|
||||
codePath,
|
||||
hasReturn: false,
|
||||
shouldCheck: isGetter(node),
|
||||
node
|
||||
};
|
||||
},
|
||||
|
||||
// Pops this function's information.
|
||||
onCodePathEnd() {
|
||||
funcInfo = funcInfo.upper;
|
||||
},
|
||||
|
||||
// Checks the return statement is valid.
|
||||
ReturnStatement(node) {
|
||||
if (funcInfo.shouldCheck) {
|
||||
funcInfo.hasReturn = true;
|
||||
|
||||
// if allowImplicit: false, should also check node.argument
|
||||
if (!options.allowImplicit && !node.argument) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "expected",
|
||||
data: {
|
||||
name: astUtils.getFunctionNameWithKind(funcInfo.node)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Reports a given function if the last path is reachable.
|
||||
"FunctionExpression:exit": checkLastSegment,
|
||||
"ArrowFunctionExpression:exit": checkLastSegment
|
||||
};
|
||||
}
|
||||
};
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @fileoverview Rule for disallowing require() outside of the top-level module context
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const ACCEPTABLE_PARENTS = [
|
||||
"AssignmentExpression",
|
||||
"VariableDeclarator",
|
||||
"MemberExpression",
|
||||
"ExpressionStatement",
|
||||
"CallExpression",
|
||||
"ConditionalExpression",
|
||||
"Program",
|
||||
"VariableDeclaration"
|
||||
];
|
||||
|
||||
/**
|
||||
* Finds the eslint-scope reference in the given scope.
|
||||
* @param {Object} scope The scope to search.
|
||||
* @param {ASTNode} node The identifier node.
|
||||
* @returns {Reference|null} Returns the found reference or null if none were found.
|
||||
*/
|
||||
function findReference(scope, node) {
|
||||
const references = scope.references.filter(reference => reference.identifier.range[0] === node.range[0] &&
|
||||
reference.identifier.range[1] === node.range[1]);
|
||||
|
||||
/* istanbul ignore else: correctly returns null */
|
||||
if (references.length === 1) {
|
||||
return references[0];
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given identifier node is shadowed in the given scope.
|
||||
* @param {Object} scope The current scope.
|
||||
* @param {ASTNode} node The identifier node to check.
|
||||
* @returns {boolean} Whether or not the name is shadowed.
|
||||
*/
|
||||
function isShadowed(scope, node) {
|
||||
const reference = findReference(scope, node);
|
||||
|
||||
return reference && reference.resolved && reference.resolved.defs.length > 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require `require()` calls to be placed at top-level module scope",
|
||||
category: "Node.js and CommonJS",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/global-require"
|
||||
},
|
||||
|
||||
schema: []
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const currentScope = context.getScope();
|
||||
|
||||
if (node.callee.name === "require" && !isShadowed(currentScope, node.callee)) {
|
||||
const isGoodRequire = context.getAncestors().every(parent => ACCEPTABLE_PARENTS.indexOf(parent.type) > -1);
|
||||
|
||||
if (!isGoodRequire) {
|
||||
context.report({ node, message: "Unexpected require()." });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag for-in loops without if statements inside
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require `for-in` loops to include an `if` statement",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/guard-for-in"
|
||||
},
|
||||
|
||||
schema: []
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
return {
|
||||
|
||||
ForInStatement(node) {
|
||||
const body = node.body;
|
||||
|
||||
// empty statement
|
||||
if (body.type === "EmptyStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// if statement
|
||||
if (body.type === "IfStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// empty block
|
||||
if (body.type === "BlockStatement" && body.body.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// block with just if statement
|
||||
if (body.type === "BlockStatement" && body.body.length === 1 && body.body[0].type === "IfStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// block that starts with if statement
|
||||
if (body.type === "BlockStatement" && body.body.length >= 1 && body.body[0].type === "IfStatement") {
|
||||
const i = body.body[0];
|
||||
|
||||
// ... whose consequent is a continue
|
||||
if (i.consequent.type === "ContinueStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
// ... whose consequent is a block that contains only a continue
|
||||
if (i.consequent.type === "BlockStatement" && i.consequent.body.length === 1 && i.consequent.body[0].type === "ContinueStatement") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
context.report({ node, message: "The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype." });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @fileoverview Ensure handling of errors when we know they exist.
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require error handling in callbacks",
|
||||
category: "Node.js and CommonJS",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/handle-callback-err"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const errorArgument = context.options[0] || "err";
|
||||
|
||||
/**
|
||||
* Checks if the given argument should be interpreted as a regexp pattern.
|
||||
* @param {string} stringToCheck The string which should be checked.
|
||||
* @returns {boolean} Whether or not the string should be interpreted as a pattern.
|
||||
*/
|
||||
function isPattern(stringToCheck) {
|
||||
const firstChar = stringToCheck[0];
|
||||
|
||||
return firstChar === "^";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given name matches the configured error argument.
|
||||
* @param {string} name The name which should be compared.
|
||||
* @returns {boolean} Whether or not the given name matches the configured error variable name.
|
||||
*/
|
||||
function matchesConfiguredErrorName(name) {
|
||||
if (isPattern(errorArgument)) {
|
||||
const regexp = new RegExp(errorArgument);
|
||||
|
||||
return regexp.test(name);
|
||||
}
|
||||
return name === errorArgument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters of a given function scope.
|
||||
* @param {Object} scope The function scope.
|
||||
* @returns {array} All parameters of the given scope.
|
||||
*/
|
||||
function getParameters(scope) {
|
||||
return scope.variables.filter(variable => variable.defs[0] && variable.defs[0].type === "Parameter");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if we're handling the error object properly.
|
||||
* @param {ASTNode} node The AST node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkForError(node) {
|
||||
const scope = context.getScope(),
|
||||
parameters = getParameters(scope),
|
||||
firstParameter = parameters[0];
|
||||
|
||||
if (firstParameter && matchesConfiguredErrorName(firstParameter.name)) {
|
||||
if (firstParameter.references.length === 0) {
|
||||
context.report({ node, message: "Expected error to be handled." });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
FunctionDeclaration: checkForError,
|
||||
FunctionExpression: checkForError,
|
||||
ArrowFunctionExpression: checkForError
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @fileoverview Rule that warns when identifier names that are
|
||||
* blacklisted in the configuration are used.
|
||||
* @author Keith Cirkel (http://keithcirkel.co.uk)
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "disallow specified identifiers",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/id-blacklist"
|
||||
},
|
||||
|
||||
schema: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const blacklist = context.options;
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a string matches the provided pattern
|
||||
* @param {string} name The string to check.
|
||||
* @returns {boolean} if the string is a match
|
||||
* @private
|
||||
*/
|
||||
function isInvalid(name) {
|
||||
return blacklist.indexOf(name) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if we should report an error or not based on the effective
|
||||
* parent node and the identifier name.
|
||||
* @param {ASTNode} effectiveParent The effective parent node of the node to be reported
|
||||
* @param {string} name The identifier name of the identifier node
|
||||
* @returns {boolean} whether an error should be reported or not
|
||||
*/
|
||||
function shouldReport(effectiveParent, name) {
|
||||
return effectiveParent.type !== "CallExpression" &&
|
||||
effectiveParent.type !== "NewExpression" &&
|
||||
isInvalid(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an AST node as a rule violation.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node) {
|
||||
context.report({
|
||||
node,
|
||||
message: "Identifier '{{name}}' is blacklisted.",
|
||||
data: {
|
||||
name: node.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Identifier(node) {
|
||||
const name = node.name,
|
||||
effectiveParent = (node.parent.type === "MemberExpression") ? node.parent.parent : node.parent;
|
||||
|
||||
// MemberExpressions get special rules
|
||||
if (node.parent.type === "MemberExpression") {
|
||||
|
||||
// Always check object names
|
||||
if (node.parent.object.type === "Identifier" &&
|
||||
node.parent.object.name === node.name) {
|
||||
if (isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Report AssignmentExpressions only if they are the left side of the assignment
|
||||
} else if (effectiveParent.type === "AssignmentExpression" &&
|
||||
(effectiveParent.right.type !== "MemberExpression" ||
|
||||
effectiveParent.left.type === "MemberExpression" &&
|
||||
effectiveParent.left.property.name === node.name)) {
|
||||
if (isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Properties have their own rules
|
||||
} else if (node.parent.type === "Property") {
|
||||
|
||||
if (shouldReport(effectiveParent, name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Report anything that is a match and not a CallExpression
|
||||
} else if (shouldReport(effectiveParent, name)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @fileoverview Rule that warns when identifier names are shorter or longer
|
||||
* than the values provided in configuration.
|
||||
* @author Burak Yigit Kaya aka BYK
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce minimum and maximum identifier lengths",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/id-length"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
min: {
|
||||
type: "number"
|
||||
},
|
||||
max: {
|
||||
type: "number"
|
||||
},
|
||||
exceptions: {
|
||||
type: "array",
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
enum: ["always", "never"]
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0] || {};
|
||||
const minLength = typeof options.min !== "undefined" ? options.min : 2;
|
||||
const maxLength = typeof options.max !== "undefined" ? options.max : Infinity;
|
||||
const properties = options.properties !== "never";
|
||||
const exceptions = (options.exceptions ? options.exceptions : [])
|
||||
.reduce((obj, item) => {
|
||||
obj[item] = true;
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
const SUPPORTED_EXPRESSIONS = {
|
||||
MemberExpression: properties && function(parent) {
|
||||
return !parent.computed && (
|
||||
|
||||
// regular property assignment
|
||||
(parent.parent.left === parent && parent.parent.type === "AssignmentExpression" ||
|
||||
|
||||
// or the last identifier in an ObjectPattern destructuring
|
||||
parent.parent.type === "Property" && parent.parent.value === parent &&
|
||||
parent.parent.parent.type === "ObjectPattern" && parent.parent.parent.parent.left === parent.parent.parent)
|
||||
);
|
||||
},
|
||||
AssignmentPattern(parent, node) {
|
||||
return parent.left === node;
|
||||
},
|
||||
VariableDeclarator(parent, node) {
|
||||
return parent.id === node;
|
||||
},
|
||||
Property: properties && function(parent, node) {
|
||||
return parent.key === node;
|
||||
},
|
||||
ImportDefaultSpecifier: true,
|
||||
RestElement: true,
|
||||
FunctionExpression: true,
|
||||
ArrowFunctionExpression: true,
|
||||
ClassDeclaration: true,
|
||||
FunctionDeclaration: true,
|
||||
MethodDefinition: true,
|
||||
CatchClause: true
|
||||
};
|
||||
|
||||
return {
|
||||
Identifier(node) {
|
||||
const name = node.name;
|
||||
const parent = node.parent;
|
||||
|
||||
const isShort = name.length < minLength;
|
||||
const isLong = name.length > maxLength;
|
||||
|
||||
if (!(isShort || isLong) || exceptions[name]) {
|
||||
return; // Nothing to report
|
||||
}
|
||||
|
||||
const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type];
|
||||
|
||||
if (isValidExpression && (isValidExpression === true || isValidExpression(parent, node))) {
|
||||
context.report({
|
||||
node,
|
||||
message: isShort
|
||||
? "Identifier name '{{name}}' is too short (< {{min}})."
|
||||
: "Identifier name '{{name}}' is too long (> {{max}}).",
|
||||
data: { name, min: minLength, max: maxLength }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* @fileoverview Rule to flag non-matching identifiers
|
||||
* @author Matthieu Larcher
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require identifiers to match a specified regular expression",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/id-match"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
properties: {
|
||||
type: "boolean"
|
||||
},
|
||||
onlyDeclarations: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignoreDestructuring: {
|
||||
type: "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
const pattern = context.options[0] || "^.+$",
|
||||
regexp = new RegExp(pattern);
|
||||
|
||||
const options = context.options[1] || {},
|
||||
properties = !!options.properties,
|
||||
onlyDeclarations = !!options.onlyDeclarations,
|
||||
ignoreDestructuring = !!options.ignoreDestructuring;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// contains reported nodes to avoid reporting twice on destructuring with shorthand notation
|
||||
const reported = new Map();
|
||||
const ALLOWED_PARENT_TYPES = new Set(["CallExpression", "NewExpression"]);
|
||||
const DECLARATION_TYPES = new Set(["FunctionDeclaration", "VariableDeclarator"]);
|
||||
const IMPORT_TYPES = new Set(["ImportSpecifier", "ImportNamespaceSpecifier", "ImportDefaultSpecifier"]);
|
||||
|
||||
/**
|
||||
* Checks if a string matches the provided pattern
|
||||
* @param {string} name The string to check.
|
||||
* @returns {boolean} if the string is a match
|
||||
* @private
|
||||
*/
|
||||
function isInvalid(name) {
|
||||
return !regexp.test(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a parent of a node is an ObjectPattern.
|
||||
* @param {ASTNode} node The node to check.
|
||||
* @returns {boolean} if the node is inside an ObjectPattern
|
||||
* @private
|
||||
*/
|
||||
function isInsideObjectPattern(node) {
|
||||
let { parent } = node;
|
||||
|
||||
while (parent) {
|
||||
if (parent.type === "ObjectPattern") {
|
||||
return true;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if we should report an error or not based on the effective
|
||||
* parent node and the identifier name.
|
||||
* @param {ASTNode} effectiveParent The effective parent node of the node to be reported
|
||||
* @param {string} name The identifier name of the identifier node
|
||||
* @returns {boolean} whether an error should be reported or not
|
||||
*/
|
||||
function shouldReport(effectiveParent, name) {
|
||||
return (!onlyDeclarations || DECLARATION_TYPES.has(effectiveParent.type)) &&
|
||||
!ALLOWED_PARENT_TYPES.has(effectiveParent.type) && isInvalid(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an AST node as a rule violation.
|
||||
* @param {ASTNode} node The node to report.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function report(node) {
|
||||
if (!reported.has(node)) {
|
||||
context.report({
|
||||
node,
|
||||
message: "Identifier '{{name}}' does not match the pattern '{{pattern}}'.",
|
||||
data: {
|
||||
name: node.name,
|
||||
pattern
|
||||
}
|
||||
});
|
||||
reported.set(node, true);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Identifier(node) {
|
||||
const name = node.name,
|
||||
parent = node.parent,
|
||||
effectiveParent = (parent.type === "MemberExpression") ? parent.parent : parent;
|
||||
|
||||
if (parent.type === "MemberExpression") {
|
||||
|
||||
if (!properties) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always check object names
|
||||
if (parent.object.type === "Identifier" &&
|
||||
parent.object.name === name) {
|
||||
if (isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Report AssignmentExpressions left side's assigned variable id
|
||||
} else if (effectiveParent.type === "AssignmentExpression" &&
|
||||
effectiveParent.left.type === "MemberExpression" &&
|
||||
effectiveParent.left.property.name === node.name) {
|
||||
if (isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Report AssignmentExpressions only if they are the left side of the assignment
|
||||
} else if (effectiveParent.type === "AssignmentExpression" && effectiveParent.right.type !== "MemberExpression") {
|
||||
if (isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Properties have their own rules, and
|
||||
* AssignmentPattern nodes can be treated like Properties:
|
||||
* e.g.: const { no_camelcased = false } = bar;
|
||||
*/
|
||||
} else if (parent.type === "Property" || parent.type === "AssignmentPattern") {
|
||||
|
||||
if (parent.parent && parent.parent.type === "ObjectPattern") {
|
||||
if (parent.shorthand && parent.value.left && isInvalid(name)) {
|
||||
|
||||
report(node);
|
||||
}
|
||||
|
||||
const assignmentKeyEqualsValue = parent.key.name === parent.value.name;
|
||||
|
||||
// prevent checking righthand side of destructured object
|
||||
if (!assignmentKeyEqualsValue && parent.key === node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueIsInvalid = parent.value.name && isInvalid(name);
|
||||
|
||||
// ignore destructuring if the option is set, unless a new identifier is created
|
||||
if (valueIsInvalid && !(assignmentKeyEqualsValue && ignoreDestructuring)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
// never check properties or always ignore destructuring
|
||||
if (!properties || (ignoreDestructuring && isInsideObjectPattern(node))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't check right hand side of AssignmentExpression to prevent duplicate warnings
|
||||
if (parent.right !== node && shouldReport(effectiveParent, name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Check if it's an import specifier
|
||||
} else if (IMPORT_TYPES.has(parent.type)) {
|
||||
|
||||
// Report only if the local imported identifier is invalid
|
||||
if (parent.local && parent.local.name === node.name && isInvalid(name)) {
|
||||
report(node);
|
||||
}
|
||||
|
||||
// Report anything that is invalid that isn't a CallExpression
|
||||
} else if (shouldReport(effectiveParent, name)) {
|
||||
report(node);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @fileoverview enforce the location of arrow function bodies
|
||||
* @author Sharmila Jesupaul
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce the location of arrow function bodies",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/implicit-arrow-linebreak"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["beside", "below"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
/**
|
||||
* Gets the applicable preference for a particular keyword
|
||||
* @returns {string} The applicable option for the keyword, e.g. 'beside'
|
||||
*/
|
||||
function getOption() {
|
||||
return context.options[0] || "beside";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the location of an arrow function body
|
||||
* @param {ASTNode} node The arrow function body
|
||||
* @param {string} keywordName The applicable keyword name for the arrow function body
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateExpression(node) {
|
||||
const option = getOption();
|
||||
|
||||
let tokenBefore = sourceCode.getTokenBefore(node.body);
|
||||
const hasParens = tokenBefore.value === "(";
|
||||
|
||||
if (node.type === "BlockStatement") {
|
||||
return;
|
||||
}
|
||||
|
||||
let fixerTarget = node.body;
|
||||
|
||||
if (hasParens) {
|
||||
|
||||
// Gets the first token before the function body that is not an open paren
|
||||
tokenBefore = sourceCode.getTokenBefore(node.body, token => token.value !== "(");
|
||||
fixerTarget = sourceCode.getTokenAfter(tokenBefore);
|
||||
}
|
||||
|
||||
if (tokenBefore.loc.end.line === fixerTarget.loc.start.line && option === "below") {
|
||||
context.report({
|
||||
node: fixerTarget,
|
||||
message: "Expected a linebreak before this expression.",
|
||||
fix: fixer => fixer.insertTextBefore(fixerTarget, "\n")
|
||||
});
|
||||
} else if (tokenBefore.loc.end.line !== fixerTarget.loc.start.line && option === "beside") {
|
||||
context.report({
|
||||
node: fixerTarget,
|
||||
message: "Expected no linebreak before this expression.",
|
||||
fix: fixer => fixer.replaceTextRange([tokenBefore.range[1], fixerTarget.range[0]], " ")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
return {
|
||||
ArrowFunctionExpression: node => validateExpression(node)
|
||||
};
|
||||
}
|
||||
};
|
||||
+1138
File diff suppressed because it is too large
Load Diff
+1593
File diff suppressed because it is too large
Load Diff
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @fileoverview A rule to control the style of variable initializations.
|
||||
* @author Colin Ihrig
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is a for loop.
|
||||
* @param {ASTNode} block - A node to check.
|
||||
* @returns {boolean} `true` when the node is a for loop.
|
||||
*/
|
||||
function isForLoop(block) {
|
||||
return block.type === "ForInStatement" ||
|
||||
block.type === "ForOfStatement" ||
|
||||
block.type === "ForStatement";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given declarator node has its initializer.
|
||||
* @param {ASTNode} node - A declarator node to check.
|
||||
* @returns {boolean} `true` when the node has its initializer.
|
||||
*/
|
||||
function isInitialized(node) {
|
||||
const declaration = node.parent;
|
||||
const block = declaration.parent;
|
||||
|
||||
if (isForLoop(block)) {
|
||||
if (block.type === "ForStatement") {
|
||||
return block.init === declaration;
|
||||
}
|
||||
return block.left === declaration;
|
||||
}
|
||||
return Boolean(node.init);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require or disallow initialization in variable declarations",
|
||||
category: "Variables",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/init-declarations"
|
||||
},
|
||||
|
||||
schema: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["always"]
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 1
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: [
|
||||
{
|
||||
enum: ["never"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
ignoreForLoopInit: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
minItems: 0,
|
||||
maxItems: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const MODE_ALWAYS = "always",
|
||||
MODE_NEVER = "never";
|
||||
|
||||
const mode = context.options[0] || MODE_ALWAYS;
|
||||
const params = context.options[1] || {};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
"VariableDeclaration:exit"(node) {
|
||||
|
||||
const kind = node.kind,
|
||||
declarations = node.declarations;
|
||||
|
||||
for (let i = 0; i < declarations.length; ++i) {
|
||||
const declaration = declarations[i],
|
||||
id = declaration.id,
|
||||
initialized = isInitialized(declaration),
|
||||
isIgnoredForLoop = params.ignoreForLoopInit && isForLoop(node.parent);
|
||||
|
||||
if (id.type !== "Identifier") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode === MODE_ALWAYS && !initialized) {
|
||||
context.report({
|
||||
node: declaration,
|
||||
message: "Variable '{{idName}}' should be initialized on declaration.",
|
||||
data: {
|
||||
idName: id.name
|
||||
}
|
||||
});
|
||||
} else if (mode === MODE_NEVER && kind !== "const" && initialized && !isIgnoredForLoop) {
|
||||
context.report({
|
||||
node: declaration,
|
||||
message: "Variable '{{idName}}' should not be initialized on declaration.",
|
||||
data: {
|
||||
idName: id.name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @fileoverview A rule to ensure consistent quotes used in jsx syntax.
|
||||
* @author Mathias Schreck <https://github.com/lo1tuma>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const QUOTE_SETTINGS = {
|
||||
"prefer-double": {
|
||||
quote: "\"",
|
||||
description: "singlequote",
|
||||
convert(str) {
|
||||
return str.replace(/'/g, "\"");
|
||||
}
|
||||
},
|
||||
"prefer-single": {
|
||||
quote: "'",
|
||||
description: "doublequote",
|
||||
convert(str) {
|
||||
return str.replace(/"/g, "'");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce the consistent use of either double or single quotes in JSX attributes",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/jsx-quotes"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["prefer-single", "prefer-double"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const quoteOption = context.options[0] || "prefer-double",
|
||||
setting = QUOTE_SETTINGS[quoteOption];
|
||||
|
||||
/**
|
||||
* Checks if the given string literal node uses the expected quotes
|
||||
* @param {ASTNode} node - A string literal node.
|
||||
* @returns {boolean} Whether or not the string literal used the expected quotes.
|
||||
* @public
|
||||
*/
|
||||
function usesExpectedQuotes(node) {
|
||||
return node.value.indexOf(setting.quote) !== -1 || astUtils.isSurroundedBy(node.raw, setting.quote);
|
||||
}
|
||||
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
const attributeValue = node.value;
|
||||
|
||||
if (attributeValue && astUtils.isStringLiteral(attributeValue) && !usesExpectedQuotes(attributeValue)) {
|
||||
context.report({
|
||||
node: attributeValue,
|
||||
message: "Unexpected usage of {{description}}.",
|
||||
data: {
|
||||
description: setting.description
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(attributeValue, setting.convert(attributeValue.raw));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+644
@@ -0,0 +1,644 @@
|
||||
/**
|
||||
* @fileoverview Rule to specify spacing of object literal keys and values
|
||||
* @author Brandon Mills
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether a string contains a line terminator as defined in
|
||||
* http://www.ecma-international.org/ecma-262/5.1/#sec-7.3
|
||||
* @param {string} str String to test.
|
||||
* @returns {boolean} True if str contains a line terminator.
|
||||
*/
|
||||
function containsLineTerminator(str) {
|
||||
return astUtils.LINEBREAK_MATCHER.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last element of an array.
|
||||
* @param {Array} arr An array.
|
||||
* @returns {any} Last element of arr.
|
||||
*/
|
||||
function last(arr) {
|
||||
return arr[arr.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a node is contained on a single line.
|
||||
* @param {ASTNode} node AST Node being evaluated.
|
||||
* @returns {boolean} True if the node is a single line.
|
||||
*/
|
||||
function isSingleLine(node) {
|
||||
return (node.loc.end.line === node.loc.start.line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a single option property from the configuration with defaults for undefined values
|
||||
* @param {Object} toOptions Object to be initialized
|
||||
* @param {Object} fromOptions Object to be initialized from
|
||||
* @returns {Object} The object with correctly initialized options and values
|
||||
*/
|
||||
function initOptionProperty(toOptions, fromOptions) {
|
||||
toOptions.mode = fromOptions.mode || "strict";
|
||||
|
||||
// Set value of beforeColon
|
||||
if (typeof fromOptions.beforeColon !== "undefined") {
|
||||
toOptions.beforeColon = +fromOptions.beforeColon;
|
||||
} else {
|
||||
toOptions.beforeColon = 0;
|
||||
}
|
||||
|
||||
// Set value of afterColon
|
||||
if (typeof fromOptions.afterColon !== "undefined") {
|
||||
toOptions.afterColon = +fromOptions.afterColon;
|
||||
} else {
|
||||
toOptions.afterColon = 1;
|
||||
}
|
||||
|
||||
// Set align if exists
|
||||
if (typeof fromOptions.align !== "undefined") {
|
||||
if (typeof fromOptions.align === "object") {
|
||||
toOptions.align = fromOptions.align;
|
||||
} else { // "string"
|
||||
toOptions.align = {
|
||||
on: fromOptions.align,
|
||||
mode: toOptions.mode,
|
||||
beforeColon: toOptions.beforeColon,
|
||||
afterColon: toOptions.afterColon
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return toOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all the option values (singleLine, multiLine and align) from the configuration with defaults for undefined values
|
||||
* @param {Object} toOptions Object to be initialized
|
||||
* @param {Object} fromOptions Object to be initialized from
|
||||
* @returns {Object} The object with correctly initialized options and values
|
||||
*/
|
||||
function initOptions(toOptions, fromOptions) {
|
||||
if (typeof fromOptions.align === "object") {
|
||||
|
||||
// Initialize the alignment configuration
|
||||
toOptions.align = initOptionProperty({}, fromOptions.align);
|
||||
toOptions.align.on = fromOptions.align.on || "colon";
|
||||
toOptions.align.mode = fromOptions.align.mode || "strict";
|
||||
|
||||
toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions));
|
||||
toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions));
|
||||
|
||||
} else { // string or undefined
|
||||
toOptions.multiLine = initOptionProperty({}, (fromOptions.multiLine || fromOptions));
|
||||
toOptions.singleLine = initOptionProperty({}, (fromOptions.singleLine || fromOptions));
|
||||
|
||||
// If alignment options are defined in multiLine, pull them out into the general align configuration
|
||||
if (toOptions.multiLine.align) {
|
||||
toOptions.align = {
|
||||
on: toOptions.multiLine.align.on,
|
||||
mode: toOptions.multiLine.align.mode || toOptions.multiLine.mode,
|
||||
beforeColon: toOptions.multiLine.align.beforeColon,
|
||||
afterColon: toOptions.multiLine.align.afterColon
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return toOptions;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const messages = {
|
||||
key: "{{error}} space after {{computed}}key '{{key}}'.",
|
||||
value: "{{error}} space before value for {{computed}}key '{{key}}'."
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent spacing between keys and values in object literal properties",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/key-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [{
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
align: {
|
||||
anyOf: [
|
||||
{
|
||||
enum: ["colon", "value"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
enum: ["strict", "minimum"]
|
||||
},
|
||||
on: {
|
||||
enum: ["colon", "value"]
|
||||
},
|
||||
beforeColon: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterColon: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
mode: {
|
||||
enum: ["strict", "minimum"]
|
||||
},
|
||||
beforeColon: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterColon: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
singleLine: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
enum: ["strict", "minimum"]
|
||||
},
|
||||
beforeColon: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterColon: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
multiLine: {
|
||||
type: "object",
|
||||
properties: {
|
||||
align: {
|
||||
anyOf: [
|
||||
{
|
||||
enum: ["colon", "value"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
enum: ["strict", "minimum"]
|
||||
},
|
||||
on: {
|
||||
enum: ["colon", "value"]
|
||||
},
|
||||
beforeColon: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterColon: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
mode: {
|
||||
enum: ["strict", "minimum"]
|
||||
},
|
||||
beforeColon: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterColon: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
singleLine: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
enum: ["strict", "minimum"]
|
||||
},
|
||||
beforeColon: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterColon: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
multiLine: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
enum: ["strict", "minimum"]
|
||||
},
|
||||
beforeColon: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterColon: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
},
|
||||
align: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
enum: ["strict", "minimum"]
|
||||
},
|
||||
on: {
|
||||
enum: ["colon", "value"]
|
||||
},
|
||||
beforeColon: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterColon: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/**
|
||||
* OPTIONS
|
||||
* "key-spacing": [2, {
|
||||
* beforeColon: false,
|
||||
* afterColon: true,
|
||||
* align: "colon" // Optional, or "value"
|
||||
* }
|
||||
*/
|
||||
const options = context.options[0] || {},
|
||||
ruleOptions = initOptions({}, options),
|
||||
multiLineOptions = ruleOptions.multiLine,
|
||||
singleLineOptions = ruleOptions.singleLine,
|
||||
alignmentOptions = ruleOptions.align || null;
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Checks whether a property is a member of the property group it follows.
|
||||
* @param {ASTNode} lastMember The last Property known to be in the group.
|
||||
* @param {ASTNode} candidate The next Property that might be in the group.
|
||||
* @returns {boolean} True if the candidate property is part of the group.
|
||||
*/
|
||||
function continuesPropertyGroup(lastMember, candidate) {
|
||||
const groupEndLine = lastMember.loc.start.line,
|
||||
candidateStartLine = candidate.loc.start.line;
|
||||
|
||||
if (candidateStartLine - groupEndLine <= 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check that the first comment is adjacent to the end of the group, the
|
||||
* last comment is adjacent to the candidate property, and that successive
|
||||
* comments are adjacent to each other.
|
||||
*/
|
||||
const leadingComments = sourceCode.getCommentsBefore(candidate);
|
||||
|
||||
if (
|
||||
leadingComments.length &&
|
||||
leadingComments[0].loc.start.line - groupEndLine <= 1 &&
|
||||
candidateStartLine - last(leadingComments).loc.end.line <= 1
|
||||
) {
|
||||
for (let i = 1; i < leadingComments.length; i++) {
|
||||
if (leadingComments[i].loc.start.line - leadingComments[i - 1].loc.end.line > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given property is key-value property.
|
||||
* @param {ASTNode} property Property node to check.
|
||||
* @returns {boolean} Whether the property is a key-value property.
|
||||
*/
|
||||
function isKeyValueProperty(property) {
|
||||
return !(
|
||||
(property.method ||
|
||||
property.shorthand ||
|
||||
property.kind !== "init" || property.type !== "Property") // Could be "ExperimentalSpreadProperty" or "SpreadElement"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from the given a node (a property.key node here) looks forward
|
||||
* until it finds the last token before a colon punctuator and returns it.
|
||||
* @param {ASTNode} node The node to start looking from.
|
||||
* @returns {ASTNode} The last token before a colon punctuator.
|
||||
*/
|
||||
function getLastTokenBeforeColon(node) {
|
||||
const colonToken = sourceCode.getTokenAfter(node, astUtils.isColonToken);
|
||||
|
||||
return sourceCode.getTokenBefore(colonToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from the given a node (a property.key node here) looks forward
|
||||
* until it finds the colon punctuator and returns it.
|
||||
* @param {ASTNode} node The node to start looking from.
|
||||
* @returns {ASTNode} The colon punctuator.
|
||||
*/
|
||||
function getNextColon(node) {
|
||||
return sourceCode.getTokenAfter(node, astUtils.isColonToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an object literal property's key as the identifier name or string value.
|
||||
* @param {ASTNode} property Property node whose key to retrieve.
|
||||
* @returns {string} The property's key.
|
||||
*/
|
||||
function getKey(property) {
|
||||
const key = property.key;
|
||||
|
||||
if (property.computed) {
|
||||
return sourceCode.getText().slice(key.range[0], key.range[1]);
|
||||
}
|
||||
|
||||
return property.key.name || property.key.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports an appropriately-formatted error if spacing is incorrect on one
|
||||
* side of the colon.
|
||||
* @param {ASTNode} property Key-value pair in an object literal.
|
||||
* @param {string} side Side being verified - either "key" or "value".
|
||||
* @param {string} whitespace Actual whitespace string.
|
||||
* @param {int} expected Expected whitespace length.
|
||||
* @param {string} mode Value of the mode as "strict" or "minimum"
|
||||
* @returns {void}
|
||||
*/
|
||||
function report(property, side, whitespace, expected, mode) {
|
||||
const diff = whitespace.length - expected,
|
||||
nextColon = getNextColon(property.key),
|
||||
tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { includeComments: true }),
|
||||
tokenAfterColon = sourceCode.getTokenAfter(nextColon, { includeComments: true }),
|
||||
isKeySide = side === "key",
|
||||
locStart = isKeySide ? tokenBeforeColon.loc.start : tokenAfterColon.loc.start,
|
||||
isExtra = diff > 0,
|
||||
diffAbs = Math.abs(diff),
|
||||
spaces = Array(diffAbs + 1).join(" ");
|
||||
|
||||
if ((
|
||||
diff && mode === "strict" ||
|
||||
diff < 0 && mode === "minimum" ||
|
||||
diff > 0 && !expected && mode === "minimum") &&
|
||||
!(expected && containsLineTerminator(whitespace))
|
||||
) {
|
||||
let fix;
|
||||
|
||||
if (isExtra) {
|
||||
let range;
|
||||
|
||||
// Remove whitespace
|
||||
if (isKeySide) {
|
||||
range = [tokenBeforeColon.range[1], tokenBeforeColon.range[1] + diffAbs];
|
||||
} else {
|
||||
range = [tokenAfterColon.range[0] - diffAbs, tokenAfterColon.range[0]];
|
||||
}
|
||||
fix = function(fixer) {
|
||||
return fixer.removeRange(range);
|
||||
};
|
||||
} else {
|
||||
|
||||
// Add whitespace
|
||||
if (isKeySide) {
|
||||
fix = function(fixer) {
|
||||
return fixer.insertTextAfter(tokenBeforeColon, spaces);
|
||||
};
|
||||
} else {
|
||||
fix = function(fixer) {
|
||||
return fixer.insertTextBefore(tokenAfterColon, spaces);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: property[side],
|
||||
loc: locStart,
|
||||
message: messages[side],
|
||||
data: {
|
||||
error: isExtra ? "Extra" : "Missing",
|
||||
computed: property.computed ? "computed " : "",
|
||||
key: getKey(property)
|
||||
},
|
||||
fix
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of characters in a key, including quotes around string
|
||||
* keys and braces around computed property keys.
|
||||
* @param {ASTNode} property Property of on object literal.
|
||||
* @returns {int} Width of the key.
|
||||
*/
|
||||
function getKeyWidth(property) {
|
||||
const startToken = sourceCode.getFirstToken(property);
|
||||
const endToken = getLastTokenBeforeColon(property.key);
|
||||
|
||||
return endToken.range[1] - startToken.range[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the whitespace around the colon in an object literal property.
|
||||
* @param {ASTNode} property Property node from an object literal.
|
||||
* @returns {Object} Whitespace before and after the property's colon.
|
||||
*/
|
||||
function getPropertyWhitespace(property) {
|
||||
const whitespace = /(\s*):(\s*)/.exec(sourceCode.getText().slice(
|
||||
property.key.range[1], property.value.range[0]
|
||||
));
|
||||
|
||||
if (whitespace) {
|
||||
return {
|
||||
beforeColon: whitespace[1],
|
||||
afterColon: whitespace[2]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates groups of properties.
|
||||
* @param {ASTNode} node ObjectExpression node being evaluated.
|
||||
* @returns {Array.<ASTNode[]>} Groups of property AST node lists.
|
||||
*/
|
||||
function createGroups(node) {
|
||||
if (node.properties.length === 1) {
|
||||
return [node.properties];
|
||||
}
|
||||
|
||||
return node.properties.reduce((groups, property) => {
|
||||
const currentGroup = last(groups),
|
||||
prev = last(currentGroup);
|
||||
|
||||
if (!prev || continuesPropertyGroup(prev, property)) {
|
||||
currentGroup.push(property);
|
||||
} else {
|
||||
groups.push([property]);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [
|
||||
[]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies correct vertical alignment of a group of properties.
|
||||
* @param {ASTNode[]} properties List of Property AST nodes.
|
||||
* @returns {void}
|
||||
*/
|
||||
function verifyGroupAlignment(properties) {
|
||||
const length = properties.length,
|
||||
widths = properties.map(getKeyWidth), // Width of keys, including quotes
|
||||
align = alignmentOptions.on; // "value" or "colon"
|
||||
let targetWidth = Math.max(...widths),
|
||||
beforeColon, afterColon, mode;
|
||||
|
||||
if (alignmentOptions && length > 1) { // When aligning values within a group, use the alignment configuration.
|
||||
beforeColon = alignmentOptions.beforeColon;
|
||||
afterColon = alignmentOptions.afterColon;
|
||||
mode = alignmentOptions.mode;
|
||||
} else {
|
||||
beforeColon = multiLineOptions.beforeColon;
|
||||
afterColon = multiLineOptions.afterColon;
|
||||
mode = alignmentOptions.mode;
|
||||
}
|
||||
|
||||
// Conditionally include one space before or after colon
|
||||
targetWidth += (align === "colon" ? beforeColon : afterColon);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const property = properties[i];
|
||||
const whitespace = getPropertyWhitespace(property);
|
||||
|
||||
if (whitespace) { // Object literal getters/setters lack a colon
|
||||
const width = widths[i];
|
||||
|
||||
if (align === "value") {
|
||||
report(property, "key", whitespace.beforeColon, beforeColon, mode);
|
||||
report(property, "value", whitespace.afterColon, targetWidth - width, mode);
|
||||
} else { // align = "colon"
|
||||
report(property, "key", whitespace.beforeColon, targetWidth - width, mode);
|
||||
report(property, "value", whitespace.afterColon, afterColon, mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies vertical alignment, taking into account groups of properties.
|
||||
* @param {ASTNode} node ObjectExpression node being evaluated.
|
||||
* @returns {void}
|
||||
*/
|
||||
function verifyAlignment(node) {
|
||||
createGroups(node).forEach(group => {
|
||||
verifyGroupAlignment(group.filter(isKeyValueProperty));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies spacing of property conforms to specified options.
|
||||
* @param {ASTNode} node Property node being evaluated.
|
||||
* @param {Object} lineOptions Configured singleLine or multiLine options
|
||||
* @returns {void}
|
||||
*/
|
||||
function verifySpacing(node, lineOptions) {
|
||||
const actual = getPropertyWhitespace(node);
|
||||
|
||||
if (actual) { // Object literal getters/setters lack colons
|
||||
report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode);
|
||||
report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies spacing of each property in a list.
|
||||
* @param {ASTNode[]} properties List of Property AST nodes.
|
||||
* @returns {void}
|
||||
*/
|
||||
function verifyListSpacing(properties) {
|
||||
const length = properties.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
verifySpacing(properties[i], singleLineOptions);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
if (alignmentOptions) { // Verify vertical alignment
|
||||
|
||||
return {
|
||||
ObjectExpression(node) {
|
||||
if (isSingleLine(node)) {
|
||||
verifyListSpacing(node.properties.filter(isKeyValueProperty));
|
||||
} else {
|
||||
verifyAlignment(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Obey beforeColon and afterColon in each property as configured
|
||||
return {
|
||||
Property(node) {
|
||||
verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
+580
@@ -0,0 +1,580 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce spacing before and after keywords.
|
||||
* @author Toru Nagashima
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils"),
|
||||
keywords = require("../util/keywords");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const PREV_TOKEN = /^[)\]}>]$/;
|
||||
const NEXT_TOKEN = /^(?:[([{<~!]|\+\+?|--?)$/;
|
||||
const PREV_TOKEN_M = /^[)\]}>*]$/;
|
||||
const NEXT_TOKEN_M = /^[{*]$/;
|
||||
const TEMPLATE_OPEN_PAREN = /\$\{$/;
|
||||
const TEMPLATE_CLOSE_PAREN = /^\}/;
|
||||
const CHECK_TYPE = /^(?:JSXElement|RegularExpression|String|Template)$/;
|
||||
const KEYS = keywords.concat(["as", "async", "await", "from", "get", "let", "of", "set", "yield"]);
|
||||
|
||||
// check duplications.
|
||||
(function() {
|
||||
KEYS.sort();
|
||||
for (let i = 1; i < KEYS.length; ++i) {
|
||||
if (KEYS[i] === KEYS[i - 1]) {
|
||||
throw new Error(`Duplication was found in the keyword list: ${KEYS[i]}`);
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given token is a "Template" token ends with "${".
|
||||
*
|
||||
* @param {Token} token - A token to check.
|
||||
* @returns {boolean} `true` if the token is a "Template" token ends with "${".
|
||||
*/
|
||||
function isOpenParenOfTemplate(token) {
|
||||
return token.type === "Template" && TEMPLATE_OPEN_PAREN.test(token.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given token is a "Template" token starts with "}".
|
||||
*
|
||||
* @param {Token} token - A token to check.
|
||||
* @returns {boolean} `true` if the token is a "Template" token starts with "}".
|
||||
*/
|
||||
function isCloseParenOfTemplate(token) {
|
||||
return token.type === "Template" && TEMPLATE_CLOSE_PAREN.test(token.value);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent spacing before and after keywords",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/keyword-spacing"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: { type: "boolean" },
|
||||
after: { type: "boolean" },
|
||||
overrides: {
|
||||
type: "object",
|
||||
properties: KEYS.reduce((retv, key) => {
|
||||
retv[key] = {
|
||||
type: "object",
|
||||
properties: {
|
||||
before: { type: "boolean" },
|
||||
after: { type: "boolean" }
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
return retv;
|
||||
}, {}),
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Reports a given token if there are not space(s) before the token.
|
||||
*
|
||||
* @param {Token} token - A token to report.
|
||||
* @param {RegExp} pattern - A pattern of the previous token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function expectSpaceBefore(token, pattern) {
|
||||
const prevToken = sourceCode.getTokenBefore(token);
|
||||
|
||||
if (prevToken &&
|
||||
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
|
||||
!isOpenParenOfTemplate(prevToken) &&
|
||||
astUtils.isTokenOnSameLine(prevToken, token) &&
|
||||
!sourceCode.isSpaceBetweenTokens(prevToken, token)
|
||||
) {
|
||||
context.report({
|
||||
loc: token.loc.start,
|
||||
message: "Expected space(s) before \"{{value}}\".",
|
||||
data: token,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(token, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given token if there are space(s) before the token.
|
||||
*
|
||||
* @param {Token} token - A token to report.
|
||||
* @param {RegExp} pattern - A pattern of the previous token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function unexpectSpaceBefore(token, pattern) {
|
||||
const prevToken = sourceCode.getTokenBefore(token);
|
||||
|
||||
if (prevToken &&
|
||||
(CHECK_TYPE.test(prevToken.type) || pattern.test(prevToken.value)) &&
|
||||
!isOpenParenOfTemplate(prevToken) &&
|
||||
astUtils.isTokenOnSameLine(prevToken, token) &&
|
||||
sourceCode.isSpaceBetweenTokens(prevToken, token)
|
||||
) {
|
||||
context.report({
|
||||
loc: token.loc.start,
|
||||
message: "Unexpected space(s) before \"{{value}}\".",
|
||||
data: token,
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([prevToken.range[1], token.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given token if there are not space(s) after the token.
|
||||
*
|
||||
* @param {Token} token - A token to report.
|
||||
* @param {RegExp} pattern - A pattern of the next token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function expectSpaceAfter(token, pattern) {
|
||||
const nextToken = sourceCode.getTokenAfter(token);
|
||||
|
||||
if (nextToken &&
|
||||
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
|
||||
!isCloseParenOfTemplate(nextToken) &&
|
||||
astUtils.isTokenOnSameLine(token, nextToken) &&
|
||||
!sourceCode.isSpaceBetweenTokens(token, nextToken)
|
||||
) {
|
||||
context.report({
|
||||
loc: token.loc.start,
|
||||
message: "Expected space(s) after \"{{value}}\".",
|
||||
data: token,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(token, " ");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given token if there are space(s) after the token.
|
||||
*
|
||||
* @param {Token} token - A token to report.
|
||||
* @param {RegExp} pattern - A pattern of the next token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function unexpectSpaceAfter(token, pattern) {
|
||||
const nextToken = sourceCode.getTokenAfter(token);
|
||||
|
||||
if (nextToken &&
|
||||
(CHECK_TYPE.test(nextToken.type) || pattern.test(nextToken.value)) &&
|
||||
!isCloseParenOfTemplate(nextToken) &&
|
||||
astUtils.isTokenOnSameLine(token, nextToken) &&
|
||||
sourceCode.isSpaceBetweenTokens(token, nextToken)
|
||||
) {
|
||||
context.report({
|
||||
loc: token.loc.start,
|
||||
message: "Unexpected space(s) after \"{{value}}\".",
|
||||
data: token,
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([token.range[1], nextToken.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the option object and determines check methods for each keyword.
|
||||
*
|
||||
* @param {Object|undefined} options - The option object to parse.
|
||||
* @returns {Object} - Normalized option object.
|
||||
* Keys are keywords (there are for every keyword).
|
||||
* Values are instances of `{"before": function, "after": function}`.
|
||||
*/
|
||||
function parseOptions(options) {
|
||||
const before = !options || options.before !== false;
|
||||
const after = !options || options.after !== false;
|
||||
const defaultValue = {
|
||||
before: before ? expectSpaceBefore : unexpectSpaceBefore,
|
||||
after: after ? expectSpaceAfter : unexpectSpaceAfter
|
||||
};
|
||||
const overrides = (options && options.overrides) || {};
|
||||
const retv = Object.create(null);
|
||||
|
||||
for (let i = 0; i < KEYS.length; ++i) {
|
||||
const key = KEYS[i];
|
||||
const override = overrides[key];
|
||||
|
||||
if (override) {
|
||||
const thisBefore = ("before" in override) ? override.before : before;
|
||||
const thisAfter = ("after" in override) ? override.after : after;
|
||||
|
||||
retv[key] = {
|
||||
before: thisBefore ? expectSpaceBefore : unexpectSpaceBefore,
|
||||
after: thisAfter ? expectSpaceAfter : unexpectSpaceAfter
|
||||
};
|
||||
} else {
|
||||
retv[key] = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return retv;
|
||||
}
|
||||
|
||||
const checkMethodMap = parseOptions(context.options[0]);
|
||||
|
||||
/**
|
||||
* Reports a given token if usage of spacing followed by the token is
|
||||
* invalid.
|
||||
*
|
||||
* @param {Token} token - A token to report.
|
||||
* @param {RegExp|undefined} pattern - Optional. A pattern of the previous
|
||||
* token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingBefore(token, pattern) {
|
||||
checkMethodMap[token.value].before(token, pattern || PREV_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given token if usage of spacing preceded by the token is
|
||||
* invalid.
|
||||
*
|
||||
* @param {Token} token - A token to report.
|
||||
* @param {RegExp|undefined} pattern - Optional. A pattern of the next
|
||||
* token to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingAfter(token, pattern) {
|
||||
checkMethodMap[token.value].after(token, pattern || NEXT_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a given token if usage of spacing around the token is invalid.
|
||||
*
|
||||
* @param {Token} token - A token to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingAround(token) {
|
||||
checkSpacingBefore(token);
|
||||
checkSpacingAfter(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the first token of a given node if the first token is a keyword
|
||||
* and usage of spacing around the token is invalid.
|
||||
*
|
||||
* @param {ASTNode|null} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingAroundFirstToken(node) {
|
||||
const firstToken = node && sourceCode.getFirstToken(node);
|
||||
|
||||
if (firstToken && firstToken.type === "Keyword") {
|
||||
checkSpacingAround(firstToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the first token of a given node if the first token is a keyword
|
||||
* and usage of spacing followed by the token is invalid.
|
||||
*
|
||||
* This is used for unary operators (e.g. `typeof`), `function`, and `super`.
|
||||
* Other rules are handling usage of spacing preceded by those keywords.
|
||||
*
|
||||
* @param {ASTNode|null} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingBeforeFirstToken(node) {
|
||||
const firstToken = node && sourceCode.getFirstToken(node);
|
||||
|
||||
if (firstToken && firstToken.type === "Keyword") {
|
||||
checkSpacingBefore(firstToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the previous token of a given node if the token is a keyword and
|
||||
* usage of spacing around the token is invalid.
|
||||
*
|
||||
* @param {ASTNode|null} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingAroundTokenBefore(node) {
|
||||
if (node) {
|
||||
const token = sourceCode.getTokenBefore(node, astUtils.isKeywordToken);
|
||||
|
||||
checkSpacingAround(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `async` or `function` keywords of a given node if usage of
|
||||
* spacing around those keywords is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForFunction(node) {
|
||||
const firstToken = node && sourceCode.getFirstToken(node);
|
||||
|
||||
if (firstToken &&
|
||||
((firstToken.type === "Keyword" && firstToken.value === "function") ||
|
||||
firstToken.value === "async")
|
||||
) {
|
||||
checkSpacingBefore(firstToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `class` and `extends` keywords of a given node if usage of
|
||||
* spacing around those keywords is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForClass(node) {
|
||||
checkSpacingAroundFirstToken(node);
|
||||
checkSpacingAroundTokenBefore(node.superClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `if` and `else` keywords of a given node if usage of spacing
|
||||
* around those keywords is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForIfStatement(node) {
|
||||
checkSpacingAroundFirstToken(node);
|
||||
checkSpacingAroundTokenBefore(node.alternate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `try`, `catch`, and `finally` keywords of a given node if usage
|
||||
* of spacing around those keywords is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForTryStatement(node) {
|
||||
checkSpacingAroundFirstToken(node);
|
||||
checkSpacingAroundFirstToken(node.handler);
|
||||
checkSpacingAroundTokenBefore(node.finalizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `do` and `while` keywords of a given node if usage of spacing
|
||||
* around those keywords is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForDoWhileStatement(node) {
|
||||
checkSpacingAroundFirstToken(node);
|
||||
checkSpacingAroundTokenBefore(node.test);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `for` and `in` keywords of a given node if usage of spacing
|
||||
* around those keywords is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForForInStatement(node) {
|
||||
checkSpacingAroundFirstToken(node);
|
||||
checkSpacingAroundTokenBefore(node.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `for` and `of` keywords of a given node if usage of spacing
|
||||
* around those keywords is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForForOfStatement(node) {
|
||||
if (node.await) {
|
||||
checkSpacingBefore(sourceCode.getFirstToken(node, 0));
|
||||
checkSpacingAfter(sourceCode.getFirstToken(node, 1));
|
||||
} else {
|
||||
checkSpacingAroundFirstToken(node);
|
||||
}
|
||||
checkSpacingAround(sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `import`, `export`, `as`, and `from` keywords of a given node if
|
||||
* usage of spacing around those keywords is invalid.
|
||||
*
|
||||
* This rule handles the `*` token in module declarations.
|
||||
*
|
||||
* import*as A from "./a"; /*error Expected space(s) after "import".
|
||||
* error Expected space(s) before "as".
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForModuleDeclaration(node) {
|
||||
const firstToken = sourceCode.getFirstToken(node);
|
||||
|
||||
checkSpacingBefore(firstToken, PREV_TOKEN_M);
|
||||
checkSpacingAfter(firstToken, NEXT_TOKEN_M);
|
||||
|
||||
if (node.source) {
|
||||
const fromToken = sourceCode.getTokenBefore(node.source);
|
||||
|
||||
checkSpacingBefore(fromToken, PREV_TOKEN_M);
|
||||
checkSpacingAfter(fromToken, NEXT_TOKEN_M);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `as` keyword of a given node if usage of spacing around this
|
||||
* keyword is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForImportNamespaceSpecifier(node) {
|
||||
const asToken = sourceCode.getFirstToken(node, 1);
|
||||
|
||||
checkSpacingBefore(asToken, PREV_TOKEN_M);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `static`, `get`, and `set` keywords of a given node if usage of
|
||||
* spacing around those keywords is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForProperty(node) {
|
||||
if (node.static) {
|
||||
checkSpacingAroundFirstToken(node);
|
||||
}
|
||||
if (node.kind === "get" ||
|
||||
node.kind === "set" ||
|
||||
(
|
||||
(node.method || node.type === "MethodDefinition") &&
|
||||
node.value.async
|
||||
)
|
||||
) {
|
||||
const token = sourceCode.getTokenBefore(
|
||||
node.key,
|
||||
tok => {
|
||||
switch (tok.value) {
|
||||
case "get":
|
||||
case "set":
|
||||
case "async":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
throw new Error("Failed to find token get, set, or async beside method name");
|
||||
}
|
||||
|
||||
|
||||
checkSpacingAround(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports `await` keyword of a given node if usage of spacing before
|
||||
* this keyword is invalid.
|
||||
*
|
||||
* @param {ASTNode} node - A node to report.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkSpacingForAwaitExpression(node) {
|
||||
checkSpacingBefore(sourceCode.getFirstToken(node));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
// Statements
|
||||
DebuggerStatement: checkSpacingAroundFirstToken,
|
||||
WithStatement: checkSpacingAroundFirstToken,
|
||||
|
||||
// Statements - Control flow
|
||||
BreakStatement: checkSpacingAroundFirstToken,
|
||||
ContinueStatement: checkSpacingAroundFirstToken,
|
||||
ReturnStatement: checkSpacingAroundFirstToken,
|
||||
ThrowStatement: checkSpacingAroundFirstToken,
|
||||
TryStatement: checkSpacingForTryStatement,
|
||||
|
||||
// Statements - Choice
|
||||
IfStatement: checkSpacingForIfStatement,
|
||||
SwitchStatement: checkSpacingAroundFirstToken,
|
||||
SwitchCase: checkSpacingAroundFirstToken,
|
||||
|
||||
// Statements - Loops
|
||||
DoWhileStatement: checkSpacingForDoWhileStatement,
|
||||
ForInStatement: checkSpacingForForInStatement,
|
||||
ForOfStatement: checkSpacingForForOfStatement,
|
||||
ForStatement: checkSpacingAroundFirstToken,
|
||||
WhileStatement: checkSpacingAroundFirstToken,
|
||||
|
||||
// Statements - Declarations
|
||||
ClassDeclaration: checkSpacingForClass,
|
||||
ExportNamedDeclaration: checkSpacingForModuleDeclaration,
|
||||
ExportDefaultDeclaration: checkSpacingAroundFirstToken,
|
||||
ExportAllDeclaration: checkSpacingForModuleDeclaration,
|
||||
FunctionDeclaration: checkSpacingForFunction,
|
||||
ImportDeclaration: checkSpacingForModuleDeclaration,
|
||||
VariableDeclaration: checkSpacingAroundFirstToken,
|
||||
|
||||
// Expressions
|
||||
ArrowFunctionExpression: checkSpacingForFunction,
|
||||
AwaitExpression: checkSpacingForAwaitExpression,
|
||||
ClassExpression: checkSpacingForClass,
|
||||
FunctionExpression: checkSpacingForFunction,
|
||||
NewExpression: checkSpacingBeforeFirstToken,
|
||||
Super: checkSpacingBeforeFirstToken,
|
||||
ThisExpression: checkSpacingBeforeFirstToken,
|
||||
UnaryExpression: checkSpacingBeforeFirstToken,
|
||||
YieldExpression: checkSpacingBeforeFirstToken,
|
||||
|
||||
// Others
|
||||
ImportNamespaceSpecifier: checkSpacingForImportNamespaceSpecifier,
|
||||
MethodDefinition: checkSpacingForProperty,
|
||||
Property: checkSpacingForProperty
|
||||
};
|
||||
}
|
||||
};
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce the position of line comments
|
||||
* @author Alberto Rodríguez
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce position of line comments",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/line-comment-position"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["above", "beside"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
position: {
|
||||
enum: ["above", "beside"]
|
||||
},
|
||||
ignorePattern: {
|
||||
type: "string"
|
||||
},
|
||||
applyDefaultPatterns: {
|
||||
type: "boolean"
|
||||
},
|
||||
applyDefaultIgnorePatterns: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const options = context.options[0];
|
||||
|
||||
let above,
|
||||
ignorePattern,
|
||||
applyDefaultIgnorePatterns = true;
|
||||
|
||||
if (!options || typeof options === "string") {
|
||||
above = !options || options === "above";
|
||||
|
||||
} else {
|
||||
above = !options.position || options.position === "above";
|
||||
ignorePattern = options.ignorePattern;
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(options, "applyDefaultIgnorePatterns")) {
|
||||
applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
|
||||
} else {
|
||||
applyDefaultIgnorePatterns = options.applyDefaultPatterns !== false;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
|
||||
const fallThroughRegExp = /^\s*falls?\s?through/;
|
||||
const customIgnoreRegExp = new RegExp(ignorePattern);
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program() {
|
||||
const comments = sourceCode.getAllComments();
|
||||
|
||||
comments.filter(token => token.type === "Line").forEach(node => {
|
||||
if (applyDefaultIgnorePatterns && (defaultIgnoreRegExp.test(node.value) || fallThroughRegExp.test(node.value))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignorePattern && customIgnoreRegExp.test(node.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = sourceCode.getTokenBefore(node, { includeComments: true });
|
||||
const isOnSameLine = previous && previous.loc.end.line === node.loc.start.line;
|
||||
|
||||
if (above) {
|
||||
if (isOnSameLine) {
|
||||
context.report({
|
||||
node,
|
||||
message: "Expected comment to be above code."
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!isOnSameLine) {
|
||||
context.report({
|
||||
node,
|
||||
message: "Expected comment to be beside code."
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce a single linebreak style.
|
||||
* @author Erik Mueller
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce consistent linebreak style",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/linebreak-style"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["unix", "windows"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const EXPECTED_LF_MSG = "Expected linebreaks to be 'LF' but found 'CRLF'.",
|
||||
EXPECTED_CRLF_MSG = "Expected linebreaks to be 'CRLF' but found 'LF'.";
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Builds a fix function that replaces text at the specified range in the source text.
|
||||
* @param {int[]} range The range to replace
|
||||
* @param {string} text The text to insert.
|
||||
* @returns {Function} Fixer function
|
||||
* @private
|
||||
*/
|
||||
function createFix(range, text) {
|
||||
return function(fixer) {
|
||||
return fixer.replaceTextRange(range, text);
|
||||
};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program: function checkForlinebreakStyle(node) {
|
||||
const linebreakStyle = context.options[0] || "unix",
|
||||
expectedLF = linebreakStyle === "unix",
|
||||
expectedLFChars = expectedLF ? "\n" : "\r\n",
|
||||
source = sourceCode.getText(),
|
||||
pattern = astUtils.createGlobalLinebreakMatcher();
|
||||
let match;
|
||||
|
||||
let i = 0;
|
||||
|
||||
while ((match = pattern.exec(source)) !== null) {
|
||||
i++;
|
||||
if (match[0] === expectedLFChars) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = match.index;
|
||||
const range = [index, index + match[0].length];
|
||||
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
line: i,
|
||||
column: sourceCode.lines[i - 1].length
|
||||
},
|
||||
message: expectedLF ? EXPECTED_LF_MSG : EXPECTED_CRLF_MSG,
|
||||
fix: createFix(range, expectedLFChars)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* @fileoverview Enforces empty lines around comments.
|
||||
* @author Jamund Ferguson
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const lodash = require("lodash"),
|
||||
astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return an array with with any line numbers that are empty.
|
||||
* @param {Array} lines An array of each line of the file.
|
||||
* @returns {Array} An array of line numbers.
|
||||
*/
|
||||
function getEmptyLineNums(lines) {
|
||||
const emptyLines = lines.map((line, i) => ({
|
||||
code: line.trim(),
|
||||
num: i + 1
|
||||
})).filter(line => !line.code).map(line => line.num);
|
||||
|
||||
return emptyLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array with with any line numbers that contain comments.
|
||||
* @param {Array} comments An array of comment tokens.
|
||||
* @returns {Array} An array of line numbers.
|
||||
*/
|
||||
function getCommentLineNums(comments) {
|
||||
const lines = [];
|
||||
|
||||
comments.forEach(token => {
|
||||
const start = token.loc.start.line;
|
||||
const end = token.loc.end.line;
|
||||
|
||||
lines.push(start, end);
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "require empty lines around comments",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/lines-around-comment"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
beforeBlockComment: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterBlockComment: {
|
||||
type: "boolean"
|
||||
},
|
||||
beforeLineComment: {
|
||||
type: "boolean"
|
||||
},
|
||||
afterLineComment: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowBlockStart: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowBlockEnd: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowClassStart: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowClassEnd: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowObjectStart: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowObjectEnd: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowArrayStart: {
|
||||
type: "boolean"
|
||||
},
|
||||
allowArrayEnd: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignorePattern: {
|
||||
type: "string"
|
||||
},
|
||||
applyDefaultIgnorePatterns: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = context.options[0] ? Object.assign({}, context.options[0]) : {};
|
||||
const ignorePattern = options.ignorePattern;
|
||||
const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
|
||||
const customIgnoreRegExp = new RegExp(ignorePattern);
|
||||
const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
|
||||
|
||||
|
||||
options.beforeLineComment = options.beforeLineComment || false;
|
||||
options.afterLineComment = options.afterLineComment || false;
|
||||
options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
|
||||
options.afterBlockComment = options.afterBlockComment || false;
|
||||
options.allowBlockStart = options.allowBlockStart || false;
|
||||
options.allowBlockEnd = options.allowBlockEnd || false;
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
const lines = sourceCode.lines,
|
||||
numLines = lines.length + 1,
|
||||
comments = sourceCode.getAllComments(),
|
||||
commentLines = getCommentLineNums(comments),
|
||||
emptyLines = getEmptyLineNums(lines),
|
||||
commentAndEmptyLines = commentLines.concat(emptyLines);
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are on lines starting with or ending with code
|
||||
* @param {token} token The comment token to check.
|
||||
* @returns {boolean} True if the comment is not alone.
|
||||
*/
|
||||
function codeAroundComment(token) {
|
||||
let currentToken = token;
|
||||
|
||||
do {
|
||||
currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true });
|
||||
} while (currentToken && astUtils.isCommentToken(currentToken));
|
||||
|
||||
if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
currentToken = token;
|
||||
do {
|
||||
currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
|
||||
} while (currentToken && astUtils.isCommentToken(currentToken));
|
||||
|
||||
if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are inside a node type or not.
|
||||
* @param {ASTNode} parent The Comment parent node.
|
||||
* @param {string} nodeType The parent type to check against.
|
||||
* @returns {boolean} True if the comment is inside nodeType.
|
||||
*/
|
||||
function isParentNodeType(parent, nodeType) {
|
||||
return parent.type === nodeType ||
|
||||
(parent.body && parent.body.type === nodeType) ||
|
||||
(parent.consequent && parent.consequent.type === nodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent node that contains the given token.
|
||||
* @param {token} token The token to check.
|
||||
* @returns {ASTNode} The parent node that contains the given token.
|
||||
*/
|
||||
function getParentNodeOfToken(token) {
|
||||
return sourceCode.getNodeByRangeIndex(token.range[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the parent start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @param {string} nodeType The parent type to check against.
|
||||
* @returns {boolean} True if the comment is at parent start.
|
||||
*/
|
||||
function isCommentAtParentStart(token, nodeType) {
|
||||
const parent = getParentNodeOfToken(token);
|
||||
|
||||
return parent && isParentNodeType(parent, nodeType) &&
|
||||
token.loc.start.line - parent.loc.start.line === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the parent end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @param {string} nodeType The parent type to check against.
|
||||
* @returns {boolean} True if the comment is at parent end.
|
||||
*/
|
||||
function isCommentAtParentEnd(token, nodeType) {
|
||||
const parent = getParentNodeOfToken(token);
|
||||
|
||||
return parent && isParentNodeType(parent, nodeType) &&
|
||||
parent.loc.end.line - token.loc.end.line === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the block start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at block start.
|
||||
*/
|
||||
function isCommentAtBlockStart(token) {
|
||||
return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the block end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at block end.
|
||||
*/
|
||||
function isCommentAtBlockEnd(token) {
|
||||
return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the class start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at class start.
|
||||
*/
|
||||
function isCommentAtClassStart(token) {
|
||||
return isCommentAtParentStart(token, "ClassBody");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the class end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at class end.
|
||||
*/
|
||||
function isCommentAtClassEnd(token) {
|
||||
return isCommentAtParentEnd(token, "ClassBody");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the object start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at object start.
|
||||
*/
|
||||
function isCommentAtObjectStart(token) {
|
||||
return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the object end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at object end.
|
||||
*/
|
||||
function isCommentAtObjectEnd(token) {
|
||||
return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the array start or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at array start.
|
||||
*/
|
||||
function isCommentAtArrayStart(token) {
|
||||
return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not comments are at the array end or not.
|
||||
* @param {token} token The Comment token.
|
||||
* @returns {boolean} True if the comment is at array end.
|
||||
*/
|
||||
function isCommentAtArrayEnd(token) {
|
||||
return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a comment token has lines around it (ignores inline comments)
|
||||
* @param {token} token The Comment token.
|
||||
* @param {Object} opts Options to determine the newline.
|
||||
* @param {boolean} opts.after Should have a newline after this line.
|
||||
* @param {boolean} opts.before Should have a newline before this line.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkForEmptyLine(token, opts) {
|
||||
if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignorePattern && customIgnoreRegExp.test(token.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let after = opts.after,
|
||||
before = opts.before;
|
||||
|
||||
const prevLineNum = token.loc.start.line - 1,
|
||||
nextLineNum = token.loc.end.line + 1,
|
||||
commentIsNotAlone = codeAroundComment(token);
|
||||
|
||||
const blockStartAllowed = options.allowBlockStart &&
|
||||
isCommentAtBlockStart(token) &&
|
||||
!(options.allowClassStart === false &&
|
||||
isCommentAtClassStart(token)),
|
||||
blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)),
|
||||
classStartAllowed = options.allowClassStart && isCommentAtClassStart(token),
|
||||
classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token),
|
||||
objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
|
||||
objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
|
||||
arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
|
||||
arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
|
||||
|
||||
const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed;
|
||||
const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed;
|
||||
|
||||
// ignore top of the file and bottom of the file
|
||||
if (prevLineNum < 1) {
|
||||
before = false;
|
||||
}
|
||||
if (nextLineNum >= numLines) {
|
||||
after = false;
|
||||
}
|
||||
|
||||
// we ignore all inline comments
|
||||
if (commentIsNotAlone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true });
|
||||
const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
|
||||
|
||||
// check for newline before
|
||||
if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
|
||||
!(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
|
||||
const lineStart = token.range[0] - token.loc.start.column;
|
||||
const range = [lineStart, lineStart];
|
||||
|
||||
context.report({
|
||||
node: token,
|
||||
message: "Expected line before comment.",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBeforeRange(range, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// check for newline after
|
||||
if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
|
||||
!(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
|
||||
context.report({
|
||||
node: token,
|
||||
message: "Expected line after comment.",
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(token, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program() {
|
||||
comments.forEach(token => {
|
||||
if (token.type === "Line") {
|
||||
if (options.beforeLineComment || options.afterLineComment) {
|
||||
checkForEmptyLine(token, {
|
||||
after: options.afterLineComment,
|
||||
before: options.beforeLineComment
|
||||
});
|
||||
}
|
||||
} else if (token.type === "Block") {
|
||||
if (options.beforeBlockComment || options.afterBlockComment) {
|
||||
checkForEmptyLine(token, {
|
||||
after: options.afterBlockComment,
|
||||
before: options.beforeBlockComment
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* @fileoverview Require or disallow newlines around directives.
|
||||
* @author Kai Cataldo
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "require or disallow newlines around directives",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/lines-around-directive"
|
||||
},
|
||||
|
||||
schema: [{
|
||||
oneOf: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
before: {
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
after: {
|
||||
enum: ["always", "never"]
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
minProperties: 2
|
||||
}
|
||||
]
|
||||
}],
|
||||
|
||||
fixable: "whitespace",
|
||||
deprecated: true,
|
||||
replacedBy: ["padding-line-between-statements"]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const config = context.options[0] || "always";
|
||||
const expectLineBefore = typeof config === "string" ? config : config.before;
|
||||
const expectLineAfter = typeof config === "string" ? config : config.after;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if node is preceded by a blank newline.
|
||||
* @param {ASTNode} node Node to check.
|
||||
* @returns {boolean} Whether or not the passed in node is preceded by a blank newline.
|
||||
*/
|
||||
function hasNewlineBefore(node) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true });
|
||||
const tokenLineBefore = tokenBefore ? tokenBefore.loc.end.line : 0;
|
||||
|
||||
return node.loc.start.line - tokenLineBefore >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last token of a node that is on the same line as the rest of the node.
|
||||
* This will usually be the last token of the node, but it will be the second-to-last token if the node has a trailing
|
||||
* semicolon on a different line.
|
||||
* @param {ASTNode} node A directive node
|
||||
* @returns {Token} The last token of the node on the line
|
||||
*/
|
||||
function getLastTokenOnLine(node) {
|
||||
const lastToken = sourceCode.getLastToken(node);
|
||||
const secondToLastToken = sourceCode.getTokenBefore(lastToken);
|
||||
|
||||
return astUtils.isSemicolonToken(lastToken) && lastToken.loc.start.line > secondToLastToken.loc.end.line
|
||||
? secondToLastToken
|
||||
: lastToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if node is followed by a blank newline.
|
||||
* @param {ASTNode} node Node to check.
|
||||
* @returns {boolean} Whether or not the passed in node is followed by a blank newline.
|
||||
*/
|
||||
function hasNewlineAfter(node) {
|
||||
const lastToken = getLastTokenOnLine(node);
|
||||
const tokenAfter = sourceCode.getTokenAfter(lastToken, { includeComments: true });
|
||||
|
||||
return tokenAfter.loc.start.line - lastToken.loc.end.line >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report errors for newlines around directives.
|
||||
* @param {ASTNode} node Node to check.
|
||||
* @param {string} location Whether the error was found before or after the directive.
|
||||
* @param {boolean} expected Whether or not a newline was expected or unexpected.
|
||||
* @returns {void}
|
||||
*/
|
||||
function reportError(node, location, expected) {
|
||||
context.report({
|
||||
node,
|
||||
message: "{{expected}} newline {{location}} \"{{value}}\" directive.",
|
||||
data: {
|
||||
expected: expected ? "Expected" : "Unexpected",
|
||||
value: node.expression.value,
|
||||
location
|
||||
},
|
||||
fix(fixer) {
|
||||
const lastToken = getLastTokenOnLine(node);
|
||||
|
||||
if (expected) {
|
||||
return location === "before" ? fixer.insertTextBefore(node, "\n") : fixer.insertTextAfter(lastToken, "\n");
|
||||
}
|
||||
return fixer.removeRange(location === "before" ? [node.range[0] - 1, node.range[0]] : [lastToken.range[1], lastToken.range[1] + 1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check lines around directives in node
|
||||
* @param {ASTNode} node - node to check
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkDirectives(node) {
|
||||
const directives = astUtils.getDirectivePrologue(node);
|
||||
|
||||
if (!directives.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstDirective = directives[0];
|
||||
const leadingComments = sourceCode.getCommentsBefore(firstDirective);
|
||||
|
||||
/*
|
||||
* Only check before the first directive if it is preceded by a comment or if it is at the top of
|
||||
* the file and expectLineBefore is set to "never". This is to not force a newline at the top of
|
||||
* the file if there are no comments as well as for compatibility with padded-blocks.
|
||||
*/
|
||||
if (leadingComments.length) {
|
||||
if (expectLineBefore === "always" && !hasNewlineBefore(firstDirective)) {
|
||||
reportError(firstDirective, "before", true);
|
||||
}
|
||||
|
||||
if (expectLineBefore === "never" && hasNewlineBefore(firstDirective)) {
|
||||
reportError(firstDirective, "before", false);
|
||||
}
|
||||
} else if (
|
||||
node.type === "Program" &&
|
||||
expectLineBefore === "never" &&
|
||||
!leadingComments.length &&
|
||||
hasNewlineBefore(firstDirective)
|
||||
) {
|
||||
reportError(firstDirective, "before", false);
|
||||
}
|
||||
|
||||
const lastDirective = directives[directives.length - 1];
|
||||
const statements = node.type === "Program" ? node.body : node.body.body;
|
||||
|
||||
/*
|
||||
* Do not check after the last directive if the body only
|
||||
* contains a directive prologue and isn't followed by a comment to ensure
|
||||
* this rule behaves well with padded-blocks.
|
||||
*/
|
||||
if (lastDirective === statements[statements.length - 1] && !lastDirective.trailingComments) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectLineAfter === "always" && !hasNewlineAfter(lastDirective)) {
|
||||
reportError(lastDirective, "after", true);
|
||||
}
|
||||
|
||||
if (expectLineAfter === "never" && hasNewlineAfter(lastDirective)) {
|
||||
reportError(lastDirective, "after", false);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program: checkDirectives,
|
||||
FunctionDeclaration: checkDirectives,
|
||||
FunctionExpression: checkDirectives,
|
||||
ArrowFunctionExpression: checkDirectives
|
||||
};
|
||||
}
|
||||
};
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @fileoverview Rule to check empty newline between class members
|
||||
* @author 薛定谔的猫<hh_2013@foxmail.com>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "require or disallow an empty line between class members",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/lines-between-class-members"
|
||||
},
|
||||
|
||||
fixable: "whitespace",
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptAfterSingleLine: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
const options = [];
|
||||
|
||||
options[0] = context.options[0] || "always";
|
||||
options[1] = context.options[1] || { exceptAfterSingleLine: false };
|
||||
|
||||
const ALWAYS_MESSAGE = "Expected blank line between class members.";
|
||||
const NEVER_MESSAGE = "Unexpected blank line between class members.";
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Checks if there is padding between two tokens
|
||||
* @param {Token} first The first token
|
||||
* @param {Token} second The second token
|
||||
* @returns {boolean} True if there is at least a line between the tokens
|
||||
*/
|
||||
function isPaddingBetweenTokens(first, second) {
|
||||
const comments = sourceCode.getCommentsBefore(second);
|
||||
const len = comments.length;
|
||||
|
||||
// If there is no comments
|
||||
if (len === 0) {
|
||||
const linesBetweenFstAndSnd = second.loc.start.line - first.loc.end.line - 1;
|
||||
|
||||
return linesBetweenFstAndSnd >= 1;
|
||||
}
|
||||
|
||||
|
||||
// If there are comments
|
||||
let sumOfCommentLines = 0; // the numbers of lines of comments
|
||||
let prevCommentLineNum = -1; // line number of the end of the previous comment
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const commentLinesOfThisComment = comments[i].loc.end.line - comments[i].loc.start.line + 1;
|
||||
|
||||
sumOfCommentLines += commentLinesOfThisComment;
|
||||
|
||||
/*
|
||||
* If this comment and the previous comment are in the same line,
|
||||
* the count of comment lines is duplicated. So decrement sumOfCommentLines.
|
||||
*/
|
||||
if (prevCommentLineNum === comments[i].loc.start.line) {
|
||||
sumOfCommentLines -= 1;
|
||||
}
|
||||
|
||||
prevCommentLineNum = comments[i].loc.end.line;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the first block and the first comment are in the same line,
|
||||
* the count of comment lines is duplicated. So decrement sumOfCommentLines.
|
||||
*/
|
||||
if (first.loc.end.line === comments[0].loc.start.line) {
|
||||
sumOfCommentLines -= 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the last comment and the second block are in the same line,
|
||||
* the count of comment lines is duplicated. So decrement sumOfCommentLines.
|
||||
*/
|
||||
if (comments[len - 1].loc.end.line === second.loc.start.line) {
|
||||
sumOfCommentLines -= 1;
|
||||
}
|
||||
|
||||
const linesBetweenFstAndSnd = second.loc.start.line - first.loc.end.line - 1;
|
||||
|
||||
return linesBetweenFstAndSnd - sumOfCommentLines >= 1;
|
||||
}
|
||||
|
||||
return {
|
||||
ClassBody(node) {
|
||||
const body = node.body;
|
||||
|
||||
for (let i = 0; i < body.length - 1; i++) {
|
||||
const curFirst = sourceCode.getFirstToken(body[i]);
|
||||
const curLast = sourceCode.getLastToken(body[i]);
|
||||
const nextFirst = sourceCode.getFirstToken(body[i + 1]);
|
||||
const isPadded = isPaddingBetweenTokens(curLast, nextFirst);
|
||||
const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast);
|
||||
const skip = !isMulti && options[1].exceptAfterSingleLine;
|
||||
|
||||
|
||||
if ((options[0] === "always" && !skip && !isPadded) ||
|
||||
(options[0] === "never" && isPadded)) {
|
||||
context.report({
|
||||
node: body[i + 1],
|
||||
message: isPadded ? NEVER_MESSAGE : ALWAYS_MESSAGE,
|
||||
fix(fixer) {
|
||||
return isPadded
|
||||
? fixer.replaceTextRange([curLast.range[1], nextFirst.range[0]], "\n")
|
||||
: fixer.insertTextAfter(curLast, "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @fileoverview Enforce a maximum number of classes per file
|
||||
* @author James Garbutt <https://github.com/43081j>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce a maximum number of classes per file",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/max-classes-per-file"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 1
|
||||
}
|
||||
],
|
||||
|
||||
messages: {
|
||||
maximumExceeded: "Number of classes per file must not exceed {{ max }}"
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
|
||||
const maxClasses = context.options[0] || 1;
|
||||
|
||||
let classCount = 0;
|
||||
|
||||
return {
|
||||
Program() {
|
||||
classCount = 0;
|
||||
},
|
||||
"Program:exit"(node) {
|
||||
if (classCount > maxClasses) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "maximumExceeded",
|
||||
data: {
|
||||
max: maxClasses
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
"ClassDeclaration, ClassExpression"() {
|
||||
classCount++;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* @fileoverview A rule to set the maximum depth block can be nested in a function.
|
||||
* @author Ian Christian Myers
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce a maximum depth that blocks can be nested",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/max-depth"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
maximum: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const functionStack = [],
|
||||
option = context.options[0];
|
||||
let maxDepth = 4;
|
||||
|
||||
if (typeof option === "object" && Object.prototype.hasOwnProperty.call(option, "maximum") && typeof option.maximum === "number") {
|
||||
maxDepth = option.maximum;
|
||||
}
|
||||
if (typeof option === "object" && Object.prototype.hasOwnProperty.call(option, "max") && typeof option.max === "number") {
|
||||
maxDepth = option.max;
|
||||
}
|
||||
if (typeof option === "number") {
|
||||
maxDepth = option;
|
||||
}
|
||||
|
||||
/**
|
||||
* When parsing a new function, store it in our function stack
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function startFunction() {
|
||||
functionStack.push(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* When parsing is done then pop out the reference
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function endFunction() {
|
||||
functionStack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the block and Evaluate the node
|
||||
* @param {ASTNode} node node to evaluate
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function pushBlock(node) {
|
||||
const len = ++functionStack[functionStack.length - 1];
|
||||
|
||||
if (len > maxDepth) {
|
||||
context.report({ node, message: "Blocks are nested too deeply ({{depth}}).", data: { depth: len } });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the saved block
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function popBlock() {
|
||||
functionStack[functionStack.length - 1]--;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program: startFunction,
|
||||
FunctionDeclaration: startFunction,
|
||||
FunctionExpression: startFunction,
|
||||
ArrowFunctionExpression: startFunction,
|
||||
|
||||
IfStatement(node) {
|
||||
if (node.parent.type !== "IfStatement") {
|
||||
pushBlock(node);
|
||||
}
|
||||
},
|
||||
SwitchStatement: pushBlock,
|
||||
TryStatement: pushBlock,
|
||||
DoWhileStatement: pushBlock,
|
||||
WhileStatement: pushBlock,
|
||||
WithStatement: pushBlock,
|
||||
ForStatement: pushBlock,
|
||||
ForInStatement: pushBlock,
|
||||
ForOfStatement: pushBlock,
|
||||
|
||||
"IfStatement:exit": popBlock,
|
||||
"SwitchStatement:exit": popBlock,
|
||||
"TryStatement:exit": popBlock,
|
||||
"DoWhileStatement:exit": popBlock,
|
||||
"WhileStatement:exit": popBlock,
|
||||
"WithStatement:exit": popBlock,
|
||||
"ForStatement:exit": popBlock,
|
||||
"ForInStatement:exit": popBlock,
|
||||
"ForOfStatement:exit": popBlock,
|
||||
|
||||
"FunctionDeclaration:exit": endFunction,
|
||||
"FunctionExpression:exit": endFunction,
|
||||
"ArrowFunctionExpression:exit": endFunction,
|
||||
"Program:exit": endFunction
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* @fileoverview Rule to check for max length on a line.
|
||||
* @author Matt DuVall <http://www.mattduvall.com>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const OPTIONS_SCHEMA = {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
comments: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
tabWidth: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
ignorePattern: {
|
||||
type: "string"
|
||||
},
|
||||
ignoreComments: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignoreStrings: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignoreUrls: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignoreTemplateLiterals: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignoreRegExpLiterals: {
|
||||
type: "boolean"
|
||||
},
|
||||
ignoreTrailingComments: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
const OPTIONS_OR_INTEGER_SCHEMA = {
|
||||
anyOf: [
|
||||
OPTIONS_SCHEMA,
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "layout",
|
||||
|
||||
docs: {
|
||||
description: "enforce a maximum line length",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/max-len"
|
||||
},
|
||||
|
||||
schema: [
|
||||
OPTIONS_OR_INTEGER_SCHEMA,
|
||||
OPTIONS_OR_INTEGER_SCHEMA,
|
||||
OPTIONS_SCHEMA
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
/*
|
||||
* Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however:
|
||||
* - They're matching an entire string that we know is a URI
|
||||
* - We're matching part of a string where we think there *might* be a URL
|
||||
* - We're only concerned about URLs, as picking out any URI would cause
|
||||
* too many false positives
|
||||
* - We don't care about matching the entire URL, any small segment is fine
|
||||
*/
|
||||
const URL_REGEXP = /[^:/?#]:\/\/[^?#]/;
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Computes the length of a line that may contain tabs. The width of each
|
||||
* tab will be the number of spaces to the next tab stop.
|
||||
* @param {string} line The line.
|
||||
* @param {int} tabWidth The width of each tab stop in spaces.
|
||||
* @returns {int} The computed line length.
|
||||
* @private
|
||||
*/
|
||||
function computeLineLength(line, tabWidth) {
|
||||
let extraCharacterCount = 0;
|
||||
|
||||
line.replace(/\t/g, (match, offset) => {
|
||||
const totalOffset = offset + extraCharacterCount,
|
||||
previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0,
|
||||
spaceCount = tabWidth - previousTabStopOffset;
|
||||
|
||||
extraCharacterCount += spaceCount - 1; // -1 for the replaced tab
|
||||
});
|
||||
return Array.from(line).length + extraCharacterCount;
|
||||
}
|
||||
|
||||
// The options object must be the last option specified…
|
||||
const lastOption = context.options[context.options.length - 1];
|
||||
const options = typeof lastOption === "object" ? Object.create(lastOption) : {};
|
||||
|
||||
// …but max code length…
|
||||
if (typeof context.options[0] === "number") {
|
||||
options.code = context.options[0];
|
||||
}
|
||||
|
||||
// …and tabWidth can be optionally specified directly as integers.
|
||||
if (typeof context.options[1] === "number") {
|
||||
options.tabWidth = context.options[1];
|
||||
}
|
||||
|
||||
const maxLength = options.code || 80,
|
||||
tabWidth = options.tabWidth || 4,
|
||||
ignoreComments = options.ignoreComments || false,
|
||||
ignoreStrings = options.ignoreStrings || false,
|
||||
ignoreTemplateLiterals = options.ignoreTemplateLiterals || false,
|
||||
ignoreRegExpLiterals = options.ignoreRegExpLiterals || false,
|
||||
ignoreTrailingComments = options.ignoreTrailingComments || options.ignoreComments || false,
|
||||
ignoreUrls = options.ignoreUrls || false,
|
||||
maxCommentLength = options.comments;
|
||||
let ignorePattern = options.ignorePattern || null;
|
||||
|
||||
if (ignorePattern) {
|
||||
ignorePattern = new RegExp(ignorePattern);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Tells if a given comment is trailing: it starts on the current line and
|
||||
* extends to or past the end of the current line.
|
||||
* @param {string} line The source line we want to check for a trailing comment on
|
||||
* @param {number} lineNumber The one-indexed line number for line
|
||||
* @param {ASTNode} comment The comment to inspect
|
||||
* @returns {boolean} If the comment is trailing on the given line
|
||||
*/
|
||||
function isTrailingComment(line, lineNumber, comment) {
|
||||
return comment &&
|
||||
(comment.loc.start.line === lineNumber && lineNumber <= comment.loc.end.line) &&
|
||||
(comment.loc.end.line > lineNumber || comment.loc.end.column === line.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if a comment encompasses the entire line.
|
||||
* @param {string} line The source line with a trailing comment
|
||||
* @param {number} lineNumber The one-indexed line number this is on
|
||||
* @param {ASTNode} comment The comment to remove
|
||||
* @returns {boolean} If the comment covers the entire line
|
||||
*/
|
||||
function isFullLineComment(line, lineNumber, comment) {
|
||||
const start = comment.loc.start,
|
||||
end = comment.loc.end,
|
||||
isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim();
|
||||
|
||||
return comment &&
|
||||
(start.line < lineNumber || (start.line === lineNumber && isFirstTokenOnLine)) &&
|
||||
(end.line > lineNumber || (end.line === lineNumber && end.column === line.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the line after the comment and any remaining trailing whitespace is
|
||||
* stripped.
|
||||
* @param {string} line The source line with a trailing comment
|
||||
* @param {ASTNode} comment The comment to remove
|
||||
* @returns {string} Line without comment and trailing whitepace
|
||||
*/
|
||||
function stripTrailingComment(line, comment) {
|
||||
|
||||
// loc.column is zero-indexed
|
||||
return line.slice(0, comment.loc.start.column).replace(/\s+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that an array exists at [key] on `object`, and add `value` to it.
|
||||
*
|
||||
* @param {Object} object the object to mutate
|
||||
* @param {string} key the object's key
|
||||
* @param {*} value the value to add
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function ensureArrayAndPush(object, key, value) {
|
||||
if (!Array.isArray(object[key])) {
|
||||
object[key] = [];
|
||||
}
|
||||
object[key].push(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array containing all strings (" or ') in the source code.
|
||||
*
|
||||
* @returns {ASTNode[]} An array of string nodes.
|
||||
*/
|
||||
function getAllStrings() {
|
||||
return sourceCode.ast.tokens.filter(token => (token.type === "String" ||
|
||||
(token.type === "JSXText" && sourceCode.getNodeByRangeIndex(token.range[0] - 1).type === "JSXAttribute")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array containing all template literals in the source code.
|
||||
*
|
||||
* @returns {ASTNode[]} An array of template literal nodes.
|
||||
*/
|
||||
function getAllTemplateLiterals() {
|
||||
return sourceCode.ast.tokens.filter(token => token.type === "Template");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves an array containing all RegExp literals in the source code.
|
||||
*
|
||||
* @returns {ASTNode[]} An array of RegExp literal nodes.
|
||||
*/
|
||||
function getAllRegExpLiterals() {
|
||||
return sourceCode.ast.tokens.filter(token => token.type === "RegularExpression");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A reducer to group an AST node by line number, both start and end.
|
||||
*
|
||||
* @param {Object} acc the accumulator
|
||||
* @param {ASTNode} node the AST node in question
|
||||
* @returns {Object} the modified accumulator
|
||||
* @private
|
||||
*/
|
||||
function groupByLineNumber(acc, node) {
|
||||
for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) {
|
||||
ensureArrayAndPush(acc, i, node);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the program for max length
|
||||
* @param {ASTNode} node Node to examine
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function checkProgramForMaxLength(node) {
|
||||
|
||||
// split (honors line-ending)
|
||||
const lines = sourceCode.lines,
|
||||
|
||||
// list of comments to ignore
|
||||
comments = ignoreComments || maxCommentLength || ignoreTrailingComments ? sourceCode.getAllComments() : [];
|
||||
|
||||
// we iterate over comments in parallel with the lines
|
||||
let commentsIndex = 0;
|
||||
|
||||
const strings = getAllStrings();
|
||||
const stringsByLine = strings.reduce(groupByLineNumber, {});
|
||||
|
||||
const templateLiterals = getAllTemplateLiterals();
|
||||
const templateLiteralsByLine = templateLiterals.reduce(groupByLineNumber, {});
|
||||
|
||||
const regExpLiterals = getAllRegExpLiterals();
|
||||
const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {});
|
||||
|
||||
lines.forEach((line, i) => {
|
||||
|
||||
// i is zero-indexed, line numbers are one-indexed
|
||||
const lineNumber = i + 1;
|
||||
|
||||
/*
|
||||
* if we're checking comment length; we need to know whether this
|
||||
* line is a comment
|
||||
*/
|
||||
let lineIsComment = false;
|
||||
let textToMeasure;
|
||||
|
||||
/*
|
||||
* We can short-circuit the comment checks if we're already out of
|
||||
* comments to check.
|
||||
*/
|
||||
if (commentsIndex < comments.length) {
|
||||
let comment = null;
|
||||
|
||||
// iterate over comments until we find one past the current line
|
||||
do {
|
||||
comment = comments[++commentsIndex];
|
||||
} while (comment && comment.loc.start.line <= lineNumber);
|
||||
|
||||
// and step back by one
|
||||
comment = comments[--commentsIndex];
|
||||
|
||||
if (isFullLineComment(line, lineNumber, comment)) {
|
||||
lineIsComment = true;
|
||||
textToMeasure = line;
|
||||
} else if (ignoreTrailingComments && isTrailingComment(line, lineNumber, comment)) {
|
||||
textToMeasure = stripTrailingComment(line, comment);
|
||||
} else {
|
||||
textToMeasure = line;
|
||||
}
|
||||
} else {
|
||||
textToMeasure = line;
|
||||
}
|
||||
if (ignorePattern && ignorePattern.test(textToMeasure) ||
|
||||
ignoreUrls && URL_REGEXP.test(textToMeasure) ||
|
||||
ignoreStrings && stringsByLine[lineNumber] ||
|
||||
ignoreTemplateLiterals && templateLiteralsByLine[lineNumber] ||
|
||||
ignoreRegExpLiterals && regExpLiteralsByLine[lineNumber]
|
||||
) {
|
||||
|
||||
// ignore this line
|
||||
return;
|
||||
}
|
||||
|
||||
const lineLength = computeLineLength(textToMeasure, tabWidth);
|
||||
const commentLengthApplies = lineIsComment && maxCommentLength;
|
||||
|
||||
if (lineIsComment && ignoreComments) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (commentLengthApplies) {
|
||||
if (lineLength > maxCommentLength) {
|
||||
context.report({
|
||||
node,
|
||||
loc: { line: lineNumber, column: 0 },
|
||||
message: "Line {{lineNumber}} exceeds the maximum comment line length of {{maxCommentLength}}.",
|
||||
data: {
|
||||
lineNumber: i + 1,
|
||||
maxCommentLength
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (lineLength > maxLength) {
|
||||
context.report({
|
||||
node,
|
||||
loc: { line: lineNumber, column: 0 },
|
||||
message: "Line {{lineNumber}} exceeds the maximum line length of {{maxLength}}.",
|
||||
data: {
|
||||
lineNumber: i + 1,
|
||||
maxLength
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
Program: checkProgramForMaxLength
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* @fileoverview A rule to set the maximum number of line of code in a function.
|
||||
* @author Pete Ward <peteward44@gmail.com>
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("../util/ast-utils");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Constants
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const OPTIONS_SCHEMA = {
|
||||
type: "object",
|
||||
properties: {
|
||||
max: {
|
||||
type: "integer",
|
||||
minimum: 0
|
||||
},
|
||||
skipComments: {
|
||||
type: "boolean"
|
||||
},
|
||||
skipBlankLines: {
|
||||
type: "boolean"
|
||||
},
|
||||
IIFEs: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
|
||||
const OPTIONS_OR_INTEGER_SCHEMA = {
|
||||
oneOf: [
|
||||
OPTIONS_SCHEMA,
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a list of comment nodes, return a map with numeric keys (source code line numbers) and comment token values.
|
||||
* @param {Array} comments An array of comment nodes.
|
||||
* @returns {Map.<string,Node>} A map with numeric keys (source code line numbers) and comment token values.
|
||||
*/
|
||||
function getCommentLineNumbers(comments) {
|
||||
const map = new Map();
|
||||
|
||||
if (!comments) {
|
||||
return map;
|
||||
}
|
||||
comments.forEach(comment => {
|
||||
for (let i = comment.loc.start.line; i <= comment.loc.end.line; i++) {
|
||||
map.set(i, comment);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "enforce a maximum number of line of code in a function",
|
||||
category: "Stylistic Issues",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/max-lines-per-function"
|
||||
},
|
||||
|
||||
schema: [
|
||||
OPTIONS_OR_INTEGER_SCHEMA
|
||||
]
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const lines = sourceCode.lines;
|
||||
|
||||
const option = context.options[0];
|
||||
let maxLines = 50;
|
||||
let skipComments = false;
|
||||
let skipBlankLines = false;
|
||||
let IIFEs = false;
|
||||
|
||||
if (typeof option === "object") {
|
||||
if (typeof option.max === "number") {
|
||||
maxLines = option.max;
|
||||
}
|
||||
if (typeof option.skipComments === "boolean") {
|
||||
skipComments = option.skipComments;
|
||||
}
|
||||
if (typeof option.skipBlankLines === "boolean") {
|
||||
skipBlankLines = option.skipBlankLines;
|
||||
}
|
||||
if (typeof option.IIFEs === "boolean") {
|
||||
IIFEs = option.IIFEs;
|
||||
}
|
||||
} else if (typeof option === "number") {
|
||||
maxLines = option;
|
||||
}
|
||||
|
||||
const commentLineNumbers = getCommentLineNumbers(sourceCode.getAllComments());
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Tells if a comment encompasses the entire line.
|
||||
* @param {string} line The source line with a trailing comment
|
||||
* @param {number} lineNumber The one-indexed line number this is on
|
||||
* @param {ASTNode} comment The comment to remove
|
||||
* @returns {boolean} If the comment covers the entire line
|
||||
*/
|
||||
function isFullLineComment(line, lineNumber, comment) {
|
||||
const start = comment.loc.start,
|
||||
end = comment.loc.end,
|
||||
isFirstTokenOnLine = start.line === lineNumber && !line.slice(0, start.column).trim(),
|
||||
isLastTokenOnLine = end.line === lineNumber && !line.slice(end.column).trim();
|
||||
|
||||
return comment &&
|
||||
(start.line < lineNumber || isFirstTokenOnLine) &&
|
||||
(end.line > lineNumber || isLastTokenOnLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies is a node is a FunctionExpression which is part of an IIFE
|
||||
* @param {ASTNode} node Node to test
|
||||
* @returns {boolean} True if it's an IIFE
|
||||
*/
|
||||
function isIIFE(node) {
|
||||
return node.type === "FunctionExpression" && node.parent && node.parent.type === "CallExpression" && node.parent.callee === node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies is a node is a FunctionExpression which is embedded within a MethodDefinition or Property
|
||||
* @param {ASTNode} node Node to test
|
||||
* @returns {boolean} True if it's a FunctionExpression embedded within a MethodDefinition or Property
|
||||
*/
|
||||
function isEmbedded(node) {
|
||||
if (!node.parent) {
|
||||
return false;
|
||||
}
|
||||
if (node !== node.parent.value) {
|
||||
return false;
|
||||
}
|
||||
if (node.parent.type === "MethodDefinition") {
|
||||
return true;
|
||||
}
|
||||
if (node.parent.type === "Property") {
|
||||
return node.parent.method === true || node.parent.kind === "get" || node.parent.kind === "set";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the lines in the function
|
||||
* @param {ASTNode} funcNode Function AST node
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function processFunction(funcNode) {
|
||||
const node = isEmbedded(funcNode) ? funcNode.parent : funcNode;
|
||||
|
||||
if (!IIFEs && isIIFE(node)) {
|
||||
return;
|
||||
}
|
||||
let lineCount = 0;
|
||||
|
||||
for (let i = node.loc.start.line - 1; i < node.loc.end.line; ++i) {
|
||||
const line = lines[i];
|
||||
|
||||
if (skipComments) {
|
||||
if (commentLineNumbers.has(i + 1) && isFullLineComment(line, i + 1, commentLineNumbers.get(i + 1))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (skipBlankLines) {
|
||||
if (line.match(/^\s*$/)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
if (lineCount > maxLines) {
|
||||
const name = astUtils.getFunctionNameWithKind(funcNode);
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}.",
|
||||
data: { name, lineCount, maxLines }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
FunctionDeclaration: processFunction,
|
||||
FunctionExpression: processFunction,
|
||||
ArrowFunctionExpression: processFunction
|
||||
};
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user