diff options
Diffstat (limited to 'tools/eslint/lib')
46 files changed, 927 insertions, 570 deletions
diff --git a/tools/eslint/lib/ast-utils.js b/tools/eslint/lib/ast-utils.js index 47cc71990f..fc0471e9a4 100644 --- a/tools/eslint/lib/ast-utils.js +++ b/tools/eslint/lib/ast-utils.js @@ -1041,7 +1041,8 @@ module.exports = { } else if (parent.type === "Property" || parent.type === "MethodDefinition") { if (parent.kind === "constructor") { return "constructor"; - } else if (parent.kind === "get") { + } + if (parent.kind === "get") { tokens.push("getter"); } else if (parent.kind === "set") { tokens.push("setter"); diff --git a/tools/eslint/lib/cli-engine.js b/tools/eslint/lib/cli-engine.js index 38c49cb31d..5fc35ae0ac 100644 --- a/tools/eslint/lib/cli-engine.js +++ b/tools/eslint/lib/cli-engine.js @@ -240,8 +240,8 @@ function processFile(filename, configHelper, options, linter) { function createIgnoreResult(filePath, baseDir) { let message; const isHidden = /^\./.test(path.basename(filePath)); - const isInNodeModules = baseDir && /^node_modules/.test(path.relative(baseDir, filePath)); - const isInBowerComponents = baseDir && /^bower_components/.test(path.relative(baseDir, filePath)); + const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules"); + const isInBowerComponents = baseDir && path.relative(baseDir, filePath).startsWith("bower_components"); if (isHidden) { message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override."; diff --git a/tools/eslint/lib/cli.js b/tools/eslint/lib/cli.js index 0c8a7d62fb..6a5482bf9a 100644 --- a/tools/eslint/lib/cli.js +++ b/tools/eslint/lib/cli.js @@ -63,7 +63,7 @@ function translateOptions(cliOptions) { cache: cliOptions.cache, cacheFile: cliOptions.cacheFile, cacheLocation: cliOptions.cacheLocation, - fix: cliOptions.fix && (cliOptions.quiet ? quietFixPredicate : true), + fix: (cliOptions.fix || cliOptions.fixDryRun) && (cliOptions.quiet ? quietFixPredicate : true), allowInlineConfig: cliOptions.inlineConfig, reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives }; @@ -144,6 +144,8 @@ const cli = { const files = currentOptions._; + const useStdin = typeof text === "string"; + if (currentOptions.version) { // version from package.json log.info(`v${require("../package.json").version}`); @@ -152,7 +154,8 @@ const cli = { if (files.length) { log.error("The --print-config option must be used with exactly one file name."); return 1; - } else if (text) { + } + if (useStdin) { log.error("The --print-config option is not available for piped-in code."); return 1; } @@ -163,23 +166,27 @@ const cli = { log.info(JSON.stringify(fileConfig, null, " ")); return 0; - } else if (currentOptions.help || (!files.length && !text)) { + } else if (currentOptions.help || (!files.length && !useStdin)) { log.info(options.generateHelp()); } else { - debug(`Running on ${text ? "text" : "files"}`); + debug(`Running on ${useStdin ? "text" : "files"}`); + + if (currentOptions.fix && currentOptions.fixDryRun) { + log.error("The --fix option and the --fix-dry-run option cannot be used together."); + return 1; + } - // disable --fix for piped-in code until we know how to do it correctly - if (text && currentOptions.fix) { - log.error("The --fix option is not available for piped-in code."); + if (useStdin && currentOptions.fix) { + log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead."); return 1; } const engine = new CLIEngine(translateOptions(currentOptions)); - const report = text ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files); + const report = useStdin ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files); if (currentOptions.fix) { debug("Fix mode enabled - applying fixes"); diff --git a/tools/eslint/lib/formatters/html.js b/tools/eslint/lib/formatters/html.js index e61fdea6a9..d450f9dee2 100644 --- a/tools/eslint/lib/formatters/html.js +++ b/tools/eslint/lib/formatters/html.js @@ -51,7 +51,8 @@ function renderSummary(totalErrors, totalWarnings) { function renderColor(totalErrors, totalWarnings) { if (totalErrors !== 0) { return 2; - } else if (totalWarnings !== 0) { + } + if (totalWarnings !== 0) { return 1; } return 0; diff --git a/tools/eslint/lib/ignored-paths.js b/tools/eslint/lib/ignored-paths.js index 3b7a00e9cd..f43ce46422 100644 --- a/tools/eslint/lib/ignored-paths.js +++ b/tools/eslint/lib/ignored-paths.js @@ -184,7 +184,7 @@ class IgnoredPaths { addPattern(this.ig.default, pattern); }); } else { - throw new Error("Package.json eslintIgnore property requires an array of paths"); + throw new TypeError("Package.json eslintIgnore property requires an array of paths"); } } } diff --git a/tools/eslint/lib/internal-rules/.eslintrc.yml b/tools/eslint/lib/internal-rules/.eslintrc.yml deleted file mode 100644 index 22d3a30ce3..0000000000 --- a/tools/eslint/lib/internal-rules/.eslintrc.yml +++ /dev/null @@ -1,3 +0,0 @@ -rules: - internal-no-invalid-meta: "error" - internal-consistent-docs-description: "error" diff --git a/tools/eslint/lib/internal-rules/internal-consistent-docs-description.js b/tools/eslint/lib/internal-rules/internal-consistent-docs-description.js deleted file mode 100644 index 55e2a6c764..0000000000 --- a/tools/eslint/lib/internal-rules/internal-consistent-docs-description.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @fileoverview Internal rule to enforce meta.docs.description conventions. - * @author Vitor Balocco - */ - -"use strict"; - -const ALLOWED_FIRST_WORDS = [ - "enforce", - "require", - "disallow" -]; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Gets the property of the Object node passed in that has the name specified. - * - * @param {string} property Name of the property to return. - * @param {ASTNode} node The ObjectExpression node. - * @returns {ASTNode} The Property node or null if not found. - */ -function getPropertyFromObject(property, node) { - const properties = node.properties; - - for (let i = 0; i < properties.length; i++) { - if (properties[i].key.name === property) { - return properties[i]; - } - } - - return null; -} - -/** - * Verifies that the meta.docs.description property follows our internal conventions. - * - * @param {RuleContext} context The ESLint rule context. - * @param {ASTNode} exportsNode ObjectExpression node that the rule exports. - * @returns {void} - */ -function checkMetaDocsDescription(context, exportsNode) { - if (exportsNode.type !== "ObjectExpression") { - - // if the exported node is not the correct format, "internal-no-invalid-meta" will already report this. - return; - } - - const metaProperty = getPropertyFromObject("meta", exportsNode); - const metaDocs = metaProperty && getPropertyFromObject("docs", metaProperty.value); - const metaDocsDescription = metaDocs && getPropertyFromObject("description", metaDocs.value); - - if (!metaDocsDescription) { - - // if there is no `meta.docs.description` property, "internal-no-invalid-meta" will already report this. - return; - } - - const description = metaDocsDescription.value.value; - - if (typeof description !== "string") { - context.report({ - node: metaDocsDescription.value, - message: "`meta.docs.description` should be a string." - }); - return; - } - - if (description === "") { - context.report({ - node: metaDocsDescription.value, - message: "`meta.docs.description` should not be empty." - }); - return; - } - - if (description.indexOf(" ") === 0) { - context.report({ - node: metaDocsDescription.value, - message: "`meta.docs.description` should not start with whitespace." - }); - return; - } - - const firstWord = description.split(" ")[0]; - - if (ALLOWED_FIRST_WORDS.indexOf(firstWord) === -1) { - context.report({ - node: metaDocsDescription.value, - message: "`meta.docs.description` should start with one of the following words: {{ allowedWords }}. Started with \"{{ firstWord }}\" instead.", - data: { - allowedWords: ALLOWED_FIRST_WORDS.join(", "), - firstWord - } - }); - } -} - -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ - -module.exports = { - meta: { - docs: { - description: "enforce correct conventions of `meta.docs.description` property in core rules", - category: "Internal", - recommended: false - }, - - schema: [] - }, - - create(context) { - return { - AssignmentExpression(node) { - if (node.left && - node.right && - node.left.type === "MemberExpression" && - node.left.object.name === "module" && - node.left.property.name === "exports") { - - checkMetaDocsDescription(context, node.right); - } - } - }; - } -}; diff --git a/tools/eslint/lib/internal-rules/internal-no-invalid-meta.js b/tools/eslint/lib/internal-rules/internal-no-invalid-meta.js deleted file mode 100644 index d13df358bf..0000000000 --- a/tools/eslint/lib/internal-rules/internal-no-invalid-meta.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @fileoverview Internal rule to prevent missing or invalid meta property in core rules. - * @author Vitor Balocco - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Gets the property of the Object node passed in that has the name specified. - * - * @param {string} property Name of the property to return. - * @param {ASTNode} node The ObjectExpression node. - * @returns {ASTNode} The Property node or null if not found. - */ -function getPropertyFromObject(property, node) { - const properties = node.properties; - - for (let i = 0; i < properties.length; i++) { - if (properties[i].key.name === property) { - return properties[i]; - } - } - - return null; -} - -/** - * Extracts the `meta` property from the ObjectExpression that all rules export. - * - * @param {ASTNode} exportsNode ObjectExpression node that the rule exports. - * @returns {ASTNode} The `meta` Property node or null if not found. - */ -function getMetaPropertyFromExportsNode(exportsNode) { - return getPropertyFromObject("meta", exportsNode); -} - -/** - * Whether this `meta` ObjectExpression has a `docs` property defined or not. - * - * @param {ASTNode} metaPropertyNode The `meta` ObjectExpression for this rule. - * @returns {boolean} `true` if a `docs` property exists. - */ -function hasMetaDocs(metaPropertyNode) { - return Boolean(getPropertyFromObject("docs", metaPropertyNode.value)); -} - -/** - * Whether this `meta` ObjectExpression has a `docs.description` property defined or not. - * - * @param {ASTNode} metaPropertyNode The `meta` ObjectExpression for this rule. - * @returns {boolean} `true` if a `docs.description` property exists. - */ -function hasMetaDocsDescription(metaPropertyNode) { - const metaDocs = getPropertyFromObject("docs", metaPropertyNode.value); - - return metaDocs && getPropertyFromObject("description", metaDocs.value); -} - -/** - * Whether this `meta` ObjectExpression has a `docs.category` property defined or not. - * - * @param {ASTNode} metaPropertyNode The `meta` ObjectExpression for this rule. - * @returns {boolean} `true` if a `docs.category` property exists. - */ -function hasMetaDocsCategory(metaPropertyNode) { - const metaDocs = getPropertyFromObject("docs", metaPropertyNode.value); - - return metaDocs && getPropertyFromObject("category", metaDocs.value); -} - -/** - * Whether this `meta` ObjectExpression has a `docs.recommended` property defined or not. - * - * @param {ASTNode} metaPropertyNode The `meta` ObjectExpression for this rule. - * @returns {boolean} `true` if a `docs.recommended` property exists. - */ -function hasMetaDocsRecommended(metaPropertyNode) { - const metaDocs = getPropertyFromObject("docs", metaPropertyNode.value); - - return metaDocs && getPropertyFromObject("recommended", metaDocs.value); -} - -/** - * Whether this `meta` ObjectExpression has a `schema` property defined or not. - * - * @param {ASTNode} metaPropertyNode The `meta` ObjectExpression for this rule. - * @returns {boolean} `true` if a `schema` property exists. - */ -function hasMetaSchema(metaPropertyNode) { - return getPropertyFromObject("schema", metaPropertyNode.value); -} - -/** - * Checks the validity of the meta definition of this rule and reports any errors found. - * - * @param {RuleContext} context The ESLint rule context. - * @param {ASTNode} exportsNode ObjectExpression node that the rule exports. - * @param {boolean} ruleIsFixable whether the rule is fixable or not. - * @returns {void} - */ -function checkMetaValidity(context, exportsNode) { - const metaProperty = getMetaPropertyFromExportsNode(exportsNode); - - if (!metaProperty) { - context.report(exportsNode, "Rule is missing a meta property."); - return; - } - - if (!hasMetaDocs(metaProperty)) { - context.report(metaProperty, "Rule is missing a meta.docs property."); - return; - } - - if (!hasMetaDocsDescription(metaProperty)) { - context.report(metaProperty, "Rule is missing a meta.docs.description property."); - return; - } - - if (!hasMetaDocsCategory(metaProperty)) { - context.report(metaProperty, "Rule is missing a meta.docs.category property."); - return; - } - - if (!hasMetaDocsRecommended(metaProperty)) { - context.report(metaProperty, "Rule is missing a meta.docs.recommended property."); - return; - } - - if (!hasMetaSchema(metaProperty)) { - context.report(metaProperty, "Rule is missing a meta.schema property."); - } -} - -/** - * Whether this node is the correct format for a rule definition or not. - * - * @param {ASTNode} node node that the rule exports. - * @returns {boolean} `true` if the exported node is the correct format for a rule definition - */ -function isCorrectExportsFormat(node) { - return node.type === "ObjectExpression"; -} - -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ - -module.exports = { - meta: { - docs: { - description: "enforce correct use of `meta` property in core rules", - category: "Internal", - recommended: false - }, - - schema: [] - }, - - create(context) { - let exportsNode; - - return { - AssignmentExpression(node) { - if (node.left && - node.right && - node.left.type === "MemberExpression" && - node.left.object.name === "module" && - node.left.property.name === "exports") { - - exportsNode = node.right; - } - }, - - "Program:exit"() { - if (!isCorrectExportsFormat(exportsNode)) { - context.report({ node: exportsNode, message: "Rule does not export an Object. Make sure the rule follows the new rule format." }); - return; - } - - checkMetaValidity(context, exportsNode); - } - }; - } -}; diff --git a/tools/eslint/lib/linter.js b/tools/eslint/lib/linter.js index e96e713ffd..089f0bb250 100755 --- a/tools/eslint/lib/linter.js +++ b/tools/eslint/lib/linter.js @@ -291,7 +291,7 @@ function createDisableDirectives(type, loc, value) { */ function modifyConfigsFromComments(filename, ast, config, linterContext) { - let commentConfig = { + const commentConfig = { exported: {}, astGlobals: {}, rules: {}, @@ -320,10 +320,6 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) { Object.assign(commentConfig.astGlobals, parseBooleanConfig(value, comment)); break; - case "eslint-env": - Object.assign(commentConfig.env, parseListConfig(value)); - break; - case "eslint-disable": [].push.apply(disableDirectives, createDisableDirectives("disable", comment.loc.start, value)); break; @@ -361,14 +357,6 @@ function modifyConfigsFromComments(filename, ast, config, linterContext) { } }); - // apply environment configs - Object.keys(commentConfig.env).forEach(name => { - const env = linterContext.environments.get(name); - - if (env) { - commentConfig = ConfigOps.merge(commentConfig, env); - } - }); Object.assign(commentConfig.rules, commentRules); return { diff --git a/tools/eslint/lib/options.js b/tools/eslint/lib/options.js index 3bc45b3ac7..ee1d3369ce 100644 --- a/tools/eslint/lib/options.js +++ b/tools/eslint/lib/options.js @@ -191,6 +191,12 @@ module.exports = optionator({ description: "Automatically fix problems" }, { + option: "fix-dry-run", + type: "Boolean", + default: false, + description: "Automatically fix problems without saving the changes to the file system" + }, + { option: "debug", type: "Boolean", default: false, diff --git a/tools/eslint/lib/rules/array-bracket-newline.js b/tools/eslint/lib/rules/array-bracket-newline.js index 319ac60f2c..5115ef4b56 100644 --- a/tools/eslint/lib/rules/array-bracket-newline.js +++ b/tools/eslint/lib/rules/array-bracket-newline.js @@ -23,7 +23,7 @@ module.exports = { { oneOf: [ { - enum: ["always", "never"] + enum: ["always", "never", "consistent"] }, { type: "object", @@ -58,11 +58,15 @@ module.exports = { * @returns {{multiline: boolean, minItems: number}} Normalized option object. */ function normalizeOptionValue(option) { + let consistent = false; let multiline = false; let minItems = 0; if (option) { - if (option === "always" || option.minItems === 0) { + if (option === "consistent") { + consistent = true; + minItems = Number.POSITIVE_INFINITY; + } else if (option === "always" || option.minItems === 0) { minItems = 0; } else if (option === "never") { minItems = Number.POSITIVE_INFINITY; @@ -71,11 +75,12 @@ module.exports = { minItems = option.minItems || Number.POSITIVE_INFINITY; } } else { + consistent = false; multiline = true; minItems = Number.POSITIVE_INFINITY; } - return { multiline, minItems }; + return { consistent, multiline, minItems }; } /** @@ -173,8 +178,7 @@ module.exports = { /** * Reports a given node if it violated this rule. * - * @param {ASTNode} node - A node to check. This is an ObjectExpression node or an ObjectPattern node. - * @param {{multiline: boolean, minItems: number}} options - An option object. + * @param {ASTNode} node - A node to check. This is an ArrayExpression node or an ArrayPattern node. * @returns {void} */ function check(node) { @@ -194,6 +198,16 @@ module.exports = { options.multiline && elements.length > 0 && firstIncComment.loc.start.line !== lastIncComment.loc.end.line + ) || + ( + elements.length === 0 && + firstIncComment.type === "Block" && + firstIncComment.loc.start.line !== lastIncComment.loc.end.line && + firstIncComment === lastIncComment + ) || + ( + options.consistent && + firstIncComment.loc.start.line !== openBracket.loc.end.line ) ); diff --git a/tools/eslint/lib/rules/block-spacing.js b/tools/eslint/lib/rules/block-spacing.js index f18381a310..b3ea8405e2 100644 --- a/tools/eslint/lib/rules/block-spacing.js +++ b/tools/eslint/lib/rules/block-spacing.js @@ -14,7 +14,7 @@ const util = require("../ast-utils"); module.exports = { meta: { docs: { - description: "enforce consistent spacing inside single-line blocks", + description: "disallow or enforce spaces inside of blocks after opening block and before closing block", category: "Stylistic Issues", recommended: false }, diff --git a/tools/eslint/lib/rules/callback-return.js b/tools/eslint/lib/rules/callback-return.js index 08600c01e5..0fa7f278a1 100644 --- a/tools/eslint/lib/rules/callback-return.js +++ b/tools/eslint/lib/rules/callback-return.js @@ -60,7 +60,8 @@ module.exports = { if (node.type === "MemberExpression") { if (node.object.type === "Identifier") { return true; - } else if (node.object.type === "MemberExpression") { + } + if (node.object.type === "MemberExpression") { return containsOnlyIdentifiers(node.object); } } diff --git a/tools/eslint/lib/rules/capitalized-comments.js b/tools/eslint/lib/rules/capitalized-comments.js index bb5e5825a3..1a27608067 100644 --- a/tools/eslint/lib/rules/capitalized-comments.js +++ b/tools/eslint/lib/rules/capitalized-comments.js @@ -247,7 +247,8 @@ module.exports = { if (capitalize === "always" && isLowercase) { return false; - } else if (capitalize === "never" && isUppercase) { + } + if (capitalize === "never" && isUppercase) { return false; } diff --git a/tools/eslint/lib/rules/comma-style.js b/tools/eslint/lib/rules/comma-style.js index 1a9382bea3..4c65338a44 100644 --- a/tools/eslint/lib/rules/comma-style.js +++ b/tools/eslint/lib/rules/comma-style.js @@ -208,7 +208,9 @@ module.exports = { if (item) { const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken); - previousItemToken = tokenAfterItem ? sourceCode.getTokenBefore(tokenAfterItem) : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1]; + previousItemToken = tokenAfterItem + ? sourceCode.getTokenBefore(tokenAfterItem) + : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1]; } }); diff --git a/tools/eslint/lib/rules/indent-legacy.js b/tools/eslint/lib/rules/indent-legacy.js index 5534944e62..a5db2bb5d7 100644 --- a/tools/eslint/lib/rules/indent-legacy.js +++ b/tools/eslint/lib/rules/indent-legacy.js @@ -733,7 +733,9 @@ module.exports = { } else if (parent.type === "ObjectExpression" || parent.type === "ArrayExpression") { const parentElements = node.parent.type === "ObjectExpression" ? node.parent.properties : node.parent.elements; - if (parentElements[0] && parentElements[0].loc.start.line === parent.loc.start.line && parentElements[0].loc.end.line !== parent.loc.start.line) { + if (parentElements[0] && + parentElements[0].loc.start.line === parent.loc.start.line && + parentElements[0].loc.end.line !== parent.loc.start.line) { /* * If the first element of the array spans multiple lines, don't increase the expected indentation of the rest. @@ -797,7 +799,8 @@ module.exports = { } } - checkLastNodeLineIndent(node, nodeIndent + (isNodeInVarOnTop(node, parentVarNode) ? options.VariableDeclarator[parentVarNode.parent.kind] * indentSize : 0)); + checkLastNodeLineIndent(node, nodeIndent + + (isNodeInVarOnTop(node, parentVarNode) ? options.VariableDeclarator[parentVarNode.parent.kind] * indentSize : 0)); } /** diff --git a/tools/eslint/lib/rules/indent.js b/tools/eslint/lib/rules/indent.js index 0f6468a7e1..a804f544ab 100644 --- a/tools/eslint/lib/rules/indent.js +++ b/tools/eslint/lib/rules/indent.js @@ -235,10 +235,12 @@ class OffsetStorage { /** * @param {TokenInfo} tokenInfo a TokenInfo instance * @param {number} indentSize The desired size of each indentation level + * @param {string} indentType The indentation character */ - constructor(tokenInfo, indentSize) { + constructor(tokenInfo, indentSize, indentType) { this._tokenInfo = tokenInfo; this._indentSize = indentSize; + this._indentType = indentType; this._tree = new BinarySearchTree(); this._tree.insert(0, { offset: 0, from: null, force: false }); @@ -408,7 +410,7 @@ class OffsetStorage { /** * Gets the desired indent of a token * @param {Token} token The token - * @returns {number} The desired indent of the token + * @returns {string} The desired indent of the token */ getDesiredIndent(token) { if (!this._desiredIndentCache.has(token)) { @@ -417,7 +419,10 @@ class OffsetStorage { // If the token is ignored, use the actual indent of the token as the desired indent. // This ensures that no errors are reported for this token. - this._desiredIndentCache.set(token, this._tokenInfo.getTokenIndent(token).length / this._indentSize); + this._desiredIndentCache.set( + token, + this._tokenInfo.getTokenIndent(token) + ); } else if (this._lockedFirstTokens.has(token)) { const firstToken = this._lockedFirstTokens.get(token); @@ -428,7 +433,7 @@ class OffsetStorage { this.getDesiredIndent(this._tokenInfo.getFirstTokenOfLine(firstToken)) + // (space between the start of the first element's line and the first element) - (firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column) / this._indentSize + this._indentType.repeat(firstToken.loc.start.column - this._tokenInfo.getFirstTokenOfLine(firstToken).loc.start.column) ); } else { const offsetInfo = this._getOffsetDescriptor(token); @@ -436,9 +441,12 @@ class OffsetStorage { offsetInfo.from && offsetInfo.from.loc.start.line === token.loc.start.line && !offsetInfo.force - ) ? 0 : offsetInfo.offset; + ) ? 0 : offsetInfo.offset * this._indentSize; - this._desiredIndentCache.set(token, offset + (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : 0)); + this._desiredIndentCache.set( + token, + (offsetInfo.from ? this.getDesiredIndent(offsetInfo.from) : "") + this._indentType.repeat(offset) + ); } } return this._desiredIndentCache.get(token); @@ -655,7 +663,7 @@ module.exports = { const sourceCode = context.getSourceCode(); const tokenInfo = new TokenInfo(sourceCode); - const offsets = new OffsetStorage(tokenInfo, indentSize); + const offsets = new OffsetStorage(tokenInfo, indentSize, indentType === "space" ? " " : "\t"); const parameterParens = new WeakSet(); /** @@ -688,27 +696,24 @@ module.exports = { /** * Reports a given indent violation * @param {Token} token Token violating the indent rule - * @param {int} neededIndentLevel Expected indentation level - * @param {int} gottenSpaces Actual number of indentation spaces for the token - * @param {int} gottenTabs Actual number of indentation tabs for the token + * @param {string} neededIndent Expected indentation string * @returns {void} */ - function report(token, neededIndentLevel) { + function report(token, neededIndent) { const actualIndent = Array.from(tokenInfo.getTokenIndent(token)); const numSpaces = actualIndent.filter(char => char === " ").length; const numTabs = actualIndent.filter(char => char === "\t").length; - const neededChars = neededIndentLevel * indentSize; context.report({ node: token, - message: createErrorMessage(neededChars, numSpaces, numTabs), + message: createErrorMessage(neededIndent.length, numSpaces, numTabs), loc: { start: { line: token.loc.start.line, column: 0 }, end: { line: token.loc.start.line, column: token.loc.start.column } }, fix(fixer) { const range = [token.range[0] - token.loc.start.column, token.range[0]]; - const newText = (indentType === "space" ? " " : "\t").repeat(neededChars); + const newText = neededIndent; return fixer.replaceTextRange(range, newText); } @@ -718,14 +723,13 @@ module.exports = { /** * Checks if a token's indentation is correct * @param {Token} token Token to examine - * @param {int} desiredIndentLevel needed indent level + * @param {string} desiredIndent Desired indentation of the string * @returns {boolean} `true` if the token's indentation is correct */ - function validateTokenIndent(token, desiredIndentLevel) { + function validateTokenIndent(token, desiredIndent) { const indentation = tokenInfo.getTokenIndent(token); - const expectedChar = indentType === "space" ? " " : "\t"; - return indentation === expectedChar.repeat(desiredIndentLevel * indentSize) || + return indentation === desiredIndent || // To avoid conflicts with no-mixed-spaces-and-tabs, don't report mixed spaces and tabs. indentation.includes(" ") && indentation.includes("\t"); @@ -1241,7 +1245,9 @@ module.exports = { NewExpression(node) { // Only indent the arguments if the NewExpression has parens (e.g. `new Foo(bar)` or `new Foo()`, but not `new Foo` - if (node.arguments.length > 0 || astUtils.isClosingParenToken(sourceCode.getLastToken(node)) && astUtils.isOpeningParenToken(sourceCode.getLastToken(node, 1))) { + if (node.arguments.length > 0 || + astUtils.isClosingParenToken(sourceCode.getLastToken(node)) && + astUtils.isOpeningParenToken(sourceCode.getLastToken(node, 1))) { addFunctionCallIndent(node); } }, diff --git a/tools/eslint/lib/rules/lines-around-comment.js b/tools/eslint/lib/rules/lines-around-comment.js index 5b4cd8ebe9..3df9cb86f4 100644 --- a/tools/eslint/lib/rules/lines-around-comment.js +++ b/tools/eslint/lib/rules/lines-around-comment.js @@ -82,6 +82,12 @@ module.exports = { allowBlockEnd: { type: "boolean" }, + allowClassStart: { + type: "boolean" + }, + allowClassEnd: { + type: "boolean" + }, allowObjectStart: { type: "boolean" }, @@ -225,6 +231,24 @@ module.exports = { } /** + * Returns whether or not comments are at the class start or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at class start. + */ + function isCommentAtClassStart(token) { + return isCommentAtParentStart(token, "ClassBody"); + } + + /** + * Returns whether or not comments are at the class end or not. + * @param {token} token The Comment token. + * @returns {boolean} True if the comment is at class end. + */ + function isCommentAtClassEnd(token) { + return isCommentAtParentEnd(token, "ClassBody"); + } + + /** * Returns whether or not comments are at the object start or not. * @param {token} token The Comment token. * @returns {boolean} True if the comment is at object start. @@ -284,15 +308,20 @@ module.exports = { nextLineNum = token.loc.end.line + 1, commentIsNotAlone = codeAroundComment(token); - const blockStartAllowed = options.allowBlockStart && isCommentAtBlockStart(token), - blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token), + const blockStartAllowed = options.allowBlockStart && + isCommentAtBlockStart(token) && + !(options.allowClassStart === false && + isCommentAtClassStart(token)), + blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)), + classStartAllowed = options.allowClassStart && isCommentAtClassStart(token), + classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token), objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token), objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token), arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token), arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token); - const exceptionStartAllowed = blockStartAllowed || objectStartAllowed || arrayStartAllowed; - const exceptionEndAllowed = blockEndAllowed || objectEndAllowed || arrayEndAllowed; + const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed; + const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed; // ignore top of the file and bottom of the file if (prevLineNum < 1) { diff --git a/tools/eslint/lib/rules/lines-between-class-members.js b/tools/eslint/lib/rules/lines-between-class-members.js new file mode 100644 index 0000000000..85e8c69358 --- /dev/null +++ b/tools/eslint/lib/rules/lines-between-class-members.js @@ -0,0 +1,91 @@ +/** + * @fileoverview Rule to check empty newline between class members + * @author 薛定谔的猫<hh_2013@foxmail.com> + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "require or disallow an empty line between class members", + category: "Stylistic Issues", + recommended: false + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + exceptAfterSingleLine: { + type: "boolean" + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + + const options = []; + + options[0] = context.options[0] || "always"; + options[1] = context.options[1] || { exceptAfterSingleLine: false }; + + const ALWAYS_MESSAGE = "Expected blank line between class members."; + const NEVER_MESSAGE = "Unexpected blank line between class members."; + + const sourceCode = context.getSourceCode(); + + /** + * Checks if there is padding between two tokens + * @param {Token} first The first token + * @param {Token} second The second token + * @returns {boolean} True if there is at least a line between the tokens + */ + function isPaddingBetweenTokens(first, second) { + return second.loc.start.line - first.loc.end.line >= 2; + } + + return { + ClassBody(node) { + const body = node.body; + + for (let i = 0; i < body.length - 1; i++) { + const curFirst = sourceCode.getFirstToken(body[i]); + const curLast = sourceCode.getLastToken(body[i]); + const comments = sourceCode.getCommentsBefore(body[i + 1]); + const nextFirst = comments.length ? comments[0] : sourceCode.getFirstToken(body[i + 1]); + const isPadded = isPaddingBetweenTokens(curLast, nextFirst); + const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast); + const skip = !isMulti && options[1].exceptAfterSingleLine; + + + if ((options[0] === "always" && !skip && !isPadded) || + (options[0] === "never" && isPadded)) { + context.report({ + node: body[i + 1], + message: isPadded ? NEVER_MESSAGE : ALWAYS_MESSAGE, + fix(fixer) { + return isPadded + ? fixer.replaceTextRange([curLast.range[1], nextFirst.range[0]], "\n") + : fixer.insertTextAfter(curLast, "\n"); + } + }); + } + } + } + }; + } +}; diff --git a/tools/eslint/lib/rules/multiline-comment-style.js b/tools/eslint/lib/rules/multiline-comment-style.js new file mode 100644 index 0000000000..db4f768443 --- /dev/null +++ b/tools/eslint/lib/rules/multiline-comment-style.js @@ -0,0 +1,294 @@ +/** + * @fileoverview enforce a particular style for multiline comments + * @author Teddy Katz + */ +"use strict"; + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce a particular style for multiline comments", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [{ enum: ["starred-block", "separate-lines", "bare-block"] }] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const option = context.options[0] || "starred-block"; + + const EXPECTED_BLOCK_ERROR = "Expected a block comment instead of consecutive line comments."; + const START_NEWLINE_ERROR = "Expected a linebreak after '/*'."; + const END_NEWLINE_ERROR = "Expected a linebreak before '*/'."; + const MISSING_STAR_ERROR = "Expected a '*' at the start of this line."; + const ALIGNMENT_ERROR = "Expected this line to be aligned with the start of the comment."; + const EXPECTED_LINES_ERROR = "Expected multiple line comments instead of a block comment."; + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Gets a list of comment lines in a group + * @param {Token[]} commentGroup A group of comments, containing either multiple line comments or a single block comment + * @returns {string[]} A list of comment lines + */ + function getCommentLines(commentGroup) { + if (commentGroup[0].type === "Line") { + return commentGroup.map(comment => comment.value); + } + return commentGroup[0].value + .split(astUtils.LINEBREAK_MATCHER) + .map(line => line.replace(/^\s*\*?/, "")); + } + + /** + * Converts a comment into starred-block form + * @param {Token} firstComment The first comment of the group being converted + * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment + * @returns {string} A representation of the comment value in starred-block form, excluding start and end markers + */ + function convertToStarredBlock(firstComment, commentLinesList) { + const initialOffset = sourceCode.text.slice(firstComment.range[0] - firstComment.loc.start.column, firstComment.range[0]); + const starredLines = commentLinesList.map(line => `${initialOffset} *${line}`); + + return `\n${starredLines.join("\n")}\n${initialOffset} `; + } + + /** + * Converts a comment into separate-line form + * @param {Token} firstComment The first comment of the group being converted + * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment + * @returns {string} A representation of the comment value in separate-line form + */ + function convertToSeparateLines(firstComment, commentLinesList) { + const initialOffset = sourceCode.text.slice(firstComment.range[0] - firstComment.loc.start.column, firstComment.range[0]); + const separateLines = commentLinesList.map(line => `// ${line.trim()}`); + + return separateLines.join(`\n${initialOffset}`); + } + + /** + * Converts a comment into bare-block form + * @param {Token} firstComment The first comment of the group being converted + * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment + * @returns {string} A representation of the comment value in bare-block form + */ + function convertToBlock(firstComment, commentLinesList) { + const initialOffset = sourceCode.text.slice(firstComment.range[0] - firstComment.loc.start.column, firstComment.range[0]); + const blockLines = commentLinesList.map(line => line.trim()); + + return `/* ${blockLines.join(`\n${initialOffset} `)} */`; + } + + /** + * Check a comment is JSDoc form + * @param {Token[]} commentGroup A group of comments, containing either multiple line comments or a single block comment + * @returns {boolean} if commentGroup is JSDoc form, return true + */ + function isJSDoc(commentGroup) { + const lines = commentGroup[0].value.split(astUtils.LINEBREAK_MATCHER); + + return commentGroup[0].type === "Block" && + /^\*\s*$/.test(lines[0]) && + lines.slice(1, -1).every(line => /^\s* /.test(line)) && + /^\s*$/.test(lines[lines.length - 1]); + } + + /** + * Each method checks a group of comments to see if it's valid according to the given option. + * @param {Token[]} commentGroup A list of comments that appear together. This will either contain a single + * block comment or multiple line comments. + * @returns {void} + */ + const commentGroupCheckers = { + "starred-block"(commentGroup) { + const commentLines = getCommentLines(commentGroup); + + if (commentLines.some(value => value.includes("*/"))) { + return; + } + + if (commentGroup.length > 1) { + context.report({ + loc: { + start: commentGroup[0].loc.start, + end: commentGroup[commentGroup.length - 1].loc.end + }, + message: EXPECTED_BLOCK_ERROR, + fix(fixer) { + const range = [commentGroup[0].range[0], commentGroup[commentGroup.length - 1].range[1]]; + const starredBlock = `/*${convertToStarredBlock(commentGroup[0], commentLines)}*/`; + + return commentLines.some(value => value.startsWith("/")) + ? null + : fixer.replaceTextRange(range, starredBlock); + } + }); + } else { + const block = commentGroup[0]; + const lines = block.value.split(astUtils.LINEBREAK_MATCHER); + const expectedLinePrefix = `${sourceCode.text.slice(block.range[0] - block.loc.start.column, block.range[0])} *`; + + if (!/^\*?\s*$/.test(lines[0])) { + const start = block.value.startsWith("*") ? block.range[0] + 1 : block.range[0]; + + context.report({ + loc: { + start: block.loc.start, + end: { line: block.loc.start.line, column: block.loc.start.column + 2 } + }, + message: START_NEWLINE_ERROR, + fix: fixer => fixer.insertTextAfterRange([start, start + 2], `\n${expectedLinePrefix}`) + }); + } + + if (!/^\s*$/.test(lines[lines.length - 1])) { + context.report({ + loc: { + start: { line: block.loc.end.line, column: block.loc.end.column - 2 }, + end: block.loc.end + }, + message: END_NEWLINE_ERROR, + fix: fixer => fixer.replaceTextRange([block.range[1] - 2, block.range[1]], `\n${expectedLinePrefix}/`) + }); + } + + for (let lineNumber = block.loc.start.line + 1; lineNumber <= block.loc.end.line; lineNumber++) { + const lineText = sourceCode.lines[lineNumber - 1]; + + if (!lineText.startsWith(expectedLinePrefix)) { + context.report({ + loc: { + start: { line: lineNumber, column: 0 }, + end: { line: lineNumber, column: sourceCode.lines[lineNumber - 1].length } + }, + message: /^\s*\*/.test(lineText) + ? ALIGNMENT_ERROR + : MISSING_STAR_ERROR, + fix(fixer) { + const lineStartIndex = sourceCode.getIndexFromLoc({ line: lineNumber, column: 0 }); + const linePrefixLength = lineText.match(/^\s*\*? ?/)[0].length; + const commentStartIndex = lineStartIndex + linePrefixLength; + + const replacementText = lineNumber === block.loc.end.line || lineText.length === linePrefixLength + ? expectedLinePrefix + : `${expectedLinePrefix} `; + + return fixer.replaceTextRange([lineStartIndex, commentStartIndex], replacementText); + } + }); + } + } + } + }, + "separate-lines"(commentGroup) { + if (!isJSDoc(commentGroup) && commentGroup[0].type === "Block") { + const commentLines = getCommentLines(commentGroup); + const block = commentGroup[0]; + const tokenAfter = sourceCode.getTokenAfter(block, { includeComments: true }); + + if (tokenAfter && block.loc.end.line === tokenAfter.loc.start.line) { + return; + } + + context.report({ + loc: { + start: block.loc.start, + end: { line: block.loc.start.line, column: block.loc.start.column + 2 } + }, + message: EXPECTED_LINES_ERROR, + fix(fixer) { + return fixer.replaceText(block, convertToSeparateLines(block, commentLines.filter(line => line))); + } + }); + } + }, + "bare-block"(commentGroup) { + if (!isJSDoc(commentGroup)) { + const commentLines = getCommentLines(commentGroup); + + // disallows consecutive line comments in favor of using a block comment. + if (commentGroup[0].type === "Line" && commentLines.length > 1 && + !commentLines.some(value => value.includes("*/"))) { + context.report({ + loc: { + start: commentGroup[0].loc.start, + end: commentGroup[commentGroup.length - 1].loc.end + }, + message: EXPECTED_BLOCK_ERROR, + fix(fixer) { + const range = [commentGroup[0].range[0], commentGroup[commentGroup.length - 1].range[1]]; + const block = convertToBlock(commentGroup[0], commentLines.filter(line => line)); + + return fixer.replaceTextRange(range, block); + } + }); + } + + // prohibits block comments from having a * at the beginning of each line. + if (commentGroup[0].type === "Block") { + const block = commentGroup[0]; + const lines = block.value.split(astUtils.LINEBREAK_MATCHER).filter(line => line.trim()); + + if (lines.length > 0 && lines.every(line => /^\s*\*/.test(line))) { + context.report({ + loc: { + start: block.loc.start, + end: { line: block.loc.start.line, column: block.loc.start.column + 2 } + }, + message: EXPECTED_BLOCK_ERROR, + fix(fixer) { + return fixer.replaceText(block, convertToBlock(block, commentLines.filter(line => line))); + } + }); + } + } + } + } + }; + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + Program() { + return sourceCode.getAllComments() + .filter(comment => comment.type !== "Shebang") + .filter(comment => !astUtils.COMMENTS_IGNORE_PATTERN.test(comment.value)) + .filter(comment => { + const tokenBefore = sourceCode.getTokenBefore(comment, { includeComments: true }); + + return !tokenBefore || tokenBefore.loc.end.line < comment.loc.start.line; + }) + .reduce((commentGroups, comment, index, commentList) => { + const tokenBefore = sourceCode.getTokenBefore(comment, { includeComments: true }); + + if ( + comment.type === "Line" && + index && commentList[index - 1].type === "Line" && + tokenBefore && tokenBefore.loc.end.line === comment.loc.start.line - 1 && + tokenBefore === commentList[index - 1] + ) { + commentGroups[commentGroups.length - 1].push(comment); + } else { + commentGroups.push([comment]); + } + + return commentGroups; + }, []) + .filter(commentGroup => !(commentGroup.length === 1 && commentGroup[0].loc.start.line === commentGroup[0].loc.end.line)) + .forEach(commentGroupCheckers[option]); + } + }; + } +}; diff --git a/tools/eslint/lib/rules/new-cap.js b/tools/eslint/lib/rules/new-cap.js index 2f02c0924b..f01e8f90ac 100644 --- a/tools/eslint/lib/rules/new-cap.js +++ b/tools/eslint/lib/rules/new-cap.js @@ -178,7 +178,8 @@ module.exports = { // char has no uppercase variant, so it's non-alphabetic return "non-alpha"; - } else if (firstChar === firstCharLower) { + } + if (firstChar === firstCharLower) { return "lower"; } return "upper"; diff --git a/tools/eslint/lib/rules/newline-before-return.js b/tools/eslint/lib/rules/newline-before-return.js index 1b9b0c7939..ef6c4a9264 100644 --- a/tools/eslint/lib/rules/newline-before-return.js +++ b/tools/eslint/lib/rules/newline-before-return.js @@ -59,9 +59,11 @@ module.exports = { if (parentType === "IfStatement") { return isPrecededByTokens(node, ["else", ")"]); - } else if (parentType === "DoWhileStatement") { + } + if (parentType === "DoWhileStatement") { return isPrecededByTokens(node, ["do"]); - } else if (parentType === "SwitchCase") { + } + if (parentType === "SwitchCase") { return isPrecededByTokens(node, [":"]); } return isPrecededByTokens(node, [")"]); diff --git a/tools/eslint/lib/rules/no-alert.js b/tools/eslint/lib/rules/no-alert.js index ecb52cedd6..bc1087253b 100644 --- a/tools/eslint/lib/rules/no-alert.js +++ b/tools/eslint/lib/rules/no-alert.js @@ -71,7 +71,8 @@ function isShadowed(scope, node) { function isGlobalThisReferenceOrGlobalWindow(scope, node) { if (scope.type === "global" && node.type === "ThisExpression") { return true; - } else if (node.name === "window") { + } + if (node.name === "window") { return !isShadowed(scope, node); } diff --git a/tools/eslint/lib/rules/no-catch-shadow.js b/tools/eslint/lib/rules/no-catch-shadow.js index 7cffae3b99..beb16aa2ba 100644 --- a/tools/eslint/lib/rules/no-catch-shadow.js +++ b/tools/eslint/lib/rules/no-catch-shadow.js @@ -51,7 +51,7 @@ module.exports = { CatchClause(node) { let scope = context.getScope(); - // When blockBindings is enabled, CatchClause creates its own scope + // When ecmaVersion >= 6, CatchClause creates its own scope // so start from one upper scope to exclude the current node if (scope.block === node) { scope = scope.upper; diff --git a/tools/eslint/lib/rules/no-constant-condition.js b/tools/eslint/lib/rules/no-constant-condition.js index 31e5b372c4..0cd445dfdb 100644 --- a/tools/eslint/lib/rules/no-constant-condition.js +++ b/tools/eslint/lib/rules/no-constant-condition.js @@ -138,7 +138,7 @@ module.exports = { function checkConstantConditionLoopInSet(node) { if (loopsInCurrentScope.has(node)) { loopsInCurrentScope.delete(node); - context.report({ node, message: "Unexpected constant condition." }); + context.report({ node: node.test, message: "Unexpected constant condition." }); } } @@ -150,7 +150,7 @@ module.exports = { */ function reportIfConstant(node) { if (node.test && isConstant(node.test, true)) { - context.report({ node, message: "Unexpected constant condition." }); + context.report({ node: node.test, message: "Unexpected constant condition." }); } } diff --git a/tools/eslint/lib/rules/no-control-regex.js b/tools/eslint/lib/rules/no-control-regex.js index 1ebf980000..14981f4ab1 100644 --- a/tools/eslint/lib/rules/no-control-regex.js +++ b/tools/eslint/lib/rules/no-control-regex.js @@ -31,7 +31,8 @@ module.exports = { function getRegExp(node) { if (node.value instanceof RegExp) { return node.value; - } else if (typeof node.value === "string") { + } + if (typeof node.value === "string") { const parent = context.getAncestors().pop(); diff --git a/tools/eslint/lib/rules/no-else-return.js b/tools/eslint/lib/rules/no-else-return.js index 36bfc26b3d..6eb6717495 100644 --- a/tools/eslint/lib/rules/no-else-return.js +++ b/tools/eslint/lib/rules/no-else-return.js @@ -24,8 +24,15 @@ module.exports = { recommended: false }, - schema: [], - + schema: [{ + type: "object", + properties: { + allowElseIf: { + type: "boolean" + } + }, + additionalProperties: false + }], fixable: "code" }, @@ -134,13 +141,13 @@ module.exports = { /** * Check to see if the node is valid for evaluation, - * meaning it has an else and not an else-if + * meaning it has an else. * * @param {Node} node The node being evaluated * @returns {boolean} True if the node is valid */ function hasElse(node) { - return node.alternate && node.consequent && node.alternate.type !== "IfStatement"; + return node.alternate && node.consequent; } /** @@ -189,14 +196,15 @@ module.exports = { return checkForReturnOrIf(node); } + /** - * Check the if statement + * Check the if statement, but don't catch else-if blocks. * @returns {void} * @param {Node} node The node for the if statement to check * @private */ - function IfStatement(node) { - const parent = context.getAncestors().pop(); + function checkIfWithoutElse(node) { + const parent = node.parent; let consequents, alternate; @@ -221,13 +229,40 @@ module.exports = { } } + /** + * Check the if statement + * @returns {void} + * @param {Node} node The node for the if statement to check + * @private + */ + function checkIfWithElse(node) { + const parent = node.parent; + + + /* + * Fixing this would require splitting one statement into two, so no error should + * be reported if this node is in a position where only one statement is allowed. + */ + if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) { + return; + } + + const alternate = node.alternate; + + if (alternate && alwaysReturns(node.consequent)) { + displayReport(alternate); + } + } + + const allowElseIf = !(context.options[0] && context.options[0].allowElseIf === false); + //-------------------------------------------------------------------------- // Public API //-------------------------------------------------------------------------- return { - "IfStatement:exit": IfStatement + "IfStatement:exit": allowElseIf ? checkIfWithoutElse : checkIfWithElse }; diff --git a/tools/eslint/lib/rules/no-extra-parens.js b/tools/eslint/lib/rules/no-extra-parens.js index 9c8fe70f03..020d2aeb10 100644 --- a/tools/eslint/lib/rules/no-extra-parens.js +++ b/tools/eslint/lib/rules/no-extra-parens.js @@ -195,10 +195,12 @@ module.exports = { function containsAssignment(node) { if (node.type === "AssignmentExpression") { return true; - } else if (node.type === "ConditionalExpression" && + } + if (node.type === "ConditionalExpression" && (node.consequent.type === "AssignmentExpression" || node.alternate.type === "AssignmentExpression")) { return true; - } else if ((node.left && node.left.type === "AssignmentExpression") || + } + if ((node.left && node.left.type === "AssignmentExpression") || (node.right && node.right.type === "AssignmentExpression")) { return true; } @@ -219,7 +221,8 @@ module.exports = { if (node.type === "ReturnStatement") { return node.argument && containsAssignment(node.argument); - } else if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") { + } + if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") { return containsAssignment(node.body); } return containsAssignment(node); diff --git a/tools/eslint/lib/rules/no-lonely-if.js b/tools/eslint/lib/rules/no-lonely-if.js index 31f47b90e0..db127d1945 100644 --- a/tools/eslint/lib/rules/no-lonely-if.js +++ b/tools/eslint/lib/rules/no-lonely-if.js @@ -45,7 +45,8 @@ module.exports = { const lastIfToken = sourceCode.getLastToken(node.consequent); const sourceText = sourceCode.getText(); - if (sourceText.slice(openingElseCurly.range[1], node.range[0]).trim() || sourceText.slice(node.range[1], closingElseCurly.range[0]).trim()) { + if (sourceText.slice(openingElseCurly.range[1], + node.range[0]).trim() || sourceText.slice(node.range[1], closingElseCurly.range[0]).trim()) { // Don't fix if there are any non-whitespace characters interfering (e.g. comments) return null; diff --git a/tools/eslint/lib/rules/no-mixed-requires.js b/tools/eslint/lib/rules/no-mixed-requires.js index 55ad1e73e0..171052a52a 100644 --- a/tools/eslint/lib/rules/no-mixed-requires.js +++ b/tools/eslint/lib/rules/no-mixed-requires.js @@ -104,14 +104,16 @@ module.exports = { // "var x = require('util');" return DECL_REQUIRE; - } else if (allowCall && + } + if (allowCall && initExpression.type === "CallExpression" && initExpression.callee.type === "CallExpression" ) { // "var x = require('diagnose')('sub-module');" return getDeclarationType(initExpression.callee); - } else if (initExpression.type === "MemberExpression") { + } + if (initExpression.type === "MemberExpression") { // "var x = require('glob').Glob;" return getDeclarationType(initExpression.object); @@ -131,7 +133,8 @@ module.exports = { // "var x = require('glob').Glob;" return inferModuleType(initExpression.object); - } else if (initExpression.arguments.length === 0) { + } + if (initExpression.arguments.length === 0) { // "var x = require();" return REQ_COMPUTED; @@ -149,7 +152,8 @@ module.exports = { // "var fs = require('fs');" return REQ_CORE; - } else if (/^\.{0,2}\//.test(arg.value)) { + } + if (/^\.{0,2}\//.test(arg.value)) { // "var utils = require('./utils');" return REQ_FILE; diff --git a/tools/eslint/lib/rules/no-restricted-imports.js b/tools/eslint/lib/rules/no-restricted-imports.js index c245f22a0a..d46b098ace 100644 --- a/tools/eslint/lib/rules/no-restricted-imports.js +++ b/tools/eslint/lib/rules/no-restricted-imports.js @@ -5,6 +5,13 @@ "use strict"; //------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const DEFAULT_MESSAGE_TEMPLATE = "'{{importName}}' import is restricted from being used."; +const CUSTOM_MESSAGE_TEMPLATE = "'{{importName}}' import is restricted from being used. {{customMessage}}"; + +//------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -12,8 +19,28 @@ const ignore = require("ignore"); const arrayOfStrings = { type: "array", + items: { type: "string" }, + uniqueItems: true +}; + +const arrayOfStringsOrObjects = { + type: "array", items: { - type: "string" + anyOf: [ + { type: "string" }, + { + type: "object", + properties: { + name: { type: "string" }, + message: { + type: "string", + minLength: 1 + } + }, + additionalProperties: false, + required: ["name"] + } + ] }, uniqueItems: true }; @@ -28,17 +55,17 @@ module.exports = { schema: { anyOf: [ - arrayOfStrings, + arrayOfStringsOrObjects, { type: "array", - items: [{ + items: { type: "object", properties: { - paths: arrayOfStrings, + paths: arrayOfStringsOrObjects, patterns: arrayOfStrings }, additionalProperties: false - }], + }, additionalItems: false } ] @@ -47,35 +74,77 @@ module.exports = { create(context) { const options = Array.isArray(context.options) ? context.options : []; - const isStringArray = typeof options[0] !== "object"; - const restrictedPaths = new Set(isStringArray ? context.options : options[0].paths || []); - const restrictedPatterns = isStringArray ? [] : options[0].patterns || []; + const isPathAndPatternsObject = + typeof options[0] === "object" && + (options[0].hasOwnProperty("paths") || options[0].hasOwnProperty("patterns")); + + const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || []; + const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || []; + + const restrictedPathMessages = restrictedPaths.reduce((memo, importName) => { + if (typeof importName === "string") { + memo[importName] = null; + } else { + memo[importName.name] = importName.message; + } + return memo; + }, {}); // if no imports are restricted we don"t need to check - if (restrictedPaths.size === 0 && restrictedPatterns.length === 0) { + if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) { return {}; } const ig = ignore().add(restrictedPatterns); + /** + * Report a restricted path. + * @param {node} node representing the restricted path reference + * @returns {void} + * @private + */ + function reportPath(node) { + const importName = node.source.value.trim(); + const customMessage = restrictedPathMessages[importName]; + const message = customMessage + ? CUSTOM_MESSAGE_TEMPLATE + : DEFAULT_MESSAGE_TEMPLATE; + + context.report({ + node, + message, + data: { + importName, + customMessage + } + }); + } + + /** + * Check if the given name is a restricted path name. + * @param {string} name name of a variable + * @returns {boolean} whether the variable is a restricted path or not + * @private + */ + function isRestrictedPath(name) { + return Object.prototype.hasOwnProperty.call(restrictedPathMessages, name); + } + return { ImportDeclaration(node) { if (node && node.source && node.source.value) { - const importName = node.source.value.trim(); - if (restrictedPaths.has(importName)) { - context.report({ - node, - message: "'{{importName}}' import is restricted from being used.", - data: { importName } - }); + if (isRestrictedPath(importName)) { + reportPath(node); } if (restrictedPatterns.length > 0 && ig.ignores(importName)) { context.report({ node, message: "'{{importName}}' import is restricted from being used by a pattern.", - data: { importName } + data: { + importName + } }); } } diff --git a/tools/eslint/lib/rules/no-restricted-modules.js b/tools/eslint/lib/rules/no-restricted-modules.js index 3a9634de9e..cd47975733 100644 --- a/tools/eslint/lib/rules/no-restricted-modules.js +++ b/tools/eslint/lib/rules/no-restricted-modules.js @@ -5,6 +5,13 @@ "use strict"; //------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const DEFAULT_MESSAGE_TEMPLATE = "'{{moduleName}}' module is restricted from being used."; +const CUSTOM_MESSAGE_TEMPLATE = "'{{moduleName}}' module is restricted from being used. {{customMessage}}"; + +//------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -12,8 +19,28 @@ const ignore = require("ignore"); const arrayOfStrings = { type: "array", + items: { type: "string" }, + uniqueItems: true +}; + +const arrayOfStringsOrObjects = { + type: "array", items: { - type: "string" + anyOf: [ + { type: "string" }, + { + type: "object", + properties: { + name: { type: "string" }, + message: { + type: "string", + minLength: 1 + } + }, + additionalProperties: false, + required: ["name"] + } + ] }, uniqueItems: true }; @@ -28,17 +55,17 @@ module.exports = { schema: { anyOf: [ - arrayOfStrings, + arrayOfStringsOrObjects, { type: "array", - items: [{ + items: { type: "object", properties: { - paths: arrayOfStrings, + paths: arrayOfStringsOrObjects, patterns: arrayOfStrings }, additionalProperties: false - }], + }, additionalItems: false } ] @@ -47,17 +74,30 @@ module.exports = { create(context) { const options = Array.isArray(context.options) ? context.options : []; - const isStringArray = typeof options[0] !== "object"; - const restrictedPaths = new Set(isStringArray ? context.options : options[0].paths || []); - const restrictedPatterns = isStringArray ? [] : options[0].patterns || []; + const isPathAndPatternsObject = + typeof options[0] === "object" && + (options[0].hasOwnProperty("paths") || options[0].hasOwnProperty("patterns")); + + const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || []; + const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || []; + + const restrictedPathMessages = restrictedPaths.reduce((memo, importName) => { + if (typeof importName === "string") { + memo[importName] = null; + } else { + memo[importName.name] = importName.message; + } + return memo; + }, {}); // if no imports are restricted we don"t need to check - if (restrictedPaths.size === 0 && restrictedPatterns.length === 0) { + if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) { return {}; } const ig = ignore().add(restrictedPatterns); + /** * Function to check if a node is a string literal. * @param {ASTNode} node The node to check. @@ -76,6 +116,39 @@ module.exports = { return node.callee.type === "Identifier" && node.callee.name === "require"; } + /** + * Report a restricted path. + * @param {node} node representing the restricted path reference + * @returns {void} + * @private + */ + function reportPath(node) { + const moduleName = node.arguments[0].value.trim(); + const customMessage = restrictedPathMessages[moduleName]; + const message = customMessage + ? CUSTOM_MESSAGE_TEMPLATE + : DEFAULT_MESSAGE_TEMPLATE; + + context.report({ + node, + message, + data: { + moduleName, + customMessage + } + }); + } + + /** + * Check if the given name is a restricted path name + * @param {string} name name of a variable + * @returns {boolean} whether the variable is a restricted path or not + * @private + */ + function isRestrictedPath(name) { + return Object.prototype.hasOwnProperty.call(restrictedPathMessages, name); + } + return { CallExpression(node) { if (isRequireCall(node)) { @@ -85,12 +158,8 @@ module.exports = { const moduleName = node.arguments[0].value.trim(); // check if argument value is in restricted modules array - if (restrictedPaths.has(moduleName)) { - context.report({ - node, - message: "'{{moduleName}}' module is restricted from being used.", - data: { moduleName } - }); + if (isRestrictedPath(moduleName)) { + reportPath(node); } if (restrictedPatterns.length > 0 && ig.ignores(moduleName)) { diff --git a/tools/eslint/lib/rules/no-trailing-spaces.js b/tools/eslint/lib/rules/no-trailing-spaces.js index b5d2f8d1b5..598bbea4f9 100644 --- a/tools/eslint/lib/rules/no-trailing-spaces.js +++ b/tools/eslint/lib/rules/no-trailing-spaces.js @@ -49,7 +49,7 @@ module.exports = { const options = context.options[0] || {}, skipBlankLines = options.skipBlankLines || false, - ignoreComments = typeof options.ignoreComments === "undefined" || options.ignoreComments; + ignoreComments = typeof options.ignoreComments === "boolean" && options.ignoreComments; /** * Report the error message diff --git a/tools/eslint/lib/rules/no-unneeded-ternary.js b/tools/eslint/lib/rules/no-unneeded-ternary.js index 929991f86b..5745537805 100644 --- a/tools/eslint/lib/rules/no-unneeded-ternary.js +++ b/tools/eslint/lib/rules/no-unneeded-ternary.js @@ -72,8 +72,10 @@ module.exports = { node.right, token => token.value === node.operator ); + const text = sourceCode.getText(); - return sourceCode.getText().slice(node.range[0], operatorToken.range[0]) + OPERATOR_INVERSES[node.operator] + sourceCode.getText().slice(operatorToken.range[1], node.range[1]); + return text.slice(node.range[0], + operatorToken.range[0]) + OPERATOR_INVERSES[node.operator] + text.slice(operatorToken.range[1], node.range[1]); } if (astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression" })) { diff --git a/tools/eslint/lib/rules/no-unused-labels.js b/tools/eslint/lib/rules/no-unused-labels.js index 12f60ca184..bcd3cfdc47 100644 --- a/tools/eslint/lib/rules/no-unused-labels.js +++ b/tools/eslint/lib/rules/no-unused-labels.js @@ -59,7 +59,8 @@ module.exports = { * Only perform a fix if there are no comments between the label and the body. This will be the case * when there is exactly one token/comment (the ":") between the label and the body. */ - if (sourceCode.getTokenAfter(node.label, { includeComments: true }) === sourceCode.getTokenBefore(node.body, { includeComments: true })) { + if (sourceCode.getTokenAfter(node.label, { includeComments: true }) === + sourceCode.getTokenBefore(node.body, { includeComments: true })) { return fixer.removeRange([node.range[0], node.body.range[0]]); } diff --git a/tools/eslint/lib/rules/no-useless-computed-key.js b/tools/eslint/lib/rules/no-useless-computed-key.js index 23de2f3734..f8114ab754 100644 --- a/tools/eslint/lib/rules/no-useless-computed-key.js +++ b/tools/eslint/lib/rules/no-useless-computed-key.js @@ -50,7 +50,8 @@ module.exports = { const rightSquareBracket = sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken); const tokensBetween = sourceCode.getTokensBetween(leftSquareBracket, rightSquareBracket, 1); - if (tokensBetween.slice(0, -1).some((token, index) => sourceCode.getText().slice(token.range[1], tokensBetween[index + 1].range[0]).trim())) { + if (tokensBetween.slice(0, -1).some((token, index) => + sourceCode.getText().slice(token.range[1], tokensBetween[index + 1].range[0]).trim())) { // If there are comments between the brackets and the property name, don't do a fix. return null; diff --git a/tools/eslint/lib/rules/no-useless-escape.js b/tools/eslint/lib/rules/no-useless-escape.js index 0212bd60e3..9e39eb6f43 100644 --- a/tools/eslint/lib/rules/no-useless-escape.js +++ b/tools/eslint/lib/rules/no-useless-escape.js @@ -63,7 +63,14 @@ function parseRegExp(regExpText) { return Object.assign(state, { inCharClass: false, startingCharClass: false }); } } - charList.push({ text: char, index, escaped: state.escapeNextChar, inCharClass: state.inCharClass, startsCharClass: state.startingCharClass, endsCharClass: false }); + charList.push({ + text: char, + index, + escaped: state.escapeNextChar, + inCharClass: state.inCharClass, + startsCharClass: state.startingCharClass, + endsCharClass: false + }); return Object.assign(state, { escapeNextChar: false, startingCharClass: false }); }, { escapeNextChar: false, inCharClass: false, startingCharClass: false }); diff --git a/tools/eslint/lib/rules/no-var.js b/tools/eslint/lib/rules/no-var.js index c74e0b9ad9..d3c163e557 100644 --- a/tools/eslint/lib/rules/no-var.js +++ b/tools/eslint/lib/rules/no-var.js @@ -16,6 +16,15 @@ const astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ /** + * Check whether a given variable is a global variable or not. + * @param {eslint-scope.Variable} variable The variable to check. + * @returns {boolean} `true` if the variable is a global variable. + */ +function isGlobal(variable) { + return Boolean(variable.scope) && variable.scope.type === "global"; +} + +/** * Finds the nearest function scope or global scope walking up the scope * hierarchy. * @@ -203,6 +212,7 @@ module.exports = { * Checks whether it can fix a given variable declaration or not. * It cannot fix if the following cases: * + * - A variable is a global variable. * - A variable is declared on a SwitchCase node. * - A variable is redeclared. * - A variable is used from outside the scope. @@ -256,6 +266,7 @@ module.exports = { if (node.parent.type === "SwitchCase" || node.declarations.some(hasSelfReferenceInTDZ) || + variables.some(isGlobal) || variables.some(isRedeclared) || variables.some(isUsedFromOutsideOf(scopeNode)) ) { diff --git a/tools/eslint/lib/rules/object-shorthand.js b/tools/eslint/lib/rules/object-shorthand.js index 2f7b0ccf90..dfd8d1a64e 100644 --- a/tools/eslint/lib/rules/object-shorthand.js +++ b/tools/eslint/lib/rules/object-shorthand.js @@ -215,8 +215,12 @@ module.exports = { * @returns {Object} A fix for this node */ function makeFunctionShorthand(fixer, node) { - const firstKeyToken = node.computed ? sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken) : sourceCode.getFirstToken(node.key); - const lastKeyToken = node.computed ? sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken) : sourceCode.getLastToken(node.key); + const firstKeyToken = node.computed + ? sourceCode.getFirstToken(node, astUtils.isOpeningBracketToken) + : sourceCode.getFirstToken(node.key); + const lastKeyToken = node.computed + ? sourceCode.getFirstTokenBetween(node.key, node.value, astUtils.isClosingBracketToken) + : sourceCode.getLastToken(node.key); const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]); let keyPrefix = ""; diff --git a/tools/eslint/lib/rules/operator-linebreak.js b/tools/eslint/lib/rules/operator-linebreak.js index 8253094c52..809da1fc8c 100644 --- a/tools/eslint/lib/rules/operator-linebreak.js +++ b/tools/eslint/lib/rules/operator-linebreak.js @@ -87,7 +87,9 @@ module.exports = { if (hasLinebreakBefore !== hasLinebreakAfter && desiredStyle !== "none") { // If there is a comment before and after the operator, don't do a fix. - if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore && sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) { + if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore && + sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) { + return null; } diff --git a/tools/eslint/lib/rules/require-jsdoc.js b/tools/eslint/lib/rules/require-jsdoc.js index f1ecde81f9..a02ee3659c 100644 --- a/tools/eslint/lib/rules/require-jsdoc.js +++ b/tools/eslint/lib/rules/require-jsdoc.js @@ -30,6 +30,9 @@ module.exports = { }, ArrowFunctionExpression: { type: "boolean" + }, + FunctionExpression: { + type: "boolean" } }, additionalProperties: false @@ -45,7 +48,9 @@ module.exports = { const DEFAULT_OPTIONS = { FunctionDeclaration: true, MethodDefinition: false, - ClassDeclaration: false + ClassDeclaration: false, + ArrowFunctionExpression: false, + FunctionExpression: false }; const options = Object.assign(DEFAULT_OPTIONS, context.options[0] && context.options[0].require || {}); @@ -59,21 +64,6 @@ module.exports = { } /** - * Check if the jsdoc comment is present for class methods - * @param {ASTNode} node node to examine - * @returns {void} - */ - function checkClassMethodJsDoc(node) { - if (node.parent.type === "MethodDefinition") { - const jsdocComment = source.getJSDocComment(node); - - if (!jsdocComment) { - report(node); - } - } - } - - /** * Check if the jsdoc comment is present or not. * @param {ASTNode} node node to examine * @returns {void} @@ -93,8 +83,11 @@ module.exports = { } }, FunctionExpression(node) { - if (options.MethodDefinition) { - checkClassMethodJsDoc(node); + if ( + (options.MethodDefinition && node.parent.type === "MethodDefinition") || + (options.FunctionExpression && (node.parent.type === "VariableDeclarator" || (node.parent.type === "Property" && node === node.parent.value))) + ) { + checkJsDoc(node); } }, ClassDeclaration(node) { diff --git a/tools/eslint/lib/rules/sort-imports.js b/tools/eslint/lib/rules/sort-imports.js index 74db02ad3d..be1605dc47 100644 --- a/tools/eslint/lib/rules/sort-imports.js +++ b/tools/eslint/lib/rules/sort-imports.js @@ -67,9 +67,11 @@ module.exports = { function usedMemberSyntax(node) { if (node.specifiers.length === 0) { return "none"; - } else if (node.specifiers[0].type === "ImportNamespaceSpecifier") { + } + if (node.specifiers[0].type === "ImportNamespaceSpecifier") { return "all"; - } else if (node.specifiers.length === 1) { + } + if (node.specifiers.length === 1) { return "single"; } return "multiple"; @@ -149,7 +151,8 @@ module.exports = { message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.", data: { memberName: importSpecifiers[firstUnsortedIndex].local.name }, fix(fixer) { - if (importSpecifiers.some(specifier => sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) { + if (importSpecifiers.some(specifier => + sourceCode.getCommentsBefore(specifier).length || sourceCode.getCommentsAfter(specifier).length)) { // If there are comments in the ImportSpecifier list, don't rearrange the specifiers. return null; diff --git a/tools/eslint/lib/rules/valid-jsdoc.js b/tools/eslint/lib/rules/valid-jsdoc.js index d4ee4e0738..ea60b115ce 100644 --- a/tools/eslint/lib/rules/valid-jsdoc.js +++ b/tools/eslint/lib/rules/valid-jsdoc.js @@ -226,8 +226,10 @@ module.exports = { function checkJSDoc(node) { const jsdocNode = sourceCode.getJSDocComment(node), functionData = fns.pop(), - params = Object.create(null); + params = Object.create(null), + paramsTags = []; let hasReturns = false, + returnsTag, hasConstructor = false, isInterface = false, isOverride = false, @@ -261,43 +263,13 @@ module.exports = { case "param": case "arg": case "argument": - if (!tag.type) { - context.report({ node: jsdocNode, message: "Missing JSDoc parameter type for '{{name}}'.", data: { name: tag.name } }); - } - - if (!tag.description && requireParamDescription) { - context.report({ node: jsdocNode, message: "Missing JSDoc parameter description for '{{name}}'.", data: { name: tag.name } }); - } - - if (params[tag.name]) { - context.report({ node: jsdocNode, message: "Duplicate JSDoc parameter '{{name}}'.", data: { name: tag.name } }); - } else if (tag.name.indexOf(".") === -1) { - params[tag.name] = 1; - } + paramsTags.push(tag); break; case "return": case "returns": hasReturns = true; - - if (!requireReturn && !functionData.returnPresent && (tag.type === null || !isValidReturnType(tag)) && !isAbstract) { - context.report({ - node: jsdocNode, - message: "Unexpected @{{title}} tag; function has no return statement.", - data: { - title: tag.title - } - }); - } else { - if (requireReturnType && !tag.type) { - context.report({ node: jsdocNode, message: "Missing JSDoc return type." }); - } - - if (!isValidReturnType(tag) && !tag.description && requireReturnDescription) { - context.report({ node: jsdocNode, message: "Missing JSDoc return description." }); - } - } - + returnsTag = tag; break; case "constructor": @@ -333,6 +305,40 @@ module.exports = { } }); + paramsTags.forEach(param => { + if (!param.type) { + context.report({ node: jsdocNode, message: "Missing JSDoc parameter type for '{{name}}'.", data: { name: param.name } }); + } + if (!param.description && requireParamDescription) { + context.report({ node: jsdocNode, message: "Missing JSDoc parameter description for '{{name}}'.", data: { name: param.name } }); + } + if (params[param.name]) { + context.report({ node: jsdocNode, message: "Duplicate JSDoc parameter '{{name}}'.", data: { name: param.name } }); + } else if (param.name.indexOf(".") === -1) { + params[param.name] = 1; + } + }); + + if (hasReturns) { + if (!requireReturn && !functionData.returnPresent && (returnsTag.type === null || !isValidReturnType(returnsTag)) && !isAbstract) { + context.report({ + node: jsdocNode, + message: "Unexpected @{{title}} tag; function has no return statement.", + data: { + title: returnsTag.title + } + }); + } else { + if (requireReturnType && !returnsTag.type) { + context.report({ node: jsdocNode, message: "Missing JSDoc return type." }); + } + + if (!isValidReturnType(returnsTag) && !returnsTag.description && requireReturnDescription) { + context.report({ node: jsdocNode, message: "Missing JSDoc return description." }); + } + } + } + // check for functions missing @returns if (!isOverride && !hasReturns && !hasConstructor && !isInterface && node.parent.kind !== "get" && node.parent.kind !== "constructor" && diff --git a/tools/eslint/lib/testers/rule-tester.js b/tools/eslint/lib/testers/rule-tester.js index d7e14878cf..a76a38c655 100644 --- a/tools/eslint/lib/testers/rule-tester.js +++ b/tools/eslint/lib/testers/rule-tester.js @@ -178,7 +178,7 @@ class RuleTester { */ static setDefaultConfig(config) { if (typeof config !== "object") { - throw new Error("RuleTester.setDefaultConfig: config must be an object"); + throw new TypeError("RuleTester.setDefaultConfig: config must be an object"); } defaultConfig = config; @@ -254,7 +254,7 @@ class RuleTester { linter = this.linter; if (lodash.isNil(test) || typeof test !== "object") { - throw new Error(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`); + throw new TypeError(`Test Scenarios for rule ${ruleName} : Could not find test scenario object`); } requiredScenarios.forEach(scenarioType => { @@ -369,6 +369,7 @@ class RuleTester { if (!lodash.isEqual(beforeAST, afterAST)) { // Not using directly to avoid performance problem in node 6.1.0. See #6111 + // eslint-disable-next-line no-restricted-properties assert.deepEqual(beforeAST, afterAST, "Rule should not modify AST."); } } @@ -384,7 +385,7 @@ class RuleTester { const result = runRuleForItem(item); const messages = result.messages; - assert.equal(messages.length, 0, util.format("Should have no errors but had %d: %s", + assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s", messages.length, util.inspect(messages))); assertASTDidntChange(result.beforeAST, result.afterAST); @@ -408,7 +409,7 @@ class RuleTester { `Expected '${actual}' to match ${expected}` ); } else { - assert.equal(actual, expected); + assert.strictEqual(actual, expected); } } @@ -428,10 +429,10 @@ class RuleTester { if (typeof item.errors === "number") { - assert.equal(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s", + assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s", item.errors, item.errors === 1 ? "" : "s", messages.length, util.inspect(messages))); } else { - assert.equal( + assert.strictEqual( messages.length, item.errors.length, util.format( "Should have %d error%s but had %d: %s", @@ -460,23 +461,35 @@ class RuleTester { assertMessageMatches(messages[i].message, item.errors[i].message); } + // The following checks use loose equality assertions for backwards compatibility. + if (item.errors[i].type) { + + // eslint-disable-next-line no-restricted-properties assert.equal(messages[i].nodeType, item.errors[i].type, `Error type should be ${item.errors[i].type}, found ${messages[i].nodeType}`); } if (item.errors[i].hasOwnProperty("line")) { + + // eslint-disable-next-line no-restricted-properties assert.equal(messages[i].line, item.errors[i].line, `Error line should be ${item.errors[i].line}`); } if (item.errors[i].hasOwnProperty("column")) { + + // eslint-disable-next-line no-restricted-properties assert.equal(messages[i].column, item.errors[i].column, `Error column should be ${item.errors[i].column}`); } if (item.errors[i].hasOwnProperty("endLine")) { + + // eslint-disable-next-line no-restricted-properties assert.equal(messages[i].endLine, item.errors[i].endLine, `Error endLine should be ${item.errors[i].endLine}`); } if (item.errors[i].hasOwnProperty("endColumn")) { + + // eslint-disable-next-line no-restricted-properties assert.equal(messages[i].endColumn, item.errors[i].endColumn, `Error endColumn should be ${item.errors[i].endColumn}`); } } else { @@ -497,6 +510,7 @@ class RuleTester { } else { const fixResult = SourceCodeFixer.applyFixes(item.code, messages); + // eslint-disable-next-line no-restricted-properties assert.equal(fixResult.output, item.output, "Output is incorrect."); } } diff --git a/tools/eslint/lib/util/node-event-generator.js b/tools/eslint/lib/util/node-event-generator.js index 05b343ae9f..34ee78b494 100644 --- a/tools/eslint/lib/util/node-event-generator.js +++ b/tools/eslint/lib/util/node-event-generator.js @@ -160,7 +160,7 @@ function tryParseSelector(rawSelector) { return esquery.parse(rawSelector.replace(/:exit$/, "")); } catch (err) { if (typeof err.offset === "number") { - throw new Error(`Syntax error in selector "${rawSelector}" at position ${err.offset}: ${err.message}`); + throw new SyntaxError(`Syntax error in selector "${rawSelector}" at position ${err.offset}: ${err.message}`); } throw err; } diff --git a/tools/eslint/lib/util/source-code.js b/tools/eslint/lib/util/source-code.js index af01ff3faf..d3fed5989c 100644 --- a/tools/eslint/lib/util/source-code.js +++ b/tools/eslint/lib/util/source-code.js @@ -25,7 +25,6 @@ const TokenStore = require("../token-store"), * @private */ function validate(ast) { - if (!ast.tokens) { throw new Error("AST is missing the tokens array."); } @@ -44,34 +43,9 @@ function validate(ast) { } /** - * Finds a JSDoc comment node in an array of comment nodes. - * @param {ASTNode[]} comments The array of comment nodes to search. - * @param {int} line Line number to look around - * @returns {ASTNode} The node if found, null if not. - * @private - */ -function findJSDocComment(comments, line) { - - if (comments) { - for (let i = comments.length - 1; i >= 0; i--) { - if (comments[i].type === "Block" && comments[i].value.charAt(0) === "*") { - - if (line - comments[i].loc.end.line <= 1) { - return comments[i]; - } - break; - - } - } - } - - return null; -} - -/** - * Check to see if its a ES6 export declaration - * @param {ASTNode} astNode - any node - * @returns {boolean} whether the given node represents a export declaration + * Check to see if its a ES6 export declaration. + * @param {ASTNode} astNode An AST node. + * @returns {boolean} whether the given node represents an export declaration. * @private */ function looksLikeExport(astNode) { @@ -80,10 +54,11 @@ function looksLikeExport(astNode) { } /** - * Merges two sorted lists into a larger sorted list in O(n) time - * @param {Token[]} tokens The list of tokens - * @param {Token[]} comments The list of comments - * @returns {Token[]} A sorted list of tokens and comments + * Merges two sorted lists into a larger sorted list in O(n) time. + * @param {Token[]} tokens The list of tokens. + * @param {Token[]} comments The list of comments. + * @returns {Token[]} A sorted list of tokens and comments. + * @private */ function sortedMerge(tokens, comments) { const result = []; @@ -182,9 +157,9 @@ class SourceCode extends TokenStore { } /** - * Split the source code into multiple lines based on the line delimiters - * @param {string} text Source code as a string - * @returns {string[]} Array of source code lines + * Split the source code into multiple lines based on the line delimiters. + * @param {string} text Source code as a string. + * @returns {string[]} Array of source code lines. * @public */ static splitLines(text) { @@ -197,6 +172,7 @@ class SourceCode extends TokenStore { * @param {int=} beforeCount The number of characters before the node to retrieve. * @param {int=} afterCount The number of characters after the node to retrieve. * @returns {string} The text representing the AST node. + * @public */ getText(node, beforeCount, afterCount) { if (node) { @@ -209,6 +185,7 @@ class SourceCode extends TokenStore { /** * Gets the entire source text split into an array of lines. * @returns {Array} The source text as an array of lines. + * @public */ getLines() { return this.lines; @@ -217,6 +194,7 @@ class SourceCode extends TokenStore { /** * Retrieves an array containing all comments in the source code. * @returns {ASTNode[]} An array of comment nodes. + * @public */ getAllComments() { return this.ast.comments; @@ -225,7 +203,8 @@ class SourceCode extends TokenStore { /** * Gets all comments for the given node. * @param {ASTNode} node The AST node to get the comments for. - * @returns {Object} The list of comments indexed by their position. + * @returns {Object} An object containing a leading and trailing array + * of comments indexed by their position. * @public */ getComments(node) { @@ -297,47 +276,68 @@ class SourceCode extends TokenStore { /** * Retrieves the JSDoc comment for a given node. * @param {ASTNode} node The AST node to get the comment for. - * @returns {ASTNode} The Block comment node containing the JSDoc for the - * given node or null if not found. + * @returns {Token|null} The Block comment token containing the JSDoc comment + * for the given node or null if not found. * @public */ getJSDocComment(node) { + + /** + * Checks for the presence of a JSDoc comment for the given node and returns it. + * @param {ASTNode} astNode The AST node to get the comment for. + * @returns {Token|null} The Block comment token containing the JSDoc comment + * for the given node or null if not found. + * @private + */ + const findJSDocComment = astNode => { + const tokenBefore = this.getTokenBefore(astNode, { includeComments: true }); + + if ( + tokenBefore && + astUtils.isCommentToken(tokenBefore) && + tokenBefore.type === "Block" && + tokenBefore.value.charAt(0) === "*" && + astNode.loc.start.line - tokenBefore.loc.end.line <= 1 + ) { + return tokenBefore; + } + + return null; + }; let parent = node.parent; - const leadingComments = this.getCommentsBefore(node); switch (node.type) { case "ClassDeclaration": case "FunctionDeclaration": - if (looksLikeExport(parent)) { - return findJSDocComment(this.getCommentsBefore(parent), parent.loc.start.line); - } - return findJSDocComment(leadingComments, node.loc.start.line); + return findJSDocComment(looksLikeExport(parent) ? parent : node); case "ClassExpression": - return findJSDocComment(this.getCommentsBefore(parent.parent), parent.parent.loc.start.line); + return findJSDocComment(parent.parent); case "ArrowFunctionExpression": case "FunctionExpression": if (parent.type !== "CallExpression" && parent.type !== "NewExpression") { - let parentLeadingComments = this.getCommentsBefore(parent); - - while (!parentLeadingComments.length && !/Function/.test(parent.type) && parent.type !== "MethodDefinition" && parent.type !== "Property") { + while ( + !this.getCommentsBefore(parent).length && + !/Function/.test(parent.type) && + parent.type !== "MethodDefinition" && + parent.type !== "Property" + ) { parent = parent.parent; if (!parent) { break; } - - parentLeadingComments = this.getCommentsBefore(parent); } - return parent && parent.type !== "FunctionDeclaration" && parent.type !== "Program" ? findJSDocComment(parentLeadingComments, parent.loc.start.line) : null; - } else if (leadingComments.length) { - return findJSDocComment(leadingComments, node.loc.start.line); + if (parent && parent.type !== "FunctionDeclaration" && parent.type !== "Program") { + return findJSDocComment(parent); + } } - // falls through + return findJSDocComment(node); + // falls through default: return null; } @@ -347,6 +347,7 @@ class SourceCode extends TokenStore { * Gets the deepest node containing a range index. * @param {int} index Range index of the desired node. * @returns {ASTNode} The node if found or null if not found. + * @public */ getNodeByRangeIndex(index) { let result = null, @@ -380,6 +381,7 @@ class SourceCode extends TokenStore { * @param {Token} second The token to check before. * @returns {boolean} True if there is only space between tokens, false * if there is anything other than whitespace between tokens. + * @public */ isSpaceBetweenTokens(first, second) { const text = this.text.slice(first.range[1], second.range[0]); @@ -388,10 +390,11 @@ class SourceCode extends TokenStore { } /** - * Converts a source text index into a (line, column) pair. - * @param {number} index The index of a character in a file - * @returns {Object} A {line, column} location object with a 0-indexed column - */ + * Converts a source text index into a (line, column) pair. + * @param {number} index The index of a character in a file + * @returns {Object} A {line, column} location object with a 0-indexed column + * @public + */ getLocFromIndex(index) { if (typeof index !== "number") { throw new TypeError("Expected `index` to be a number."); @@ -422,12 +425,13 @@ class SourceCode extends TokenStore { } /** - * Converts a (line, column) pair into a range index. - * @param {Object} loc A line/column location - * @param {number} loc.line The line number of the location (1-indexed) - * @param {number} loc.column The column number of the location (0-indexed) - * @returns {number} The range index of the location in the file. - */ + * Converts a (line, column) pair into a range index. + * @param {Object} loc A line/column location + * @param {number} loc.line The line number of the location (1-indexed) + * @param {number} loc.column The column number of the location (0-indexed) + * @returns {number} The range index of the location in the file. + * @public + */ getIndexFromLoc(loc) { if (typeof loc !== "object" || typeof loc.line !== "number" || typeof loc.column !== "number") { throw new TypeError("Expected `loc` to be an object with numeric `line` and `column` properties."); |