diff options
author | Rich Trott <rtrott@gmail.com> | 2016-07-07 15:44:32 -0700 |
---|---|---|
committer | Evan Lucas <evanlucas@me.com> | 2016-07-15 08:09:42 -0500 |
commit | df697c486e7af5b50efca4a21105bb137cbeddbd (patch) | |
tree | f5ecf30000db4281c3538df7b9372ede231d9f8a /tools/eslint/lib | |
parent | a81ff702cc57f9d7912cdbbcee826af9cda35915 (diff) | |
download | node-new-df697c486e7af5b50efca4a21105bb137cbeddbd.tar.gz |
tools: update ESLint, fix unused vars bug
Update ESLint to 3.0.0. This includes an enhancement to `no-unused-vars`
such that it finds a few instances in our code base that it did not find
previously (fixed in previous commits readying this for landing).
PR-URL: https://github.com/nodejs/node/pull/7601
Reviewed-By: Michaƫl Zasso <mic.besace@gmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
Diffstat (limited to 'tools/eslint/lib')
105 files changed, 3023 insertions, 791 deletions
diff --git a/tools/eslint/lib/ast-utils.js b/tools/eslint/lib/ast-utils.js index e008beeb2a..c8d6dcb491 100644 --- a/tools/eslint/lib/ast-utils.js +++ b/tools/eslint/lib/ast-utils.js @@ -57,7 +57,7 @@ function isModifyingReference(reference, index, references) { function isES5Constructor(node) { return ( node.id && - node.id.name[0] === node.id.name[0].toLocaleUpperCase() + node.id.name[0] !== node.id.name[0].toLocaleLowerCase() ); } @@ -176,14 +176,14 @@ function hasJSDocThisTag(node, sourceCode) { /** * Determines if a node is surrounded by parentheses. - * @param {RuleContext} context The context object passed to the rule + * @param {SourceCode} sourceCode The ESLint source code object * @param {ASTNode} node The node to be checked. * @returns {boolean} True if the node is parenthesised. * @private */ -function isParenthesised(context, node) { - var previousToken = context.getTokenBefore(node), - nextToken = context.getTokenAfter(node); +function isParenthesised(sourceCode, node) { + var previousToken = sourceCode.getTokenBefore(node), + nextToken = sourceCode.getTokenAfter(node); return Boolean(previousToken && nextToken) && previousToken.value === "(" && previousToken.range[1] <= node.range[0] && @@ -460,5 +460,96 @@ module.exports = { /* istanbul ignore next */ return true; + }, + + /** + * Get the precedence level based on the node type + * @param {ASTNode} node node to evaluate + * @returns {int} precedence level + * @private + */ + getPrecedence: function(node) { + switch (node.type) { + case "SequenceExpression": + return 0; + + case "AssignmentExpression": + case "ArrowFunctionExpression": + case "YieldExpression": + return 1; + + case "ConditionalExpression": + return 3; + + case "LogicalExpression": + switch (node.operator) { + case "||": + return 4; + case "&&": + return 5; + + // no default + } + + /* falls through */ + + case "BinaryExpression": + + switch (node.operator) { + case "|": + return 6; + case "^": + return 7; + case "&": + return 8; + case "==": + case "!=": + case "===": + case "!==": + return 9; + case "<": + case "<=": + case ">": + case ">=": + case "in": + case "instanceof": + return 10; + case "<<": + case ">>": + case ">>>": + return 11; + case "+": + case "-": + return 12; + case "*": + case "/": + case "%": + return 13; + + // no default + } + + /* falls through */ + + case "UnaryExpression": + return 14; + + case "UpdateExpression": + return 15; + + case "CallExpression": + + // IIFE is allowed to have parens in any position (#655) + if (node.callee.type === "FunctionExpression") { + return -1; + } + return 16; + + case "NewExpression": + return 17; + + // no default + } + return 18; } }; diff --git a/tools/eslint/lib/cli-engine.js b/tools/eslint/lib/cli-engine.js index 7fa1a794ea..410fd7f367 100644 --- a/tools/eslint/lib/cli-engine.js +++ b/tools/eslint/lib/cli-engine.js @@ -20,7 +20,6 @@ var fs = require("fs"), lodash = require("lodash"), debug = require("debug"), - isAbsolute = require("path-is-absolute"), rules = require("./rules"), eslint = require("./eslint"), @@ -68,6 +67,8 @@ var fs = require("fs"), * @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 or errors for the result. + * @property {number} warningCount Number or warnings for the result. */ //------------------------------------------------------------------------------ @@ -132,23 +133,19 @@ function multipassFix(text, config, options) { fixedResult, fixed = false, passNumber = 0, - lastMessageCount, MAX_PASSES = 10; /** * This loop continues until one of the following is true: * * 1. No more fixes have been applied. - * 2. There are no more linting errors reported. - * 3. The number of linting errors is no different between two passes. - * 4. Ten passes have been made. + * 2. Ten passes have been made. * * That means anytime a fix is successfully applied, there will be another pass. * Essentially, guaranteeing a minimum of two passes. */ do { passNumber++; - lastMessageCount = messages.length; debug("Linting code for " + options.filename + " (pass " + passNumber + ")"); messages = eslint.verify(text, config, options); @@ -156,6 +153,12 @@ function multipassFix(text, config, options) { debug("Generating fixed text for " + options.filename + " (pass " + passNumber + ")"); fixedResult = SourceCodeFixer.applyFixes(eslint.getSourceCode(), messages); + // stop if there are any syntax errors. + // 'fixedResult.output' is a empty string. + if (messages.length === 1 && messages[0].fatal) { + break; + } + // keep track if any fixes were ever applied - important for return value fixed = fixed || fixedResult.fixed; @@ -163,8 +166,7 @@ function multipassFix(text, config, options) { text = fixedResult.output; } while ( - fixedResult.fixed && fixedResult.messages.length > 0 && - fixedResult.messages.length !== lastMessageCount && + fixedResult.fixed && passNumber < MAX_PASSES ); @@ -300,18 +302,34 @@ function processFile(filename, configHelper, options) { /** * Returns result with warning by ignore settings - * @param {string} filePath File path of checked code - * @returns {Result} Result with single warning + * @param {string} filePath - File path of checked code + * @param {string} baseDir - Absolute path of base directory + * @returns {Result} Result with single warning * @private */ -function createIgnoreResult(filePath) { +function createIgnoreResult(filePath, baseDir) { + var message; + var isHidden = /^\./.test(path.basename(filePath)); + var isInNodeModules = baseDir && /^node_modules/.test(path.relative(baseDir, filePath)); + var isInBowerComponents = baseDir && /^bower_components/.test(path.relative(baseDir, filePath)); + + 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: "File ignored because of a matching ignore pattern. Use --no-ignore to override." + message: message } ], errorCount: 0, @@ -438,10 +456,6 @@ function CLIEngine(options) { */ this._fileCache = fileEntryCache.create(cacheFile); - if (!this.options.cache) { - this._fileCache.destroy(); - } - // load in additional rules if (this.options.rulePaths) { var cwd = this.options.cwd; @@ -489,7 +503,8 @@ CLIEngine.getFormatter = function(format) { try { return require(formatterPath); } catch (ex) { - return null; + ex.message = "There was a problem loading formatter: " + formatterPath + "\nError: " + ex.message; + throw ex; } } else { @@ -511,7 +526,9 @@ CLIEngine.getErrorResults = function(results) { if (filteredMessages.length > 0) { filtered.push({ filePath: result.filePath, - messages: filteredMessages + messages: filteredMessages, + errorCount: filteredMessages.length, + warningCount: 0 }); } }); @@ -563,7 +580,6 @@ CLIEngine.prototype = { */ executeOnFiles: function(patterns) { var results = [], - processed = {}, options = this.options, fileCache = this._fileCache, configHelper = new Config(options), @@ -612,7 +628,7 @@ CLIEngine.prototype = { var hashOfConfig; if (warnIgnored) { - results.push(createIgnoreResult(filename)); + results.push(createIgnoreResult(filename, options.cwd)); return; } @@ -634,13 +650,6 @@ CLIEngine.prototype = { debug("Skipping file since hasn't changed: " + filename); /* - * Adding the filename to the processed hashmap - * so the reporting is not affected (showing a warning about .eslintignore being used - * when it is not really used) - */ - processed[filename] = true; - - /* * Add the the cached results (always will be 0 error and * 0 warnings). We should not cache results for files that * failed, in order to guarantee that next execution will @@ -651,12 +660,12 @@ CLIEngine.prototype = { // move to the next file return; } + } else { + fileCache.destroy(); } debug("Processing " + filename); - processed[filename] = true; - var res = processFile(filename, configHelper, options); if (options.cache) { @@ -718,9 +727,10 @@ CLIEngine.prototype = { * 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: function(text, filename) { + executeOnText: function(text, filename, warnIgnored) { var results = [], stats, @@ -729,12 +739,11 @@ CLIEngine.prototype = { ignoredPaths = new IgnoredPaths(options); // resolve filename based on options.cwd (for reporting, ignoredPaths also resolves) - if (filename && !isAbsolute(filename)) { + if (filename && !path.isAbsolute(filename)) { filename = path.resolve(options.cwd, filename); } - if (filename && (options.ignore !== false) && ignoredPaths.contains(filename)) { - - results.push(createIgnoreResult(filename)); + if (filename && warnIgnored && ignoredPaths.contains(filename)) { + results.push(createIgnoreResult(filename, options.cwd)); } else { results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig)); } @@ -770,12 +779,8 @@ CLIEngine.prototype = { var ignoredPaths; var resolvedPath = path.resolve(this.options.cwd, filePath); - if (this.options.ignore) { - ignoredPaths = new IgnoredPaths(this.options); - return ignoredPaths.contains(resolvedPath); - } - - return false; + ignoredPaths = new IgnoredPaths(this.options); + return ignoredPaths.contains(resolvedPath); }, getFormatter: CLIEngine.getFormatter diff --git a/tools/eslint/lib/cli.js b/tools/eslint/lib/cli.js index adb70d8ce1..887c3c7671 100644 --- a/tools/eslint/lib/cli.js +++ b/tools/eslint/lib/cli.js @@ -74,9 +74,10 @@ function printResults(engine, results, format, outputFile) { output, filePath; - formatter = engine.getFormatter(format); - if (!formatter) { - log.error("Could not find formatter '%s'.", format); + try { + formatter = engine.getFormatter(format); + } catch (e) { + log.error(e.message); return false; } @@ -177,7 +178,7 @@ var cli = { return 0; } - report = text ? engine.executeOnText(text, currentOptions.stdinFilename) : engine.executeOnFiles(files); + report = text ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files); if (currentOptions.fix) { debug("Fix mode enabled - applying fixes"); CLIEngine.outputFixes(report); diff --git a/tools/eslint/lib/config.js b/tools/eslint/lib/config.js index b9c7061953..a485774d77 100644 --- a/tools/eslint/lib/config.js +++ b/tools/eslint/lib/config.js @@ -71,7 +71,7 @@ function loadConfig(configToLoad) { /** * Get personal config object from ~/.eslintrc. - * @returns {Object} the personal config object (empty object if there is no personal config) + * @returns {Object} the personal config object (null if there is no personal config) * @private */ function getPersonalConfig() { @@ -87,7 +87,16 @@ function getPersonalConfig() { } } - return config || {}; + return config || null; +} + +/** + * Determine if 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. + */ +function hasRules(options) { + return options.rules && Object.keys(options.rules).length > 0; } /** @@ -105,7 +114,8 @@ function getLocalConfig(thisConfig, directory) { localConfigFiles = thisConfig.findLocalConfigFiles(directory), numFiles = localConfigFiles.length, rootPath, - projectConfigPath = ConfigFile.getFilenameForDirectory(thisConfig.options.cwd); + projectConfigPath = ConfigFile.getFilenameForDirectory(thisConfig.options.cwd), + personalConfig; for (i = 0; i < numFiles; i++) { @@ -140,8 +150,34 @@ function getLocalConfig(thisConfig, directory) { config = ConfigOps.merge(localConfig, config); } - // Use the personal config file if there are no other local config files found. - return found || thisConfig.useSpecificConfig ? config : ConfigOps.merge(config, getPersonalConfig()); + if (!found && !thisConfig.useSpecificConfig) { + + /* + * - Is there a personal config in the user's home directory? If so, + * merge that with the passed-in config. + * - Otherwise, if no rules were manually passed in, throw and error. + * - Note: This function is not called if useEslintrc is false. + */ + personalConfig = getPersonalConfig(); + + if (personalConfig) { + config = ConfigOps.merge(config, personalConfig); + } else if (!hasRules(thisConfig.options)) { + + // No config file, no manual configuration, and no rules, so error. + var noConfigError = new Error("No ESLint configuration found."); + + noConfigError.messageTemplate = "no-config-found"; + noConfigError.messageData = { + directory: directory, + filesExamined: localConfigFiles + }; + + throw noConfigError; + } + } + + return config; } //------------------------------------------------------------------------------ @@ -231,7 +267,7 @@ Config.prototype.getConfig = function(filePath) { } // Step 2: Create a copy of the baseConfig - config = ConfigOps.merge({parser: this.parser, parserOptions: this.parserOptions}, this.baseConfig); + config = ConfigOps.merge({}, this.baseConfig); // Step 3: Merge in the user-specified configuration from .eslintrc and package.json config = ConfigOps.merge(config, userConfig); @@ -256,6 +292,20 @@ Config.prototype.getConfig = function(filePath) { // Step 7: Merge in command line globals config = ConfigOps.merge(config, { globals: this.globals }); + // Only override parser 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 + }); + } + + if (this.parserOptions) { + config = ConfigOps.merge(config, { + parserOptions: this.parserOptions + }); + } + // Step 8: Merge in command line plugins if (this.options.plugins) { debug("Merging command line plugins"); diff --git a/tools/eslint/lib/config/config-file.js b/tools/eslint/lib/config/config-file.js index 51a81c7331..e2996e3eb9 100644 --- a/tools/eslint/lib/config/config-file.js +++ b/tools/eslint/lib/config/config-file.js @@ -20,9 +20,9 @@ var debug = require("debug"), pathUtil = require("../util/path-util"), ModuleResolver = require("../util/module-resolver"), pathIsInside = require("path-is-inside"), + stripBom = require("strip-bom"), stripComments = require("strip-json-comments"), stringify = require("json-stable-stringify"), - isAbsolutePath = require("path-is-absolute"), defaultOptions = require("../../conf/eslint.json"), requireUncached = require("require-uncached"); @@ -68,7 +68,7 @@ debug = debug("eslint:config-file"); * @private */ function readFile(filePath) { - return fs.readFileSync(filePath, "utf8"); + return stripBom(fs.readFileSync(filePath, "utf8")); } /** @@ -80,7 +80,7 @@ function readFile(filePath) { * @private */ function isFilePath(filePath) { - return isAbsolutePath(filePath) || !/\w|@/.test(filePath.charAt(0)); + return path.isAbsolute(filePath) || !/\w|@/.test(filePath.charAt(0)); } /** @@ -369,14 +369,20 @@ function applyExtends(config, filePath, relativeTo) { * this lets us use the eslint.json file as the recommended rules */ parentPath = path.resolve(__dirname, "../../conf/eslint.json"); + } else if (parentPath === "eslint:all") { + + /* + * Add an explicit substitution for eslint:all to conf/eslint-all.js + */ + parentPath = path.resolve(__dirname, "../../conf/eslint-all.js"); } 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. */ - parentPath = (!isAbsolutePath(parentPath) ? - path.join(path.dirname(filePath), parentPath) : + parentPath = (!path.isAbsolute(parentPath) ? + path.join(relativeTo || path.dirname(filePath), parentPath) : parentPath ); } @@ -489,7 +495,6 @@ function resolve(filePath, relativeTo) { function load(filePath, applyEnvironments, relativeTo) { var resolvedPath = resolve(filePath, relativeTo), dirname = path.dirname(resolvedPath.filePath), - basedir = getBaseDir(dirname), lookupPath = getLookupPath(dirname), config = loadConfigFile(resolvedPath); @@ -508,7 +513,7 @@ function load(filePath, applyEnvironments, relativeTo) { // include full path of parser if present if (config.parser) { if (isFilePath(config.parser)) { - config.parser = path.resolve(basedir || "", config.parser); + config.parser = path.resolve(dirname || "", config.parser); } else { config.parser = resolver.resolve(config.parser, lookupPath); } @@ -522,7 +527,7 @@ function load(filePath, applyEnvironments, relativeTo) { * a "parent". Load the referenced file and merge the configuration recursively. */ if (config.extends) { - config = applyExtends(config, filePath, basedir); + config = applyExtends(config, filePath, dirname); } if (config.env && applyEnvironments) { diff --git a/tools/eslint/lib/config/config-initializer.js b/tools/eslint/lib/config/config-initializer.js index 3d0e78fefe..91d2454a8a 100644 --- a/tools/eslint/lib/config/config-initializer.js +++ b/tools/eslint/lib/config/config-initializer.js @@ -46,7 +46,6 @@ function writeFile(config, format) { extname = ".json"; } - ConfigFile.write(config, "./.eslintrc" + extname); log.info("Successfully created .eslintrc" + extname + " file in " + process.cwd()); @@ -318,7 +317,8 @@ function promptUser(callback) { message: "Which style guide do you want to follow?", choices: [{name: "Google", value: "google"}, {name: "AirBnB", value: "airbnb"}, {name: "Standard", value: "standard"}], when: function(answers) { - return answers.source === "guide"; + answers.packageJsonExists = npmUtil.checkPackageJson(); + return answers.source === "guide" && answers.packageJsonExists; } }, { @@ -342,13 +342,18 @@ function promptUser(callback) { default: "JavaScript", choices: ["JavaScript", "YAML", "JSON"], when: function(answers) { - return (answers.source === "guide" || answers.source === "auto"); + return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto"); } } ], function(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; + } + try { config = getConfigForStyleGuide(earlyAnswers.styleguide); writeFile(config, earlyAnswers.format); diff --git a/tools/eslint/lib/config/config-ops.js b/tools/eslint/lib/config/config-ops.js index 727d3afa04..d62169502b 100644 --- a/tools/eslint/lib/config/config-ops.js +++ b/tools/eslint/lib/config/config-ops.js @@ -23,7 +23,8 @@ var RULE_SEVERITY_STRINGS = ["off", "warn", "error"], RULE_SEVERITY = RULE_SEVERITY_STRINGS.reduce(function(map, value, index) { map[value] = index; return map; - }, {}); + }, {}), + VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"]; //------------------------------------------------------------------------------ // Public Interface @@ -248,6 +249,30 @@ module.exports = { } return (typeof severity === "number" && severity === 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: function(ruleConfig) { + var 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: function(config) { + return Object.keys(config).every(function(ruleId) { + return this.isValidSeverity(config[ruleId]); + }, this); + } }; diff --git a/tools/eslint/lib/config/environments.js b/tools/eslint/lib/config/environments.js index e7711836e5..8daef864e3 100644 --- a/tools/eslint/lib/config/environments.js +++ b/tools/eslint/lib/config/environments.js @@ -8,15 +8,12 @@ // Requirements //------------------------------------------------------------------------------ -var debug = require("debug"), - envs = require("../../conf/environments"); +var envs = require("../../conf/environments"); //------------------------------------------------------------------------------ // Private //------------------------------------------------------------------------------ -debug = debug("eslint:enviroments"); - var environments = Object.create(null); /** diff --git a/tools/eslint/lib/eslint.js b/tools/eslint/lib/eslint.js index 3a52bb3af1..69ad96e820 100644 --- a/tools/eslint/lib/eslint.js +++ b/tools/eslint/lib/eslint.js @@ -9,25 +9,25 @@ // Requirements //------------------------------------------------------------------------------ -var lodash = require("lodash"), - Traverser = require("./util/traverser"), +var assert = require("assert"), + EventEmitter = require("events").EventEmitter, escope = require("escope"), - Environments = require("./config/environments"), + levn = require("levn"), + lodash = require("lodash"), blankScriptAST = require("../conf/blank-script.json"), - rules = require("./rules"), - RuleContext = require("./rule-context"), - timing = require("./timing"), - SourceCode = require("./util/source-code"), - NodeEventGenerator = require("./util/node-event-generator"), - CommentEventGenerator = require("./util/comment-event-generator"), - EventEmitter = require("events").EventEmitter, + DEFAULT_PARSER = require("../conf/eslint.json").parser, + replacements = require("../conf/replacements.json"), + CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), ConfigOps = require("./config/config-ops"), validator = require("./config/config-validator"), - replacements = require("../conf/replacements.json"), - assert = require("assert"), - CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"); - -var DEFAULT_PARSER = require("../conf/eslint.json").parser; + Environments = require("./config/environments"), + CommentEventGenerator = require("./util/comment-event-generator"), + NodeEventGenerator = require("./util/node-event-generator"), + SourceCode = require("./util/source-code"), + Traverser = require("./util/traverser"), + RuleContext = require("./rule-context"), + rules = require("./rules"), + timing = require("./timing"); //------------------------------------------------------------------------------ // Helpers @@ -80,6 +80,25 @@ function parseBooleanConfig(string, comment) { function parseJsonConfig(string, location, messages) { var items = {}; + // Parses a JSON-like comment by the same way as parsing CLI option. + try { + items = levn.parse("Object", string) || {}; + + // Some tests say that it should ignore invalid comments such as `/*eslint no-alert:abc*/`. + // Also, commaless notations have invalid severity: + // "no-alert: 2 no-console: 2" --> {"no-alert": "2 no-console: 2"} + // Should ignore that case as well. + if (ConfigOps.isEverySeverityValid(items)) { + return items; + } + } catch (ex) { + + // ignore to parse the string by a fallback. + } + + // Optionator cannot parse commaless notations. + // But we are supporting that. So this is a fallback for that. + items = {}; string = string.replace(/([a-zA-Z0-9\-\/]+):/g, "\"$1\":").replace(/(\]|[0-9])\s+(?=")/, "$1,"); try { items = JSON.parse("{" + string + "}"); @@ -962,8 +981,14 @@ module.exports = (function() { source: sourceCode.lines[location.line - 1] || "" }; - // ensure there's range and text properties as well as metadata switch, otherwise it's not a valid fix - if (fix && Array.isArray(fix.range) && (typeof fix.text === "string") && (!meta || meta.fixable)) { + // ensure there's range and text properties, otherwise it's not a valid fix + if (fix && Array.isArray(fix.range) && (typeof fix.text === "string")) { + + // If rule uses fix, has metadata, but has no metadata.fixable, we should throw + if (meta && !meta.fixable) { + throw new Error("Fixable rules should export a `meta.fixable` property."); + } + problem.fix = fix; } diff --git a/tools/eslint/lib/file-finder.js b/tools/eslint/lib/file-finder.js index 4dbb7544a3..4559405700 100644 --- a/tools/eslint/lib/file-finder.js +++ b/tools/eslint/lib/file-finder.js @@ -70,64 +70,6 @@ function normalizeDirectoryEntries(entries, directory, supportedConfigs) { } /** - * Find one instance of a specified file name in directory or in a parent directory. - * Cache the results. - * Does not check if a matching directory entry is a file, and intentionally - * only searches for the first file name in this.fileNames. - * Is currently used by lib/ignored_paths.js to find an .eslintignore file. - * @param {string} directory The directory to start the search from. - * @returns {string} Path of the file found, or an empty string if not found. - */ -FileFinder.prototype.findInDirectoryOrParents = function(directory) { - var cache = this.cache, - child, - dirs, - filePath, - i, - names, - searched; - - if (!directory) { - directory = this.cwd; - } - - if (cache.hasOwnProperty(directory)) { - return cache[directory]; - } - - dirs = []; - searched = 0; - names = this.fileNames; - - (function() { - while (directory !== child) { - dirs[searched++] = directory; - var filesMap = normalizeDirectoryEntries(getDirectoryEntries(directory), directory, names); - - if (Object.keys(filesMap).length) { - for (var k = 0; k < names.length; k++) { - if (filesMap[names[k]]) { - filePath = filesMap[names[k]]; - return; - } - } - } - - child = directory; - - // Assign parent directory to directory. - directory = path.dirname(directory); - } - }()); - - for (i = 0; i < searched; i++) { - cache[dirs[i]] = filePath; - } - - return filePath || String(); -}; - -/** * Find all instances of files with the specified file names, in directory and * parent directories. Cache the results. * Does not check if a matching directory entry is a file. @@ -146,7 +88,9 @@ FileFinder.prototype.findAllInDirectoryAndParents = function(directory) { j, searched; - if (!directory) { + if (directory) { + directory = path.resolve(this.cwd, directory); + } else { directory = this.cwd; } diff --git a/tools/eslint/lib/ignored-paths.js b/tools/eslint/lib/ignored-paths.js index c6a710a9b5..9a7739ebee 100644 --- a/tools/eslint/lib/ignored-paths.js +++ b/tools/eslint/lib/ignored-paths.js @@ -24,9 +24,9 @@ debug = debug("eslint:ignored-paths"); //------------------------------------------------------------------------------ var ESLINT_IGNORE_FILENAME = ".eslintignore"; -var DEFAULT_IGNORE_PATTERNS = [ - "/node_modules/*", - "/bower_components/*" +var DEFAULT_IGNORE_DIRS = [ + "node_modules/", + "bower_components/" ]; var DEFAULT_OPTIONS = { dotfiles: false, @@ -97,7 +97,9 @@ function IgnoredPaths(options) { return ig.add(fs.readFileSync(filepath, "utf8")); } - this.defaultPatterns = DEFAULT_IGNORE_PATTERNS.concat(options.patterns || []); + this.defaultPatterns = DEFAULT_IGNORE_DIRS.map(function(dir) { + return "/" + dir + "*"; + }).concat(options.patterns || []); this.baseDir = options.cwd; this.ig = { @@ -124,11 +126,6 @@ function IgnoredPaths(options) { if (options.ignore !== false) { var ignorePath; - if (options.ignorePattern) { - addPattern(this.ig.custom, options.ignorePattern); - addPattern(this.ig.default, options.ignorePattern); - } - if (options.ignorePath) { debug("Using specific ignore file"); @@ -159,6 +156,10 @@ function IgnoredPaths(options) { addIgnoreFile(this.ig.default, ignorePath); } + if (options.ignorePattern) { + addPattern(this.ig.custom, options.ignorePattern); + addPattern(this.ig.default, options.ignorePattern); + } } this.options = options; @@ -189,4 +190,36 @@ IgnoredPaths.prototype.contains = function(filepath, category) { }; +/** + * Returns a list of dir patterns for glob to ignore + * @returns {string[]} list of glob ignore patterns + */ +IgnoredPaths.prototype.getIgnoredFoldersGlobPatterns = function() { + var dirs = DEFAULT_IGNORE_DIRS; + + if (this.options.ignore) { + + /* eslint-disable no-underscore-dangle */ + + var patterns = this.ig.custom._rules.filter(function(rule) { + return rule.negative; + }).map(function(rule) { + return rule.origin; + }); + + /* eslint-enable no-underscore-dangle */ + + dirs = dirs.filter(function(dir) { + return patterns.every(function(p) { + return (p.indexOf("!" + dir) !== 0 && p.indexOf("!/" + dir) !== 0); + }); + }); + } + + + return dirs.map(function(dir) { + return dir + "**"; + }); +}; + module.exports = IgnoredPaths; diff --git a/tools/eslint/lib/options.js b/tools/eslint/lib/options.js index 7d61458581..eb58a62333 100644 --- a/tools/eslint/lib/options.js +++ b/tools/eslint/lib/options.js @@ -57,7 +57,6 @@ module.exports = optionator({ { option: "parser", type: "String", - default: "espree", description: "Specify the parser to be used" }, { @@ -115,7 +114,7 @@ module.exports = optionator({ option: "ignore", type: "Boolean", default: "true", - description: "Disable use of .eslintignore" + description: "Disable use of ignore files and patterns" }, { option: "ignore-pattern", diff --git a/tools/eslint/lib/rule-context.js b/tools/eslint/lib/rule-context.js index 88e68abd73..49b4dfc77d 100644 --- a/tools/eslint/lib/rule-context.js +++ b/tools/eslint/lib/rule-context.js @@ -15,18 +15,21 @@ var RuleFixer = require("./util/rule-fixer"); //------------------------------------------------------------------------------ var PASSTHROUGHS = [ - "getAllComments", "getAncestors", - "getComments", "getDeclaredVariables", "getFilename", + "getScope", + "markVariableAsUsed", + + // DEPRECATED + "getAllComments", + "getComments", "getFirstToken", "getFirstTokens", "getJSDocComment", "getLastToken", "getLastTokens", "getNodeByRangeIndex", - "getScope", "getSource", "getSourceLines", "getTokenAfter", @@ -35,8 +38,7 @@ var PASSTHROUGHS = [ "getTokens", "getTokensAfter", "getTokensBefore", - "getTokensBetween", - "markVariableAsUsed" + "getTokensBetween" ]; //------------------------------------------------------------------------------ diff --git a/tools/eslint/lib/rules/accessor-pairs.js b/tools/eslint/lib/rules/accessor-pairs.js index 1b91ef2715..3ed9f0dc0c 100644 --- a/tools/eslint/lib/rules/accessor-pairs.js +++ b/tools/eslint/lib/rules/accessor-pairs.js @@ -73,7 +73,7 @@ function isPropertyDescriptor(node) { module.exports = { meta: { docs: { - description: "Enforces getter/setter pairs in objects", + description: "enforce getter and setter pairs in objects", category: "Best Practices", recommended: false }, diff --git a/tools/eslint/lib/rules/array-bracket-spacing.js b/tools/eslint/lib/rules/array-bracket-spacing.js index 379ed0fa59..09598031b4 100644 --- a/tools/eslint/lib/rules/array-bracket-spacing.js +++ b/tools/eslint/lib/rules/array-bracket-spacing.js @@ -13,7 +13,7 @@ var astUtils = require("../ast-utils"); module.exports = { meta: { docs: { - description: "Enforce spacing inside array brackets", + description: "enforce consistent spacing inside array brackets", category: "Stylistic Issues", recommended: false }, @@ -77,7 +77,7 @@ module.exports = { loc: token.loc.start, message: "There should be no space after '" + token.value + "'", fix: function(fixer) { - var nextToken = context.getSourceCode().getTokenAfter(token); + var nextToken = sourceCode.getTokenAfter(token); return fixer.removeRange([token.range[1], nextToken.range[0]]); } @@ -96,7 +96,7 @@ module.exports = { loc: token.loc.start, message: "There should be no space before '" + token.value + "'", fix: function(fixer) { - var previousToken = context.getSourceCode().getTokenBefore(token); + var previousToken = sourceCode.getTokenBefore(token); return fixer.removeRange([previousToken.range[1], token.range[0]]); } @@ -165,10 +165,10 @@ module.exports = { return; } - var first = context.getFirstToken(node), - second = context.getFirstToken(node, 1), - penultimate = context.getLastToken(node, 1), - last = context.getLastToken(node), + var first = sourceCode.getFirstToken(node), + second = sourceCode.getFirstToken(node, 1), + penultimate = sourceCode.getLastToken(node, 1), + last = sourceCode.getLastToken(node), firstElement = node.elements[0], lastElement = node.elements[node.elements.length - 1]; diff --git a/tools/eslint/lib/rules/arrow-body-style.js b/tools/eslint/lib/rules/arrow-body-style.js index 79fde90f80..13486fa74b 100644 --- a/tools/eslint/lib/rules/arrow-body-style.js +++ b/tools/eslint/lib/rules/arrow-body-style.js @@ -16,16 +16,45 @@ module.exports = { recommended: false }, - schema: [ - { - enum: ["always", "as-needed"] - } - ] + 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 + } + ] + } }, create: function(context) { - var always = context.options[0] === "always"; - var asNeeded = !context.options[0] || context.options[0] === "as-needed"; + var options = context.options; + var always = options[0] === "always"; + var asNeeded = !options[0] || options[0] === "as-needed"; + var never = options[0] === "never"; + var requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral; /** * Determines whether a arrow function body needs braces @@ -36,21 +65,34 @@ module.exports = { var arrowBody = node.body; if (arrowBody.type === "BlockStatement") { - var blockBody = arrowBody.body; - - if (blockBody.length !== 1) { - return; - } - - if (asNeeded && blockBody[0].type === "ReturnStatement") { + if (never) { context.report({ node: node, loc: arrowBody.loc.start, message: "Unexpected block statement surrounding arrow body." }); + } else { + var blockBody = arrowBody.body; + + if (blockBody.length !== 1) { + return; + } + + if (asNeeded && requireReturnForObjectLiteral && blockBody[0].type === "ReturnStatement" && + blockBody[0].argument.type === "ObjectExpression") { + return; + } + + if (asNeeded && blockBody[0].type === "ReturnStatement") { + context.report({ + node: node, + loc: arrowBody.loc.start, + message: "Unexpected block statement surrounding arrow body." + }); + } } } else { - if (always) { + if (always || (asNeeded && requireReturnForObjectLiteral && arrowBody.type === "ObjectExpression")) { context.report({ node: node, loc: arrowBody.loc.start, diff --git a/tools/eslint/lib/rules/arrow-parens.js b/tools/eslint/lib/rules/arrow-parens.js index 78ce045017..86b972e800 100644 --- a/tools/eslint/lib/rules/arrow-parens.js +++ b/tools/eslint/lib/rules/arrow-parens.js @@ -16,6 +16,8 @@ module.exports = { recommended: false }, + fixable: "code", + schema: [ { enum: ["always", "as-needed"] @@ -28,28 +30,48 @@ module.exports = { var asNeededMessage = "Unexpected parentheses around single function argument"; var asNeeded = context.options[0] === "as-needed"; + var sourceCode = context.getSourceCode(); + /** * Determines whether a arrow function argument end with `)` * @param {ASTNode} node The arrow function node. * @returns {void} */ function parens(node) { - var token = context.getFirstToken(node); + var token = sourceCode.getFirstToken(node); // as-needed: x => x if (asNeeded && node.params.length === 1 && node.params[0].type === "Identifier") { if (token.type === "Punctuator" && token.value === "(") { - context.report(node, asNeededMessage); + context.report({ + node: node, + message: asNeededMessage, + fix: function(fixer) { + var paramToken = context.getTokenAfter(token); + var closingParenToken = context.getTokenAfter(paramToken); + + return fixer.replaceTextRange([ + token.range[0], + closingParenToken.range[1] + ], paramToken.value); + } + }); } return; } if (token.type === "Identifier") { - var after = context.getTokenAfter(token); + var after = sourceCode.getTokenAfter(token); // (x) => x if (after.value !== ")") { - context.report(node, message); + context.report({ + node: node, + message: message, + fix: function(fixer) { + return fixer.replaceText(token, "(" + token.value + ")"); + } + }); } } } diff --git a/tools/eslint/lib/rules/arrow-spacing.js b/tools/eslint/lib/rules/arrow-spacing.js index 82cec87ed2..3af5ae1f84 100644 --- a/tools/eslint/lib/rules/arrow-spacing.js +++ b/tools/eslint/lib/rules/arrow-spacing.js @@ -43,20 +43,22 @@ module.exports = { rule.before = option.before !== false; rule.after = option.after !== false; + var 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) { - var t = context.getFirstToken(node); + var t = sourceCode.getFirstToken(node); var before; while (t.type !== "Punctuator" || t.value !== "=>") { before = t; - t = context.getTokenAfter(t); + t = sourceCode.getTokenAfter(t); } - var after = context.getTokenAfter(t); + var after = sourceCode.getTokenAfter(t); return { before: before, arrow: t, after: after }; } diff --git a/tools/eslint/lib/rules/block-spacing.js b/tools/eslint/lib/rules/block-spacing.js index b6dc6e701d..54ae83d117 100644 --- a/tools/eslint/lib/rules/block-spacing.js +++ b/tools/eslint/lib/rules/block-spacing.js @@ -39,11 +39,11 @@ module.exports = { function getOpenBrace(node) { if (node.type === "SwitchStatement") { if (node.cases.length > 0) { - return context.getTokenBefore(node.cases[0]); + return sourceCode.getTokenBefore(node.cases[0]); } - return context.getLastToken(node, 1); + return sourceCode.getLastToken(node, 1); } - return context.getFirstToken(node); + return sourceCode.getFirstToken(node); } /** @@ -73,7 +73,7 @@ module.exports = { // Gets braces and the first/last token of content. var openBrace = getOpenBrace(node); - var closeBrace = context.getLastToken(node); + var closeBrace = sourceCode.getLastToken(node); var firstToken = sourceCode.getTokenOrCommentAfter(openBrace); var lastToken = sourceCode.getTokenOrCommentBefore(closeBrace); diff --git a/tools/eslint/lib/rules/callback-return.js b/tools/eslint/lib/rules/callback-return.js index a995da3a98..1d70d0c637 100644 --- a/tools/eslint/lib/rules/callback-return.js +++ b/tools/eslint/lib/rules/callback-return.js @@ -24,7 +24,8 @@ module.exports = { create: function(context) { - var callbacks = context.options[0] || ["callback", "cb", "next"]; + var callbacks = context.options[0] || ["callback", "cb", "next"], + sourceCode = context.getSourceCode(); //-------------------------------------------------------------------------- // Helpers @@ -47,12 +48,33 @@ module.exports = { } /** + * 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; + } else 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 node.callee.type === "Identifier" && callbacks.indexOf(node.callee.name) > -1; + return containsOnlyIdentifiers(node.callee) && callbacks.indexOf(sourceCode.getText(node.callee)) > -1; } /** @@ -90,7 +112,7 @@ module.exports = { return { CallExpression: function(node) { - // if we"re not a callback we can return + // if we're not a callback we can return if (!isCallback(node)) { return; } diff --git a/tools/eslint/lib/rules/comma-dangle.js b/tools/eslint/lib/rules/comma-dangle.js index 7c2451b608..d2478cacfb 100644 --- a/tools/eslint/lib/rules/comma-dangle.js +++ b/tools/eslint/lib/rules/comma-dangle.js @@ -31,8 +31,8 @@ module.exports = { meta: { docs: { description: "require or disallow trailing commas", - category: "Possible Errors", - recommended: true + category: "Stylistic Issues", + recommended: false }, fixable: "code", @@ -143,13 +143,13 @@ module.exports = { } var sourceCode = context.getSourceCode(), - trailingToken; - - // last item can be surrounded by parentheses for object and array literals - if (node.type === "ObjectExpression" || node.type === "ArrayExpression") { - trailingToken = sourceCode.getTokenBefore(sourceCode.getLastToken(node)); - } else { + penultimateToken = lastItem, trailingToken = sourceCode.getTokenAfter(lastItem); + + // Skip close parentheses. + while (trailingToken.value === ")") { + penultimateToken = trailingToken; + trailingToken = sourceCode.getTokenAfter(trailingToken); } if (trailingToken.value !== ",") { @@ -158,7 +158,7 @@ module.exports = { loc: lastItem.loc.end, message: MISSING_MESSAGE, fix: function(fixer) { - return fixer.insertTextAfter(lastItem, ","); + return fixer.insertTextAfter(penultimateToken, ","); } }); } diff --git a/tools/eslint/lib/rules/comma-spacing.js b/tools/eslint/lib/rules/comma-spacing.js index 2a4ec1f417..22fb8b235f 100644 --- a/tools/eslint/lib/rules/comma-spacing.js +++ b/tools/eslint/lib/rules/comma-spacing.js @@ -136,19 +136,19 @@ module.exports = { * @returns {void} */ function addNullElementsToIgnoreList(node) { - var previousToken = context.getFirstToken(node); + var previousToken = sourceCode.getFirstToken(node); node.elements.forEach(function(element) { var token; if (element === null) { - token = context.getTokenAfter(previousToken); + token = sourceCode.getTokenAfter(previousToken); if (isComma(token)) { commaTokensToIgnore.push(token); } } else { - token = context.getTokenAfter(element); + token = sourceCode.getTokenAfter(element); } previousToken = token; diff --git a/tools/eslint/lib/rules/comma-style.js b/tools/eslint/lib/rules/comma-style.js index 9c7d266d3d..173df90c33 100644 --- a/tools/eslint/lib/rules/comma-style.js +++ b/tools/eslint/lib/rules/comma-style.js @@ -39,9 +39,9 @@ module.exports = { }, create: function(context) { - var style = context.options[0] || "last", - exceptions = {}; + exceptions = {}, + sourceCode = context.getSourceCode(); if (context.options.length === 2 && context.options[1].hasOwnProperty("exceptions")) { exceptions = context.options[1].exceptions; @@ -115,12 +115,18 @@ module.exports = { if (items.length > 1 || arrayLiteral) { // seed as opening [ - previousItemToken = context.getFirstToken(node); + previousItemToken = sourceCode.getFirstToken(node); items.forEach(function(item) { - var commaToken = item ? context.getTokenBefore(item) : previousItemToken, - currentItemToken = item ? context.getFirstToken(item) : context.getTokenAfter(commaToken), - reportItem = item || currentItemToken; + var commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken, + currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken), + reportItem = item || currentItemToken, + tokenBeforeComma = sourceCode.getTokenBefore(commaToken); + + // Check if previous token is wrapped in parentheses + if (tokenBeforeComma && tokenBeforeComma.value === ")") { + previousItemToken = tokenBeforeComma; + } /* * This works by comparing three token locations: @@ -141,7 +147,7 @@ module.exports = { currentItemToken, reportItem); } - previousItemToken = item ? context.getLastToken(item) : previousItemToken; + previousItemToken = item ? sourceCode.getLastToken(item) : previousItemToken; }); /* @@ -152,12 +158,12 @@ module.exports = { */ if (arrayLiteral) { - var lastToken = context.getLastToken(node), - nextToLastToken = context.getTokenBefore(lastToken); + var lastToken = sourceCode.getLastToken(node), + nextToLastToken = sourceCode.getTokenBefore(lastToken); if (isComma(nextToLastToken)) { validateCommaItemSpacing( - context.getTokenBefore(nextToLastToken), + sourceCode.getTokenBefore(nextToLastToken), nextToLastToken, lastToken, lastToken diff --git a/tools/eslint/lib/rules/computed-property-spacing.js b/tools/eslint/lib/rules/computed-property-spacing.js index 1ae674e90d..89f0cc87b1 100644 --- a/tools/eslint/lib/rules/computed-property-spacing.js +++ b/tools/eslint/lib/rules/computed-property-spacing.js @@ -119,10 +119,10 @@ module.exports = { var property = node[propertyName]; - var before = context.getTokenBefore(property), - first = context.getFirstToken(property), - last = context.getLastToken(property), - after = context.getTokenAfter(property); + var before = sourceCode.getTokenBefore(property), + first = sourceCode.getFirstToken(property), + last = sourceCode.getLastToken(property), + after = sourceCode.getTokenAfter(property); if (astUtils.isTokenOnSameLine(before, first)) { if (propertyNameMustBeSpaced) { diff --git a/tools/eslint/lib/rules/consistent-return.js b/tools/eslint/lib/rules/consistent-return.js index 0e9a8c8b0f..10f1d41cf4 100644 --- a/tools/eslint/lib/rules/consistent-return.js +++ b/tools/eslint/lib/rules/consistent-return.js @@ -15,6 +15,16 @@ var astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ /** + * 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. @@ -35,10 +45,20 @@ module.exports = { recommended: false }, - schema: [] + schema: [{ + type: "object", + properties: { + treatUndefinedAsUnspecified: { + type: "boolean" + } + }, + additionalProperties: false + }] }, create: function(context) { + var options = context.options[0] || {}; + var treatUndefinedAsUnspecified = options.treatUndefinedAsUnspecified === true; var funcInfo = null; /** @@ -115,7 +135,12 @@ module.exports = { // Reports a given return statement if it's inconsistent. ReturnStatement: function(node) { - var hasReturnValue = Boolean(node.argument); + var argument = node.argument; + var hasReturnValue = Boolean(argument); + + if (treatUndefinedAsUnspecified && hasReturnValue) { + hasReturnValue = !isIdentifier(argument, "undefined") && argument.operator !== "void"; + } if (!funcInfo.hasReturn) { funcInfo.hasReturn = true; diff --git a/tools/eslint/lib/rules/curly.js b/tools/eslint/lib/rules/curly.js index 0bc5fdb3de..257366fabe 100644 --- a/tools/eslint/lib/rules/curly.js +++ b/tools/eslint/lib/rules/curly.js @@ -58,6 +58,8 @@ module.exports = { var multiOrNest = (context.options[0] === "multi-or-nest"); var consistent = (context.options[1] === "consistent"); + var sourceCode = context.getSourceCode(); + //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- @@ -69,8 +71,8 @@ module.exports = { * @private */ function isCollapsedOneLiner(node) { - var before = context.getTokenBefore(node), - last = context.getLastToken(node); + var before = sourceCode.getTokenBefore(node), + last = sourceCode.getLastToken(node); return before.loc.start.line === last.loc.end.line; } @@ -82,8 +84,8 @@ module.exports = { * @private */ function isOneLiner(node) { - var first = context.getFirstToken(node), - last = context.getLastToken(node); + var first = sourceCode.getFirstToken(node), + last = sourceCode.getLastToken(node); return first.loc.start.line === last.loc.end.line; } @@ -94,7 +96,6 @@ module.exports = { * @returns {Token} The `else` keyword token. */ function getElseKeyword(node) { - var sourceCode = context.getSourceCode(); var token = sourceCode.getTokenAfter(node.consequent); while (token.type !== "Keyword" || token.value !== "else") { diff --git a/tools/eslint/lib/rules/default-case.js b/tools/eslint/lib/rules/default-case.js index a4f3eef3cc..ae70a59284 100644 --- a/tools/eslint/lib/rules/default-case.js +++ b/tools/eslint/lib/rules/default-case.js @@ -13,7 +13,7 @@ var DEFAULT_COMMENT_PATTERN = /^no default$/; module.exports = { meta: { docs: { - description: "require `default` cases in <code>switch</code> statements", + description: "require `default` cases in `switch` statements", category: "Best Practices", recommended: false }, @@ -35,6 +35,8 @@ module.exports = { new RegExp(options.commentPattern) : DEFAULT_COMMENT_PATTERN; + var sourceCode = context.getSourceCode(); + //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- @@ -76,7 +78,7 @@ module.exports = { var lastCase = last(node.cases); - comments = context.getComments(lastCase).trailing; + comments = sourceCode.getComments(lastCase).trailing; if (comments.length) { comment = last(comments); diff --git a/tools/eslint/lib/rules/dot-location.js b/tools/eslint/lib/rules/dot-location.js index 0e1c257d39..2b29e0f49b 100644 --- a/tools/eslint/lib/rules/dot-location.js +++ b/tools/eslint/lib/rules/dot-location.js @@ -28,11 +28,12 @@ module.exports = { create: function(context) { - var config = context.options[0], - onObject; + var config = context.options[0]; // default to onObject if no preference is passed - onObject = config === "object" || !config; + var onObject = config === "object" || !config; + + var sourceCode = context.getSourceCode(); /** * Reports if the dot between object and property is on the correct loccation. @@ -42,7 +43,7 @@ module.exports = { * @returns {void} */ function checkDotLocation(obj, prop, node) { - var dot = context.getTokenBefore(prop); + var dot = sourceCode.getTokenBefore(prop); if (dot.type === "Punctuator" && dot.value === ".") { if (onObject) { diff --git a/tools/eslint/lib/rules/eol-last.js b/tools/eslint/lib/rules/eol-last.js index 1bd7c2897a..60b070f1ab 100644 --- a/tools/eslint/lib/rules/eol-last.js +++ b/tools/eslint/lib/rules/eol-last.js @@ -35,8 +35,8 @@ module.exports = { Program: function checkBadEOF(node) { - // Get the whole source code, not for node only. - var src = context.getSource(), + var sourceCode = context.getSourceCode(), + src = sourceCode.getText(), location = {column: 1}, linebreakStyle = context.options[0] || "unix", linebreak = linebreakStyle === "unix" ? "\n" : "\r\n"; diff --git a/tools/eslint/lib/rules/eqeqeq.js b/tools/eslint/lib/rules/eqeqeq.js index f1d1d15429..441f5b751c 100644 --- a/tools/eslint/lib/rules/eqeqeq.js +++ b/tools/eslint/lib/rules/eqeqeq.js @@ -19,12 +19,13 @@ module.exports = { schema: [ { - enum: ["smart", "allow-null"] + enum: ["always", "smart", "allow-null"] } ] }, create: function(context) { + var sourceCode = context.getSourceCode(); /** * Checks if an expression is a typeof expression @@ -75,7 +76,7 @@ module.exports = { * @private */ function getOperatorLocation(node) { - var opToken = context.getTokenAfter(node.left); + var opToken = sourceCode.getTokenAfter(node.left); return {line: opToken.loc.start.line, column: opToken.loc.start.column}; } diff --git a/tools/eslint/lib/rules/func-names.js b/tools/eslint/lib/rules/func-names.js index 51a1ffe046..44b989b2c4 100644 --- a/tools/eslint/lib/rules/func-names.js +++ b/tools/eslint/lib/rules/func-names.js @@ -12,15 +12,20 @@ module.exports = { meta: { docs: { - description: "enforce named `function` expressions", + description: "require or disallow named `function` expressions", category: "Stylistic Issues", recommended: false }, - schema: [] + schema: [ + { + enum: ["always", "never"] + } + ] }, create: function(context) { + var never = context.options[0] === "never"; /** * Determines whether the current FunctionExpression node is a get, set, or @@ -44,8 +49,14 @@ module.exports = { var name = node.id && node.id.name; - if (!name && !isObjectOrClassMethod()) { - context.report(node, "Missing function expression name."); + if (never) { + if (name) { + context.report(node, "Unexpected function expression name."); + } + } else { + if (!name && !isObjectOrClassMethod()) { + context.report(node, "Missing function expression name."); + } } } }; diff --git a/tools/eslint/lib/rules/generator-star-spacing.js b/tools/eslint/lib/rules/generator-star-spacing.js index f05f9f4201..0cab2be50e 100644 --- a/tools/eslint/lib/rules/generator-star-spacing.js +++ b/tools/eslint/lib/rules/generator-star-spacing.js @@ -52,6 +52,26 @@ module.exports = { return option; }(context.options[0])); + var sourceCode = context.getSourceCode(); + + /** + * Gets `*` token from a given node. + * + * @param {ASTNode} node - A node to get `*` token. This is one of + * FunctionDeclaration, FunctionExpression, Property, and + * MethodDefinition. + * @returns {Token} `*` token. + */ + function getStarToken(node) { + var token = sourceCode.getFirstToken(node); + + while (token.value !== "*") { + token = sourceCode.getTokenAfter(token); + } + + return token; + } + /** * Checks the spacing between two tokens before or after the star token. * @param {string} side Either "before" or "after". @@ -98,18 +118,18 @@ module.exports = { } if (node.parent.method || node.parent.type === "MethodDefinition") { - starToken = context.getTokenBefore(node, 1); + starToken = getStarToken(node.parent); } else { - starToken = context.getFirstToken(node, 1); + starToken = getStarToken(node); } - // Only check before when preceded by `function` keyword - prevToken = context.getTokenBefore(starToken); + // Only check before when preceded by `function`|`static` keyword + prevToken = sourceCode.getTokenBefore(starToken); if (prevToken.value === "function" || prevToken.value === "static") { checkSpacing("before", prevToken, starToken); } - nextToken = context.getTokenAfter(starToken); + nextToken = sourceCode.getTokenAfter(starToken); checkSpacing("after", starToken, nextToken); } diff --git a/tools/eslint/lib/rules/indent.js b/tools/eslint/lib/rules/indent.js index 3e8f4b92e1..3c0c9827d8 100644 --- a/tools/eslint/lib/rules/indent.js +++ b/tools/eslint/lib/rules/indent.js @@ -67,6 +67,10 @@ module.exports = { } } ] + }, + outerIIFEBody: { + type: "integer", + minimum: 0 } }, additionalProperties: false @@ -87,9 +91,12 @@ module.exports = { var: DEFAULT_VARIABLE_INDENT, let: DEFAULT_VARIABLE_INDENT, const: DEFAULT_VARIABLE_INDENT - } + }, + outerIIFEBody: null }; + var sourceCode = context.getSourceCode(); + if (context.options.length) { if (context.options[0] === "tab") { indentSize = 1; @@ -114,6 +121,10 @@ module.exports = { } else if (typeof variableDeclaratorRules === "object") { lodash.assign(options.VariableDeclarator, variableDeclaratorRules); } + + if (typeof opts.outerIIFEBody === "number") { + options.outerIIFEBody = opts.outerIIFEBody; + } } } @@ -206,15 +217,15 @@ module.exports = { } /** - * Get node indent + * Get the actual indent of node * @param {ASTNode|Token} node Node to examine * @param {boolean} [byLastLine=false] get indent of node's last line * @param {boolean} [excludeCommas=false] skip comma on start of line * @returns {int} Indent */ function getNodeIndent(node, byLastLine, excludeCommas) { - var token = byLastLine ? context.getLastToken(node) : context.getFirstToken(node); - var src = context.getSource(token, token.loc.start.column); + var token = byLastLine ? sourceCode.getLastToken(node) : sourceCode.getFirstToken(node); + var src = sourceCode.getText(token, token.loc.start.column); var regExp = excludeCommas ? indentPattern.excludeCommas : indentPattern.normal; var indent = regExp.exec(src); @@ -228,7 +239,7 @@ module.exports = { * @returns {boolean} true if its the first in the its start line */ function isNodeFirstInLine(node, byEndLocation) { - var firstToken = byEndLocation === true ? context.getLastToken(node, 1) : context.getTokenBefore(node), + var firstToken = byEndLocation === true ? sourceCode.getLastToken(node, 1) : sourceCode.getTokenBefore(node), startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line, endLine = firstToken ? firstToken.loc.end.line : -1; @@ -263,7 +274,7 @@ module.exports = { function checkNodesIndent(nodes, indent, excludeCommas) { nodes.forEach(function(node) { if (node.type === "IfStatement" && node.alternate) { - var elseToken = context.getTokenBefore(node.alternate); + var elseToken = sourceCode.getTokenBefore(node.alternate); checkNodeIndent(elseToken, indent, excludeCommas); } @@ -278,7 +289,7 @@ module.exports = { * @returns {void} */ function checkLastNodeLineIndent(node, lastLineIndent) { - var lastToken = context.getLastToken(node); + var lastToken = sourceCode.getLastToken(node); var endIndent = getNodeIndent(lastToken, true); if (endIndent !== lastLineIndent && isNodeFirstInLine(node, true)) { @@ -356,9 +367,25 @@ module.exports = { return false; } + /** + * Check to see if the node is a file level IIFE + * @param {ASTNode} node The function node to check. + * @returns {boolean} True if the node is the outer IIFE + */ + function isOuterIIFE(node) { + var parent = node.parent; + + return ( + parent.type === "CallExpression" && + parent.callee === node && + parent.parent.type === "ExpressionStatement" && + parent.parent.parent && parent.parent.parent.type === "Program" + ); + } + /** * Check indent for function block content - * @param {ASTNode} node node to examine + * @param {ASTNode} node A BlockStatement node that is inside of a function. * @returns {void} */ function checkIndentInFunctionBlock(node) { @@ -407,8 +434,14 @@ module.exports = { } } - // function body indent should be indent + indent size - indent += indentSize; + // function body indent should be indent + indent size, unless this + // is the outer IIFE and that option is enabled. + var functionOffset = indentSize; + + if (options.outerIIFEBody !== null && isOuterIIFE(calleeNode)) { + functionOffset = options.outerIIFEBody * indentSize; + } + indent += functionOffset; // check if the node is inside a variable var parentVarNode = getVariableDeclaratorNode(node); @@ -421,7 +454,7 @@ module.exports = { checkNodesIndent(node.body, indent); } - checkLastNodeLineIndent(node, indent - indentSize); + checkLastNodeLineIndent(node, indent - functionOffset); } @@ -431,7 +464,7 @@ module.exports = { * @returns {boolean} Whether or not the block starts and ends on the same line. */ function isSingleLineNode(node) { - var lastToken = context.getLastToken(node), + var lastToken = sourceCode.getLastToken(node), startLine = node.loc.start.line, endLine = lastToken.loc.end.line; @@ -641,11 +674,11 @@ module.exports = { checkNodesIndent(elements, elementsIndent, true); // Only check the last line if there is any token after the last item - if (context.getLastToken(node).loc.end.line <= lastElement.loc.end.line) { + if (sourceCode.getLastToken(node).loc.end.line <= lastElement.loc.end.line) { return; } - var tokenBeforeLastElement = context.getTokenBefore(lastElement); + var tokenBeforeLastElement = sourceCode.getTokenBefore(lastElement); if (tokenBeforeLastElement.value === ",") { diff --git a/tools/eslint/lib/rules/key-spacing.js b/tools/eslint/lib/rules/key-spacing.js index 1bc14aefec..1cf677865d 100644 --- a/tools/eslint/lib/rules/key-spacing.js +++ b/tools/eslint/lib/rules/key-spacing.js @@ -118,6 +118,8 @@ module.exports = { recommended: false }, + fixable: "whitespace", + schema: [{ anyOf: [ { @@ -196,6 +198,8 @@ module.exports = { multiLineOptions = initOptions({}, (options.multiLine || options)), singleLineOptions = initOptions({}, (options.singleLine || options)); + var sourceCode = context.getSourceCode(); + /** * Determines if the given property is key-value property. * @param {ASTNode} property Property node to check. @@ -220,7 +224,7 @@ module.exports = { while (node && (node.type !== "Punctuator" || node.value !== ":")) { prevNode = node; - node = context.getTokenAfter(node); + node = sourceCode.getTokenAfter(node); } return prevNode; @@ -235,7 +239,7 @@ module.exports = { function getNextColon(node) { while (node && (node.type !== "Punctuator" || node.value !== ":")) { - node = context.getTokenAfter(node); + node = sourceCode.getTokenAfter(node); } return node; @@ -250,7 +254,7 @@ module.exports = { var key = property.key; if (property.computed) { - return context.getSource().slice(key.range[0], key.range[1]); + return sourceCode.getText().slice(key.range[0], key.range[1]); } return property.key.name || property.key.value; @@ -268,9 +272,16 @@ module.exports = { */ function report(property, side, whitespace, expected, mode) { var diff = whitespace.length - expected, - key = property.key, - firstTokenAfterColon = context.getTokenAfter(getNextColon(key)), - location = side === "key" ? key.loc.start : firstTokenAfterColon.loc.start; + nextColon = getNextColon(property.key), + tokenBeforeColon = sourceCode.getTokenBefore(nextColon), + tokenAfterColon = sourceCode.getTokenAfter(nextColon), + isKeySide = side === "key", + locStart = isKeySide ? tokenBeforeColon.loc.start : tokenAfterColon.loc.start, + isExtra = diff > 0, + diffAbs = Math.abs(diff), + spaces = Array(diffAbs + 1).join(" "), + fix, + range; if (( diff && mode === "strict" || @@ -278,10 +289,41 @@ module.exports = { diff > 0 && !expected && mode === "minimum") && !(expected && containsLineTerminator(whitespace)) ) { - context.report(property[side], location, messages[side], { - error: diff > 0 ? "Extra" : "Missing", - computed: property.computed ? "computed " : "", - key: getKey(property) + if (isExtra) { + + // Remove whitespace + if (isKeySide) { + range = [tokenBeforeColon.end, tokenBeforeColon.end + diffAbs]; + } else { + range = [tokenAfterColon.start - diffAbs, tokenAfterColon.start]; + } + 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: fix }); } } @@ -295,7 +337,7 @@ module.exports = { function getKeyWidth(property) { var startToken, endToken; - startToken = context.getFirstToken(property); + startToken = sourceCode.getFirstToken(property); endToken = getLastTokenBeforeColon(property.key); return endToken.range[1] - startToken.range[0]; @@ -307,7 +349,7 @@ module.exports = { * @returns {Object} Whitespace before and after the property's colon. */ function getPropertyWhitespace(property) { - var whitespace = /(\s*):(\s*)/.exec(context.getSource().slice( + var whitespace = /(\s*):(\s*)/.exec(sourceCode.getText().slice( property.key.range[1], property.value.range[0] )); diff --git a/tools/eslint/lib/rules/linebreak-style.js b/tools/eslint/lib/rules/linebreak-style.js index d99d7e5857..5e6b819f3f 100644 --- a/tools/eslint/lib/rules/linebreak-style.js +++ b/tools/eslint/lib/rules/linebreak-style.js @@ -31,6 +31,8 @@ module.exports = { var EXPECTED_LF_MSG = "Expected linebreaks to be 'LF' but found 'CRLF'.", EXPECTED_CRLF_MSG = "Expected linebreaks to be 'CRLF' but found 'LF'."; + var sourceCode = context.getSourceCode(); + //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- @@ -57,7 +59,7 @@ module.exports = { var linebreakStyle = context.options[0] || "unix", expectedLF = linebreakStyle === "unix", expectedLFChars = expectedLF ? "\n" : "\r\n", - source = context.getSource(), + source = sourceCode.getText(), pattern = /\r\n|\r|\n|\u2028|\u2029/g, match, index, @@ -77,7 +79,7 @@ module.exports = { node: node, loc: { line: i, - column: context.getSourceLines()[i - 1].length + column: sourceCode.lines[i - 1].length }, message: expectedLF ? EXPECTED_LF_MSG : EXPECTED_CRLF_MSG, fix: createFix(range, expectedLFChars) diff --git a/tools/eslint/lib/rules/lines-around-comment.js b/tools/eslint/lib/rules/lines-around-comment.js index 92fc3252ae..a227fe4184 100644 --- a/tools/eslint/lib/rules/lines-around-comment.js +++ b/tools/eslint/lib/rules/lines-around-comment.js @@ -8,7 +8,8 @@ // Requirements //------------------------------------------------------------------------------ -var lodash = require("lodash"); +var lodash = require("lodash"), + astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ // Helpers @@ -51,16 +52,6 @@ function getCommentLineNums(comments) { return lines; } -/** - * Determines if a value is an array. - * @param {number} val The value we wish to check for in the array.. - * @param {Array} array An array. - * @returns {boolean} True if the value is in the array.. - */ -function contains(val, array) { - return array.indexOf(val) > -1; -} - //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -73,6 +64,8 @@ module.exports = { recommended: false }, + fixable: "whitespace", + schema: [ { type: "object", @@ -126,6 +119,22 @@ module.exports = { var sourceCode = context.getSourceCode(); + var lines = sourceCode.lines, + numLines = lines.length + 1, + comments = sourceCode.getAllComments(), + commentLines = getCommentLineNums(comments), + emptyLines = getEmptyLineNums(lines), + commentAndEmptyLines = commentLines.concat(emptyLines); + + /** + * Returns whether or not a token is a comment node type + * @param {Token} token The token to check + * @returns {boolean} True if the token is a comment node + */ + function isCommentNodeType(token) { + return token && (token.type === "Block" || token.type === "Line"); + } + /** * Returns whether or not comments are on lines starting with or ending with code * @param {ASTNode} node The comment node to check. @@ -137,18 +146,18 @@ module.exports = { token = node; do { token = sourceCode.getTokenOrCommentBefore(token); - } while (token && (token.type === "Block" || token.type === "Line")); + } while (isCommentNodeType(token)); - if (token && token.loc.end.line === node.loc.start.line) { + if (token && astUtils.isTokenOnSameLine(token, node)) { return true; } token = node; do { token = sourceCode.getTokenOrCommentAfter(token); - } while (token && (token.type === "Block" || token.type === "Line")); + } while (isCommentNodeType(token)); - if (token && token.loc.start.line === node.loc.end.line) { + if (token && astUtils.isTokenOnSameLine(node, token)) { return true; } @@ -267,14 +276,6 @@ module.exports = { * @returns {void} */ function checkForEmptyLine(node, opts) { - - var lines = context.getSourceLines(), - numLines = lines.length + 1, - comments = context.getAllComments(), - commentLines = getCommentLineNums(comments), - emptyLines = getEmptyLineNums(lines), - commentAndEmptyLines = commentLines.concat(emptyLines); - var after = opts.after, before = opts.before; @@ -305,14 +306,34 @@ module.exports = { return; } + var previousTokenOrComment = sourceCode.getTokenOrCommentBefore(node); + var nextTokenOrComment = sourceCode.getTokenOrCommentAfter(node); + // check for newline before - if (!exceptionStartAllowed && before && !contains(prevLineNum, commentAndEmptyLines)) { - context.report(node, "Expected line before comment."); + if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) && + !(isCommentNodeType(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, node))) { + var lineStart = node.range[0] - node.loc.start.column; + var range = [lineStart, lineStart]; + + context.report({ + node: node, + message: "Expected line before comment.", + fix: function(fixer) { + return fixer.insertTextBeforeRange(range, "\n"); + } + }); } // check for newline after - if (!exceptionEndAllowed && after && !contains(nextLineNum, commentAndEmptyLines)) { - context.report(node, "Expected line after comment."); + if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) && + !(isCommentNodeType(nextTokenOrComment) && astUtils.isTokenOnSameLine(node, nextTokenOrComment))) { + context.report({ + node: node, + message: "Expected line after comment.", + fix: function(fixer) { + return fixer.insertTextAfter(node, "\n"); + } + }); } } diff --git a/tools/eslint/lib/rules/max-len.js b/tools/eslint/lib/rules/max-len.js index 1ba539a119..b5813bbfaa 100644 --- a/tools/eslint/lib/rules/max-len.js +++ b/tools/eslint/lib/rules/max-len.js @@ -81,6 +81,8 @@ module.exports = { */ var URL_REGEXP = /[^:/?#]:\/\/[^?#]/; + var 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. @@ -155,11 +157,12 @@ module.exports = { */ function isFullLineComment(line, lineNumber, comment) { var start = comment.loc.start, - end = comment.loc.end; + end = comment.loc.end, + isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim(); return comment && - (start.line < lineNumber || (start.line === lineNumber && start.column === 0)) && - (end.line > lineNumber || end.column === line.length); + (start.line < lineNumber || (start.line === lineNumber && isFirstTokenOnLine)) && + (end.line > lineNumber || (end.line === lineNumber && end.column === line.length)); } /** @@ -185,10 +188,10 @@ module.exports = { function checkProgramForMaxLength(node) { // split (honors line-ending) - var lines = context.getSourceLines(), + var lines = sourceCode.lines, // list of comments to ignore - comments = ignoreComments || maxCommentLength || ignoreTrailingComments ? context.getAllComments() : [], + comments = ignoreComments || maxCommentLength || ignoreTrailingComments ? sourceCode.getAllComments() : [], // we iterate over comments in parallel with the lines commentsIndex = 0; diff --git a/tools/eslint/lib/rules/max-lines.js b/tools/eslint/lib/rules/max-lines.js new file mode 100644 index 0000000000..751310e81d --- /dev/null +++ b/tools/eslint/lib/rules/max-lines.js @@ -0,0 +1,148 @@ +/** + * @fileoverview enforce a maximum file length + * @author Alberto RodrĆguez + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var lodash = require("lodash"); +var astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a maximum number of lines per file", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + oneOf: [ + { + type: "integer", + minimum: 0 + }, + { + type: "object", + properties: { + max: { + type: "integer", + minimum: 0 + }, + skipComments: { + type: "boolean" + }, + skipBlankLines: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create: function(context) { + var option = context.options[0], + max = 300; + + if (typeof option === "object" && option.hasOwnProperty("max") && typeof option.max === "number") { + max = option.max; + } + + if (typeof option === "number") { + max = option; + } + + var skipComments = option && option.skipComments; + var skipBlankLines = option && option.skipBlankLines; + + var sourceCode = context.getSourceCode(); + + /** + * Returns whether or not a token is a comment node type + * @param {Token} token The token to check + * @returns {boolean} True if the token is a comment node + */ + function isCommentNodeType(token) { + return token && (token.type === "Block" || token.type === "Line"); + } + + /** + * Returns the line numbers of a comment that don't have any code on the same line + * @param {Node} comment The comment node to check + * @returns {int[]} The line numbers + */ + function getLinesWithoutCode(comment) { + var start = comment.loc.start.line; + var end = comment.loc.end.line; + + var token; + + token = comment; + do { + token = sourceCode.getTokenOrCommentBefore(token); + } while (isCommentNodeType(token)); + + if (token && astUtils.isTokenOnSameLine(token, comment)) { + start += 1; + } + + token = comment; + do { + token = sourceCode.getTokenOrCommentAfter(token); + } while (isCommentNodeType(token)); + + if (token && astUtils.isTokenOnSameLine(comment, token)) { + end -= 1; + } + + if (start <= end) { + return lodash.range(start, end + 1); + } + return []; + } + + return { + "Program:exit": function() { + var lines = sourceCode.lines.map(function(text, i) { + return { lineNumber: i + 1, text: text }; + }); + + if (skipBlankLines) { + lines = lines.filter(function(l) { + return l.text.trim() !== ""; + }); + } + + if (skipComments) { + var comments = sourceCode.getAllComments(); + + var commentLines = lodash.flatten(comments.map(function(comment) { + return getLinesWithoutCode(comment); + })); + + lines = lines.filter(function(l) { + return !lodash.includes(commentLines, l.lineNumber); + }); + } + + if (lines.length > max) { + context.report({ + loc: { line: 1, column: 0 }, + message: "File must be at most " + max + " lines long" + }); + } + } + }; + } +}; diff --git a/tools/eslint/lib/rules/max-statements-per-line.js b/tools/eslint/lib/rules/max-statements-per-line.js index 64058f0d30..55f09746c3 100644 --- a/tools/eslint/lib/rules/max-statements-per-line.js +++ b/tools/eslint/lib/rules/max-statements-per-line.js @@ -21,7 +21,8 @@ module.exports = { type: "object", properties: { max: { - type: "integer" + type: "integer", + minimum: 0 } }, additionalProperties: false @@ -31,72 +32,81 @@ module.exports = { create: function(context) { - var options = context.options[0] || {}, + var sourceCode = context.getSourceCode(), + options = context.options[0] || {}, lastStatementLine = 0, numberOfStatementsOnThisLine = 0, - maxStatementsPerLine = typeof options.max !== "undefined" ? options.max : 1; + maxStatementsPerLine = typeof options.max !== "undefined" ? options.max : 1, + message = "This line has too many statements. Maximum allowed is " + maxStatementsPerLine + "."; //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- + var SINGLE_CHILD_ALLOWED = /^(?:(?:DoWhile|For|ForIn|ForOf|If|Labeled|While)Statement|Export(?:Default|Named)Declaration)$/; + /** - * Reports a node - * @param {ASTNode} node The node to report - * @returns {void} - * @private + * Gets the actual last token of a given node. + * + * @param {ASTNode} node - A node to get. This is a node except EmptyStatement. + * @returns {Token} The actual last token. */ - function report(node) { - context.report( - node, - "This line has too many statements. Maximum allowed is {{max}}.", - { max: maxStatementsPerLine }); + function getActualLastToken(node) { + var lastToken = sourceCode.getLastToken(node); + + if (lastToken.value === ";") { + lastToken = sourceCode.getTokenBefore(lastToken); + } + return lastToken; } /** - * Enforce a maximum number of statements per line - * @param {ASTNode} nodes Array of nodes to evaluate + * Addresses a given node. + * It updates the state of this rule, then reports the node if the node violated this rule. + * + * @param {ASTNode} node - A node to check. * @returns {void} - * @private */ - function enforceMaxStatementsPerLine(nodes) { - if (nodes.length < 1) { + function enterStatement(node) { + var line = node.loc.start.line; + + // Skip to allow non-block statements if this is direct child of control statements. + // `if (a) foo();` is counted as 1. + // But `if (a) foo(); else foo();` should be counted as 2. + if (SINGLE_CHILD_ALLOWED.test(node.parent.type) && + node.parent.alternate !== node + ) { return; } - for (var i = 0, l = nodes.length; i < l; ++i) { - var currentStatement = nodes[i]; + // Update state. + if (line === lastStatementLine) { + numberOfStatementsOnThisLine += 1; + } else { + numberOfStatementsOnThisLine = 1; + lastStatementLine = line; + } - if (currentStatement.loc.start.line === lastStatementLine) { - ++numberOfStatementsOnThisLine; - } else { - numberOfStatementsOnThisLine = 1; - lastStatementLine = currentStatement.loc.end.line; - } - if (numberOfStatementsOnThisLine === maxStatementsPerLine + 1) { - report(currentStatement); - } + // Reports if the node violated this rule. + if (numberOfStatementsOnThisLine === maxStatementsPerLine + 1) { + context.report({node: node, message: message}); } } /** - * Check each line in the body of a node - * @param {ASTNode} node node to evaluate + * Updates the state of this rule with the end line of leaving node to check with the next statement. + * + * @param {ASTNode} node - A node to check. * @returns {void} - * @private */ - function checkLinesInBody(node) { - enforceMaxStatementsPerLine(node.body); - } + function leaveStatement(node) { + var line = getActualLastToken(node).loc.end.line; - /** - * Check each line in the consequent of a switch case - * @param {ASTNode} node node to evaluate - * @returns {void} - * @private - */ - function checkLinesInConsequent(node) { - enforceMaxStatementsPerLine(node.consequent); + // Update state. + if (line !== lastStatementLine) { + numberOfStatementsOnThisLine = 1; + lastStatementLine = line; + } } //-------------------------------------------------------------------------- @@ -104,10 +114,61 @@ module.exports = { //-------------------------------------------------------------------------- return { - Program: checkLinesInBody, - BlockStatement: checkLinesInBody, - SwitchCase: checkLinesInConsequent - }; + BreakStatement: enterStatement, + ClassDeclaration: enterStatement, + ContinueStatement: enterStatement, + DebuggerStatement: enterStatement, + DoWhileStatement: enterStatement, + ExpressionStatement: enterStatement, + ForInStatement: enterStatement, + ForOfStatement: enterStatement, + ForStatement: enterStatement, + FunctionDeclaration: enterStatement, + IfStatement: enterStatement, + ImportDeclaration: enterStatement, + LabeledStatement: enterStatement, + ReturnStatement: enterStatement, + SwitchStatement: enterStatement, + ThrowStatement: enterStatement, + TryStatement: enterStatement, + VariableDeclaration: enterStatement, + WhileStatement: enterStatement, + WithStatement: enterStatement, + ExportNamedDeclaration: enterStatement, + ExportDefaultDeclaration: enterStatement, + ExportAllDeclaration: enterStatement, + "BreakStatement:exit": leaveStatement, + "ClassDeclaration:exit": leaveStatement, + "ContinueStatement:exit": leaveStatement, + "DebuggerStatement:exit": leaveStatement, + "DoWhileStatement:exit": leaveStatement, + "ExpressionStatement:exit": leaveStatement, + "ForInStatement:exit": leaveStatement, + "ForOfStatement:exit": leaveStatement, + "ForStatement:exit": leaveStatement, + "FunctionDeclaration:exit": leaveStatement, + "IfStatement:exit": leaveStatement, + "ImportDeclaration:exit": leaveStatement, + "LabeledStatement:exit": leaveStatement, + "ReturnStatement:exit": leaveStatement, + "SwitchStatement:exit": leaveStatement, + "ThrowStatement:exit": leaveStatement, + "TryStatement:exit": leaveStatement, + "VariableDeclaration:exit": leaveStatement, + "WhileStatement:exit": leaveStatement, + "WithStatement:exit": leaveStatement, + "ExportNamedDeclaration:exit": leaveStatement, + "ExportDefaultDeclaration:exit": leaveStatement, + "ExportAllDeclaration:exit": leaveStatement, + + // For backward compatibility. + // Empty blocks should be warned if `{max: 0}` was given. + BlockStatement: function reportIfZero(node) { + if (maxStatementsPerLine === 0 && node.body.length === 0) { + context.report({node: node, message: message}); + } + } + }; } }; diff --git a/tools/eslint/lib/rules/new-cap.js b/tools/eslint/lib/rules/new-cap.js index 697b60d0f1..2dabb30a65 100644 --- a/tools/eslint/lib/rules/new-cap.js +++ b/tools/eslint/lib/rules/new-cap.js @@ -127,6 +127,8 @@ module.exports = { var listeners = {}; + var sourceCode = context.getSourceCode(); + //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- @@ -186,7 +188,7 @@ module.exports = { * @returns {Boolean} Returns true if the callee may be capitalized */ function isCapAllowed(allowedMap, node, calleeName) { - if (allowedMap[calleeName] || allowedMap[context.getSource(node.callee)]) { + if (allowedMap[calleeName] || allowedMap[sourceCode.getText(node.callee)]) { return true; } diff --git a/tools/eslint/lib/rules/new-parens.js b/tools/eslint/lib/rules/new-parens.js index 0d7a84e3cc..ec6106647a 100644 --- a/tools/eslint/lib/rules/new-parens.js +++ b/tools/eslint/lib/rules/new-parens.js @@ -21,11 +21,12 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); return { NewExpression: function(node) { - var tokens = context.getTokens(node); + var tokens = sourceCode.getTokens(node); var prenticesTokens = tokens.filter(function(token) { return token.value === "(" || token.value === ")"; }); diff --git a/tools/eslint/lib/rules/newline-after-var.js b/tools/eslint/lib/rules/newline-after-var.js index fd80c8c542..8801407c0b 100644 --- a/tools/eslint/lib/rules/newline-after-var.js +++ b/tools/eslint/lib/rules/newline-after-var.js @@ -35,7 +35,7 @@ module.exports = { var mode = context.options[0] === "never" ? "never" : "always"; // Cache starting and ending line numbers of comments for faster lookup - var commentEndLine = context.getAllComments().reduce(function(result, token) { + var commentEndLine = sourceCode.getAllComments().reduce(function(result, token) { result[token.loc.start.line] = token.loc.end.line; return result; }, {}); diff --git a/tools/eslint/lib/rules/newline-before-return.js b/tools/eslint/lib/rules/newline-before-return.js index 77f3aedaa8..5c8a139358 100644 --- a/tools/eslint/lib/rules/newline-before-return.js +++ b/tools/eslint/lib/rules/newline-before-return.js @@ -133,32 +133,17 @@ module.exports = { return (lineNumNode - lineNumTokenBefore - commentLines) > 1; } - /** - * Reports expected/unexpected newline before return statement - * @param {ASTNode} node - the node to report in the event of an error - * @param {boolean} isExpected - whether the newline is expected or not - * @returns {void} - * @private - */ - function reportError(node, isExpected) { - var expected = isExpected ? "Expected" : "Unexpected"; - - context.report({ - node: node, - message: expected + " newline before return statement." - }); - } - //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- return { ReturnStatement: function(node) { - if (isFirstNode(node) && hasNewlineBefore(node)) { - reportError(node, false); - } else if (!isFirstNode(node) && !hasNewlineBefore(node)) { - reportError(node, true); + if (!isFirstNode(node) && !hasNewlineBefore(node)) { + context.report({ + node: node, + message: "Expected newline before return statement." + }); } } }; diff --git a/tools/eslint/lib/rules/newline-per-chained-call.js b/tools/eslint/lib/rules/newline-per-chained-call.js index 80401415aa..c412d53e99 100644 --- a/tools/eslint/lib/rules/newline-per-chained-call.js +++ b/tools/eslint/lib/rules/newline-per-chained-call.js @@ -36,6 +36,23 @@ module.exports = { var options = context.options[0] || {}, ignoreChainWithDepth = options.ignoreChainWithDepth || 2; + var sourceCode = context.getSourceCode(); + + /** + * Gets the property text of a given MemberExpression node. + * If the text is multiline, this returns only the first line. + * + * @param {ASTNode} node - A MemberExpression node to get. + * @returns {string} The property text of the node. + */ + function getPropertyText(node) { + var prefix = node.computed ? "[" : "."; + var lines = sourceCode.getText(node.property).split(/\r\n|\r|\n/g); + var suffix = node.computed && lines.length === 1 ? "]" : ""; + + return prefix + lines[0] + suffix; + } + return { "CallExpression:exit": function(node) { if (!node.callee || node.callee.type !== "MemberExpression") { @@ -55,7 +72,7 @@ module.exports = { context.report( callee.property, callee.property.loc.start, - "Expected line break after `" + context.getSource(callee.object).replace(/\r\n|\r|\n/g, "\\n") + "`." + "Expected line break before `" + getPropertyText(callee) + "`." ); } } diff --git a/tools/eslint/lib/rules/no-cond-assign.js b/tools/eslint/lib/rules/no-cond-assign.js index 27b99c6b54..e0979ddaf2 100644 --- a/tools/eslint/lib/rules/no-cond-assign.js +++ b/tools/eslint/lib/rules/no-cond-assign.js @@ -34,6 +34,8 @@ module.exports = { var prohibitAssign = (context.options[0] || "except-parens"); + var sourceCode = context.getSourceCode(); + /** * Check whether an AST node is the test expression for a conditional statement. * @param {!Object} node The node to test. @@ -68,8 +70,8 @@ module.exports = { * @returns {boolean} `true` if the code is enclosed in parentheses; otherwise, `false`. */ function isParenthesised(node) { - var previousToken = context.getTokenBefore(node), - nextToken = context.getTokenAfter(node); + var previousToken = sourceCode.getTokenBefore(node), + nextToken = sourceCode.getTokenAfter(node); return previousToken.value === "(" && previousToken.range[1] <= node.range[0] && nextToken.value === ")" && nextToken.range[0] >= node.range[1]; @@ -81,8 +83,8 @@ module.exports = { * @returns {boolean} `true` if the code is enclosed in two sets of parentheses; otherwise, `false`. */ function isParenthesisedTwice(node) { - var previousToken = context.getTokenBefore(node, 1), - nextToken = context.getTokenAfter(node, 1); + var previousToken = sourceCode.getTokenBefore(node, 1), + nextToken = sourceCode.getTokenAfter(node, 1); return isParenthesised(node) && previousToken.value === "(" && previousToken.range[1] <= node.range[0] && diff --git a/tools/eslint/lib/rules/no-confusing-arrow.js b/tools/eslint/lib/rules/no-confusing-arrow.js index d951a53cf9..1f18aa3567 100644 --- a/tools/eslint/lib/rules/no-confusing-arrow.js +++ b/tools/eslint/lib/rules/no-confusing-arrow.js @@ -44,6 +44,7 @@ module.exports = { create: function(context) { var config = context.options[0] || {}; + var sourceCode = context.getSourceCode(); /** * Reports if an arrow function contains an ambiguous conditional. @@ -53,7 +54,7 @@ module.exports = { function checkArrowFunc(node) { var body = node.body; - if (isConditional(body) && !(config.allowParens && astUtils.isParenthesised(context, body))) { + if (isConditional(body) && !(config.allowParens && astUtils.isParenthesised(sourceCode, body))) { context.report(node, "Arrow function used ambiguously with a conditional expression."); } } diff --git a/tools/eslint/lib/rules/no-constant-condition.js b/tools/eslint/lib/rules/no-constant-condition.js index 0072491bee..7c4ede7f78 100644 --- a/tools/eslint/lib/rules/no-constant-condition.js +++ b/tools/eslint/lib/rules/no-constant-condition.js @@ -17,10 +17,23 @@ module.exports = { recommended: true }, - schema: [] + schema: [ + { + type: "object", + properties: { + checkLoops: { + type: "boolean" + } + }, + additionalProperties: false + } + + ] }, create: function(context) { + var options = context.options[0] || {}, + checkLoops = options.checkLoops !== false; //-------------------------------------------------------------------------- // Helpers @@ -102,6 +115,18 @@ module.exports = { } } + /** + * Checks node when checkLoops option is enabled + * @param {ASTNode} node The AST node to check. + * @returns {void} + * @private + */ + function checkLoop(node) { + if (checkLoops) { + checkConstantCondition(node); + } + } + //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -109,9 +134,9 @@ module.exports = { return { ConditionalExpression: checkConstantCondition, IfStatement: checkConstantCondition, - WhileStatement: checkConstantCondition, - DoWhileStatement: checkConstantCondition, - ForStatement: checkConstantCondition + WhileStatement: checkLoop, + DoWhileStatement: checkLoop, + ForStatement: checkLoop }; } diff --git a/tools/eslint/lib/rules/no-div-regex.js b/tools/eslint/lib/rules/no-div-regex.js index 58c44662cb..75a6085595 100644 --- a/tools/eslint/lib/rules/no-div-regex.js +++ b/tools/eslint/lib/rules/no-div-regex.js @@ -21,11 +21,12 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); return { Literal: function(node) { - var token = context.getFirstToken(node); + var token = sourceCode.getFirstToken(node); if (token.type === "RegularExpression" && token.value[1] === "=") { context.report(node, "A regular expression literal can be confused with '/='."); diff --git a/tools/eslint/lib/rules/no-duplicate-case.js b/tools/eslint/lib/rules/no-duplicate-case.js index 1308730a63..8c877ed4e8 100644 --- a/tools/eslint/lib/rules/no-duplicate-case.js +++ b/tools/eslint/lib/rules/no-duplicate-case.js @@ -22,13 +22,14 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); return { SwitchStatement: function(node) { var mapping = {}; node.cases.forEach(function(switchCase) { - var key = context.getSource(switchCase.test); + var key = sourceCode.getText(switchCase.test); if (mapping[key]) { context.report(switchCase, "Duplicate case label."); diff --git a/tools/eslint/lib/rules/no-else-return.js b/tools/eslint/lib/rules/no-else-return.js index 4678d320af..528d4ca566 100644 --- a/tools/eslint/lib/rules/no-else-return.js +++ b/tools/eslint/lib/rules/no-else-return.js @@ -33,7 +33,7 @@ module.exports = { * @returns {void} */ function displayReport(node) { - context.report(node, "Unexpected 'else' after 'return'."); + context.report(node, "Unnecessary 'else' after 'return'."); } /** @@ -112,14 +112,13 @@ module.exports = { // If we have a BlockStatement, check each consequent body node. return node.body.some(checkForReturnOrIf); - } else { - - /* - * If not a block statement, make sure the consequent isn't a - * ReturnStatement or an IfStatement with returns on both paths. - */ - return checkForReturnOrIf(node); } + + /* + * If not a block statement, make sure the consequent isn't a + * ReturnStatement or an IfStatement with returns on both paths. + */ + return checkForReturnOrIf(node); } //-------------------------------------------------------------------------- diff --git a/tools/eslint/lib/rules/no-empty-character-class.js b/tools/eslint/lib/rules/no-empty-character-class.js index e015e0cc06..34ef78a396 100644 --- a/tools/eslint/lib/rules/no-empty-character-class.js +++ b/tools/eslint/lib/rules/no-empty-character-class.js @@ -39,11 +39,12 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); return { Literal: function(node) { - var token = context.getFirstToken(node); + var token = sourceCode.getFirstToken(node); if (token.type === "RegularExpression" && !regex.test(token.value)) { context.report(node, "Empty class."); diff --git a/tools/eslint/lib/rules/no-empty-function.js b/tools/eslint/lib/rules/no-empty-function.js index 412614b501..0102acff51 100644 --- a/tools/eslint/lib/rules/no-empty-function.js +++ b/tools/eslint/lib/rules/no-empty-function.js @@ -121,6 +121,8 @@ module.exports = { var options = context.options[0] || {}; var allowed = options.allow || []; + var sourceCode = context.getSourceCode(); + /** * Reports a given function node if the node matches the following patterns. * @@ -139,7 +141,7 @@ module.exports = { if (allowed.indexOf(kind) === -1 && node.body.type === "BlockStatement" && node.body.body.length === 0 && - context.getComments(node.body).trailing.length === 0 + sourceCode.getComments(node.body).trailing.length === 0 ) { context.report({ node: node, diff --git a/tools/eslint/lib/rules/no-empty.js b/tools/eslint/lib/rules/no-empty.js index 8f32428304..1302a90753 100644 --- a/tools/eslint/lib/rules/no-empty.js +++ b/tools/eslint/lib/rules/no-empty.js @@ -35,6 +35,8 @@ module.exports = { var options = context.options[0] || {}, allowEmptyCatch = options.allowEmptyCatch || false; + var sourceCode = context.getSourceCode(); + return { BlockStatement: function(node) { @@ -53,7 +55,7 @@ module.exports = { } // any other block is only allowed to be empty, if it contains a comment - if (context.getComments(node).trailing.length > 0) { + if (sourceCode.getComments(node).trailing.length > 0) { return; } diff --git a/tools/eslint/lib/rules/no-extra-parens.js b/tools/eslint/lib/rules/no-extra-parens.js index cf129394ba..c33a64920f 100644 --- a/tools/eslint/lib/rules/no-extra-parens.js +++ b/tools/eslint/lib/rules/no-extra-parens.js @@ -40,7 +40,8 @@ module.exports = { type: "object", properties: { conditionalAssign: {type: "boolean"}, - nestedBinaryExpressions: {type: "boolean"} + nestedBinaryExpressions: {type: "boolean"}, + returnAssign: {type: "boolean"} }, additionalProperties: false } @@ -53,11 +54,14 @@ module.exports = { }, create: function(context) { - var isParenthesised = astUtils.isParenthesised.bind(astUtils, context); + var sourceCode = context.getSourceCode(); + + var isParenthesised = astUtils.isParenthesised.bind(astUtils, sourceCode); + var precedence = astUtils.getPrecedence; var ALL_NODES = context.options[0] !== "functions"; var EXCEPT_COND_ASSIGN = ALL_NODES && context.options[1] && context.options[1].conditionalAssign === false; var NESTED_BINARY = ALL_NODES && context.options[1] && context.options[1].nestedBinaryExpressions === false; - var sourceCode = context.getSourceCode(); + var EXCEPT_RETURN_ASSIGN = ALL_NODES && context.options[1] && context.options[1].returnAssign === false; /** * Determines if this rule should be enforced for a node given the current configuration. @@ -76,8 +80,8 @@ module.exports = { * @private */ function isParenthesisedTwice(node) { - var previousToken = context.getTokenBefore(node, 1), - nextToken = context.getTokenAfter(node, 1); + var previousToken = sourceCode.getTokenBefore(node, 1), + nextToken = sourceCode.getTokenAfter(node, 1); return isParenthesised(node) && previousToken && nextToken && previousToken.value === "(" && previousToken.range[1] <= node.range[0] && @@ -116,6 +120,64 @@ module.exports = { } /** + * Determines if a node is in a return statement + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the node is in a return statement. + * @private + */ + function isInReturnStatement(node) { + while (node) { + if (node.type === "ReturnStatement" || + (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement")) { + return true; + } + node = node.parent; + } + + return false; + } + + /** + * Determines if a node is or contains an assignment expression + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the node is or contains an assignment expression. + * @private + */ + function containsAssignment(node) { + if (node.type === "AssignmentExpression") { + return true; + } else if (node.type === "ConditionalExpression" && + (node.consequent.type === "AssignmentExpression" || node.alternate.type === "AssignmentExpression")) { + return true; + } else if ((node.left && node.left.type === "AssignmentExpression") || + (node.right && node.right.type === "AssignmentExpression")) { + return true; + } + + return false; + } + + /** + * Determines if a node is contained by or is itself a return statement and is allowed to have a parenthesised assignment + * @param {ASTNode} node - The node to be checked. + * @returns {boolean} True if the assignment can be parenthesised. + * @private + */ + function isReturnAssignException(node) { + if (!EXCEPT_RETURN_ASSIGN || !isInReturnStatement(node)) { + return false; + } + + if (node.type === "ReturnStatement") { + return node.argument && containsAssignment(node.argument); + } else if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") { + return containsAssignment(node.body); + } else { + return containsAssignment(node); + } + } + + /** * Determines if a node following a [no LineTerminator here] restriction is * surrounded by (potentially) invalid extra parentheses. * @param {Token} token - The token preceding the [no LineTerminator here] restriction. @@ -195,105 +257,13 @@ module.exports = { } /** - * Get the precedence level based on the node type - * @param {ASTNode} node node to evaluate - * @returns {int} precedence level - * @private - */ - function precedence(node) { - - switch (node.type) { - case "SequenceExpression": - return 0; - - case "AssignmentExpression": - case "ArrowFunctionExpression": - case "YieldExpression": - return 1; - - case "ConditionalExpression": - return 3; - - case "LogicalExpression": - switch (node.operator) { - case "||": - return 4; - case "&&": - return 5; - - // no default - } - - /* falls through */ - - case "BinaryExpression": - - switch (node.operator) { - case "|": - return 6; - case "^": - return 7; - case "&": - return 8; - case "==": - case "!=": - case "===": - case "!==": - return 9; - case "<": - case "<=": - case ">": - case ">=": - case "in": - case "instanceof": - return 10; - case "<<": - case ">>": - case ">>>": - return 11; - case "+": - case "-": - return 12; - case "*": - case "/": - case "%": - return 13; - - // no default - } - - /* falls through */ - - case "UnaryExpression": - return 14; - - case "UpdateExpression": - return 15; - - case "CallExpression": - - // IIFE is allowed to have parens in any position (#655) - if (node.callee.type === "FunctionExpression") { - return -1; - } - return 16; - - case "NewExpression": - return 17; - - // no default - } - return 18; - } - - /** * Report the node * @param {ASTNode} node node to evaluate * @returns {void} * @private */ function report(node) { - var previousToken = context.getTokenBefore(node); + var previousToken = sourceCode.getTokenBefore(node); context.report(node, previousToken.loc.start, "Gratuitous parentheses around expression."); } @@ -368,6 +338,10 @@ module.exports = { }, ArrowFunctionExpression: function(node) { + if (isReturnAssignException(node)) { + return; + } + if (node.body.type !== "BlockStatement") { if (sourceCode.getFirstToken(node.body).value !== "{" && hasExcessParens(node.body) && precedence(node.body) >= precedence({type: "AssignmentExpression"})) { report(node.body); @@ -383,6 +357,10 @@ module.exports = { }, AssignmentExpression: function(node) { + if (isReturnAssignException(node)) { + return; + } + if (hasExcessParens(node.right) && precedence(node.right) >= precedence(node)) { report(node.right); } @@ -392,12 +370,18 @@ module.exports = { CallExpression: dryCallNew, ConditionalExpression: function(node) { + if (isReturnAssignException(node)) { + return; + } + if (hasExcessParens(node.test) && precedence(node.test) >= precedence({type: "LogicalExpression", operator: "||"})) { report(node.test); } + if (hasExcessParens(node.consequent) && precedence(node.consequent) >= precedence({type: "AssignmentExpression"})) { report(node.consequent); } + if (hasExcessParens(node.alternate) && precedence(node.alternate) >= precedence({type: "AssignmentExpression"})) { report(node.alternate); } @@ -413,7 +397,7 @@ module.exports = { var firstToken, secondToken, firstTokens; if (hasExcessParens(node.expression)) { - firstTokens = context.getFirstTokens(node.expression, 2); + firstTokens = sourceCode.getFirstTokens(node.expression, 2); firstToken = firstTokens[0]; secondToken = firstTokens[1]; @@ -476,7 +460,7 @@ module.exports = { !( (node.object.type === "Literal" && typeof node.object.value === "number" && - /^[0-9]+$/.test(context.getFirstToken(node.object).value)) + /^[0-9]+$/.test(sourceCode.getFirstToken(node.object).value)) || // RegExp literal is allowed to have parens (#1589) @@ -511,6 +495,10 @@ module.exports = { ReturnStatement: function(node) { var returnToken = sourceCode.getFirstToken(node); + if (isReturnAssignException(node)) { + return; + } + if (node.argument && hasExcessParensNoLineTerminator(returnToken, node.argument) && diff --git a/tools/eslint/lib/rules/no-extra-semi.js b/tools/eslint/lib/rules/no-extra-semi.js index e451b9e42e..679a16641b 100644 --- a/tools/eslint/lib/rules/no-extra-semi.js +++ b/tools/eslint/lib/rules/no-extra-semi.js @@ -22,6 +22,7 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); /** * Reports an unnecessary semicolon error. @@ -48,7 +49,7 @@ module.exports = { function checkForPartOfClassBody(firstToken) { for (var token = firstToken; token.type === "Punctuator" && token.value !== "}"; - token = context.getTokenAfter(token) + token = sourceCode.getTokenAfter(token) ) { if (token.value === ";") { report(token); @@ -65,7 +66,16 @@ module.exports = { */ EmptyStatement: function(node) { var parent = node.parent, - allowedParentTypes = ["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement"]; + allowedParentTypes = [ + "ForStatement", + "ForInStatement", + "ForOfStatement", + "WhileStatement", + "DoWhileStatement", + "IfStatement", + "LabeledStatement", + "WithStatement" + ]; if (allowedParentTypes.indexOf(parent.type) === -1) { report(node); @@ -78,7 +88,7 @@ module.exports = { * @returns {void} */ ClassBody: function(node) { - checkForPartOfClassBody(context.getFirstToken(node, 1)); // 0 is `{`. + checkForPartOfClassBody(sourceCode.getFirstToken(node, 1)); // 0 is `{`. }, /** @@ -87,7 +97,7 @@ module.exports = { * @returns {void} */ MethodDefinition: function(node) { - checkForPartOfClassBody(context.getTokenAfter(node)); + checkForPartOfClassBody(sourceCode.getTokenAfter(node)); } }; diff --git a/tools/eslint/lib/rules/no-implicit-coercion.js b/tools/eslint/lib/rules/no-implicit-coercion.js index 058d9f3572..113c205855 100644 --- a/tools/eslint/lib/rules/no-implicit-coercion.js +++ b/tools/eslint/lib/rules/no-implicit-coercion.js @@ -179,6 +179,8 @@ module.exports = { var options = parseOptions(context.options[0]), operatorAllowed = false; + var sourceCode = context.getSourceCode(); + return { UnaryExpression: function(node) { @@ -188,7 +190,7 @@ module.exports = { context.report( node, "use `Boolean({{code}})` instead.", { - code: context.getSource(node.argument.argument) + code: sourceCode.getText(node.argument.argument) }); } @@ -198,7 +200,7 @@ module.exports = { context.report( node, "use `{{code}} !== -1` instead.", { - code: context.getSource(node.argument) + code: sourceCode.getText(node.argument) }); } @@ -208,7 +210,7 @@ module.exports = { context.report( node, "use `Number({{code}})` instead.", { - code: context.getSource(node.argument) + code: sourceCode.getText(node.argument) }); } }, @@ -224,7 +226,7 @@ module.exports = { context.report( node, "use `Number({{code}})` instead.", { - code: context.getSource(nonNumericOperand) + code: sourceCode.getText(nonNumericOperand) }); } @@ -234,7 +236,7 @@ module.exports = { context.report( node, "use `String({{code}})` instead.", { - code: context.getSource(getOtherOperand(node, "")) + code: sourceCode.getText(getOtherOperand(node, "")) }); } }, @@ -247,7 +249,7 @@ module.exports = { context.report( node, "use `{{code}} = String({{code}})` instead.", { - code: context.getSource(getOtherOperand(node, "")) + code: sourceCode.getText(getOtherOperand(node, "")) }); } } diff --git a/tools/eslint/lib/rules/no-inline-comments.js b/tools/eslint/lib/rules/no-inline-comments.js index 7835ed30fc..e313eac06f 100644 --- a/tools/eslint/lib/rules/no-inline-comments.js +++ b/tools/eslint/lib/rules/no-inline-comments.js @@ -22,6 +22,7 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); /** * Will check that comments are not on lines starting with or ending with code @@ -32,8 +33,8 @@ module.exports = { function testCodeAroundComment(node) { // Get the whole line and cut it off at the start of the comment - var startLine = String(context.getSourceLines()[node.loc.start.line - 1]); - var endLine = String(context.getSourceLines()[node.loc.end.line - 1]); + var startLine = String(sourceCode.lines[node.loc.start.line - 1]); + var endLine = String(sourceCode.lines[node.loc.end.line - 1]); var preamble = startLine.slice(0, node.loc.start.column).trim(); diff --git a/tools/eslint/lib/rules/no-irregular-whitespace.js b/tools/eslint/lib/rules/no-irregular-whitespace.js index 1dbea8f5a9..032dd96c11 100644 --- a/tools/eslint/lib/rules/no-irregular-whitespace.js +++ b/tools/eslint/lib/rules/no-irregular-whitespace.js @@ -7,6 +7,15 @@ "use strict"; //------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +var ALL_IRREGULARS = /[\f\v\u0085\u00A0\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\u2028\u2029]/; +var IRREGULAR_WHITESPACE = /[\f\v\u0085\u00A0\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mg; +var IRREGULAR_LINE_TERMINATORS = /[\u2028\u2029]/mg; +var LINE_BREAK = /\r\n|\r|\n|\u2028|\u2029/g; + +//------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -24,6 +33,15 @@ module.exports = { properties: { skipComments: { type: "boolean" + }, + skipStrings: { + type: "boolean" + }, + skipTemplates: { + type: "boolean" + }, + skipRegExps: { + type: "boolean" } }, additionalProperties: false @@ -33,9 +51,6 @@ module.exports = { create: function(context) { - var irregularWhitespace = /[\u0085\u00A0\ufeff\f\v\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mg, - irregularLineTerminators = /[\u2028\u2029]/mg; - // Module store of errors that we have found var errors = []; @@ -45,6 +60,11 @@ module.exports = { // Lookup the `skipComments` option, which defaults to `false`. var options = context.options[0] || {}; var skipComments = !!options.skipComments; + var skipStrings = options.skipStrings !== false; + var skipRegExps = !!options.skipRegExps; + var skipTemplates = !!options.skipTemplates; + + var sourceCode = context.getSourceCode(); /** * Removes errors that occur inside a string node @@ -75,10 +95,27 @@ module.exports = { * @private */ function removeInvalidNodeErrorsInIdentifierOrLiteral(node) { - if (typeof node.value === "string") { + var shouldCheckStrings = skipStrings && (typeof node.value === "string"); + var shouldCheckRegExps = skipRegExps && (node.value instanceof RegExp); + + if (shouldCheckStrings || shouldCheckRegExps) { // If we have irregular characters remove them from the errors list - if (node.raw.match(irregularWhitespace) || node.raw.match(irregularLineTerminators)) { + if (ALL_IRREGULARS.test(node.raw)) { + removeWhitespaceError(node); + } + } + } + + /** + * Checks template string literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors + * @param {ASTNode} node to check for matching errors. + * @returns {void} + * @private + */ + function removeInvalidNodeErrorsInTemplateLiteral(node) { + if (typeof node.value.raw === "string") { + if (ALL_IRREGULARS.test(node.value.raw)) { removeWhitespaceError(node); } } @@ -91,7 +128,7 @@ module.exports = { * @private */ function removeInvalidNodeErrorsInComment(node) { - if (node.value.match(irregularWhitespace) || node.value.match(irregularLineTerminators)) { + if (ALL_IRREGULARS.test(node.value)) { removeWhitespaceError(node); } } @@ -103,14 +140,14 @@ module.exports = { * @private */ function checkForIrregularWhitespace(node) { - var sourceLines = context.getSourceLines(); + var sourceLines = sourceCode.lines; sourceLines.forEach(function(sourceLine, lineIndex) { var lineNumber = lineIndex + 1, location, match; - while ((match = irregularWhitespace.exec(sourceLine)) !== null) { + while ((match = IRREGULAR_WHITESPACE.exec(sourceLine)) !== null) { location = { line: lineNumber, column: match.index @@ -128,15 +165,15 @@ module.exports = { * @private */ function checkForIrregularLineTerminators(node) { - var source = context.getSource(), - sourceLines = context.getSourceLines(), - linebreaks = source.match(/\r\n|\r|\n|\u2028|\u2029/g), + var source = sourceCode.getText(), + sourceLines = sourceCode.lines, + linebreaks = source.match(LINE_BREAK), lastLineIndex = -1, lineIndex, location, match; - while ((match = irregularLineTerminators.exec(source)) !== null) { + while ((match = IRREGULAR_LINE_TERMINATORS.exec(source)) !== null) { lineIndex = linebreaks.indexOf(match[0], lastLineIndex + 1) || 0; location = { @@ -166,8 +203,10 @@ module.exports = { */ function noop() {} - return { - Program: function(node) { + var nodes = {}; + + if (ALL_IRREGULARS.test(sourceCode.getText())) { + nodes.Program = function(node) { /* * As we can easily fire warnings for all white space issues with @@ -182,13 +221,14 @@ module.exports = { checkForIrregularWhitespace(node); checkForIrregularLineTerminators(node); - }, + }; - Identifier: removeInvalidNodeErrorsInIdentifierOrLiteral, - Literal: removeInvalidNodeErrorsInIdentifierOrLiteral, - LineComment: skipComments ? rememberCommentNode : noop, - BlockComment: skipComments ? rememberCommentNode : noop, - "Program:exit": function() { + nodes.Identifier = removeInvalidNodeErrorsInIdentifierOrLiteral; + nodes.Literal = removeInvalidNodeErrorsInIdentifierOrLiteral; + nodes.TemplateElement = skipTemplates ? removeInvalidNodeErrorsInTemplateLiteral : noop; + nodes.LineComment = skipComments ? rememberCommentNode : noop; + nodes.BlockComment = skipComments ? rememberCommentNode : noop; + nodes["Program:exit"] = function() { if (skipComments) { @@ -200,7 +240,11 @@ module.exports = { errors.forEach(function(error) { context.report.apply(context, error); }); - } - }; + }; + } else { + nodes.Program = noop; + } + + return nodes; } }; diff --git a/tools/eslint/lib/rules/no-loop-func.js b/tools/eslint/lib/rules/no-loop-func.js index 2b76093cce..247dc52cd4 100644 --- a/tools/eslint/lib/rules/no-loop-func.js +++ b/tools/eslint/lib/rules/no-loop-func.js @@ -73,7 +73,7 @@ function getContainingLoopNode(node) { * @returns {ASTNode} The most outer loop node. */ function getTopLoopNode(node, excludedNode) { - var retv = null; + var retv = node; var border = excludedNode ? excludedNode.range[1] : 0; while (node && node.range[0] >= border) { diff --git a/tools/eslint/lib/rules/no-mixed-operators.js b/tools/eslint/lib/rules/no-mixed-operators.js new file mode 100644 index 0000000000..9a8b1c3925 --- /dev/null +++ b/tools/eslint/lib/rules/no-mixed-operators.js @@ -0,0 +1,212 @@ +/** + * @fileoverview Rule to disallow mixed binary operators. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var astUtils = require("../ast-utils.js"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +var ARITHMETIC_OPERATORS = ["+", "-", "*", "/", "%", "**"]; +var BITWISE_OPERATORS = ["&", "|", "^", "~", "<<", ">>", ">>>"]; +var COMPARISON_OPERATORS = ["==", "!=", "===", "!==", ">", ">=", "<", "<="]; +var LOGICAL_OPERATORS = ["&&", "||"]; +var RELATIONAL_OPERATORS = ["in", "instanceof"]; +var ALL_OPERATORS = [].concat( + ARITHMETIC_OPERATORS, + BITWISE_OPERATORS, + COMPARISON_OPERATORS, + LOGICAL_OPERATORS, + RELATIONAL_OPERATORS +); +var DEFAULT_GROUPS = [ + ARITHMETIC_OPERATORS, + BITWISE_OPERATORS, + COMPARISON_OPERATORS, + LOGICAL_OPERATORS, + RELATIONAL_OPERATORS +]; +var TARGET_NODE_TYPE = /^(?:Binary|Logical)Expression$/; + +/** + * Normalizes options. + * + * @param {object|undefined} options - A options object to normalize. + * @returns {object} Normalized option object. + */ +function normalizeOptions(options) { + var hasGroups = (options && options.groups && options.groups.length > 0); + var groups = hasGroups ? options.groups : DEFAULT_GROUPS; + var allowSamePrecedence = (options && options.allowSamePrecedence) !== false; + + return { + groups: groups, + allowSamePrecedence: allowSamePrecedence + }; +} + +/** + * Checks whether any group which includes both given operator exists or not. + * + * @param {Array.<string[]>} groups - A list of groups to check. + * @param {string} left - An operator. + * @param {string} right - Another operator. + * @returns {boolean} `true` if such group existed. + */ +function includesBothInAGroup(groups, left, right) { + return groups.some(function(group) { + return group.indexOf(left) !== -1 && group.indexOf(right) !== -1; + }); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow mixed binary operators", + category: "Stylistic Issues", + recommended: false + }, + schema: [ + { + type: "object", + properties: { + groups: { + type: "array", + items: { + type: "array", + items: {enum: ALL_OPERATORS}, + minItems: 2, + uniqueItems: true + }, + uniqueItems: true + }, + allowSamePrecedence: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create: function(context) { + var sourceCode = context.getSourceCode(); + var options = normalizeOptions(context.options[0]); + + /** + * Checks whether a given node should be ignored by options or not. + * + * @param {ASTNode} node - A node to check. This is a BinaryExpression + * node or a LogicalExpression node. This parent node is one of + * them, too. + * @returns {boolean} `true` if the node should be ignored. + */ + function shouldIgnore(node) { + var a = node; + var b = node.parent; + + return ( + !includesBothInAGroup(options.groups, a.operator, b.operator) || + ( + options.allowSamePrecedence && + astUtils.getPrecedence(a) === astUtils.getPrecedence(b) + ) + ); + } + + /** + * Checks whether the operator of a given node is mixed with parent + * node's operator or not. + * + * @param {ASTNode} node - A node to check. This is a BinaryExpression + * node or a LogicalExpression node. This parent node is one of + * them, too. + * @returns {boolean} `true` if the node was mixed. + */ + function isMixedWithParent(node) { + return ( + node.operator !== node.parent.operator && + !astUtils.isParenthesised(sourceCode, node) + ); + } + + /** + * Gets the operator token of a given node. + * + * @param {ASTNode} node - A node to check. This is a BinaryExpression + * node or a LogicalExpression node. + * @returns {Token} The operator token of the node. + */ + function getOperatorToken(node) { + var token = sourceCode.getTokenAfter(node.left); + + while (token.value === ")") { + token = sourceCode.getTokenAfter(token); + } + + return token; + } + + /** + * Reports both the operator of a given node and the operator of the + * parent node. + * + * @param {ASTNode} node - A node to check. This is a BinaryExpression + * node or a LogicalExpression node. This parent node is one of + * them, too. + * @returns {void} + */ + function reportBothOperators(node) { + var parent = node.parent; + var left = (parent.left === node) ? node : parent; + var right = (parent.left !== node) ? node : parent; + var message = + "Unexpected mix of '" + left.operator + "' and '" + + right.operator + "'."; + + context.report({ + node: left, + loc: getOperatorToken(left).loc.start, + message: message + }); + context.report({ + node: right, + loc: getOperatorToken(right).loc.start, + message: message + }); + } + + /** + * Checks between the operator of this node and the operator of the + * parent node. + * + * @param {ASTNode} node - A node to check. + * @returns {void} + */ + function check(node) { + if (TARGET_NODE_TYPE.test(node.parent.type) && + isMixedWithParent(node) && + !shouldIgnore(node) + ) { + reportBothOperators(node); + } + } + + return { + BinaryExpression: check, + LogicalExpression: check + }; + } +}; diff --git a/tools/eslint/lib/rules/no-mixed-spaces-and-tabs.js b/tools/eslint/lib/rules/no-mixed-spaces-and-tabs.js index b3cdd38a60..74553f6511 100644 --- a/tools/eslint/lib/rules/no-mixed-spaces-and-tabs.js +++ b/tools/eslint/lib/rules/no-mixed-spaces-and-tabs.js @@ -24,6 +24,7 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); var smartTabs, ignoredLocs = []; @@ -86,8 +87,8 @@ module.exports = { */ var regex = /^(?=[\t ]*(\t | \t))/, match, - lines = context.getSourceLines(), - comments = context.getAllComments(); + lines = sourceCode.lines, + comments = sourceCode.getAllComments(); comments.forEach(function(comment) { ignoredLocs.push(comment.loc); diff --git a/tools/eslint/lib/rules/no-multi-spaces.js b/tools/eslint/lib/rules/no-multi-spaces.js index d6c0e4198d..2fd89ef4dc 100644 --- a/tools/eslint/lib/rules/no-multi-spaces.js +++ b/tools/eslint/lib/rules/no-multi-spaces.js @@ -95,8 +95,9 @@ module.exports = { return { Program: function() { - var source = context.getSource(), - allComments = context.getAllComments(), + var sourceCode = context.getSourceCode(), + source = sourceCode.getText(), + allComments = sourceCode.getAllComments(), pattern = /[^\n\r\u2028\u2029\t ].? {2,}/g, // note: repeating space token, previousToken, @@ -121,12 +122,12 @@ module.exports = { // do not flag anything inside of comments if (!isIndexInComment(pattern.lastIndex, allComments)) { - token = context.getTokenByRangeStart(pattern.lastIndex); + token = sourceCode.getTokenByRangeStart(pattern.lastIndex); if (token) { - previousToken = context.getTokenBefore(token); + previousToken = sourceCode.getTokenBefore(token); if (hasExceptions) { - parent = context.getNodeByRangeIndex(pattern.lastIndex - 1); + parent = sourceCode.getNodeByRangeIndex(pattern.lastIndex - 1); } if (!parent || !exceptions[parent.type]) { diff --git a/tools/eslint/lib/rules/no-multiple-empty-lines.js b/tools/eslint/lib/rules/no-multiple-empty-lines.js index 02a41fdbe3..7508164d38 100644 --- a/tools/eslint/lib/rules/no-multiple-empty-lines.js +++ b/tools/eslint/lib/rules/no-multiple-empty-lines.js @@ -17,6 +17,8 @@ module.exports = { recommended: false }, + fixable: "whitespace", + schema: [ { type: "object", @@ -52,10 +54,12 @@ module.exports = { if (context.options.length) { max = context.options[0].max; - maxEOF = context.options[0].maxEOF; - maxBOF = context.options[0].maxBOF; + maxEOF = typeof context.options[0].maxEOF !== "undefined" ? context.options[0].maxEOF : max; + maxBOF = typeof context.options[0].maxBOF !== "undefined" ? context.options[0].maxBOF : max; } + var sourceCode = context.getSourceCode(); + //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -73,22 +77,35 @@ module.exports = { }, "Program:exit": function checkBlankLines(node) { - var lines = context.getSourceLines(), - currentLocation = -1, - lastLocation, + var lines = sourceCode.lines, + fullLines = sourceCode.text.match(/.*(\r\n|\r|\n|\u2028|\u2029)/g) || [], + firstNonBlankLine = -1, + trimmedLines = [], + linesRangeStart = [], blankCounter = 0, + currentLocation, + lastLocation, location, firstOfEndingBlankLines, - firstNonBlankLine = -1, - trimmedLines = []; + diff, + fix, + rangeStart, + rangeEnd; + fix = function(fixer) { + return fixer.removeRange([rangeStart, rangeEnd]); + }; + + linesRangeStart.push(0); lines.forEach(function(str, i) { - var trimmed = str.trim(); + var length = i < fullLines.length ? fullLines[i].length : 0, + trimmed = str.trim(); if ((firstNonBlankLine === -1) && (trimmed !== "")) { firstNonBlankLine = i; } + linesRangeStart.push(linesRangeStart[linesRangeStart.length - 1] + length); trimmedLines.push(trimmed); }); @@ -120,9 +137,17 @@ module.exports = { // Aggregate and count blank lines if (firstNonBlankLine > maxBOF) { - context.report(node, 0, - "Too many blank lines at the beginning of file. Max of " + maxBOF + " allowed."); + diff = firstNonBlankLine - maxBOF; + rangeStart = linesRangeStart[firstNonBlankLine - diff]; + rangeEnd = linesRangeStart[firstNonBlankLine]; + context.report({ + node: node, + loc: node.loc.start, + message: "Too many blank lines at the beginning of file. Max of " + maxBOF + " allowed.", + fix: fix + }); } + currentLocation = firstNonBlankLine - 1; lastLocation = currentLocation; currentLocation = trimmedLines.indexOf("", currentLocation + 1); @@ -141,20 +166,29 @@ module.exports = { // within the file, not at the end if (blankCounter >= max) { + diff = blankCounter - max + 1; + rangeStart = linesRangeStart[location.line - diff]; + rangeEnd = linesRangeStart[location.line]; + context.report({ node: node, loc: location, - message: "More than " + max + " blank " + (max === 1 ? "line" : "lines") + " not allowed." + message: "More than " + max + " blank " + (max === 1 ? "line" : "lines") + " not allowed.", + fix: fix }); } } else { // inside the last blank lines if (blankCounter > maxEOF) { + diff = blankCounter - maxEOF + 1; + rangeStart = linesRangeStart[location.line - diff]; + rangeEnd = linesRangeStart[location.line - 1]; context.report({ node: node, loc: location, - message: "Too many blank lines at the end of file. Max of " + maxEOF + " allowed." + message: "Too many blank lines at the end of file. Max of " + maxEOF + " allowed.", + fix: fix }); } } diff --git a/tools/eslint/lib/rules/no-native-reassign.js b/tools/eslint/lib/rules/no-native-reassign.js index 7d45e277d5..8b75f022a0 100644 --- a/tools/eslint/lib/rules/no-native-reassign.js +++ b/tools/eslint/lib/rules/no-native-reassign.js @@ -1,5 +1,5 @@ /** - * @fileoverview Rule to flag when re-assigning native objects + * @fileoverview Rule to disallow assignments to native objects or read-only global variables * @author Ilya Volodin */ @@ -12,9 +12,9 @@ module.exports = { meta: { docs: { - description: "disallow reassigning native objects", + description: "disallow assignments to native objects or read-only global variables", category: "Best Practices", - recommended: false + recommended: true }, schema: [ @@ -55,14 +55,14 @@ module.exports = { ) { context.report({ node: identifier, - message: "{{name}} is a read-only native object.", + message: "Read-only global '{{name}}' should not be modified.", data: identifier }); } } /** - * Reports write references if a given variable is readonly builtin. + * Reports write references if a given variable is read-only builtin. * @param {Variable} variable - A variable to check. * @returns {void} */ diff --git a/tools/eslint/lib/rules/no-prototype-builtins.js b/tools/eslint/lib/rules/no-prototype-builtins.js new file mode 100644 index 0000000000..febb1459be --- /dev/null +++ b/tools/eslint/lib/rules/no-prototype-builtins.js @@ -0,0 +1,52 @@ +/** + * @fileoverview Rule to disallow use of Object.prototype builtins on objects + * @author Andrew Levine + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow calling some `Object.prototype` methods directly on objects", + category: "Possible Errors", + recommended: false + } + }, + + create: function(context) { + var DISALLOWED_PROPS = [ + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable" + ]; + + /** + * Reports if a disallowed property is used in a CallExpression + * @param {ASTNode} node The CallExpression node. + * @returns {void} + */ + function disallowBuiltIns(node) { + if (node.callee.type !== "MemberExpression" || node.callee.computed) { + return; + } + var propName = node.callee.property.name; + + if (DISALLOWED_PROPS.indexOf(propName) > -1) { + context.report({ + message: "Do not access Object.prototype method '{{prop}}' from target object.", + loc: node.callee.property.loc.start, + data: {prop: propName}, + node: node + }); + } + } + + return { + CallExpression: disallowBuiltIns + }; + } +}; diff --git a/tools/eslint/lib/rules/no-regex-spaces.js b/tools/eslint/lib/rules/no-regex-spaces.js index 0584131924..a2f1d48f71 100644 --- a/tools/eslint/lib/rules/no-regex-spaces.js +++ b/tools/eslint/lib/rules/no-regex-spaces.js @@ -12,7 +12,7 @@ module.exports = { meta: { docs: { - description: "disallow multiple spaces in regular expression literals", + description: "disallow multiple spaces in regular expressions", category: "Possible Errors", recommended: true }, @@ -21,24 +21,66 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); - return { + /** + * Validate regular expressions + * @param {ASTNode} node node to validate + * @param {string} value regular expression to validate + * @returns {void} + * @private + */ + function checkRegex(node, value) { + var multipleSpacesRegex = /( {2,})+?/, + regexResults = multipleSpacesRegex.exec(value); - Literal: function(node) { - var token = context.getFirstToken(node), - nodeType = token.type, - nodeValue = token.value, - multipleSpacesRegex = /( {2,})+?/, - regexResults; + if (regexResults !== null) { + context.report(node, "Spaces are hard to count. Use {" + regexResults[0].length + "}."); + } + } - if (nodeType === "RegularExpression") { - regexResults = multipleSpacesRegex.exec(nodeValue); + /** + * Validate regular expression literals + * @param {ASTNode} node node to validate + * @returns {void} + * @private + */ + function checkLiteral(node) { + var token = sourceCode.getFirstToken(node), + nodeType = token.type, + nodeValue = token.value; - if (regexResults !== null) { - context.report(node, "Spaces are hard to count. Use {" + regexResults[0].length + "}."); - } - } + if (nodeType === "RegularExpression") { + checkRegex(node, nodeValue); } + } + + /** + * Check if node is a string + * @param {ASTNode} node node to evaluate + * @returns {boolean} True if its a string + * @private + */ + function isString(node) { + return node && node.type === "Literal" && typeof node.value === "string"; + } + + /** + * Validate strings passed to the RegExp constructor + * @param {ASTNode} node node to validate + * @returns {void} + * @private + */ + function checkFunction(node) { + if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(node.arguments[0])) { + checkRegex(node, node.arguments[0].value); + } + } + + return { + Literal: checkLiteral, + CallExpression: checkFunction, + NewExpression: checkFunction }; } diff --git a/tools/eslint/lib/rules/no-return-assign.js b/tools/eslint/lib/rules/no-return-assign.js index 0d9e0b4b07..38fc1cb0eb 100644 --- a/tools/eslint/lib/rules/no-return-assign.js +++ b/tools/eslint/lib/rules/no-return-assign.js @@ -8,26 +8,19 @@ // Helpers //------------------------------------------------------------------------------ -/** - * Checks whether or not a node is an `AssignmentExpression`. - * @param {Node|null} node - A node to check. - * @returns {boolean} Whether or not the node is an `AssignmentExpression`. - */ -function isAssignment(node) { - return node && node.type === "AssignmentExpression"; -} +var SENTINEL_TYPE = /^(?:[a-zA-Z]+?Statement|ArrowFunctionExpression|FunctionExpression|ClassExpression)$/; /** * Checks whether or not a node is enclosed in parentheses. * @param {Node|null} node - A node to check. - * @param {RuleContext} context - The current context. + * @param {sourceCode} sourceCode - The ESLint SourceCode object. * @returns {boolean} Whether or not the node is enclosed in parentheses. */ -function isEnclosedInParens(node, context) { - var prevToken = context.getTokenBefore(node); - var nextToken = context.getTokenAfter(node); +function isEnclosedInParens(node, sourceCode) { + var prevToken = sourceCode.getTokenBefore(node); + var nextToken = sourceCode.getTokenAfter(node); - return prevToken.value === "(" && nextToken.value === ")"; + return prevToken && prevToken.value === "(" && nextToken && nextToken.value === ")"; } //------------------------------------------------------------------------------ @@ -51,32 +44,33 @@ module.exports = { create: function(context) { var always = (context.options[0] || "except-parens") !== "except-parens"; - - /** - * Check whether return statement contains assignment - * @param {ASTNode} nodeToCheck node to check - * @param {ASTNode} nodeToReport node to report - * @param {string} message message to report - * @returns {void} - * @private - */ - function checkForAssignInReturn(nodeToCheck, nodeToReport, message) { - if (isAssignment(nodeToCheck) && (always || !isEnclosedInParens(nodeToCheck, context))) { - context.report(nodeToReport, message); - } - } + var sourceCode = context.getSourceCode(); return { - ReturnStatement: function(node) { - var message = "Return statement should not contain assignment."; + AssignmentExpression: function(node) { + if (!always && isEnclosedInParens(node, sourceCode)) { + return; + } - checkForAssignInReturn(node.argument, node, message); - }, - ArrowFunctionExpression: function(node) { - if (node.body.type !== "BlockStatement") { - var message = "Arrow function should not return assignment."; + var parent = node.parent; + + // Find ReturnStatement or ArrowFunctionExpression in ancestors. + while (parent && !SENTINEL_TYPE.test(parent.type)) { + node = parent; + parent = parent.parent; + } - checkForAssignInReturn(node.body, node, message); + // Reports. + if (parent && parent.type === "ReturnStatement") { + context.report({ + node: parent, + message: "Return statement should not contain assignment." + }); + } else if (parent && parent.type === "ArrowFunctionExpression" && parent.body === node) { + context.report({ + node: parent, + message: "Arrow function should not return assignment." + }); } } }; diff --git a/tools/eslint/lib/rules/no-script-url.js b/tools/eslint/lib/rules/no-script-url.js index 0605cd8642..1985cf3b95 100644 --- a/tools/eslint/lib/rules/no-script-url.js +++ b/tools/eslint/lib/rules/no-script-url.js @@ -14,7 +14,7 @@ module.exports = { meta: { docs: { - description: "disallow `javascript", + description: "disallow `javascript:` urls", category: "Best Practices", recommended: false }, diff --git a/tools/eslint/lib/rules/no-sequences.js b/tools/eslint/lib/rules/no-sequences.js index ea20a4b955..b0d318c7d0 100644 --- a/tools/eslint/lib/rules/no-sequences.js +++ b/tools/eslint/lib/rules/no-sequences.js @@ -21,6 +21,7 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); /** * Parts of the grammar that are required to have parens. @@ -30,7 +31,8 @@ module.exports = { IfStatement: "test", SwitchStatement: "discriminant", WhileStatement: "test", - WithStatement: "object" + WithStatement: "object", + ArrowFunctionExpression: "body" // Omitting CallExpression - commas are parsed as argument separators // Omitting NewExpression - commas are parsed as argument separators @@ -55,8 +57,8 @@ module.exports = { * @returns {boolean} True if the node has a paren on each side. */ function isParenthesised(node) { - var previousToken = context.getTokenBefore(node), - nextToken = context.getTokenAfter(node); + var previousToken = sourceCode.getTokenBefore(node), + nextToken = sourceCode.getTokenAfter(node); return previousToken && nextToken && previousToken.value === "(" && previousToken.range[1] <= node.range[0] && @@ -69,8 +71,8 @@ module.exports = { * @returns {boolean} True if two parens surround the node on each side. */ function isParenthesisedTwice(node) { - var previousToken = context.getTokenBefore(node, 1), - nextToken = context.getTokenAfter(node, 1); + var previousToken = sourceCode.getTokenBefore(node, 1), + nextToken = sourceCode.getTokenAfter(node, 1); return isParenthesised(node) && previousToken && nextToken && previousToken.value === "(" && previousToken.range[1] <= node.range[0] && @@ -97,7 +99,7 @@ module.exports = { } } - var child = context.getTokenAfter(node.expressions[0]); + var child = sourceCode.getTokenAfter(node.expressions[0]); context.report(node, child.loc.start, "Unexpected use of comma operator."); } diff --git a/tools/eslint/lib/rules/no-unexpected-multiline.js b/tools/eslint/lib/rules/no-unexpected-multiline.js index c066673301..af0beb2c4d 100644 --- a/tools/eslint/lib/rules/no-unexpected-multiline.js +++ b/tools/eslint/lib/rules/no-unexpected-multiline.js @@ -24,6 +24,8 @@ module.exports = { var PROPERTY_MESSAGE = "Unexpected newline between object and [ of property access."; var TAGGED_TEMPLATE_MESSAGE = "Unexpected newline between template tag and template literal."; + var sourceCode = context.getSourceCode(); + /** * Check to see if there is a newline between the node and the following open bracket * line's expression @@ -34,12 +36,12 @@ module.exports = { */ function checkForBreakAfter(node, msg) { var nodeExpressionEnd = node; - var openParen = context.getTokenAfter(node); + var openParen = sourceCode.getTokenAfter(node); // Move along until the end of the wrapped expression while (openParen.value === ")") { nodeExpressionEnd = openParen; - openParen = context.getTokenAfter(nodeExpressionEnd); + openParen = sourceCode.getTokenAfter(nodeExpressionEnd); } if (openParen.loc.start.line !== nodeExpressionEnd.loc.end.line) { diff --git a/tools/eslint/lib/rules/no-unsafe-finally.js b/tools/eslint/lib/rules/no-unsafe-finally.js index 55ea2971f8..8c3815459c 100644 --- a/tools/eslint/lib/rules/no-unsafe-finally.js +++ b/tools/eslint/lib/rules/no-unsafe-finally.js @@ -9,7 +9,10 @@ // Helpers //------------------------------------------------------------------------------ -var SENTINEL_NODE_TYPE = /^(?:Program|(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression)$/; +var SENTINEL_NODE_TYPE_RETURN_THROW = /^(?:Program|(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression)$/; +var SENTINEL_NODE_TYPE_BREAK = /^(?:Program|(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|DoWhileStatement|WhileStatement|ForOfStatement|ForInStatement|ForStatement|SwitchStatement)$/; +var SENTINEL_NODE_TYPE_CONTINUE = /^(?:Program|(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|DoWhileStatement|WhileStatement|ForOfStatement|ForInStatement|ForStatement)$/; + //------------------------------------------------------------------------------ // Rule Definition @@ -18,9 +21,9 @@ var SENTINEL_NODE_TYPE = /^(?:Program|(?:Function|Class)(?:Declaration|Expressio module.exports = { meta: { docs: { - description: "disallow control flow statements in finally blocks", + description: "disallow control flow statements in `finally` blocks", category: "Possible Errors", - recommended: false + recommended: true } }, create: function(context) { @@ -39,11 +42,29 @@ module.exports = { * Climbs up the tree if the node is not a sentinel node * * @param {ASTNode} node - node to check. + * @param {String} label - label of the break or continue statement * @returns {Boolean} - return whether the node is a finally block or a sentinel node */ - function isInFinallyBlock(node) { - while (node && !SENTINEL_NODE_TYPE.test(node.type)) { + function isInFinallyBlock(node, label) { + var labelInside = false; + var sentinelNodeType; + + if (node.type === "BreakStatement" && !node.label) { + sentinelNodeType = SENTINEL_NODE_TYPE_BREAK; + } else if (node.type === "ContinueStatement") { + sentinelNodeType = SENTINEL_NODE_TYPE_CONTINUE; + } else { + sentinelNodeType = SENTINEL_NODE_TYPE_RETURN_THROW; + } + + while (node && !sentinelNodeType.test(node.type)) { + if (node.parent.label && label && (node.parent.label.name === label.name)) { + labelInside = true; + } if (isFinallyBlock(node)) { + if (label && labelInside) { + return false; + } return true; } node = node.parent; @@ -58,7 +79,7 @@ module.exports = { * @returns {void} */ function check(node) { - if (isInFinallyBlock(node)) { + if (isInFinallyBlock(node, node.label)) { context.report({ message: "Unsafe usage of " + node.type, node: node, diff --git a/tools/eslint/lib/rules/no-unused-vars.js b/tools/eslint/lib/rules/no-unused-vars.js index 89d43c7bfd..39b7703c3c 100644 --- a/tools/eslint/lib/rules/no-unused-vars.js +++ b/tools/eslint/lib/rules/no-unused-vars.js @@ -10,6 +10,7 @@ //------------------------------------------------------------------------------ var lodash = require("lodash"); +var astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ // Rule Definition @@ -95,6 +96,8 @@ module.exports = { // Helpers //-------------------------------------------------------------------------- + var STATEMENT_TYPE = /(?:Statement|Declaration)$/; + /** * Determines if a given variable is being exported from a module. * @param {Variable} variable - EScope variable object. @@ -124,7 +127,7 @@ module.exports = { /** * Determines if a reference is a read operation. * @param {Reference} ref - An escope Reference - * @returns {Boolean} whether the given reference represents a read operation + * @returns {boolean} whether the given reference represents a read operation * @private */ function isReadRef(ref) { @@ -153,10 +156,203 @@ module.exports = { } /** + * Checks the position of given nodes. + * + * @param {ASTNode} inner - A node which is expected as inside. + * @param {ASTNode} outer - A node which is expected as outside. + * @returns {boolean} `true` if the `inner` node exists in the `outer` node. + */ + function isInside(inner, outer) { + return ( + inner.range[0] >= outer.range[0] && + inner.range[1] <= outer.range[1] + ); + } + + /** + * If a given reference is left-hand side of an assignment, this gets + * the right-hand side node of the assignment. + * + * @param {escope.Reference} ref - A reference to check. + * @param {ASTNode} prevRhsNode - The previous RHS node. + * @returns {ASTNode} The RHS node. + */ + function getRhsNode(ref, prevRhsNode) { + var id = ref.identifier; + var parent = id.parent; + var granpa = parent.parent; + var refScope = ref.from.variableScope; + var varScope = ref.resolved.scope.variableScope; + var canBeUsedLater = refScope !== varScope; + + /* + * Inherits the previous node if this reference is in the node. + * This is for `a = a + a`-like code. + */ + if (prevRhsNode && isInside(id, prevRhsNode)) { + return prevRhsNode; + } + + if (parent.type === "AssignmentExpression" && + granpa.type === "ExpressionStatement" && + id === parent.left && + !canBeUsedLater + ) { + return parent.right; + } + return null; + } + + /** + * Checks whether a given function node is stored to somewhere or not. + * If the function node is stored, the function can be used later. + * + * @param {ASTNode} funcNode - A function node to check. + * @param {ASTNode} rhsNode - The RHS node of the previous assignment. + * @returns {boolean} `true` if under the following conditions: + * - the funcNode is assigned to a variable. + * - the funcNode is bound as an argument of a function call. + * - the function is bound to a property and the object satisfies above conditions. + */ + function isStorableFunction(funcNode, rhsNode) { + var node = funcNode; + var parent = funcNode.parent; + + while (parent && isInside(parent, rhsNode)) { + switch (parent.type) { + case "SequenceExpression": + if (parent.expressions[parent.expressions.length - 1] !== node) { + return false; + } + break; + + case "CallExpression": + case "NewExpression": + return parent.callee !== node; + + case "AssignmentExpression": + case "TaggedTemplateExpression": + case "YieldExpression": + return true; + + default: + if (STATEMENT_TYPE.test(parent.type)) { + + /* + * If it encountered statements, this is a complex pattern. + * Since analyzeing complex patterns is hard, this returns `true` to avoid false positive. + */ + return true; + } + } + + node = parent; + parent = parent.parent; + } + + return false; + } + + /** + * Checks whether a given Identifier node exists inside of a function node which can be used later. + * + * "can be used later" means: + * - the function is assigned to a variable. + * - the function is bound to a property and the object can be used later. + * - the function is bound as an argument of a function call. + * + * If a reference exists in a function which can be used later, the reference is read when the function is called. + * + * @param {ASTNode} id - An Identifier node to check. + * @param {ASTNode} rhsNode - The RHS node of the previous assignment. + * @returns {boolean} `true` if the `id` node exists inside of a function node which can be used later. + */ + function isInsideOfStorableFunction(id, rhsNode) { + var funcNode = astUtils.getUpperFunction(id); + + return ( + funcNode && + isInside(funcNode, rhsNode) && + isStorableFunction(funcNode, rhsNode) + ); + } + + /** + * Checks whether a given reference is a read to update itself or not. + * + * @param {escope.Reference} ref - A reference to check. + * @param {ASTNode} rhsNode - The RHS node of the previous assignment. + * @returns {boolean} The reference is a read to update itself. + */ + function isReadForItself(ref, rhsNode) { + var id = ref.identifier; + var parent = id.parent; + var granpa = parent.parent; + + return ref.isRead() && ( + + // self update. e.g. `a += 1`, `a++` + ( + parent.type === "AssignmentExpression" && + granpa.type === "ExpressionStatement" && + parent.left === id + ) || + ( + parent.type === "UpdateExpression" && + granpa.type === "ExpressionStatement" + ) || + + // in RHS of an assignment for itself. e.g. `a = a + 1` + ( + rhsNode && + isInside(id, rhsNode) && + !isInsideOfStorableFunction(id, rhsNode) + ) + ); + } + + /** + * Determine if an identifier is used either in for-in loops. + * + * @param {Reference} ref - The reference to check. + * @returns {boolean} whether reference is used in the for-in loops + * @private + */ + function isForInRef(ref) { + var target = ref.identifier.parent; + + + // "for (var ...) { return; }" + if (target.type === "VariableDeclarator") { + target = target.parent.parent; + } + + if (target.type !== "ForInStatement") { + return false; + } + + // "for (...) { return; }" + if (target.body.type === "BlockStatement") { + target = target.body.body[0]; + + // "for (...) return;" + } else { + target = target.body; + } + + // For empty loop body + if (!target) { + return false; + } + + return target.type === "ReturnStatement"; + } + + /** * Determines if the variable is used. * @param {Variable} variable - The variable to check. - * @param {Reference[]} references - The variable references to check. * @returns {boolean} True if the variable is used + * @private */ function isUsedVariable(variable) { var functionNodes = variable.defs.filter(function(def) { @@ -164,10 +360,23 @@ module.exports = { }).map(function(def) { return def.node; }), - isFunctionDefinition = functionNodes.length > 0; + isFunctionDefinition = functionNodes.length > 0, + rhsNode = null; return variable.references.some(function(ref) { - return isReadRef(ref) && !(isFunctionDefinition && isSelfReference(ref, functionNodes)); + if (isForInRef(ref)) { + return true; + } + + var forItself = isReadForItself(ref, rhsNode); + + rhsNode = getRhsNode(ref, rhsNode); + + return ( + isReadRef(ref) && + !forItself && + !(isFunctionDefinition && isSelfReference(ref, functionNodes)) + ); }); } @@ -268,6 +477,7 @@ module.exports = { * @param {escope.Variable} variable - A variable to get. * @param {ASTNode} comment - A comment node which includes the variable name. * @returns {number} The index of the variable name's location. + * @private */ function getColumnInComment(variable, comment) { var namePattern = new RegExp("[\\s,]" + lodash.escapeRegExp(variable.name) + "(?:$|[\\s,:])", "g"); @@ -287,6 +497,7 @@ module.exports = { * * @param {escope.Variable} variable - A variable to get its location. * @returns {{line: number, column: number}} The location object for the variable. + * @private */ function getLocation(variable) { var comment = variable.eslintExplicitGlobalComment; diff --git a/tools/eslint/lib/rules/no-useless-call.js b/tools/eslint/lib/rules/no-useless-call.js index eb14f0baf9..49cbbc5401 100644 --- a/tools/eslint/lib/rules/no-useless-call.js +++ b/tools/eslint/lib/rules/no-useless-call.js @@ -32,12 +32,12 @@ function isCallOrNonVariadicApply(node) { * Checks whether or not the tokens of two given nodes are same. * @param {ASTNode} left - A node 1 to compare. * @param {ASTNode} right - A node 2 to compare. - * @param {RuleContext} context - The ESLint rule context object. + * @param {SourceCode} sourceCode - The ESLint source code object. * @returns {boolean} the source code for the given node. */ -function equalTokens(left, right, context) { - var tokensL = context.getTokens(left); - var tokensR = context.getTokens(right); +function equalTokens(left, right, sourceCode) { + var tokensL = sourceCode.getTokens(left); + var tokensR = sourceCode.getTokens(right); if (tokensL.length !== tokensR.length) { return false; @@ -57,14 +57,14 @@ function equalTokens(left, right, context) { * Checks whether or not `thisArg` is not changed by `.call()`/`.apply()`. * @param {ASTNode|null} expectedThis - The node that is the owner of the applied function. * @param {ASTNode} thisArg - The node that is given to the first argument of the `.call()`/`.apply()`. - * @param {RuleContext} context - The ESLint rule context object. + * @param {SourceCode} sourceCode - The ESLint source code object. * @returns {boolean} Whether or not `thisArg` is not changed by `.call()`/`.apply()`. */ -function isValidThisArg(expectedThis, thisArg, context) { +function isValidThisArg(expectedThis, thisArg, sourceCode) { if (!expectedThis) { return astUtils.isNullOrUndefined(thisArg); } - return equalTokens(expectedThis, thisArg, context); + return equalTokens(expectedThis, thisArg, sourceCode); } //------------------------------------------------------------------------------ @@ -83,6 +83,8 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); + return { CallExpression: function(node) { if (!isCallOrNonVariadicApply(node)) { @@ -93,7 +95,7 @@ module.exports = { var expectedThis = (applied.type === "MemberExpression") ? applied.object : null; var thisArg = node.arguments[0]; - if (isValidThisArg(expectedThis, thisArg, context)) { + if (isValidThisArg(expectedThis, thisArg, sourceCode)) { context.report( node, "unnecessary '.{{name}}()'.", diff --git a/tools/eslint/lib/rules/no-useless-computed-key.js b/tools/eslint/lib/rules/no-useless-computed-key.js index 0894fb7ca4..2e0ac18019 100644 --- a/tools/eslint/lib/rules/no-useless-computed-key.js +++ b/tools/eslint/lib/rules/no-useless-computed-key.js @@ -19,6 +19,8 @@ module.exports = { } }, create: function(context) { + var sourceCode = context.getSourceCode(); + return { Property: function(node) { if (!node.computed) { @@ -29,7 +31,7 @@ module.exports = { nodeType = typeof key.value; if (key.type === "Literal" && (nodeType === "string" || nodeType === "number")) { - context.report(node, MESSAGE_UNNECESSARY_COMPUTED, { property: context.getSource(key) }); + context.report(node, MESSAGE_UNNECESSARY_COMPUTED, { property: sourceCode.getText(key) }); } } }; diff --git a/tools/eslint/lib/rules/no-useless-concat.js b/tools/eslint/lib/rules/no-useless-concat.js index ce9589d488..8569d4276a 100644 --- a/tools/eslint/lib/rules/no-useless-concat.js +++ b/tools/eslint/lib/rules/no-useless-concat.js @@ -67,6 +67,8 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); + return { BinaryExpression: function(node) { @@ -85,10 +87,10 @@ module.exports = { ) { // move warning location to operator - var operatorToken = context.getTokenAfter(left); + var operatorToken = sourceCode.getTokenAfter(left); while (operatorToken.value !== "+") { - operatorToken = context.getTokenAfter(operatorToken); + operatorToken = sourceCode.getTokenAfter(operatorToken); } context.report( diff --git a/tools/eslint/lib/rules/no-useless-rename.js b/tools/eslint/lib/rules/no-useless-rename.js new file mode 100644 index 0000000000..ec2282784b --- /dev/null +++ b/tools/eslint/lib/rules/no-useless-rename.js @@ -0,0 +1,150 @@ +/** + * @fileoverview Disallow renaming import, export, and destructured assignments to the same name. + * @author Kai Cataldo + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "disallow renaming import, export, and destructured assignments to the same name", + category: "ECMAScript 6", + recommended: false + }, + fixable: "code", + schema: [ + { + type: "object", + properties: { + ignoreDestructuring: { type: "boolean" }, + ignoreImport: { type: "boolean" }, + ignoreExport: { type: "boolean" } + }, + additionalProperties: false + } + ] + }, + + create: function(context) { + var options = context.options[0] || {}, + ignoreDestructuring = options.ignoreDestructuring === true, + ignoreImport = options.ignoreImport === true, + ignoreExport = options.ignoreExport === true; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Reports error for unnecessarily renamed assignments + * @param {ASTNode} node - node to report + * @param {ASTNode} initial - node with initial name value + * @param {ASTNode} result - node with new name value + * @param {string} type - the type of the offending node + * @returns {void} + */ + function reportError(node, initial, result, type) { + var name = initial.type === "Identifier" ? initial.name : initial.value; + + return context.report({ + node: node, + message: "{{type}} {{name}} unnecessarily renamed.", + data: { + name: name, + type: type + }, + fix: function(fixer) { + return fixer.replaceTextRange([ + initial.range[0], + result.range[1] + ], name); + } + }); + } + + /** + * Checks whether a destructured assignment is unnecessarily renamed + * @param {ASTNode} node - node to check + * @returns {void} + */ + function checkDestructured(node) { + var properties, + i; + + if (ignoreDestructuring) { + return; + } + + properties = node.properties; + + for (i = 0; i < properties.length; i++) { + if (properties[i].shorthand) { + continue; + } + + /** + * If an ObjectPattern property is computed, we have no idea + * if a rename is useless or not. If an ObjectPattern property + * lacks a key, it is likely an ExperimentalRestProperty and + * so there is no "renaming" occurring here. + */ + if (properties[i].computed || !properties[i].key) { + continue; + } + + if (properties[i].key.type === "Identifier" && properties[i].key.name === properties[i].value.name || + properties[i].key.type === "Literal" && properties[i].key.value === properties[i].value.name) { + reportError(properties[i], properties[i].key, properties[i].value, "Destructuring assignment"); + } + } + } + + /** + * Checks whether an import is unnecessarily renamed + * @param {ASTNode} node - node to check + * @returns {void} + */ + function checkImport(node) { + if (ignoreImport) { + return; + } + + if (node.imported.name === node.local.name && + node.imported.range[0] !== node.local.range[0]) { + reportError(node, node.imported, node.local, "Import"); + } + } + + /** + * Checks whether an export is unnecessarily renamed + * @param {ASTNode} node - node to check + * @returns {void} + */ + function checkExport(node) { + if (ignoreExport) { + return; + } + + if (node.local.name === node.exported.name && + node.local.range[0] !== node.exported.range[0]) { + reportError(node, node.local, node.exported, "Export"); + } + + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + ObjectPattern: checkDestructured, + ImportSpecifier: checkImport, + ExportSpecifier: checkExport + }; + } +}; diff --git a/tools/eslint/lib/rules/object-curly-newline.js b/tools/eslint/lib/rules/object-curly-newline.js new file mode 100644 index 0000000000..aaa4af02a2 --- /dev/null +++ b/tools/eslint/lib/rules/object-curly-newline.js @@ -0,0 +1,209 @@ +/** + * @fileoverview Rule to require or disallow line breaks inside braces. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +// Schema objects. +var OPTION_VALUE = { + oneOf: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + multiline: { + type: "boolean" + }, + minProperties: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false, + minProperties: 1 + } + ] +}; + +/** + * Normalizes a given option value. + * + * @param {string|object|undefined} value - An option value to parse. + * @returns {{multiline: boolean, minProperties: number}} Normalized option object. + */ +function normalizeOptionValue(value) { + var multiline = false; + var minProperties = Number.POSITIVE_INFINITY; + + if (value) { + if (value === "always") { + minProperties = 0; + } else if (value === "never") { + minProperties = Number.POSITIVE_INFINITY; + } else { + multiline = Boolean(value.multiline); + minProperties = value.minProperties || Number.POSITIVE_INFINITY; + } + } else { + multiline = true; + } + + return {multiline: multiline, minProperties: minProperties}; +} + +/** + * Normalizes a given option value. + * + * @param {string|object|undefined} options - An option value to parse. + * @returns {{ObjectExpression: {multiline: boolean, minProperties: number}, ObjectPattern: {multiline: boolean, minProperties: number}}} Normalized option object. + */ +function normalizeOptions(options) { + if (options && (options.ObjectExpression || options.ObjectPattern)) { + return { + ObjectExpression: normalizeOptionValue(options.ObjectExpression), + ObjectPattern: normalizeOptionValue(options.ObjectPattern) + }; + } + + var value = normalizeOptionValue(options); + + return {ObjectExpression: value, ObjectPattern: value}; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent line breaks inside braces", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + oneOf: [ + OPTION_VALUE, + { + type: "object", + properties: { + ObjectExpression: OPTION_VALUE, + ObjectPattern: OPTION_VALUE + }, + additionalProperties: false, + minProperties: 1 + } + ] + } + ] + }, + + create: function(context) { + var sourceCode = context.getSourceCode(); + var normalizedOptions = normalizeOptions(context.options[0]); + + /** + * 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, minProperties: number}} options - An option object. + * @returns {void} + */ + function check(node) { + var options = normalizedOptions[node.type]; + var openBrace = sourceCode.getFirstToken(node); + var closeBrace = sourceCode.getLastToken(node); + var first = sourceCode.getTokenOrCommentAfter(openBrace); + var last = sourceCode.getTokenOrCommentBefore(closeBrace); + var needsLinebreaks = ( + node.properties.length >= options.minProperties || + ( + options.multiline && + node.properties.length > 0 && + first.loc.start.line !== last.loc.end.line + ) + ); + + /* + * Use tokens or comments to check multiline or not. + * But use only tokens to check whether line breaks are needed. + * This allows: + * var obj = { // eslint-disable-line foo + * a: 1 + * } + */ + first = sourceCode.getTokenAfter(openBrace); + last = sourceCode.getTokenBefore(closeBrace); + + if (needsLinebreaks) { + if (astUtils.isTokenOnSameLine(openBrace, first)) { + context.report({ + message: "Expected a line break after this open brace.", + node: node, + loc: openBrace.loc.start, + fix: function(fixer) { + return fixer.insertTextAfter(openBrace, "\n"); + } + }); + } + if (astUtils.isTokenOnSameLine(last, closeBrace)) { + context.report({ + message: "Expected a line break before this close brace.", + node: node, + loc: closeBrace.loc.start, + fix: function(fixer) { + return fixer.insertTextBefore(closeBrace, "\n"); + } + }); + } + } else { + if (!astUtils.isTokenOnSameLine(openBrace, first)) { + context.report({ + message: "Unexpected a line break after this open brace.", + node: node, + loc: openBrace.loc.start, + fix: function(fixer) { + return fixer.removeRange([ + openBrace.range[1], + first.range[0] + ]); + } + }); + } + if (!astUtils.isTokenOnSameLine(last, closeBrace)) { + context.report({ + message: "Unexpected a line break before this close brace.", + node: node, + loc: closeBrace.loc.start, + fix: function(fixer) { + return fixer.removeRange([ + last.range[1], + closeBrace.range[0] + ]); + } + }); + } + } + } + + return { + ObjectExpression: check, + ObjectPattern: check + }; + } +}; diff --git a/tools/eslint/lib/rules/object-curly-spacing.js b/tools/eslint/lib/rules/object-curly-spacing.js index 11224bbdd1..e5dfb8d036 100644 --- a/tools/eslint/lib/rules/object-curly-spacing.js +++ b/tools/eslint/lib/rules/object-curly-spacing.js @@ -171,7 +171,7 @@ module.exports = { closingCurlyBraceMustBeSpaced = ( options.arraysInObjectsException && penultimateType === "ArrayExpression" || - options.objectsInObjectsException && penultimateType === "ObjectExpression" + options.objectsInObjectsException && (penultimateType === "ObjectExpression" || penultimateType === "ObjectPattern") ) ? !options.spaced : options.spaced; lastSpaced = sourceCode.isSpaceBetweenTokens(penultimate, last); diff --git a/tools/eslint/lib/rules/object-property-newline.js b/tools/eslint/lib/rules/object-property-newline.js new file mode 100644 index 0000000000..eb96152bb1 --- /dev/null +++ b/tools/eslint/lib/rules/object-property-newline.js @@ -0,0 +1,73 @@ +/** + * @fileoverview Rule to enforce placing object properties on separate lines. + * @author Vitor Balocco + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce placing object properties on separate lines", + category: "Stylistic Issues", + recommended: false + }, + + schema: [ + { + type: "object", + properties: { + allowMultiplePropertiesPerLine: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create: function(context) { + var allowSameLine = context.options[0] && Boolean(context.options[0].allowMultiplePropertiesPerLine); + var errorMessage = allowSameLine ? + "Object properties must go on a new line if they aren't all on the same line" : + "Object properties must go on a new line"; + + var sourceCode = context.getSourceCode(); + + return { + ObjectExpression: function(node) { + var lastTokenOfPreviousProperty, firstTokenOfCurrentProperty; + + if (allowSameLine) { + if (node.properties.length > 1) { + var firstTokenOfFirstProperty = sourceCode.getFirstToken(node.properties[0]); + var lastTokenOfLastProperty = sourceCode.getLastToken(node.properties[node.properties.length - 1]); + + if (firstTokenOfFirstProperty.loc.end.line === lastTokenOfLastProperty.loc.start.line) { + + // All keys and values are on the same line + return; + } + } + } + + for (var i = 1; i < node.properties.length; i++) { + lastTokenOfPreviousProperty = sourceCode.getLastToken(node.properties[i - 1]); + firstTokenOfCurrentProperty = sourceCode.getFirstToken(node.properties[i]); + + if (lastTokenOfPreviousProperty.loc.end.line === firstTokenOfCurrentProperty.loc.start.line) { + context.report({ + node: node, + loc: firstTokenOfCurrentProperty.loc.start, + message: errorMessage + }); + } + } + } + }; + } +}; diff --git a/tools/eslint/lib/rules/object-shorthand.js b/tools/eslint/lib/rules/object-shorthand.js index 4c7c066bcd..a528c00d38 100644 --- a/tools/eslint/lib/rules/object-shorthand.js +++ b/tools/eslint/lib/rules/object-shorthand.js @@ -23,6 +23,8 @@ module.exports = { recommended: false }, + fixable: "code", + schema: { anyOf: [ { @@ -39,6 +41,25 @@ module.exports = { type: "array", items: [ { + enum: ["always", "methods", "properties"] + }, + { + type: "object", + properties: { + avoidQuotes: { + type: "boolean" + } + }, + additionalProperties: false + } + ], + minItems: 0, + maxItems: 2 + }, + { + type: "array", + items: [ + { enum: ["always", "methods"] }, { @@ -46,6 +67,9 @@ module.exports = { properties: { ignoreConstructors: { type: "boolean" + }, + avoidQuotes: { + type: "boolean" } }, additionalProperties: false @@ -66,6 +90,7 @@ module.exports = { var PARAMS = context.options[1] || {}; var IGNORE_CONSTRUCTORS = PARAMS.ignoreConstructors; + var AVOID_QUOTES = PARAMS.avoidQuotes; //-------------------------------------------------------------------------- // Helpers @@ -83,6 +108,15 @@ module.exports = { return firstChar === firstChar.toUpperCase(); } + /** + * Checks whether a node is a string literal. + * @param {ASTNode} node - Any AST node. + * @returns {boolean} `true` if it is a string literal. + */ + function isStringLiteral(node) { + return node.type === "Literal" && typeof node.value === "string"; + } + //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -97,45 +131,127 @@ module.exports = { return; } - // if we're "never" and concise we should warn now - if (APPLY_NEVER && isConciseProperty) { - type = node.method ? "method" : "property"; - context.report(node, "Expected longform " + type + " syntax."); - } - - // at this point if we're concise or if we're "never" we can leave - if (APPLY_NEVER || isConciseProperty) { + // getters and setters are ignored + if (node.kind === "get" || node.kind === "set") { return; } // only computed methods can fail the following checks - if (!APPLY_TO_METHODS && node.computed) { + if (node.computed && node.value.type !== "FunctionExpression") { return; } - // getters and setters are ignored - if (node.kind === "get" || node.kind === "set") { + //-------------------------------------------------------------- + // Checks for property/method shorthand. + if (isConciseProperty) { + + // if we're "never" and concise we should warn now + if (APPLY_NEVER) { + type = node.method ? "method" : "property"; + context.report({ + node: node, + message: "Expected longform " + type + " syntax.", + fix: function(fixer) { + if (node.method) { + if (node.value.generator) { + return fixer.replaceTextRange([node.range[0], node.key.range[1]], node.key.name + ": function*"); + } + + return fixer.insertTextAfter(node.key, ": function"); + } + + return fixer.insertTextAfter(node.key, ": " + node.key.name); + } + }); + } + + // {'xyz'() {}} should be written as {'xyz': function() {}} + if (AVOID_QUOTES && isStringLiteral(node.key)) { + context.report({ + node: node, + message: "Expected longform method syntax for string literal keys.", + fix: function(fixer) { + if (node.computed) { + return fixer.insertTextAfterRange([node.key.range[0], node.key.range[1] + 1], ": function"); + } + + return fixer.insertTextAfter(node.key, ": function"); + } + }); + } + return; } + //-------------------------------------------------------------- + // Checks for longform properties. if (node.value.type === "FunctionExpression" && !node.value.id && APPLY_TO_METHODS) { if (IGNORE_CONSTRUCTORS && isConstructor(node.key.name)) { return; } + if (AVOID_QUOTES && isStringLiteral(node.key)) { + return; + } + + // {[x]: function(){}} should be written as {[x]() {}} + if (node.computed) { + context.report({ + node: node, + message: "Expected method shorthand.", + fix: function(fixer) { + if (node.value.generator) { + return fixer.replaceTextRange( + [node.key.range[0], node.value.range[0] + "function*".length], + "*[" + node.key.name + "]" + ); + } + + return fixer.removeRange([node.key.range[1] + 1, node.value.range[0] + "function".length]); + } + }); + return; + } // {x: function(){}} should be written as {x() {}} - context.report(node, "Expected method shorthand."); + context.report({ + node: node, + message: "Expected method shorthand.", + fix: function(fixer) { + if (node.value.generator) { + return fixer.replaceTextRange( + [node.key.range[0], node.value.range[0] + "function*".length], + "*" + node.key.name + ); + } + + return fixer.removeRange([node.key.range[1], node.value.range[0] + "function".length]); + } + }); } else if (node.value.type === "Identifier" && node.key.name === node.value.name && APPLY_TO_PROPS) { // {x: x} should be written as {x} - context.report(node, "Expected property shorthand."); + context.report({ + node: node, + message: "Expected property shorthand.", + fix: function(fixer) { + return fixer.replaceText(node, node.value.name); + } + }); } else if (node.value.type === "Identifier" && node.key.type === "Literal" && node.key.value === node.value.name && APPLY_TO_PROPS) { + if (AVOID_QUOTES) { + return; + } // {"x": x} should be written as {x} - context.report(node, "Expected property shorthand."); + context.report({ + node: node, + message: "Expected property shorthand.", + fix: function(fixer) { + return fixer.replaceText(node, node.value.name); + } + }); } } }; - } }; diff --git a/tools/eslint/lib/rules/one-var.js b/tools/eslint/lib/rules/one-var.js index 805cec3654..2bd49f511f 100644 --- a/tools/eslint/lib/rules/one-var.js +++ b/tools/eslint/lib/rules/one-var.js @@ -286,6 +286,9 @@ module.exports = { context.report(node, "Combine this with the previous '" + type + "' statement with initialized variables."); } if (options[type].uninitialized === MODE_ALWAYS) { + if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) { + return; + } context.report(node, "Combine this with the previous '" + type + "' statement with uninitialized variables."); } } diff --git a/tools/eslint/lib/rules/operator-linebreak.js b/tools/eslint/lib/rules/operator-linebreak.js index 85f90b908f..8f17155b86 100644 --- a/tools/eslint/lib/rules/operator-linebreak.js +++ b/tools/eslint/lib/rules/operator-linebreak.js @@ -57,6 +57,8 @@ module.exports = { styleOverrides[":"] = "before"; } + var sourceCode = context.getSourceCode(); + //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- @@ -69,8 +71,8 @@ module.exports = { * @returns {void} */ function validateNode(node, leftSide) { - var leftToken = context.getLastToken(leftSide); - var operatorToken = context.getTokenAfter(leftToken); + var leftToken = sourceCode.getLastToken(leftSide); + var operatorToken = sourceCode.getTokenAfter(leftToken); // When the left part of a binary expression is a single expression wrapped in // parentheses (ex: `(a) + b`), leftToken will be the last token of the expression @@ -79,10 +81,10 @@ module.exports = { // should be the token right after that. while (operatorToken.value === ")") { leftToken = operatorToken; - operatorToken = context.getTokenAfter(operatorToken); + operatorToken = sourceCode.getTokenAfter(operatorToken); } - var rightToken = context.getTokenAfter(operatorToken); + var rightToken = sourceCode.getTokenAfter(operatorToken); var operator = operatorToken.value; var operatorStyleOverride = styleOverrides[operator]; var style = operatorStyleOverride || globalStyle; diff --git a/tools/eslint/lib/rules/padded-blocks.js b/tools/eslint/lib/rules/padded-blocks.js index 39a2b07c8d..7ec24c65d7 100644 --- a/tools/eslint/lib/rules/padded-blocks.js +++ b/tools/eslint/lib/rules/padded-blocks.js @@ -17,6 +17,8 @@ module.exports = { recommended: false }, + fixable: "whitespace", + schema: [ { oneOf: [ @@ -164,6 +166,9 @@ module.exports = { context.report({ node: node, loc: { line: openBrace.loc.start.line, column: openBrace.loc.start.column }, + fix: function(fixer) { + return fixer.insertTextAfter(openBrace, "\n"); + }, message: ALWAYS_MESSAGE }); } @@ -171,23 +176,36 @@ module.exports = { context.report({ node: node, loc: {line: closeBrace.loc.end.line, column: closeBrace.loc.end.column - 1 }, + fix: function(fixer) { + return fixer.insertTextBefore(closeBrace, "\n"); + }, message: ALWAYS_MESSAGE }); } } else { if (blockHasTopPadding) { + var nextToken = sourceCode.getTokenOrCommentAfter(openBrace); + context.report({ node: node, loc: { line: openBrace.loc.start.line, column: openBrace.loc.start.column }, + fix: function(fixer) { + return fixer.replaceTextRange([openBrace.end, nextToken.start - nextToken.loc.start.column], "\n"); + }, message: NEVER_MESSAGE }); } if (blockHasBottomPadding) { + var previousToken = sourceCode.getTokenOrCommentBefore(closeBrace); + context.report({ node: node, loc: {line: closeBrace.loc.end.line, column: closeBrace.loc.end.column - 1 }, - message: NEVER_MESSAGE + message: NEVER_MESSAGE, + fix: function(fixer) { + return fixer.replaceTextRange([previousToken.end, closeBrace.start - closeBrace.loc.start.column], "\n"); + } }); } } diff --git a/tools/eslint/lib/rules/prefer-const.js b/tools/eslint/lib/rules/prefer-const.js index 668453520f..7b8ac42519 100644 --- a/tools/eslint/lib/rules/prefer-const.js +++ b/tools/eslint/lib/rules/prefer-const.js @@ -10,6 +10,7 @@ //------------------------------------------------------------------------------ var Map = require("es6-map"); +var lodash = require("lodash"); //------------------------------------------------------------------------------ // Helpers @@ -62,45 +63,72 @@ function canBecomeVariableDeclaration(identifier) { } /** - * Gets the WriteReference of a given variable if the variable should be - * declared as const. + * Gets an identifier node of a given variable. + * + * If the initialization exists or one or more reading references exist before + * the first assignment, the identifier node is the node of the declaration. + * Otherwise, the identifier node is the node of the first assignment. + * + * If the variable should not change to const, this function returns null. + * - If the variable is reassigned. + * - If the variable is never initialized and assigned. + * - If the variable is initialized in a different scope from the declaration. + * - If the unique assignment of the variable cannot change to a declaration. * * @param {escope.Variable} variable - A variable to get. - * @returns {escope.Reference|null} The singular WriteReference or null. + * @param {boolean} ignoreReadBeforeAssign - + * The value of `ignoreReadBeforeAssign` option. + * @returns {ASTNode|null} + * An Identifier node if the variable should change to const. + * Otherwise, null. */ -function getWriteReferenceIfShouldBeConst(variable) { +function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) { if (variable.eslintUsed) { return null; } - // Finds the singular WriteReference. - var retv = null; + // Finds the unique WriteReference. + var writer = null; + var isReadBeforeInit = false; var references = variable.references; for (var i = 0; i < references.length; ++i) { var reference = references[i]; if (reference.isWrite()) { - var isReassigned = Boolean( - retv && retv.identifier !== reference.identifier + var isReassigned = ( + writer !== null && + writer.identifier !== reference.identifier ); if (isReassigned) { return null; } - retv = reference; + writer = reference; + + } else if (reference.isRead() && writer === null) { + if (ignoreReadBeforeAssign) { + return null; + } + isReadBeforeInit = true; } } - // Checks the writer is located in the same scope and can be modified to - // const. - var isSameScopeAndCanBecomeVariableDeclaration = Boolean( - retv && - retv.from === variable.scope && - canBecomeVariableDeclaration(retv.identifier) + // If the assignment is from a different scope, ignore it. + // If the assignment cannot change to a declaration, ignore it. + var shouldBeConst = ( + writer !== null && + writer.from === variable.scope && + canBecomeVariableDeclaration(writer.identifier) ); - return isSameScopeAndCanBecomeVariableDeclaration ? retv : null; + if (!shouldBeConst) { + return null; + } + if (isReadBeforeInit) { + return variable.defs[0].name; + } + return writer.identifier; } /** @@ -136,15 +164,17 @@ function getDestructuringHost(reference) { * destructuring. * * @param {escope.Variable[]} variables - Variables to group by destructuring. - * @returns {Map<ASTNode, (escope.Reference|null)[]>} Grouped references. + * @param {boolean} ignoreReadBeforeAssign - + * The value of `ignoreReadBeforeAssign` option. + * @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes. */ -function groupByDestructuring(variables) { - var writersMap = new Map(); +function groupByDestructuring(variables, ignoreReadBeforeAssign) { + var identifierMap = new Map(); for (var i = 0; i < variables.length; ++i) { var variable = variables[i]; var references = variable.references; - var writer = getWriteReferenceIfShouldBeConst(variable); + var identifier = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign); var prevId = null; for (var j = 0; j < references.length; ++j) { @@ -158,20 +188,38 @@ function groupByDestructuring(variables) { } prevId = id; - // Add the writer into the destructuring group. + // Add the identifier node into the destructuring group. var group = getDestructuringHost(reference); if (group) { - if (writersMap.has(group)) { - writersMap.get(group).push(writer); + if (identifierMap.has(group)) { + identifierMap.get(group).push(identifier); } else { - writersMap.set(group, [writer]); + identifierMap.set(group, [identifier]); } } } } - return writersMap; + return identifierMap; +} + +/** + * Finds the nearest parent of node with a given type. + * + * @param {ASTNode} node ā The node to search from. + * @param {string} type ā The type field of the parent node. + * @param {function} shouldStop ā a predicate that returns true if the traversal should stop, and false otherwise. + * @returns {ASTNode} The closest ancestor with the specified type; null if no such ancestor exists. + */ +function findUp(node, type, shouldStop) { + if (!node || shouldStop(node)) { + return null; + } + if (node.type === type) { + return node; + } + return findUp(node.parent, type, shouldStop); } //------------------------------------------------------------------------------ @@ -186,11 +234,14 @@ module.exports = { recommended: false }, + fixable: "code", + schema: [ { type: "object", properties: { - destructuring: {enum: ["any", "all"]} + destructuring: {enum: ["any", "all"]}, + ignoreReadBeforeAssign: {type: "boolean"} }, additionalProperties: false } @@ -200,22 +251,64 @@ module.exports = { create: function(context) { var options = context.options[0] || {}; var checkingMixedDestructuring = options.destructuring !== "all"; + var ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true; var variables = null; /** - * Reports a given reference. + * Reports a given Identifier node. * - * @param {escope.Reference} reference - A reference to report. + * @param {ASTNode} node - An Identifier node to report. * @returns {void} */ - function report(reference) { - var id = reference.identifier; + function report(node) { + var reportArgs = { + node: node, + message: "'{{name}}' is never reassigned. Use 'const' instead.", + data: node + }, + varDeclParent = findUp(node, "VariableDeclaration", function(parentNode) { + return lodash.endsWith(parentNode.type, "Statement"); + }), + isNormalVarDecl = (node.parent.parent.parent.type === "ForInStatement" || + node.parent.parent.parent.type === "ForOfStatement" || + node.parent.init), + + isDestructuringVarDecl = + + // {let {a} = obj} should be written as {const {a} = obj} + (node.parent.parent.type === "ObjectPattern" && + + // If options.destucturing is "all", then this warning will not occur unless + // every assignment in the destructuring should be const. In that case, it's safe + // to apply the fix. Otherwise, it's safe to apply the fix if there's only one + // assignment occurring. If there is more than one assignment and options.destructuring + // is not "all", then it's not clear how the developer would want to resolve the issue, + // so we should not attempt to do it programmatically. + (options.destructuring === "all" || node.parent.parent.properties.length === 1)) || + + // {let [a] = [1]} should be written as {const [a] = [1]} + (node.parent.type === "ArrayPattern" && + + // See note above about fixing multiple warnings at once. + (options.destructuring === "all" || node.parent.elements.length === 1)); + + if (varDeclParent && + (isNormalVarDecl || isDestructuringVarDecl) && + + // If there are multiple variable declarations, like {let a = 1, b = 2}, then + // do not attempt to fix if one of the declarations should be `const`. It's + // too hard to know how the developer would want to automatically resolve the issue. + varDeclParent.declarations.length === 1) { + + reportArgs.fix = function(fixer) { + return fixer.replaceTextRange( + [varDeclParent.start, varDeclParent.start + "let".length], + "const" + ); + }; + } - context.report({ - node: id, - message: "'{{name}}' is never reassigned, use 'const' instead.", - data: id - }); + context.report(reportArgs); } /** @@ -225,30 +318,30 @@ module.exports = { * @returns {void} */ function checkVariable(variable) { - var writer = getWriteReferenceIfShouldBeConst(variable); + var node = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign); - if (writer) { - report(writer); + if (node) { + report(node); } } /** - * Reports given references if all of the reference should be declared as - * const. + * Reports given identifier nodes if all of the nodes should be declared + * as const. * - * The argument 'writers' is an array of references. - * This reference is the result of - * 'getWriteReferenceIfShouldBeConst(variable)', so it's nullable. - * In simple declaration or assignment cases, the length of the array is 1. - * In destructuring cases, the length of the array can be 2 or more. + * The argument 'nodes' is an array of Identifier nodes. + * This node is the result of 'getIdentifierIfShouldBeConst()', so it's + * nullable. In simple declaration or assignment cases, the length of + * the array is 1. In destructuring cases, the length of the array can + * be 2 or more. * - * @param {(escope.Reference|null)[]} writers - References which are grouped - * by destructuring to report. + * @param {(escope.Reference|null)[]} nodes - + * References which are grouped by destructuring to report. * @returns {void} */ - function checkGroup(writers) { - if (writers.every(Boolean)) { - writers.forEach(report); + function checkGroup(nodes) { + if (nodes.every(Boolean)) { + nodes.forEach(report); } } @@ -261,7 +354,8 @@ module.exports = { if (checkingMixedDestructuring) { variables.forEach(checkVariable); } else { - groupByDestructuring(variables).forEach(checkGroup); + groupByDestructuring(variables, ignoreReadBeforeAssign) + .forEach(checkGroup); } variables = null; diff --git a/tools/eslint/lib/rules/prefer-spread.js b/tools/eslint/lib/rules/prefer-spread.js index 79c7eb0243..67f1e855b0 100644 --- a/tools/eslint/lib/rules/prefer-spread.js +++ b/tools/eslint/lib/rules/prefer-spread.js @@ -31,12 +31,12 @@ function isVariadicApplyCalling(node) { * Checks whether or not the tokens of two given nodes are same. * @param {ASTNode} left - A node 1 to compare. * @param {ASTNode} right - A node 2 to compare. - * @param {RuleContext} context - The ESLint rule context object. + * @param {SourceCode} sourceCode - The ESLint source code object. * @returns {boolean} the source code for the given node. */ -function equalTokens(left, right, context) { - var tokensL = context.getTokens(left); - var tokensR = context.getTokens(right); +function equalTokens(left, right, sourceCode) { + var tokensL = sourceCode.getTokens(left); + var tokensR = sourceCode.getTokens(right); if (tokensL.length !== tokensR.length) { return false; @@ -82,6 +82,8 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); + return { CallExpression: function(node) { if (!isVariadicApplyCalling(node)) { @@ -92,7 +94,7 @@ module.exports = { var expectedThis = (applied.type === "MemberExpression") ? applied.object : null; var thisArg = node.arguments[0]; - if (isValidThisArg(expectedThis, thisArg, context)) { + if (isValidThisArg(expectedThis, thisArg, sourceCode)) { context.report(node, "use the spread operator instead of the '.apply()'."); } } diff --git a/tools/eslint/lib/rules/require-yield.js b/tools/eslint/lib/rules/require-yield.js index 441d354ed8..cde7d8c2c4 100644 --- a/tools/eslint/lib/rules/require-yield.js +++ b/tools/eslint/lib/rules/require-yield.js @@ -14,7 +14,7 @@ module.exports = { docs: { description: "require generator functions to contain `yield`", category: "ECMAScript 6", - recommended: false + recommended: true }, schema: [] diff --git a/tools/eslint/lib/rules/rest-spread-spacing.js b/tools/eslint/lib/rules/rest-spread-spacing.js new file mode 100644 index 0000000000..7ffafa5319 --- /dev/null +++ b/tools/eslint/lib/rules/rest-spread-spacing.js @@ -0,0 +1,107 @@ +/** + * @fileoverview Enforce spacing between rest and spread operators and their expressions. + * @author Kai Cataldo + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce spacing between rest and spread operators and their expressions", + category: "ECMAScript 6", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + enum: ["always", "never"] + } + ] + }, + + create: function(context) { + var sourceCode = context.getSourceCode(), + alwaysSpace = context.options[0] === "always"; + + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** + * Checks whitespace between rest/spread operators and their expressions + * @param {ASTNode} node - The node to check + * @returns {void} + */ + function checkWhiteSpace(node) { + var operator = sourceCode.getFirstToken(node), + nextToken = sourceCode.getTokenAfter(operator), + hasWhitespace = sourceCode.isSpaceBetweenTokens(operator, nextToken), + type; + + switch (node.type) { + case "SpreadElement": + type = "spread"; + break; + case "RestElement": + type = "rest"; + break; + case "ExperimentalSpreadProperty": + type = "spread property"; + break; + case "ExperimentalRestProperty": + type = "rest property"; + break; + default: + return; + } + + if (alwaysSpace && !hasWhitespace) { + context.report({ + node: node, + loc: { + line: operator.loc.end.line, + column: operator.loc.end.column + }, + message: "Expected whitespace after {{type}} operator", + data: { + type: type + }, + fix: function(fixer) { + return fixer.replaceTextRange([operator.range[1], nextToken.range[0]], " "); + } + }); + } else if (!alwaysSpace && hasWhitespace) { + context.report({ + node: node, + loc: { + line: operator.loc.end.line, + column: operator.loc.end.column + }, + message: "Unexpected whitespace after {{type}} operator", + data: { + type: type + }, + fix: function(fixer) { + return fixer.removeRange([operator.range[1], nextToken.range[0]]); + } + }); + } + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + SpreadElement: checkWhiteSpace, + RestElement: checkWhiteSpace, + ExperimentalSpreadProperty: checkWhiteSpace, + ExperimentalRestProperty: checkWhiteSpace + }; + } +}; diff --git a/tools/eslint/lib/rules/semi-spacing.js b/tools/eslint/lib/rules/semi-spacing.js index ea43e9243d..830044d2f3 100644 --- a/tools/eslint/lib/rules/semi-spacing.js +++ b/tools/eslint/lib/rules/semi-spacing.js @@ -59,7 +59,7 @@ module.exports = { * @returns {boolean} True if the given token has leading space, false if not. */ function hasLeadingSpace(token) { - var tokenBefore = context.getTokenBefore(token); + var tokenBefore = sourceCode.getTokenBefore(token); return tokenBefore && astUtils.isTokenOnSameLine(tokenBefore, token) && sourceCode.isSpaceBetweenTokens(tokenBefore, token); } @@ -70,7 +70,7 @@ module.exports = { * @returns {boolean} True if the given token has trailing space, false if not. */ function hasTrailingSpace(token) { - var tokenAfter = context.getTokenAfter(token); + var tokenAfter = sourceCode.getTokenAfter(token); return tokenAfter && astUtils.isTokenOnSameLine(token, tokenAfter) && sourceCode.isSpaceBetweenTokens(token, tokenAfter); } @@ -81,7 +81,7 @@ module.exports = { * @returns {boolean} Whether or not the token is the last in its line. */ function isLastTokenInCurrentLine(token) { - var tokenAfter = context.getTokenAfter(token); + var tokenAfter = sourceCode.getTokenAfter(token); return !(tokenAfter && astUtils.isTokenOnSameLine(token, tokenAfter)); } @@ -92,7 +92,7 @@ module.exports = { * @returns {boolean} Whether or not the token is the first in its line. */ function isFirstTokenInCurrentLine(token) { - var tokenBefore = context.getTokenBefore(token); + var tokenBefore = sourceCode.getTokenBefore(token); return !(tokenBefore && astUtils.isTokenOnSameLine(token, tokenBefore)); } @@ -103,7 +103,7 @@ module.exports = { * @returns {boolean} Whether or not the next token of a given token is a closing parenthesis. */ function isBeforeClosingParen(token) { - var nextToken = context.getTokenAfter(token); + var nextToken = sourceCode.getTokenAfter(token); return ( nextToken && @@ -140,7 +140,7 @@ module.exports = { loc: location, message: "Unexpected whitespace before semicolon.", fix: function(fixer) { - var tokenBefore = context.getTokenBefore(token); + var tokenBefore = sourceCode.getTokenBefore(token); return fixer.removeRange([tokenBefore.range[1], token.range[0]]); } @@ -167,7 +167,7 @@ module.exports = { loc: location, message: "Unexpected whitespace after semicolon.", fix: function(fixer) { - var tokenAfter = context.getTokenAfter(token); + var tokenAfter = sourceCode.getTokenAfter(token); return fixer.removeRange([token.range[1], tokenAfter.range[0]]); } @@ -195,7 +195,7 @@ module.exports = { * @returns {void} */ function checkNode(node) { - var token = context.getLastToken(node); + var token = sourceCode.getLastToken(node); checkSemicolonSpacing(token, node); } @@ -210,11 +210,11 @@ module.exports = { ThrowStatement: checkNode, ForStatement: function(node) { if (node.init) { - checkSemicolonSpacing(context.getTokenAfter(node.init), node); + checkSemicolonSpacing(sourceCode.getTokenAfter(node.init), node); } if (node.test) { - checkSemicolonSpacing(context.getTokenAfter(node.test), node); + checkSemicolonSpacing(sourceCode.getTokenAfter(node.test), node); } } }; diff --git a/tools/eslint/lib/rules/semi.js b/tools/eslint/lib/rules/semi.js index e386084faf..d530725040 100644 --- a/tools/eslint/lib/rules/semi.js +++ b/tools/eslint/lib/rules/semi.js @@ -121,7 +121,7 @@ module.exports = { return false; } - nextToken = context.getTokenAfter(lastToken); + nextToken = sourceCode.getTokenAfter(lastToken); if (!nextToken) { return true; @@ -141,7 +141,7 @@ module.exports = { * @returns {boolean} whether the node is in a one-liner block statement. */ function isOneLinerBlock(node) { - var nextToken = context.getTokenAfter(node); + var nextToken = sourceCode.getTokenAfter(node); if (!nextToken || nextToken.value !== "}") { return false; @@ -159,7 +159,7 @@ module.exports = { * @returns {void} */ function checkForSemicolon(node) { - var lastToken = context.getLastToken(node); + var lastToken = sourceCode.getLastToken(node); if (never) { if (isUnnecessarySemicolon(lastToken)) { diff --git a/tools/eslint/lib/rules/space-before-blocks.js b/tools/eslint/lib/rules/space-before-blocks.js index 7fb9d5cddc..468b320447 100644 --- a/tools/eslint/lib/rules/space-before-blocks.js +++ b/tools/eslint/lib/rules/space-before-blocks.js @@ -81,7 +81,7 @@ module.exports = { * @returns {void} undefined. */ function checkPrecedingSpace(node) { - var precedingToken = context.getTokenBefore(node), + var precedingToken = sourceCode.getTokenBefore(node), hasSpace, parent, requireSpace; @@ -133,9 +133,9 @@ module.exports = { if (cases.length > 0) { firstCase = cases[0]; - openingBrace = context.getTokenBefore(firstCase); + openingBrace = sourceCode.getTokenBefore(firstCase); } else { - openingBrace = context.getLastToken(node, 1); + openingBrace = sourceCode.getLastToken(node, 1); } checkPrecedingSpace(openingBrace); diff --git a/tools/eslint/lib/rules/space-before-function-paren.js b/tools/eslint/lib/rules/space-before-function-paren.js index d96cb4a608..2d26e41e4a 100644 --- a/tools/eslint/lib/rules/space-before-function-paren.js +++ b/tools/eslint/lib/rules/space-before-function-paren.js @@ -106,7 +106,7 @@ module.exports = { while (rightToken.value !== "(") { rightToken = sourceCode.getTokenAfter(rightToken); } - leftToken = context.getTokenBefore(rightToken); + leftToken = sourceCode.getTokenBefore(rightToken); location = leftToken.loc.end; if (sourceCode.isSpaceBetweenTokens(leftToken, rightToken)) { diff --git a/tools/eslint/lib/rules/space-infix-ops.js b/tools/eslint/lib/rules/space-infix-ops.js index 862ff66fb5..bea82ba0b6 100644 --- a/tools/eslint/lib/rules/space-infix-ops.js +++ b/tools/eslint/lib/rules/space-infix-ops.js @@ -41,6 +41,8 @@ module.exports = { "?", ":", ",", "**" ]; + var sourceCode = context.getSourceCode(); + /** * Returns the first token which violates the rule * @param {ASTNode} left - The left node of the main node @@ -50,7 +52,7 @@ module.exports = { */ function getFirstNonSpacedToken(left, right) { var op, - tokens = context.getTokensBetween(left, right, 1); + tokens = sourceCode.getTokensBetween(left, right, 1); for (var i = 1, l = tokens.length - 1; i < l; ++i) { op = tokens[i]; @@ -78,8 +80,8 @@ module.exports = { loc: culpritToken.loc.start, message: "Infix operators must be spaced.", fix: function(fixer) { - var previousToken = context.getTokenBefore(culpritToken); - var afterToken = context.getTokenAfter(culpritToken); + var previousToken = sourceCode.getTokenBefore(culpritToken); + var afterToken = sourceCode.getTokenAfter(culpritToken); var fixString = ""; if (culpritToken.range[0] - previousToken.range[1] === 0) { @@ -107,7 +109,7 @@ module.exports = { var nonSpacedNode = getFirstNonSpacedToken(node.left, node.right); if (nonSpacedNode) { - if (!(int32Hint && context.getSource(node).substr(-2) === "|0")) { + if (!(int32Hint && sourceCode.getText(node).substr(-2) === "|0")) { report(node, nonSpacedNode); } } diff --git a/tools/eslint/lib/rules/space-unary-ops.js b/tools/eslint/lib/rules/space-unary-ops.js index 0bb92af1e6..fdb1c03e98 100644 --- a/tools/eslint/lib/rules/space-unary-ops.js +++ b/tools/eslint/lib/rules/space-unary-ops.js @@ -43,6 +43,8 @@ module.exports = { create: function(context) { var options = context.options && Array.isArray(context.options) && context.options[0] || { words: true, nonwords: false }; + var sourceCode = context.getSourceCode(); + //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- @@ -158,7 +160,7 @@ module.exports = { * @returns {void} */ function checkForSpacesAfterYield(node) { - var tokens = context.getFirstTokens(node, 3), + var tokens = sourceCode.getFirstTokens(node, 3), word = "yield"; if (!node.argument || node.delegate) { @@ -239,7 +241,7 @@ module.exports = { * @returns {void} */ function checkForSpaces(node) { - var tokens = context.getFirstTokens(node, 2), + var tokens = sourceCode.getFirstTokens(node, 2), firstToken = tokens[0], secondToken = tokens[1]; diff --git a/tools/eslint/lib/rules/strict.js b/tools/eslint/lib/rules/strict.js index 4097a32793..45021517c7 100644 --- a/tools/eslint/lib/rules/strict.js +++ b/tools/eslint/lib/rules/strict.js @@ -23,7 +23,9 @@ var messages = { unnecessary: "Unnecessary 'use strict' directive.", module: "'use strict' is unnecessary inside of modules.", implied: "'use strict' is unnecessary when implied strict mode is enabled.", - unnecessaryInClasses: "'use strict' is unnecessary inside of classes." + unnecessaryInClasses: "'use strict' is unnecessary inside of classes.", + nonSimpleParameterList: "'use strict' directive inside a function with non-simple parameter list throws a syntax error since ES2016.", + wrap: "Wrap this function in a function with 'use strict' directive." }; /** @@ -53,6 +55,26 @@ function getUseStrictDirectives(statements) { return directives; } +/** + * Checks whether a given parameter is a simple parameter. + * + * @param {ASTNode} node - A pattern node to check. + * @returns {boolean} `true` if the node is an Identifier node. + */ +function isSimpleParameter(node) { + return node.type === "Identifier"; +} + +/** + * Checks whether a given parameter list is a simple parameter list. + * + * @param {ASTNode[]} params - A parameter list to check. + * @returns {boolean} `true` if the every parameter is an Identifier node. + */ +function isSimpleParameterList(params) { + return params.every(isSimpleParameter); +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -136,7 +158,9 @@ module.exports = { isStrict = useStrictDirectives.length > 0; if (isStrict) { - if (isParentStrict) { + if (!isSimpleParameterList(node.params)) { + context.report(useStrictDirectives[0], messages.nonSimpleParameterList); + } else if (isParentStrict) { context.report(useStrictDirectives[0], messages.unnecessary); } else if (isInClass) { context.report(useStrictDirectives[0], messages.unnecessaryInClasses); @@ -144,7 +168,11 @@ module.exports = { reportAllExceptFirst(useStrictDirectives, messages.multiple); } else if (isParentGlobal) { - context.report(node, messages.function); + if (isSimpleParameterList(node.params)) { + context.report(node, messages.function); + } else { + context.report(node, messages.wrap); + } } scopes.push(isParentStrict || isStrict); @@ -172,8 +200,13 @@ module.exports = { if (mode === "function") { enterFunctionInFunctionMode(node, useStrictDirectives); - } else { - reportAll(useStrictDirectives, messages[mode]); + } else if (useStrictDirectives.length > 0) { + if (isSimpleParameterList(node.params)) { + reportAll(useStrictDirectives, messages[mode]); + } else { + context.report(useStrictDirectives[0], messages.nonSimpleParameterList); + reportAllExceptFirst(useStrictDirectives, messages.multiple); + } } } diff --git a/tools/eslint/lib/rules/unicode-bom.js b/tools/eslint/lib/rules/unicode-bom.js new file mode 100644 index 0000000000..a152b03ac9 --- /dev/null +++ b/tools/eslint/lib/rules/unicode-bom.js @@ -0,0 +1,66 @@ +/** + * @fileoverview Require or disallow Unicode BOM + * @author Andrew Johnston <https://github.com/ehjay> + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow Unicode byte order mark (BOM)", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + } + ] + }, + + create: function(context) { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + + Program: function checkUnicodeBOM(node) { + + var sourceCode = context.getSourceCode(), + location = {column: 0, line: 1}, + requireBOM = context.options[0] || "never"; + + if (!sourceCode.hasBOM && (requireBOM === "always")) { + context.report({ + node: node, + loc: location, + message: "Expected Unicode BOM (Byte Order Mark).", + fix: function(fixer) { + return fixer.insertTextBefore(node, "\uFEFF"); + } + }); + } else if (sourceCode.hasBOM && (requireBOM === "never")) { + context.report({ + node: node, + loc: location, + message: "Unexpected Unicode BOM (Byte Order Mark).", + fix: function(fixer) { + return fixer.removeRange([-1, 0]); + } + }); + } + } + + }; + + } +}; diff --git a/tools/eslint/lib/rules/valid-jsdoc.js b/tools/eslint/lib/rules/valid-jsdoc.js index e7d6fdeadf..65ed539d55 100644 --- a/tools/eslint/lib/rules/valid-jsdoc.js +++ b/tools/eslint/lib/rules/valid-jsdoc.js @@ -340,14 +340,14 @@ module.exports = { if (node.params) { node.params.forEach(function(param, i) { - var name = param.name; - if (param.type === "AssignmentPattern") { - name = param.left.name; + param = param.left; } + var name = param.name; + // TODO(nzakas): Figure out logical things to do with destructured, default, rest params - if (param.type === "Identifier" || param.type === "AssignmentPattern") { + if (param.type === "Identifier") { if (jsdocParams[i] && (name !== jsdocParams[i])) { context.report(jsdocNode, "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.", { name: name, diff --git a/tools/eslint/lib/rules/vars-on-top.js b/tools/eslint/lib/rules/vars-on-top.js index b44f77eb3f..25bef0411d 100644 --- a/tools/eslint/lib/rules/vars-on-top.js +++ b/tools/eslint/lib/rules/vars-on-top.js @@ -47,6 +47,23 @@ module.exports = { } /** + * Checks whether a given node is a variable declaration or not. + * + * @param {ASTNode} node - any node + * @returns {boolean} `true` if the node is a variable declaration. + */ + function isVariableDeclaration(node) { + return ( + node.type === "VariableDeclaration" || + ( + node.type === "ExportNamedDeclaration" && + node.declaration && + node.declaration.type === "VariableDeclaration" + ) + ); + } + + /** * Checks whether this variable is on top of the block body * @param {ASTNode} node - The node to check * @param {ASTNode[]} statements - collection of ASTNodes for the parent node block @@ -64,9 +81,7 @@ module.exports = { } for (; i < l; ++i) { - if (statements[i].type !== "VariableDeclaration" && - (statements[i].type !== "ExportNamedDeclaration" || - statements[i].declaration.type !== "VariableDeclaration")) { + if (!isVariableDeclaration(statements[i])) { return false; } if (statements[i] === node) { diff --git a/tools/eslint/lib/rules/wrap-iife.js b/tools/eslint/lib/rules/wrap-iife.js index 1dd1a0c5af..2f73699a42 100644 --- a/tools/eslint/lib/rules/wrap-iife.js +++ b/tools/eslint/lib/rules/wrap-iife.js @@ -28,6 +28,8 @@ module.exports = { var style = context.options[0] || "outside"; + var sourceCode = context.getSourceCode(); + /** * Check if the node is wrapped in () * @param {ASTNode} node node to evaluate @@ -35,8 +37,8 @@ module.exports = { * @private */ function wrapped(node) { - var previousToken = context.getTokenBefore(node), - nextToken = context.getTokenAfter(node); + var previousToken = sourceCode.getTokenBefore(node), + nextToken = sourceCode.getTokenAfter(node); return previousToken && previousToken.value === "(" && nextToken && nextToken.value === ")"; diff --git a/tools/eslint/lib/rules/wrap-regex.js b/tools/eslint/lib/rules/wrap-regex.js index 96df3304c8..1aed713bdd 100644 --- a/tools/eslint/lib/rules/wrap-regex.js +++ b/tools/eslint/lib/rules/wrap-regex.js @@ -21,18 +21,19 @@ module.exports = { }, create: function(context) { + var sourceCode = context.getSourceCode(); return { Literal: function(node) { - var token = context.getFirstToken(node), + var token = sourceCode.getFirstToken(node), nodeType = token.type, source, grandparent, ancestors; if (nodeType === "RegularExpression") { - source = context.getTokenBefore(node); + source = sourceCode.getTokenBefore(node); ancestors = context.getAncestors(); grandparent = ancestors[ancestors.length - 1]; diff --git a/tools/eslint/lib/rules/yoda.js b/tools/eslint/lib/rules/yoda.js index ce2709ec9a..0373e91a4a 100644 --- a/tools/eslint/lib/rules/yoda.js +++ b/tools/eslint/lib/rules/yoda.js @@ -151,6 +151,8 @@ module.exports = { var exceptRange = (context.options[1] && context.options[1].exceptRange); var onlyEquality = (context.options[1] && context.options[1].onlyEquality); + var sourceCode = context.getSourceCode(); + /** * Determines whether node represents a range test. * A range test is a "between" test like `(0 <= x && x < 1)` or an "outside" @@ -202,9 +204,9 @@ module.exports = { function isParenWrapped() { var tokenBefore, tokenAfter; - return ((tokenBefore = context.getTokenBefore(node)) && + return ((tokenBefore = sourceCode.getTokenBefore(node)) && tokenBefore.value === "(" && - (tokenAfter = context.getTokenAfter(node)) && + (tokenAfter = sourceCode.getTokenAfter(node)) && tokenAfter.value === ")"); } diff --git a/tools/eslint/lib/testers/rule-tester.js b/tools/eslint/lib/testers/rule-tester.js index 4485e0d5fc..2ee87eca6d 100644 --- a/tools/eslint/lib/testers/rule-tester.js +++ b/tools/eslint/lib/testers/rule-tester.js @@ -331,6 +331,21 @@ RuleTester.prototype = { } /** + * Check if the AST was changed + * @param {ASTNode} beforeAST AST node before running + * @param {ASTNode} afterAST AST node after running + * @returns {void} + * @private + */ + function assertASTDidntChange(beforeAST, afterAST) { + if (!lodash.isEqual(beforeAST, afterAST)) { + + // Not using directly to avoid performance problem in node 6.1.0. See #6111 + assert.deepEqual(beforeAST, afterAST, "Rule should not modify AST."); + } + } + + /** * Check if the template is valid or not * all valid cases go through this * @param {string} ruleName name of the rule @@ -345,11 +360,7 @@ RuleTester.prototype = { assert.equal(messages.length, 0, util.format("Should have no errors but had %d: %s", messages.length, util.inspect(messages))); - assert.deepEqual( - result.beforeAST, - result.afterAST, - "Rule should not modify AST." - ); + assertASTDidntChange(result.beforeAST, result.afterAST); } /** @@ -422,11 +433,7 @@ RuleTester.prototype = { } - assert.deepEqual( - result.beforeAST, - result.afterAST, - "Rule should not modify AST." - ); + assertASTDidntChange(result.beforeAST, result.afterAST); } /* diff --git a/tools/eslint/lib/util/glob-util.js b/tools/eslint/lib/util/glob-util.js index dadefbd966..1209dabd6a 100644 --- a/tools/eslint/lib/util/glob-util.js +++ b/tools/eslint/lib/util/glob-util.js @@ -124,10 +124,12 @@ function listFilesToProcess(globPatterns, options) { var ignored = false; var isSilentlyIgnored; + if (ignoredPaths.contains(filename, "default")) { + ignored = (options.ignore !== false) && shouldWarnIgnored; + isSilentlyIgnored = !shouldWarnIgnored; + } + if (options.ignore !== false) { - if (ignoredPaths.contains(filename, "default")) { - isSilentlyIgnored = true; - } if (ignoredPaths.contains(filename, "custom")) { if (shouldWarnIgnored) { ignored = true; @@ -135,10 +137,12 @@ function listFilesToProcess(globPatterns, options) { isSilentlyIgnored = true; } } - if (isSilentlyIgnored && !ignored) { - return; - } } + + if (isSilentlyIgnored && !ignored) { + return; + } + if (added[filename]) { return; } @@ -150,7 +154,8 @@ function listFilesToProcess(globPatterns, options) { ignoredPaths = new IgnoredPaths(options); globOptions = { nodir: true, - cwd: cwd + cwd: cwd, + ignore: ignoredPaths.getIgnoredFoldersGlobPatterns() }; debug("Creating list of files to process."); diff --git a/tools/eslint/lib/util/npm-util.js b/tools/eslint/lib/util/npm-util.js index fd081307fd..9f28dc2b7e 100644 --- a/tools/eslint/lib/util/npm-util.js +++ b/tools/eslint/lib/util/npm-util.js @@ -11,7 +11,8 @@ var fs = require("fs"), path = require("path"), - shell = require("shelljs"); + shell = require("shelljs"), + log = require("../logging"); //------------------------------------------------------------------------------ // Helpers @@ -69,11 +70,18 @@ function installSyncSaveDev(packages) { function check(packages, opt) { var deps = []; var pkgJson = (opt) ? findPackageJson(opt.startDir) : findPackageJson(); + var fileJson; if (!pkgJson) { throw new Error("Could not find a package.json file. Run 'npm init' to create one."); } - var fileJson = JSON.parse(fs.readFileSync(pkgJson, "utf8")); + + try { + fileJson = JSON.parse(fs.readFileSync(pkgJson, "utf8")); + } catch (e) { + log.info("Could not read package.json file. Please check that the file contains valid JSON."); + throw new Error(e); + } if (opt.devDependencies && typeof fileJson.devDependencies === "object") { deps = deps.concat(Object.keys(fileJson.devDependencies)); @@ -116,6 +124,16 @@ function checkDevDeps(packages) { return check(packages, {devDependencies: true}); } +/** + * Check whether package.json is found in current path. + * + * @param {string=} startDir Starting directory + * @returns {boolean} Whether a package.json is found in current path. + */ +function checkPackageJson(startDir) { + return !!findPackageJson(startDir); +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -123,5 +141,6 @@ function checkDevDeps(packages) { module.exports = { installSyncSaveDev: installSyncSaveDev, checkDeps: checkDeps, - checkDevDeps: checkDevDeps + checkDevDeps: checkDevDeps, + checkPackageJson: checkPackageJson }; diff --git a/tools/eslint/lib/util/path-util.js b/tools/eslint/lib/util/path-util.js index ddc0b60625..a199046bb7 100644 --- a/tools/eslint/lib/util/path-util.js +++ b/tools/eslint/lib/util/path-util.js @@ -8,8 +8,7 @@ // Requirements //------------------------------------------------------------------------------ -var path = require("path"), - isAbsolute = require("path-is-absolute"); +var path = require("path"); //------------------------------------------------------------------------------ // Private @@ -51,11 +50,11 @@ function convertPathToPosix(filepath) { function getRelativePath(filepath, baseDir) { var relativePath; - if (!isAbsolute(filepath)) { + if (!path.isAbsolute(filepath)) { filepath = path.resolve(filepath); } if (baseDir) { - if (!isAbsolute(baseDir)) { + if (!path.isAbsolute(baseDir)) { throw new Error("baseDir should be an absolute path"); } relativePath = path.relative(baseDir, filepath); diff --git a/tools/eslint/lib/util/source-code-fixer.js b/tools/eslint/lib/util/source-code-fixer.js index e8c440d7c2..042eff591f 100644 --- a/tools/eslint/lib/util/source-code-fixer.js +++ b/tools/eslint/lib/util/source-code-fixer.js @@ -85,11 +85,7 @@ SourceCodeFixer.applyFixes = function(sourceCode, messages) { // sort in reverse order of occurrence fixes.sort(function(a, b) { - if (a.fix.range[1] <= b.fix.range[0]) { - return 1; - } else { - return -1; - } + return b.fix.range[1] - a.fix.range[1] || b.fix.range[0] - a.fix.range[0]; }); // split into array of characters for easier manipulation |