From 4916dc614d3f6ed9a8329b82ca1e43273e77f506 Mon Sep 17 00:00:00 2001 From: Ilya Panasenko Date: Fri, 6 Nov 2015 12:27:37 +0200 Subject: [PATCH 01/63] Update: no-implicit-coercion validate AssignmentExpression (fixes #4348) --- docs/rules/no-implicit-coercion.md | 4 ++++ lib/rules/no-implicit-coercion.js | 19 +++++++++++++++++++ tests/lib/rules/no-implicit-coercion.js | 6 ++++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/rules/no-implicit-coercion.md b/docs/rules/no-implicit-coercion.md index 896576f265b7..2e06e76e8f6f 100644 --- a/docs/rules/no-implicit-coercion.md +++ b/docs/rules/no-implicit-coercion.md @@ -11,6 +11,7 @@ var b = ~foo.indexOf("."); var n = +foo; var n = 1 * foo; var s = "" + foo; +foo += ""; ``` Those can be replaced with the following code: @@ -21,6 +22,7 @@ var b = foo.indexOf(".") !== -1; var n = Number(foo); var n = Number(foo); var s = String(foo); +foo = String(foo); ``` ## Rule Details @@ -96,6 +98,8 @@ The following patterns are considered problems: /*eslint no-implicit-coercion: 2*/ var n = "" + foo; /*error use `String(foo)` instead.*/ + +foo += ""; /*error use `foo = String(foo)` instead.*/ ``` The following patterns are not considered problems: diff --git a/lib/rules/no-implicit-coercion.js b/lib/rules/no-implicit-coercion.js index dac5169205cd..595412b184c6 100644 --- a/lib/rules/no-implicit-coercion.js +++ b/lib/rules/no-implicit-coercion.js @@ -113,6 +113,15 @@ function isConcatWithEmptyString(node) { ); } +/** + * Checks whether or not a node is appended with an empty string. + * @param {ASTNode} node - An AssignmentExpression node to check. + * @returns {boolean} Whether or not the node is appended with an empty string. + */ +function isAppendEmptyString(node) { + return node.operator === "+=" && node.right.type === "Literal" && node.right.value === ""; +} + /** * Gets a node that is the left or right operand of a node, is not the specified literal. * @param {ASTNode} node - A BinaryExpression node to get. @@ -178,6 +187,16 @@ module.exports = function(context) { "use `String({{code}})` instead.", {code: context.getSource(getOtherOperand(node, ""))}); } + }, + + "AssignmentExpression": function(node) { + // foo += "" + if (options.string && isAppendEmptyString(node)) { + context.report( + node, + "use `{{code}} = String({{code}})` instead.", + {code: context.getSource(getOtherOperand(node, ""))}); + } } }; }; diff --git a/tests/lib/rules/no-implicit-coercion.js b/tests/lib/rules/no-implicit-coercion.js index 63038185bdf5..1ff05d87fa02 100644 --- a/tests/lib/rules/no-implicit-coercion.js +++ b/tests/lib/rules/no-implicit-coercion.js @@ -68,7 +68,8 @@ ruleTester.run("no-implicit-coercion", rule, { {code: "~foo.indexOf(1)", options: [{boolean: false}]}, {code: "+foo", options: [{number: false}]}, {code: "1*foo", options: [{number: false}]}, - {code: "\"\"+foo", options: [{string: false}]} + {code: "\"\"+foo", options: [{string: false}]}, + {code: "foo += \"\"", options: [{string: false}]} ], invalid: [ {code: "!!foo", errors: [{message: "use `Boolean(foo)` instead.", type: "UnaryExpression"}]}, @@ -82,6 +83,7 @@ ruleTester.run("no-implicit-coercion", rule, { {code: "1*foo.bar", errors: [{message: "use `Number(foo.bar)` instead.", type: "BinaryExpression"}]}, {code: "\"\"+foo", errors: [{message: "use `String(foo)` instead.", type: "BinaryExpression"}]}, {code: "foo+\"\"", errors: [{message: "use `String(foo)` instead.", type: "BinaryExpression"}]}, - {code: "\"\"+foo.bar", errors: [{message: "use `String(foo.bar)` instead.", type: "BinaryExpression"}]} + {code: "\"\"+foo.bar", errors: [{message: "use `String(foo.bar)` instead.", type: "BinaryExpression"}]}, + {code: "foo += \"\"", errors: [{message: "use `foo = String(foo)` instead.", type: "AssignmentExpression"}]} ] }); From fdfc6cd94e41c2aabfb609fcba4c9742e1cca05b Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Sun, 1 Nov 2015 21:28:53 -0600 Subject: [PATCH 02/63] Fix: eslint.report can be called w/o node if loc provided (fixes #4220) Also updated docs to note that either node or loc (or both) must be supplied. Code will now throw clearer message if neither is supplied and will not throw TypeError if node is not supplied. --- docs/developer-guide/working-with-rules.md | 2 + lib/eslint.js | 11 ++- tests/lib/eslint.js | 40 +++++++++ tests/lib/rule-context.js | 98 ++++++++++++++++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 tests/lib/rule-context.js diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index 478dcea866cc..c4c0c5d2e31f 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -118,6 +118,8 @@ The main method you'll use is `context.report()`, which publishes a warning or e * `data` - (optional) placeholder data for `message`. * `fix` - (optional) a function that applies a fix to resolve the problem. +Note that at least one of `node` or `loc` is required. + The simplest example is to use just `node` and `message`: ```js diff --git a/lib/eslint.js b/lib/eslint.js index 9c24a02191c5..9b5a278c85c9 100755 --- a/lib/eslint.js +++ b/lib/eslint.js @@ -24,7 +24,8 @@ var estraverse = require("./util/estraverse"), CommentEventGenerator = require("./util/comment-event-generator"), EventEmitter = require("events").EventEmitter, validator = require("./config-validator"), - replacements = require("../conf/replacements.json"); + replacements = require("../conf/replacements.json"), + assert = require("assert"); var DEFAULT_PARSER = require("../conf/eslint.json").parser; @@ -806,13 +807,19 @@ module.exports = (function() { * @returns {void} */ api.report = function(ruleId, severity, node, location, message, opts, fix) { + if (node) { + assert.strictEqual(typeof node, "object", "Node must be an object"); + } if (typeof location === "string") { + assert.ok(node, "Node must be provided when reporting error if location is not provided"); + fix = opts; opts = message; message = location; location = node.loc.start; } + // else, assume location was provided, so node may be omitted if (isDisabledByReportingConfig(reportingConfig, ruleId, location)) { return; @@ -835,7 +842,7 @@ module.exports = (function() { message: message, line: location.line, column: location.column + 1, // switch to 1-base instead of 0-base - nodeType: node.type, + nodeType: node && node.type, source: sourceCode.lines[location.line - 1] || "" }; diff --git a/tests/lib/eslint.js b/tests/lib/eslint.js index 8f9af5c67fd4..4a38959e2939 100644 --- a/tests/lib/eslint.js +++ b/tests/lib/eslint.js @@ -718,6 +718,46 @@ describe("eslint", function() { }); }); + it("should not throw an error if node is provided and location is not", function() { + eslint.on("Program", function(node) { + eslint.report("test-rule", 2, node, "hello world"); + }); + + assert.doesNotThrow(function() { + eslint.verify("0", config, "", true); + }); + }); + + it("should not throw an error if location is provided and node is not", function() { + eslint.on("Program", function() { + eslint.report("test-rule", 2, null, { line: 1, column: 1}, "hello world"); + }); + + assert.doesNotThrow(function() { + eslint.verify("0", config, "", true); + }); + }); + + it("should throw an error if neither node nor location is provided", function() { + eslint.on("Program", function() { + eslint.report("test-rule", 2, null, "hello world"); + }); + + assert.throws(function() { + eslint.verify("0", config, "", true); + }, /Node must be provided when reporting error if location is not provided$/); + }); + + it("should throw an error if node is not an object", function() { + eslint.on("Program", function() { + eslint.report("test-rule", 2, "not a node", "hello world"); + }); + + assert.throws(function() { + eslint.verify("0", config, "", true); + }, /Node must be an object$/); + }); + it("should correctly parse a message with object keys as numbers", function() { eslint.on("Program", function(node) { eslint.report("test-rule", 2, node, "my message {{name}}{{0}}", {0: "!", name: "testing"}); diff --git a/tests/lib/rule-context.js b/tests/lib/rule-context.js new file mode 100644 index 000000000000..a04067805dfd --- /dev/null +++ b/tests/lib/rule-context.js @@ -0,0 +1,98 @@ +/** + * @fileoverview Tests for RuleContext object. + * @author Kevin Partington + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var sinon = require("sinon"), + leche = require("leche"), + realESLint = require("../../lib/eslint"), + RuleContext = require("../../lib/rule-context"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("RuleContext", function() { + var sandbox = sinon.sandbox.create(); + + describe("report()", function() { + var ruleContext, eslint; + + beforeEach(function() { + eslint = leche.fake(realESLint); + ruleContext = new RuleContext("fake-rule", eslint, 2, {}, {}, {}); + }); + + describe("old-style call with location", function() { + it("should call eslint.report() with rule ID and severity prepended", function() { + var node = {}, + location = {}, + message = "Message", + messageOpts = {}; + + var mockESLint = sandbox.mock(eslint); + + mockESLint.expects("report") + .once() + .withArgs("fake-rule", 2, node, location, message, messageOpts); + + ruleContext.report(node, location, message, messageOpts); + + mockESLint.verify(); + }); + }); + + describe("old-style call without location", function() { + it("should call eslint.report() with rule ID and severity prepended", function() { + var node = {}, + message = "Message", + messageOpts = {}; + + var mockESLint = sandbox.mock(eslint); + + mockESLint.expects("report") + .once() + .withArgs("fake-rule", 2, node, message, messageOpts); + + ruleContext.report(node, message, messageOpts); + + mockESLint.verify(); + }); + }); + + describe("new-style call with all options", function() { + it("should call eslint.report() with rule ID and severity prepended and all new-style options", function() { + var node = {}, + location = {}, + message = "Message", + messageOpts = {}, + fixerObj = {}, + fix = sandbox.mock().returns(fixerObj).once(); + + var mockESLint = sandbox.mock(eslint); + + mockESLint.expects("report") + .once() + .withArgs("fake-rule", 2, node, location, message, messageOpts, fixerObj); + + ruleContext.report({ + node: node, + loc: location, + message: message, + data: messageOpts, + fix: fix + }); + + fix.verify(); + mockESLint.verify(); + }); + }); + }); + +}); From ae696ebb2bf81e21c9debb7518ca14a2101c4fe5 Mon Sep 17 00:00:00 2001 From: Jamund Ferguson Date: Wed, 4 Nov 2015 20:36:55 -0800 Subject: [PATCH 03/63] Update: Add Popular Style Guides (fixes #4320) --- lib/config-initializer.js | 173 ++++++++++++++++++++++---------- tests/lib/config-initializer.js | 17 ++++ 2 files changed, 139 insertions(+), 51 deletions(-) diff --git a/lib/config-initializer.js b/lib/config-initializer.js index 1cd197ffc115..a8caad46a4b6 100644 --- a/lib/config-initializer.js +++ b/lib/config-initializer.js @@ -26,6 +26,22 @@ function writeFile(config, isJson, callback) { callback(e); return; } + + // install any external configs as well as any included plugins + if (config.extends && config.extends.indexOf("eslint") === -1) { + exec("npm i eslint-config-" + config.extends + " --save-dev", function(err) { + + if (err) { + return callback(err); + } + + // TODO: consider supporting more than 1 plugin though it's required yet. + exec("npm i eslint-plugin-" + config.plugins[0] + " --save-dev", callback); + }); + return; + } + + // install the react plugin if it was explictly chosen if (config.plugins && config.plugins.indexOf("react") >= 0) { exec("npm i eslint-plugin-react --save-dev", callback); return; @@ -60,6 +76,23 @@ function processAnswers(answers) { return config; } +/** + * process user's style guide of choice and return an appropriate config object. + * @param {string} guide name of the chosen style guide + * @returns {object} config object + */ +function getConfigForStyleGuide(guide) { + var guides = { + google: {extends: "google"}, + airbnb: {extends: "airbnb", plugins: ["react"]}, + standard: {extends: "standard", plugins: ["standard"]} + }; + if (!guides[guide]) { + throw new Error("You referenced an unsupported guide."); + } + return guides[guide]; +} + /* istanbul ignore next: no need to test inquirer*/ /** * Ask use a few questions on command prompt @@ -70,57 +103,18 @@ function promptUser(callback) { inquirer.prompt([ { type: "list", - name: "indent", - message: "What style of indentation do you use?", - default: "tabs", - choices: [{name: "Tabs", value: "tab"}, {name: "Spaces", value: 4}] - }, - { - type: "list", - name: "quotes", - message: "What quotes do you use for strings?", - default: "double", - choices: [{name: "Double", value: "double"}, {name: "Single", value: "single"}] + name: "source", + message: "How would you like to configure ESLint?", + default: "prompt", + choices: [{name: "Answer questions about your style", value: "prompt"}, {name: "Use a popular style guide", value: "guide"}] }, { type: "list", - name: "linebreak", - message: "What line endings do you use?", - default: "unix", - choices: [{name: "Unix", value: "unix"}, {name: "Windows", value: "windows"}] - }, - { - type: "confirm", - name: "semi", - message: "Do you require semicolons?", - default: true - }, - { - type: "confirm", - name: "es6", - message: "Are you using ECMAScript 6 features?", - default: false - }, - { - type: "checkbox", - name: "env", - message: "Where will your code run?", - default: ["browser"], - choices: [{name: "Node", value: "node"}, {name: "Browser", value: "browser"}] - }, - { - type: "confirm", - name: "jsx", - message: "Do you use JSX?", - default: false - }, - { - type: "confirm", - name: "react", - message: "Do you use React", - default: false, + name: "styleguide", + 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.jsx; + return answers.source === "guide"; } }, { @@ -128,15 +122,92 @@ function promptUser(callback) { name: "format", message: "What format do you want your config file to be in?", default: "JSON", - choices: ["JSON", "YAML"] + choices: ["JSON", "YAML"], + when: function(answers) { + return answers.source === "guide"; + } } - ], function(answers) { - var config = processAnswers(answers); - writeFile(config, answers.format === "JSON", callback); + ], function(earlyAnswers) { + + // early exit if you are using a style guide + if (earlyAnswers.source === "guide") { + writeFile(getConfigForStyleGuide(earlyAnswers.styleguide), earlyAnswers.format === "JSON", callback); + return; + } + + // continue with the style questions otherwise... + inquirer.prompt([ + { + type: "list", + name: "indent", + message: "What style of indentation do you use?", + default: "tabs", + choices: [{name: "Tabs", value: "tab"}, {name: "Spaces", value: 4}] + }, + { + type: "list", + name: "quotes", + message: "What quotes do you use for strings?", + default: "double", + choices: [{name: "Double", value: "double"}, {name: "Single", value: "single"}] + }, + { + type: "list", + name: "linebreak", + message: "What line endings do you use?", + default: "unix", + choices: [{name: "Unix", value: "unix"}, {name: "Windows", value: "windows"}] + }, + { + type: "confirm", + name: "semi", + message: "Do you require semicolons?", + default: true + }, + { + type: "confirm", + name: "es6", + message: "Are you using ECMAScript 6 features?", + default: false + }, + { + type: "checkbox", + name: "env", + message: "Where will your code run?", + default: ["browser"], + choices: [{name: "Node", value: "node"}, {name: "Browser", value: "browser"}] + }, + { + type: "confirm", + name: "jsx", + message: "Do you use JSX?", + default: false + }, + { + type: "confirm", + name: "react", + message: "Do you use React", + default: false, + when: function(answers) { + return answers.jsx; + } + }, + { + type: "list", + name: "format", + message: "What format do you want your config file to be in?", + default: "JSON", + choices: ["JSON", "YAML"] + } + ], function(answers) { + var config = processAnswers(answers); + writeFile(config, answers.format === "JSON", callback); + }); }); } var init = { + getConfigForStyleGuide: getConfigForStyleGuide, processAnswers: processAnswers, initializeConfig: /* istanbul ignore next */ function(callback) { promptUser(callback); diff --git a/tests/lib/config-initializer.js b/tests/lib/config-initializer.js index f5ac73fbcebb..2d8ba414dba3 100644 --- a/tests/lib/config-initializer.js +++ b/tests/lib/config-initializer.js @@ -67,4 +67,21 @@ describe("configInitializer", function() { var config = init.processAnswers(answers); assert.equal(config.extends, "eslint:recommended"); }); + it("should support the google style guide", function() { + var config = init.getConfigForStyleGuide("google"); + assert.deepEqual(config, {extends: "google"}); + }); + it("should support the airbnb style guide", function() { + var config = init.getConfigForStyleGuide("airbnb"); + assert.deepEqual(config, {extends: "airbnb", plugins: ["react"]}); + }); + it("should support the standard style guide", function() { + var config = init.getConfigForStyleGuide("standard"); + assert.deepEqual(config, {extends: "standard", plugins: ["standard"]}); + }); + it("should throw when encountering an unsupported style guide", function() { + assert.throws(function() { + init.getConfigForStyleGuide("non-standard"); + }, "You referenced an unsupported guide."); + }); }); From b9d1fc67bee75183765beca713b99cb92aebef43 Mon Sep 17 00:00:00 2001 From: Marius Schulz Date: Mon, 9 Nov 2015 22:27:47 +0100 Subject: [PATCH 04/63] Fix: Display singular/plural version of "line" in message (fixes #4359) --- lib/rules/no-multiple-empty-lines.js | 2 +- tests/lib/rules/no-multiple-empty-lines.js | 68 ++++++++++++---------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/lib/rules/no-multiple-empty-lines.js b/lib/rules/no-multiple-empty-lines.js index 77bb341aef00..46080277bc37 100644 --- a/lib/rules/no-multiple-empty-lines.js +++ b/lib/rules/no-multiple-empty-lines.js @@ -88,7 +88,7 @@ module.exports = function(context) { // within the file, not at the end if (blankCounter >= max) { context.report(node, location, - "More than " + max + " blank lines not allowed."); + "More than " + max + " blank " + (max === 1 ? "line" : "lines") + " not allowed."); } } else { // inside the last blank lines diff --git a/tests/lib/rules/no-multiple-empty-lines.js b/tests/lib/rules/no-multiple-empty-lines.js index 06abe2349219..494b42ddb952 100644 --- a/tests/lib/rules/no-multiple-empty-lines.js +++ b/tests/lib/rules/no-multiple-empty-lines.js @@ -16,12 +16,7 @@ var rule = require("../../../lib/rules/no-multiple-empty-lines"), // Tests //------------------------------------------------------------------------------ -var ruleTester = new RuleTester(), - ruleArgs = [ - { - max: 2 - } - ]; +var ruleTester = new RuleTester(); /** * Creates the expected error message object for the specified number of lines @@ -30,12 +25,12 @@ var ruleTester = new RuleTester(), * @private */ function getExpectedError(lines) { - if (typeof lines !== "number") { - lines = 2; - } + var message = lines === 1 + ? "More than 1 blank line not allowed." + : "More than " + lines + " blank lines not allowed."; return { - message: "More than " + lines + " blank lines not allowed.", + message: message, type: "Program" }; } @@ -60,13 +55,21 @@ function getExpectedErrorEOF(lines) { ruleTester.run("no-multiple-empty-lines", rule, { valid: [ + { + code: "// valid 1\nvar a = 5;\nvar b = 3;", + options: [ { max: 1 } ] + }, + { + code: "// valid 1\n\nvar a = 5;\n\nvar b = 3;", + options: [ { max: 1 } ] + }, { code: "// valid 1\nvar a = 5;\n\nvar b = 3;", - options: ruleArgs + options: [ { max: 2 } ] }, { code: "// valid 2\nvar a = 5,\n b = 3;", - options: ruleArgs + options: [ { max: 2 } ] }, { code: "// valid 3\nvar a = 5;\n\n\n\n\nvar b = 3;", @@ -78,18 +81,18 @@ ruleTester.run("no-multiple-empty-lines", rule, { }, { code: "// valid 5\nvar a = 5;\n", - options: [{ max: 0 } ] + options: [ { max: 0 } ] }, // template strings { code: "x = `\n\n\n\nhi\n\n\n\n`", - options: ruleArgs, + options: [ { max: 2 } ], ecmaFeatures: { templateStrings: true } }, { code: "`\n\n`", - options: [{ max: 0 }], + options: [ { max: 0 } ], ecmaFeatures: { templateStrings: true } }, @@ -104,45 +107,50 @@ ruleTester.run("no-multiple-empty-lines", rule, { ], invalid: [ + { + code: "// invalid 1\nvar a = 5;\n\n\nvar b = 3;", + errors: [ getExpectedError(1) ], + options: [ { max: 1 } ] + }, { code: "// invalid 1\n\n\n\n\nvar a = 5;", - errors: [ getExpectedError() ], - options: ruleArgs + errors: [ getExpectedError(2) ], + options: [ { max: 2 } ] }, { code: "// invalid 2\nvar a = 5;\n\n\n\n", - errors: [ getExpectedError() ], - options: ruleArgs + errors: [ getExpectedError(2) ], + options: [ { max: 2 } ] }, { code: "// invalid 2\nvar a = 5;\n \n \n \n", - errors: [ getExpectedError() ], - options: ruleArgs + errors: [ getExpectedError(2) ], + options: [ { max: 2 } ] }, { code: "// invalid 3\nvar a=5;\n\n\n\nvar b = 3;", - errors: [ getExpectedError() ], - options: ruleArgs + errors: [ getExpectedError(2) ], + options: [ { max: 2 } ] }, { code: "// invalid 3\nvar a=5;\n\n\n\nvar b = 3;\n", - errors: [ getExpectedError() ], - options: ruleArgs + errors: [ getExpectedError(2) ], + options: [ { max: 2 } ] }, { code: "// invalid 4\nvar a = 5;\n\n\n\nb = 3;\nvar c = 5;\n\n\n\nvar d = 3;", errors: 2, - options: ruleArgs + options: [ { max: 2 } ] }, { code: "// invalid 5\nvar a = 5;\n\n\n\n\n\n\n\n\n\n\n\n\n\nb = 3;", - errors: [ getExpectedError() ], - options: ruleArgs + errors: [ getExpectedError(2) ], + options: [ { max: 2 } ] }, { code: "// invalid 6\nvar a=5;\n\n\n\n\n", - errors: [ getExpectedError() ], - options: ruleArgs + errors: [ getExpectedError(2) ], + options: [ { max: 2 } ] }, { code: "// invalid 7\nvar a = 5;\n\nvar b = 3;", From ea1da0ba9c6989259231683b65be58bc7296db5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski?= Date: Mon, 9 Nov 2015 19:04:55 +0100 Subject: [PATCH 05/63] Fix: Add the missing "as-needed" docs to the radix rule (fixes #4364) Refs #4048 Refs #4084 Refs #4351 --- docs/rules/radix.md | 57 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/rules/radix.md b/docs/rules/radix.md index c7cf2222e660..1e245c663fa9 100644 --- a/docs/rules/radix.md +++ b/docs/rules/radix.md @@ -16,18 +16,39 @@ var num = parseInt("071", 10); // 71 ECMAScript 5 changed the behavior of `parseInt()` so that it no longer autodetects octal literals and instead treats them as decimal literals. However, the differences between hexadecimal and decimal interpretation of the first parameter causes many developers to continue using the radix parameter to ensure the string is interpreted in the intended way. +On the other hand, if the code is targeting only ES5-compliant environments passing the radix `10` may be redundant. In such a case you might want to disallow using such a radix. + ## Rule Details -This rule is aimed at preventing the unintended conversion of a string to a number of a different base than intended. +This rule is aimed at preventing the unintended conversion of a string to a number of a different base than intended or at preventing the redundant `10` radix if targeting modern environments only. + +### Options + +There are two options for this rule: + +* `"always"` enforces providing a radix (default) +* `"as-needed"` disallows providing the `10` radix + +Depending on your coding conventions, you can choose either option by specifying it in your configuration: + +```json +"radix": [2, "always"] +``` + +#### always The following patterns are considered problems: ```js /*eslint radix: 2*/ -var num = parseInt("071"); /*error Missing radix parameter.*/ +var num = parseInt("071"); /*error Missing radix parameter.*/ -var num = parseInt(someValue); /*error Missing radix parameter.*/ +var num = parseInt(someValue); /*error Missing radix parameter.*/ + +var num = parseInt("071", "abc"); /*error Invalid radix parameter.*/ + +var num = parseInt(); /*error Missing parameters.*/ ``` The following patterns are not considered problems: @@ -37,12 +58,40 @@ The following patterns are not considered problems: var num = parseInt("071", 10); +var num = parseInt("071", 8); + +var num = parseFloat(someValue); +``` + +#### as-needed + +The following patterns are considered problems: + +```js +/*eslint radix: [2. "as-needed"] */ + +var num = parseInt("071", 10); /*error Redundant radix parameter.*/ + +var num = parseInt("071", "abc"); /*error Invalid radix parameter.*/ + +var num = parseInt(); /*error Missing parameters.*/ +``` + +The following patterns are not considered problems: + +```js +/*eslint radix: [2. "as-needed"] */ + +var num = parseInt("071"); + +var num = parseInt("071", 8); + var num = parseFloat(someValue); ``` ## When Not To Use It -If you are certain of the first argument's format, then the second argument is unnecessary and you can safely turn this rule off. +If you don't want to enforce either presence or omission of the `10` radix value you can turn this rule off. ## Further Reading From c58f2949b8160ac2add8cf0301ad795f3c8f7ce6 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 11 Nov 2015 16:17:23 -0800 Subject: [PATCH 06/63] Fix: no-undef-init should ignore const (fixes #4284) --- docs/rules/no-undef-init.md | 1 + lib/rules/no-undef-init.js | 8 +++++--- tests/lib/rules/no-undef-init.js | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/rules/no-undef-init.md b/docs/rules/no-undef-init.md index ef4ef582e8f0..6c0e793c6d6b 100644 --- a/docs/rules/no-undef-init.md +++ b/docs/rules/no-undef-init.md @@ -39,6 +39,7 @@ The following patterns are not considered problems: var foo; let bar; +const baz = undefined; ``` ## When Not To Use It diff --git a/lib/rules/no-undef-init.js b/lib/rules/no-undef-init.js index fc08c9b5088a..1348c641e362 100644 --- a/lib/rules/no-undef-init.js +++ b/lib/rules/no-undef-init.js @@ -1,6 +1,8 @@ /** * @fileoverview Rule to flag when initializing to undefined * @author Ilya Volodin + * @copyright 2013 Ilya Volodin. All rights reserved. + * See LICENSE in root directory for full license. */ "use strict"; @@ -14,10 +16,10 @@ module.exports = function(context) { return { "VariableDeclarator": function(node) { - var name = node.id.name; - var init = node.init && node.init.name; + var name = node.id.name, + init = node.init && node.init.name; - if (init === "undefined") { + if (init === "undefined" && node.parent.kind !== "const") { context.report(node, "It's not necessary to initialize '{{name}}' to undefined.", { name: name }); } } diff --git a/tests/lib/rules/no-undef-init.js b/tests/lib/rules/no-undef-init.js index 64666d586e71..9344805cc1a2 100644 --- a/tests/lib/rules/no-undef-init.js +++ b/tests/lib/rules/no-undef-init.js @@ -1,6 +1,8 @@ /** * @fileoverview Tests for undefined rule. * @author Ilya Volodin + * @copyright 2013 Ilya Volodin. All rights reserved. + * See LICENSE in root directory for full license. */ "use strict"; @@ -19,7 +21,8 @@ var rule = require("../../../lib/rules/no-undef-init"), var ruleTester = new RuleTester(); ruleTester.run("no-undef-init", rule, { valid: [ - "var a;" + "var a;", + { code: "const foo = undefined", ecmaFeatures: { blockBindings: true } } ], invalid: [ { code: "var a = undefined;", errors: [{ message: "It's not necessary to initialize 'a' to undefined.", type: "VariableDeclarator"}] } From 22d1e037f148732758396c7e6bd1db4cdd63182d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 11 Nov 2015 16:43:57 -0800 Subject: [PATCH 07/63] Update: Refactor eslint.verify args (fixes #4395) --- docs/developer-guide/nodejs-api.md | 11 +++---- lib/eslint.js | 20 ++++++++----- tests/lib/eslint.js | 46 +++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index a18aa072cfbc..b9776e6cff6c 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -28,8 +28,9 @@ The most important method on `linter` is `verify()`, which initiates linting of * `code` - the source code to lint (a string or instance of `SourceCode`). * `config` - a configuration object. -* `filename` - (optional) the filename to associate with the source code. -* `saveState` - (optional) set to true to maintain the internal state of `linter` after linting (mostly used for testing purposes). +* `options` - (optional) Additional options for this run. + * `filename` - (optional) the filename to associate with the source code. + * `saveState` - (optional) set to true to maintain the internal state of `linter` after linting (mostly used for testing purposes). You can call `verify()` like this: @@ -40,7 +41,7 @@ var messages = linter.verify("var foo;", { rules: { semi: 2 } -}, "foo.js"); +}, { filename: "foo.js" }); // or using SourceCode @@ -53,7 +54,7 @@ var messages = linter.verify(code, { rules: { semi: 2 } -}, "foo.js"); +}, { filename: "foo.js" }); ``` The `verify()` method returns an array of objects containing information about the linting warnings and errors. Here's an example: @@ -95,7 +96,7 @@ var messages = linter.verify("var foo = bar;", { rules: { semi: 2 } -}, "foo.js"); +}, { filename: "foo.js" }); var code = linter.getSourceCode(); diff --git a/lib/eslint.js b/lib/eslint.js index 9b5a278c85c9..99ec893e7efc 100755 --- a/lib/eslint.js +++ b/lib/eslint.js @@ -606,13 +606,14 @@ module.exports = (function() { * Verifies the text against the rules specified by the second argument. * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object. * @param {Object} config An object whose keys specify the rules to use. - * @param {string=} filename The optional filename of the file being checked. - * If this is not set, the filename will default to '' in the rule context. - * @param {boolean=} saveState Indicates if the state from the last run should be saved. + * @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked. + * If this is not set, the filename will default to '' in the rule context. If + * an object, then it has "filename" and "saveState" properties. + * @param {boolean} [saveState] Indicates if the state from the last run should be saved. * Mostly useful for testing purposes. * @returns {Object[]} The results as an array of messages or null if no messages. */ - api.verify = function(textOrSourceCode, config, filename, saveState) { + api.verify = function(textOrSourceCode, config, filenameOrOptions, saveState) { var ast, shebang, @@ -620,8 +621,13 @@ module.exports = (function() { ecmaVersion, text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null; - // set the current parsed filename - currentFilename = filename; + // evaluate arguments + if (typeof filenameOrOptions === "object") { + currentFilename = filenameOrOptions.filename; + saveState = filenameOrOptions.saveState; + } else { + currentFilename = filenameOrOptions; + } if (!saveState) { this.reset(); @@ -668,7 +674,7 @@ module.exports = (function() { if (ast) { // parse global comments and modify config - config = modifyConfigsFromComments(filename, ast, config, reportingConfig, messages); + config = modifyConfigsFromComments(currentFilename, ast, config, reportingConfig, messages); // enable appropriate rules Object.keys(config.rules).filter(function(key) { diff --git a/tests/lib/eslint.js b/tests/lib/eslint.js index 4a38959e2939..b6543ead9360 100644 --- a/tests/lib/eslint.js +++ b/tests/lib/eslint.js @@ -1,8 +1,10 @@ -/* globals window */ /** * @fileoverview Tests for eslint object. * @author Nicholas C. Zakas + * @copyright 2013 Nicholas C. Zakas. All rights reserved. + * See LICENSE file in root directory for full license. */ +/* globals window */ "use strict"; @@ -2336,6 +2338,48 @@ describe("eslint", function() { describe("verify()", function() { + describe("filenames", function() { + it("should allow filename to be passed on options object", function() { + + eslint.verify("foo;", {}, { filename: "foo.js"}); + var result = eslint.getFilename(); + assert.equal(result, "foo.js"); + }); + + it("should allow filename to be passed as third argument", function() { + + eslint.verify("foo;", {}, "foo.js"); + var result = eslint.getFilename(); + assert.equal(result, "foo.js"); + }); + + it("should default filename to when options object doesn't have filename", function() { + + eslint.verify("foo;", {}, {}); + var result = eslint.getFilename(); + assert.equal(result, ""); + }); + + it("should default filename to when only two arguments are passed", function() { + + eslint.verify("foo;", {}); + var result = eslint.getFilename(); + assert.equal(result, ""); + }); + }); + + describe("saveState", function() { + it("should save the state when saveState is passed as an option", function() { + + var spy = sinon.spy(eslint, "reset"); + eslint.verify("foo;", {}, { saveState: true }); + assert.equal(spy.callCount, 0); + }); + + + }); + + it("should report warnings in order by line and column when called", function() { var code = "foo()\n alert('test')"; From a4b3114804c730d6176fe2d80cc94a890741be11 Mon Sep 17 00:00:00 2001 From: alberto Date: Thu, 12 Nov 2015 21:50:20 +0100 Subject: [PATCH 08/63] Fix: Handle comments in comma-spacing (fixes #4389) --- lib/rules/comma-spacing.js | 91 +++++++++----------------------- tests/lib/rules/comma-spacing.js | 21 ++++++-- 2 files changed, 43 insertions(+), 69 deletions(-) diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js index f9474be6455c..de7da6aff6a1 100644 --- a/lib/rules/comma-spacing.js +++ b/lib/rules/comma-spacing.js @@ -14,6 +14,7 @@ var astUtils = require("../ast-utils"); module.exports = function(context) { var sourceCode = context.getSourceCode(); + var tokensAndComments = sourceCode.tokensAndComments; var options = { before: context.options[0] ? !!context.options[0].before : false, @@ -24,10 +25,6 @@ module.exports = function(context) { // Helpers //-------------------------------------------------------------------------- - // the index of the last comment that was checked - var lastCommentIndex = 0; - var allComments; - // list of comma tokens to ignore for the check of leading whitespace var commaTokensToIgnore = []; @@ -41,39 +38,6 @@ module.exports = function(context) { return !!token && (token.type === "Punctuator") && (token.value === ","); } - /** - * Determines if a given source index is in a comment or not by checking - * the index against the comment range. Since the check goes straight - * through the file, once an index is passed a certain comment, we can - * go to the next comment to check that. - * @param {int} index The source index to check. - * @param {ASTNode[]} comments An array of comment nodes. - * @returns {boolean} True if the index is within a comment, false if not. - * @private - */ - function isIndexInComment(index, comments) { - - var comment; - lastCommentIndex = 0; - - while (lastCommentIndex < comments.length) { - - comment = comments[lastCommentIndex]; - - if (comment.range[0] <= index && index < comment.range[1]) { - return true; - } else if (index > comment.range[1]) { - lastCommentIndex++; - } else { - break; - } - - } - - return false; - } - - /** * Reports a spacing error with an appropriate message. * @param {ASTNode} node The binary expression node to report. @@ -93,9 +57,6 @@ module.exports = function(context) { return fixer.insertTextAfter(node, " "); } } else { - /* - * Comments handling - */ var start, end; var newText = ""; @@ -107,11 +68,6 @@ module.exports = function(context) { end = otherNode.range[0]; } - for (var i = start; i < end; i++) { - if (isIndexInComment(i, allComments)) { - newText += context.getSource()[i]; - } - } return fixer.replaceTextRange([start, end], newText); } }, @@ -137,6 +93,11 @@ module.exports = function(context) { ) { report(reportItem, "before", tokens.left); } + + if (tokens.right && !options.after && tokens.right.type === "Line") { + return false; + } + if (tokens.right && astUtils.isTokenOnSameLine(tokens.comma, tokens.right) && (options.after !== sourceCode.isSpaceBetweenTokens(tokens.comma, tokens.right)) ) { @@ -176,30 +137,28 @@ module.exports = function(context) { return { "Program:exit": function() { - var source = context.getSource(), - pattern = /,/g, - commaToken, - previousToken, + var previousToken, nextToken; - allComments = context.getAllComments(); - while (pattern.test(source)) { - - // do not flag anything inside of comments - if (!isIndexInComment(pattern.lastIndex, allComments)) { - commaToken = context.getTokenByRangeStart(pattern.lastIndex - 1); - - if (commaToken && commaToken.type !== "JSXText") { - previousToken = context.getTokenBefore(commaToken); - nextToken = context.getTokenAfter(commaToken); - validateCommaItemSpacing({ - comma: commaToken, - left: isComma(previousToken) || commaTokensToIgnore.indexOf(commaToken) > -1 ? null : previousToken, - right: isComma(nextToken) ? null : nextToken - }, commaToken); - } + tokensAndComments.forEach(function(token, i) { + + if (!isComma(token)) { + return; } - } + + if (token && token.type === "JSXText") { + return; + } + + previousToken = tokensAndComments[i - 1]; + nextToken = tokensAndComments[i + 1]; + + validateCommaItemSpacing({ + comma: token, + left: isComma(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken, + right: isComma(nextToken) ? null : nextToken + }, token); + }); }, "ArrayExpression": addNullElementsToIgnoreList, "ArrayPattern": addNullElementsToIgnoreList diff --git a/tests/lib/rules/comma-spacing.js b/tests/lib/rules/comma-spacing.js index ac939274f0c3..cc8ee981fded 100644 --- a/tests/lib/rules/comma-spacing.js +++ b/tests/lib/rules/comma-spacing.js @@ -21,7 +21,12 @@ var ruleTester = new RuleTester(); ruleTester.run("comma-spacing", rule, { valid: [ "myfunc(404, true/* bla bla bla */, 'hello');", + "myfunc(404, true /* bla bla bla */, 'hello');", "myfunc(404, true/* bla bla bla *//* hi */, 'hello');", + "myfunc(404, true/* bla bla bla */ /* hi */, 'hello');", + "myfunc(404, true, /* bla bla bla */ 'hello');", + "myfunc(404, // comment\n true, /* bla bla bla */ 'hello');", + {code: "myfunc(404, // comment\n true,/* bla bla bla */ 'hello');", options: [{before: false, after: false}]}, "var a = 1, b = 2;", "var arr = [, ];", "var arr = [1, ];", @@ -456,11 +461,21 @@ ruleTester.run("comma-spacing", rule, { ] }, { - code: "myfunc(404, true/* bla bla bla */ /* hi */, 'hello');", - output: "myfunc(404, true/* bla bla bla *//* hi */, 'hello');", + code: "myfunc(404, true,/* bla bla bla */ 'hello');", + output: "myfunc(404, true, /* bla bla bla */ 'hello');", errors: [ { - message: "There should be no space before ','.", + message: "A space is required after ','.", + type: "Punctuator" + } + ] + }, + { + code: "myfunc(404,// comment\n true, 'hello');", + output: "myfunc(404, // comment\n true, 'hello');", + errors: [ + { + message: "A space is required after ','.", type: "Punctuator" } ] From 013664f5569c69821c9b696fbc04793d7f4afb84 Mon Sep 17 00:00:00 2001 From: alberto Date: Sat, 14 Nov 2015 13:29:23 +0100 Subject: [PATCH 09/63] Update: Allow empty arrow body (fixes #4411) --- docs/rules/arrow-body-style.md | 14 +------------- lib/rules/arrow-body-style.js | 19 +++---------------- tests/lib/rules/arrow-body-style.js | 8 +------- 3 files changed, 5 insertions(+), 36 deletions(-) diff --git a/docs/rules/arrow-body-style.md b/docs/rules/arrow-body-style.md index d17e7bc46bf2..2e3a2c6c45f2 100644 --- a/docs/rules/arrow-body-style.md +++ b/docs/rules/arrow-body-style.md @@ -2,17 +2,6 @@ Arrow functions can omit braces when there is a single statement in the body. This rule enforces the consistent use of braces in arrow functions. -Additionally, this rule specifically warns against a possible developer error when the intention is to return an empty object literal but creates an empty block instead, returning undefined. - -```js -/*eslint-env es6*/ -// Bad -var foo = () => {}; - -// Good -var foo = () => ({}); -``` - ## Rule Details This rule can enforce the use of braces around arrow function body. @@ -61,8 +50,6 @@ When the rule is set to `"as-needed"` the following patterns are considered prob let foo = () => { return 0; }; - -let foo = () => {}; ``` The following patterns are not considered problems: @@ -77,6 +64,7 @@ let foo = (retv, name) => { return retv; }; let foo = () => { bar(); }; +let foo = () => {}; let foo = () => { /* do nothing */ }; let foo = () => { // do nothing. diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js index 92c8dc33a33e..e7e6b6bdb473 100644 --- a/lib/rules/arrow-body-style.js +++ b/lib/rules/arrow-body-style.js @@ -24,29 +24,16 @@ module.exports = function(context) { if (arrowBody.type === "BlockStatement") { var blockBody = arrowBody.body; - if (blockBody.length > 1) { + if (blockBody.length !== 1) { return; } - if (blockBody.length === 0) { - var hasComments = context.getComments(arrowBody).trailing.length > 0; - if (hasComments) { - return; - } - + if (asNeeded && blockBody[0].type === "ReturnStatement") { context.report({ node: node, loc: arrowBody.loc.start, - message: "Unexpected empty block in arrow body." + message: "Unexpected block statement surrounding arrow body." }); - } else { - if (asNeeded && blockBody[0].type === "ReturnStatement") { - context.report({ - node: node, - loc: arrowBody.loc.start, - message: "Unexpected block statement surrounding arrow body." - }); - } } } else { if (always) { diff --git a/tests/lib/rules/arrow-body-style.js b/tests/lib/rules/arrow-body-style.js index f10e2573303d..c00438c6c008 100644 --- a/tests/lib/rules/arrow-body-style.js +++ b/tests/lib/rules/arrow-body-style.js @@ -20,6 +20,7 @@ var rule = require("../../../lib/rules/arrow-body-style"), var ruleTester = new RuleTester(); ruleTester.run("arrow-body-style", rule, { valid: [ + { code: "var foo = () => {};", ecmaFeatures: { arrowFunctions: true } }, { code: "var foo = () => 0;", ecmaFeatures: { arrowFunctions: true } }, { code: "var addToB = (a) => { b = b + a };", ecmaFeatures: { arrowFunctions: true } }, { code: "var foo = () => { /* do nothing */ };", ecmaFeatures: { arrowFunctions: true } }, @@ -33,13 +34,6 @@ ruleTester.run("arrow-body-style", rule, { { code: "var foo = () => { return bar(); };", ecmaFeatures: { arrowFunctions: true }, options: ["always"] } ], invalid: [ - { - code: "var foo = () => {};", - ecmaFeatures: { arrowFunctions: true }, - errors: [ - { line: 1, column: 17, type: "ArrowFunctionExpression", message: "Unexpected empty block in arrow body." } - ] - }, { code: "var foo = () => 0;", ecmaFeatures: { arrowFunctions: true }, From 1ff31d391c44cda78e75569c96733f1b9b98a8a9 Mon Sep 17 00:00:00 2001 From: alberto Date: Thu, 12 Nov 2015 22:45:34 +0100 Subject: [PATCH 10/63] Docs: Document semi-spacing behaviour (fixes #4404) --- docs/rules/semi-spacing.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/rules/semi-spacing.md b/docs/rules/semi-spacing.md index 0339c48ef668..92acaa04797e 100644 --- a/docs/rules/semi-spacing.md +++ b/docs/rules/semi-spacing.md @@ -14,8 +14,13 @@ var c = "d";var e = "f"; This rule aims to enforce spacing around a semicolon. This rule prevents the use of spaces before a semicolon in expressions. -This rule doesn't check spacing which is after semicolons if the semicolon is before a closing parenthesis (`)` or `}`). -That spacing is checked by `space-in-parens` or `block-spacing`. +This rule doesn't check spacing in the following cases: + +* The spacing after the semicolon if it is the first token in the line. + +* The spacing before the semicolon if it is after an opening parenthesis (`(` or `{`), or the spacing after the semicolon if it is before a closing parenthesis (`)` or `}`). That spacing is checked by `space-in-parens` or `block-spacing`. + +* The spacing around the semicolon in a for loop with an empty condition (`for(;;)`). ### Options @@ -57,6 +62,9 @@ var foo; var bar; throw new Error("error"); while (a) { break; } for (i = 0; i < 10; i++) {} +for (;;) {} +if (true) {;} +;foo(); ``` #### {"before": true, "after": false} From 1f265b1ff104db6cab6cceac92914bdf4bfc7aa8 Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Sat, 14 Nov 2015 09:51:20 -0600 Subject: [PATCH 11/63] Fix: eqeqeq autofix avoids clashes with space-infix-ops (fixes #4423) --- lib/rules/eqeqeq.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/rules/eqeqeq.js b/lib/rules/eqeqeq.js index 54e8690a4b68..5c34346cb95a 100644 --- a/lib/rules/eqeqeq.js +++ b/lib/rules/eqeqeq.js @@ -13,7 +13,11 @@ module.exports = function(context) { - var sourceCode = context.getSourceCode(); + var sourceCode = context.getSourceCode(), + replacements = { + "==": "===", + "!=": "!==" + }; /** * Checks if an expression is a typeof expression @@ -89,7 +93,7 @@ module.exports = function(context) { message: "Expected '{{op}}=' and instead saw '{{op}}'.", data: { op: node.operator }, fix: function(fixer) { - return fixer.insertTextAfter(sourceCode.getTokenAfter(node.left), "="); + return fixer.replaceText(sourceCode.getTokenAfter(node.left), replacements[node.operator]); } }); From 59920bfae1cfc4867c0ba7def3fb6131157a150d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Sat, 14 Nov 2015 08:05:10 -0800 Subject: [PATCH 12/63] Revert "Update: Allow empty arrow body (fixes #4411)" --- docs/rules/arrow-body-style.md | 14 +++++++++++++- lib/rules/arrow-body-style.js | 19 ++++++++++++++++--- tests/lib/rules/arrow-body-style.js | 8 +++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/rules/arrow-body-style.md b/docs/rules/arrow-body-style.md index 2e3a2c6c45f2..d17e7bc46bf2 100644 --- a/docs/rules/arrow-body-style.md +++ b/docs/rules/arrow-body-style.md @@ -2,6 +2,17 @@ Arrow functions can omit braces when there is a single statement in the body. This rule enforces the consistent use of braces in arrow functions. +Additionally, this rule specifically warns against a possible developer error when the intention is to return an empty object literal but creates an empty block instead, returning undefined. + +```js +/*eslint-env es6*/ +// Bad +var foo = () => {}; + +// Good +var foo = () => ({}); +``` + ## Rule Details This rule can enforce the use of braces around arrow function body. @@ -50,6 +61,8 @@ When the rule is set to `"as-needed"` the following patterns are considered prob let foo = () => { return 0; }; + +let foo = () => {}; ``` The following patterns are not considered problems: @@ -64,7 +77,6 @@ let foo = (retv, name) => { return retv; }; let foo = () => { bar(); }; -let foo = () => {}; let foo = () => { /* do nothing */ }; let foo = () => { // do nothing. diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js index e7e6b6bdb473..92c8dc33a33e 100644 --- a/lib/rules/arrow-body-style.js +++ b/lib/rules/arrow-body-style.js @@ -24,16 +24,29 @@ module.exports = function(context) { if (arrowBody.type === "BlockStatement") { var blockBody = arrowBody.body; - if (blockBody.length !== 1) { + if (blockBody.length > 1) { return; } - if (asNeeded && blockBody[0].type === "ReturnStatement") { + if (blockBody.length === 0) { + var hasComments = context.getComments(arrowBody).trailing.length > 0; + if (hasComments) { + return; + } + context.report({ node: node, loc: arrowBody.loc.start, - message: "Unexpected block statement surrounding arrow body." + message: "Unexpected empty block in arrow body." }); + } else { + if (asNeeded && blockBody[0].type === "ReturnStatement") { + context.report({ + node: node, + loc: arrowBody.loc.start, + message: "Unexpected block statement surrounding arrow body." + }); + } } } else { if (always) { diff --git a/tests/lib/rules/arrow-body-style.js b/tests/lib/rules/arrow-body-style.js index c00438c6c008..f10e2573303d 100644 --- a/tests/lib/rules/arrow-body-style.js +++ b/tests/lib/rules/arrow-body-style.js @@ -20,7 +20,6 @@ var rule = require("../../../lib/rules/arrow-body-style"), var ruleTester = new RuleTester(); ruleTester.run("arrow-body-style", rule, { valid: [ - { code: "var foo = () => {};", ecmaFeatures: { arrowFunctions: true } }, { code: "var foo = () => 0;", ecmaFeatures: { arrowFunctions: true } }, { code: "var addToB = (a) => { b = b + a };", ecmaFeatures: { arrowFunctions: true } }, { code: "var foo = () => { /* do nothing */ };", ecmaFeatures: { arrowFunctions: true } }, @@ -34,6 +33,13 @@ ruleTester.run("arrow-body-style", rule, { { code: "var foo = () => { return bar(); };", ecmaFeatures: { arrowFunctions: true }, options: ["always"] } ], invalid: [ + { + code: "var foo = () => {};", + ecmaFeatures: { arrowFunctions: true }, + errors: [ + { line: 1, column: 17, type: "ArrowFunctionExpression", message: "Unexpected empty block in arrow body." } + ] + }, { code: "var foo = () => 0;", ecmaFeatures: { arrowFunctions: true }, From 5444116c351f0be8cabc1d33b0f4ce6b099a6902 Mon Sep 17 00:00:00 2001 From: "@storkme" Date: Sat, 14 Nov 2015 13:45:33 +0000 Subject: [PATCH 13/63] Docs: missing close rbracket in example --- docs/rules/no-underscore-dangle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-underscore-dangle.md b/docs/rules/no-underscore-dangle.md index 5fcafa041679..f78f24874e76 100644 --- a/docs/rules/no-underscore-dangle.md +++ b/docs/rules/no-underscore-dangle.md @@ -47,7 +47,7 @@ var file = __filename; ```js -/*eslint no-underscore-dangle: [2, { "allow": ["foo_", "_bar"] }*/ +/*eslint no-underscore-dangle: [2, { "allow": ["foo_", "_bar"] }]*/ var foo_; foo._bar(); From 6d59e0ebeb74c7cb52f0e62ebe4b17a67436bf65 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 15 Nov 2015 11:44:05 +0900 Subject: [PATCH 14/63] Fix: `id-length` properties never option (fixes #4347) --- lib/rules/id-length.js | 4 ++-- tests/lib/rules/id-length.js | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/rules/id-length.js b/lib/rules/id-length.js index a0a1019e3070..0ea42e14ef4b 100644 --- a/lib/rules/id-length.js +++ b/lib/rules/id-length.js @@ -25,12 +25,12 @@ module.exports = function(context) { }, {}); var SUPPORTED_EXPRESSIONS = { - "MemberExpression": function(parent) { + "MemberExpression": properties && function(parent) { return !parent.computed && ( // regular property assignment parent.parent.left === parent || ( // or the last identifier in an ObjectPattern destructuring - parent.parent.type === "Property" && properties && parent.parent.value === parent && + parent.parent.type === "Property" && parent.parent.value === parent && parent.parent.parent.type === "ObjectPattern" && parent.parent.parent.parent.left === parent.parent.parent ) ); diff --git a/tests/lib/rules/id-length.js b/tests/lib/rules/id-length.js index 645404215989..183a83754866 100644 --- a/tests/lib/rules/id-length.js +++ b/tests/lib/rules/id-length.js @@ -53,8 +53,13 @@ ruleTester.run("id-length", rule, { { code: "({ prop: obj.x.y.something }) = {};", ecmaFeatures: { destructuring: true } }, { code: "({ prop: obj.longName }) = {};", ecmaFeatures: { destructuring: true } }, { code: "var obj = { a: 1, bc: 2 };", options: [{ "properties": "never" }] }, + { code: "var obj = {}; obj.a = 1; obj.bc = 2;", options: [{ "properties": "never" }] }, { code: "({ a: obj.x.y.z }) = {};", options: [{ "properties": "never" }], ecmaFeatures: { destructuring: true } }, - { code: "({ prop: obj.x }) = {};", options: [{ "properties": "never" }], ecmaFeatures: { destructuring: true } } + { code: "({ prop: obj.x }) = {};", options: [{ "properties": "never" }], ecmaFeatures: { destructuring: true } }, + { code: "var obj = { aaaaa: 1 };", options: [{ "max": 4, "properties": "never" }] }, + { code: "var obj = {}; obj.aaaaa = 1;", options: [{ "max": 4, "properties": "never" }] }, + { code: "({ a: obj.x.y.z }) = {};", options: [{ "max": 4, "properties": "never" }], ecmaFeatures: { destructuring: true } }, + { code: "({ prop: obj.xxxxx }) = {};", options: [{ "max": 4, "properties": "never" }], ecmaFeatures: { destructuring: true } } ], invalid: [ { code: "var x = 1;", errors: [{ message: "Identifier name 'x' is too short. (< 2)", type: "Identifier" }] }, From 7b0afc35b4b0d27dea64586012d59cef89fc59b2 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 15 Nov 2015 12:39:32 +0900 Subject: [PATCH 15/63] Fix: `curly` warns wrong location for `else` (fixes #4362) --- lib/rules/curly.js | 35 +++++++++++++++++++++++++++++------ tests/lib/rules/curly.js | 24 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/lib/rules/curly.js b/lib/rules/curly.js index 31ef3c7916d8..af29ce6fb4ff 100644 --- a/lib/rules/curly.js +++ b/lib/rules/curly.js @@ -51,6 +51,22 @@ module.exports = function(context) { return first.loc.start.line === last.loc.end.line; } + /** + * Gets the `else` keyword token of a given `IfStatement` node. + * @param {ASTNode} node - A `IfStatement` node to get. + * @returns {Token} The `else` keyword token. + */ + function getElseKeyword(node) { + var sourceCode = context.getSourceCode(); + var token = sourceCode.getTokenAfter(node.consequent); + + while (token.type !== "Keyword" || token.value !== "else") { + token = sourceCode.getTokenAfter(token); + } + + return token; + } + /** * Checks a given IfStatement node requires braces of the consequent chunk. * This returns `true` when below: @@ -89,11 +105,15 @@ module.exports = function(context) { * @private */ function reportExpectedBraceError(node, name, suffix) { - context.report(node, "Expected { after '{{name}}'{{suffix}}.", - { + context.report({ + node: node, + loc: (name !== "else" ? node : getElseKeyword(node)).loc.start, + message: "Expected { after '{{name}}'{{suffix}}.", + data: { name: name, suffix: (suffix ? " " + suffix : "") - }); + } + }); } /** @@ -105,12 +125,15 @@ module.exports = function(context) { * @private */ function reportUnnecessaryBraceError(node, name, suffix) { - context.report(node, "Unnecessary { after '{{name}}'{{suffix}}.", - { + context.report({ + node: node, + loc: (name !== "else" ? node : getElseKeyword(node)).loc.start, + message: "Unnecessary { after '{{name}}'{{suffix}}.", + data: { name: name, suffix: (suffix ? " " + suffix : "") } - ); + }); } /** diff --git a/tests/lib/rules/curly.js b/tests/lib/rules/curly.js index fceb78896c82..89177572fa49 100644 --- a/tests/lib/rules/curly.js +++ b/tests/lib/rules/curly.js @@ -255,6 +255,30 @@ ruleTester.run("curly", rule, { } ] }, + { + code: [ + "if (0)", + " console.log(0)", + "else if (1) {", + " console.log(1)", + " console.log(1)", + "} else {", + " if (2)", + " console.log(2)", + " else", + " console.log(3)", + "}" + ].join("\n"), + options: ["multi"], + errors: [ + { + message: "Unnecessary { after 'else'.", + type: "IfStatement", + line: 6, + column: 3 + } + ] + }, { code: "if (foo) \n baz()", options: ["multi-line"], From 70926376022305e911971adf4a3020b8c8b434ab Mon Sep 17 00:00:00 2001 From: alberto Date: Wed, 11 Nov 2015 22:06:52 +0100 Subject: [PATCH 16/63] Fix: Support empty if blocks in lines-around-comment (fixes #4339) --- lib/rules/lines-around-comment.js | 4 +++- tests/lib/rules/lines-around-comment.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/rules/lines-around-comment.js b/lib/rules/lines-around-comment.js index 096fe7596903..83cd0a193ac9 100644 --- a/lib/rules/lines-around-comment.js +++ b/lib/rules/lines-around-comment.js @@ -95,7 +95,9 @@ module.exports = function(context) { * @returns {boolean} True if the comment is inside nodeType. */ function isCommentInsideNodeType(node, parent, nodeType) { - return parent.type === nodeType || (parent.body && parent.body.type === nodeType); + return parent.type === nodeType || + (parent.body && parent.body.type === nodeType) || + (parent.consequent && parent.consequent.type === nodeType); } /** diff --git a/tests/lib/rules/lines-around-comment.js b/tests/lib/rules/lines-around-comment.js index b01cd20b88cf..8a220c6694ba 100644 --- a/tests/lib/rules/lines-around-comment.js +++ b/tests/lib/rules/lines-around-comment.js @@ -121,6 +121,13 @@ ruleTester.run("lines-around-comment", rule, { allowBlockStart: true }] }, + { + code: "var foo = function(){\n// line at block start\n}", + options: [{ + beforeLineComment: true, + allowBlockStart: true + }] + }, { code: "if(true){\n// line at block start\nvar g = 1;\n}", options: [{ @@ -135,6 +142,20 @@ ruleTester.run("lines-around-comment", rule, { allowBlockStart: true }] }, + { + code: "if(true){\n// line at block start\n}", + options: [{ + beforeLineComment: true, + allowBlockStart: true + }] + }, + { + code: "if(true){ bar(); } else {\n// line at block start\n}", + options: [{ + beforeLineComment: true, + allowBlockStart: true + }] + }, { code: "function foo(){ \n/* block comment at block start */\nvar g = 1;\n}", options: [{ From 812c263b7e1b568b6bbb8557d6135224b9bcfda2 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Mon, 16 Nov 2015 12:31:19 +0900 Subject: [PATCH 17/63] Fix: `no-extend-native` crashed at empty defineProperty (fixes #4438) --- lib/rules/no-extend-native.js | 3 +-- tests/lib/rules/no-extend-native.js | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/rules/no-extend-native.js b/lib/rules/no-extend-native.js index dc7b3e30d646..49e139a29b52 100644 --- a/lib/rules/no-extend-native.js +++ b/lib/rules/no-extend-native.js @@ -68,8 +68,7 @@ module.exports = function(context) { // verify the object being added to is a native prototype subject = node.arguments[0]; - object = subject.object; - + object = subject && subject.object; if (object && object.type === "Identifier" && (modifiedBuiltins.indexOf(object.name) > -1) && diff --git a/tests/lib/rules/no-extend-native.js b/tests/lib/rules/no-extend-native.js index e5aedf57404b..d1b1ce2aeecd 100644 --- a/tests/lib/rules/no-extend-native.js +++ b/tests/lib/rules/no-extend-native.js @@ -35,7 +35,11 @@ ruleTester.run("no-extend-native", rule, { { code: "Object.prototype.g = 0", options: [{exceptions: ["Object"]}] - } + }, + + // https://github.com/eslint/eslint/issues/4438 + "Object.defineProperty()", + "Object.defineProperties()" ], invalid: [{ code: "Object.prototype.p = 0", From 032b7a6549d013d1e5bba9d3bbed6e78a065523d Mon Sep 17 00:00:00 2001 From: Andrew Marshall Date: Fri, 13 Nov 2015 21:42:20 -0500 Subject: [PATCH 18/63] Docs: Replace link to deprecated rule with newer rule no-space-before-semi is deprecated and replaced with semi-spacing, so link to that instead. --- docs/rules/semi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/semi.md b/docs/rules/semi.md index 2a55cef83283..ad990b6e60fc 100644 --- a/docs/rules/semi.md +++ b/docs/rules/semi.md @@ -146,5 +146,5 @@ If you do not want to enforce semicolon usage (or omission) in any particular wa ## Related Rules * [no-extra-semi](no-extra-semi.md) -* [no-space-before-semi](no-space-before-semi.md) * [no-unexpected-multiline](no-unexpected-multiline.md) +* [semi-spacing](semi-spacing.md) From 169bd96e1c56dabe2bcd9e90a2dbaa65ba3939f1 Mon Sep 17 00:00:00 2001 From: Brandon Mills Date: Mon, 16 Nov 2015 01:00:03 -0500 Subject: [PATCH 19/63] Update: Add JSX exceptions to no-extra-parens (fixes #4229) --- docs/rules/no-extra-parens.md | 49 +++++++++++++++++++++- lib/rules/no-extra-parens.js | 67 +++++++++++++++++++++++++++--- tests/lib/rules/no-extra-parens.js | 16 ++++++- 3 files changed, 124 insertions(+), 8 deletions(-) diff --git a/docs/rules/no-extra-parens.md b/docs/rules/no-extra-parens.md index c739652c22ed..44bb69da6a05 100644 --- a/docs/rules/no-extra-parens.md +++ b/docs/rules/no-extra-parens.md @@ -1,6 +1,6 @@ # Disallow Extra Parens (no-extra-parens) -This rule restricts the use of parentheses to only where they are necessary. It may be restricted to report only function expressions. +This rule restricts the use of parentheses to only where they are necessary. It may be restricted to report only function expressions. It can also be configured to allow parentheses around JSX elements. ## Rule Details @@ -69,6 +69,53 @@ a = (b * c); typeof (a); ``` +#### JSX + +The second, optional configuration parameter for the rule is an exceptions object. There is one configurable exception for JSX elements, with possible values `"never"`, `"all"`, or `"multiline"`. + +By default, the rule will warn about parentheses around JSX elements: + +```jsx +/*eslint no-extra-parens: [2, "all"]*/ + +var app = (); /*error Gratuitous parentheses around expression.*/ +``` + +This is equivalent to explicitly setting the JSX exception to `"never"`: + +```jsx +/*eslint no-extra-parens: [2, "all", { "jsx": "never" }]*/ + +var app = (); /*error Gratuitous parentheses around expression.*/ +``` + +If those parentheses are considered acceptable, set the JSX exception to `"all"` to allow parentheses around all JSX elements: + +```jsx +/*eslint no-extra-parens: [2, "all", { "jsx": "all" }]*/ + +var app = (); +``` + +Set the JSX exception `"multiline"` to allow parentheses only around JSX elements that span multiple lines: + +```jsx +/*eslint no-extra-parens: [2, "all", { "jsx": "multiline" }]*/ + +var app = ( + + Hello, world! + +); +``` + +The JSX `"multiline"` exception mode will still warn about parentheses around JSX elements that do not span more than one line: + +```jsx +/*eslint no-extra-parens: [2, "all", { "jsx": "multiline" }]*/ + +var app = (Hello world); /*error Gratuitous parentheses around expression.*/ +``` ## Further Reading diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index 29a716dd445b..95a2c29ba5a3 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -6,6 +6,20 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Determines whether a node spans multiple lines. + * @param {ASTNode} node - The node to check. + * @returns {boolean} True if the node spans multiple lines. + * @private + */ +function isMultiline(node) { + return node.loc.end.line - node.loc.start.line > 0; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -13,6 +27,9 @@ module.exports = function(context) { var ALL_NODES = context.options[0] !== "functions"; + var ALLOW_JSX = context.options[1] && context.options[1].jsx === "all"; + var ALLOW_MULTILINE_JSX = ALLOW_JSX || + (context.options[1] && context.options[1].jsx === "multiline"); /** * Determines if this rule should be enforced for a node given the current configuration. @@ -21,6 +38,12 @@ module.exports = function(context) { * @private */ function ruleApplies(node) { + if (node.type === "JSXElement" && (ALLOW_JSX || + (ALLOW_MULTILINE_JSX && isMultiline(node)) + )) { + return false; + } + return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"; } @@ -474,8 +497,42 @@ module.exports = function(context) { }; -module.exports.schema = [ - { - "enum": ["all", "functions"] - } -]; +module.exports.schema = { + "anyOf": [ + { + "type": "array", + "items": [ + { + "enum": [0, 1, 2] + }, + { + "enum": ["functions"] + } + ], + "minItems": 1, + "maxItems": 2 + }, + { + "type": "array", + "items": [ + { + "enum": [0, 1, 2] + }, + { + "enum": ["all"] + }, + { + "type": "object", + "properties": { + "jsx": { + "enum": ["never", "all", "multiline"] + } + }, + "additionalProperties": false + } + ], + "minItems": 1, + "maxItems": 3 + } + ] +}; diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index 2baef91aff9e..3fd2391e7c47 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -195,7 +195,13 @@ ruleTester.run("no-extra-parens", rule, { {code: "(class{}).foo() ? bar : baz;", ecmaFeatures: {classes: true}}, {code: "(class{}).foo.bar();", ecmaFeatures: {classes: true}}, {code: "(class{}.foo());", ecmaFeatures: {classes: true}}, - {code: "(class{}.foo.bar);", ecmaFeatures: {classes: true}} + {code: "(class{}.foo.bar);", ecmaFeatures: {classes: true}}, + + {code: "", ecmaFeatures: {jsx: true}}, + {code: "\n Hello\n", ecmaFeatures: {jsx: true}}, + {code: "()", options: ["all", { "jsx": "all" }], ecmaFeatures: { jsx: true }}, + {code: "(\n Hello\n)", options: ["all", { "jsx": "all" }], ecmaFeatures: {jsx: true}}, + {code: "(\n Hello\n)", options: ["all", { "jsx": "multiline" }], ecmaFeatures: {jsx: true}} ], invalid: [ invalid("(0)", "Literal"), @@ -291,6 +297,12 @@ ruleTester.run("no-extra-parens", rule, { invalid("bar ? baz : (class{}).foo();", "ClassExpression", null, {ecmaFeatures: {classes: true}}), invalid("bar((class{}).foo(), 0);", "ClassExpression", null, {ecmaFeatures: {classes: true}}), invalid("bar[(class{}).foo()];", "ClassExpression", null, {ecmaFeatures: {classes: true}}), - invalid("var bar = (class{}).foo();", "ClassExpression", null, {ecmaFeatures: {classes: true}}) + invalid("var bar = (class{}).foo();", "ClassExpression", null, {ecmaFeatures: {classes: true}}), + + invalid("()", "JSXElement", null, {ecmaFeatures: {jsx: true}}), + invalid("(\n Hello\n)", "JSXElement", null, {ecmaFeatures: {jsx: true}}), + invalid("()", "JSXElement", null, {ecmaFeatures: {jsx: true}, options: ["all", {"jsx": "never"}]}), + invalid("(\n Hello\n)", "JSXElement", null, {ecmaFeatures: {jsx: true}, options: ["all", {"jsx": "never"}]}), + invalid("()", "JSXElement", null, {ecmaFeatures: {jsx: true}, options: ["all", {"jsx": "multiline"}]}) ] }); From c411406df13ac8e3b0251135719ea0b4f2024ace Mon Sep 17 00:00:00 2001 From: Brandon Mills Date: Mon, 16 Nov 2015 13:24:40 -0500 Subject: [PATCH 20/63] Revert "Update: Add JSX exceptions to no-extra-parens (fixes #4229)" --- docs/rules/no-extra-parens.md | 49 +--------------------- lib/rules/no-extra-parens.js | 67 +++--------------------------- tests/lib/rules/no-extra-parens.js | 16 +------ 3 files changed, 8 insertions(+), 124 deletions(-) diff --git a/docs/rules/no-extra-parens.md b/docs/rules/no-extra-parens.md index 44bb69da6a05..c739652c22ed 100644 --- a/docs/rules/no-extra-parens.md +++ b/docs/rules/no-extra-parens.md @@ -1,6 +1,6 @@ # Disallow Extra Parens (no-extra-parens) -This rule restricts the use of parentheses to only where they are necessary. It may be restricted to report only function expressions. It can also be configured to allow parentheses around JSX elements. +This rule restricts the use of parentheses to only where they are necessary. It may be restricted to report only function expressions. ## Rule Details @@ -69,53 +69,6 @@ a = (b * c); typeof (a); ``` -#### JSX - -The second, optional configuration parameter for the rule is an exceptions object. There is one configurable exception for JSX elements, with possible values `"never"`, `"all"`, or `"multiline"`. - -By default, the rule will warn about parentheses around JSX elements: - -```jsx -/*eslint no-extra-parens: [2, "all"]*/ - -var app = (); /*error Gratuitous parentheses around expression.*/ -``` - -This is equivalent to explicitly setting the JSX exception to `"never"`: - -```jsx -/*eslint no-extra-parens: [2, "all", { "jsx": "never" }]*/ - -var app = (); /*error Gratuitous parentheses around expression.*/ -``` - -If those parentheses are considered acceptable, set the JSX exception to `"all"` to allow parentheses around all JSX elements: - -```jsx -/*eslint no-extra-parens: [2, "all", { "jsx": "all" }]*/ - -var app = (); -``` - -Set the JSX exception `"multiline"` to allow parentheses only around JSX elements that span multiple lines: - -```jsx -/*eslint no-extra-parens: [2, "all", { "jsx": "multiline" }]*/ - -var app = ( - - Hello, world! - -); -``` - -The JSX `"multiline"` exception mode will still warn about parentheses around JSX elements that do not span more than one line: - -```jsx -/*eslint no-extra-parens: [2, "all", { "jsx": "multiline" }]*/ - -var app = (Hello world); /*error Gratuitous parentheses around expression.*/ -``` ## Further Reading diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index 95a2c29ba5a3..29a716dd445b 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -6,20 +6,6 @@ */ "use strict"; -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Determines whether a node spans multiple lines. - * @param {ASTNode} node - The node to check. - * @returns {boolean} True if the node spans multiple lines. - * @private - */ -function isMultiline(node) { - return node.loc.end.line - node.loc.start.line > 0; -} - //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -27,9 +13,6 @@ function isMultiline(node) { module.exports = function(context) { var ALL_NODES = context.options[0] !== "functions"; - var ALLOW_JSX = context.options[1] && context.options[1].jsx === "all"; - var ALLOW_MULTILINE_JSX = ALLOW_JSX || - (context.options[1] && context.options[1].jsx === "multiline"); /** * Determines if this rule should be enforced for a node given the current configuration. @@ -38,12 +21,6 @@ module.exports = function(context) { * @private */ function ruleApplies(node) { - if (node.type === "JSXElement" && (ALLOW_JSX || - (ALLOW_MULTILINE_JSX && isMultiline(node)) - )) { - return false; - } - return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"; } @@ -497,42 +474,8 @@ module.exports = function(context) { }; -module.exports.schema = { - "anyOf": [ - { - "type": "array", - "items": [ - { - "enum": [0, 1, 2] - }, - { - "enum": ["functions"] - } - ], - "minItems": 1, - "maxItems": 2 - }, - { - "type": "array", - "items": [ - { - "enum": [0, 1, 2] - }, - { - "enum": ["all"] - }, - { - "type": "object", - "properties": { - "jsx": { - "enum": ["never", "all", "multiline"] - } - }, - "additionalProperties": false - } - ], - "minItems": 1, - "maxItems": 3 - } - ] -}; +module.exports.schema = [ + { + "enum": ["all", "functions"] + } +]; diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index 3fd2391e7c47..2baef91aff9e 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -195,13 +195,7 @@ ruleTester.run("no-extra-parens", rule, { {code: "(class{}).foo() ? bar : baz;", ecmaFeatures: {classes: true}}, {code: "(class{}).foo.bar();", ecmaFeatures: {classes: true}}, {code: "(class{}.foo());", ecmaFeatures: {classes: true}}, - {code: "(class{}.foo.bar);", ecmaFeatures: {classes: true}}, - - {code: "", ecmaFeatures: {jsx: true}}, - {code: "\n Hello\n", ecmaFeatures: {jsx: true}}, - {code: "()", options: ["all", { "jsx": "all" }], ecmaFeatures: { jsx: true }}, - {code: "(\n Hello\n)", options: ["all", { "jsx": "all" }], ecmaFeatures: {jsx: true}}, - {code: "(\n Hello\n)", options: ["all", { "jsx": "multiline" }], ecmaFeatures: {jsx: true}} + {code: "(class{}.foo.bar);", ecmaFeatures: {classes: true}} ], invalid: [ invalid("(0)", "Literal"), @@ -297,12 +291,6 @@ ruleTester.run("no-extra-parens", rule, { invalid("bar ? baz : (class{}).foo();", "ClassExpression", null, {ecmaFeatures: {classes: true}}), invalid("bar((class{}).foo(), 0);", "ClassExpression", null, {ecmaFeatures: {classes: true}}), invalid("bar[(class{}).foo()];", "ClassExpression", null, {ecmaFeatures: {classes: true}}), - invalid("var bar = (class{}).foo();", "ClassExpression", null, {ecmaFeatures: {classes: true}}), - - invalid("()", "JSXElement", null, {ecmaFeatures: {jsx: true}}), - invalid("(\n Hello\n)", "JSXElement", null, {ecmaFeatures: {jsx: true}}), - invalid("()", "JSXElement", null, {ecmaFeatures: {jsx: true}, options: ["all", {"jsx": "never"}]}), - invalid("(\n Hello\n)", "JSXElement", null, {ecmaFeatures: {jsx: true}, options: ["all", {"jsx": "never"}]}), - invalid("()", "JSXElement", null, {ecmaFeatures: {jsx: true}, options: ["all", {"jsx": "multiline"}]}) + invalid("var bar = (class{}).foo();", "ClassExpression", null, {ecmaFeatures: {classes: true}}) ] }); From c9a8883d450d63a8d044a9e66d275f5b1973a3ba Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 12 Nov 2015 16:43:27 -0800 Subject: [PATCH 21/63] New: Config files with extensions (fixes #4045, fixes #4263) --- .eslintrc => .eslintrc.yml | 0 docs/user-guide/configuring.md | 21 +- lib/cli-engine.js | 2 +- lib/config.js | 222 ++-------- lib/config/config-file.js | 366 ++++++++++++++++ lib/{ => config}/config-initializer.js | 12 + lib/config/config-ops.js | 186 ++++++++ lib/{ => config}/config-validator.js | 12 +- lib/eslint.js | 16 +- lib/file-finder.js | 50 ++- lib/testers/rule-tester.js | 2 +- lib/util.js | 92 ---- .../config-file/extends/.eslintrc.yml | 3 + tests/fixtures/config-file/js/.eslintrc | 1 + tests/fixtures/config-file/js/.eslintrc.js | 5 + tests/fixtures/config-file/json/.eslintrc | 1 + .../fixtures/config-file/json/.eslintrc.json | 5 + tests/fixtures/config-file/legacy/.eslintrc | 5 + .../config-file/package-json/package.json | 7 + tests/fixtures/config-file/yaml/.eslintrc | 1 + .../fixtures/config-file/yaml/.eslintrc.yaml | 2 + tests/fixtures/config-file/yaml/.eslintrc.yml | 1 + tests/fixtures/config-file/yml/.eslintrc | 1 + tests/fixtures/config-file/yml/.eslintrc.yml | 2 + .../config-hierarchy/fileexts/.eslintrc.js | 6 + .../fileexts/subdir/.eslintrc.yml | 2 + .../fileexts/subdir/subsubdir/.eslintrc.json | 5 + .../home-folder/{.eslintrc => .eslintrc.json} | 0 tests/fixtures/file-finder/subdir/empty2 | 0 tests/lib/cli-engine.js | 4 +- tests/lib/cli.js | 16 +- tests/lib/config.js | 116 ++++- tests/lib/config/config-file.js | 301 +++++++++++++ tests/lib/{ => config}/config-initializer.js | 6 +- tests/lib/config/config-ops.js | 407 ++++++++++++++++++ tests/lib/{ => config}/config-validator.js | 4 +- tests/lib/file-finder.js | 78 +++- tests/lib/util.js | 278 ------------ 38 files changed, 1625 insertions(+), 613 deletions(-) rename .eslintrc => .eslintrc.yml (100%) create mode 100644 lib/config/config-file.js rename lib/{ => config}/config-initializer.js (93%) create mode 100644 lib/config/config-ops.js rename lib/{ => config}/config-validator.js (90%) create mode 100644 tests/fixtures/config-file/extends/.eslintrc.yml create mode 100644 tests/fixtures/config-file/js/.eslintrc create mode 100644 tests/fixtures/config-file/js/.eslintrc.js create mode 100644 tests/fixtures/config-file/json/.eslintrc create mode 100644 tests/fixtures/config-file/json/.eslintrc.json create mode 100644 tests/fixtures/config-file/legacy/.eslintrc create mode 100644 tests/fixtures/config-file/package-json/package.json create mode 100644 tests/fixtures/config-file/yaml/.eslintrc create mode 100644 tests/fixtures/config-file/yaml/.eslintrc.yaml create mode 100644 tests/fixtures/config-file/yaml/.eslintrc.yml create mode 100644 tests/fixtures/config-file/yml/.eslintrc create mode 100644 tests/fixtures/config-file/yml/.eslintrc.yml create mode 100644 tests/fixtures/config-hierarchy/fileexts/.eslintrc.js create mode 100644 tests/fixtures/config-hierarchy/fileexts/subdir/.eslintrc.yml create mode 100644 tests/fixtures/config-hierarchy/fileexts/subdir/subsubdir/.eslintrc.json rename tests/fixtures/config-hierarchy/personal-config/home-folder/{.eslintrc => .eslintrc.json} (100%) create mode 100644 tests/fixtures/file-finder/subdir/empty2 create mode 100644 tests/lib/config/config-file.js rename tests/lib/{ => config}/config-initializer.js (93%) create mode 100644 tests/lib/config/config-ops.js rename tests/lib/{ => config}/config-validator.js (98%) diff --git a/.eslintrc b/.eslintrc.yml similarity index 100% rename from .eslintrc rename to .eslintrc.yml diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 0616338b4e2d..cf25f69d1e61 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -3,7 +3,7 @@ ESLint is designed to be completely configurable, meaning you can turn off every rule and run only with basic syntax validation, or mix and match the bundled rules and your custom rules to make ESLint perfect for your project. There are two primary ways to configure ESLint: 1. **Configuration Comments** - use JavaScript comments to embed configuration information directly into a file. -1. **Configuration Files** - use a JSON or YAML file to specify configuration information for an entire directory and all of its subdirectories. This can be in the form of an `.eslintrc` file or an `eslintConfig` field in a `package.json` file, both of which ESLint will look for and read automatically, or you can specify a configuration file on the [command line](command-line-interface). +1. **Configuration Files** - use a JavaScript, JSON or YAML file to specify configuration information for an entire directory and all of its subdirectories. This can be in the form of an `.eslintrc` file or an `eslintConfig` field in a `package.json` file, both of which ESLint will look for and read automatically, or you can specify a configuration file on the [command line](command-line-interface). There are several pieces of information that can be configured: @@ -382,6 +382,25 @@ The second way to use configuration files is via `.eslintrc` and `package.json` In each case, the settings in the configuration file override default settings. +## Configuration File Formats + +ESLint supports configuration files in several formats: + +* **JavaScript** - use `.eslintrc.js` and export an object containing your configuration. +* **YAML** - use `.eslintrc.yaml` or `.eslintrc.yml` to define the configuration structure. +* **JSON** - use `.eslintrc.json` to define the configuration structure. ESLint's JSON files also allow JavaScript-style comments. +* **package.json** - create an `eslintConfig` property in your `package.json` file and define your configuration there. +* **Deprecated** - use `.eslintrc`, which can be either JSON or YAML. + +If there are multiple `.eslintrc.*` files in the same directory, ESLint will only use one. The priority order is: + +1. `.eslintrc.js` +1. `.eslintrc.yaml` +1. `.eslintrc.yml` +1. `.eslintrc.json` +1. `.eslintrc` + + ## Configuration Cascading and Hierarchy When using `.eslintrc` and `package.json` files for configuration, you can take advantage of configuration cascading. For instance, suppose you have the following structure: diff --git a/lib/cli-engine.js b/lib/cli-engine.js index dbda87f83333..e112847eae4f 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -32,7 +32,7 @@ var fs = require("fs"), fileEntryCache = require("file-entry-cache"), globUtil = require("./util/glob-util"), SourceCodeFixer = require("./util/source-code-fixer"), - validator = require("./config-validator"), + validator = require("./config/config-validator"), stringify = require("json-stable-stringify"), crypto = require( "crypto" ), diff --git a/lib/config.js b/lib/config.js index 57c04540dbe2..3eb174e72d53 100644 --- a/lib/config.js +++ b/lib/config.js @@ -2,8 +2,9 @@ * @fileoverview Responsible for loading config files * @author Seth McLaughlin * @copyright 2014 Nicholas C. Zakas. All rights reserved. - * @copyright 2013 Seth McLaughlin. All rights reserved. * @copyright 2014 Michael McLaughlin. All rights reserved. + * @copyright 2013 Seth McLaughlin. All rights reserved. + * See LICENSE in root directory for full license. */ "use strict"; @@ -11,29 +12,22 @@ // Requirements //------------------------------------------------------------------------------ -var fs = require("fs"), - path = require("path"), - environments = require("../conf/environments"), +var path = require("path"), + ConfigOps = require("./config/config-ops"), + ConfigFile = require("./config/config-file"), util = require("./util"), FileFinder = require("./file-finder"), - stripComments = require("strip-json-comments"), - assign = require("object-assign"), debug = require("debug"), - yaml = require("js-yaml"), userHome = require("user-home"), - isAbsolutePath = require("path-is-absolute"), isResolvable = require("is-resolvable"), - validator = require("./config-validator"), pathIsInside = require("path-is-inside"); //------------------------------------------------------------------------------ // Constants //------------------------------------------------------------------------------ -var LOCAL_CONFIG_FILENAME = ".eslintrc", - PACKAGE_CONFIG_FILENAME = "package.json", - PACKAGE_CONFIG_FIELD_NAME = "eslintConfig", - PERSONAL_CONFIG_PATH = userHome ? path.join(userHome, LOCAL_CONFIG_FILENAME) : null; +var PACKAGE_CONFIG_FILENAME = "package.json", + PERSONAL_CONFIG_DIR = userHome || null; //------------------------------------------------------------------------------ // Private @@ -47,18 +41,6 @@ var loadedPlugins = Object.create(null); debug = debug("eslint:config"); -/** - * Determines if a given string represents a filepath or not using the same - * conventions as require(), meaning that the first character must be nonalphanumeric - * and not the @ sign which is used for scoped packages to be considered a file path. - * @param {string} filePath The string to check. - * @returns {boolean} True if it's a filepath, false if not. - * @private - */ -function isFilePath(filePath) { - return isAbsolutePath(filePath) || !/\w|@/.test(filePath.charAt(0)); -} - /** * Check if item is an javascript object * @param {*} item object to check for @@ -69,94 +51,6 @@ function isObject(item) { return typeof item === "object" && !Array.isArray(item) && item !== null; } -/** - * Creates an environment config based on the specified environments. - * @param {Object} envs The environment settings. - * @returns {Object} A configuration object with the appropriate rules and globals - * set. - * @private - */ -function createEnvironmentConfig(envs) { - - var envConfig = { - globals: {}, - env: envs || {}, - rules: {}, - ecmaFeatures: {} - }; - - if (envs) { - Object.keys(envs).filter(function(name) { - return envs[name]; - }).forEach(function(name) { - var environment = environments[name]; - - if (environment) { - - if (environment.globals) { - assign(envConfig.globals, environment.globals); - } - - if (environment.ecmaFeatures) { - assign(envConfig.ecmaFeatures, environment.ecmaFeatures); - } - } - }); - } - - return envConfig; -} - -/** - * Read the config from the config JSON file - * @param {string} filePath the path to the JSON config file - * @returns {Object} config object - * @private - */ -function readConfigFromFile(filePath) { - var config = {}; - - if (isFilePath(filePath)) { - if (path.extname(filePath) === ".js") { // using js files for config - config = require(filePath); - } else { - try { - config = yaml.safeLoad(stripComments(fs.readFileSync(filePath, "utf8"))) || {}; - } catch (e) { - debug("Error reading YAML file: " + filePath); - e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; - throw e; - } - } - - if (path.basename(filePath) === PACKAGE_CONFIG_FILENAME) { - config = config[PACKAGE_CONFIG_FIELD_NAME] || {}; - } - - } else { - - // it's a package - - if (filePath.charAt(0) === "@") { - // it's a scoped package - - // package name is "eslint-config", or just a username - var scopedPackageShortcutRegex = /^(@[^\/]+)(?:\/(?:eslint-config)?)?$/; - if (scopedPackageShortcutRegex.test(filePath)) { - filePath = filePath.replace(scopedPackageShortcutRegex, "$1/eslint-config"); - } else if (filePath.split("/")[1].indexOf("eslint-config-") !== 0) { - // for scoped packages, insert the eslint-config after the first / - filePath = filePath.replace(/^@([^\/]+)\/(.*)$/, "@$1/eslint-config-$2"); - } - } else if (filePath.indexOf("eslint-config-") !== 0) { - filePath = "eslint-config-" + filePath; - } - - config = util.mergeConfigs(config, require(filePath)); - } - return config; -} - /** * Load and parse a JSON config object from a file. * @param {string|Object} configToLoad the path to the JSON config file or the config object itself. @@ -164,67 +58,24 @@ function readConfigFromFile(filePath) { * @private */ function loadConfig(configToLoad) { - var config = {}; - var filePath = ""; + var config = {}, + filePath = ""; if (configToLoad) { if (isObject(configToLoad)) { config = configToLoad; - } else { - filePath = configToLoad; - config = readConfigFromFile(filePath); - } - - validator.validate(config, filePath); - - // If an `extends` property is defined, it represents a configuration file to use as - // a "parent". Load the referenced file and merge the configuration recursively. - if (config.extends) { - var configExtends = config.extends; - if (!Array.isArray(config.extends)) { - configExtends = [config.extends]; + if (config.extends) { + config = ConfigFile.applyExtends(config, filePath); } - - // Make the last element in an array take the highest precedence - config = configExtends.reduceRight(function(previousValue, parentPath) { - - if (parentPath === "eslint:recommended") { - // Add an explicit substitution for eslint:recommended to conf/eslint.json - // this lets us use the eslint.json file as the recommended rules - parentPath = path.resolve(__dirname, "../conf/eslint.json"); - } 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 - ); - } - - try { - return util.mergeConfigs(loadConfig(parentPath), previousValue); - } catch (e) { - // If the file referenced by `extends` failed to load, add the path to the - // configuration file that referenced it to the error message so the user is - // able to see where it was referenced from, then re-throw - e.message += "\nReferenced from: " + filePath; - throw e; - } - - }, config); - - } - - if (config.env) { - // Merge in environment-specific globals and ecmaFeatures. - config = util.mergeConfigs(createEnvironmentConfig(config.env), config); + } else { + filePath = configToLoad; + config = ConfigFile.load(filePath); } } - return config; } @@ -263,7 +114,7 @@ function getPluginsConfig(pluginNames) { rules[pluginNameWithoutPrefix + "/" + item] = plugin.rulesConfig[item]; }); - pluginConfig = util.mergeConfigs(pluginConfig, rules); + pluginConfig = ConfigOps.merge(pluginConfig, rules); }); return {rules: pluginConfig}; @@ -275,11 +126,16 @@ function getPluginsConfig(pluginNames) { * @private */ function getPersonalConfig() { - var config = {}; + var config = {}, + filename; + + if (PERSONAL_CONFIG_DIR) { + filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR); - if (PERSONAL_CONFIG_PATH && fs.existsSync(PERSONAL_CONFIG_PATH)) { - debug("Using personal config"); - config = loadConfig(PERSONAL_CONFIG_PATH); + if (filename) { + debug("Using personal config"); + config = loadConfig(filename); + } } return config; @@ -300,7 +156,7 @@ function getLocalConfig(thisConfig, directory) { localConfigFiles = thisConfig.findLocalConfigFiles(directory), numFiles = localConfigFiles.length, rootPath, - projectConfigPath = path.join(process.cwd(), LOCAL_CONFIG_FILENAME); + projectConfigPath = ConfigFile.getFilenameForDirectory(process.cwd()); for (i = 0; i < numFiles; i++) { @@ -308,7 +164,7 @@ function getLocalConfig(thisConfig, directory) { // Don't consider the personal config file in the home directory, // except if the home directory is the same as the current working directory - if (localConfigFile === PERSONAL_CONFIG_PATH && localConfigFile !== projectConfigPath) { + if (path.dirname(localConfigFile) === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) { continue; } @@ -320,8 +176,8 @@ function getLocalConfig(thisConfig, directory) { debug("Loading " + localConfigFile); localConfig = loadConfig(localConfigFile); - // Don't consider a local config file found if the config is empty. - if (!Object.keys(localConfig).length) { + // Don't consider a local config file found if the config is null + if (!localConfig) { continue; } @@ -332,11 +188,11 @@ function getLocalConfig(thisConfig, directory) { found = true; debug("Using " + localConfigFile); - config = util.mergeConfigs(localConfig, config); + config = ConfigOps.merge(localConfig, config); } // Use the personal config file if there are no other local config files found. - return found ? config : util.mergeConfigs(config, getPersonalConfig()); + return found ? config : ConfigOps.merge(config, getPersonalConfig()); } //------------------------------------------------------------------------------ @@ -420,42 +276,42 @@ Config.prototype.getConfig = function(filePath) { } // Step 2: Create a copy of the baseConfig - config = util.mergeConfigs({parser: this.parser}, this.baseConfig); + config = ConfigOps.merge({parser: this.parser}, this.baseConfig); // Step 3: Merge in the user-specified configuration from .eslintrc and package.json - config = util.mergeConfigs(config, userConfig); + config = ConfigOps.merge(config, userConfig); // Step 4: Merge in command line config file if (this.useSpecificConfig) { debug("Merging command line config file"); - config = util.mergeConfigs(config, this.useSpecificConfig); + config = ConfigOps.merge(config, this.useSpecificConfig); } // Step 5: Merge in command line environments debug("Merging command line environment settings"); - config = util.mergeConfigs(config, createEnvironmentConfig(this.env)); + config = ConfigOps.merge(config, ConfigOps.createEnvironmentConfig(this.env)); // Step 6: Merge in command line rules if (this.options.rules) { debug("Merging command line rules"); - config = util.mergeConfigs(config, { rules: this.options.rules }); + config = ConfigOps.merge(config, { rules: this.options.rules }); } // Step 7: Merge in command line globals - config = util.mergeConfigs(config, { globals: this.globals }); + config = ConfigOps.merge(config, { globals: this.globals }); // Step 8: Merge in command line plugins if (this.options.plugins) { debug("Merging command line plugins"); pluginConfig = getPluginsConfig(this.options.plugins); - config = util.mergeConfigs(config, { plugins: this.options.plugins }); + config = ConfigOps.merge(config, { plugins: this.options.plugins }); } // Step 9: Merge in plugin specific rules in reverse if (config.plugins) { pluginConfig = getPluginsConfig(config.plugins); - config = util.mergeConfigs(pluginConfig, config); + config = ConfigOps.merge(pluginConfig, config); } this.cache[directory] = config; @@ -471,7 +327,7 @@ Config.prototype.getConfig = function(filePath) { Config.prototype.findLocalConfigFiles = function(directory) { if (!this.localConfigFinder) { - this.localConfigFinder = new FileFinder(LOCAL_CONFIG_FILENAME, PACKAGE_CONFIG_FILENAME); + this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, PACKAGE_CONFIG_FILENAME); } return this.localConfigFinder.findAllInDirectoryAndParents(directory); diff --git a/lib/config/config-file.js b/lib/config/config-file.js new file mode 100644 index 000000000000..6824303b69f6 --- /dev/null +++ b/lib/config/config-file.js @@ -0,0 +1,366 @@ +/** + * @fileoverview Helper to locate and load configuration files. + * @author Nicholas C. Zakas + * @copyright 2015 Nicholas C. Zakas. All rights reserved. + * See LICENSE file in root directory for full license. + */ +/* eslint no-use-before-define: 0 */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var debug = require("debug"), + fs = require("fs"), + path = require("path"), + ConfigOps = require("./config-ops"), + validator = require("./config-validator"), + stripComments = require("strip-json-comments"), + isAbsolutePath = require("path-is-absolute"); + +//------------------------------------------------------------------------------ +// Private +//------------------------------------------------------------------------------ + +var CONFIG_FILES = [ + ".eslintrc.js", + ".eslintrc.yaml", + ".eslintrc.yml", + ".eslintrc.json", + ".eslintrc" +]; + +debug = debug("eslint:config-file"); + +/** + * Convenience wrapper for synchronously reading file contents. + * @param {string} filePath The filename to read. + * @returns {string} The file contents. + * @private + */ +function readFile(filePath) { + return fs.readFileSync(filePath, "utf8"); +} + +/** + * Determines if a given string represents a filepath or not using the same + * conventions as require(), meaning that the first character must be nonalphanumeric + * and not the @ sign which is used for scoped packages to be considered a file path. + * @param {string} filePath The string to check. + * @returns {boolean} True if it's a filepath, false if not. + * @private + */ +function isFilePath(filePath) { + return isAbsolutePath(filePath) || !/\w|@/.test(filePath.charAt(0)); +} + +/** + * Loads a YAML configuration from a file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadYAMLConfigFile(filePath) { + debug("Loading YAML config file: " + filePath); + + // lazy load YAML to improve performance when not used + var yaml = require("js-yaml"); + + try { + // empty YAML file can be null, so always use + return yaml.safeLoad(readFile(filePath)) || {}; + } catch (e) { + debug("Error reading YAML file: " + filePath); + e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; + throw e; + } +} + +/** + * Loads a JSON configuration from a file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadJSONConfigFile(filePath) { + debug("Loading JSON config file: " + filePath); + + try { + return JSON.parse(stripComments(readFile(filePath))); + } catch (e) { + debug("Error reading JSON file: " + filePath); + e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; + throw e; + } +} + +/** + * Loads a legacy (.eslintrc) configuration from a file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadLegacyConfigFile(filePath) { + debug("Loading config file: " + filePath); + + // lazy load YAML to improve performance when not used + var yaml = require("js-yaml"); + + try { + return yaml.safeLoad(stripComments(readFile(filePath))) || {}; + } catch (e) { + debug("Error reading YAML file: " + filePath); + e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; + throw e; + } +} + +/** + * Loads a JavaScript configuration from a file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadJSConfigFile(filePath) { + debug("Loading JS config file: " + filePath); + try { + return require(filePath); + } catch (e) { + debug("Error reading JavaScript file: " + filePath); + e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; + throw e; + } +} + +/** + * Loads a configuration from a package.json file. + * @param {string} filePath The filename to load. + * @returns {Object} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadPackageJSONConfigFile(filePath) { + debug("Loading package.json config file: " + filePath); + try { + return require(filePath).eslintConfig || null; + } catch (e) { + debug("Error reading JavaScript file: " + filePath); + e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; + throw e; + } +} + +/** + * Loads a JavaScript configuration from a package. + * @param {string} filePath The package name to load. + * @returns {Object} The configuration object from the package. + * @throws {Error} If the package cannot be read. + * @private + */ +function loadPackage(filePath) { + debug("Loading config package: " + filePath); + try { + return require(filePath); + } catch (e) { + debug("Error reading package: " + filePath); + e.message = "Cannot read config package: " + filePath + "\nError: " + e.message; + throw e; + } +} + +/** + * Loads a configuration file regardless of the source. Inspects the file path + * to determine the correctly way to load the config file. + * @param {string} filePath The path to the configuration. + * @returns {Object} The configuration information. + * @private + */ +function loadConfigFile(filePath) { + var config; + + if (isFilePath(filePath)) { + switch (path.extname(filePath)) { + case ".js": + config = loadJSConfigFile(filePath); + break; + + case ".json": + if (path.basename(filePath) === "package.json") { + config = loadPackageJSONConfigFile(filePath); + if (config === null) { + return null; + } + } else { + config = loadJSONConfigFile(filePath); + } + break; + + case ".yaml": + case ".yml": + config = loadYAMLConfigFile(filePath); + break; + + default: + config = loadLegacyConfigFile(filePath); + } + } else { + config = loadPackage(filePath); + } + + return ConfigOps.merge(ConfigOps.createEmptyConfig(), config); +} + +/** + * Applies values from the "extends" field in a configuration file. + * @param {Object} config The configuration information. + * @param {string} filePath The file path from which the configuration information + * was loaded. + * @returns {Object} A new configuration object with all of the "extends" fields + * loaded and merged. + * @private + */ +function applyExtends(config, filePath) { + var configExtends = config.extends; + + // normalize into an array for easier handling + if (!Array.isArray(config.extends)) { + configExtends = [config.extends]; + } + + // Make the last element in an array take the highest precedence + config = configExtends.reduceRight(function(previousValue, parentPath) { + + if (parentPath === "eslint:recommended") { + // Add an explicit substitution for eslint:recommended to conf/eslint.json + // this lets us use the eslint.json file as the recommended rules + parentPath = path.resolve(__dirname, "../../conf/eslint.json"); + } 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 + ); + } + + try { + debug("Loading " + parentPath); + return ConfigOps.merge(load(parentPath), previousValue); + } catch (e) { + // If the file referenced by `extends` failed to load, add the path to the + // configuration file that referenced it to the error message so the user is + // able to see where it was referenced from, then re-throw + e.message += "\nReferenced from: " + filePath; + throw e; + } + + }, config); + + return config; +} + +/** + * Resolves a configuration file path into the fully-formed path, whether filename + * or package name. + * @param {string} filePath The filepath to resolve. + * @returns {string} A path that can be used directly to load the configuration. + * @private + */ +function resolve(filePath) { + + if (isFilePath(filePath)) { + return path.resolve(filePath); + } else { + + // it's a package + + if (filePath.charAt(0) === "@") { + // it's a scoped package + + // package name is "eslint-config", or just a username + var scopedPackageShortcutRegex = /^(@[^\/]+)(?:\/(?:eslint-config)?)?$/; + if (scopedPackageShortcutRegex.test(filePath)) { + filePath = filePath.replace(scopedPackageShortcutRegex, "$1/eslint-config"); + } else if (filePath.split("/")[1].indexOf("eslint-config-") !== 0) { + // for scoped packages, insert the eslint-config after the first / + filePath = filePath.replace(/^@([^\/]+)\/(.*)$/, "@$1/eslint-config-$2"); + } + } else if (filePath.indexOf("eslint-config-") !== 0) { + filePath = "eslint-config-" + filePath; + } + + return filePath; + } + +} + +/** + * Loads a configuration file from the given file path. + * @param {string} filePath The filename or package name to load the configuration + * information from. + * @returns {Object} The configuration information. + * @private + */ +function load(filePath) { + + var resolvedPath = resolve(filePath), + config = loadConfigFile(resolvedPath); + + if (config) { + + // validate the configuration before continuing + validator.validate(config, filePath); + + // If an `extends` property is defined, it represents a configuration file to use as + // a "parent". Load the referenced file and merge the configuration recursively. + if (config.extends) { + config = applyExtends(config, filePath); + } + + if (config.env) { + // Merge in environment-specific globals and ecmaFeatures. + config = ConfigOps.applyEnvironments(config); + } + + } + + return config; +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { + + load: load, + resolve: resolve, + applyExtends: applyExtends, + CONFIG_FILES: CONFIG_FILES, + + /** + * Retrieves the configuration filename for a given directory. It loops over all + * of the valid configuration filenames in order to find the first one that exists. + * @param {string} directory The directory to check for a config file. + * @returns {?string} The filename of the configuration file for the directory + * or null if there is no configuration file in the directory. + */ + getFilenameForDirectory: function(directory) { + + var filename; + + for (var i = 0, len = CONFIG_FILES.length; i < len; i++) { + filename = path.join(directory, CONFIG_FILES[i]); + if (fs.existsSync(filename)) { + return filename; + } + } + + return null; + } +}; diff --git a/lib/config-initializer.js b/lib/config/config-initializer.js similarity index 93% rename from lib/config-initializer.js rename to lib/config/config-initializer.js index a8caad46a4b6..b047c44c262c 100644 --- a/lib/config-initializer.js +++ b/lib/config/config-initializer.js @@ -6,11 +6,19 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + var exec = require("child_process").exec, fs = require("fs"), inquirer = require("inquirer"), yaml = require("js-yaml"); +//------------------------------------------------------------------------------ +// Private +//------------------------------------------------------------------------------ + /* istanbul ignore next: hard to test fs function */ /** * Create .eslintrc file in the current working directory @@ -206,6 +214,10 @@ function promptUser(callback) { }); } +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + var init = { getConfigForStyleGuide: getConfigForStyleGuide, processAnswers: processAnswers, diff --git a/lib/config/config-ops.js b/lib/config/config-ops.js new file mode 100644 index 000000000000..fa9436bc5727 --- /dev/null +++ b/lib/config/config-ops.js @@ -0,0 +1,186 @@ +/** + * @fileoverview Config file operations. This file must be usable in the browser, + * so no Node-specific code can be here. + * @author Nicholas C. Zakas + * @copyright 2015 Nicholas C. Zakas. All rights reserved. + * See LICENSE file in root directory for full license. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var debug = require("debug"), + environments = require("../../conf/environments"), + assign = require("object-assign"); + +//------------------------------------------------------------------------------ +// Private +//------------------------------------------------------------------------------ + +debug = debug("eslint:config-ops"); + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { + + /** + * Creates an empty configuration object suitable for merging as a base. + * @returns {Object} A configuration object. + */ + createEmptyConfig: function() { + return { + globals: {}, + env: {}, + rules: {}, + ecmaFeatures: {} + }; + }, + + /** + * Creates an environment config based on the specified environments. + * @param {Object} env The environment settings. + * @returns {Object} A configuration object with the appropriate rules and globals + * set. + */ + createEnvironmentConfig: function(env) { + + var envConfig = this.createEmptyConfig(); + + if (env) { + + envConfig.env = env; + + Object.keys(env).filter(function(name) { + return env[name]; + }).forEach(function(name) { + var environment = environments[name]; + + if (environment) { + debug("Creating config for environment " + name); + if (environment.globals) { + assign(envConfig.globals, environment.globals); + } + + if (environment.ecmaFeatures) { + assign(envConfig.ecmaFeatures, environment.ecmaFeatures); + } + } + }); + } + + return envConfig; + }, + + /** + * Given a config with environment settings, applies the globals and + * ecmaFeatures to the configuration and returns the result. + * @param {Object} config The configuration information. + * @returns {Object} The updated configuration information. + */ + applyEnvironments: function(config) { + if (config.env && typeof config.env === "object") { + debug("Apply environment settings to config"); + return this.merge(this.createEnvironmentConfig(config.env), config); + } + + return config; + }, + + /** + * Merges two config objects. This will not only add missing keys, but will also modify values to match. + * @param {Object} target config object + * @param {Object} src config object. Overrides in this config object will take priority over base. + * @param {boolean} [combine] Whether to combine arrays or not + * @param {boolean} [isRule] Whether its a rule + * @returns {Object} merged config object. + */ + merge: function deepmerge(target, src, combine, isRule) { + /* + The MIT License (MIT) + + Copyright (c) 2012 Nicholas Fisher + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + // This code is taken from deepmerge repo (https://github.com/KyleAMathews/deepmerge) and modified to meet our needs. + var array = Array.isArray(src) || Array.isArray(target); + var dst = array && [] || {}; + + combine = !!combine; + isRule = !!isRule; + if (array) { + target = target || []; + if (isRule && src.length > 1) { + dst = dst.concat(src); + } else { + dst = dst.concat(target); + } + if (typeof src !== "object" && !Array.isArray(src)) { + src = [src]; + } + Object.keys(src).forEach(function(e, i) { + e = src[i]; + if (typeof dst[i] === "undefined") { + dst[i] = e; + } else if (typeof e === "object") { + if (isRule) { + dst[i] = e; + } else { + dst[i] = deepmerge(target[i], e, combine, isRule); + } + } else { + if (!combine) { + dst[i] = e; + } else { + if (dst.indexOf(e) === -1) { + dst.push(e); + } + } + } + }); + } else { + if (target && typeof target === "object") { + Object.keys(target).forEach(function(key) { + dst[key] = target[key]; + }); + } + Object.keys(src).forEach(function(key) { + if (Array.isArray(src[key]) || Array.isArray(target[key])) { + dst[key] = deepmerge(target[key], src[key], key === "plugins", isRule); + } else if (typeof src[key] !== "object" || !src[key]) { + dst[key] = src[key]; + } else { + if (!target[key]) { + dst[key] = src[key]; + } else { + dst[key] = deepmerge(target[key], src[key], combine, key === "rules"); + } + } + }); + } + + return dst; + } + + +}; diff --git a/lib/config-validator.js b/lib/config/config-validator.js similarity index 90% rename from lib/config-validator.js rename to lib/config/config-validator.js index e856d8f71e1b..89e9a70e374e 100644 --- a/lib/config-validator.js +++ b/lib/config/config-validator.js @@ -10,14 +10,18 @@ // Requirements //------------------------------------------------------------------------------ -var rules = require("./rules"), - environments = require("../conf/environments"), +var rules = require("../rules"), + environments = require("../../conf/environments"), schemaValidator = require("is-my-json-valid"); var validators = { rules: Object.create(null) }; +//------------------------------------------------------------------------------ +// Private +//------------------------------------------------------------------------------ + /** * Gets a complete options schema for a rule. * @param {string} id The rule's unique name. @@ -148,6 +152,10 @@ function validate(config, source) { validateEnvironment(config.env, source); } +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + module.exports = { getRuleOptionsSchema: getRuleOptionsSchema, validate: validate, diff --git a/lib/eslint.js b/lib/eslint.js index 99ec893e7efc..38245ee9faec 100755 --- a/lib/eslint.js +++ b/lib/eslint.js @@ -16,14 +16,14 @@ var estraverse = require("./util/estraverse"), blankScriptAST = require("../conf/blank-script.json"), assign = require("object-assign"), rules = require("./rules"), - util = require("./util"), 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, - validator = require("./config-validator"), + ConfigOps = require("./config/config-ops"), + validator = require("./config/config-validator"), replacements = require("../conf/replacements.json"), assert = require("assert"); @@ -330,12 +330,12 @@ function modifyConfigsFromComments(filename, ast, config, reportingConfig, messa // apply environment configs Object.keys(commentConfig.env).forEach(function(name) { if (environments[name]) { - commentConfig = util.mergeConfigs(commentConfig, environments[name]); + commentConfig = ConfigOps.merge(commentConfig, environments[name]); } }); assign(commentConfig.rules, commentRules); - return util.mergeConfigs(config, commentConfig); + return ConfigOps.merge(config, commentConfig); } /** @@ -400,10 +400,10 @@ function prepareConfig(config) { preparedConfig = { rules: copiedRules, parser: config.parser || DEFAULT_PARSER, - globals: util.mergeConfigs({}, config.globals), - env: util.mergeConfigs({}, config.env || {}), - settings: util.mergeConfigs({}, config.settings || {}), - ecmaFeatures: util.mergeConfigs(ecmaFeatures, config.ecmaFeatures || {}) + globals: ConfigOps.merge({}, config.globals), + env: ConfigOps.merge({}, config.env || {}), + settings: ConfigOps.merge({}, config.settings || {}), + ecmaFeatures: ConfigOps.merge(ecmaFeatures, config.ecmaFeatures || {}) }; // can't have global return inside of modules diff --git a/lib/file-finder.js b/lib/file-finder.js index b2cf24ce1b33..af35cb5fd557 100644 --- a/lib/file-finder.js +++ b/lib/file-finder.js @@ -2,6 +2,8 @@ * @fileoverview Util class to find config files. * @author Aliaksei Shytkin * @copyright 2014 Michael McLaughlin. All rights reserved. + * @copyright 2014 Aliaksei Shytkin. All rights reserved. + * See LICENSE in root directory for full license. */ "use strict"; @@ -61,6 +63,7 @@ FileFinder.prototype.findInDirectoryOrParents = function(directory) { filePath, i, name, + names, searched; if (!directory) { @@ -74,20 +77,27 @@ FileFinder.prototype.findInDirectoryOrParents = function(directory) { dirs = []; searched = 0; name = this.fileNames[0]; + names = Array.isArray(name) ? name : [name]; - while (directory !== child) { - dirs[searched++] = directory; + /* eslint-disable no-labels */ + traversal: + while (directory !== child) { + dirs[searched++] = directory; - if (getDirectoryEntries(directory).indexOf(name) !== -1 && fs.statSync(path.resolve(directory, name)).isFile()) { - filePath = path.resolve(directory, name); - break; - } + for (var k = 0, found = false; k < names.length && !found; k++) { - child = directory; + if (getDirectoryEntries(directory).indexOf(names[k]) !== -1 && fs.statSync(path.resolve(directory, names[k])).isFile()) { + filePath = path.resolve(directory, names[k]); + break traversal; + } + } - // Assign parent directory to directory. - directory = path.dirname(directory); - } + child = directory; + + // Assign parent directory to directory. + directory = path.dirname(directory); + } + /* eslint-enable no-labels */ for (i = 0; i < searched; i++) { cache[dirs[i]] = filePath; @@ -137,14 +147,24 @@ FileFinder.prototype.findAllInDirectoryAndParents = function(directory) { for (i = 0; i < fileNamesCount; i++) { name = fileNames[i]; - if (getDirectoryEntries(directory).indexOf(name) !== -1 && fs.statSync(path.resolve(directory, name)).isFile()) { - filePath = path.resolve(directory, name); + // convert to an array for easier handling + if (!Array.isArray(name)) { + name = [name]; + } - // Add the file path to the cache of each directory searched. - for (j = 0; j < searched; j++) { - cache[dirs[j]].push(filePath); + for (var k = 0, found = false; k < name.length && !found; k++) { + + if (getDirectoryEntries(directory).indexOf(name[k]) !== -1 && fs.statSync(path.resolve(directory, name[k])).isFile()) { + filePath = path.resolve(directory, name[k]); + found = true; + + // Add the file path to the cache of each directory searched. + for (j = 0; j < searched; j++) { + cache[dirs[j]].push(filePath); + } } } + } child = directory; diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js index bcd40d430404..d98a4c51088f 100644 --- a/lib/testers/rule-tester.js +++ b/lib/testers/rule-tester.js @@ -53,7 +53,7 @@ var assert = require("assert"), merge = require("lodash.merge"), omit = require("lodash.omit"), clone = require("lodash.clonedeep"), - validator = require("../config-validator"), + validator = require("../config/config-validator"), validate = require("is-my-json-valid"), eslint = require("../eslint"), metaSchema = require("../../conf/json-schema-schema.json"), diff --git a/lib/util.js b/lib/util.js index 4a6eb017a5bf..fb1a7a33d5de 100644 --- a/lib/util.js +++ b/lib/util.js @@ -14,97 +14,6 @@ var PLUGIN_NAME_PREFIX = "eslint-plugin-", // Public Interface //------------------------------------------------------------------------------ -/** - * Merges two config objects. This will not only add missing keys, but will also modify values to match. - * @param {Object} target config object - * @param {Object} src config object. Overrides in this config object will take priority over base. - * @param {boolean} [combine] Whether to combine arrays or not - * @param {boolean} [isRule] Whether its a rule - * @returns {Object} merged config object. - */ -function deepmerge(target, src, combine, isRule) { - /* - The MIT License (MIT) - - Copyright (c) 2012 Nicholas Fisher - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - */ - // This code is taken from deepmerge repo (https://github.com/KyleAMathews/deepmerge) and modified to meet our needs. - var array = Array.isArray(src) || Array.isArray(target); - var dst = array && [] || {}; - - combine = !!combine; - isRule = !!isRule; - if (array) { - target = target || []; - if (isRule && src.length > 1) { - dst = dst.concat(src); - } else { - dst = dst.concat(target); - } - if (typeof src !== "object" && !Array.isArray(src)) { - src = [src]; - } - Object.keys(src).forEach(function(e, i) { - e = src[i]; - if (typeof dst[i] === "undefined") { - dst[i] = e; - } else if (typeof e === "object") { - if (isRule) { - dst[i] = e; - } else { - dst[i] = deepmerge(target[i], e, combine, isRule); - } - } else { - if (!combine) { - dst[i] = e; - } else { - if (dst.indexOf(e) === -1) { - dst.push(e); - } - } - } - }); - } else { - if (target && typeof target === "object") { - Object.keys(target).forEach(function(key) { - dst[key] = target[key]; - }); - } - Object.keys(src).forEach(function(key) { - if (Array.isArray(src[key]) || Array.isArray(target[key])) { - dst[key] = deepmerge(target[key], src[key], key === "plugins", isRule); - } else if (typeof src[key] !== "object" || !src[key]) { - dst[key] = src[key]; - } else { - if (!target[key]) { - dst[key] = src[key]; - } else { - dst[key] = deepmerge(target[key], src[key], combine, key === "rules"); - } - } - }); - } - - return dst; -} /** * Removes the prefix `eslint-plugin-` from a plugin name. @@ -133,7 +42,6 @@ function removeNameSpace(pluginName) { } module.exports = { - mergeConfigs: deepmerge, removePluginPrefix: removePluginPrefix, getNamespace: getNamespace, removeNameSpace: removeNameSpace, diff --git a/tests/fixtures/config-file/extends/.eslintrc.yml b/tests/fixtures/config-file/extends/.eslintrc.yml new file mode 100644 index 000000000000..883e4a6c9144 --- /dev/null +++ b/tests/fixtures/config-file/extends/.eslintrc.yml @@ -0,0 +1,3 @@ +extends: ../package-json/package.json +rules: + booya: 2 diff --git a/tests/fixtures/config-file/js/.eslintrc b/tests/fixtures/config-file/js/.eslintrc new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tests/fixtures/config-file/js/.eslintrc @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/config-file/js/.eslintrc.js b/tests/fixtures/config-file/js/.eslintrc.js new file mode 100644 index 000000000000..b3100591dcb4 --- /dev/null +++ b/tests/fixtures/config-file/js/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + semi: [2, "always"] + } +}; diff --git a/tests/fixtures/config-file/json/.eslintrc b/tests/fixtures/config-file/json/.eslintrc new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tests/fixtures/config-file/json/.eslintrc @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/config-file/json/.eslintrc.json b/tests/fixtures/config-file/json/.eslintrc.json new file mode 100644 index 000000000000..ae6226c976e0 --- /dev/null +++ b/tests/fixtures/config-file/json/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "quotes": [2, "double"] + } +} diff --git a/tests/fixtures/config-file/legacy/.eslintrc b/tests/fixtures/config-file/legacy/.eslintrc new file mode 100644 index 000000000000..457c2679902f --- /dev/null +++ b/tests/fixtures/config-file/legacy/.eslintrc @@ -0,0 +1,5 @@ +{ + rules: { + eqeqeq: 2 + } +} diff --git a/tests/fixtures/config-file/package-json/package.json b/tests/fixtures/config-file/package-json/package.json new file mode 100644 index 000000000000..1e0e179e12a7 --- /dev/null +++ b/tests/fixtures/config-file/package-json/package.json @@ -0,0 +1,7 @@ +{ + "eslintConfig": { + "env": { + "es6": true + } + } +} diff --git a/tests/fixtures/config-file/yaml/.eslintrc b/tests/fixtures/config-file/yaml/.eslintrc new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tests/fixtures/config-file/yaml/.eslintrc @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/config-file/yaml/.eslintrc.yaml b/tests/fixtures/config-file/yaml/.eslintrc.yaml new file mode 100644 index 000000000000..223ba3b793b5 --- /dev/null +++ b/tests/fixtures/config-file/yaml/.eslintrc.yaml @@ -0,0 +1,2 @@ +env: + browser: true diff --git a/tests/fixtures/config-file/yaml/.eslintrc.yml b/tests/fixtures/config-file/yaml/.eslintrc.yml new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tests/fixtures/config-file/yaml/.eslintrc.yml @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/config-file/yml/.eslintrc b/tests/fixtures/config-file/yml/.eslintrc new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/tests/fixtures/config-file/yml/.eslintrc @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/config-file/yml/.eslintrc.yml b/tests/fixtures/config-file/yml/.eslintrc.yml new file mode 100644 index 000000000000..485e5859fe33 --- /dev/null +++ b/tests/fixtures/config-file/yml/.eslintrc.yml @@ -0,0 +1,2 @@ +env: + node: true diff --git a/tests/fixtures/config-hierarchy/fileexts/.eslintrc.js b/tests/fixtures/config-hierarchy/fileexts/.eslintrc.js new file mode 100644 index 000000000000..afb32b9bac78 --- /dev/null +++ b/tests/fixtures/config-hierarchy/fileexts/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + rules: { + semi: [2, "always"] + }, + root: true +}; diff --git a/tests/fixtures/config-hierarchy/fileexts/subdir/.eslintrc.yml b/tests/fixtures/config-hierarchy/fileexts/subdir/.eslintrc.yml new file mode 100644 index 000000000000..586c0ffef8af --- /dev/null +++ b/tests/fixtures/config-hierarchy/fileexts/subdir/.eslintrc.yml @@ -0,0 +1,2 @@ +rules: + eqeqeq: 2 diff --git a/tests/fixtures/config-hierarchy/fileexts/subdir/subsubdir/.eslintrc.json b/tests/fixtures/config-hierarchy/fileexts/subdir/subsubdir/.eslintrc.json new file mode 100644 index 000000000000..b99c1118dd5e --- /dev/null +++ b/tests/fixtures/config-hierarchy/fileexts/subdir/subsubdir/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "browser": true + } +} diff --git a/tests/fixtures/config-hierarchy/personal-config/home-folder/.eslintrc b/tests/fixtures/config-hierarchy/personal-config/home-folder/.eslintrc.json similarity index 100% rename from tests/fixtures/config-hierarchy/personal-config/home-folder/.eslintrc rename to tests/fixtures/config-hierarchy/personal-config/home-folder/.eslintrc.json diff --git a/tests/fixtures/file-finder/subdir/empty2 b/tests/fixtures/file-finder/subdir/empty2 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js index ce137cb02598..1f10ada83847 100644 --- a/tests/lib/cli-engine.js +++ b/tests/lib/cli-engine.js @@ -227,7 +227,7 @@ describe("CLIEngine", function() { it("should report zero messages when given a config file and a valid file", function() { engine = new CLIEngine({ - configFile: path.join(__dirname, "..", "..", ".eslintrc") + configFile: path.join(__dirname, "..", "..", ".eslintrc.yml") }); var report = engine.executeOnFiles(["lib/cli*.js"]); @@ -239,7 +239,7 @@ describe("CLIEngine", function() { it("should handle multiple patterns with overlapping files", function() { engine = new CLIEngine({ - configFile: path.join(__dirname, "..", "..", ".eslintrc") + configFile: path.join(__dirname, "..", "..", ".eslintrc.yml") }); var report = engine.executeOnFiles(["lib/cli*.js", "lib/cli.?s", "lib/{cli,cli-engine}.js"]); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index f1a9841d9d1d..5668b94b04c6 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -192,18 +192,24 @@ describe("cli", function() { describe("when given a config that is a sharable config", function() { it("should execute without any errors", function() { - var stubbedConfig = proxyquire("../../lib/config", { + + var configDeps = { + "is-resolvable": function() { + return true; + } + }; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "eslint-config-xo": { rules: { "no-var": 2 } - }, - "is-resolvable": function() { - return true; } }); + + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var stubbedCLIEngine = proxyquire("../../lib/cli-engine", { - "./config": stubbedConfig + "./config": StubbedConfig }); var stubCli = proxyquire("../../lib/cli", { "./cli-engine": stubbedCLIEngine, diff --git a/tests/lib/config.js b/tests/lib/config.js index 2de7b652211c..d4fe8be161cb 100644 --- a/tests/lib/config.js +++ b/tests/lib/config.js @@ -134,6 +134,19 @@ describe("Config", function() { assert.equal(actual, expected); }); + it("should return all possible files when multiple are found", function() { + var configHelper = new Config(), + expected = [ + getFixturePath("fileexts/subdir/subsubdir/", ".eslintrc.json"), + getFixturePath("fileexts/subdir/", ".eslintrc.yml"), + getFixturePath("fileexts", ".eslintrc.js") + ], + + actual = configHelper.findLocalConfigFiles(getFixturePath("fileexts/subdir/subsubdir")); + + assert.deepEqual(actual, expected); + }); + it("should return an empty array when a package.json file is not found", function() { var configHelper = new Config(), actual = configHelper.findLocalConfigFiles(getFixturePath()); @@ -277,6 +290,8 @@ describe("Config", function() { }); it("should return a modified config when baseConfig is set to an object and no .eslintrc", function() { + + var configHelper = new Config({ baseConfig: { env: { @@ -303,7 +318,8 @@ describe("Config", function() { }); it("should return a modified config when baseConfig is set to an object with extend and no .eslintrc", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "eslint-config-foo": { rules: { eqeqeq: [2, "smart"] @@ -311,6 +327,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configHelper = new StubbedConfig({ baseConfig: { env: { @@ -564,6 +582,27 @@ describe("Config", function() { assertConfigsEqual(actual, expected); }); + + it("should merge multiple different config file formats", function() { + + var configHelper = new Config(), + file = getFixturePath("fileexts/subdir/subsubdir/foo.js"), + expected = { + env: { + browser: true + }, + rules: { + semi: [2, "always"], + eqeqeq: 2 + } + }, + actual = configHelper.getConfig(file); + + assertConfigsEqual(actual, expected); + }); + + + it("should load user config globals", function() { var expected, actual, @@ -594,13 +633,18 @@ describe("Config", function() { }); it("should load a sharable config as a command line config", function() { - var StubbedConfig = proxyquire("../../lib/config", { + + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "@test/eslint-config": { rules: { "no-var": 2 } } }); + + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configHelper = new StubbedConfig({ useEslintrc: false, configFile: "@test" @@ -649,7 +693,8 @@ describe("Config", function() { // package extends it("should extend package configuration", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "eslint-config-foo": { rules: { eqeqeq: [2, "smart"] @@ -657,6 +702,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/package/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), expected = { @@ -692,7 +739,9 @@ describe("Config", function() { eqeqeq: [2, "smart"] } }; - var StubbedConfig = proxyquire("../../lib/config", stubSetup); + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", stubSetup); + var StubbedConfig = proxyquire("../../lib/config", configDeps); var configPath = path.resolve(__dirname, "../fixtures/config-extends/js/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), @@ -707,7 +756,8 @@ describe("Config", function() { it("should extend package configuration without prefix", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "eslint-config-foo": { rules: { eqeqeq: [2, "smart"] @@ -715,6 +765,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/package2/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), expected = { @@ -745,7 +797,8 @@ describe("Config", function() { it("should extend scoped package configuration", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "@scope/eslint-config-foo": { rules: { eqeqeq: 2 @@ -753,6 +806,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/scoped-package/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), expected = { @@ -766,7 +821,8 @@ describe("Config", function() { it("should extend scoped package configuration without prefix", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "@scope/eslint-config-foo": { rules: { eqeqeq: 2 @@ -774,6 +830,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/scoped-package2/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), expected = { @@ -787,7 +845,8 @@ describe("Config", function() { it("should not modify a scoped package named 'eslint-config'", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "@scope/eslint-config": { rules: { eqeqeq: 2 @@ -795,6 +854,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/scoped-package4/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), expected = { @@ -808,7 +869,8 @@ describe("Config", function() { it("should extend a scope with a slash to '@scope/eslint-config'", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "@scope/eslint-config": { rules: { eqeqeq: 2 @@ -816,6 +878,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/scoped-package5/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), expected = { @@ -829,7 +893,8 @@ describe("Config", function() { it("should extend a lone scope to '@scope/eslint-config'", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "@scope/eslint-config": { rules: { eqeqeq: 2 @@ -837,6 +902,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/scoped-package6/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), expected = { @@ -850,7 +917,8 @@ describe("Config", function() { it("should still prefix a name prefix of 'eslint-config' without a dash, with a dash", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "@scope/eslint-config-eslint-configfoo": { rules: { eqeqeq: 2 @@ -858,6 +926,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/scoped-package7/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: false, configFile: configPath }), expected = { @@ -871,7 +941,8 @@ describe("Config", function() { it("should extend package sub-configuration without prefix", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "eslint-config-foo/bar": { rules: { eqeqeq: 2 @@ -879,6 +950,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/package3/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: true, configFile: configPath }), expected = { @@ -900,7 +973,8 @@ describe("Config", function() { it("should extend scoped package sub-configuration without prefix", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "@scope/eslint-config-foo/bar": { rules: { eqeqeq: 2 @@ -908,6 +982,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/scoped-package3/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: true, configFile: configPath }), expected = { @@ -929,7 +1005,8 @@ describe("Config", function() { it("should extend package configuration with sub directories", function() { - var StubbedConfig = proxyquire("../../lib/config", { + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", { "eslint-config-foo": { rules: { "eqeqeq": 2 @@ -937,6 +1014,8 @@ describe("Config", function() { } }); + var StubbedConfig = proxyquire("../../lib/config", configDeps); + var configPath = path.resolve(__dirname, "../fixtures/config-extends/package2/.eslintrc"), configHelper = new StubbedConfig({ useEslintrc: true, configFile: configPath }), expected = { @@ -1116,9 +1195,14 @@ describe("Config", function() { }); it("should not clobber config objects when loading shared configs", function() { - requireStubs[exampleConfigName] = { rules: { "example-rule": 2 } }; - StubbedConfig = proxyquire("../../lib/config", requireStubs); + var configFileDeps = {}; + configFileDeps[exampleConfigName] = { rules: { "example-rule": 2 } }; + + var configDeps = {}; + configDeps["./config/config-file"] = proxyquire("../../lib/config/config-file", configFileDeps); + + StubbedConfig = proxyquire("../../lib/config", configDeps); var configHelper = new StubbedConfig({}), file1 = getFixturePath("shared", "a", "index.js"), diff --git a/tests/lib/config/config-file.js b/tests/lib/config/config-file.js new file mode 100644 index 000000000000..d670a1ad9142 --- /dev/null +++ b/tests/lib/config/config-file.js @@ -0,0 +1,301 @@ +/** + * @fileoverview Tests for ConfigFile + * @author Nicholas C. Zakas + * @copyright 2015 Nicholas C. Zakas. All rights reserved. + * See LICENSE file in root directory for full license. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var assert = require("chai").assert, + leche = require("leche"), + path = require("path"), + proxyquire = require("proxyquire"), + environments = require("../../../conf/environments"), + ConfigFile = require("../../../lib/config/config-file"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +proxyquire = proxyquire.noCallThru().noPreserveCache(); + +/** + * Helper function get easily get a path in the fixtures directory. + * @param {string} filepath The path to find in the fixtures directory. + * @returns {string} Full path in the fixtures directory. + * @private + */ +function getFixturePath(filepath) { + return path.resolve(__dirname, "../../fixtures/config-file", filepath); +} + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("ConfigFile", function() { + + describe("CONFIG_FILES", function() { + it("should be present when imported", function() { + assert.isTrue(Array.isArray(ConfigFile.CONFIG_FILES)); + }); + }); + + describe("applyExtends", function() { + + it("should apply extensions when specified from package", function() { + + var StubbedConfigFile = proxyquire("../../../lib/config/config-file", { + "eslint-config-foo": { + env: { browser: true } + } + }); + + var config = StubbedConfigFile.applyExtends({ + extends: "foo", + rules: { eqeqeq: 2 } + }, "/whatever"); + + assert.deepEqual(config, { + extends: "foo", + ecmaFeatures: {}, + env: { browser: true }, + globals: environments.browser.globals, + rules: { eqeqeq: 2 } + }); + + }); + + it("should apply extensions recursively when specified from package", function() { + + var StubbedConfigFile = proxyquire("../../../lib/config/config-file", { + "eslint-config-foo": { + extends: "bar", + env: { browser: true } + }, + "eslint-config-bar": { + rules: { + bar: 2 + } + } + }); + + var config = StubbedConfigFile.applyExtends({ + extends: "foo", + rules: { eqeqeq: 2 } + }, "/whatever"); + + assert.deepEqual(config, { + extends: "foo", + ecmaFeatures: {}, + env: { browser: true }, + globals: environments.browser.globals, + rules: { + eqeqeq: 2, + bar: 2 + } + }); + + }); + + it("should apply extensions when specified from a JavaScript file", function() { + + var config = ConfigFile.applyExtends({ + extends: ".eslintrc.js", + rules: { eqeqeq: 2 } + }, getFixturePath("js/foo.js")); + + assert.deepEqual(config, { + extends: ".eslintrc.js", + ecmaFeatures: {}, + env: {}, + globals: {}, + rules: { + semi: [2, "always"], + eqeqeq: 2 + } + }); + + }); + + it("should apply extensions when specified from a YAML file", function() { + + var config = ConfigFile.applyExtends({ + extends: ".eslintrc.yaml", + rules: { eqeqeq: 2 } + }, getFixturePath("yaml/foo.js")); + + assert.deepEqual(config, { + extends: ".eslintrc.yaml", + ecmaFeatures: {}, + env: { browser: true }, + globals: environments.browser.globals, + rules: { + eqeqeq: 2 + } + }); + + }); + + it("should apply extensions when specified from a JSON file", function() { + + var config = ConfigFile.applyExtends({ + extends: ".eslintrc.json", + rules: { eqeqeq: 2 } + }, getFixturePath("json/foo.js")); + + assert.deepEqual(config, { + extends: ".eslintrc.json", + ecmaFeatures: {}, + env: {}, + globals: {}, + rules: { + eqeqeq: 2, + quotes: [2, "double"] + } + }); + + }); + + it("should apply extensions when specified from a package.json file in a sibling directory", function() { + + var config = ConfigFile.applyExtends({ + extends: "../package-json/package.json", + rules: { eqeqeq: 2 } + }, getFixturePath("json/foo.js")); + + assert.deepEqual(config, { + extends: "../package-json/package.json", + ecmaFeatures: environments.es6.ecmaFeatures, + env: { es6: true }, + globals: {}, + rules: { + eqeqeq: 2 + } + }); + + }); + + }); + + describe("load", function() { + + it("should load information from a legacy file", function() { + var config = ConfigFile.load(getFixturePath("legacy/.eslintrc")); + assert.deepEqual(config, { + ecmaFeatures: {}, + env: {}, + globals: {}, + rules: { + eqeqeq: 2 + } + }); + }); + + it("should load information from a JavaScript file", function() { + var config = ConfigFile.load(getFixturePath("js/.eslintrc.js")); + assert.deepEqual(config, { + ecmaFeatures: {}, + env: {}, + globals: {}, + rules: { + semi: [2, "always"] + } + }); + }); + + it("should load information from a JSON file", function() { + var config = ConfigFile.load(getFixturePath("json/.eslintrc.json")); + assert.deepEqual(config, { + ecmaFeatures: {}, + env: {}, + globals: {}, + rules: { + quotes: [2, "double"] + } + }); + }); + + it("should load information from a package.json file", function() { + var config = ConfigFile.load(getFixturePath("package-json/package.json")); + assert.deepEqual(config, { + ecmaFeatures: environments.es6.ecmaFeatures, + env: { es6: true }, + globals: {}, + rules: {} + }); + }); + + it("should load information from a YAML file", function() { + var config = ConfigFile.load(getFixturePath("yaml/.eslintrc.yaml")); + assert.deepEqual(config, { + ecmaFeatures: {}, + env: { browser: true }, + globals: environments.browser.globals, + rules: {} + }); + }); + + it("should load information from a YML file", function() { + var config = ConfigFile.load(getFixturePath("yml/.eslintrc.yml")); + assert.deepEqual(config, { + ecmaFeatures: { globalReturn: true }, + env: { node: true }, + globals: environments.node.globals, + rules: {} + }); + }); + + it("should load information from a YML file and apply extensions", function() { + var config = ConfigFile.load(getFixturePath("extends/.eslintrc.yml")); + assert.deepEqual(config, { + extends: "../package-json/package.json", + ecmaFeatures: environments.es6.ecmaFeatures, + env: { es6: true }, + globals: {}, + rules: { booya: 2 } + }); + }); + + }); + + describe("resolve()", function() { + + leche.withData([ + [ ".eslintrc", path.resolve(".eslintrc") ], + [ "eslint-config-foo", "eslint-config-foo" ], + [ "foo", "eslint-config-foo" ], + [ "eslint-configfoo", "eslint-config-eslint-configfoo" ], + [ "@foo/eslint-config", "@foo/eslint-config" ], + [ "@foo/bar", "@foo/eslint-config-bar" ] + ], function(input, expected) { + it("should return " + expected + " when passed " + input, function() { + var result = ConfigFile.resolve(input); + assert.equal(result, expected); + }); + }); + + }); + + describe("getFilenameFromDirectory()", function() { + + leche.withData([ + [ getFixturePath("legacy"), ".eslintrc" ], + [ getFixturePath("yaml"), ".eslintrc.yaml" ], + [ getFixturePath("yml"), ".eslintrc.yml" ], + [ getFixturePath("json"), ".eslintrc.json" ], + [ getFixturePath("js"), ".eslintrc.js" ] + ], function(input, expected) { + it("should return " + expected + " when passed " + input, function() { + var result = ConfigFile.getFilenameForDirectory(input); + assert.equal(result, path.resolve(input, expected)); + }); + }); + + }); + +}); diff --git a/tests/lib/config-initializer.js b/tests/lib/config/config-initializer.js similarity index 93% rename from tests/lib/config-initializer.js rename to tests/lib/config/config-initializer.js index 2d8ba414dba3..856cda3543fd 100644 --- a/tests/lib/config-initializer.js +++ b/tests/lib/config/config-initializer.js @@ -10,7 +10,11 @@ //------------------------------------------------------------------------------ var assert = require("chai").assert, - init = require("../../lib/config-initializer"); + init = require("../../../lib/config/config-initializer"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ var answers = {}; diff --git a/tests/lib/config/config-ops.js b/tests/lib/config/config-ops.js new file mode 100644 index 000000000000..96534ef1a660 --- /dev/null +++ b/tests/lib/config/config-ops.js @@ -0,0 +1,407 @@ +/** + * @fileoverview Tests for ConfigOps + * @author Nicholas C. Zakas + * @copyright 2015 Nicholas C. Zakas. All rights reserved. + * See LICENSE file in root directory for full license. + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var assert = require("chai").assert, + assign = require("object-assign"), + environments = require("../../../conf/environments"), + ConfigOps = require("../../../lib/config/config-ops"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("ConfigOps", function() { + + describe("applyEnvironments()", function() { + it("should apply environment settings to config without destroying original settings", function() { + var config = { + env: { + node: true + }, + rules: { + foo: 2 + } + }; + + var result = ConfigOps.applyEnvironments(config); + + assert.deepEqual(result, { + env: config.env, + rules: config.rules, + ecmaFeatures: environments.node.ecmaFeatures, + globals: environments.node.globals + }); + }); + + it("should not apply environment settings to config without environments", function() { + var config = { + rules: { + foo: 2 + } + }; + + var result = ConfigOps.applyEnvironments(config); + + assert.equal(result, config); + }); + + it("should apply multiple environment settings to config without destroying original settings", function() { + var config = { + env: { + node: true, + es6: true + }, + rules: { + foo: 2 + } + }; + + var result = ConfigOps.applyEnvironments(config); + + assert.deepEqual(result, { + env: config.env, + rules: config.rules, + ecmaFeatures: assign({}, environments.node.ecmaFeatures, environments.es6.ecmaFeatures), + globals: assign({}, environments.node.globals, environments.es6.globals) + }); + }); + }); + + describe("createEnvironmentConfig()", function() { + + it("should create the correct config for Node.js environment", function() { + var config = ConfigOps.createEnvironmentConfig({ node: true }); + assert.deepEqual(config, { + env: { + node: true + }, + ecmaFeatures: environments.node.ecmaFeatures, + globals: environments.node.globals, + rules: {} + }); + }); + + it("should create the correct config for ES6 environment", function() { + var config = ConfigOps.createEnvironmentConfig({ es6: true }); + assert.deepEqual(config, { + env: { + es6: true + }, + ecmaFeatures: environments.es6.ecmaFeatures, + globals: {}, + rules: {} + }); + }); + + it("should create empty config when no environments are specified", function() { + var config = ConfigOps.createEnvironmentConfig({}); + assert.deepEqual(config, { + env: {}, + ecmaFeatures: {}, + globals: {}, + rules: {} + }); + }); + + it("should create empty config when an unknown environment is specified", function() { + var config = ConfigOps.createEnvironmentConfig({ foo: true }); + assert.deepEqual(config, { + env: { + foo: true + }, + ecmaFeatures: {}, + globals: {}, + rules: {} + }); + }); + + }); + + describe("merge()", function() { + + it("should combine two objects when passed two objects with different top-level properties", function() { + var config = [ + { env: { browser: true } }, + { globals: { foo: "bar"} } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.equal(result.globals.foo, "bar"); + assert.isTrue(result.env.browser); + }); + + it("should combine without blowing up on null values", function() { + var config = [ + { env: { browser: true } }, + { env: { node: null } } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.equal(result.env.node, null); + assert.isTrue(result.env.browser); + }); + + it("should combine two objects with parser when passed two objects with different top-level properties", function() { + var config = [ + { env: { browser: true }, parser: "espree" }, + { globals: { foo: "bar"} } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.equal(result.parser, "espree"); + }); + + it("should combine configs and override rules when passed configs with the same rules", function() { + var config = [ + { rules: { "no-mixed-requires": [0, false] } }, + { rules: { "no-mixed-requires": [1, true] } } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.isArray(result.rules["no-mixed-requires"]); + assert.equal(result.rules["no-mixed-requires"][0], 1); + assert.equal(result.rules["no-mixed-requires"][1], true); + }); + + it("should combine configs when passed configs with ecmaFeatures", function() { + var config = [ + { ecmaFeatures: { blockBindings: true } }, + { ecmaFeatures: { forOf: true } } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.deepEqual(result, { + ecmaFeatures: { + blockBindings: true, + forOf: true + } + }); + + assert.deepEqual(config[0], { ecmaFeatures: { blockBindings: true }}); + assert.deepEqual(config[1], { ecmaFeatures: { forOf: true }}); + }); + + it("should override configs when passed configs with the same ecmaFeatures", function() { + var config = [ + { ecmaFeatures: { forOf: false } }, + { ecmaFeatures: { forOf: true } } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.deepEqual(result, { + ecmaFeatures: { + forOf: true + } + }); + }); + + it("should combine configs and override rules when merging two configs with arrays and int", function() { + + var config = [ + { rules: { "no-mixed-requires": [0, false] } }, + { rules: { "no-mixed-requires": 1 } } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.isArray(result.rules["no-mixed-requires"]); + assert.equal(result.rules["no-mixed-requires"][0], 1); + assert.equal(result.rules["no-mixed-requires"][1], false); + assert.deepEqual(config[0], { rules: { "no-mixed-requires": [0, false] }}); + assert.deepEqual(config[1], { rules: { "no-mixed-requires": 1 }}); + }); + + it("should combine configs and override rules options completely", function() { + + var config = [ + { rules: { "no-mixed-requires": [1, { "event": ["evt", "e"] }] } }, + { rules: { "no-mixed-requires": [1, { "err": ["error", "e"] }] } } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.isArray(result.rules["no-mixed-requires"]); + assert.deepEqual(result.rules["no-mixed-requires"][1], {"err": ["error", "e"]}); + assert.deepEqual(config[0], { rules: { "no-mixed-requires": [1, {"event": ["evt", "e"]}] }}); + assert.deepEqual(config[1], { rules: { "no-mixed-requires": [1, {"err": ["error", "e"]}] }}); + }); + + it("should combine configs and override rules options without array or object", function() { + + var config = [ + { rules: { "no-mixed-requires": [1, "nconf", "underscore"] } }, + { rules: { "no-mixed-requires": [2, "requirejs"] } } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.strictEqual(result.rules["no-mixed-requires"][0], 2); + assert.strictEqual(result.rules["no-mixed-requires"][1], "requirejs"); + assert.isUndefined(result.rules["no-mixed-requires"][2]); + assert.deepEqual(config[0], { rules: { "no-mixed-requires": [1, "nconf", "underscore"] }}); + assert.deepEqual(config[1], { rules: { "no-mixed-requires": [2, "requirejs"] }}); + }); + + it("should combine configs and override rules options without array or object but special case", function() { + + var config = [ + { rules: { "no-mixed-requires": [1, "nconf", "underscore"] } }, + { rules: { "no-mixed-requires": 2 } } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.strictEqual(result.rules["no-mixed-requires"][0], 2); + assert.strictEqual(result.rules["no-mixed-requires"][1], "nconf"); + assert.strictEqual(result.rules["no-mixed-requires"][2], "underscore"); + assert.deepEqual(config[0], { rules: { "no-mixed-requires": [1, "nconf", "underscore"] }}); + assert.deepEqual(config[1], { rules: { "no-mixed-requires": 2 }}); + }); + + it("should combine configs correctly", function() { + + var config = [ + { + rules: { + "no-mixed-requires": [1, { "event": ["evt", "e"] }], + "valid-jsdoc": 1, + "semi": 1, + "quotes": [2, { "exception": ["hi"] }], + "smile": [1, ["hi", "bye"]] + }, + ecmaFeatures: { blockBindings: true }, + env: { browser: true }, + globals: { foo: false} + }, + { + rules: { + "no-mixed-requires": [1, { "err": ["error", "e"] }], + "valid-jsdoc": 2, + "test": 1, + "smile": [1, ["xxx", "yyy"]] + }, + ecmaFeatures: { forOf: true }, + env: { browser: false }, + globals: { foo: true} + } + ]; + + var result = ConfigOps.merge(config[0], config[1]); + + assert.deepEqual(result, { + "ecmaFeatures": { + "blockBindings": true, + "forOf": true + }, + "env": { + "browser": false + }, + "globals": { + "foo": true + }, + "rules": { + "no-mixed-requires": [1, + { + "err": [ + "error", + "e" + ] + } + ], + "quotes": [2, + { + "exception": [ + "hi" + ] + } + ], + "semi": 1, + "smile": [1, ["xxx", "yyy"]], + "test": 1, + "valid-jsdoc": 2 + } + }); + assert.deepEqual(config[0], { + rules: { + "no-mixed-requires": [1, { "event": ["evt", "e"] }], + "valid-jsdoc": 1, + "semi": 1, + "quotes": [2, { "exception": ["hi"] }], + "smile": [1, ["hi", "bye"]] + }, + ecmaFeatures: { blockBindings: true }, + env: { browser: true }, + globals: { foo: false} + }); + assert.deepEqual(config[1], { + rules: { + "no-mixed-requires": [1, { "err": ["error", "e"] }], + "valid-jsdoc": 2, + "test": 1, + "smile": [1, ["xxx", "yyy"]] + }, + ecmaFeatures: { forOf: true }, + env: { browser: false }, + globals: { foo: true } + }); + }); + + describe("plugins", function() { + var baseConfig; + + beforeEach(function() { + baseConfig = { plugins: ["foo", "bar"] }; + }); + + it("should combine the plugin entries when each config has different plugins", function() { + var customConfig = { plugins: ["baz"] }, + expectedResult = { plugins: ["foo", "bar", "baz"] }, + result; + + result = ConfigOps.merge(baseConfig, customConfig); + + assert.deepEqual(result, expectedResult); + assert.deepEqual(baseConfig, { plugins: ["foo", "bar"] }); + assert.deepEqual(customConfig, { plugins: ["baz"] }); + }); + + it("should avoid duplicate plugin entries when each config has the same plugin", function() { + var customConfig = { plugins: ["bar"] }, + expectedResult = { plugins: ["foo", "bar"] }, + result; + + result = ConfigOps.merge(baseConfig, customConfig); + + assert.deepEqual(result, expectedResult); + }); + + it("should create a valid config when one argument is an empty object", function() { + var customConfig = { plugins: ["foo"] }, + result; + + result = ConfigOps.merge({}, customConfig); + + assert.deepEqual(result, customConfig); + assert.notEqual(result, customConfig); + }); + }); + + + }); + +}); diff --git a/tests/lib/config-validator.js b/tests/lib/config/config-validator.js similarity index 98% rename from tests/lib/config-validator.js rename to tests/lib/config/config-validator.js index 6149b24867fe..1dbe927b7b12 100644 --- a/tests/lib/config-validator.js +++ b/tests/lib/config/config-validator.js @@ -11,8 +11,8 @@ //------------------------------------------------------------------------------ var assert = require("chai").assert, - eslint = require("../../lib/eslint"), - validator = require("../../lib/config-validator"); + eslint = require("../../../lib/eslint"), + validator = require("../../../lib/config/config-validator"); //------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/file-finder.js b/tests/lib/file-finder.js index a00fcb105b5e..1ded8736ba78 100644 --- a/tests/lib/file-finder.js +++ b/tests/lib/file-finder.js @@ -20,11 +20,13 @@ var assert = require("chai").assert, describe("FileFinder", function() { var fixtureDir = path.resolve(__dirname, "..", "fixtures"), fileFinderDir = path.join(fixtureDir, "file-finder"), - subsubsubdir = path.join(fileFinderDir, "subdir", "subsubdir", "subsubsubdir"), + subdir = path.join(fileFinderDir, "subdir"), + subsubdir = path.join(subdir, "subsubdir"), + subsubsubdir = path.join(subsubdir, "subsubsubdir"), absentFileName = "4ktrgrtUTYjkopoohFe54676hnjyumlimn6r787", uniqueFileName = "xvgRHtyH56756764535jkJ6jthty65tyhteHTEY"; - describe("findInDirectoryOrParents", function() { + describe("findInDirectoryOrParents()", function() { describe("a searched for file that is present", function() { var actual, @@ -44,6 +46,30 @@ describe("FileFinder", function() { } }); + it("should be found when in the cwd and passed an array", function() { + process.chdir(fileFinderDir); + finder = new FileFinder([".eslintignore"]); + actual = finder.findInDirectoryOrParents(); + + try { + assert.equal(actual, expected); + } finally { + process.chdir(cwd); + } + }); + + it("should be found when in the cwd and passed an array with a missing file", function() { + process.chdir(fileFinderDir); + finder = new FileFinder(["missing", ".eslintignore"]); + actual = finder.findInDirectoryOrParents(); + + try { + assert.equal(actual, expected); + } finally { + process.chdir(cwd); + } + }); + it("should be found when in a parent directory of the cwd", function() { process.chdir(subsubsubdir); finder = new FileFinder(".eslintignore"); @@ -100,7 +126,7 @@ describe("FileFinder", function() { }); }); - describe("findAllInDirectoryAndParents", function() { + describe("findAllInDirectoryAndParents()", function() { var actual, expected, finder; @@ -129,11 +155,53 @@ describe("FileFinder", function() { }); }); + describe("searching for multiple files", function() { + + it("should return only the first specified file", function() { + var firstExpected = path.join(fileFinderDir, "subdir", "empty"), + secondExpected = path.join(fileFinderDir, "empty"); + + finder = new FileFinder(["empty", uniqueFileName]); + actual = finder.findAllInDirectoryAndParents(subdir); + + assert.equal(actual.length, 2); + assert.equal(actual[0], firstExpected); + assert.equal(actual[1], secondExpected); + }); + + it("should return the second file when the first is missing", function() { + var firstExpected = path.join(fileFinderDir, "subdir", uniqueFileName), + secondExpected = path.join(fileFinderDir, uniqueFileName); + + finder = new FileFinder(["notreal", uniqueFileName]); + actual = finder.findAllInDirectoryAndParents(subdir); + + assert.equal(actual.length, 2); + assert.equal(actual[0], firstExpected); + assert.equal(actual[1], secondExpected); + }); + + it("should return multiple files when the first is missing and more than one filename is requested", function() { + var firstExpected = path.join(fileFinderDir, "subdir", uniqueFileName), + secondExpected = path.join(fileFinderDir, "subdir", "empty2"); + + finder = new FileFinder(["notreal", uniqueFileName], "empty2"); + actual = finder.findAllInDirectoryAndParents(subdir); + + assert.equal(actual.length, 3); + assert.equal(actual[0], firstExpected); + assert.equal(actual[1], secondExpected); + }); + + }); + describe("two files present with the same name in parent directories", function() { var firstExpected = path.join(fileFinderDir, "subdir", uniqueFileName), secondExpected = path.join(fileFinderDir, uniqueFileName); - finder = new FileFinder(uniqueFileName); + before(function() { + finder = new FileFinder(uniqueFileName); + }); it("should both be found, and returned in an array", function() { actual = finder.findAllInDirectoryAndParents(subsubsubdir); @@ -144,8 +212,6 @@ describe("FileFinder", function() { }); it("should be in the cache after they have been found", function() { - var subdir = path.join(fileFinderDir, "subdir"), - subsubdir = path.join(subdir, "subsubdir"); assert.equal(finder.cache[subsubsubdir][0], firstExpected); assert.equal(finder.cache[subsubsubdir][1], secondExpected); diff --git a/tests/lib/util.js b/tests/lib/util.js index 29b113bc54e9..8c10c2ba173d 100644 --- a/tests/lib/util.js +++ b/tests/lib/util.js @@ -43,282 +43,4 @@ describe("util", function() { }); }); - describe("mergeConfigs()", function() { - - it("should combine two objects when passed two objects with different top-level properties", function() { - var config = [ - { env: { browser: true } }, - { globals: { foo: "bar"} } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.equal(result.globals.foo, "bar"); - assert.isTrue(result.env.browser); - }); - - it("should combine without blowing up on null values", function() { - var config = [ - { env: { browser: true } }, - { env: { node: null } } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.equal(result.env.node, null); - assert.isTrue(result.env.browser); - }); - - it("should combine two objects with parser when passed two objects with different top-level properties", function() { - var config = [ - { env: { browser: true }, parser: "espree" }, - { globals: { foo: "bar"} } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.equal(result.parser, "espree"); - }); - - it("should combine configs and override rules when passed configs with the same rules", function() { - var config = [ - { rules: { "no-mixed-requires": [0, false] } }, - { rules: { "no-mixed-requires": [1, true] } } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.isArray(result.rules["no-mixed-requires"]); - assert.equal(result.rules["no-mixed-requires"][0], 1); - assert.equal(result.rules["no-mixed-requires"][1], true); - }); - - it("should combine configs when passed configs with ecmaFeatures", function() { - var config = [ - { ecmaFeatures: { blockBindings: true } }, - { ecmaFeatures: { forOf: true } } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.deepEqual(result, { - ecmaFeatures: { - blockBindings: true, - forOf: true - } - }); - - assert.deepEqual(config[0], { ecmaFeatures: { blockBindings: true }}); - assert.deepEqual(config[1], { ecmaFeatures: { forOf: true }}); - }); - - it("should override configs when passed configs with the same ecmaFeatures", function() { - var config = [ - { ecmaFeatures: { forOf: false } }, - { ecmaFeatures: { forOf: true } } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.deepEqual(result, { - ecmaFeatures: { - forOf: true - } - }); - }); - - it("should combine configs and override rules when merging two configs with arrays and int", function() { - - var config = [ - { rules: { "no-mixed-requires": [0, false] } }, - { rules: { "no-mixed-requires": 1 } } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.isArray(result.rules["no-mixed-requires"]); - assert.equal(result.rules["no-mixed-requires"][0], 1); - assert.equal(result.rules["no-mixed-requires"][1], false); - assert.deepEqual(config[0], { rules: { "no-mixed-requires": [0, false] }}); - assert.deepEqual(config[1], { rules: { "no-mixed-requires": 1 }}); - }); - - it("should combine configs and override rules options completely", function() { - - var config = [ - { rules: { "no-mixed-requires": [1, {"event": ["evt", "e"]}] } }, - { rules: { "no-mixed-requires": [1, {"err": ["error", "e"]}] } } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.isArray(result.rules["no-mixed-requires"]); - assert.deepEqual(result.rules["no-mixed-requires"][1], {"err": ["error", "e"]}); - assert.deepEqual(config[0], { rules: { "no-mixed-requires": [1, {"event": ["evt", "e"]}] }}); - assert.deepEqual(config[1], { rules: { "no-mixed-requires": [1, {"err": ["error", "e"]}] }}); - }); - - it("should combine configs and override rules options without array or object", function() { - - var config = [ - { rules: { "no-mixed-requires": [1, "nconf", "underscore"] } }, - { rules: { "no-mixed-requires": [2, "requirejs"] } } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.strictEqual(result.rules["no-mixed-requires"][0], 2); - assert.strictEqual(result.rules["no-mixed-requires"][1], "requirejs"); - assert.isUndefined(result.rules["no-mixed-requires"][2]); - assert.deepEqual(config[0], { rules: { "no-mixed-requires": [1, "nconf", "underscore"] }}); - assert.deepEqual(config[1], { rules: { "no-mixed-requires": [2, "requirejs"] }}); - }); - - it("should combine configs and override rules options without array or object but special case", function() { - - var config = [ - { rules: { "no-mixed-requires": [1, "nconf", "underscore"] } }, - { rules: { "no-mixed-requires": 2 } } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.strictEqual(result.rules["no-mixed-requires"][0], 2); - assert.strictEqual(result.rules["no-mixed-requires"][1], "nconf"); - assert.strictEqual(result.rules["no-mixed-requires"][2], "underscore"); - assert.deepEqual(config[0], { rules: { "no-mixed-requires": [1, "nconf", "underscore"] }}); - assert.deepEqual(config[1], { rules: { "no-mixed-requires": 2 }}); - }); - - it("should combine configs correctly", function() { - - var config = [ - { - rules: { - "no-mixed-requires": [1, {"event": ["evt", "e"]}], - "valid-jsdoc": 1, - "semi": 1, - "quotes": [2, {"exception": ["hi"]}], - "smile": [1, ["hi", "bye"]] - }, - ecmaFeatures: { blockBindings: true }, - env: { browser: true }, - globals: { foo: false} - }, - { - rules: { - "no-mixed-requires": [1, {"err": ["error", "e"]}], - "valid-jsdoc": 2, - "test": 1, - "smile": [1, ["xxx", "yyy"]] - }, - ecmaFeatures: { forOf: true }, - env: { browser: false }, - globals: { foo: true} - } - ]; - - var result = util.mergeConfigs(config[0], config[1]); - - assert.deepEqual(result, { - "ecmaFeatures": { - "blockBindings": true, - "forOf": true - }, - "env": { - "browser": false - }, - "globals": { - "foo": true - }, - "rules": { - "no-mixed-requires": [1, - { - "err": [ - "error", - "e" - ] - } - ], - "quotes": [2, - { - "exception": [ - "hi" - ] - } - ], - "semi": 1, - "smile": [1, ["xxx", "yyy"]], - "test": 1, - "valid-jsdoc": 2 - } - }); - assert.deepEqual(config[0], { - rules: { - "no-mixed-requires": [1, {"event": ["evt", "e"]}], - "valid-jsdoc": 1, - "semi": 1, - "quotes": [2, {"exception": ["hi"]}], - "smile": [1, ["hi", "bye"]] - }, - ecmaFeatures: { blockBindings: true }, - env: { browser: true }, - globals: { foo: false} - }); - assert.deepEqual(config[1], { - rules: { - "no-mixed-requires": [1, {"err": ["error", "e"]}], - "valid-jsdoc": 2, - "test": 1, - "smile": [1, ["xxx", "yyy"]] - }, - ecmaFeatures: { forOf: true }, - env: { browser: false }, - globals: { foo: true} - }); - }); - - describe("plugins", function() { - var baseConfig; - - beforeEach(function() { - baseConfig = { plugins: ["foo", "bar"] }; - }); - - it("should combine the plugin entries when each config has different plugins", function() { - var customConfig = { plugins: ["baz"] }, - expectedResult = { plugins: ["foo", "bar", "baz"] }, - result; - - result = util.mergeConfigs(baseConfig, customConfig); - - assert.deepEqual(result, expectedResult); - assert.deepEqual(baseConfig, { plugins: ["foo", "bar"] }); - assert.deepEqual(customConfig, { plugins: ["baz"] }); - }); - - it("should avoid duplicate plugin entries when each config has the same plugin", function() { - var customConfig = { plugins: ["bar"] }, - expectedResult = { plugins: ["foo", "bar"] }, - result; - - result = util.mergeConfigs(baseConfig, customConfig); - - assert.deepEqual(result, expectedResult); - }); - - it("should create a valid config when one argument is an empty object", function() { - var customConfig = { plugins: ["foo"] }, - result; - - result = util.mergeConfigs({}, customConfig); - - assert.deepEqual(result, customConfig); - assert.notEqual(result, customConfig); - }); - }); - - - }); - }); From 87b5e4f1396a1984a6ba2bd69c2d8cf898990d17 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson Date: Fri, 16 Oct 2015 01:08:59 +0100 Subject: [PATCH 22/63] New: Disable comment config option (fixes #3901) This adds a `noInlineConfig` option, off by default, that disables inline configuration in comments of a particular file. This disable: * `/*eslint-disable*/` * `/*eslint-enable*/` * `/*global*/` * `/*eslint*/` * `/*eslint-env*/` (fixes #3901) --- docs/developer-guide/nodejs-api.md | 1 + docs/user-guide/command-line-interface.md | 21 ++- lib/cli-engine.js | 20 ++- lib/cli.js | 3 +- lib/eslint.js | 10 +- lib/options.js | 6 + tests/fixtures/disable-inline-config.js | 1 + tests/lib/cli-engine.js | 56 ++++++++ tests/lib/cli.js | 67 +++++++++ tests/lib/eslint.js | 162 ++++++++++++++++++++++ tests/lib/options.js | 12 ++ 11 files changed, 349 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/disable-inline-config.js diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index b9776e6cff6c..58f33deed86e 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -31,6 +31,7 @@ The most important method on `linter` is `verify()`, which initiates linting of * `options` - (optional) Additional options for this run. * `filename` - (optional) the filename to associate with the source code. * `saveState` - (optional) set to true to maintain the internal state of `linter` after linting (mostly used for testing purposes). + * `allowInlineConfig` - (optional) set to `false` to disable inline comments from changing eslint rules. You can call `verify()` like this: diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md index a1a4cbf9567d..377ecbe3264f 100644 --- a/docs/user-guide/command-line-interface.md +++ b/docs/user-guide/command-line-interface.md @@ -69,7 +69,9 @@ Miscellaneous: --debug Output debugging information -h, --help Show help -v, --version Outputs the version number - ``` + --no-inline-config Prevent comments from changing eslint rules - + default: false +``` ### Basic configuration @@ -329,6 +331,23 @@ Example: eslint -v +#### `--no-inline-config` + +This option prevents inline comments like `/*eslint-disable*/` or +`/*global foo*/` from having any effect. This allows you to set an ESLint +config without files modifying it. All inline config comments are ignored, e.g.: + +* `/*eslint-disable*/` +* `/*eslint-enable*/` +* `/*global*/` +* `/*eslint*/` +* `/*eslint-env*/` +* `// eslint-disable-line` + +Example: + + eslint --no-inline-config file.js + ## Ignoring files from linting ESLint supports `.eslintignore` files to exclude files from the linting process when ESLint operates on a directory. Files given as individual CLI arguments will be exempt from exclusion. The `.eslintignore` file is a plain text file containing one pattern per line. It can be located in any of the target directory's ancestors; it will affect files in its containing directory as well as all sub-directories. Here's a simple example of a `.eslintignore` file: diff --git a/lib/cli-engine.js b/lib/cli-engine.js index dbda87f83333..a17b1f3f6f08 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -95,7 +95,8 @@ var defaultOptions = { // it will always be used cacheLocation: "", cacheFile: ".eslintcache", - fix: false + fix: false, + allowInlineConfig: true }, loadedPlugins = Object.create(null); @@ -176,10 +177,11 @@ function calculateStatsPerRun(results) { * @param {Object} configHelper The configuration options for ESLint. * @param {string} filename An optional string representing the texts filename. * @param {boolean} fix Indicates if fixes should be processed. + * @param {boolean} allowInlineConfig Allow/ignore comments that change config. * @returns {Result} The results for linting on this text. * @private */ -function processText(text, configHelper, filename, fix) { +function processText(text, configHelper, filename, fix, allowInlineConfig) { // clear all existing settings for a new file eslint.reset(); @@ -213,7 +215,10 @@ function processText(text, configHelper, filename, fix) { var parsedBlocks = processor.preprocess(text, filename); var unprocessedMessages = []; parsedBlocks.forEach(function(block) { - unprocessedMessages.push(eslint.verify(block, config, filename)); + unprocessedMessages.push(eslint.verify(block, config, { + filename: filename, + allowInlineConfig: allowInlineConfig + })); }); // TODO(nzakas): Figure out how fixes might work for processors @@ -222,7 +227,10 @@ function processText(text, configHelper, filename, fix) { } else { - messages = eslint.verify(text, config, filename); + messages = eslint.verify(text, config, { + filename: filename, + allowInlineConfig: allowInlineConfig + }); if (fix) { debug("Generating fixed text for " + filename); @@ -259,7 +267,7 @@ function processText(text, configHelper, filename, fix) { function processFile(filename, configHelper, options) { var text = fs.readFileSync(path.resolve(filename), "utf8"), - result = processText(text, configHelper, filename, options.fix); + result = processText(text, configHelper, filename, options.fix, options.allowInlineConfig); return result; @@ -675,7 +683,7 @@ CLIEngine.prototype = { if (filename && options.ignore && exclude(filename)) { results.push(createIgnoreResult(filename)); } else { - results.push(processText(text, configHelper, filename, options.fix)); + results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig)); } stats = calculateStatsPerRun(results); diff --git a/lib/cli.js b/lib/cli.js index 975c47f76064..220136786636 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -54,7 +54,8 @@ function translateOptions(cliOptions) { cache: cliOptions.cache, cacheFile: cliOptions.cacheFile, cacheLocation: cliOptions.cacheLocation, - fix: cliOptions.fix + fix: cliOptions.fix, + allowInlineConfig: cliOptions.inlineConfig }; } diff --git a/lib/eslint.js b/lib/eslint.js index 99ec893e7efc..031b98e42db0 100755 --- a/lib/eslint.js +++ b/lib/eslint.js @@ -608,9 +608,11 @@ module.exports = (function() { * @param {Object} config An object whose keys specify the rules to use. * @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked. * If this is not set, the filename will default to '' in the rule context. If - * an object, then it has "filename" and "saveState" properties. + * an object, then it has "filename", "saveState", and "allowInlineConfig" properties. * @param {boolean} [saveState] Indicates if the state from the last run should be saved. * Mostly useful for testing purposes. + * @param {boolean} [filenameOrOptions.allowInlineConfig] Allow/disallow inline comments' ability to change config once it is set. Defaults to true if not supplied. + * Useful if you want to validate JS without comments overriding rules. * @returns {Object[]} The results as an array of messages or null if no messages. */ api.verify = function(textOrSourceCode, config, filenameOrOptions, saveState) { @@ -619,11 +621,13 @@ module.exports = (function() { shebang, ecmaFeatures, ecmaVersion, + allowInlineConfig, text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null; // evaluate arguments if (typeof filenameOrOptions === "object") { currentFilename = filenameOrOptions.filename; + allowInlineConfig = filenameOrOptions.allowInlineConfig; saveState = filenameOrOptions.saveState; } else { currentFilename = filenameOrOptions; @@ -674,7 +678,9 @@ module.exports = (function() { if (ast) { // parse global comments and modify config - config = modifyConfigsFromComments(currentFilename, ast, config, reportingConfig, messages); + if (allowInlineConfig !== false) { + config = modifyConfigsFromComments(currentFilename, ast, config, reportingConfig, messages); + } // enable appropriate rules Object.keys(config.rules).filter(function(key) { diff --git a/lib/options.js b/lib/options.js index 9985a25a43bd..9f486b629f8d 100644 --- a/lib/options.js +++ b/lib/options.js @@ -198,6 +198,12 @@ module.exports = optionator({ alias: "v", type: "Boolean", description: "Outputs the version number" + }, + { + option: "inline-config", + type: "Boolean", + default: "true", + description: "Allow comments to change eslint config/rules" } ] }); diff --git a/tests/fixtures/disable-inline-config.js b/tests/fixtures/disable-inline-config.js new file mode 100644 index 000000000000..4731ee494c7a --- /dev/null +++ b/tests/fixtures/disable-inline-config.js @@ -0,0 +1 @@ +console.log('bar'); // eslint-disable-line diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js index ce137cb02598..84c95c608ac8 100644 --- a/tests/lib/cli-engine.js +++ b/tests/lib/cli-engine.js @@ -1854,4 +1854,60 @@ describe("CLIEngine", function() { }); }); + + describe("when evaluating code with comments to change config when allowInlineConfig is disabled", function() { + + it("should report a violation for disabling rules", function() { + var code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + var config = { + envs: ["browser"], + ignore: true, + allowInlineConfig: false, + rules: { + "eol-last": 0, + "no-alert": 1, + "no-trailing-spaces": 0, + "strict": 0, + "quotes": 0 + } + }; + + var eslintCLI = new CLIEngine(config); + + var report = eslintCLI.executeOnText(code); + var messages = report.results[0].messages; + + assert.equal(messages.length, 1); + assert.equal(messages[0].ruleId, "no-alert"); + }); + + it("should not report a violation by default", function() { + var code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + var config = { + envs: ["browser"], + ignore: true, + // allowInlineConfig: true is the default + rules: { + "eol-last": 0, + "no-alert": 1, + "no-trailing-spaces": 0, + "strict": 0, + "quotes": 0 + } + }; + + var eslintCLI = new CLIEngine(config); + + var report = eslintCLI.executeOnText(code); + var messages = report.results[0].messages; + + assert.equal(messages.length, 0); + }); + + }); + }); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index f1a9841d9d1d..cfb73069662d 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -633,6 +633,73 @@ describe("cli", function() { }); }); + describe("when passed --no-inline-config", function() { + + var sandbox = sinon.sandbox.create(), + localCLI; + + afterEach(function() { + sandbox.verifyAndRestore(); + }); + + it("should pass allowInlineConfig:true to CLIEngine when --no-inline-config is used", function() { + + // create a fake CLIEngine to test with + var fakeCLIEngine = sandbox.mock().withExactArgs(sinon.match({ allowInlineConfig: false })); + fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype); + sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ + errorCount: 1, + warningCount: 0, + results: [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ] + }] + }); + sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(function() { + return "done"; + }); + fakeCLIEngine.outputFixes = sandbox.stub(); + + localCLI = proxyquire("../../lib/cli", { + "./cli-engine": fakeCLIEngine, + "./logging": log + }); + + localCLI.execute("--no-inline-config ."); + }); + + it("should not error and allowInlineConfig should be true by default", function() { + // create a fake CLIEngine to test with + var fakeCLIEngine = sandbox.mock().withExactArgs(sinon.match({ allowInlineConfig: true })); + fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype); + sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ + errorCount: 0, + warningCount: 0, + results: [] + }); + sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(function() { + return "done"; + }); + fakeCLIEngine.outputFixes = sandbox.stub(); + + localCLI = proxyquire("../../lib/cli", { + "./cli-engine": fakeCLIEngine, + "./logging": log + }); + + var exitCode = localCLI.execute("."); + assert.equal(exitCode, 0); + + }); + + }); + // NOTE: If you are adding new tests for cli.js, duplicate the following tests describe("when passed --fix", function() { diff --git a/tests/lib/eslint.js b/tests/lib/eslint.js index b6543ead9360..b0c591f5ba75 100644 --- a/tests/lib/eslint.js +++ b/tests/lib/eslint.js @@ -2287,6 +2287,168 @@ describe("eslint", function() { }); }); + describe("when evaluating code with comments to change config when allowInlineConfig is enabled", function() { + + it("should report a violation for disabling rules", function() { + var code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + var config = { + rules: { + "no-alert": 1 + } + }; + + var messages = eslint.verify(code, config, { + filename: filename, + allowInlineConfig: false + }); + + assert.equal(messages.length, 1); + assert.equal(messages[0].ruleId, "no-alert"); + }); + + it("should report a violation for global variable declarations", + function() { + var code = [ + "/* global foo */" + ].join("\n"); + var config = { + rules: { + test: 2 + } + }; + var ok = false; + + eslint.defineRules({test: function(context) { + return { + "Program": function() { + var scope = context.getScope(); + var comments = context.getAllComments(); + assert.equal(1, comments.length); + + var foo = getVariable(scope, "foo"); + assert.notOk(foo); + + ok = true; + } + }; + }}); + + eslint.verify(code, config, {allowInlineConfig: false}); + assert(ok); + }); + + it("should report a violation for eslint-disable", function() { + var code = [ + "/* eslint-disable */", + "alert('test');" + ].join("\n"); + var config = { + rules: { + "no-alert": 1 + } + }; + + var messages = eslint.verify(code, config, { + filename: filename, + allowInlineConfig: false + }); + + assert.equal(messages.length, 1); + assert.equal(messages[0].ruleId, "no-alert"); + }); + + it("should not report a violation for rule changes", function() { + var code = [ + "/*eslint no-alert:2*/", + "alert('test');" + ].join("\n"); + var config = { + rules: { + "no-alert": 0 + } + }; + + var messages = eslint.verify(code, config, { + filename: filename, + allowInlineConfig: false + }); + + assert.equal(messages.length, 0); + }); + + it("should report a violation for disable-line", function() { + var code = [ + "alert('test'); // eslint-disable-line" + ].join("\n"); + var config = { + rules: { + "no-alert": 2 + } + }; + + var messages = eslint.verify(code, config, { + filename: filename, + allowInlineConfig: false + }); + + assert.equal(messages.length, 1); + assert.equal(messages[0].ruleId, "no-alert"); + }); + + it("should report a violation for env changes", function() { + var code = [ + "/*eslint-env browser*/" + ].join("\n"); + var config = { + rules: { + test: 2 + } + }; + var ok = false; + + eslint.defineRules({test: function(context) { + return { + "Program": function() { + var scope = context.getScope(); + var comments = context.getAllComments(); + assert.equal(1, comments.length); + + var windowVar = getVariable(scope, "window"); + assert.notOk(windowVar.eslintExplicitGlobal); + + ok = true; + } + }; + }}); + + eslint.verify(code, config, {allowInlineConfig: false}); + assert(ok); + }); + }); + + describe("when evaluating code with comments to change config when allowInlineConfig is disabled", function() { + + it("should not report a violation", function() { + var code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + var config = { + rules: { + "no-alert": 1 + } + }; + + var messages = eslint.verify(code, config, { + filename: filename, + allowInlineConfig: true + }); + + assert.equal(messages.length, 0); + }); + }); + describe("when evaluating code with code comments", function() { it("should emit enter only once for each comment", function() { diff --git a/tests/lib/options.js b/tests/lib/options.js index 7c01d7df8f0e..a93aae8f8da5 100644 --- a/tests/lib/options.js +++ b/tests/lib/options.js @@ -264,6 +264,18 @@ describe("options", function() { }); }); + describe("--inline-config", function() { + it("should return false when passed --no-inline-config", function() { + var currentOptions = options.parse("--no-inline-config"); + assert.isFalse(currentOptions.inlineConfig); + }); + + it("should return true for --inline-config when empty", function() { + var currentOptions = options.parse(""); + assert.isTrue(currentOptions.inlineConfig); + }); + }); + describe("--parser", function() { it("should return a string for --parser when passed", function() { var currentOptions = options.parse("--parser test"); From 8f31572b1abf0b9d7fed02bcd27a242d1d5c6d6c Mon Sep 17 00:00:00 2001 From: Nathan Brown Date: Wed, 11 Nov 2015 15:00:31 -0700 Subject: [PATCH 23/63] Docs: Update indent.md Corrected that the example enables case indentation, not verification. It is verified even without the SwitchCase option. --- docs/rules/indent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/indent.md b/docs/rules/indent.md index 49614e545228..e14158ca71ce 100644 --- a/docs/rules/indent.md +++ b/docs/rules/indent.md @@ -46,7 +46,7 @@ Level of indentation denotes the multiple of the indent specified. Example: * Indent of tabs with SwitchCase set to 2 will indent `SwitchCase` with 2 tabs with respect to switch. -2 space indentation with enabled switch cases validation +2 space indentation with enabled switch cases indentation ```json "indent": [2, 2, {"SwitchCase": 1}] From 700f11f2a921738a61909b472a7c8ea8d2cf8440 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 17 Nov 2015 03:32:45 +0400 Subject: [PATCH 24/63] Fix: Autofix quotes produces invalid javascript (fixes #4380) --- lib/rules/quotes.js | 49 +++++++++++++++++++++++++++------------ package.json | 2 -- tests/lib/rules/quotes.js | 17 ++++++++++++++ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/lib/rules/quotes.js b/lib/rules/quotes.js index 6148ae78009c..6f12add2fdfe 100644 --- a/lib/rules/quotes.js +++ b/lib/rules/quotes.js @@ -11,9 +11,7 @@ // Requirements //------------------------------------------------------------------------------ -var astUtils = require("../ast-utils"), - toSingleQuotes = require("to-single-quotes"), - toDoubleQuotes = require("to-double-quotes"); +var astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ // Constants @@ -23,27 +21,48 @@ var QUOTE_SETTINGS = { "double": { quote: "\"", alternateQuote: "'", - description: "doublequote", - convert: function(str) { - return toDoubleQuotes(str); - } + description: "doublequote" }, "single": { quote: "'", alternateQuote: "\"", - description: "singlequote", - convert: function(str) { - return toSingleQuotes(str); - } + description: "singlequote" }, "backtick": { quote: "`", alternateQuote: "\"", - description: "backtick", - convert: function(str) { - return str.replace(/`/g, "\`").replace(/^(?:\\*)["']|(?:\\*)["']$/g, "`"); - } + description: "backtick" + } +}; +/** + * Switches quoting of javascript string between ' " and ` + * escaping and unescaping as necessary. + * Only escaping of the minimal set of characters is changed. + * Note: escaping of newlines when switching from backtick to other quotes is not handled. + * @param {string} str - A string to convert. + * @returns {string} The string with changed quotes. + * @private + */ +QUOTE_SETTINGS.double.convert = +QUOTE_SETTINGS.single.convert = +QUOTE_SETTINGS.backtick.convert = function(str) { + var newQuote = this.quote; + var oldQuote = str[0]; + if (newQuote === oldQuote) { + return str; } + return newQuote + str.slice(1, -1).replace(/\\(\${|\r\n?|\n|.)|["'`]|\${|(\r\n?|\n)/g, function(match, escaped, newline) { + if (escaped === oldQuote || oldQuote === "`" && escaped === "${") { + return escaped; // unescape + } + if (match === newQuote || newQuote === "`" && match === "${") { + return "\\" + match; // escape + } + if (newline && oldQuote === "`") { + return "\\n"; // escape newlines + } + return match; + }) + newQuote; }; var AVOID_ESCAPE = "avoid-escape", diff --git a/package.json b/package.json index dd0587adf4c5..908568ab3095 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,6 @@ "shelljs": "^0.5.3", "strip-json-comments": "~1.0.1", "text-table": "~0.2.0", - "to-double-quotes": "^2.0.0", - "to-single-quotes": "^2.0.0", "user-home": "^2.0.0", "xml-escape": "~1.0.0" }, diff --git a/tests/lib/rules/quotes.js b/tests/lib/rules/quotes.js index f154caa91566..22fb5a47df2c 100644 --- a/tests/lib/rules/quotes.js +++ b/tests/lib/rules/quotes.js @@ -66,6 +66,11 @@ ruleTester.run("quotes", rule, { options: ["single"], errors: [{ message: "Strings must use singlequote.", type: "Literal"}] }, + { + code: "var foo = 'don\\'t';", + output: "var foo = \"don't\";", + errors: [{ message: "Strings must use doublequote.", type: "Literal"}] + }, { code: "var msg = \"Plugin '\" + name + \"' not found\"", output: "var msg = 'Plugin \\'' + name + '\\' not found'", @@ -93,12 +98,24 @@ ruleTester.run("quotes", rule, { options: ["double", "avoid-escape"], errors: [{ message: "Strings must use doublequote.", type: "Literal" }] }, + { + code: "var foo = '\\\\';", + output: "var foo = \"\\\\\";", + options: ["double", "avoid-escape"], + errors: [{ message: "Strings must use doublequote.", type: "Literal" }] + }, { code: "var foo = 'bar';", output: "var foo = `bar`;", options: ["backtick"], errors: [{ message: "Strings must use backtick.", type: "Literal" }] }, + { + code: "var foo = 'b${x}a$r';", + output: "var foo = `b\\${x}a$r`;", + options: ["backtick"], + errors: [{ message: "Strings must use backtick.", type: "Literal" }] + }, { code: "var foo = \"bar\";", output: "var foo = `bar`;", From 65c33d85268790a095210c0d6d587ed636ebd2d3 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 15 Nov 2015 08:12:12 +0900 Subject: [PATCH 25/63] Fix: Improves performance (refs #3530) --- lib/ast-utils.js | 23 +++++- lib/eslint.js | 37 ++++------ lib/rule-context.js | 90 +++++++++++------------- lib/rules/consistent-this.js | 54 +++++++------- lib/rules/indent.js | 17 ++--- lib/rules/lines-around-comment.js | 12 +++- lib/rules/new-cap.js | 12 +++- lib/rules/no-alert.js | 5 +- lib/rules/no-catch-shadow.js | 20 ++---- lib/rules/no-label-var.js | 32 +++------ lib/rules/no-shadow.js | 89 ++++++++--------------- lib/rules/no-spaced-func.js | 18 ++--- lib/rules/no-undef.js | 23 +++--- lib/rules/no-use-before-define.js | 28 ++------ lib/rules/operator-linebreak.js | 5 +- lib/rules/space-before-function-paren.js | 31 ++------ lib/rules/spaced-comment.js | 4 +- lib/testers/rule-tester.js | 53 ++++++++++++-- 18 files changed, 261 insertions(+), 292 deletions(-) diff --git a/lib/ast-utils.js b/lib/ast-utils.js index fa75d636a926..397d8da429e7 100644 --- a/lib/ast-utils.js +++ b/lib/ast-utils.js @@ -129,5 +129,26 @@ module.exports = { * @param {ASTNode} A node to get. * @returns {ASTNode|null} The trailing statement's node. */ - getTrailingStatement: esutils.ast.trailingStatement + getTrailingStatement: esutils.ast.trailingStatement, + + /** + * Finds the variable by a given name in a given scope and its upper scopes. + * + * @param {escope.Scope} initScope - A scope to start find. + * @param {string} name - A variable name to find. + * @returns {escope.Variable|null} A found variable or `null`. + */ + getVariableByName: function(initScope, name) { + var scope = initScope; + while (scope) { + var variable = scope.set.get(name); + if (variable) { + return variable; + } + + scope = scope.upper; + } + + return null; + } }; diff --git a/lib/eslint.js b/lib/eslint.js index 99ec893e7efc..78851856ae49 100755 --- a/lib/eslint.js +++ b/lib/eslint.js @@ -112,25 +112,6 @@ function parseListConfig(string) { return items; } -/** - * @param {Scope} scope The scope object to check. - * @param {string} name The name of the variable to look up. - * @returns {Variable} The variable object if found or null if not. - */ -function getVariable(scope, name) { - var variable = null; - scope.variables.some(function(v) { - if (v.name === name) { - variable = v; - return true; - } else { - return false; - } - - }); - return variable; -} - /** * Ensures that variables representing built-in properties of the Global Object, * and any globals declared by special block comments, are present in the global @@ -162,29 +143,31 @@ function addDeclaredGlobals(program, globalScope, config) { assign(explicitGlobals, config.astGlobals); Object.keys(declaredGlobals).forEach(function(name) { - var variable = getVariable(globalScope, name); + var variable = globalScope.set.get(name); if (!variable) { variable = new escope.Variable(name, globalScope); variable.eslintExplicitGlobal = false; globalScope.variables.push(variable); + globalScope.set.set(name, variable); } variable.writeable = declaredGlobals[name]; }); Object.keys(explicitGlobals).forEach(function(name) { - var variable = getVariable(globalScope, name); + var variable = globalScope.set.get(name); if (!variable) { variable = new escope.Variable(name, globalScope); variable.eslintExplicitGlobal = true; variable.eslintExplicitGlobalComment = explicitGlobals[name].comment; globalScope.variables.push(variable); + globalScope.set.set(name, variable); } variable.writeable = explicitGlobals[name].value; }); // mark all exported variables as such Object.keys(exportedGlobals).forEach(function(name) { - var variable = getVariable(globalScope, name); + var variable = globalScope.set.get(name); if (variable) { variable.eslintUsed = true; } @@ -891,8 +874,14 @@ module.exports = (function() { // copy over methods Object.keys(externalMethods).forEach(function(methodName) { - api[methodName] = function() { - return sourceCode ? sourceCode[externalMethods[methodName]].apply(sourceCode, arguments) : null; + var exMethodName = externalMethods[methodName]; + + // All functions expected to have less arguments than 5. + api[methodName] = function(a, b, c, d, e) { + if (sourceCode) { + return sourceCode[exMethodName](a, b, c, d, e); + } + return null; }; }); diff --git a/lib/rule-context.js b/lib/rule-context.js index 847f2fb78810..aeed1e2a17ab 100644 --- a/lib/rule-context.js +++ b/lib/rule-context.js @@ -72,42 +72,29 @@ var PASSTHROUGHS = [ * @param {object} ecmaFeatures The ecmaFeatures settings passed from the config file. */ function RuleContext(ruleId, eslint, severity, options, settings, ecmaFeatures) { + // public. + this.id = ruleId; + this.options = options; + this.settings = settings; + this.ecmaFeatures = ecmaFeatures; - /** - * The read-only ID of the rule. - */ - Object.defineProperty(this, "id", { - value: ruleId - }); + // private. + this.eslint = eslint; + this.severity = severity; - /** - * The read-only options of the rule - */ - Object.defineProperty(this, "options", { - value: options - }); + Object.freeze(this); +} - /** - * The read-only settings shared between all rules - */ - Object.defineProperty(this, "settings", { - value: settings - }); +RuleContext.prototype = { + constructor: RuleContext, /** - * The read-only ecmaFeatures shared across all rules + * Passthrough to eslint.getSourceCode(). + * @returns {SourceCode} The SourceCode object for the code. */ - Object.defineProperty(this, "ecmaFeatures", { - value: Object.create(ecmaFeatures) - }); - Object.freeze(this.ecmaFeatures); - - // copy over passthrough methods - PASSTHROUGHS.forEach(function(name) { - this[name] = function() { - return eslint[name].apply(eslint, arguments); - }; - }, this); + getSourceCode: function() { + return this.eslint.getSourceCode(); + }, /** * Passthrough to eslint.report() that automatically assigns the rule ID and severity. @@ -119,8 +106,7 @@ function RuleContext(ruleId, eslint, severity, options, settings, ecmaFeatures) * with symbols being replaced by this object's values. * @returns {void} */ - this.report = function(nodeOrDescriptor, location, message, opts) { - + report: function(nodeOrDescriptor, location, message, opts) { var descriptor, fix = null; @@ -133,31 +119,37 @@ function RuleContext(ruleId, eslint, severity, options, settings, ecmaFeatures) fix = descriptor.fix(new RuleFixer()); } - eslint.report( - ruleId, severity, descriptor.node, + this.eslint.report( + this.id, + this.severity, + descriptor.node, descriptor.loc || descriptor.node.loc.start, - descriptor.message, descriptor.data, fix + descriptor.message, + descriptor.data, + fix ); return; } // old style call - eslint.report(ruleId, severity, nodeOrDescriptor, location, message, opts); - }; + this.eslint.report( + this.id, + this.severity, + nodeOrDescriptor, + location, + message, + opts + ); + } +}; - /** - * Passthrough to eslint.getSourceCode(). - * @returns {SourceCode} The SourceCode object for the code. - */ - this.getSourceCode = function() { - return eslint.getSourceCode(); +// copy over passthrough methods +PASSTHROUGHS.forEach(function(name) { + // All functions expected to have less arguments than 5. + this[name] = function(a, b, c, d, e) { + return this.eslint[name](a, b, c, d, e); }; - -} - -RuleContext.prototype = { - constructor: RuleContext -}; +}, RuleContext.prototype); module.exports = RuleContext; diff --git a/lib/rules/consistent-this.js b/lib/rules/consistent-this.js index 00c70fb41172..81dc8473e625 100644 --- a/lib/rules/consistent-this.js +++ b/lib/rules/consistent-this.js @@ -53,39 +53,35 @@ module.exports = function(context) { */ function ensureWasAssigned() { var scope = context.getScope(); + var variable = scope.set.get(alias); + if (!variable) { + return; + } + + if (variable.defs.some(function(def) { + return def.node.type === "VariableDeclarator" && + def.node.init !== null; + })) { + return; + } - scope.variables.some(function(variable) { - var lookup; - - if (variable.name === alias) { - if (variable.defs.some(function(def) { - return def.node.type === "VariableDeclarator" && - def.node.init !== null; - })) { - return true; - } - - lookup = scope.type === "global" ? scope : variable; - - // The alias has been declared and not assigned: check it was - // assigned later in the same scope. - if (!lookup.references.some(function(reference) { - var write = reference.writeExpr; - - if (reference.from === scope && - write && write.type === "ThisExpression" && - write.parent.operator === "=") { - return true; - } - })) { - variable.defs.map(function(def) { - return def.node; - }).forEach(reportBadAssignment); - } + var lookup = (variable.references.length === 0 && scope.type === "global") ? scope : variable; + // The alias has been declared and not assigned: check it was + // assigned later in the same scope. + if (!lookup.references.some(function(reference) { + var write = reference.writeExpr; + + if (reference.from === scope && + write && write.type === "ThisExpression" && + write.parent.operator === "=") { return true; } - }); + })) { + variable.defs.map(function(def) { + return def.node; + }).forEach(reportBadAssignment); + } } return { diff --git a/lib/rules/indent.js b/lib/rules/indent.js index c8d8b92d5dae..b69539b227a6 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -75,6 +75,11 @@ module.exports = function(context) { } } + var indentPattern = { + normal: indentType === "space" ? /^ +/ : /^\t+/, + excludeCommas: indentType === "space" ? /^[ ,]+/ : /^[\t,]+/ + }; + var caseIndentStore = {}; /** @@ -168,17 +173,9 @@ module.exports = function(context) { function getNodeIndent(node, byLastLine, excludeCommas) { var token = byLastLine ? context.getLastToken(node) : context.getFirstToken(node); var src = context.getSource(token, token.loc.start.column); - - var skip = excludeCommas ? "," : ""; - - var regExp; - if (indentType === "space") { - regExp = new RegExp("^[ " + skip + "]+"); - } else { - regExp = new RegExp("^[\t" + skip + "]+"); - } - + var regExp = excludeCommas ? indentPattern.excludeCommas : indentPattern.normal; var indent = regExp.exec(src); + return indent ? indent[0].length : 0; } diff --git a/lib/rules/lines-around-comment.js b/lib/rules/lines-around-comment.js index 096fe7596903..b73b009b8a33 100644 --- a/lib/rules/lines-around-comment.js +++ b/lib/rules/lines-around-comment.js @@ -7,6 +7,16 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var assign = require("object-assign"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + /** * Return an array with with any line numbers that are empty. * @param {Array} lines An array of each line of the file. @@ -57,7 +67,7 @@ function contains(val, array) { module.exports = function(context) { - var options = context.options[0] || {}; + var options = context.options[0] ? assign({}, context.options[0]) : {}; options.beforeLineComment = options.beforeLineComment || false; options.afterLineComment = options.afterLineComment || false; options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true; diff --git a/lib/rules/new-cap.js b/lib/rules/new-cap.js index d95f27a73afb..9518b1eff0c0 100644 --- a/lib/rules/new-cap.js +++ b/lib/rules/new-cap.js @@ -7,6 +7,16 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var assign = require("object-assign"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + var CAPS_ALLOWED = [ "Array", "Boolean", @@ -67,7 +77,7 @@ function calculateCapIsNewExceptions(config) { module.exports = function(context) { - var config = context.options[0] || {}; + var config = context.options[0] ? assign({}, context.options[0]) : {}; config.newIsCap = config.newIsCap !== false; config.capIsNew = config.capIsNew !== false; var skipProperties = config.properties === false; diff --git a/lib/rules/no-alert.js b/lib/rules/no-alert.js index 4e0232dae8a4..bfe834b44a9a 100644 --- a/lib/rules/no-alert.js +++ b/lib/rules/no-alert.js @@ -69,9 +69,8 @@ function findReference(scope, node) { * @returns {boolean} Whether or not the name is shadowed globally. */ function isGloballyShadowed(globalScope, identifierName) { - return globalScope.variables.some(function(variable) { - return variable.name === identifierName && variable.defs.length > 0; - }); + var variable = globalScope.set.get(identifierName); + return Boolean(variable && variable.defs.length > 0); } /** diff --git a/lib/rules/no-catch-shadow.js b/lib/rules/no-catch-shadow.js index 60c631345062..88eeb02fa618 100644 --- a/lib/rules/no-catch-shadow.js +++ b/lib/rules/no-catch-shadow.js @@ -5,6 +5,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var astUtils = require("../ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -22,19 +28,7 @@ module.exports = function(context) { * @returns {boolean} True is its been shadowed */ function paramIsShadowing(scope, name) { - var found = scope.variables.some(function(variable) { - return variable.name === name; - }); - - if (found) { - return true; - } - - if (scope.upper) { - return paramIsShadowing(scope.upper, name); - } - - return false; + return astUtils.getVariableByName(scope, name) !== null; } //-------------------------------------------------------------------------- diff --git a/lib/rules/no-label-var.js b/lib/rules/no-label-var.js index 8e825e248226..20fbfc182df4 100644 --- a/lib/rules/no-label-var.js +++ b/lib/rules/no-label-var.js @@ -5,6 +5,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var astUtils = require("../ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -18,32 +24,12 @@ module.exports = function(context) { /** * Check if the identifier is present inside current scope * @param {object} scope current scope - * @param {ASTNode} identifier To evaluate + * @param {string} name To evaluate * @returns {boolean} True if its present * @private */ - function findIdentifier(scope, identifier) { - var found = false; - - scope.variables.forEach(function(variable) { - if (variable.name === identifier) { - found = true; - } - }); - - scope.references.forEach(function(reference) { - if (reference.identifier.name === identifier) { - found = true; - } - }); - - // If we have not found the identifier in this scope, check the parent - // scope. - if (scope.upper && !found) { - return findIdentifier(scope.upper, identifier); - } - - return found; + function findIdentifier(scope, name) { + return astUtils.getVariableByName(scope, name) !== null; } //-------------------------------------------------------------------------- diff --git a/lib/rules/no-shadow.js b/lib/rules/no-shadow.js index 780ea247fd58..35c5a04f2e6e 100644 --- a/lib/rules/no-shadow.js +++ b/lib/rules/no-shadow.js @@ -6,6 +6,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var astUtils = require("../ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -99,70 +105,37 @@ module.exports = function(context) { ); } - /** - * Checks if a variable is contained in the list of given scope variables. - * @param {Object} variable The variable to check. - * @param {Array} scopeVars The scope variables to look for. - * @returns {boolean} Whether or not the variable is contains in the list of scope variables. - */ - function isContainedInScopeVars(variable, scopeVars) { - return scopeVars.some(function(scopeVar) { - return ( - (scopeVar.identifiers.length > 0 || (options.builtinGlobals && "writeable" in scopeVar)) && - variable.name === scopeVar.name && - !isDuplicatedClassNameVariable(scopeVar) && - !isOnInitializer(variable, scopeVar) && - !(options.hoist !== "all" && isInTdz(variable, scopeVar)) - ); - }); - } - - /** - * Checks if the given variables are shadowed in the given scope. - * @param {Array} variables The variables to look for - * @param {Object} scope The scope to be checked. - * @returns {Array} Variables which are not declared in the given scope. - */ - function checkShadowsInScope(variables, scope) { - - var passedVars = []; - - variables.forEach(function(variable) { - // "arguments" is a special case that has no identifiers (#1759) - if (variable.identifiers.length > 0 && isContainedInScopeVars(variable, scope.variables)) { - context.report( - variable.identifiers[0], - "\"{{name}}\" is already declared in the upper scope.", - {name: variable.name}); - } else { - passedVars.push(variable); - } - }); - - return passedVars; - } - /** * Checks the current context for shadowed variables. * @param {Scope} scope - Fixme * @returns {void} */ function checkForShadows(scope) { - var variables = scope.variables.filter(function(variable) { - return ( - // Skip "arguments". - variable.identifiers.length > 0 && - // Skip variables of a class name in the class scope of ClassDeclaration. - !isDuplicatedClassNameVariable(variable) && - !isAllowed(variable) - ); - }); - - // iterate through the array of variables and find duplicates with the upper scope - var upper = scope.upper; - while (upper && variables.length) { - variables = checkShadowsInScope(variables, upper); - upper = upper.upper; + var variables = scope.variables; + for (var i = 0; i < variables.length; ++i) { + var variable = variables[i]; + + // Skips "arguments" or variables of a class name in the class scope of ClassDeclaration. + if (variable.identifiers.length === 0 || + isDuplicatedClassNameVariable(variable) || + isAllowed(variable) + ) { + continue; + } + + // Gets shadowed variable. + var shadowed = astUtils.getVariableByName(scope.upper, variable.name); + if (shadowed && + (shadowed.identifiers.length > 0 || (options.builtinGlobals && "writeable" in shadowed)) && + !isOnInitializer(variable, shadowed) && + !(options.hoist !== "all" && isInTdz(variable, shadowed)) + ) { + context.report({ + node: variable.identifiers[0], + message: "\"{{name}}\" is already declared in the upper scope.", + data: variable + }); + } } } diff --git a/lib/rules/no-spaced-func.js b/lib/rules/no-spaced-func.js index ecd3bd97caed..4513d80472d2 100644 --- a/lib/rules/no-spaced-func.js +++ b/lib/rules/no-spaced-func.js @@ -21,26 +21,26 @@ module.exports = function(context) { */ function detectOpenSpaces(node) { var lastCalleeToken = sourceCode.getLastToken(node.callee), - tokens = sourceCode.getTokens(node), - i = tokens.indexOf(lastCalleeToken), - l = tokens.length; + prevToken = lastCalleeToken, + parenToken = sourceCode.getTokenAfter(lastCalleeToken); - while (i < l && tokens[i].value !== "(") { - ++i; + if (sourceCode.getLastToken(node).value !== ")") { + return; } - if (i >= l) { - return; + while (parenToken.value !== "(") { + prevToken = parenToken; + parenToken = sourceCode.getTokenAfter(parenToken); } // look for a space between the callee and the open paren - if (sourceCode.isSpaceBetweenTokens(tokens[i - 1], tokens[i])) { + if (sourceCode.isSpaceBetweenTokens(prevToken, parenToken)) { context.report({ node: node, loc: lastCalleeToken.loc.start, message: "Unexpected space between function name and paren.", fix: function(fixer) { - return fixer.removeRange([tokens[i - 1].range[1], tokens[i].range[0]]); + return fixer.removeRange([prevToken.range[1], parenToken.range[0]]); } }); } diff --git a/lib/rules/no-undef.js b/lib/rules/no-undef.js index 745daafcfa02..988d677cd72e 100644 --- a/lib/rules/no-undef.js +++ b/lib/rules/no-undef.js @@ -10,12 +10,14 @@ // Requirements //------------------------------------------------------------------------------ -// none! +var astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ +var hasOwnProperty = Object.prototype.hasOwnProperty; + /** * Check if a variable is an implicit declaration * @param {ASTNode} variable node to evaluate @@ -35,18 +37,13 @@ function isImplicitGlobal(variable) { * @returns {Variable} The variable, or null if ref refers to an undeclared variable. */ function getDeclaredGlobalVariable(scope, ref) { - var declaredGlobal = null; - scope.variables.some(function(variable) { - if (variable.name === ref.identifier.name) { - // If it's an implicit global, it must have a `writeable` field (indicating it was declared) - if (!isImplicitGlobal(variable) || {}.hasOwnProperty.call(variable, "writeable")) { - declaredGlobal = variable; - return true; - } - } - return false; - }); - return declaredGlobal; + var variable = astUtils.getVariableByName(scope, ref.identifier.name); + + // If it's an implicit global, it must have a `writeable` field (indicating it was declared) + if (variable && (!isImplicitGlobal(variable) || hasOwnProperty.call(variable, "writeable"))) { + return variable; + } + return null; } /** diff --git a/lib/rules/no-use-before-define.js b/lib/rules/no-use-before-define.js index ad38ac1b98bc..9518d9e50b1f 100644 --- a/lib/rules/no-use-before-define.js +++ b/lib/rules/no-use-before-define.js @@ -6,6 +6,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var astUtils = require("../ast-utils"); + //------------------------------------------------------------------------------ // Constants //------------------------------------------------------------------------------ @@ -18,26 +24,6 @@ var NO_FUNC = "nofunc"; module.exports = function(context) { - /** - * Finds variable declarations in a given scope. - * @param {string} name The variable name to find. - * @param {Scope} scope The scope to search in. - * @returns {Object} The variable declaration object. - * @private - */ - function findDeclaration(name, scope) { - // try searching in the current scope first - for (var i = 0, l = scope.variables.length; i < l; i++) { - if (scope.variables[i].name === name) { - return scope.variables[i]; - } - } - // check if there's upper scope and call recursivly till we find the variable - if (scope.upper) { - return findDeclaration(name, scope.upper); - } - } - /** * Finds and validates all variables in a given scope. * @param {Scope} scope The scope object. @@ -68,7 +54,7 @@ module.exports = function(context) { if (reference.resolved && reference.resolved.identifiers.length > 0) { checkLocationAndReport(reference, reference.resolved); } else { - var declaration = findDeclaration(reference.identifier.name, scope); + var declaration = astUtils.getVariableByName(scope, reference.identifier.name); // if there're no identifiers, this is a global environment variable if (declaration && declaration.identifiers.length !== 0) { checkLocationAndReport(reference, declaration); diff --git a/lib/rules/operator-linebreak.js b/lib/rules/operator-linebreak.js index a45b3ca1661a..d23032ea4a70 100644 --- a/lib/rules/operator-linebreak.js +++ b/lib/rules/operator-linebreak.js @@ -6,7 +6,8 @@ "use strict"; -var astUtils = require("../ast-utils"); +var assign = require("object-assign"), + astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ // Rule Definition @@ -17,7 +18,7 @@ module.exports = function(context) { var usedDefaultGlobal = !context.options[0]; var globalStyle = context.options[0] || "after"; var options = context.options[1] || {}; - var styleOverrides = options.overrides || {}; + var styleOverrides = options.overrides ? assign({}, options.overrides) : {}; if (usedDefaultGlobal && !styleOverrides["?"]) { styleOverrides["?"] = "before"; diff --git a/lib/rules/space-before-function-paren.js b/lib/rules/space-before-function-paren.js index 4608a3143541..b96acb6678c4 100644 --- a/lib/rules/space-before-function-paren.js +++ b/lib/rules/space-before-function-paren.js @@ -37,7 +37,7 @@ module.exports = function(context) { return true; } - parent = context.getAncestors().pop(); + parent = node.parent; return parent.type === "MethodDefinition" || (parent.type === "Property" && ( @@ -55,7 +55,6 @@ module.exports = function(context) { */ function validateSpacingBeforeParentheses(node) { var isNamed = isNamedFunction(node), - tokens, leftToken, rightToken, location; @@ -64,31 +63,11 @@ module.exports = function(context) { return; } - tokens = context.getTokens(node); - - if (node.generator) { - if (node.id) { - leftToken = tokens[2]; - rightToken = tokens[3]; - } else { - // Object methods are named but don't have an id - leftToken = context.getTokenBefore(node); - rightToken = tokens[0]; - } - } else if (isNamed) { - if (node.id) { - leftToken = tokens[1]; - rightToken = tokens[2]; - } else { - // Object methods are named but don't have an id - leftToken = context.getTokenBefore(node); - rightToken = tokens[0]; - } - } else { - leftToken = tokens[0]; - rightToken = tokens[1]; + rightToken = sourceCode.getFirstToken(node); + while (rightToken.value !== "(") { + rightToken = sourceCode.getTokenAfter(rightToken); } - + leftToken = context.getTokenBefore(rightToken); location = leftToken.loc.end; if (sourceCode.isSpaceBetweenTokens(leftToken, rightToken)) { diff --git a/lib/rules/spaced-comment.js b/lib/rules/spaced-comment.js index b69abb6bd9ae..585dc7aec1b0 100644 --- a/lib/rules/spaced-comment.js +++ b/lib/rules/spaced-comment.js @@ -41,9 +41,7 @@ function escapeAndRepeat(s) { * @returns {string[]} A marker list. */ function parseMarkersOption(markers) { - if (!markers) { - markers = []; - } + markers = markers ? markers.slice(0) : []; // `*` is a marker for JSDoc comments. if (markers.indexOf("*") === -1) { diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js index bcd40d430404..8966327b3c5e 100644 --- a/lib/testers/rule-tester.js +++ b/lib/testers/rule-tester.js @@ -56,6 +56,7 @@ var assert = require("assert"), validator = require("../config-validator"), validate = require("is-my-json-valid"), eslint = require("../eslint"), + rules = require("../rules"), metaSchema = require("../../conf/json-schema-schema.json"), SourceCodeFixer = require("../util/source-code-fixer"); @@ -106,6 +107,27 @@ function cloneDeeplyExcludesParent(x) { return x; } +/** + * Freezes a given value deeply. + * + * @param {any} x - A value to freeze. + * @returns {void} + */ +function freezeDeeply(x) { + if (typeof x === "object" && x !== null) { + if (Array.isArray(x)) { + x.forEach(freezeDeeply); + } else { + for (var key in x) { + if (key !== "parent" && hasOwnProperty(x, key)) { + freezeDeeply(x[key]); + } + } + } + Object.freeze(x); + } +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -251,7 +273,8 @@ RuleTester.prototype = { validator.validate(config, "rule-tester"); - // To cache AST. + // Setup AST getters. + // To check whether or not AST was not modified in verify. eslint.reset(); eslint.on("Program", function(node) { beforeAST = cloneDeeplyExcludesParent(node); @@ -261,11 +284,29 @@ RuleTester.prototype = { }); }); - return { - messages: eslint.verify(code, config, filename, true), - beforeAST: beforeAST, - afterAST: afterAST - }; + // Freezes rule-context properties. + var originalGet = rules.get; + try { + rules.get = function(ruleId) { + var rule = originalGet(ruleId); + return function(context) { + Object.freeze(context); + freezeDeeply(context.options); + freezeDeeply(context.settings); + freezeDeeply(context.ecmaFeatures); + + return rule(context); + }; + }; + + return { + messages: eslint.verify(code, config, filename, true), + beforeAST: beforeAST, + afterAST: afterAST + }; + } finally { + rules.get = originalGet; + } } /** From 502f2aa32a8ebdb89bfe86b03adc5ac7df310116 Mon Sep 17 00:00:00 2001 From: alberto Date: Tue, 17 Nov 2015 14:40:21 +0100 Subject: [PATCH 26/63] Fix: space-before-keywords false positive (fixes #4449) --- lib/rules/space-before-keywords.js | 5 ++++- tests/lib/rules/space-before-keywords.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/rules/space-before-keywords.js b/lib/rules/space-before-keywords.js index 3fb23d341f22..98700a12d612 100644 --- a/lib/rules/space-before-keywords.js +++ b/lib/rules/space-before-keywords.js @@ -134,7 +134,10 @@ module.exports = function(context) { check(node); // else if (node.alternate) { - check(context.getTokenBefore(node.alternate), { requireSpace: SPACE_REQUIRED }); + var tokens = context.getTokensBefore(node.alternate, 2); + if (tokens[0].value === "}") { + check(tokens[1], { requireSpace: SPACE_REQUIRED }); + } } }, "ForStatement": check, diff --git a/tests/lib/rules/space-before-keywords.js b/tests/lib/rules/space-before-keywords.js index c45d94cee2fc..c033ffba3cfe 100644 --- a/tests/lib/rules/space-before-keywords.js +++ b/tests/lib/rules/space-before-keywords.js @@ -50,6 +50,7 @@ ruleTester.run("space-before-keywords", rule, { { code: "; if ('') {}", options: never }, { code: ";\nif ('') {}", options: never }, { code: "if ('') {}else {}", options: never }, + { code: "if(true) foo();\nelse bar();", options: never }, // ForStatement { code: "; for (;;) {}" }, { code: ";\nfor (;;) {}" }, From 5de58666e74d812691716786430e37093d95f92d Mon Sep 17 00:00:00 2001 From: Ilya Panasenko Date: Tue, 17 Nov 2015 18:54:25 +0200 Subject: [PATCH 27/63] Update: replace label and break with IIFE and return (fixes #4459) --- lib/file-finder.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/file-finder.js b/lib/file-finder.js index af35cb5fd557..4f64a3f7a0a1 100644 --- a/lib/file-finder.js +++ b/lib/file-finder.js @@ -79,8 +79,7 @@ FileFinder.prototype.findInDirectoryOrParents = function(directory) { name = this.fileNames[0]; names = Array.isArray(name) ? name : [name]; - /* eslint-disable no-labels */ - traversal: + (function() { while (directory !== child) { dirs[searched++] = directory; @@ -88,7 +87,7 @@ FileFinder.prototype.findInDirectoryOrParents = function(directory) { if (getDirectoryEntries(directory).indexOf(names[k]) !== -1 && fs.statSync(path.resolve(directory, names[k])).isFile()) { filePath = path.resolve(directory, names[k]); - break traversal; + return; } } @@ -97,7 +96,7 @@ FileFinder.prototype.findInDirectoryOrParents = function(directory) { // Assign parent directory to directory. directory = path.dirname(directory); } - /* eslint-enable no-labels */ + }()); for (i = 0; i < searched; i++) { cache[dirs[i]] = filePath; From 7446e4722e52847c874dd3c9086470c625f924fe Mon Sep 17 00:00:00 2001 From: Jesse McCarthy Date: Mon, 16 Nov 2015 12:07:52 -0500 Subject: [PATCH 28/63] Update: Accept array for `ignorePattern` (fixes #3982) * Add support, tests, documentation for accepting array for `ignorePattern` and `--ignore-pattern`. * Add verifyCLIEngineOpts() helper to cli test file. Move and update note about adding new tests. * Add note to documentation about specifying array options in general. --- docs/developer-guide/nodejs-api.md | 1 + docs/user-guide/command-line-interface.md | 10 ++++- lib/ignored-paths.js | 2 +- lib/options.js | 2 +- tests/lib/cli.js | 48 ++++++++++++++++++++++- tests/lib/ignored-paths.js | 12 ++++++ 6 files changed, 70 insertions(+), 5 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 58f33deed86e..c72b62a6886e 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -125,6 +125,7 @@ The `CLIEngine` is a constructor, and you can create a new instance by passing i * `fix` - True indicates that fixes should be applied to the text when possible. * `ignore` - False disables use of `.eslintignore` (default: true). Corresponds to `--no-ignore`. * `ignorePath` - The ignore file to use instead of `.eslintignore` (default: null). Corresponds to `--ignore-path`. +* `ignorePattern` - Glob patterns for paths to ignore. String or array of strings. * `baseConfig` - Set to false to disable use of base config. Could be set to an object to override default base config as well. * `rulePaths` - An array of directories to load custom rules from (default: empty array). Corresponds to `--rulesdir`. * `rules` - An object of rules to use (default: null). Corresponds to `--rule`. diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md index 377ecbe3264f..ad0c7a52c363 100644 --- a/docs/user-guide/command-line-interface.md +++ b/docs/user-guide/command-line-interface.md @@ -46,7 +46,7 @@ Specifying rules and plugins: Ignoring files: --ignore-path path::String Specify path of ignore file --no-ignore Disable use of .eslintignore - --ignore-pattern String Pattern of files to ignore (in addition to those + --ignore-pattern [String] Patterns of files to ignore (in addition to those in .eslintignore) Using stdin: @@ -73,6 +73,14 @@ Miscellaneous: default: false ``` +Options that accept array values can be specified by repeating the option or with a comma-delimited list. + +Example: + + eslint --ignore-pattern a.js --ignore-pattern b.js file.js + + eslint --ignore-pattern a.js,b.js file.js + ### Basic configuration #### `-c`, `--config` diff --git a/lib/ignored-paths.js b/lib/ignored-paths.js index 3f71d8721192..cbf67488c5ef 100644 --- a/lib/ignored-paths.js +++ b/lib/ignored-paths.js @@ -103,7 +103,7 @@ IgnoredPaths.load = function(options) { } if (options.ignorePattern) { - patterns.push(options.ignorePattern); + patterns = patterns.concat(options.ignorePattern); } return new IgnoredPaths(patterns); diff --git a/lib/options.js b/lib/options.js index 9f486b629f8d..3fee3854b0c4 100644 --- a/lib/options.js +++ b/lib/options.js @@ -112,7 +112,7 @@ module.exports = optionator({ }, { option: "ignore-pattern", - type: "String", + type: "[String]", description: "Pattern of files to ignore (in addition to those in .eslintignore)" }, { diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 43cf3b41132d..53cf78033806 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -5,6 +5,9 @@ "use strict"; +// NOTE: If you are adding new tests for cli.js, use verifyCLIEngineOpts(). The +// test only needs to verify that CLIEngine receives the correct opts. + //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ @@ -36,6 +39,34 @@ describe("cli", function() { "./logging": log }); + /** + * Verify that CLIEngine receives correct opts via cli.execute(). + * @param {string} cmd CLI command. + * @param {object} opts Options hash that should match that received by CLIEngine. + * @returns {void} + */ + function verifyCLIEngineOpts(cmd, opts) { + var sandbox = sinon.sandbox.create(), + localCLI, + fakeCLIEngine; + + // create a fake CLIEngine to test with + fakeCLIEngine = sandbox.mock().withExactArgs(sinon.match(opts)); + + fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype); + sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({}); + sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(sinon.spy()); + + localCLI = proxyquire("../../lib/cli", { + "./cli-engine": fakeCLIEngine, + "./logging": log + }); + + localCLI.execute(cmd); + sandbox.verifyAndRestore(); + } + // verifyCLIEngineOpts + /** * Returns the path inside of the fixture directory. * @returns {string} The path inside the fixture directory. @@ -363,6 +394,21 @@ describe("cli", function() { }); }); + describe("when given patterns to ignore", function() { + it("should not process any matching files", function() { + var ignorePaths = ["a", "b"]; + + var cmd = ignorePaths.map(function(ignorePath) { + return "--ignore-pattern " + ignorePath; + }).concat(".").join(" "); + + var opts = { + ignorePattern: ignorePaths + }; + + verifyCLIEngineOpts(cmd, opts); + }); + }); describe("when executing a file with a shebang", function() { @@ -706,8 +752,6 @@ describe("cli", function() { }); - // NOTE: If you are adding new tests for cli.js, duplicate the following tests - describe("when passed --fix", function() { var sandbox = sinon.sandbox.create(), diff --git a/tests/lib/ignored-paths.js b/tests/lib/ignored-paths.js index 8f0962a6bac6..30c9f36af1e2 100644 --- a/tests/lib/ignored-paths.js +++ b/tests/lib/ignored-paths.js @@ -45,6 +45,18 @@ describe("IgnoredPaths", function() { assert.lengthOf(ignoredPaths.patterns, 0); }); + it("should accept an array for options.ignorePattern", function() { + var ignorePattern = ["a", "b"]; + + var ignoredPaths = IgnoredPaths.load({ + ignore: false, + ignorePattern: ignorePattern + }); + + assert.ok(ignorePattern.every(function(pattern) { + return ignoredPaths.patterns.indexOf(pattern) >= 0; + })); + }); }); describe("initialization with specific file", function() { From 846e8934ef140d25f11e67d1e0381e2c8a387611 Mon Sep 17 00:00:00 2001 From: alberto Date: Tue, 17 Nov 2015 19:53:17 +0100 Subject: [PATCH 29/63] Fix: Handle comments in block-spacing (fixes #4387) --- lib/rules/block-spacing.js | 9 +++++++-- lib/rules/vars-on-top.js | 4 ++-- lib/util/source-code.js | 4 ++++ tests/lib/rules/block-spacing.js | 32 +++++++++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/rules/block-spacing.js b/lib/rules/block-spacing.js index a65ff8743a70..d7bbbf11a2b7 100644 --- a/lib/rules/block-spacing.js +++ b/lib/rules/block-spacing.js @@ -59,8 +59,8 @@ module.exports = function(context) { // Gets braces and the first/last token of content. var openBrace = getOpenBrace(node); var closeBrace = context.getLastToken(node); - var firstToken = context.getTokenAfter(openBrace); - var lastToken = context.getTokenBefore(closeBrace); + var firstToken = sourceCode.getTokenOrCommentAfter(openBrace); + var lastToken = sourceCode.getTokenOrCommentBefore(closeBrace); // Skip if the node is invalid or empty. if (openBrace.type !== "Punctuator" || @@ -72,6 +72,11 @@ module.exports = function(context) { return; } + // Skip line comments for option never + if (!always && firstToken.type === "Line") { + return; + } + // Check. if (!isValid(openBrace, firstToken)) { context.report({ diff --git a/lib/rules/vars-on-top.js b/lib/rules/vars-on-top.js index 1d390db338b9..84af6477a0ef 100644 --- a/lib/rules/vars-on-top.js +++ b/lib/rules/vars-on-top.js @@ -100,8 +100,8 @@ module.exports = function(context) { var parent = ancestors.pop(); var grandParent = ancestors.pop(); - if (node.kind === "var") {// check variable is `var` type and not `let` or `const` - if (parent.type === "Program") {// That means its a global variable + if (node.kind === "var") { // check variable is `var` type and not `let` or `const` + if (parent.type === "Program") { // That means its a global variable globalVarCheck(node, parent); } else { blockScopeVarCheck(node, parent, grandParent); diff --git a/lib/util/source-code.js b/lib/util/source-code.js index 09ff1f84eb9a..6ef0f9199556 100644 --- a/lib/util/source-code.js +++ b/lib/util/source-code.js @@ -125,6 +125,10 @@ function SourceCode(text, ast) { this[methodName] = tokenStore[methodName]; }, this); + var tokensAndCommentsStore = createTokenStore(this.tokensAndComments); + this.getTokenOrCommentBefore = tokensAndCommentsStore.getTokenBefore; + this.getTokenOrCommentAfter = tokensAndCommentsStore.getTokenAfter; + // don't allow modification of this object Object.freeze(this); Object.freeze(this.lines); diff --git a/tests/lib/rules/block-spacing.js b/tests/lib/rules/block-spacing.js index e26d2d49214e..f26f5ff53e8f 100644 --- a/tests/lib/rules/block-spacing.js +++ b/tests/lib/rules/block-spacing.js @@ -39,6 +39,8 @@ ruleTester.run("block-spacing", rule, { {code: "function foo() { bar(); }"}, {code: "(function() { bar(); });"}, {code: "(() => { bar(); });", ecmaFeatures: {arrowFunctions: true}}, + {code: "if (a) { /* comment */ foo(); /* comment */ }"}, + {code: "if (a) { //comment\n foo(); }"}, // never {code: "{foo();}", options: ["never"]}, @@ -57,7 +59,9 @@ ruleTester.run("block-spacing", rule, { {code: "try {foo();} catch (e) {foo();}", options: ["never"]}, {code: "function foo() {bar();}", options: ["never"]}, {code: "(function() {bar();});", options: ["never"]}, - {code: "(() => {bar();});", ecmaFeatures: {arrowFunctions: true}, options: ["never"]} + {code: "(() => {bar();});", ecmaFeatures: {arrowFunctions: true}, options: ["never"]}, + {code: "if (a) {/* comment */ foo(); /* comment */}", options: ["never"]}, + {code: "if (a) { //comment\n foo();}", options: ["never"]} ], invalid: [ // default/always @@ -208,6 +212,23 @@ ruleTester.run("block-spacing", rule, { {type: "BlockStatement", line: 1, column: 15, message: "Requires a space before \"}\"."} ] }, + { + code: "if (a) {/* comment */ foo(); /* comment */}", + output: "if (a) { /* comment */ foo(); /* comment */ }", + ecmaFeatures: {arrowFunctions: true}, + errors: [ + {type: "BlockStatement", line: 1, column: 8, message: "Requires a space after \"{\"."}, + {type: "BlockStatement", line: 1, column: 43, message: "Requires a space before \"}\"."} + ] + }, + { + code: "if (a) {//comment\n foo(); }", + output: "if (a) { //comment\n foo(); }", + ecmaFeatures: {arrowFunctions: true}, + errors: [ + {type: "BlockStatement", line: 1, column: 8, message: "Requires a space after \"{\"."} + ] + }, //---------------------------------------------------------------------- // never @@ -365,6 +386,15 @@ ruleTester.run("block-spacing", rule, { {type: "BlockStatement", line: 1, column: 8, message: "Unexpected space(s) after \"{\"."}, {type: "BlockStatement", line: 1, column: 17, message: "Unexpected space(s) before \"}\"."} ] + }, + { + code: "if (a) { /* comment */ foo(); /* comment */ }", + output: "if (a) {/* comment */ foo(); /* comment */}", + options: ["never"], + errors: [ + {type: "BlockStatement", line: 1, column: 8, message: "Unexpected space(s) after \"{\"."}, + {type: "BlockStatement", line: 1, column: 45, message: "Unexpected space(s) before \"}\"."} + ] } ] }); From fd5d4e35b69d2067eabbabe0247d66a499052ee9 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Tue, 17 Nov 2015 21:56:10 -0500 Subject: [PATCH 30/63] Docs: Fix typo in default `cacheLocation` value --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index c72b62a6886e..fb4f5101af3e 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -133,7 +133,7 @@ The `CLIEngine` is a constructor, and you can create a new instance by passing i * `parser` - Specify the parser to be used (default: `espree`). Corresponds to `--parser`. * `cache` - Operate only on changed files (default: `false`). Corresponds to `--cache`. * `cacheFile` - Name of the file where the cache will be stored (default: `.eslintcache`). Corresponds to `--cache-file`. Deprecated: use `cacheLocation` instead. -* `cacheLocation` - Name of the file or directory where the cache will be stored (default: `.estlintcache`). Correspond to `--cache-location` +* `cacheLocation` - Name of the file or directory where the cache will be stored (default: `.eslintcache`). Correspond to `--cache-location` For example: From 2017aa5725af0a4567dd48a6ddf00a96d1ec1623 Mon Sep 17 00:00:00 2001 From: nightwing Date: Tue, 17 Nov 2015 15:40:18 +0000 Subject: [PATCH 31/63] Update: Display errors at the place where fix should go (fixes #4470) --- lib/rules/no-sequences.js | 3 ++- lib/rules/space-after-keywords.js | 2 ++ tests/lib/rules/no-sequences.js | 32 ++++++++++++++++--------- tests/lib/rules/space-after-keywords.js | 6 ++--- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/rules/no-sequences.js b/lib/rules/no-sequences.js index c0a458e05377..538e36a12fc2 100644 --- a/lib/rules/no-sequences.js +++ b/lib/rules/no-sequences.js @@ -85,7 +85,8 @@ module.exports = function(context) { } } - context.report(node, "Unexpected use of comma operator."); + var child = context.getTokenAfter(node.expressions[0]); + context.report(node, child.loc.start, "Unexpected use of comma operator."); } }; diff --git a/lib/rules/space-after-keywords.js b/lib/rules/space-after-keywords.js index c00244a24be3..eeed6d2a60a6 100644 --- a/lib/rules/space-after-keywords.js +++ b/lib/rules/space-after-keywords.js @@ -33,6 +33,7 @@ module.exports = function(context) { if (hasSpace !== requiresSpace) { context.report({ node: node, + loc: left.loc.end, message: "Keyword \"{{value}}\" must {{not}}be followed by whitespace.", data: { value: value, @@ -49,6 +50,7 @@ module.exports = function(context) { } else if (left.loc.end.line !== right.loc.start.line) { context.report({ node: node, + loc: left.loc.end, message: "Keyword \"{{value}}\" must not be followed by a newline.", data: { value: value diff --git a/tests/lib/rules/no-sequences.js b/tests/lib/rules/no-sequences.js index 4dd214b417bf..dcd89821ed06 100644 --- a/tests/lib/rules/no-sequences.js +++ b/tests/lib/rules/no-sequences.js @@ -15,10 +15,20 @@ var rule = require("../../../lib/rules/no-sequences"), // Tests //------------------------------------------------------------------------------ -var errors = [{ - message: "Unexpected use of comma operator.", - type: "SequenceExpression" -}]; +/** + * Create error message object for failure cases + * @param {int} column column of the error + * @returns {object} returns the error messages collection + * @private + */ +function errors(column) { + return [{ + message: "Unexpected use of comma operator.", + type: "SequenceExpression", + line: 1, + column: column + }]; +} var ruleTester = new RuleTester(); ruleTester.run("no-sequences", rule, { @@ -42,12 +52,12 @@ ruleTester.run("no-sequences", rule, { // Examples of code that should trigger the rule invalid: [ - { code: "a = 1, 2", errors: errors }, - { code: "do {} while (doSomething(), !!test);", errors: errors }, - { code: "for (; doSomething(), !!test; );", errors: errors }, - { code: "if (doSomething(), !!test);", errors: errors }, - { code: "switch (doSomething(), val) {}", errors: errors }, - { code: "while (doSomething(), !!test);", errors: errors }, - { code: "with (doSomething(), val) {}", errors: errors } + { code: "a = 1, 2", errors: errors(6) }, + { code: "do {} while (doSomething(), !!test);", errors: errors(27) }, + { code: "for (; doSomething(), !!test; );", errors: errors(21) }, + { code: "if (doSomething(), !!test);", errors: errors(18) }, + { code: "switch (doSomething(), val) {}", errors: errors(22) }, + { code: "while (doSomething(), !!test);", errors: errors(21) }, + { code: "with (doSomething(), val) {}", errors: errors(20) } ] }); diff --git a/tests/lib/rules/space-after-keywords.js b/tests/lib/rules/space-after-keywords.js index cb7a5e353ef2..732bc3085b6a 100644 --- a/tests/lib/rules/space-after-keywords.js +++ b/tests/lib/rules/space-after-keywords.js @@ -77,7 +77,7 @@ ruleTester.run("space-after-keywords", rule, { }, { code: "do ;while((0))", - errors: [{ message: "Keyword \"while\" must be followed by whitespace.", type: "DoWhileStatement" }], + errors: [{ message: "Keyword \"while\" must be followed by whitespace.", type: "DoWhileStatement", line: 1, column: 10 }], output: "do ;while ((0))" }, { @@ -98,7 +98,7 @@ ruleTester.run("space-after-keywords", rule, { { code: "do;while (0)", options: ["never"], - errors: [{ message: "Keyword \"while\" must not be followed by whitespace.", type: "DoWhileStatement" }], + errors: [{ message: "Keyword \"while\" must not be followed by whitespace.", type: "DoWhileStatement", line: 1, column: 9 }], output: "do;while(0)" }, { @@ -173,7 +173,7 @@ ruleTester.run("space-after-keywords", rule, { { code: "do\n{} while\n(0)", options: ["always"], - errors: [{ message: "Keyword \"do\" must not be followed by a newline." }, { message: "Keyword \"while\" must not be followed by a newline." }], + errors: [{ message: "Keyword \"do\" must not be followed by a newline." }, { message: "Keyword \"while\" must not be followed by a newline.", line: 2, column: 9 }], output: "do {} while (0)" } ] From 03209e2347ccb9f6a120051278fddf6f63d929e9 Mon Sep 17 00:00:00 2001 From: Gyandeep Singh Date: Mon, 16 Nov 2015 17:33:15 -0600 Subject: [PATCH 32/63] Update: return type error in `valid-jsdoc` rule (fixes #4443) --- docs/rules/valid-jsdoc.md | 10 ++++++++++ lib/rules/valid-jsdoc.js | 10 +++++++--- tests/lib/rules/valid-jsdoc.js | 12 ++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/rules/valid-jsdoc.md b/docs/rules/valid-jsdoc.md index 9c8d5881aa1e..a8adf8740dcb 100644 --- a/docs/rules/valid-jsdoc.md +++ b/docs/rules/valid-jsdoc.md @@ -178,6 +178,16 @@ Specify a regular expression to validate jsdoc comment block description against }] ``` +#### requireReturnType + +By default ESLint requires you to specify `type` for `@return` tag for every documented function. + +```json +"valid-jsdoc": [2, { + "requireReturnType": false +}] +``` + ## When Not To Use It If you aren't using JSDoc, then you can safely turn this rule off. diff --git a/lib/rules/valid-jsdoc.js b/lib/rules/valid-jsdoc.js index a9ec8567fa35..63d896e1f12c 100644 --- a/lib/rules/valid-jsdoc.js +++ b/lib/rules/valid-jsdoc.js @@ -24,7 +24,8 @@ module.exports = function(context) { // these both default to true, so you have to explicitly make them false requireReturn = options.requireReturn !== false, requireParamDescription = options.requireParamDescription !== false, - requireReturnDescription = options.requireReturnDescription !== false; + requireReturnDescription = options.requireReturnDescription !== false, + requireReturnType = options.requireReturnType !== false; //-------------------------------------------------------------------------- // Helpers @@ -77,7 +78,7 @@ module.exports = function(context) { * @private */ function isValidReturnType(tag) { - return tag.type.name === "void" || tag.type.type === "UndefinedLiteral"; + return tag.type === null || tag.type.name === "void" || tag.type.type === "UndefinedLiteral"; } /** @@ -144,7 +145,7 @@ module.exports = function(context) { if (!requireReturn && !functionData.returnPresent && tag.type.name !== "void" && tag.type.name !== "undefined") { context.report(jsdocNode, "Unexpected @" + tag.title + " tag; function has no return statement."); } else { - if (!tag.type) { + if (requireReturnType && !tag.type) { context.report(jsdocNode, "Missing JSDoc return type."); } @@ -258,6 +259,9 @@ module.exports.schema = [ }, "matchDescription": { "type": "string" + }, + "requireReturnType": { + "type": "boolean" } }, "additionalProperties": false diff --git a/tests/lib/rules/valid-jsdoc.js b/tests/lib/rules/valid-jsdoc.js index 2cfe1ce750ba..38a535d9745f 100644 --- a/tests/lib/rules/valid-jsdoc.js +++ b/tests/lib/rules/valid-jsdoc.js @@ -122,6 +122,10 @@ ruleTester.run("valid-jsdoc", rule, { code: "/** Foo \n@return {void} Foo\n */\nfunction foo(){}", options: [{ prefer: { "return": "return" }}] }, + { + code: "/** Foo \n@return Foo\n */\nfunction foo(){}", + options: [{ requireReturnType: false }] + }, // classes { @@ -429,6 +433,14 @@ ruleTester.run("valid-jsdoc", rule, { type: "Block" }] }, + { + code: "/** Foo \n@return Foo\n */\nfunction foo(){}", + options: [{ prefer: { "return": "return" }}], + errors: [{ + message: "Missing JSDoc return type.", + type: "Block" + }] + }, // classes { code: From 4cc8326a04672f9fd9b04d618490075e0eded201 Mon Sep 17 00:00:00 2001 From: Gyandeep Singh Date: Tue, 3 Nov 2015 13:01:24 -0600 Subject: [PATCH 33/63] Update: Add class support to `require-jsdoc` rule (fixes #4268) --- docs/rules/require-jsdoc.md | 54 ++++++++++- lib/rules/require-jsdoc.js | 63 +++++++++++- tests/lib/rules/require-jsdoc.js | 160 ++++++++++++++++++++++++++++++- 3 files changed, 272 insertions(+), 5 deletions(-) diff --git a/docs/rules/require-jsdoc.md b/docs/rules/require-jsdoc.md index 4fc142f49144..a255962204f0 100644 --- a/docs/rules/require-jsdoc.md +++ b/docs/rules/require-jsdoc.md @@ -21,21 +21,61 @@ Some style guides require JSDoc comments for all functions as a way of explainin This rule generates warnings for nodes that do not have JSDoc comments when they should. Supported nodes: * `FunctionDeclaration` +* `ClassDeclaration` +* `MethodDefinition` + +### Options + +This rule accepts a `require` object with its properties as + +* `FunctionDeclaration` (default: `true`) +* `ClassDeclaration` (default: `false`) +* `MethodDefinition` (default: `false`) + +Default option settings are + +```json +{ + "require-jsdoc": [2, { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": false, + "ClassDeclaration": false + } + }] +} +``` The following patterns are considered problems: ```js -/*eslint require-jsdoc: 2*/ +/*eslint "require-jsdoc": [2, { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": false, + "ClassDeclaration": false + } +}]*/ function foo() { /*error Missing JSDoc comment.*/ return 10; } + +class Test{ /*error Missing JSDoc comment.*/ + getDate(){} /*error Missing JSDoc comment.*/ +} ``` The following patterns are not considered problems: ```js -/*eslint require-jsdoc: 2*/ +/*eslint "require-jsdoc": [2, { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": false, + "ClassDeclaration": false + } +}]*/ /** * It returns 10 @@ -55,6 +95,16 @@ var array = [1,2,3]; array.filter(function(item) { return item > 2; }); + +/** +* It returns 10 +*/ +class Test{ + /** + * returns the date + */ + getDate(){} +} ``` ## When not to use diff --git a/lib/rules/require-jsdoc.js b/lib/rules/require-jsdoc.js index 35529ace4af6..79379140bc79 100644 --- a/lib/rules/require-jsdoc.js +++ b/lib/rules/require-jsdoc.js @@ -5,8 +5,16 @@ */ "use strict"; +var assign = require("object-assign"); + module.exports = function(context) { var source = context.getSourceCode(); + var DEFAULT_OPTIONS = { + "FunctionDeclaration": true, + "MethodDefinition": false, + "ClassDeclaration": false + }; + var options = assign(DEFAULT_OPTIONS, context.options[0] && context.options[0].require || {}); /** * Report the error message @@ -17,6 +25,21 @@ module.exports = function(context) { context.report(node, "Missing JSDoc comment."); } + /** + * 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") { + var jsdocComment = source.getJSDocComment(node); + + if (!jsdocComment) { + report(node); + } + } + } + /** * Check if the jsdoc comment is present or not. * @param {ASTNode} node node to examine @@ -31,8 +54,44 @@ module.exports = function(context) { } return { - "FunctionDeclaration": checkJsDoc + "FunctionDeclaration": function(node) { + if (options.FunctionDeclaration) { + checkJsDoc(node); + } + }, + "FunctionExpression": function(node) { + if (options.MethodDefinition) { + checkClassMethodJsDoc(node); + } + }, + "ClassDeclaration": function(node) { + if (options.ClassDeclaration) { + checkJsDoc(node); + } + } }; }; -module.exports.schema = []; +module.exports.schema = [ + { + "type": "object", + "properties": { + "require": { + "type": "object", + "properties": { + "ClassDeclaration": { + "type": "boolean" + }, + "MethodDefinition": { + "type": "boolean" + }, + "FunctionDeclaration": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } +]; diff --git a/tests/lib/rules/require-jsdoc.js b/tests/lib/rules/require-jsdoc.js index 2b99f45fd271..2208a523d1f1 100644 --- a/tests/lib/rules/require-jsdoc.js +++ b/tests/lib/rules/require-jsdoc.js @@ -42,7 +42,65 @@ ruleTester.run("require-jsdoc", rule, { "var array = [1,2,3];\narray.forEach(function() {});", "var array = [1,2,3];\narray.filter(function() {});", "Object.keys(this.options.rules || {}).forEach(function(name) {}.bind(this));", - "var object = { name: 'key'};\nObject.keys(object).forEach(function() {})" + "var object = { name: 'key'};\nObject.keys(object).forEach(function() {})", + { + code: "function myFunction() {}", + options: [{ + "require": { + "FunctionDeclaration": false, + "MethodDefinition": true, + "ClassDeclaration": true + } + }] + }, + { + code: + "/**\n" + + " * Description for A.\n" + + " */\n" + + "class A {\n" + + " /**\n" + + " * Description for constructor.\n" + + " * @param {object[]} xs - xs\n" + + " */\n" + + " constructor(xs) {\n" + + " this.a = xs;" + + " }\n" + + "}", + ecmaFeatures: { + classes: true + }, + options: [{ + "require": { + "MethodDefinition": true, + "ClassDeclaration": true + } + }] + }, + { + code: + "/**\n" + + " * Description for A.\n" + + " */\n" + + "class App extends Component {\n" + + " /**\n" + + " * Description for constructor.\n" + + " * @param {object[]} xs - xs\n" + + " */\n" + + " constructor(xs) {\n" + + " this.a = xs;" + + " }\n" + + "}", + ecmaFeatures: { + classes: true + }, + options: [{ + "require": { + "MethodDefinition": true, + "ClassDeclaration": true + } + }] + } ], invalid: [ @@ -52,6 +110,106 @@ ruleTester.run("require-jsdoc", rule, { message: "Missing JSDoc comment.", type: "FunctionDeclaration" }] + }, + { + code: + "/**\n" + + " * Description for A.\n" + + " */\n" + + "class A {\n" + + " constructor(xs) {\n" + + " this.a = xs;" + + " }\n" + + "}", + ecmaFeatures: { + classes: true + }, + options: [{ + "require": { + "MethodDefinition": true, + "ClassDeclaration": true + } + }], + errors: [{ + message: "Missing JSDoc comment.", + type: "FunctionExpression" + }] + }, + { + code: + "class A {\n" + + " /**\n" + + " * Description for constructor.\n" + + " * @param {object[]} xs - xs\n" + + " */\n" + + " constructor(xs) {\n" + + " this.a = xs;" + + " }\n" + + "}", + ecmaFeatures: { + classes: true + }, + options: [{ + "require": { + "MethodDefinition": true, + "ClassDeclaration": true + } + }], + errors: [{ + message: "Missing JSDoc comment.", + type: "ClassDeclaration" + }] + }, + { + code: + "class A extends B {\n" + + " /**\n" + + " * Description for constructor.\n" + + " * @param {object[]} xs - xs\n" + + " */\n" + + " constructor(xs) {\n" + + " this.a = xs;" + + " }\n" + + "}", + ecmaFeatures: { + classes: true + }, + options: [{ + "require": { + "MethodDefinition": true, + "ClassDeclaration": true + } + }], + errors: [{ + message: "Missing JSDoc comment.", + type: "ClassDeclaration" + }] + }, + { + code: + "export class A extends B {\n" + + " /**\n" + + " * Description for constructor.\n" + + " * @param {object[]} xs - xs\n" + + " */\n" + + " constructor(xs) {\n" + + " this.a = xs;" + + " }\n" + + "}", + ecmaFeatures: { + classes: true, + modules: true + }, + options: [{ + "require": { + "MethodDefinition": true, + "ClassDeclaration": true + } + }], + errors: [{ + message: "Missing JSDoc comment.", + type: "ClassDeclaration" + }] } ] }); From b4dfd3a6afed2a9c40409a21ff3a75dd71a5600f Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 19 Nov 2015 12:03:31 -0800 Subject: [PATCH 34/63] Docs: Fix home directory config description (fixes #4398) --- docs/user-guide/configuring.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index cf25f69d1e61..be43c22284ea 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -431,6 +431,8 @@ your-project If there is an `.eslintrc` and a `package.json` file found in the same directory, both will be used, with the `.eslintrc` having the higher precendence. +**Note:** If you have a personal configuration file in your home directory (`~/.eslintrc`), it will only be used if no other configuration files are found. Since a personal configuration would apply to everything inside of a user's directory, including third-party code, this could cause problems when running ESLint. + By default, ESLint will look for configuration files in all parent folders up to the root directory. This can be useful if you want all of your projects to follow a certain convention, but can sometimes lead to unexpected results. To limit ESLint to a specific project, place `"root": true` inside the `eslintConfig` field of the `package.json` file or in the `.eslintrc` file at your project's root level. ESLint will stop looking in parent folders once it finds a configuration with `"root": true`. ```js @@ -446,16 +448,16 @@ And in YAML: root: true ``` -For example, consider `projectA` which has `"root": true` set in the `.eslintrc` file in the main project directory. In this case, while linting main.js, the configurations within `lib/` and `projectA` will be used, but the `.eslintrc` file in `user/` will not. +For example, consider `projectA` which has `"root": true` set in the `.eslintrc` file in the main project directory. In this case, while linting `main.js`, the configurations within `lib/`will be used, but the `.eslintrc` file in `projectA/` will not. ```text home └── user - ├── .eslintrc + ├── .eslintrc <- Always skipped if other configs present └── projectA - ├── .eslintrc <- { "root": true } + ├── .eslintrc <- Not used └── lib - ├── .eslintrc + ├── .eslintrc <- { "root": true } └── main.js ``` From ff9c79774da75881f191ade7865ad4a176b3741d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 19 Nov 2015 12:05:53 -0800 Subject: [PATCH 35/63] Docs: Clean up description of recommended rules (fixes #4365) --- docs/rules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index ea9add6c8555..f8018c510434 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -1,6 +1,6 @@ # Rules -Rules in ESLint are divided into several categories to help you better understand their value. Though none are enabled by default, you can turn on rules that ESLint recommends by specifying your configuration to inherit from `eslint:recommended`. The rules that will be enabled when you inherit from `eslint:recommended` are indicated below as "(recommended)". For more information on how to configure rules and inherit from `eslint:recommended`, please see the [configuration documentation](../user-guide/configuring.md). +Rules in ESLint are divided into several categories to help you better understand their value. All rules are disabled by default. ESLint recommends some rules to catch common problems, and you can use these recommended rules by including `extends: "eslint:recommended"` in your configuration file. The rules that will be enabled when you inherit from `eslint:recommended` are indicated below as "(recommended)". For more information on how to configure rules and use `extends`, please see the [configuration documentation](../user-guide/configuring.md). Some rules are fixable using the `--fix` command line flag. Those rules are marked as "(fixable)" below. From 7ebaeae66dcf467e73c1beea0cda5012ed8742e1 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 19 Nov 2015 12:08:02 -0800 Subject: [PATCH 36/63] Docs: Move legacy rules to stylistic (files #4111) --- docs/rules/README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index ea9add6c8555..61ddce5583f4 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -158,11 +158,16 @@ These rules are purely matters of style and are quite subjective. * [key-spacing](key-spacing.md) - enforce spacing between keys and values in object literal properties * [linebreak-style](linebreak-style.md) - disallow mixed 'LF' and 'CRLF' as linebreaks * [lines-around-comment](lines-around-comment.md) - enforce empty lines around comments +* [max-depth](max-depth.md) - specify the maximum depth that blocks can be nested +* [max-len](max-len.md) - specify the maximum length of a line in your program * [max-nested-callbacks](max-nested-callbacks.md) - specify the maximum depth callbacks can be nested +* [max-params](max-params.md) - limits the number of parameters that can be used in the function declaration. +* [max-statements](max-statements.md) - specify the maximum number of statement allowed in a function * [new-cap](new-cap.md) - require a capital letter for constructors * [new-parens](new-parens.md) - disallow the omission of parentheses when invoking a constructor with no arguments * [newline-after-var](newline-after-var.md) - require or disallow an empty newline after variable declarations * [no-array-constructor](no-array-constructor.md) - disallow use of the `Array` constructor +* [no-bitwise](no-bitwise.md) - disallow use of bitwise operators * [no-continue](no-continue.md) - disallow use of the `continue` statement * [no-inline-comments](no-inline-comments.md) - disallow comments inline after code * [no-lonely-if](no-lonely-if.md) - disallow `if` as the only statement in an `else` block @@ -171,6 +176,7 @@ These rules are purely matters of style and are quite subjective. * [no-negated-condition](no-negated-condition.md) - disallow negated conditions * [no-nested-ternary](no-nested-ternary.md) - disallow nested ternary expressions * [no-new-object](no-new-object.md) - disallow the use of the `Object` constructor +* [no-plusplus](no-plusplus.md) - disallow use of unary operators, `++` and `--` * [no-restricted-syntax](no-restricted-syntax.md) - disallow use of certain syntax in code * [no-spaced-func](no-spaced-func.md) - disallow space between function identifier and application (fixable) * [no-ternary](no-ternary.md) - disallow the use of ternary operators @@ -222,16 +228,6 @@ These rules are only relevant to ES6 environments. * [prefer-template](prefer-template.md) - suggest using template literals instead of strings concatenation * [require-yield](require-yield.md) - disallow generator functions that do not have `yield` -## Legacy - -The following rules are included for compatibility with [JSHint](http://jshint.com/) and [JSLint](http://jslint.com/). While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same. - -* [max-depth](max-depth.md) - specify the maximum depth that blocks can be nested -* [max-len](max-len.md) - specify the maximum length of a line in your program -* [max-params](max-params.md) - limits the number of parameters that can be used in the function declaration. -* [max-statements](max-statements.md) - specify the maximum number of statement allowed in a function -* [no-bitwise](no-bitwise.md) - disallow use of bitwise operators -* [no-plusplus](no-plusplus.md) - disallow use of unary operators, `++` and `--` ## Removed From 6c04d518c6c64bcd6b79f1ea482dd6cc45598bd4 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 19 Nov 2015 12:13:11 -0800 Subject: [PATCH 37/63] Docs: Update description of exported comment (fixes #3916) --- docs/rules/no-unused-vars.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/rules/no-unused-vars.md b/docs/rules/no-unused-vars.md index f3f9f137fcb8..a1fd4cf70d93 100644 --- a/docs/rules/no-unused-vars.md +++ b/docs/rules/no-unused-vars.md @@ -60,8 +60,7 @@ myFunc(function foo() { ### Exporting Variables -In some environments you may use `var` to create a global variable that may be used by other scripts. You can - use the `/* exported variableName */` comment block to indicate that this variable may be used elsewhere. +In environments outside of CommonJS or ECMAScript modules, you may use `var` to create a global variable that may be used by other scripts. You can use the `/* exported variableName */` comment block to indicate that this variable is being exported and therefore should not be considered unused. Note that `/* exported */` has no effect when used with the `node` or `commonjs` environments or when `ecmaFeatures.modules` is true. ### Options From aff7d6d8ab7dbc6bab492bc44c9ff907033e0b66 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 19 Nov 2015 14:53:10 -0800 Subject: [PATCH 38/63] Update: --init to create extensioned files (fixes #4476) --- bin/eslint.js | 3 +- lib/config/config-file.js | 76 ++++++++++++- lib/config/config-initializer.js | 34 ++++-- tests/lib/config/config-file.js | 65 +++++++++++ tests/lib/config/config-initializer.js | 147 ++++++++++++++----------- 5 files changed, 244 insertions(+), 81 deletions(-) diff --git a/bin/eslint.js b/bin/eslint.js index 2d61f59a8cea..8c7bce905351 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -46,14 +46,13 @@ if (useStdIn) { } })); } else if (init) { - var configInit = require("../lib/config-initializer"); + var configInit = require("../lib/config/config-initializer"); configInit.initializeConfig(function(err) { if (err) { exitCode = 1; console.error(err.message); console.error(err.stack); } else { - console.log("Successfully created .eslintrc file in " + process.cwd()); exitCode = 0; } }); diff --git a/lib/config/config-file.js b/lib/config/config-file.js index 6824303b69f6..aaffb6fba3ce 100644 --- a/lib/config/config-file.js +++ b/lib/config/config-file.js @@ -149,7 +149,7 @@ function loadPackageJSONConfigFile(filePath) { try { return require(filePath).eslintConfig || null; } catch (e) { - debug("Error reading JavaScript file: " + filePath); + debug("Error reading package.json file: " + filePath); e.message = "Cannot read config file: " + filePath + "\nError: " + e.message; throw e; } @@ -215,6 +215,79 @@ function loadConfigFile(filePath) { return ConfigOps.merge(ConfigOps.createEmptyConfig(), config); } +/** + * Writes a configuration file in JSON format. + * @param {Object} config The configuration object to write. + * @param {string} filePath The filename to write to. + * @returns {void} + * @private + */ +function writeJSONConfigFile(config, filePath) { + debug("Writing JSON config file: " + filePath); + + var content = JSON.stringify(config, null, 4); + fs.writeFileSync(filePath, content, "utf8"); +} + +/** + * Writes a configuration file in YAML format. + * @param {Object} config The configuration object to write. + * @param {string} filePath The filename to write to. + * @returns {void} + * @private + */ +function writeYAMLConfigFile(config, filePath) { + debug("Writing YAML config file: " + filePath); + + // lazy load YAML to improve performance when not used + var yaml = require("js-yaml"); + + var content = yaml.safeDump(config); + fs.writeFileSync(filePath, content, "utf8"); +} + +/** + * Writes a configuration file in JavaScript format. + * @param {Object} config The configuration object to write. + * @param {string} filePath The filename to write to. + * @returns {void} + * @private + */ +function writeJSConfigFile(config, filePath) { + debug("Writing JS config file: " + filePath); + + var content = "module.exports = " + JSON.stringify(config, null, 4) + ";"; + fs.writeFileSync(filePath, content, "utf8"); +} + +/** + * Writes a configuration file. + * @param {Object} config The configuration object to write. + * @param {string} filePath The filename to write to. + * @returns {void} + * @throws {Error} When an unknown file type is specified. + * @private + */ +function write(config, filePath) { + switch (path.extname(filePath)) { + case ".js": + writeJSConfigFile(config, filePath); + break; + + case ".json": + writeJSONConfigFile(config, filePath); + break; + + case ".yaml": + case ".yml": + writeYAMLConfigFile(config, filePath); + break; + + default: + throw new Error("Can't write to unknown file type."); + } +} + /** * Applies values from the "extends" field in a configuration file. * @param {Object} config The configuration information. @@ -340,6 +413,7 @@ module.exports = { load: load, resolve: resolve, + write: write, applyExtends: applyExtends, CONFIG_FILES: CONFIG_FILES, diff --git a/lib/config/config-initializer.js b/lib/config/config-initializer.js index b047c44c262c..526c56d9ae80 100644 --- a/lib/config/config-initializer.js +++ b/lib/config/config-initializer.js @@ -11,9 +11,8 @@ //------------------------------------------------------------------------------ var exec = require("child_process").exec, - fs = require("fs"), inquirer = require("inquirer"), - yaml = require("js-yaml"); + ConfigFile = require("./config-file"); //------------------------------------------------------------------------------ // Private @@ -23,13 +22,24 @@ var exec = require("child_process").exec, /** * Create .eslintrc file in the current working directory * @param {object} config object that contains user's answers - * @param {bool} isJson should config file be json or yaml + * @param {string} format The file format to write to. * @param {function} callback function to call once the file is written. * @returns {void} */ -function writeFile(config, isJson, callback) { +function writeFile(config, format, callback) { + + // default is .js + var extname = ".js"; + if (format === "YAML") { + extname = ".yml"; + } else if (format === "JSON") { + extname = ".json"; + } + + try { - fs.writeFileSync("./.eslintrc", isJson ? JSON.stringify(config, null, 4) : yaml.safeDump(config)); + ConfigFile.write(config, "./.eslintrc" + extname); + console.log("Successfully created .eslintrc" + extname + " file in " + process.cwd()); } catch (e) { callback(e); return; @@ -37,6 +47,7 @@ function writeFile(config, isJson, callback) { // install any external configs as well as any included plugins if (config.extends && config.extends.indexOf("eslint") === -1) { + console.log("Installing additional dependencies"); exec("npm i eslint-config-" + config.extends + " --save-dev", function(err) { if (err) { @@ -51,6 +62,7 @@ function writeFile(config, isJson, callback) { // install the react plugin if it was explictly chosen if (config.plugins && config.plugins.indexOf("react") >= 0) { + console.log("Installing React plugin"); exec("npm i eslint-plugin-react --save-dev", callback); return; } @@ -129,8 +141,8 @@ function promptUser(callback) { type: "list", name: "format", message: "What format do you want your config file to be in?", - default: "JSON", - choices: ["JSON", "YAML"], + default: "JavaScript", + choices: ["JavaScript", "YAML", "JSON"], when: function(answers) { return answers.source === "guide"; } @@ -139,7 +151,7 @@ function promptUser(callback) { // early exit if you are using a style guide if (earlyAnswers.source === "guide") { - writeFile(getConfigForStyleGuide(earlyAnswers.styleguide), earlyAnswers.format === "JSON", callback); + writeFile(getConfigForStyleGuide(earlyAnswers.styleguide), earlyAnswers.format, callback); return; } @@ -204,12 +216,12 @@ function promptUser(callback) { type: "list", name: "format", message: "What format do you want your config file to be in?", - default: "JSON", - choices: ["JSON", "YAML"] + default: "JavaScript", + choices: ["JavaScript", "YAML", "JSON"] } ], function(answers) { var config = processAnswers(answers); - writeFile(config, answers.format === "JSON", callback); + writeFile(config, answers.format, callback); }); }); } diff --git a/tests/lib/config/config-file.js b/tests/lib/config/config-file.js index d670a1ad9142..ced0a7c2582b 100644 --- a/tests/lib/config/config-file.js +++ b/tests/lib/config/config-file.js @@ -12,7 +12,10 @@ var assert = require("chai").assert, leche = require("leche"), + sinon = require("sinon"), path = require("path"), + fs = require("fs"), + yaml = require("js-yaml"), proxyquire = require("proxyquire"), environments = require("../../../conf/environments"), ConfigFile = require("../../../lib/config/config-file"); @@ -33,6 +36,17 @@ function getFixturePath(filepath) { return path.resolve(__dirname, "../../fixtures/config-file", filepath); } +/** + * Reads a JS configuration object from a string to ensure that it parses. + * Used for testing configuration file output. + * @param {string} code The code to eval. + * @returns {*} The result of the evaluation. + * @private + */ +function readJSModule(code) { + return eval("var module = {};\n" + code); // eslint-disable-line no-eval +} + //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ @@ -298,4 +312,55 @@ describe("ConfigFile", function() { }); + describe("write()", function() { + + var sandbox, + config; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + config = { + env: { + browser: true, + node: true + }, + rules: { + quotes: 2, + semi: 1 + } + }; + }); + + afterEach(function() { + sandbox.verifyAndRestore(); + }); + + leche.withData([ + ["JavaScript", "foo.js", readJSModule], + ["JSON", "bar.json", JSON.parse], + ["YAML", "foo.yaml", yaml.safeLoad], + ["YML", "foo.yml", yaml.safeLoad] + ], function(fileType, filename, validate) { + + it("should write a file through fs when a " + fileType + " path is passed", function() { + var fakeFS = leche.fake(fs); + + sandbox.mock(fakeFS).expects("writeFileSync").withExactArgs( + filename, + sinon.match(function(value) { + return !!validate(value); + }), + "utf8" + ); + + var StubbedConfigFile = proxyquire("../../../lib/config/config-file", { + fs: fakeFS + }); + + StubbedConfigFile.write(config, filename); + }); + + }); + }); + }); diff --git a/tests/lib/config/config-initializer.js b/tests/lib/config/config-initializer.js index 856cda3543fd..be47ad8ffd0d 100644 --- a/tests/lib/config/config-initializer.js +++ b/tests/lib/config/config-initializer.js @@ -19,73 +19,86 @@ var assert = require("chai").assert, var answers = {}; describe("configInitializer", function() { - beforeEach(function() { - answers = { - extendDefault: true, - indent: 2, - quotes: "single", - linebreak: "unix", - semi: true, - es6: true, - env: ["browser"], - jsx: false, - react: false, - format: "JSON" - }; - }); - it("should create default config", function() { - var config = init.processAnswers(answers); - assert.deepEqual(config.rules.indent, [2, 2]); - assert.deepEqual(config.rules.quotes, [2, "single"]); - assert.deepEqual(config.rules["linebreak-style"], [2, "unix"]); - assert.deepEqual(config.rules.semi, [2, "always"]); - assert.equal(config.env.es6, true); - assert.equal(config.env.browser, true); - assert.equal(config.extends, "eslint:recommended"); - }); - it("should disable semi", function() { - answers.semi = false; - var config = init.processAnswers(answers); - assert.deepEqual(config.rules.semi, [2, "never"]); - }); - it("should enable jsx flag", function() { - answers.jsx = true; - var config = init.processAnswers(answers); - assert.equal(config.ecmaFeatures.jsx, true); - }); - it("should enable react plugin", function() { - answers.jsx = true; - answers.react = true; - var config = init.processAnswers(answers); - assert.equal(config.ecmaFeatures.jsx, true); - assert.equal(config.ecmaFeatures.experimentalObjectRestSpread, true); - assert.deepEqual(config.plugins, ["react"]); - }); - it("should not enable es6", function() { - answers.es6 = false; - var config = init.processAnswers(answers); - assert.isUndefined(config.env.es6); - }); - it("should extend eslint:recommended", function() { - var config = init.processAnswers(answers); - assert.equal(config.extends, "eslint:recommended"); - }); - it("should support the google style guide", function() { - var config = init.getConfigForStyleGuide("google"); - assert.deepEqual(config, {extends: "google"}); - }); - it("should support the airbnb style guide", function() { - var config = init.getConfigForStyleGuide("airbnb"); - assert.deepEqual(config, {extends: "airbnb", plugins: ["react"]}); - }); - it("should support the standard style guide", function() { - var config = init.getConfigForStyleGuide("standard"); - assert.deepEqual(config, {extends: "standard", plugins: ["standard"]}); - }); - it("should throw when encountering an unsupported style guide", function() { - assert.throws(function() { - init.getConfigForStyleGuide("non-standard"); - }, "You referenced an unsupported guide."); + describe("processAnswers()", function() { + beforeEach(function() { + answers = { + extendDefault: true, + indent: 2, + quotes: "single", + linebreak: "unix", + semi: true, + es6: true, + env: ["browser"], + jsx: false, + react: false, + format: "JSON" + }; + }); + + it("should create default config", function() { + var config = init.processAnswers(answers); + assert.deepEqual(config.rules.indent, [2, 2]); + assert.deepEqual(config.rules.quotes, [2, "single"]); + assert.deepEqual(config.rules["linebreak-style"], [2, "unix"]); + assert.deepEqual(config.rules.semi, [2, "always"]); + assert.equal(config.env.es6, true); + assert.equal(config.env.browser, true); + assert.equal(config.extends, "eslint:recommended"); + }); + + it("should disable semi", function() { + answers.semi = false; + var config = init.processAnswers(answers); + assert.deepEqual(config.rules.semi, [2, "never"]); + }); + + it("should enable jsx flag", function() { + answers.jsx = true; + var config = init.processAnswers(answers); + assert.equal(config.ecmaFeatures.jsx, true); + }); + + it("should enable react plugin", function() { + answers.jsx = true; + answers.react = true; + var config = init.processAnswers(answers); + assert.equal(config.ecmaFeatures.jsx, true); + assert.equal(config.ecmaFeatures.experimentalObjectRestSpread, true); + assert.deepEqual(config.plugins, ["react"]); + }); + + it("should not enable es6", function() { + answers.es6 = false; + var config = init.processAnswers(answers); + assert.isUndefined(config.env.es6); + }); + + it("should extend eslint:recommended", function() { + var config = init.processAnswers(answers); + assert.equal(config.extends, "eslint:recommended"); + }); + + it("should support the google style guide", function() { + var config = init.getConfigForStyleGuide("google"); + assert.deepEqual(config, {extends: "google"}); + }); + + it("should support the airbnb style guide", function() { + var config = init.getConfigForStyleGuide("airbnb"); + assert.deepEqual(config, {extends: "airbnb", plugins: ["react"]}); + }); + + it("should support the standard style guide", function() { + var config = init.getConfigForStyleGuide("standard"); + assert.deepEqual(config, {extends: "standard", plugins: ["standard"]}); + }); + + it("should throw when encountering an unsupported style guide", function() { + assert.throws(function() { + init.getConfigForStyleGuide("non-standard"); + }, "You referenced an unsupported guide."); + }); }); + }); From be2c000ad0c1ee5ebd04e26e89678cd0ba659201 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 20 Nov 2015 10:25:55 -0800 Subject: [PATCH 39/63] 1.10.0 --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c62dd9c9ec..1acd66004e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +v1.10.0 - November 20, 2015 + +* Docs: Remove dupes from changelog (Nicholas C. Zakas) +* Update: --init to create extensioned files (fixes #4476) (Nicholas C. Zakas) +* Docs: Update description of exported comment (fixes #3916) (Nicholas C. Zakas) +* Docs: Move legacy rules to stylistic (files #4111) (Nicholas C. Zakas) +* Docs: Clean up description of recommended rules (fixes #4365) (Nicholas C. Zakas) +* Docs: Fix home directory config description (fixes #4398) (Nicholas C. Zakas) +* Update: Add class support to `require-jsdoc` rule (fixes #4268) (Gyandeep Singh) +* Update: return type error in `valid-jsdoc` rule (fixes #4443) (Gyandeep Singh) +* Update: Display errors at the place where fix should go (fixes #4470) (nightwing) +* Docs: Fix typo in default `cacheLocation` value (Andrew Hutchings) +* Fix: Handle comments in block-spacing (fixes #4387) (alberto) +* Update: Accept array for `ignorePattern` (fixes #3982) (Jesse McCarthy) +* Update: replace label and break with IIFE and return (fixes #4459) (Ilya Panasenko) +* Fix: space-before-keywords false positive (fixes #4449) (alberto) +* Fix: Improves performance (refs #3530) (Toru Nagashima) +* Fix: Autofix quotes produces invalid javascript (fixes #4380) (nightwing) +* Docs: Update indent.md (Nathan Brown) +* New: Disable comment config option (fixes #3901) (Matthew Riley MacPherson) +* New: Config files with extensions (fixes #4045, fixes #4263) (Nicholas C. Zakas) +* Revert "Update: Add JSX exceptions to no-extra-parens (fixes #4229)" (Brandon Mills) +* Update: Add JSX exceptions to no-extra-parens (fixes #4229) (Brandon Mills) +* Docs: Replace link to deprecated rule with newer rule (Andrew Marshall) +* Fix: `no-extend-native` crashed at empty defineProperty (fixes #4438) (Toru Nagashima) +* Fix: Support empty if blocks in lines-around-comment (fixes #4339) (alberto) +* Fix: `curly` warns wrong location for `else` (fixes #4362) (Toru Nagashima) +* Fix: `id-length` properties never option (fixes #4347) (Toru Nagashima) +* Docs: missing close rbracket in example (@storkme) +* Revert "Update: Allow empty arrow body (fixes #4411)" (Nicholas C. Zakas) +* Fix: eqeqeq autofix avoids clashes with space-infix-ops (fixes #4423) (Kevin Partington) +* Docs: Document semi-spacing behaviour (fixes #4404) (alberto) +* Update: Allow empty arrow body (fixes #4411) (alberto) +* Fix: Handle comments in comma-spacing (fixes #4389) (alberto) +* Update: Refactor eslint.verify args (fixes #4395) (Nicholas C. Zakas) +* Fix: no-undef-init should ignore const (fixes #4284) (Nicholas C. Zakas) +* Fix: Add the missing "as-needed" docs to the radix rule (fixes #4364) (Michał Gołębiowski) +* Fix: Display singular/plural version of "line" in message (fixes #4359) (Marius Schulz) +* Update: Add Popular Style Guides (fixes #4320) (Jamund Ferguson) +* Fix: eslint.report can be called w/o node if loc provided (fixes #4220) (Kevin Partington) +* Update: no-implicit-coercion validate AssignmentExpression (fixes #4348) (Ilya Panasenko) + v1.9.0 - November 6, 2015 * Update: Make radix accept a "as-needed" option (fixes #4048) (Michał Gołębiowski) diff --git a/package.json b/package.json index 908568ab3095..61bbe6096f98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "1.9.0", + "version": "1.10.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From bec6c9b0e2edca5f5bc99a7abc13b6f62a9a5411 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 20 Nov 2015 14:48:42 -0800 Subject: [PATCH 40/63] Fix: Revert freezing context object (refs #4495) --- lib/rule-context.js | 90 ++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/lib/rule-context.js b/lib/rule-context.js index aeed1e2a17ab..847f2fb78810 100644 --- a/lib/rule-context.js +++ b/lib/rule-context.js @@ -72,29 +72,42 @@ var PASSTHROUGHS = [ * @param {object} ecmaFeatures The ecmaFeatures settings passed from the config file. */ function RuleContext(ruleId, eslint, severity, options, settings, ecmaFeatures) { - // public. - this.id = ruleId; - this.options = options; - this.settings = settings; - this.ecmaFeatures = ecmaFeatures; - // private. - this.eslint = eslint; - this.severity = severity; + /** + * The read-only ID of the rule. + */ + Object.defineProperty(this, "id", { + value: ruleId + }); - Object.freeze(this); -} + /** + * The read-only options of the rule + */ + Object.defineProperty(this, "options", { + value: options + }); -RuleContext.prototype = { - constructor: RuleContext, + /** + * The read-only settings shared between all rules + */ + Object.defineProperty(this, "settings", { + value: settings + }); /** - * Passthrough to eslint.getSourceCode(). - * @returns {SourceCode} The SourceCode object for the code. + * The read-only ecmaFeatures shared across all rules */ - getSourceCode: function() { - return this.eslint.getSourceCode(); - }, + Object.defineProperty(this, "ecmaFeatures", { + value: Object.create(ecmaFeatures) + }); + Object.freeze(this.ecmaFeatures); + + // copy over passthrough methods + PASSTHROUGHS.forEach(function(name) { + this[name] = function() { + return eslint[name].apply(eslint, arguments); + }; + }, this); /** * Passthrough to eslint.report() that automatically assigns the rule ID and severity. @@ -106,7 +119,8 @@ RuleContext.prototype = { * with symbols being replaced by this object's values. * @returns {void} */ - report: function(nodeOrDescriptor, location, message, opts) { + this.report = function(nodeOrDescriptor, location, message, opts) { + var descriptor, fix = null; @@ -119,37 +133,31 @@ RuleContext.prototype = { fix = descriptor.fix(new RuleFixer()); } - this.eslint.report( - this.id, - this.severity, - descriptor.node, + eslint.report( + ruleId, severity, descriptor.node, descriptor.loc || descriptor.node.loc.start, - descriptor.message, - descriptor.data, - fix + descriptor.message, descriptor.data, fix ); return; } // old style call - this.eslint.report( - this.id, - this.severity, - nodeOrDescriptor, - location, - message, - opts - ); - } -}; + eslint.report(ruleId, severity, nodeOrDescriptor, location, message, opts); + }; -// copy over passthrough methods -PASSTHROUGHS.forEach(function(name) { - // All functions expected to have less arguments than 5. - this[name] = function(a, b, c, d, e) { - return this.eslint[name](a, b, c, d, e); + /** + * Passthrough to eslint.getSourceCode(). + * @returns {SourceCode} The SourceCode object for the code. + */ + this.getSourceCode = function() { + return eslint.getSourceCode(); }; -}, RuleContext.prototype); + +} + +RuleContext.prototype = { + constructor: RuleContext +}; module.exports = RuleContext; From bee05aa19153a19b950f47a5cc392f6fc162fbcd Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 20 Nov 2015 14:57:16 -0800 Subject: [PATCH 41/63] Build: Update eslint bot messages (fixes #4497) --- ISSUE_CREATE.md => templates/issue-create.md.ejs | 2 +- templates/pr-create.md.ejs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) rename ISSUE_CREATE.md => templates/issue-create.md.ejs (76%) create mode 100644 templates/pr-create.md.ejs diff --git a/ISSUE_CREATE.md b/templates/issue-create.md.ejs similarity index 76% rename from ISSUE_CREATE.md rename to templates/issue-create.md.ejs index 413b9dba6966..f7641ebe9ccd 100644 --- a/ISSUE_CREATE.md +++ b/templates/issue-create.md.ejs @@ -1,4 +1,4 @@ -Thanks for the issue! If you're reporting a bug, please be sure to include: +@<%= payload.sender.login %> Thanks for the issue! If you're reporting a bug, please be sure to include: 1. The version of ESLint you are using (run `eslint -v`) 2. What you did (the source code and ESLint configuration) diff --git a/templates/pr-create.md.ejs b/templates/pr-create.md.ejs new file mode 100644 index 000000000000..ff79928788aa --- /dev/null +++ b/templates/pr-create.md.ejs @@ -0,0 +1,5 @@ +Thanks for the pull request, @<%= payload.sender.login %>! It looks like this is your first time contributing to ESLint, so please take a moment to read over our [contribution guidelines](http://eslint.org/docs/developer-guide/contributing/pull-requests). + +We'll also need you to sign our [CLA](http://eslint.org/cla), which is just a way for you to say that you give us permission to use your contribution. + +If you have any questions about the process, don't hesitate to ask. From 01f33d37573252a6d8cf03391792d1d711037bd4 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 20 Nov 2015 15:05:15 -0800 Subject: [PATCH 42/63] 1.10.1 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1acd66004e3c..eeb7483c32b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +v1.10.1 - November 20, 2015 + +* Fix: Revert freezing context object (refs #4495) (Nicholas C. Zakas) +* 1.10.0 (Nicholas C. Zakas) + v1.10.0 - November 20, 2015 * Docs: Remove dupes from changelog (Nicholas C. Zakas) diff --git a/package.json b/package.json index 61bbe6096f98..943c52d15e11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "1.10.0", + "version": "1.10.1", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 6dfe712516425e2f3eda0725b84b42f1f1f997c8 Mon Sep 17 00:00:00 2001 From: Brian J Brennan Date: Fri, 20 Nov 2015 20:09:05 -0500 Subject: [PATCH 43/63] Docs: Load badge from HTTPS See #4489 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0e610f24a62..099a0dfd3554 100644 --- a/README.md +++ b/README.md @@ -122,5 +122,5 @@ Join our [Mailing List](https://groups.google.com/group/eslint) or [Chatroom](ht [travis-url]: https://travis-ci.org/eslint/eslint [coveralls-image]: https://img.shields.io/coveralls/eslint/eslint/master.svg?style=flat-square [coveralls-url]: https://coveralls.io/r/eslint/eslint?branch=master -[downloads-image]: http://img.shields.io/npm/dm/eslint.svg?style=flat-square +[downloads-image]: https://img.shields.io/npm/dm/eslint.svg?style=flat-square [downloads-url]: https://www.npmjs.com/package/eslint From 6b1d048f1922519edc8d90f9b2e5617dffdefb9b Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sat, 21 Nov 2015 19:39:07 +0900 Subject: [PATCH 44/63] Fix: Add a RestProperty test of `no-undef` (fixes #3271) --- package.json | 2 +- tests/lib/rules/no-undef.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 943c52d15e11..47eda9d4fcd2 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "debug": "^2.1.1", "doctrine": "^0.7.0", "escape-string-regexp": "^1.0.2", - "escope": "^3.2.0", + "escope": "^3.2.1", "espree": "^2.2.4", "estraverse": "^4.1.1", "estraverse-fb": "^1.3.1", diff --git a/tests/lib/rules/no-undef.js b/tests/lib/rules/no-undef.js index 421d768e5c7a..15323ca463fe 100644 --- a/tests/lib/rules/no-undef.js +++ b/tests/lib/rules/no-undef.js @@ -52,7 +52,14 @@ ruleTester.run("no-undef", rule, { { code: "var a; [a] = [0];", ecmaFeatures: {destructuring: true} }, { code: "var a; ({a}) = {};", ecmaFeatures: {destructuring: true} }, { code: "var a; ({b: a}) = {};", ecmaFeatures: {destructuring: true} }, - { code: "var obj; [obj.a, obj.b] = [0, 1];", ecmaFeatures: {destructuring: true} } + { code: "var obj; [obj.a, obj.b] = [0, 1];", ecmaFeatures: {destructuring: true} }, + + // Experimental, + { + code: "var {bacon, ...others} = stuff; foo(others)", + ecmaFeatures: {destructuring: true, experimentalObjectRestSpread: true}, + globals: {stuff: false, foo: false} + } ], invalid: [ { code: "a = 1;", errors: [{ message: "\"a\" is not defined.", type: "Identifier"}] }, From 828f4cb39d8c6b175e26a924f1f49f29abffda22 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Mon, 23 Nov 2015 08:38:41 +0900 Subject: [PATCH 45/63] Fix: `no-spaced-func` had been crashed (fixes #4508) If the `callee` is enclosed with parentheses and arguments parentheses are omitted, `no-spaced-func` had been crashed. --- lib/rules/no-spaced-func.js | 16 ++++++++++------ tests/lib/rules/no-spaced-func.js | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-spaced-func.js b/lib/rules/no-spaced-func.js index 4513d80472d2..551a3c609717 100644 --- a/lib/rules/no-spaced-func.js +++ b/lib/rules/no-spaced-func.js @@ -24,17 +24,21 @@ module.exports = function(context) { prevToken = lastCalleeToken, parenToken = sourceCode.getTokenAfter(lastCalleeToken); - if (sourceCode.getLastToken(node).value !== ")") { - return; - } - - while (parenToken.value !== "(") { + // advances to an open parenthesis. + while ( + parenToken && + parenToken.range[1] < node.range[1] && + parenToken.value !== "(" + ) { prevToken = parenToken; parenToken = sourceCode.getTokenAfter(parenToken); } // look for a space between the callee and the open paren - if (sourceCode.isSpaceBetweenTokens(prevToken, parenToken)) { + if (parenToken && + parenToken.range[1] < node.range[1] && + sourceCode.isSpaceBetweenTokens(prevToken, parenToken) + ) { context.report({ node: node, loc: lastCalleeToken.loc.start, diff --git a/tests/lib/rules/no-spaced-func.js b/tests/lib/rules/no-spaced-func.js index 83e22a6339de..77a0be90f25d 100644 --- a/tests/lib/rules/no-spaced-func.js +++ b/tests/lib/rules/no-spaced-func.js @@ -33,7 +33,8 @@ ruleTester.run("no-spaced-func", rule, { "( f()() )(0)", "(function(){ if (foo) { bar(); } }());", "f(0, (1))", - "describe/*.only*/('foo', function () {});" + "describe/**/('foo', function () {});", + "new (foo())" ], invalid: [ { From f4af63befeb32121e994ff7bf2d341a95462f92d Mon Sep 17 00:00:00 2001 From: alberto Date: Mon, 23 Nov 2015 08:26:05 +0100 Subject: [PATCH 46/63] Fix: Incorrect location in no-fallthrough (fixes #4516) --- lib/rules/no-fallthrough.js | 3 +-- tests/lib/rules/no-fallthrough.js | 12 ++++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/rules/no-fallthrough.js b/lib/rules/no-fallthrough.js index dcdde44579b5..98334e3fc55e 100644 --- a/lib/rules/no-fallthrough.js +++ b/lib/rules/no-fallthrough.js @@ -48,8 +48,7 @@ module.exports = function(context) { // check for comment if (!comment || !FALLTHROUGH_COMMENT.test(comment.value)) { - - context.report(switchData.lastCase, + context.report(node, "Expected a \"break\" statement before \"{{code}}\".", { code: node.test ? "case" : "default" }); } diff --git a/tests/lib/rules/no-fallthrough.js b/tests/lib/rules/no-fallthrough.js index aaea79e1210a..4fe41d431ffa 100644 --- a/tests/lib/rules/no-fallthrough.js +++ b/tests/lib/rules/no-fallthrough.js @@ -48,20 +48,24 @@ ruleTester.run("no-fallthrough", rule, { ], invalid: [ { - code: "switch(foo) { case 0: a(); case 1: b() }", + code: "switch(foo) { case 0: a();\ncase 1: b() }", errors: [ { message: "Expected a \"break\" statement before \"case\".", - type: "SwitchCase" + type: "SwitchCase", + line: 2, + column: 1 } ] }, { - code: "switch(foo) { case 0: a(); default: b() }", + code: "switch(foo) { case 0: a();\ndefault: b() }", errors: [ { message: "Expected a \"break\" statement before \"default\".", - type: "SwitchCase" + type: "SwitchCase", + line: 2, + column: 1 } ] } From 7302727150d4286cd268c33514565109f87c4efc Mon Sep 17 00:00:00 2001 From: alberto Date: Sun, 22 Nov 2015 22:53:27 +0100 Subject: [PATCH 47/63] Build: Allow revert commits in commit messages (fixes #4452) --- Makefile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.js b/Makefile.js index 1cca5576aeaf..dd71dd56bb02 100644 --- a/Makefile.js +++ b/Makefile.js @@ -944,7 +944,7 @@ target.checkGitCommit = function() { } // Only check non-release messages - if (!semver.valid(commitMsgs[0])) { + if (!semver.valid(commitMsgs[0]) && !/^Revert /.test(commitMsgs[0])) { if (commitMsgs[0].slice(0, commitMsgs[0].indexOf("\n")).length > 72) { echo(" - First line of commit message must not exceed 72 characters"); failed = true; From e47265fa8c6d6cb832a95015562024000618c83a Mon Sep 17 00:00:00 2001 From: Gyandeep Singh Date: Mon, 23 Nov 2015 11:11:24 -0600 Subject: [PATCH 48/63] Build: Add branch update during release process (fixes #4491) --- Makefile.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile.js b/Makefile.js index dd71dd56bb02..56935cd83eb7 100644 --- a/Makefile.js +++ b/Makefile.js @@ -182,6 +182,9 @@ function getReleaseType(version) { function release(type) { var newVersion;/* , changes;*/ + exec("git checkout master && git fetch origin && git reset --hard origin/master"); + exec("npm install && npm prune"); + target.test(); echo("Generating new version"); newVersion = execSilent("npm version " + type).trim(); From e0ecb2276ab49b46bb7fbe11c32753c42c78a8b8 Mon Sep 17 00:00:00 2001 From: Gyandeep Singh Date: Mon, 23 Nov 2015 13:30:12 -0600 Subject: [PATCH 49/63] Fix: `brace-style` ASI fix for if-else condition (fixes #4520) --- lib/rules/brace-style.js | 4 +++- tests/lib/rules/brace-style.js | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/rules/brace-style.js b/lib/rules/brace-style.js index 42d10db2d176..a64d0c7811e1 100644 --- a/lib/rules/brace-style.js +++ b/lib/rules/brace-style.js @@ -109,7 +109,9 @@ module.exports = function(context) { tokens = context.getTokensBefore(node.alternate, 2); if (style === "1tbs") { - if (tokens[0].loc.start.line !== tokens[1].loc.start.line && isCurlyPunctuator(tokens[0]) ) { + if (tokens[0].loc.start.line !== tokens[1].loc.start.line && + node.consequent.type === "BlockStatement" && + isCurlyPunctuator(tokens[0]) ) { context.report(node.alternate, CLOSE_MESSAGE); } } else if (tokens[0].loc.start.line === tokens[1].loc.start.line) { diff --git a/tests/lib/rules/brace-style.js b/tests/lib/rules/brace-style.js index 03ad83bd03af..ada1bc9e0339 100644 --- a/tests/lib/rules/brace-style.js +++ b/tests/lib/rules/brace-style.js @@ -25,6 +25,14 @@ var OPEN_MESSAGE = "Opening curly brace does not appear on the same line as cont var ruleTester = new RuleTester(); ruleTester.run("brace-style", rule, { valid: [ + "function f() {\n" + + " if (true)\n" + + " return {x: 1}\n" + + " else {\n" + + " var y = 2\n" + + " return y\n" + + " }\n" + + "}", "if (tag === 1) glyph.id = pbf.readVarint();\nelse if (tag === 2) glyph.bitmap = pbf.readBytes();", "function foo () { \nreturn; \n}", "function a(b,\nc,\nd) { }", From 9fe6bd14941d2c78577913f79315895238dad003 Mon Sep 17 00:00:00 2001 From: Gyandeep Singh Date: Mon, 23 Nov 2015 14:09:41 -0600 Subject: [PATCH 50/63] Fix: `valid-jsdoc` unneeded require check fix (fixes #4527) --- lib/rules/valid-jsdoc.js | 2 +- tests/lib/rules/valid-jsdoc.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/rules/valid-jsdoc.js b/lib/rules/valid-jsdoc.js index 63d896e1f12c..2832a8a5d064 100644 --- a/lib/rules/valid-jsdoc.js +++ b/lib/rules/valid-jsdoc.js @@ -142,7 +142,7 @@ module.exports = function(context) { case "returns": hasReturns = true; - if (!requireReturn && !functionData.returnPresent && tag.type.name !== "void" && tag.type.name !== "undefined") { + if (!requireReturn && !functionData.returnPresent && (tag.type === null || !isValidReturnType(tag))) { context.report(jsdocNode, "Unexpected @" + tag.title + " tag; function has no return statement."); } else { if (requireReturnType && !tag.type) { diff --git a/tests/lib/rules/valid-jsdoc.js b/tests/lib/rules/valid-jsdoc.js index 38a535d9745f..9ec5cd2f55bd 100644 --- a/tests/lib/rules/valid-jsdoc.js +++ b/tests/lib/rules/valid-jsdoc.js @@ -441,6 +441,17 @@ ruleTester.run("valid-jsdoc", rule, { type: "Block" }] }, + { + code: "/** Foo \n@return sdf\n */\nfunction foo(){}", + options: [{ + prefer: { "return": "return" }, + requireReturn: false + }], + errors: [{ + message: "Unexpected @return tag; function has no return statement.", + type: "Block" + }] + }, // classes { code: From 5fd685ac0af98504bf5cbdb0bddaa9deec89e4e2 Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Tue, 24 Nov 2015 21:09:00 -0500 Subject: [PATCH 51/63] Fix: Add for-in to `curly` rule (fixes #4436) --- lib/rules/curly.js | 4 ++ tests/lib/rules/curly.js | 85 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/lib/rules/curly.js b/lib/rules/curly.js index af29ce6fb4ff..1478785e5dea 100644 --- a/lib/rules/curly.js +++ b/lib/rules/curly.js @@ -247,6 +247,10 @@ module.exports = function(context) { "ForStatement": function(node) { prepareCheck(node, node.body, "for", "condition").check(); + }, + + "ForInStatement": function(node) { + prepareCheck(node, node.body, "for-in").check(); } }; diff --git a/tests/lib/rules/curly.js b/tests/lib/rules/curly.js index 89177572fa49..bbef4f532003 100644 --- a/tests/lib/rules/curly.js +++ b/tests/lib/rules/curly.js @@ -24,6 +24,7 @@ ruleTester.run("curly", rule, { "while (foo) { bar() }", "do { bar(); } while (foo)", "for (;foo;) { bar() }", + "for (var foo in bar) { console.log(foo) }", { code: "for (;foo;) bar()", options: ["multi"] @@ -36,6 +37,14 @@ ruleTester.run("curly", rule, { code: "if (a) { b; c; }", options: ["multi"] }, + { + code: "for (var foo in bar) console.log(foo)", + options: ["multi"] + }, + { + code: "for (var foo in bar) { console.log(1); console.log(2) }", + options: ["multi"] + }, { code: "if (foo) bar()", options: ["multi-line"] @@ -64,6 +73,14 @@ ruleTester.run("curly", rule, { code: "if (foo) { bar() }", options: ["multi-line"] }, + { + code: "for (var foo in bar) console.log(foo)", + options: ["multi-line"] + }, + { + code: "for (var foo in bar) { \n console.log(1); \n console.log(2); \n }", + options: ["multi-line"] + }, { code: "if (foo) { \n bar(); \n baz(); \n }", options: ["multi-line"] @@ -96,6 +113,14 @@ ruleTester.run("curly", rule, { code: "if (foo) { \n if(bar) \n doSomething(); \n } else \n doSomethingElse();", options: ["multi-or-nest"] }, + { + code: "for (var foo in bar) \n console.log(foo)", + options: ["multi-or-nest"] + }, + { + code: "for (var foo in bar) { \n if (foo) console.log(1); \n else console.log(2) \n }", + options: ["multi-or-nest"] + }, // https://github.com/eslint/eslint/issues/3856 { @@ -195,6 +220,15 @@ ruleTester.run("curly", rule, { } ] }, + { + code: "for (var foo in bar) console.log(foo)", + errors: [ + { + message: "Expected { after 'for-in'.", + type: "ForInStatement" + } + ] + }, { code: "for (;foo;) { bar() }", options: ["multi"], @@ -279,6 +313,16 @@ ruleTester.run("curly", rule, { } ] }, + { + code: "for (var foo in bar) { console.log(foo) }", + options: ["multi"], + errors: [ + { + message: "Unnecessary { after 'for-in'.", + type: "ForInStatement" + } + ] + }, { code: "if (foo) \n baz()", options: ["multi-line"], @@ -339,6 +383,27 @@ ruleTester.run("curly", rule, { } ] }, + { + code: "for (var foo in bar) \n console.log(foo)", + options: ["multi-line"], + errors: [ + { + message: "Expected { after 'for-in'.", + type: "ForInStatement" + } + ] + }, + { + code: "for (var foo in bar) \n console.log(1); \n console.log(2)", + options: ["multi-line"], + errors: [ + { + message: "Expected { after 'for-in'.", + type: "ForInStatement" + } + ] + }, + { code: "if (foo) \n quz = { \n bar: baz, \n qux: foo \n };", options: ["multi-or-nest"], @@ -389,6 +454,26 @@ ruleTester.run("curly", rule, { } ] }, + { + code: "for (var foo in bar) \n if (foo) console.log(1); \n else console.log(2);", + options: ["multi-or-nest"], + errors: [ + { + message: "Expected { after 'for-in'.", + type: "ForInStatement" + } + ] + }, + { + code: "for (var foo in bar) { if (foo) console.log(1) }", + options: ["multi-or-nest"], + errors: [ + { + message: "Unnecessary { after 'for-in'.", + type: "ForInStatement" + } + ] + }, { code: "if (true) foo(); \n else { \n bar(); \n baz(); \n }", options: ["multi", "consistent"], From 03ff54f5ee7f2cd39a1669bf0ccf05e6f3cf193d Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Wed, 25 Nov 2015 14:06:46 -0600 Subject: [PATCH 52/63] Fix: Bugfix for eqeqeq autofix (fixes #4540) Finding range for ==/!= instead of assuming it is next token --- lib/rules/eqeqeq.js | 13 ++++++++++++- tests/lib/rules/eqeqeq.js | 10 +++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/rules/eqeqeq.js b/lib/rules/eqeqeq.js index 5c34346cb95a..f4ef2e5ed7a8 100644 --- a/lib/rules/eqeqeq.js +++ b/lib/rules/eqeqeq.js @@ -93,7 +93,18 @@ module.exports = function(context) { message: "Expected '{{op}}=' and instead saw '{{op}}'.", data: { op: node.operator }, fix: function(fixer) { - return fixer.replaceText(sourceCode.getTokenAfter(node.left), replacements[node.operator]); + var tokens = sourceCode.getTokensBetween(node.left, node.right), + opToken, + i; + + for (i = 0; i < tokens.length; ++i) { + if (tokens[i].value === node.operator) { + opToken = tokens[i]; + break; + } + } + + return fixer.replaceTextRange(opToken.range, replacements[node.operator]); } }); diff --git a/tests/lib/rules/eqeqeq.js b/tests/lib/rules/eqeqeq.js index 1cc834d39502..114b1018cd13 100644 --- a/tests/lib/rules/eqeqeq.js +++ b/tests/lib/rules/eqeqeq.js @@ -51,6 +51,14 @@ ruleTester.run("eqeqeq", rule, { { code: "'hello' != 'world'", output: "'hello' !== 'world'", options: ["allow-null"], errors: [{ message: "Expected '!==' and instead saw '!='.", type: "BinaryExpression"}] }, { code: "2 == 3", output: "2 === 3", options: ["allow-null"], errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression"}] }, { code: "true == true", output: "true === true", options: ["allow-null"], errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression"}] }, - { code: "a\n==\nb", output: "a\n===\nb", errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression", line: 2 }] } + { code: "a\n==\nb", output: "a\n===\nb", errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression", line: 2 }] }, + { code: "(a) == b", output: "(a) === b", errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression", line: 1 }] }, + { code: "(a) != b", output: "(a) !== b", errors: [{ message: "Expected '!==' and instead saw '!='.", type: "BinaryExpression", line: 1 }] }, + { code: "a == (b)", output: "a === (b)", errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression", line: 1 }] }, + { code: "a != (b)", output: "a !== (b)", errors: [{ message: "Expected '!==' and instead saw '!='.", type: "BinaryExpression", line: 1 }] }, + { code: "(a) == (b)", output: "(a) === (b)", errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression", line: 1 }] }, + { code: "(a) != (b)", output: "(a) !== (b)", errors: [{ message: "Expected '!==' and instead saw '!='.", type: "BinaryExpression", line: 1 }] }, + { code: "(a == b) == (c)", output: "(a === b) === (c)", errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression", line: 1 }, { message: "Expected '===' and instead saw '=='.", type: "BinaryExpression", line: 1 }] }, + { code: "(a != b) != (c)", output: "(a !== b) !== (c)", errors: [{ message: "Expected '!==' and instead saw '!='.", type: "BinaryExpression", line: 1 }, { message: "Expected '!==' and instead saw '!='.", type: "BinaryExpression", line: 1 }] } ] }); From d0683f368a23ffd93eb48d50421a5bc0461e013c Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Wed, 25 Nov 2015 14:57:43 -0600 Subject: [PATCH 53/63] Upgrade: doctrine@0.7.1 (fixes #4545) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 47eda9d4fcd2..e38e4ef491e8 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "chalk": "^1.0.0", "concat-stream": "^1.4.6", "debug": "^2.1.1", - "doctrine": "^0.7.0", + "doctrine": "^0.7.1", "escape-string-regexp": "^1.0.2", "escope": "^3.2.1", "espree": "^2.2.4", From 30cd6b0658cee27bf5a0883abc1133c631a1b7e1 Mon Sep 17 00:00:00 2001 From: alberto Date: Thu, 26 Nov 2015 21:01:40 +0100 Subject: [PATCH 54/63] Fix: lines-around-comment with multiple comments (fixes #3509) --- lib/rules/lines-around-comment.js | 28 ++++++++++++++++--------- tests/lib/rules/lines-around-comment.js | 10 +++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/rules/lines-around-comment.js b/lib/rules/lines-around-comment.js index 0381af096ce6..e2416fc59ead 100644 --- a/lib/rules/lines-around-comment.js +++ b/lib/rules/lines-around-comment.js @@ -75,26 +75,34 @@ module.exports = function(context) { options.allowBlockStart = options.allowBlockStart || false; options.allowBlockEnd = options.allowBlockEnd || false; + var sourceCode = context.getSourceCode(); /** - * Returns whether or not comments are not on lines starting with or ending with code + * Returns whether or not comments are on lines starting with or ending with code * @param {ASTNode} node The comment node to check. * @returns {boolean} True if the comment is not alone. */ function codeAroundComment(node) { + var token; - var lines = context.getSourceLines(); + token = node; + do { + token = sourceCode.getTokenOrCommentBefore(token); + } while (token && (token.type === "Block" || token.type === "Line")); - // Get the whole line and cut it off at the start of the comment - var startLine = lines[node.loc.start.line - 1]; - var endLine = lines[node.loc.end.line - 1]; + if (token && token.loc.end.line === node.loc.start.line) { + return true; + } - var preamble = startLine.slice(0, node.loc.start.column).trim(); + token = node; + do { + token = sourceCode.getTokenOrCommentAfter(token); + } while (token && (token.type === "Block" || token.type === "Line")); - // Also check after the comment - var postamble = endLine.slice(node.loc.end.column).trim(); + if (token && token.loc.start.line === node.loc.end.line) { + return true; + } - // Should be false if there was only whitespace around the comment - return !!(preamble || postamble); + return false; } /** diff --git a/tests/lib/rules/lines-around-comment.js b/tests/lib/rules/lines-around-comment.js index 8a220c6694ba..8d922dbc934d 100644 --- a/tests/lib/rules/lines-around-comment.js +++ b/tests/lib/rules/lines-around-comment.js @@ -723,6 +723,16 @@ ruleTester.run("lines-around-comment", rule, { options: [{ afterBlockComment: true, beforeBlockComment: true }], errors: [{ message: beforeMessage, type: "Block", line: 2 }, { message: afterMessage, type: "Block", line: 2 }] }, + { + code: "bar()\n/* first block comment */ /* second block comment */\nvar a = 1;", + options: [{ afterBlockComment: true, beforeBlockComment: true }], + errors: [ + { message: beforeMessage, type: "Block", line: 2 }, + { message: afterMessage, type: "Block", line: 2 }, + { message: beforeMessage, type: "Block", line: 2 }, + { message: afterMessage, type: "Block", line: 2 } + ] + }, { code: "bar()\n/**\n * block block block\n */\nvar a = 1;", options: [{ afterBlockComment: true, beforeBlockComment: false }], From 76d104d22239ec3562a68e20a7bb0bcf49909c54 Mon Sep 17 00:00:00 2001 From: alberto Date: Thu, 26 Nov 2015 22:00:51 +0100 Subject: [PATCH 55/63] Upgrade: Pinned down js-yaml to avoid breaking dep (fixes #4553) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e38e4ef491e8..c053b9d29aaf 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "inquirer": "^0.11.0", "is-my-json-valid": "^2.10.0", "is-resolvable": "^1.0.0", - "js-yaml": "^3.2.5", + "js-yaml": "3.4.5", "json-stable-stringify": "^1.0.0", "lodash.clonedeep": "^3.0.1", "lodash.merge": "^3.3.2", From 0f5b86f99cec4b920ab3f7262006decaa95cfede Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 27 Nov 2015 10:29:46 -0800 Subject: [PATCH 56/63] Upgrade: escope@3.3.0 (refs #4485) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c053b9d29aaf..21331d687991 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "debug": "^2.1.1", "doctrine": "^0.7.1", "escape-string-regexp": "^1.0.2", - "escope": "^3.2.1", + "escope": "^3.3.0", "espree": "^2.2.4", "estraverse": "^4.1.1", "estraverse-fb": "^1.3.1", From 931e0a298c7d5c4caabb8407f1c2ee8714115fa6 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 27 Nov 2015 10:31:20 -0800 Subject: [PATCH 57/63] 1.10.2 --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb7483c32b3..07a46e08cae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +v1.10.2 - November 27, 2015 + +* Upgrade: escope@3.3.0 (refs #4485) (Nicholas C. Zakas) +* Upgrade: Pinned down js-yaml to avoid breaking dep (fixes #4553) (alberto) +* Fix: lines-around-comment with multiple comments (fixes #3509) (alberto) +* Upgrade: doctrine@0.7.1 (fixes #4545) (Kevin Partington) +* Fix: Bugfix for eqeqeq autofix (fixes #4540) (Kevin Partington) +* Fix: Add for-in to `curly` rule (fixes #4436) (Kai Cataldo) +* Fix: `valid-jsdoc` unneeded require check fix (fixes #4527) (Gyandeep Singh) +* Fix: `brace-style` ASI fix for if-else condition (fixes #4520) (Gyandeep Singh) +* Build: Add branch update during release process (fixes #4491) (Gyandeep Singh) +* Build: Allow revert commits in commit messages (fixes #4452) (alberto) +* Fix: Incorrect location in no-fallthrough (fixes #4516) (alberto) +* Fix: `no-spaced-func` had been crashed (fixes #4508) (Toru Nagashima) +* Fix: Add a RestProperty test of `no-undef` (fixes #3271) (Toru Nagashima) +* Docs: Load badge from HTTPS (Brian J Brennan) +* Build: Update eslint bot messages (fixes #4497) (Nicholas C. Zakas) + v1.10.1 - November 20, 2015 * Fix: Revert freezing context object (refs #4495) (Nicholas C. Zakas) diff --git a/package.json b/package.json index 21331d687991..3a567ceb3087 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "1.10.1", + "version": "1.10.2", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From 95d2e0bb712eb347cfd984f50e1ec3662fa7fe94 Mon Sep 17 00:00:00 2001 From: alberto Date: Sun, 29 Nov 2015 22:27:19 +0100 Subject: [PATCH 58/63] Fix: Ignore space before function in array start (fixes #4569) --- lib/rules/space-before-keywords.js | 2 +- tests/lib/rules/space-before-keywords.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/rules/space-before-keywords.js b/lib/rules/space-before-keywords.js index 98700a12d612..cba8cffa5fd1 100644 --- a/lib/rules/space-before-keywords.js +++ b/lib/rules/space-before-keywords.js @@ -188,7 +188,7 @@ module.exports = function(context) { return; } - checkTokens(node, left, right, { allowedPrecedingChars: [ "(", "{" ] }); + checkTokens(node, left, right, { allowedPrecedingChars: [ "(", "{", "[" ] }); }, "YieldExpression": function(node) { check(node, { allowedPrecedingChars: [ "(", "{" ] }); diff --git a/tests/lib/rules/space-before-keywords.js b/tests/lib/rules/space-before-keywords.js index c033ffba3cfe..19653151ca12 100644 --- a/tests/lib/rules/space-before-keywords.js +++ b/tests/lib/rules/space-before-keywords.js @@ -133,6 +133,7 @@ ruleTester.run("space-before-keywords", rule, { { code: "; function foo () {}", options: never }, { code: ";\nfunction foo () {}", options: never }, // FunctionExpression + { code: "[function () {}]" }, { code: "var foo = function bar () {}" }, { code: "var foo =\nfunction bar () {}" }, { code: "function foo () { return function () {} }" }, From f01840d940e2ea0ff8b5a3deb89b21c2f205d71b Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Sun, 29 Nov 2015 20:53:08 -0600 Subject: [PATCH 59/63] Fix: Add for-of to `curly` rule (fixes #4571) --- lib/rules/curly.js | 5 +- tests/lib/rules/curly.js | 101 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/lib/rules/curly.js b/lib/rules/curly.js index 1478785e5dea..dacd9c5ec5e1 100644 --- a/lib/rules/curly.js +++ b/lib/rules/curly.js @@ -251,9 +251,12 @@ module.exports = function(context) { "ForInStatement": function(node) { prepareCheck(node, node.body, "for-in").check(); + }, + + "ForOfStatement": function(node) { + prepareCheck(node, node.body, "for-of").check(); } }; - }; module.exports.schema = { diff --git a/tests/lib/rules/curly.js b/tests/lib/rules/curly.js index bbef4f532003..38a50e0c7a45 100644 --- a/tests/lib/rules/curly.js +++ b/tests/lib/rules/curly.js @@ -25,6 +25,10 @@ ruleTester.run("curly", rule, { "do { bar(); } while (foo)", "for (;foo;) { bar() }", "for (var foo in bar) { console.log(foo) }", + { + code: "for (var foo of bar) { console.log(foo) }", + ecmaFeatures: { forOf: true } + }, { code: "for (;foo;) bar()", options: ["multi"] @@ -45,6 +49,16 @@ ruleTester.run("curly", rule, { code: "for (var foo in bar) { console.log(1); console.log(2) }", options: ["multi"] }, + { + code: "for (var foo of bar) console.log(foo)", + options: ["multi"], + ecmaFeatures: { forOf: true } + }, + { + code: "for (var foo of bar) { console.log(1); console.log(2) }", + options: ["multi"], + ecmaFeatures: { forOf: true } + }, { code: "if (foo) bar()", options: ["multi-line"] @@ -81,6 +95,16 @@ ruleTester.run("curly", rule, { code: "for (var foo in bar) { \n console.log(1); \n console.log(2); \n }", options: ["multi-line"] }, + { + code: "for (var foo of bar) console.log(foo)", + options: ["multi-line"], + ecmaFeatures: { forOf: true } + }, + { + code: "for (var foo of bar) { \n console.log(1); \n console.log(2); \n }", + options: ["multi-line"], + ecmaFeatures: { forOf: true } + }, { code: "if (foo) { \n bar(); \n baz(); \n }", options: ["multi-line"] @@ -121,6 +145,16 @@ ruleTester.run("curly", rule, { code: "for (var foo in bar) { \n if (foo) console.log(1); \n else console.log(2) \n }", options: ["multi-or-nest"] }, + { + code: "for (var foo of bar) \n console.log(foo)", + options: ["multi-or-nest"], + ecmaFeatures: { forOf: true } + }, + { + code: "for (var foo of bar) { \n if (foo) console.log(1); \n else console.log(2) \n }", + options: ["multi-or-nest"], + ecmaFeatures: { forOf: true } + }, // https://github.com/eslint/eslint/issues/3856 { @@ -151,7 +185,6 @@ ruleTester.run("curly", rule, { code: "if (true) foo(); else { bar(); baz(); }", options: ["multi"] }, - { code: "if (true) { foo(); } else { bar(); baz(); }", options: ["multi", "consistent"] @@ -229,6 +262,16 @@ ruleTester.run("curly", rule, { } ] }, + { + code: "for (var foo of bar) console.log(foo)", + ecmaFeatures: { forOf: true }, + errors: [ + { + message: "Expected { after 'for-of'.", + type: "ForOfStatement" + } + ] + }, { code: "for (;foo;) { bar() }", options: ["multi"], @@ -323,6 +366,17 @@ ruleTester.run("curly", rule, { } ] }, + { + code: "for (var foo of bar) { console.log(foo) }", + options: ["multi"], + ecmaFeatures: { forOf: true }, + errors: [ + { + message: "Unnecessary { after 'for-of'.", + type: "ForOfStatement" + } + ] + }, { code: "if (foo) \n baz()", options: ["multi-line"], @@ -403,7 +457,28 @@ ruleTester.run("curly", rule, { } ] }, - + { + code: "for (var foo of bar) \n console.log(foo)", + options: ["multi-line"], + ecmaFeatures: { forOf: true }, + errors: [ + { + message: "Expected { after 'for-of'.", + type: "ForOfStatement" + } + ] + }, + { + code: "for (var foo of bar) \n console.log(1); \n console.log(2)", + options: ["multi-line"], + ecmaFeatures: { forOf: true }, + errors: [ + { + message: "Expected { after 'for-of'.", + type: "ForOfStatement" + } + ] + }, { code: "if (foo) \n quz = { \n bar: baz, \n qux: foo \n };", options: ["multi-or-nest"], @@ -474,6 +549,28 @@ ruleTester.run("curly", rule, { } ] }, + { + code: "for (var foo of bar) \n if (foo) console.log(1); \n else console.log(2);", + options: ["multi-or-nest"], + ecmaFeatures: { forOf: true }, + errors: [ + { + message: "Expected { after 'for-of'.", + type: "ForOfStatement" + } + ] + }, + { + code: "for (var foo of bar) { if (foo) console.log(1) }", + options: ["multi-or-nest"], + ecmaFeatures: { forOf: true }, + errors: [ + { + message: "Unnecessary { after 'for-of'.", + type: "ForOfStatement" + } + ] + }, { code: "if (true) foo(); \n else { \n bar(); \n baz(); \n }", options: ["multi", "consistent"], From 6189ebb67216daccf02d4b3cfcb2492a4541b3ac Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Mon, 30 Nov 2015 11:04:14 -0500 Subject: [PATCH 60/63] Docs: Reference .eslintrc.* in contributing docs (fixes #4532) --- docs/user-guide/configuring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index be43c22284ea..2d948db97734 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -3,7 +3,7 @@ ESLint is designed to be completely configurable, meaning you can turn off every rule and run only with basic syntax validation, or mix and match the bundled rules and your custom rules to make ESLint perfect for your project. There are two primary ways to configure ESLint: 1. **Configuration Comments** - use JavaScript comments to embed configuration information directly into a file. -1. **Configuration Files** - use a JavaScript, JSON or YAML file to specify configuration information for an entire directory and all of its subdirectories. This can be in the form of an `.eslintrc` file or an `eslintConfig` field in a `package.json` file, both of which ESLint will look for and read automatically, or you can specify a configuration file on the [command line](command-line-interface). +1. **Configuration Files** - use a JavaScript, JSON or YAML file to specify configuration information for an entire directory and all of its subdirectories. This can be in the form of an [.eslintrc.*](#configuration-file-formats) file or an `eslintConfig` field in a `package.json` file, both of which ESLint will look for and read automatically, or you can specify a configuration file on the [command line](command-line-interface). There are several pieces of information that can be configured: From 4e163a7da06f4f053ccc5bafdaafc44c47ce5653 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 1 Dec 2015 11:20:41 -0800 Subject: [PATCH 61/63] Docs: Update strict rule docs (fixes #4583) --- docs/rules/strict.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/rules/strict.md b/docs/rules/strict.md index 1a5aefa3e05f..19817e767e11 100644 --- a/docs/rules/strict.md +++ b/docs/rules/strict.md @@ -22,7 +22,15 @@ var bar = function() { ## Rule Details -This rule is aimed at controlling how Use Strict Directives are used in code. It has three modes, each enabled by a single string argument: +This rule is aimed at using strict directives effectively, and as such, will flag any unexpected uses or omissions of strict directives. + +### Options + +There are three options for this rule: + +1. `never` - don't use `"use strict"` at all +1. `global` - require `"use strict"` in the global scope +1. `function` - require `"use strict"` in function scopes only ### "never" mode From 2436cc6c1816a7890e35dab38e609daee84d7530 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 1 Dec 2015 12:15:26 -0800 Subject: [PATCH 62/63] 1.10.3 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07a46e08cae1..59fc0173219b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +v1.10.3 - December 1, 2015 + +* Docs: Update strict rule docs (fixes #4583) (Nicholas C. Zakas) +* Docs: Reference .eslintrc.* in contributing docs (fixes #4532) (Kai Cataldo) +* Fix: Add for-of to `curly` rule (fixes #4571) (Kai Cataldo) +* Fix: Ignore space before function in array start (fixes #4569) (alberto) + v1.10.2 - November 27, 2015 * Upgrade: escope@3.3.0 (refs #4485) (Nicholas C. Zakas) diff --git a/package.json b/package.json index 3a567ceb3087..b3fb701e5a2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "1.10.2", + "version": "1.10.3", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From fd49900c20691ecceaf962f03d9b0929ec0921ce Mon Sep 17 00:00:00 2001 From: ABaldwinHunter Date: Thu, 10 Dec 2015 12:23:38 -0500 Subject: [PATCH 63/63] Permit airbnb config packages only --- lib/config/config-file.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/config/config-file.js b/lib/config/config-file.js index aaffb6fba3ce..8daabf3037b1 100644 --- a/lib/config/config-file.js +++ b/lib/config/config-file.js @@ -165,7 +165,11 @@ function loadPackageJSONConfigFile(filePath) { function loadPackage(filePath) { debug("Loading config package: " + filePath); try { - return require(filePath); + if (filePath.match(/^eslint-config-airbnb.*/)) { + return require(filePath); + } else { + return {}; + } } catch (e) { debug("Error reading package: " + filePath); e.message = "Cannot read config package: " + filePath + "\nError: " + e.message;