testParsers = require("../../fixtures/parsers/linter-test-parsers");
const { Linter } = require("../../../lib/linter");
+const { FlatConfigArray } = require("../../../lib/config/flat-config-array");
//------------------------------------------------------------------------------
// Constants
linter.defineRule("checker", () => ({ Program: spy }));
linter.verify("foo", { rules: { checker: "error" } });
- assert(spy.calledOnce);
- assert.strictEqual(spy.firstCall.thisValue, void 0);
+ assert(spy.calledOnce, "Rule should have been called");
+ assert.strictEqual(spy.firstCall.thisValue, void 0, "this value should be undefined");
});
it("does not allow listeners to use special EventEmitter values", () => {
assert.strictEqual(messages[1].ruleId, "no-console");
});
- it("should not ignore violations if comment is of the type Shebang", () => {
+ it("should not ignore violations if comment is of the type hashbang", () => {
const code = [
"#! eslint-disable-next-line no-alert",
"alert('test');",
});
});
- describe("when evaluating a file with a shebang", () => {
- const code = "#!bin/program\n\nvar foo;;";
+ describe("when evaluating a file with a hashbang", () => {
it("should preserve line numbers", () => {
+ const code = "#!bin/program\n\nvar foo;;";
const config = { rules: { "no-extra-semi": 1 } };
const messages = linter.verify(code, config);
assert.strictEqual(messages[0].line, 3);
});
- it("should have a comment with the shebang in it", () => {
+ it("should have a comment with the hashbang in it", () => {
+ const code = "#!bin/program\n\nvar foo;;";
const config = { rules: { checker: "error" } };
const spy = sinon.spy(context => {
const comments = context.getAllComments();
linter.verify(code, config);
assert(spy.calledOnce);
});
+
+ it("should comment hashbang without breaking offset", () => {
+ const code = "#!/usr/bin/env node\n'123';";
+ const config = { rules: { checker: "error" } };
+ let spy;
+
+ linter.defineRule("checker", context => {
+ spy = sinon.spy(node => {
+ assert.strictEqual(context.getSource(node), "'123';");
+ });
+ return { ExpressionStatement: spy };
+ });
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
});
describe("when evaluating broken code", () => {
linter = new Linter();
const code = TEST_CODE;
- const results = linter.verify(code, { rules: { foobar: 2 } });
- const result = results[0];
- const warningResult = linter.verify(code, { rules: { foobar: 1 } })[0];
- const arrayOptionResults = linter.verify(code, { rules: { foobar: [2, "always"] } });
- const objectOptionResults = linter.verify(code, { rules: { foobar: [1, { bar: false }] } });
- const resultsMultiple = linter.verify(code, { rules: { foobar: 2, barfoo: 1 } });
+ let results, result, warningResult, arrayOptionResults, objectOptionResults, resultsMultiple;
+
+ beforeEach(() => {
+ results = linter.verify(code, { rules: { foobar: 2 } });
+ result = results[0];
+ warningResult = linter.verify(code, { rules: { foobar: 1 } })[0];
+ arrayOptionResults = linter.verify(code, { rules: { foobar: [2, "always"] } });
+ objectOptionResults = linter.verify(code, { rules: { foobar: [1, { bar: false }] } });
+ resultsMultiple = linter.verify(code, { rules: { foobar: 2, barfoo: 1 } });
+ });
it("should report a problem", () => {
assert.isNotNull(result);
describe("when using a rule which has been replaced", () => {
const code = TEST_CODE;
- const results = linter.verify(code, { rules: { "no-comma-dangle": 2 } });
it("should report the new rule", () => {
+ const results = linter.verify(code, { rules: { "no-comma-dangle": 2 } });
+
assert.strictEqual(results[0].ruleId, "no-comma-dangle");
assert.strictEqual(results[0].message, "Rule 'no-comma-dangle' was removed and replaced by: comma-dangle");
});
assert.strictEqual(messages.length, 0);
});
- it("the 'latest' is equal to espree.lastEcmaVersion", () => {
+ it("the 'latest' is equal to espree.latestEcmaVersion", () => {
let ecmaVersion = null;
const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } };
}
}));
linter.verify("", config);
- assert.strictEqual(ecmaVersion, espree.latestEcmaVersion);
+ assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13");
+ });
+
+ it("the 'latest' is not normalized for custom parsers", () => {
+ let ecmaVersion = null;
+ const config = { rules: { "ecma-version": 2 }, parser: "custom-parser", parserOptions: { ecmaVersion: "latest" } };
+
+ linter.defineParser("custom-parser", testParsers.enhancedParser);
+ linter.defineRule("ecma-version", context => ({
+ Program() {
+ ecmaVersion = context.parserOptions.ecmaVersion;
+ }
+ }));
+ linter.verify("", config);
+ assert.strictEqual(ecmaVersion, "latest", "ecmaVersion should be latest");
+ });
+
+ it("the 'latest' is equal to espree.latestEcmaVersion on languageOptions", () => {
+ let ecmaVersion = null;
+ const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } };
+
+ linter.defineRule("ecma-version", context => ({
+ Program() {
+ ecmaVersion = context.languageOptions.ecmaVersion;
+ }
+ }));
+ linter.verify("", config);
+ assert.strictEqual(ecmaVersion, espree.latestEcmaVersion + 2009, "ecmaVersion should be 2022");
+ });
+
+ it("the 'next' is equal to espree.latestEcmaVersion on languageOptions with custom parser", () => {
+ let ecmaVersion = null;
+ const config = { rules: { "ecma-version": 2 }, parser: "custom-parser", parserOptions: { ecmaVersion: "next" } };
+
+ linter.defineParser("custom-parser", testParsers.stubParser);
+ linter.defineRule("ecma-version", context => ({
+ Program() {
+ ecmaVersion = context.languageOptions.ecmaVersion;
+ }
+ }));
+ linter.verify("", config);
+ assert.strictEqual(ecmaVersion, espree.latestEcmaVersion + 2009, "ecmaVersion should be 2022");
+ });
+
+ it("missing ecmaVersion is equal to 5 on languageOptions with custom parser", () => {
+ let ecmaVersion = null;
+ const config = { rules: { "ecma-version": 2 }, parser: "custom-parser" };
+
+ linter.defineParser("custom-parser", testParsers.enhancedParser);
+ linter.defineRule("ecma-version", context => ({
+ Program() {
+ ecmaVersion = context.languageOptions.ecmaVersion;
+ }
+ }));
+ linter.verify("", config);
+ assert.strictEqual(ecmaVersion, 5, "ecmaVersion should be 5");
});
it("should pass normalized ecmaVersion to eslint-scope", () => {
assert.strictEqual(messages.length, 0);
});
+ it("should not allow the use of reserved words as variable names in ES3", () => {
+ const code = "var char;";
+ const messages = linter.verify(code, { parserOptions: { ecmaVersion: 3 } }, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.isTrue(messages[0].fatal);
+ assert.match(messages[0].message, /^Parsing error:.*'char'/u);
+ });
+
+ it("should not allow the use of reserved words as property names in member expressions in ES3", () => {
+ const code = "obj.char;";
+ const messages = linter.verify(code, { parserOptions: { ecmaVersion: 3 } }, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.isTrue(messages[0].fatal);
+ assert.match(messages[0].message, /^Parsing error:.*'char'/u);
+ });
+
+ it("should not allow the use of reserved words as property names in object literals in ES3", () => {
+ const code = "var obj = { char: 1 };";
+ const messages = linter.verify(code, { parserOptions: { ecmaVersion: 3 } }, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.isTrue(messages[0].fatal);
+ assert.match(messages[0].message, /^Parsing error:.*'char'/u);
+ });
+
+ it("should allow the use of reserved words as variable and property names in ES3 when allowReserved is true", () => {
+ const code = "var char; obj.char; var obj = { char: 1 };";
+ const messages = linter.verify(code, { parserOptions: { ecmaVersion: 3, allowReserved: true } }, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not allow the use of reserved words as variable names in ES > 3", () => {
+ const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)];
+
+ ecmaVersions.forEach(ecmaVersion => {
+ const code = "var enum;";
+ const messages = linter.verify(code, { parserOptions: { ecmaVersion } }, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.isTrue(messages[0].fatal);
+ assert.match(messages[0].message, /^Parsing error:.*'enum'/u);
+ });
+ });
+
+ it("should allow the use of reserved words as property names in ES > 3", () => {
+ const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)];
+
+ ecmaVersions.forEach(ecmaVersion => {
+ const code = "obj.enum; obj.function; var obj = { enum: 1, function: 2 };";
+ const messages = linter.verify(code, { parserOptions: { ecmaVersion } }, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+ });
+
+ it("should not allow `allowReserved: true` in ES > 3", () => {
+ const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)];
+
+ ecmaVersions.forEach(ecmaVersion => {
+ const code = "";
+ const messages = linter.verify(code, { parserOptions: { ecmaVersion, allowReserved: true } }, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.isTrue(messages[0].fatal);
+ assert.match(messages[0].message, /^Parsing error:.*allowReserved/u);
+ });
+ });
+
it("should be able to use es6 features if there is a comment which has \"eslint-env es6\"", () => {
const code = [
"/* eslint-env es6 */",
});
});
});
+
+describe("Linter with FlatConfigArray", () => {
+
+ let linter;
+ const filename = "filename.js";
+
+ /**
+ * Creates a config array with some default properties.
+ * @param {FlatConfig|FlatConfig[]} value The value to base the
+ * config array on.
+ * @returns {FlatConfigArray} The created config array.
+ */
+ function createFlatConfigArray(value) {
+ return new FlatConfigArray(value, { basePath: "" });
+ }
+
+ beforeEach(() => {
+ linter = new Linter({ configType: "flat" });
+ });
+
+ describe("Static Members", () => {
+ describe("version", () => {
+ it("should return same version as instance property", () => {
+ assert.strictEqual(Linter.version, linter.version);
+ });
+ });
+ });
+
+ describe("Config Options", () => {
+
+ describe("languageOptions", () => {
+
+ describe("ecmaVersion", () => {
+
+ it("should error when accessing a global that isn't available in ecmaVersion 5", () => {
+ const messages = linter.verify("new Map()", {
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: "script"
+ },
+ rules: {
+ "no-undef": "error"
+ }
+ });
+
+ assert.strictEqual(messages.length, 1, "There should be one linting error.");
+ assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef.");
+ });
+
+ it("should error when accessing a global that isn't available in ecmaVersion 3", () => {
+ const messages = linter.verify("JSON.stringify({})", {
+ languageOptions: {
+ ecmaVersion: 3,
+ sourceType: "script"
+ },
+ rules: {
+ "no-undef": "error"
+ }
+ });
+
+ assert.strictEqual(messages.length, 1, "There should be one linting error.");
+ assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef.");
+ });
+
+ it("should add globals for ES6 when ecmaVersion is 6", () => {
+ const messages = linter.verify("new Map()", {
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: {
+ "no-undef": "error"
+ }
+ });
+
+ assert.strictEqual(messages.length, 0, "There should be no linting errors.");
+ });
+
+ it("should allow destructuring when ecmaVersion is 6", () => {
+ const messages = linter.verify("let {a} = b", {
+ languageOptions: {
+ ecmaVersion: 6
+ }
+ });
+
+ assert.strictEqual(messages.length, 0, "There should be no linting errors.");
+ });
+
+ it("ecmaVersion should be normalized to year name for ES 6", () => {
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker(context) {
+ return {
+ Program() {
+ assert.strictEqual(context.languageOptions.ecmaVersion, 2015);
+ }
+ };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("foo", config, filename);
+ });
+
+ it("ecmaVersion should be normalized to latest year by default", () => {
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker(context) {
+ return {
+ Program() {
+ assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009);
+ }
+ };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("foo", config, filename);
+ });
+
+ it("ecmaVersion should not be normalized to year name for ES 5", () => {
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker(context) {
+ return {
+ Program() {
+ assert.strictEqual(context.languageOptions.ecmaVersion, 5);
+ }
+ };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 5
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("foo", config, filename);
+ });
+
+ it("ecmaVersion should be normalized to year name for 'latest'", () => {
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker(context) {
+ return {
+ Program() {
+ assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009);
+ }
+ };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: "latest"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("foo", config, filename);
+ });
+
+
+ });
+
+ describe("sourceType", () => {
+
+ it("should be module by default", () => {
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker(context) {
+ return {
+ Program() {
+ assert.strictEqual(context.languageOptions.sourceType, "module");
+ }
+ };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("import foo from 'bar'", config, filename);
+ });
+
+ it("should default to commonjs when passed a .cjs filename", () => {
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker(context) {
+ return {
+ Program() {
+ assert.strictEqual(context.languageOptions.sourceType, "commonjs");
+ }
+ };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("import foo from 'bar'", config, `${filename}.cjs`);
+ });
+
+
+ it("should error when import is used in a script", () => {
+ const messages = linter.verify("import foo from 'bar';", {
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "script"
+ }
+ });
+
+ assert.strictEqual(messages.length, 1, "There should be one parsing error.");
+ assert.strictEqual(messages[0].message, "Parsing error: 'import' and 'export' may appear only with 'sourceType: module'");
+ });
+
+ it("should not error when import is used in a module", () => {
+ const messages = linter.verify("import foo from 'bar';", {
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "module"
+ }
+ });
+
+ assert.strictEqual(messages.length, 0, "There should no linting errors.");
+ });
+
+ it("should error when return is used at the top-level outside of commonjs", () => {
+ const messages = linter.verify("return", {
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "script"
+ }
+ });
+
+ assert.strictEqual(messages.length, 1, "There should be one parsing error.");
+ assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function");
+ });
+
+ it("should not error when top-level return is used in commonjs", () => {
+ const messages = linter.verify("return", {
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "commonjs"
+ }
+ });
+
+ assert.strictEqual(messages.length, 0, "There should no linting errors.");
+ });
+
+ it("should error when accessing a Node.js global outside of commonjs", () => {
+ const messages = linter.verify("require()", {
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: {
+ "no-undef": "error"
+ }
+ });
+
+ assert.strictEqual(messages.length, 1, "There should be one linting error.");
+ assert.strictEqual(messages[0].ruleId, "no-undef", "The linting error should be no-undef.");
+ });
+
+ it("should add globals for Node.js when sourceType is commonjs", () => {
+ const messages = linter.verify("require()", {
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "commonjs"
+ },
+ rules: {
+ "no-undef": "error"
+ }
+ });
+
+ assert.strictEqual(messages.length, 0, "There should be no linting errors.");
+ });
+
+ it("should allow 'await' as a property name in modules", () => {
+ const result = linter.verify(
+ "obj.await",
+ {
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "module"
+ }
+ }
+ );
+
+ assert(result.length === 0);
+ });
+
+ });
+
+ describe("parser", () => {
+
+ it("should be able to define a custom parser", () => {
+ const parser = {
+ parseForESLint: function parse(code, options) {
+ return {
+ ast: esprima.parse(code, options),
+ services: {
+ test: {
+ getMessage() {
+ return "Hi!";
+ }
+ }
+ }
+ };
+ }
+ };
+
+ const config = {
+ plugins: {
+ test: {
+ parsers: {
+ "test-parser": parser
+ }
+ }
+ },
+ languageOptions: {
+ parser: "test/test-parser"
+ }
+ };
+
+
+ const messages = linter.verify("0", config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should pass parser as context.languageOptions.parser to all rules when provided on config", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "test-rule": sinon.mock().withArgs(
+ sinon.match({ languageOptions: { parser: esprima } })
+ ).returns({})
+ }
+ }
+ },
+ languageOptions: {
+ parser: esprima
+ },
+ rules: {
+ "test/test-rule": 2
+ }
+ };
+
+ linter.verify("0", config, filename);
+ });
+
+ it("should use parseForESLint() in custom parser when custom parser is specified", () => {
+ const config = {
+ plugins: {
+ test: {
+ parsers: {
+ "enhanced-parser": testParsers.enhancedParser
+ }
+ }
+ },
+ languageOptions: {
+ parser: "test/enhanced-parser"
+ }
+ };
+
+ const messages = linter.verify("0", config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should expose parser services when using parseForESLint() and services are specified", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ parsers: {
+ "enhanced-parser": testParsers.enhancedParser
+ },
+ rules: {
+ "test-service-rule": context => ({
+ Literal(node) {
+ context.report({
+ node,
+ message: context.parserServices.test.getMessage()
+ });
+ }
+ })
+ }
+ }
+ },
+ languageOptions: {
+ parser: "test/enhanced-parser"
+ },
+ rules: {
+ "test/test-service-rule": 2
+ }
+ };
+
+ const messages = linter.verify("0", config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].message, "Hi!");
+ });
+
+ it("should use the same parserServices if source code object is reused", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ parsers: {
+ "enhanced-parser": testParsers.enhancedParser
+ },
+ rules: {
+ "test-service-rule": context => ({
+ Literal(node) {
+ context.report({
+ node,
+ message: context.parserServices.test.getMessage()
+ });
+ }
+ })
+ }
+ }
+ },
+ languageOptions: {
+ parser: "test/enhanced-parser"
+ },
+ rules: {
+ "test/test-service-rule": 2
+ }
+ };
+
+ const messages = linter.verify("0", config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].message, "Hi!");
+
+ const messages2 = linter.verify(linter.getSourceCode(), config, filename);
+
+ assert.strictEqual(messages2.length, 1);
+ assert.strictEqual(messages2[0].message, "Hi!");
+ });
+
+ it("should pass parser as context.languageOptions.parser to all rules when default parser is used", () => {
+
+ // references to Espree get messed up in a browser context, so wrap it
+ const fakeParser = {
+ parse: espree.parse
+ };
+
+ const spy = sinon.spy(context => {
+ assert.strictEqual(context.languageOptions.parser, fakeParser);
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "test-rule": spy
+ }
+ }
+ },
+ languageOptions: {
+ parser: fakeParser
+ },
+ rules: {
+ "test/test-rule": 2
+ }
+ };
+
+ linter.verify("0", config, filename);
+ assert.isTrue(spy.calledOnce);
+ });
+
+
+ describe("Custom Parsers", () => {
+
+ const errorPrefix = "Parsing error: ";
+
+ it("should have file path passed to it", () => {
+ const code = "/* this is code */";
+ const parseSpy = sinon.spy(testParsers.stubParser, "parse");
+ const config = {
+ languageOptions: {
+ parser: testParsers.stubParser
+ }
+ };
+
+ linter.verify(code, config, filename, true);
+
+ sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename });
+ });
+
+ it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => {
+ const code = "var myDivElement = <div {...this.props} />;";
+ const config = {
+ languageOptions: {
+ parser: esprima,
+ parserOptions: {
+ jsx: true
+ }
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not throw or report errors when the custom parser returns unrecognized operators (https://github.com/eslint/eslint/issues/10475)", () => {
+ const code = "null %% 'foo'";
+ const config = {
+ languageOptions: {
+ parser: testParsers.unknownLogicalOperator
+ }
+ };
+
+ // This shouldn't throw
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not throw or report errors when the custom parser returns nested unrecognized operators (https://github.com/eslint/eslint/issues/10560)", () => {
+ const code = "foo && bar %% baz";
+ const config = {
+ languageOptions: {
+ parser: testParsers.unknownLogicalOperatorNested
+ }
+ };
+
+ // This shouldn't throw
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not throw or return errors when the custom parser returns unknown AST nodes", () => {
+ const code = "foo && bar %% baz";
+ const nodes = [];
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "collect-node-types": () => ({
+ "*"(node) {
+ nodes.push(node.type);
+ }
+ })
+ }
+ }
+ },
+ languageOptions: {
+ parser: testParsers.nonJSParser
+ },
+ rules: {
+ "test/collect-node-types": "error"
+ }
+ };
+
+ const messages = linter.verify(code, config, filename, true);
+
+ assert.strictEqual(messages.length, 0);
+ assert.isTrue(nodes.length > 0);
+ });
+
+ it("should strip leading line: prefix from parser error", () => {
+ const messages = linter.verify(";", {
+ languageOptions: {
+ parser: testParsers.lineError
+ }
+ }, "filename");
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError);
+ });
+
+ it("should not modify a parser error message without a leading line: prefix", () => {
+ const messages = linter.verify(";", {
+ languageOptions: {
+ parser: testParsers.noLineError
+ }
+ }, "filename");
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError);
+ });
+
+ describe("if a parser provides 'visitorKeys'", () => {
+ let types = [];
+ let sourceCode;
+ let scopeManager;
+ let firstChildNodes = [];
+
+ beforeEach(() => {
+ types = [];
+ firstChildNodes = [];
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "collect-node-types": () => ({
+ "*"(node) {
+ types.push(node.type);
+ }
+ }),
+ "save-scope-manager": context => {
+ scopeManager = context.getSourceCode().scopeManager;
+
+ return {};
+ },
+ "esquery-option": () => ({
+ ":first-child"(node) {
+ firstChildNodes.push(node);
+ }
+ })
+ }
+ }
+ },
+ languageOptions: {
+ parser: testParsers.enhancedParser2
+ },
+ rules: {
+ "test/collect-node-types": "error",
+ "test/save-scope-manager": "error",
+ "test/esquery-option": "error"
+ }
+ };
+
+ linter.verify("@foo class A {}", config);
+
+ sourceCode = linter.getSourceCode();
+ });
+
+ it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => {
+ assert.deepStrictEqual(
+ types,
+ ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"]
+ );
+ });
+
+ it("eslint-scope should use the visitorKeys (so 'childVisitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => {
+ assert.deepStrictEqual(
+ scopeManager.__options.childVisitorKeys.ClassDeclaration, // eslint-disable-line no-underscore-dangle -- ScopeManager API
+ ["experimentalDecorators", "id", "superClass", "body"]
+ );
+ });
+
+ it("should use the same visitorKeys if the source code object is reused", () => {
+ const types2 = [];
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "collect-node-types": () => ({
+ "*"(node) {
+ types2.push(node.type);
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ "test/collect-node-types": "error"
+ }
+ };
+
+ linter.verify(sourceCode, config);
+
+ assert.deepStrictEqual(
+ types2,
+ ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"]
+ );
+ });
+
+ it("esquery should use the visitorKeys (so 'visitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => {
+ assert.deepStrictEqual(
+ firstChildNodes,
+ [sourceCode.ast.body[0], sourceCode.ast.body[0].experimentalDecorators[0]]
+ );
+ });
+ });
+
+ describe("if a parser provides 'scope'", () => {
+ let scope = null;
+ let sourceCode = null;
+
+ beforeEach(() => {
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "save-scope1": context => ({
+ Program() {
+ scope = context.getScope();
+ }
+ })
+ }
+ }
+ },
+ languageOptions: {
+ parser: testParsers.enhancedParser3
+ },
+ rules: {
+ "test/save-scope1": "error"
+ }
+ };
+
+ linter.verify("@foo class A {}", config);
+
+ sourceCode = linter.getSourceCode();
+ });
+
+ it("should use the scope (so the global scope has the reference of '@foo')", () => {
+ assert.strictEqual(scope.references.length, 1);
+ assert.deepStrictEqual(
+ scope.references[0].identifier.name,
+ "foo"
+ );
+ });
+
+ it("should use the same scope if the source code object is reused", () => {
+ let scope2 = null;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "save-scope2": context => ({
+ Program() {
+ scope2 = context.getScope();
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ "test/save-scope2": "error"
+ }
+ };
+
+ linter.verify(sourceCode, config, "test.js");
+
+ assert(scope2 !== null);
+ assert(scope2 === scope);
+ });
+ });
+
+ it("should pass default languageOptions to the parser", () => {
+
+ const spy = sinon.spy((code, options) => espree.parse(code, options));
+
+ linter.verify(";", {
+ languageOptions: {
+ parser: {
+ parse: spy
+ }
+ }
+ }, "filename.js");
+
+ assert(spy.calledWithMatch(";", {
+ ecmaVersion: espree.latestEcmaVersion + 2009,
+ sourceType: "module"
+ }));
+ });
+ });
+
+
+ });
+
+ describe("parseOptions", () => {
+
+ it("should pass ecmaFeatures to all rules when provided on config", () => {
+
+ const parserOptions = {
+ ecmaFeatures: {
+ jsx: true
+ }
+ };
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "test-rule": sinon.mock().withArgs(
+ sinon.match({ languageOptions: { parserOptions } })
+ ).returns({})
+ }
+ }
+ },
+ languageOptions: {
+ parserOptions
+ },
+ rules: {
+ "test/test-rule": 2
+ }
+ };
+
+ linter.verify("0", config, filename);
+ });
+
+ it("should switch globalReturn to false if sourceType is module", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "test-rule": sinon.mock().withArgs(
+ sinon.match({
+ languageOptions: {
+ parserOptions: {
+ ecmaFeatures: {
+ globalReturn: false
+ }
+ }
+ }
+ })
+ ).returns({})
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "module",
+ parserOptions: {
+ ecmaFeatures: {
+ globalReturn: true
+ }
+ }
+ },
+ rules: {
+ "test/test-rule": 2
+ }
+ };
+
+ linter.verify("0", config, filename);
+ });
+
+ it("should not parse sloppy-mode code when impliedStrict is true", () => {
+
+ const messages = linter.verify("var private;", {
+ languageOptions: {
+ parserOptions: {
+ ecmaFeatures: {
+ impliedStrict: true
+ }
+ }
+ }
+ }, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].message, "Parsing error: The keyword 'private' is reserved");
+ });
+
+ it("should properly parse valid code when impliedStrict is true", () => {
+
+ const messages = linter.verify("var foo;", {
+ languageOptions: {
+ parserOptions: {
+ ecmaFeatures: {
+ impliedStrict: true
+ }
+ }
+ }
+ }, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should properly parse JSX when passed ecmaFeatures", () => {
+
+ const messages = linter.verify("var x = <div/>;", {
+ languageOptions: {
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true
+ }
+ }
+ }
+ }, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should report an error when JSX code is encountered and JSX is not enabled", () => {
+ const code = "var myDivElement = <div className=\"foo\" />;";
+ const messages = linter.verify(code, {}, "filename");
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].line, 1);
+ assert.strictEqual(messages[0].column, 20);
+ assert.strictEqual(messages[0].message, "Parsing error: Unexpected token <");
+ });
+
+ it("should not report an error when JSX code is encountered and JSX is enabled", () => {
+ const code = "var myDivElement = <div className=\"foo\" />;";
+ const messages = linter.verify(code, {
+ languageOptions: {
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true
+ }
+ }
+ }
+
+ }, "filename");
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => {
+ const code = "var myDivElement = <div {...this.props} />;";
+ const messages = linter.verify(code, {
+ languageOptions: {
+ ecmaVersion: 6,
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true
+ }
+ }
+ }
+
+ }, "filename");
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ });
+
+ });
+
+ describe("settings", () => {
+ const ruleId = "test-rule";
+
+ it("should pass settings to all rules", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ [ruleId]: context => ({
+ Literal(node) {
+ context.report(node, context.settings.info);
+ }
+ })
+ }
+ }
+ },
+ settings: {
+ info: "Hello"
+ },
+ rules: {
+ [`test/${ruleId}`]: 1
+ }
+ };
+
+ const messages = linter.verify("0", config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].message, "Hello");
+ });
+
+ it("should not have any settings if they were not passed in", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ [ruleId]: context => ({
+ Literal(node) {
+ if (Object.getOwnPropertyNames(context.settings).length !== 0) {
+ context.report(node, "Settings should be empty");
+ }
+ }
+ })
+ }
+ }
+ },
+ settings: {
+ },
+ rules: {
+ [`test/${ruleId}`]: 1
+ }
+ };
+
+ const messages = linter.verify("0", config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+ });
+
+ describe("rules", () => {
+ const code = "var answer = 6 * 7";
+
+ it("should be configurable by only setting the integer value", () => {
+ const rule = "semi",
+ config = { rules: {} };
+
+ config.rules[rule] = 1;
+
+ const messages = linter.verify(code, config, filename, true);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, rule);
+ });
+
+ it("should be configurable by only setting the string value", () => {
+ const rule = "semi",
+ config = { rules: {} };
+
+ config.rules[rule] = "warn";
+
+ const messages = linter.verify(code, config, filename, true);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 1);
+ assert.strictEqual(messages[0].ruleId, rule);
+ });
+
+ it("should be configurable by passing in values as an array", () => {
+ const rule = "semi",
+ config = { rules: {} };
+
+ config.rules[rule] = [1];
+
+ const messages = linter.verify(code, config, filename, true);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, rule);
+ });
+
+ it("should be configurable by passing in string value as an array", () => {
+ const rule = "semi",
+ config = { rules: {} };
+
+ config.rules[rule] = ["warn"];
+
+ const messages = linter.verify(code, config, filename, true);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 1);
+ assert.strictEqual(messages[0].ruleId, rule);
+ });
+
+ it("should not be configurable by setting other value", () => {
+ const rule = "semi",
+ config = { rules: {} };
+
+ config.rules[rule] = "1";
+
+ assert.throws(() => {
+ linter.verify(code, config, filename, true);
+ }, /Key "rules": Key "semi"/u);
+ });
+
+ it("should process empty config", () => {
+ const config = {};
+ const messages = linter.verify(code, config, filename, true);
+
+ assert.strictEqual(messages.length, 0);
+ });
+ });
+
+ });
+
+ describe("verify()", () => {
+
+ it("should report warnings in order by line and column when called", () => {
+
+ const code = "foo()\n alert('test')";
+ const config = { rules: { "no-mixed-spaces-and-tabs": 1, "eol-last": 1, semi: [1, "always"] } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 3);
+ assert.strictEqual(messages[0].line, 1);
+ assert.strictEqual(messages[0].column, 6);
+ assert.strictEqual(messages[1].line, 2);
+ assert.strictEqual(messages[1].column, 18);
+ assert.strictEqual(messages[2].line, 2);
+ assert.strictEqual(messages[2].column, 18);
+ });
+
+ describe("Plugins", () => {
+
+ it("should not load rule definition when rule isn't used", () => {
+
+ const spy = sinon.spy();
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ }
+ };
+
+ linter.verify("code", config, filename);
+ assert.isTrue(spy.notCalled, "Rule should not have been called");
+ });
+ });
+
+ describe("Rule Internals", () => {
+
+ const code = TEST_CODE;
+
+ it("should throw an error when an error occurs inside of a rule visitor", () => {
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: () => ({
+ Program() {
+ throw new Error("Intentional error.");
+ }
+ })
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ assert.throws(() => {
+ linter.verify(code, config, filename);
+ }, `Intentional error.\nOccurred while linting ${filename}:1\nRule: "test/checker"`);
+ });
+
+ it("should not call rule visitor with a `this` value", () => {
+ const spy = sinon.spy();
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: () => ({
+ Program: spy
+ })
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("foo", config);
+ assert(spy.calledOnce);
+ assert.strictEqual(spy.firstCall.thisValue, void 0);
+ });
+
+ it("should not call unrecognized rule visitor when present in a rule", () => {
+ const spy = sinon.spy();
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: () => ({
+ newListener: spy
+ })
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error",
+ "no-undef": "error"
+ }
+ };
+
+ linter.verify("foo", config);
+ assert(spy.notCalled);
+ });
+
+ it("should have all the `parent` properties on nodes when the rule visitors are created", () => {
+ const spy = sinon.spy(context => {
+ const ast = context.getSourceCode().ast;
+
+ assert.strictEqual(ast.body[0].parent, ast);
+ assert.strictEqual(ast.body[0].expression.parent, ast.body[0]);
+ assert.strictEqual(ast.body[0].expression.left.parent, ast.body[0].expression);
+ assert.strictEqual(ast.body[0].expression.right.parent, ast.body[0].expression);
+
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("foo + bar", config);
+ assert(spy.calledOnce);
+ });
+
+ it("events for each node type should fire", () => {
+
+ // spies for various AST node types
+ const spyLiteral = sinon.spy(),
+ spyVariableDeclarator = sinon.spy(),
+ spyVariableDeclaration = sinon.spy(),
+ spyIdentifier = sinon.spy(),
+ spyBinaryExpression = sinon.spy();
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker() {
+ return {
+ Literal: spyLiteral,
+ VariableDeclarator: spyVariableDeclarator,
+ VariableDeclaration: spyVariableDeclaration,
+ Identifier: spyIdentifier,
+ BinaryExpression: spyBinaryExpression
+ };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ const messages = linter.verify(code, config, filename, true);
+
+ assert.strictEqual(messages.length, 0);
+ sinon.assert.calledOnce(spyVariableDeclaration);
+ sinon.assert.calledOnce(spyVariableDeclarator);
+ sinon.assert.calledOnce(spyIdentifier);
+ sinon.assert.calledTwice(spyLiteral);
+ sinon.assert.calledOnce(spyBinaryExpression);
+ });
+
+ it("should throw an error if a rule reports a problem without a message", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "invalid-report"(context) {
+ return {
+ Program(node) {
+ context.report({ node });
+ }
+ };
+ }
+ }
+ }
+ },
+ rules: { "test/invalid-report": "error" }
+ };
+
+ assert.throws(
+ () => linter.verify("foo", config),
+ TypeError,
+ "Missing `message` property in report() call; add a message that describes the linting problem."
+ );
+ });
+
+
+ });
+
+ describe("Rule Context", () => {
+
+ describe("context.getFilename()", () => {
+ const ruleId = "filename-rule";
+
+ it("has access to the filename", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ [ruleId]: context => ({
+ Literal(node) {
+ context.report(node, context.getFilename());
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ [`test/${ruleId}`]: 1
+ }
+ };
+
+ const messages = linter.verify("0", config, filename);
+
+ assert.strictEqual(messages[0].message, filename);
+ });
+
+ it("defaults filename to '<input>'", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ [ruleId]: context => ({
+ Literal(node) {
+ context.report(node, context.getFilename());
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ [`test/${ruleId}`]: 1
+ }
+ };
+
+
+ const messages = linter.verify("0", config);
+
+ assert.strictEqual(messages[0].message, "<input>");
+ });
+ });
+
+ describe("context.getPhysicalFilename()", () => {
+
+ const ruleId = "filename-rule";
+
+ it("has access to the physicalFilename", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ [ruleId]: context => ({
+ Literal(node) {
+ context.report(node, context.getPhysicalFilename());
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ [`test/${ruleId}`]: 1
+ }
+ };
+
+ const messages = linter.verify("0", config, filename);
+
+ assert.strictEqual(messages[0].message, filename);
+ });
+
+ });
+
+ describe("context.getSourceLines()", () => {
+
+ it("should get proper lines when using \\n as a line break", () => {
+ const code = "a;\nb;";
+ const spy = sinon.spy(context => {
+ assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]);
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should get proper lines when using \\r\\n as a line break", () => {
+ const code = "a;\r\nb;";
+ const spy = sinon.spy(context => {
+ assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]);
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should get proper lines when using \\r as a line break", () => {
+ const code = "a;\rb;";
+ const spy = sinon.spy(context => {
+ assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]);
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should get proper lines when using \\u2028 as a line break", () => {
+ const code = "a;\u2028b;";
+ const spy = sinon.spy(context => {
+ assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]);
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should get proper lines when using \\u2029 as a line break", () => {
+ const code = "a;\u2029b;";
+ const spy = sinon.spy(context => {
+ assert.deepStrictEqual(context.getSourceLines(), ["a;", "b;"]);
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ });
+
+ describe("context.getSource()", () => {
+ const code = TEST_CODE;
+
+ it("should retrieve all text when used without parameters", () => {
+
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ assert.strictEqual(context.getSource(), TEST_CODE);
+ });
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve all text for root node", () => {
+
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(node => {
+ assert.strictEqual(context.getSource(node), TEST_CODE);
+ });
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should clamp to valid range when retrieving characters before start of source", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(node => {
+ assert.strictEqual(context.getSource(node, 2, 0), TEST_CODE);
+ });
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve all text for binary expression", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(node => {
+ assert.strictEqual(context.getSource(node), "6 * 7");
+ });
+ return { BinaryExpression: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve all text plus two characters before for binary expression", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(node => {
+ assert.strictEqual(context.getSource(node, 2), "= 6 * 7");
+ });
+ return { BinaryExpression: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve all text plus one character after for binary expression", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(node => {
+ assert.strictEqual(context.getSource(node, 0, 1), "6 * 7;");
+ });
+ return { BinaryExpression: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve all text plus two characters before and one character after for binary expression", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(node => {
+ assert.strictEqual(context.getSource(node, 2, 1), "= 6 * 7;");
+ });
+ return { BinaryExpression: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ });
+
+ describe("context.getAncestors()", () => {
+ const code = TEST_CODE;
+
+ it("should retrieve all ancestors when used", () => {
+
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const ancestors = context.getAncestors();
+
+ assert.strictEqual(ancestors.length, 3);
+ });
+ return { BinaryExpression: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config, filename, true);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve empty ancestors for root node", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const ancestors = context.getAncestors();
+
+ assert.strictEqual(ancestors.length, 0);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+ });
+
+ describe("context.getNodeByRangeIndex()", () => {
+ const code = TEST_CODE;
+
+ it("should retrieve a node starting at the given index", () => {
+ const spy = sinon.spy(context => {
+ assert.strictEqual(context.getNodeByRangeIndex(4).type, "Identifier");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should retrieve a node containing the given index", () => {
+ const spy = sinon.spy(context => {
+ assert.strictEqual(context.getNodeByRangeIndex(6).type, "Identifier");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should retrieve a node that is exactly the given index", () => {
+ const spy = sinon.spy(context => {
+ const node = context.getNodeByRangeIndex(13);
+
+ assert.strictEqual(node.type, "Literal");
+ assert.strictEqual(node.value, 6);
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should retrieve a node ending with the given index", () => {
+ const spy = sinon.spy(context => {
+ assert.strictEqual(context.getNodeByRangeIndex(9).type, "Identifier");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should retrieve the deepest node containing the given index", () => {
+ const spy = sinon.spy(context => {
+ const node1 = context.getNodeByRangeIndex(14);
+
+ assert.strictEqual(node1.type, "BinaryExpression");
+
+ const node2 = context.getNodeByRangeIndex(3);
+
+ assert.strictEqual(node2.type, "VariableDeclaration");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+
+ it("should return null if the index is outside the range of any node", () => {
+ const spy = sinon.spy(context => {
+ const node1 = context.getNodeByRangeIndex(-1);
+
+ assert.isNull(node1);
+
+ const node2 = context.getNodeByRangeIndex(-99);
+
+ assert.isNull(node2);
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+ });
+
+ describe("context.getScope()", () => {
+ const codeToTestScope = "function foo() { q: for(;;) { break q; } } function bar () { var q = t; } var baz = (() => { return 1; });";
+
+ it("should retrieve the global scope correctly from a Program", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "global");
+ });
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(codeToTestScope, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve the function scope correctly from a FunctionDeclaration", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "function");
+ });
+ return { FunctionDeclaration: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(codeToTestScope, config);
+ assert(spy && spy.calledTwice);
+ });
+
+ it("should retrieve the function scope correctly from a LabeledStatement", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block.id.name, "foo");
+ });
+ return { LabeledStatement: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify(codeToTestScope, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve the function scope correctly from within an ArrowFunctionExpression", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block.type, "ArrowFunctionExpression");
+ });
+
+ return { ReturnStatement: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify(codeToTestScope, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve the function scope correctly from within an SwitchStatement", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "switch");
+ assert.strictEqual(scope.block.type, "SwitchStatement");
+ });
+
+ return { SwitchStatement: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("switch(foo){ case 'a': var b = 'foo'; }", config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve the function scope correctly from within a BlockStatement", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "block");
+ assert.strictEqual(scope.block.type, "BlockStatement");
+ });
+
+ return { BlockStatement: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify("var x; {let y = 1}", config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve the function scope correctly from within a nested block statement", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "block");
+ assert.strictEqual(scope.block.type, "BlockStatement");
+ });
+
+ return { BlockStatement: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify("if (true) { let x = 1 }", config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve the function scope correctly from within a FunctionDeclaration", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block.type, "FunctionDeclaration");
+ });
+
+ return { FunctionDeclaration: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify("function foo() {}", config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve the function scope correctly from within a FunctionExpression", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block.type, "FunctionExpression");
+ });
+
+ return { FunctionExpression: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify("(function foo() {})();", config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve the catch scope correctly from within a CatchClause", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "catch");
+ assert.strictEqual(scope.block.type, "CatchClause");
+ });
+
+ return { CatchClause: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("try {} catch (err) {}", config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve module scope correctly from an ES6 module", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "module");
+ });
+
+ return { AssignmentExpression: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "module"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify("var foo = {}; foo.bar = 1;", config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should retrieve function scope correctly when sourceType is commonjs", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(scope.type, "function");
+ });
+
+ return { AssignmentExpression: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "commonjs"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify("var foo = {}; foo.bar = 1;", config);
+ assert(spy && spy.calledOnce);
+ });
+
+ describe("Scope Internals", () => {
+
+ /**
+ * Get the scope on the node `astSelector` specified.
+ * @param {string} codeToEvaluate The source code to verify.
+ * @param {string} astSelector The AST selector to get scope.
+ * @param {number} [ecmaVersion=5] The ECMAScript version.
+ * @returns {{node: ASTNode, scope: escope.Scope}} Gotten scope.
+ */
+ function getScope(codeToEvaluate, astSelector, ecmaVersion = 5) {
+ let node, scope;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "get-scope": context => ({
+ [astSelector](node0) {
+ node = node0;
+ scope = context.getScope();
+ }
+ })
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion,
+ sourceType: "script"
+ },
+ rules: { "test/get-scope": "error" }
+ };
+
+ linter.verify(
+ codeToEvaluate,
+ config
+ );
+
+ return { node, scope };
+ }
+
+ it("should return 'function' scope on FunctionDeclaration (ES5)", () => {
+ const { node, scope } = getScope("function f() {}", "FunctionDeclaration");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node);
+ });
+
+ it("should return 'function' scope on FunctionExpression (ES5)", () => {
+ const { node, scope } = getScope("!function f() {}", "FunctionExpression");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node);
+ });
+
+ it("should return 'function' scope on the body of FunctionDeclaration (ES5)", () => {
+ const { node, scope } = getScope("function f() {}", "BlockStatement");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent);
+ });
+
+ it("should return 'function' scope on the body of FunctionDeclaration (ES2015)", () => {
+ const { node, scope } = getScope("function f() {}", "BlockStatement", 2015);
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent);
+ });
+
+ it("should return 'function' scope on BlockStatement in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { { var b; } }", "BlockStatement > BlockStatement");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]);
+ });
+
+ it("should return 'block' scope on BlockStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { { let a; var b; } }", "BlockStatement > BlockStatement", 2015);
+
+ assert.strictEqual(scope.type, "block");
+ assert.strictEqual(scope.upper.type, "function");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["a"]);
+ assert.deepStrictEqual(scope.variableScope.variables.map(v => v.name), ["arguments", "b"]);
+ });
+
+ it("should return 'block' scope on nested BlockStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { { let a; { let b; var c; } } }", "BlockStatement > BlockStatement > BlockStatement", 2015);
+
+ assert.strictEqual(scope.type, "block");
+ assert.strictEqual(scope.upper.type, "block");
+ assert.strictEqual(scope.upper.upper.type, "function");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["b"]);
+ assert.deepStrictEqual(scope.upper.variables.map(v => v.name), ["a"]);
+ assert.deepStrictEqual(scope.variableScope.variables.map(v => v.name), ["arguments", "c"]);
+ });
+
+ it("should return 'function' scope on SwitchStatement in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { switch (a) { case 0: var b; } }", "SwitchStatement");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]);
+ });
+
+ it("should return 'switch' scope on SwitchStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { switch (a) { case 0: let b; } }", "SwitchStatement", 2015);
+
+ assert.strictEqual(scope.type, "switch");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["b"]);
+ });
+
+ it("should return 'function' scope on SwitchCase in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { switch (a) { case 0: var b; } }", "SwitchCase");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent.parent.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]);
+ });
+
+ it("should return 'switch' scope on SwitchCase in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { switch (a) { case 0: let b; } }", "SwitchCase", 2015);
+
+ assert.strictEqual(scope.type, "switch");
+ assert.strictEqual(scope.block, node.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["b"]);
+ });
+
+ it("should return 'catch' scope on CatchClause in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { try {} catch (e) { var a; } }", "CatchClause");
+
+ assert.strictEqual(scope.type, "catch");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]);
+ });
+
+ it("should return 'catch' scope on CatchClause in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { try {} catch (e) { let a; } }", "CatchClause", 2015);
+
+ assert.strictEqual(scope.type, "catch");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]);
+ });
+
+ it("should return 'catch' scope on the block of CatchClause in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { try {} catch (e) { var a; } }", "CatchClause > BlockStatement");
+
+ assert.strictEqual(scope.type, "catch");
+ assert.strictEqual(scope.block, node.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]);
+ });
+
+ it("should return 'block' scope on the block of CatchClause in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { try {} catch (e) { let a; } }", "CatchClause > BlockStatement", 2015);
+
+ assert.strictEqual(scope.type, "block");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["a"]);
+ });
+
+ it("should return 'function' scope on ForStatement in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { for (var i = 0; i < 10; ++i) {} }", "ForStatement");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "i"]);
+ });
+
+ it("should return 'for' scope on ForStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { for (let i = 0; i < 10; ++i) {} }", "ForStatement", 2015);
+
+ assert.strictEqual(scope.type, "for");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["i"]);
+ });
+
+ it("should return 'function' scope on the block body of ForStatement in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { for (var i = 0; i < 10; ++i) {} }", "ForStatement > BlockStatement");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent.parent.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "i"]);
+ });
+
+ it("should return 'block' scope on the block body of ForStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { for (let i = 0; i < 10; ++i) {} }", "ForStatement > BlockStatement", 2015);
+
+ assert.strictEqual(scope.type, "block");
+ assert.strictEqual(scope.upper.type, "for");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), []);
+ assert.deepStrictEqual(scope.upper.variables.map(v => v.name), ["i"]);
+ });
+
+ it("should return 'function' scope on ForInStatement in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { for (var key in obj) {} }", "ForInStatement");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "key"]);
+ });
+
+ it("should return 'for' scope on ForInStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { for (let key in obj) {} }", "ForInStatement", 2015);
+
+ assert.strictEqual(scope.type, "for");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["key"]);
+ });
+
+ it("should return 'function' scope on the block body of ForInStatement in functions (ES5)", () => {
+ const { node, scope } = getScope("function f() { for (var key in obj) {} }", "ForInStatement > BlockStatement");
+
+ assert.strictEqual(scope.type, "function");
+ assert.strictEqual(scope.block, node.parent.parent.parent);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "key"]);
+ });
+
+ it("should return 'block' scope on the block body of ForInStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { for (let key in obj) {} }", "ForInStatement > BlockStatement", 2015);
+
+ assert.strictEqual(scope.type, "block");
+ assert.strictEqual(scope.upper.type, "for");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), []);
+ assert.deepStrictEqual(scope.upper.variables.map(v => v.name), ["key"]);
+ });
+
+ it("should return 'for' scope on ForOfStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { for (let x of xs) {} }", "ForOfStatement", 2015);
+
+ assert.strictEqual(scope.type, "for");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), ["x"]);
+ });
+
+ it("should return 'block' scope on the block body of ForOfStatement in functions (ES2015)", () => {
+ const { node, scope } = getScope("function f() { for (let x of xs) {} }", "ForOfStatement > BlockStatement", 2015);
+
+ assert.strictEqual(scope.type, "block");
+ assert.strictEqual(scope.upper.type, "for");
+ assert.strictEqual(scope.block, node);
+ assert.deepStrictEqual(scope.variables.map(v => v.name), []);
+ assert.deepStrictEqual(scope.upper.variables.map(v => v.name), ["x"]);
+ });
+
+ it("should shadow the same name variable by the iteration variable.", () => {
+ const { node, scope } = getScope("let x; for (let x of x) {}", "ForOfStatement", 2015);
+
+ assert.strictEqual(scope.type, "for");
+ assert.strictEqual(scope.upper.type, "global");
+ assert.strictEqual(scope.block, node);
+ assert.strictEqual(scope.upper.variables[0].references.length, 0);
+ assert.strictEqual(scope.references[0].identifier, node.left.declarations[0].id);
+ assert.strictEqual(scope.references[1].identifier, node.right);
+ assert.strictEqual(scope.references[1].resolved, scope.variables[0]);
+ });
+ });
+
+ describe("Variables and references", () => {
+ const code = [
+ "a;",
+ "function foo() { b; }",
+ "Object;",
+ "foo;",
+ "var c;",
+ "c;",
+ "/* global d */",
+ "d;",
+ "e;",
+ "f;"
+ ].join("\n");
+ let scope = null;
+
+ beforeEach(() => {
+ let ok = false;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ test(context) {
+ return {
+ Program() {
+ scope = context.getScope();
+ ok = true;
+ }
+ };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ globals: { e: true, f: false },
+ sourceType: "script",
+ ecmaVersion: 5
+ },
+ rules: {
+ "test/test": 2
+ }
+ };
+
+ linter.verify(code, config);
+ assert(ok);
+ });
+
+ afterEach(() => {
+ scope = null;
+ });
+
+ it("Scope#through should contain references of undefined variables", () => {
+ assert.strictEqual(scope.through.length, 2);
+ assert.strictEqual(scope.through[0].identifier.name, "a");
+ assert.strictEqual(scope.through[0].identifier.loc.start.line, 1);
+ assert.strictEqual(scope.through[0].resolved, null);
+ assert.strictEqual(scope.through[1].identifier.name, "b");
+ assert.strictEqual(scope.through[1].identifier.loc.start.line, 2);
+ assert.strictEqual(scope.through[1].resolved, null);
+ });
+
+ it("Scope#variables should contain global variables", () => {
+ assert(scope.variables.some(v => v.name === "Object"));
+ assert(scope.variables.some(v => v.name === "foo"));
+ assert(scope.variables.some(v => v.name === "c"));
+ assert(scope.variables.some(v => v.name === "d"));
+ assert(scope.variables.some(v => v.name === "e"));
+ assert(scope.variables.some(v => v.name === "f"));
+ });
+
+ it("Scope#set should contain global variables", () => {
+ assert(scope.set.get("Object"));
+ assert(scope.set.get("foo"));
+ assert(scope.set.get("c"));
+ assert(scope.set.get("d"));
+ assert(scope.set.get("e"));
+ assert(scope.set.get("f"));
+ });
+
+ it("Variables#references should contain their references", () => {
+ assert.strictEqual(scope.set.get("Object").references.length, 1);
+ assert.strictEqual(scope.set.get("Object").references[0].identifier.name, "Object");
+ assert.strictEqual(scope.set.get("Object").references[0].identifier.loc.start.line, 3);
+ assert.strictEqual(scope.set.get("Object").references[0].resolved, scope.set.get("Object"));
+ assert.strictEqual(scope.set.get("foo").references.length, 1);
+ assert.strictEqual(scope.set.get("foo").references[0].identifier.name, "foo");
+ assert.strictEqual(scope.set.get("foo").references[0].identifier.loc.start.line, 4);
+ assert.strictEqual(scope.set.get("foo").references[0].resolved, scope.set.get("foo"));
+ assert.strictEqual(scope.set.get("c").references.length, 1);
+ assert.strictEqual(scope.set.get("c").references[0].identifier.name, "c");
+ assert.strictEqual(scope.set.get("c").references[0].identifier.loc.start.line, 6);
+ assert.strictEqual(scope.set.get("c").references[0].resolved, scope.set.get("c"));
+ assert.strictEqual(scope.set.get("d").references.length, 1);
+ assert.strictEqual(scope.set.get("d").references[0].identifier.name, "d");
+ assert.strictEqual(scope.set.get("d").references[0].identifier.loc.start.line, 8);
+ assert.strictEqual(scope.set.get("d").references[0].resolved, scope.set.get("d"));
+ assert.strictEqual(scope.set.get("e").references.length, 1);
+ assert.strictEqual(scope.set.get("e").references[0].identifier.name, "e");
+ assert.strictEqual(scope.set.get("e").references[0].identifier.loc.start.line, 9);
+ assert.strictEqual(scope.set.get("e").references[0].resolved, scope.set.get("e"));
+ assert.strictEqual(scope.set.get("f").references.length, 1);
+ assert.strictEqual(scope.set.get("f").references[0].identifier.name, "f");
+ assert.strictEqual(scope.set.get("f").references[0].identifier.loc.start.line, 10);
+ assert.strictEqual(scope.set.get("f").references[0].resolved, scope.set.get("f"));
+ });
+
+ it("Reference#resolved should be their variable", () => {
+ assert.strictEqual(scope.set.get("Object").references[0].resolved, scope.set.get("Object"));
+ assert.strictEqual(scope.set.get("foo").references[0].resolved, scope.set.get("foo"));
+ assert.strictEqual(scope.set.get("c").references[0].resolved, scope.set.get("c"));
+ assert.strictEqual(scope.set.get("d").references[0].resolved, scope.set.get("d"));
+ assert.strictEqual(scope.set.get("e").references[0].resolved, scope.set.get("e"));
+ assert.strictEqual(scope.set.get("f").references[0].resolved, scope.set.get("f"));
+ });
+ });
+ });
+
+ describe("context.getDeclaredVariables(node)", () => {
+
+ /**
+ * Assert `context.getDeclaredVariables(node)` is valid.
+ * @param {string} code A code to check.
+ * @param {string} type A type string of ASTNode. This method checks variables on the node of the type.
+ * @param {Array<Array<string>>} expectedNamesList An array of expected variable names. The expected variable names is an array of string.
+ * @returns {void}
+ */
+ function verify(code, type, expectedNamesList) {
+ const config = {
+ plugins: {
+ test: {
+
+ rules: {
+ test(context) {
+
+ /**
+ * Assert `context.getDeclaredVariables(node)` is empty.
+ * @param {ASTNode} node A node to check.
+ * @returns {void}
+ */
+ function checkEmpty(node) {
+ assert.strictEqual(0, context.getDeclaredVariables(node).length);
+ }
+ const rule = {
+ Program: checkEmpty,
+ EmptyStatement: checkEmpty,
+ BlockStatement: checkEmpty,
+ ExpressionStatement: checkEmpty,
+ LabeledStatement: checkEmpty,
+ BreakStatement: checkEmpty,
+ ContinueStatement: checkEmpty,
+ WithStatement: checkEmpty,
+ SwitchStatement: checkEmpty,
+ ReturnStatement: checkEmpty,
+ ThrowStatement: checkEmpty,
+ TryStatement: checkEmpty,
+ WhileStatement: checkEmpty,
+ DoWhileStatement: checkEmpty,
+ ForStatement: checkEmpty,
+ ForInStatement: checkEmpty,
+ DebuggerStatement: checkEmpty,
+ ThisExpression: checkEmpty,
+ ArrayExpression: checkEmpty,
+ ObjectExpression: checkEmpty,
+ Property: checkEmpty,
+ SequenceExpression: checkEmpty,
+ UnaryExpression: checkEmpty,
+ BinaryExpression: checkEmpty,
+ AssignmentExpression: checkEmpty,
+ UpdateExpression: checkEmpty,
+ LogicalExpression: checkEmpty,
+ ConditionalExpression: checkEmpty,
+ CallExpression: checkEmpty,
+ NewExpression: checkEmpty,
+ MemberExpression: checkEmpty,
+ SwitchCase: checkEmpty,
+ Identifier: checkEmpty,
+ Literal: checkEmpty,
+ ForOfStatement: checkEmpty,
+ ArrowFunctionExpression: checkEmpty,
+ YieldExpression: checkEmpty,
+ TemplateLiteral: checkEmpty,
+ TaggedTemplateExpression: checkEmpty,
+ TemplateElement: checkEmpty,
+ ObjectPattern: checkEmpty,
+ ArrayPattern: checkEmpty,
+ RestElement: checkEmpty,
+ AssignmentPattern: checkEmpty,
+ ClassBody: checkEmpty,
+ MethodDefinition: checkEmpty,
+ MetaProperty: checkEmpty
+ };
+
+ rule[type] = function(node) {
+ const expectedNames = expectedNamesList.shift();
+ const variables = context.getDeclaredVariables(node);
+
+ assert(Array.isArray(expectedNames));
+ assert(Array.isArray(variables));
+ assert.strictEqual(expectedNames.length, variables.length);
+ for (let i = variables.length - 1; i >= 0; i--) {
+ assert.strictEqual(expectedNames[i], variables[i].name);
+ }
+ };
+ return rule;
+ }
+ }
+
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "module"
+ },
+ rules: {
+ "test/test": 2
+ }
+ };
+
+ linter.verify(code, config);
+
+ // Check all expected names are asserted.
+ assert.strictEqual(0, expectedNamesList.length);
+ }
+
+ it("VariableDeclaration", () => {
+ const code = "\n var {a, x: [b], y: {c = 0}} = foo;\n let {d, x: [e], y: {f = 0}} = foo;\n const {g, x: [h], y: {i = 0}} = foo, {j, k = function(z) { let l; }} = bar;\n ";
+ const namesList = [
+ ["a", "b", "c"],
+ ["d", "e", "f"],
+ ["g", "h", "i", "j", "k"],
+ ["l"]
+ ];
+
+ verify(code, "VariableDeclaration", namesList);
+ });
+
+ it("VariableDeclaration (on for-in/of loop)", () => {
+
+ // TDZ scope is created here, so tests to exclude those.
+ const code = "\n for (var {a, x: [b], y: {c = 0}} in foo) {\n let g;\n }\n for (let {d, x: [e], y: {f = 0}} of foo) {\n let h;\n }\n ";
+ const namesList = [
+ ["a", "b", "c"],
+ ["g"],
+ ["d", "e", "f"],
+ ["h"]
+ ];
+
+ verify(code, "VariableDeclaration", namesList);
+ });
+
+ it("VariableDeclarator", () => {
+
+ // TDZ scope is created here, so tests to exclude those.
+ const code = "\n var {a, x: [b], y: {c = 0}} = foo;\n let {d, x: [e], y: {f = 0}} = foo;\n const {g, x: [h], y: {i = 0}} = foo, {j, k = function(z) { let l; }} = bar;\n ";
+ const namesList = [
+ ["a", "b", "c"],
+ ["d", "e", "f"],
+ ["g", "h", "i"],
+ ["j", "k"],
+ ["l"]
+ ];
+
+ verify(code, "VariableDeclarator", namesList);
+ });
+
+ it("FunctionDeclaration", () => {
+ const code = "\n function foo({a, x: [b], y: {c = 0}}, [d, e]) {\n let z;\n }\n function bar({f, x: [g], y: {h = 0}}, [i, j = function(q) { let w; }]) {\n let z;\n }\n ";
+ const namesList = [
+ ["foo", "a", "b", "c", "d", "e"],
+ ["bar", "f", "g", "h", "i", "j"]
+ ];
+
+ verify(code, "FunctionDeclaration", namesList);
+ });
+
+ it("FunctionExpression", () => {
+ const code = "\n (function foo({a, x: [b], y: {c = 0}}, [d, e]) {\n let z;\n });\n (function bar({f, x: [g], y: {h = 0}}, [i, j = function(q) { let w; }]) {\n let z;\n });\n ";
+ const namesList = [
+ ["foo", "a", "b", "c", "d", "e"],
+ ["bar", "f", "g", "h", "i", "j"],
+ ["q"]
+ ];
+
+ verify(code, "FunctionExpression", namesList);
+ });
+
+ it("ArrowFunctionExpression", () => {
+ const code = "\n (({a, x: [b], y: {c = 0}}, [d, e]) => {\n let z;\n });\n (({f, x: [g], y: {h = 0}}, [i, j]) => {\n let z;\n });\n ";
+ const namesList = [
+ ["a", "b", "c", "d", "e"],
+ ["f", "g", "h", "i", "j"]
+ ];
+
+ verify(code, "ArrowFunctionExpression", namesList);
+ });
+
+ it("ClassDeclaration", () => {
+ const code = "\n class A { foo(x) { let y; } }\n class B { foo(x) { let y; } }\n ";
+ const namesList = [
+ ["A", "A"], // outer scope's and inner scope's.
+ ["B", "B"]
+ ];
+
+ verify(code, "ClassDeclaration", namesList);
+ });
+
+ it("ClassExpression", () => {
+ const code = "\n (class A { foo(x) { let y; } });\n (class B { foo(x) { let y; } });\n ";
+ const namesList = [
+ ["A"],
+ ["B"]
+ ];
+
+ verify(code, "ClassExpression", namesList);
+ });
+
+ it("CatchClause", () => {
+ const code = "\n try {} catch ({a, b}) {\n let x;\n try {} catch ({c, d}) {\n let y;\n }\n }\n ";
+ const namesList = [
+ ["a", "b"],
+ ["c", "d"]
+ ];
+
+ verify(code, "CatchClause", namesList);
+ });
+
+ it("ImportDeclaration", () => {
+ const code = "\n import \"aaa\";\n import * as a from \"bbb\";\n import b, {c, x as d} from \"ccc\";\n ";
+ const namesList = [
+ [],
+ ["a"],
+ ["b", "c", "d"]
+ ];
+
+ verify(code, "ImportDeclaration", namesList);
+ });
+
+ it("ImportSpecifier", () => {
+ const code = "\n import \"aaa\";\n import * as a from \"bbb\";\n import b, {c, x as d} from \"ccc\";\n ";
+ const namesList = [
+ ["c"],
+ ["d"]
+ ];
+
+ verify(code, "ImportSpecifier", namesList);
+ });
+
+ it("ImportDefaultSpecifier", () => {
+ const code = "\n import \"aaa\";\n import * as a from \"bbb\";\n import b, {c, x as d} from \"ccc\";\n ";
+ const namesList = [
+ ["b"]
+ ];
+
+ verify(code, "ImportDefaultSpecifier", namesList);
+ });
+
+ it("ImportNamespaceSpecifier", () => {
+ const code = "\n import \"aaa\";\n import * as a from \"bbb\";\n import b, {c, x as d} from \"ccc\";\n ";
+ const namesList = [
+ ["a"]
+ ];
+
+ verify(code, "ImportNamespaceSpecifier", namesList);
+ });
+ });
+
+ describe("context.markVariableAsUsed()", () => {
+
+ it("should mark variables in current scope as used", () => {
+ const code = "var a = 1, b = 2;";
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ assert.isTrue(context.markVariableAsUsed("a"));
+
+ const scope = context.getScope();
+
+ assert.isTrue(getVariable(scope, "a").eslintUsed);
+ assert.notOk(getVariable(scope, "b").eslintUsed);
+ });
+
+ return { "Program:exit": spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should mark variables in function args as used", () => {
+ const code = "function abc(a, b) { return 1; }";
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ assert.isTrue(context.markVariableAsUsed("a"));
+
+ const scope = context.getScope();
+
+ assert.isTrue(getVariable(scope, "a").eslintUsed);
+ assert.notOk(getVariable(scope, "b").eslintUsed);
+ });
+
+ return { ReturnStatement: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should mark variables in higher scopes as used", () => {
+ const code = "var a, b; function abc() { return 1; }";
+ let returnSpy, exitSpy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ returnSpy = sinon.spy(() => {
+ assert.isTrue(context.markVariableAsUsed("a"));
+ });
+ exitSpy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.isTrue(getVariable(scope, "a").eslintUsed);
+ assert.notOk(getVariable(scope, "b").eslintUsed);
+ });
+
+ return { ReturnStatement: returnSpy, "Program:exit": exitSpy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(returnSpy && returnSpy.calledOnce);
+ assert(exitSpy && exitSpy.calledOnce);
+ });
+
+ it("should mark variables as used when sourceType is commonjs", () => {
+ const code = "var a = 1, b = 2;";
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const globalScope = context.getScope(),
+ childScope = globalScope.childScopes[0];
+
+ assert.isTrue(context.markVariableAsUsed("a"), "Call to markVariableAsUsed should return true");
+
+ assert.isTrue(getVariable(childScope, "a").eslintUsed, "'a' should be marked as used.");
+ assert.isUndefined(getVariable(childScope, "b").eslintUsed, "'b' should be marked as used.");
+ });
+
+ return { "Program:exit": spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "commonjs"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce, "Spy wasn't called.");
+ });
+
+ it("should mark variables in modules as used", () => {
+ const code = "var a = 1, b = 2;";
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const globalScope = context.getScope(),
+ childScope = globalScope.childScopes[0];
+
+ assert.isTrue(context.markVariableAsUsed("a"));
+
+ assert.isTrue(getVariable(childScope, "a").eslintUsed);
+ assert.isUndefined(getVariable(childScope, "b").eslintUsed);
+ });
+
+ return { "Program:exit": spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "module"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should return false if the given variable is not found", () => {
+ const code = "var a = 1, b = 2;";
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ assert.isFalse(context.markVariableAsUsed("c"));
+ });
+
+ return { "Program:exit": spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+ });
+
+ describe("context.getCwd()", () => {
+ const code = "a;\nb;";
+ const baseConfig = { rules: { "test/checker": "error" } };
+
+ it("should get cwd correctly in the context", () => {
+ const cwd = "cwd";
+ const linterWithOption = new Linter({ cwd, configType: "flat" });
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ assert.strictEqual(context.getCwd(), cwd);
+ });
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ ...baseConfig
+ };
+
+ linterWithOption.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should assign process.cwd() to it if cwd is undefined", () => {
+
+ const linterWithOption = new Linter({ configType: "flat" });
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+
+ spy = sinon.spy(() => {
+ assert.strictEqual(context.getCwd(), process.cwd());
+ });
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ ...baseConfig
+ };
+
+ linterWithOption.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("should assign process.cwd() to it if the option is undefined", () => {
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+
+ spy = sinon.spy(() => {
+ assert.strictEqual(context.getCwd(), process.cwd());
+ });
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ ...baseConfig
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+ });
+
+ });
+
+ describe("Rule Severity", () => {
+
+ it("rule should run as warning when set to 1 with a config array", () => {
+ const ruleId = "semi",
+ configs = createFlatConfigArray({
+ files: ["**/*.js"],
+ rules: {
+ [ruleId]: 1
+ }
+ });
+
+ configs.normalizeSync();
+ const messages = linter.verify("foo", configs, filename, true);
+
+ assert.strictEqual(messages.length, 1, "Message length is wrong");
+ assert.strictEqual(messages[0].ruleId, ruleId);
+ });
+
+ it("rule should run as warning when set to 1 with a plain array", () => {
+ const ruleId = "semi",
+ configs = [{
+ files: ["**/*.js"],
+ rules: {
+ [ruleId]: 1
+ }
+ }];
+
+ const messages = linter.verify("foo", configs, filename, true);
+
+ assert.strictEqual(messages.length, 1, "Message length is wrong");
+ assert.strictEqual(messages[0].ruleId, ruleId);
+ });
+
+ it("rule should run as warning when set to 1 with an object", () => {
+ const ruleId = "semi",
+ config = {
+ files: ["**/*.js"],
+ rules: {
+ [ruleId]: 1
+ }
+ };
+
+ const messages = linter.verify("foo", config, filename, true);
+
+ assert.strictEqual(messages.length, 1, "Message length is wrong");
+ assert.strictEqual(messages[0].ruleId, ruleId);
+ });
+ });
+
+ describe("Code with a hashbang comment", () => {
+ const code = "#!bin/program\n\nvar foo;;";
+
+ it("should preserve line numbers", () => {
+ const config = { rules: { "no-extra-semi": 1 } };
+ const messages = linter.verify(code, config);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-extra-semi");
+ assert.strictEqual(messages[0].nodeType, "EmptyStatement");
+ assert.strictEqual(messages[0].line, 3);
+ });
+
+ it("should have a comment with the hashbang in it", () => {
+ const spy = sinon.spy(context => {
+ const comments = context.getAllComments();
+
+ assert.strictEqual(comments.length, 1);
+ assert.strictEqual(comments[0].type, "Shebang");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: spy
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify(code, config);
+ assert(spy.calledOnce);
+ });
+ });
+
+ describe("Options", () => {
+
+ describe("filename", () => {
+ it("should allow filename to be passed on options object", () => {
+ const filenameChecker = sinon.spy(context => {
+ assert.strictEqual(context.getFilename(), "foo.js");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: filenameChecker
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify("foo;", config, { filename: "foo.js" });
+ assert(filenameChecker.calledOnce);
+ });
+
+ it("should allow filename to be passed as third argument", () => {
+ const filenameChecker = sinon.spy(context => {
+ assert.strictEqual(context.getFilename(), "bar.js");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: filenameChecker
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify("foo;", config, "bar.js");
+ assert(filenameChecker.calledOnce);
+ });
+
+ it("should default filename to <input> when options object doesn't have filename", () => {
+ const filenameChecker = sinon.spy(context => {
+ assert.strictEqual(context.getFilename(), "<input>");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: filenameChecker
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify("foo;", config, {});
+ assert(filenameChecker.calledOnce);
+ });
+
+ it("should default filename to <input> when only two arguments are passed", () => {
+ const filenameChecker = sinon.spy(context => {
+ assert.strictEqual(context.getFilename(), "<input>");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: filenameChecker
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify("foo;", config);
+ assert(filenameChecker.calledOnce);
+ });
+ });
+
+ describe("physicalFilename", () => {
+ it("should be same as `filename` passed on options object, if no processors are used", () => {
+ const physicalFilenameChecker = sinon.spy(context => {
+ assert.strictEqual(context.getPhysicalFilename(), "foo.js");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: physicalFilenameChecker
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify("foo;", config, { filename: "foo.js" });
+ assert(physicalFilenameChecker.calledOnce);
+ });
+
+ it("should default physicalFilename to <input> when options object doesn't have filename", () => {
+ const physicalFilenameChecker = sinon.spy(context => {
+ assert.strictEqual(context.getPhysicalFilename(), "<input>");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: physicalFilenameChecker
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify("foo;", config, {});
+ assert(physicalFilenameChecker.calledOnce);
+ });
+
+ it("should default physicalFilename to <input> when only two arguments are passed", () => {
+ const physicalFilenameChecker = sinon.spy(context => {
+ assert.strictEqual(context.getPhysicalFilename(), "<input>");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: physicalFilenameChecker
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify("foo;", config);
+ assert(physicalFilenameChecker.calledOnce);
+ });
+ });
+
+ });
+
+ describe("Inline Directives", () => {
+
+ describe("/*global*/ Comments", () => {
+
+ describe("when evaluating code containing /*global */ and /*globals */ blocks", () => {
+
+ it("variables should be available in global scope", () => {
+ const code = `
+ /*global a b:true c:false d:readable e:writeable Math:off */
+ function foo() {}
+ /*globals f:true*/
+ /* global ConfigGlobal : readable */
+ `;
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+ const a = getVariable(scope, "a"),
+ b = getVariable(scope, "b"),
+ c = getVariable(scope, "c"),
+ d = getVariable(scope, "d"),
+ e = getVariable(scope, "e"),
+ f = getVariable(scope, "f"),
+ mathGlobal = getVariable(scope, "Math"),
+ arrayGlobal = getVariable(scope, "Array"),
+ configGlobal = getVariable(scope, "ConfigGlobal");
+
+ assert.strictEqual(a.name, "a");
+ assert.strictEqual(a.writeable, false);
+ assert.strictEqual(b.name, "b");
+ assert.strictEqual(b.writeable, true);
+ assert.strictEqual(c.name, "c");
+ assert.strictEqual(c.writeable, false);
+ assert.strictEqual(d.name, "d");
+ assert.strictEqual(d.writeable, false);
+ assert.strictEqual(e.name, "e");
+ assert.strictEqual(e.writeable, true);
+ assert.strictEqual(f.name, "f");
+ assert.strictEqual(f.writeable, true);
+ assert.strictEqual(mathGlobal, null);
+ assert.strictEqual(arrayGlobal, null);
+ assert.strictEqual(configGlobal.name, "ConfigGlobal");
+ assert.strictEqual(configGlobal.writeable, false);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" },
+ languageOptions: {
+ globals: { Array: "off", ConfigGlobal: "writeable" }
+ }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+ });
+
+ describe("when evaluating code containing a /*global */ block with sloppy whitespace", () => {
+ const code = "/* global a b : true c: false*/";
+
+ it("variables should be available in global scope", () => {
+
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope(),
+ a = getVariable(scope, "a"),
+ b = getVariable(scope, "b"),
+ c = getVariable(scope, "c");
+
+ assert.strictEqual(a.name, "a");
+ assert.strictEqual(a.writeable, false);
+ assert.strictEqual(b.name, "b");
+ assert.strictEqual(b.writeable, true);
+ assert.strictEqual(c.name, "c");
+ assert.strictEqual(c.writeable, false);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+ });
+
+ describe("when evaluating code containing a line comment", () => {
+ const code = "//global a \n function f() {}";
+
+ it("should not introduce a global variable", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(getVariable(scope, "a"), null);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+ });
+
+ describe("when evaluating code containing normal block comments", () => {
+ const code = "/**/ /*a*/ /*b:true*/ /*foo c:false*/";
+
+ it("should not introduce a global variable", () => {
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.strictEqual(getVariable(scope, "a"), null);
+ assert.strictEqual(getVariable(scope, "b"), null);
+ assert.strictEqual(getVariable(scope, "foo"), null);
+ assert.strictEqual(getVariable(scope, "c"), null);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ rules: { "test/checker": "error" }
+ };
+
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+ });
+
+ it("should attach a \"/*global\" comment node to declared variables", () => {
+ const code = "/* global foo */\n/* global bar, baz */";
+ let ok = false;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ test(context) {
+ return {
+ Program() {
+ const scope = context.getScope();
+ const sourceCode = context.getSourceCode();
+ const comments = sourceCode.getAllComments();
+
+ assert.strictEqual(2, comments.length);
+
+ const foo = getVariable(scope, "foo");
+
+ assert.strictEqual(foo.eslintExplicitGlobal, true);
+ assert.strictEqual(foo.eslintExplicitGlobalComments[0], comments[0]);
+
+ const bar = getVariable(scope, "bar");
+
+ assert.strictEqual(bar.eslintExplicitGlobal, true);
+ assert.strictEqual(bar.eslintExplicitGlobalComments[0], comments[1]);
+
+ const baz = getVariable(scope, "baz");
+
+ assert.strictEqual(baz.eslintExplicitGlobal, true);
+ assert.strictEqual(baz.eslintExplicitGlobalComments[0], comments[1]);
+
+ ok = true;
+ }
+ };
+ }
+ }
+ }
+ },
+ rules: { "test/test": "error" }
+ };
+
+
+ linter.verify(code, config);
+ assert(ok);
+ });
+
+ it("should report a linting error when a global is set to an invalid value", () => {
+ const results = linter.verify("/* global foo: AAAAA, bar: readonly */\nfoo;\nbar;", { rules: { "no-undef": "error" } });
+
+ assert.deepStrictEqual(results, [
+ {
+ ruleId: null,
+ severity: 2,
+ message: "'AAAAA' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')",
+ line: 1,
+ column: 1,
+ endLine: 1,
+ endColumn: 39,
+ nodeType: null
+ },
+ {
+ ruleId: "no-undef",
+ messageId: "undef",
+ severity: 2,
+ message: "'foo' is not defined.",
+ line: 2,
+ column: 1,
+ endLine: 2,
+ endColumn: 4,
+ nodeType: "Identifier"
+ }
+ ]);
+ });
+
+ });
+
+ describe("/*exported*/ Comments", () => {
+
+ it("we should behave nicely when no matching variable is found", () => {
+ const code = "/* exported horse */";
+ const config = { rules: {} };
+
+ linter.verify(code, config, filename, true);
+ });
+
+ it("variables should be exported", () => {
+ const code = "/* exported horse */\n\nvar horse = 'circus'";
+ let spy;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope(),
+ horse = getVariable(scope, "horse");
+
+ assert.strictEqual(horse.eslintUsed, true);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("undefined variables should not be exported", () => {
+ const code = "/* exported horse */\n\nhorse = 'circus'";
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope(),
+ horse = getVariable(scope, "horse");
+
+ assert.strictEqual(horse, null);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("variables should be exported in strict mode", () => {
+ const code = "/* exported horse */\n'use strict';\nvar horse = 'circus'";
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope(),
+ horse = getVariable(scope, "horse");
+
+ assert.strictEqual(horse.eslintUsed, true);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("variables should not be exported in the es6 module environment", () => {
+ const code = "/* exported horse */\nvar horse = 'circus'";
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope(),
+ horse = getVariable(scope, "horse");
+
+ assert.strictEqual(horse, null); // there is no global scope at all
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6,
+ sourceType: "module"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ it("variables should not be exported when in a commonjs file", () => {
+ const code = "/* exported horse */\nvar horse = 'circus'";
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope(),
+ horse = getVariable(scope, "horse");
+
+ assert.strictEqual(horse, null); // there is no global scope at all
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "commonjs"
+ },
+ rules: { "test/checker": "error" }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+ });
+
+ describe("/*eslint*/ Comments", () => {
+ describe("when evaluating code with comments to enable rules", () => {
+
+ it("should report a violation", () => {
+ const code = "/*eslint no-alert:1*/ alert('test');";
+ const config = { rules: {} };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].message, "Unexpected alert.");
+ assert.include(messages[0].nodeType, "CallExpression");
+ });
+
+ it("rules should not change initial config", () => {
+ const config = {
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { strict: 2 }
+ };
+ const codeA = "/*eslint strict: 0*/ function bar() { return 2; }";
+ const codeB = "function foo() { return 1; }";
+ let messages = linter.verify(codeA, config, filename, false);
+
+ assert.strictEqual(messages.length, 0);
+
+ messages = linter.verify(codeB, config, filename, false);
+ assert.strictEqual(messages.length, 1);
+ });
+
+ it("rules should not change initial config", () => {
+ const config = {
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { quotes: [2, "double"] }
+ };
+ const codeA = "/*eslint quotes: 0*/ function bar() { return '2'; }";
+ const codeB = "function foo() { return '1'; }";
+ let messages = linter.verify(codeA, config, filename, false);
+
+ assert.strictEqual(messages.length, 0);
+
+ messages = linter.verify(codeB, config, filename, false);
+ assert.strictEqual(messages.length, 1);
+ });
+
+ it("rules should not change initial config", () => {
+ const config = { rules: { quotes: [2, "double"] } };
+ const codeA = "/*eslint quotes: [0, \"single\"]*/ function bar() { return '2'; }";
+ const codeB = "function foo() { return '1'; }";
+ let messages = linter.verify(codeA, config, filename, false);
+
+ assert.strictEqual(messages.length, 0);
+
+ messages = linter.verify(codeB, config, filename, false);
+ assert.strictEqual(messages.length, 1);
+ });
+
+ it("rules should not change initial config", () => {
+ const config = {
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { "no-unused-vars": [2, { vars: "all" }] }
+ };
+ const codeA = "/*eslint no-unused-vars: [0, {\"vars\": \"local\"}]*/ var a = 44;";
+ const codeB = "var b = 55;";
+ let messages = linter.verify(codeA, config, filename, false);
+
+ assert.strictEqual(messages.length, 0);
+
+ messages = linter.verify(codeB, config, filename, false);
+ assert.strictEqual(messages.length, 1);
+ });
+ });
+
+ describe("when evaluating code with invalid comments to enable rules", () => {
+ it("should report a violation when the config is not a valid rule configuration", () => {
+ assert.deepStrictEqual(
+ linter.verify("/*eslint no-alert:true*/ alert('test');", {}),
+ [
+ {
+ severity: 2,
+ ruleId: "no-alert",
+ message: "Configuration for rule \"no-alert\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed 'true').\n",
+ line: 1,
+ column: 1,
+ endLine: 1,
+ endColumn: 25,
+ nodeType: null
+ }
+ ]
+ );
+ });
+
+ it("should report a violation when the config violates a rule's schema", () => {
+ assert.deepStrictEqual(
+ linter.verify("/* eslint no-alert: [error, {nonExistentPropertyName: true}]*/", {}),
+ [
+ {
+ severity: 2,
+ ruleId: "no-alert",
+ message: "Configuration for rule \"no-alert\" is invalid:\n\tValue [{\"nonExistentPropertyName\":true}] should NOT have more than 0 items.\n",
+ line: 1,
+ column: 1,
+ endLine: 1,
+ endColumn: 63,
+ nodeType: null
+ }
+ ]
+ );
+ });
+ });
+
+ describe("when evaluating code with comments to disable rules", () => {
+
+ it("should not report a violation", () => {
+ const config = { rules: { "no-alert": 1 } };
+ const messages = linter.verify("/*eslint no-alert:0*/ alert('test');", config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should report an error when disabling a non-existent rule in inline comment", () => {
+ let code = "/*eslint foo:0*/ ;";
+ let messages = linter.verify(code, {}, filename);
+
+ assert.strictEqual(messages.length, 1, "/*eslint*/ comment should report problem.");
+ assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found.");
+
+ code = "/*eslint-disable foo*/ ;";
+ messages = linter.verify(code, {}, filename);
+ assert.strictEqual(messages.length, 1, "/*eslint-disable*/ comment should report problem.");
+ assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found.");
+
+ code = "/*eslint-disable-line foo*/ ;";
+ messages = linter.verify(code, {}, filename);
+ assert.strictEqual(messages.length, 1, "/*eslint-disable-line*/ comment should report problem.");
+ assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found.");
+
+ code = "/*eslint-disable-next-line foo*/ ;";
+ messages = linter.verify(code, {}, filename);
+ assert.strictEqual(messages.length, 1, "/*eslint-disable-next-line*/ comment should report problem.");
+ assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found.");
+ });
+
+ it("should not report an error, when disabling a non-existent rule in config", () => {
+ const messages = linter.verify("", { rules: { foo: 0 } }, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should throw an error when a non-existent rule in config", () => {
+ assert.throws(() => {
+ linter.verify("", { rules: { foo: 1 } }, filename);
+ }, /Key "rules": Key "foo":/u);
+
+ assert.throws(() => {
+ linter.verify("", { rules: { foo: 2 } }, filename);
+ }, /Key "rules": Key "foo":/u);
+
+ });
+ });
+
+ describe("when evaluating code with comments to enable multiple rules", () => {
+ const code = "/*eslint no-alert:1 no-console:1*/ alert('test'); console.log('test');";
+
+ it("should report a violation", () => {
+ const config = { rules: {} };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].message, "Unexpected alert.");
+ assert.include(messages[0].nodeType, "CallExpression");
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ });
+ });
+
+ describe("when evaluating code with comments to enable and disable multiple rules", () => {
+ const code = "/*eslint no-alert:1 no-console:0*/ alert('test'); console.log('test');";
+
+ it("should report a violation", () => {
+ const config = { rules: { "no-console": 1, "no-alert": 0 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].message, "Unexpected alert.");
+ assert.include(messages[0].nodeType, "CallExpression");
+ });
+ });
+
+ describe("when evaluating code with comments to disable and enable configurable rule as part of plugin", () => {
+
+ let baseConfig;
+
+ beforeEach(() => {
+ baseConfig = {
+ plugins: {
+ "test-plugin": {
+ rules: {
+ "test-rule"(context) {
+ return {
+ Literal(node) {
+ if (node.value === "trigger violation") {
+ context.report(node, "Reporting violation.");
+ }
+ }
+ };
+ }
+ }
+ }
+ }
+ };
+
+ });
+
+ it("should not report a violation when inline comment enables plugin rule and there's no violation", () => {
+ const config = { ...baseConfig, rules: {} };
+ const code = "/*eslint test-plugin/test-rule: 2*/ var a = \"no violation\";";
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not report a violation when inline comment disables plugin rule", () => {
+ const code = "/*eslint test-plugin/test-rule:0*/ var a = \"trigger violation\"";
+ const config = { ...baseConfig, rules: { "test-plugin/test-rule": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should report a violation when the report is right before the comment", () => {
+ const code = " /* eslint-disable */ ";
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => ({
+ Program() {
+ context.report({ loc: { line: 1, column: 0 }, message: "foo" });
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ const problems = linter.verify(code, config);
+
+ assert.strictEqual(problems.length, 1);
+ assert.strictEqual(problems[0].message, "foo");
+ });
+
+ it("should not report a violation when the report is right at the start of the comment", () => {
+ const code = " /* eslint-disable */ ";
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => ({
+ Program() {
+ context.report({ loc: { line: 1, column: 1 }, message: "foo" });
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ const problems = linter.verify(code, config);
+
+ assert.strictEqual(problems.length, 0);
+ });
+
+ it("rules should not change initial config", () => {
+ const config = { ...baseConfig, rules: { "test-plugin/test-rule": 2 } };
+ const codeA = "/*eslint test-plugin/test-rule: 0*/ var a = \"trigger violation\";";
+ const codeB = "var a = \"trigger violation\";";
+ let messages = linter.verify(codeA, config, filename, false);
+
+ assert.strictEqual(messages.length, 0);
+
+ messages = linter.verify(codeB, config, filename, false);
+ assert.strictEqual(messages.length, 1);
+ });
+ });
+
+ describe("when evaluating code with comments to enable and disable all reporting", () => {
+ it("should report a violation", () => {
+
+ const code = [
+ "/*eslint-disable */",
+ "alert('test');",
+ "/*eslint-enable */",
+ "alert('test');"
+ ].join("\n");
+ const config = { rules: { "no-alert": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].message, "Unexpected alert.");
+ assert.include(messages[0].nodeType, "CallExpression");
+ assert.strictEqual(messages[0].line, 4);
+ });
+
+ it("should not report a violation", () => {
+ const code = [
+ "/*eslint-disable */",
+ "alert('test');",
+ "alert('test');"
+ ].join("\n");
+ const config = { rules: { "no-alert": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not report a violation", () => {
+ const code = [
+ " alert('test1');/*eslint-disable */\n",
+ "alert('test');",
+ " alert('test');\n",
+ "/*eslint-enable */alert('test2');"
+ ].join("");
+ const config = { rules: { "no-alert": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(messages[0].column, 21);
+ assert.strictEqual(messages[1].column, 19);
+ });
+
+ it("should report a violation", () => {
+
+ const code = [
+ "/*eslint-disable */",
+ "alert('test');",
+ "/*eslint-disable */",
+ "alert('test');",
+ "/*eslint-enable*/",
+ "alert('test');",
+ "/*eslint-enable*/"
+ ].join("\n");
+
+ const config = { rules: { "no-alert": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ });
+
+
+ it("should not report a violation", () => {
+ const code = [
+ "/*eslint-disable */",
+ "(function(){ var b = 44;})()",
+ "/*eslint-enable */;any();"
+ ].join("\n");
+
+ const config = { rules: { "no-unused-vars": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not report a violation", () => {
+ const code = [
+ "(function(){ /*eslint-disable */ var b = 44;})()",
+ "/*eslint-enable */;any();"
+ ].join("\n");
+
+ const config = { rules: { "no-unused-vars": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+ });
+
+ describe("when evaluating code with comments to enable and disable multiple comma separated rules", () => {
+ const code = "/*eslint no-alert:1, no-console:0*/ alert('test'); console.log('test');";
+
+ it("should report a violation", () => {
+ const config = { rules: { "no-console": 1, "no-alert": 0 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].message, "Unexpected alert.");
+ assert.include(messages[0].nodeType, "CallExpression");
+ });
+ });
+
+ describe("when evaluating code with comments to enable configurable rule", () => {
+ const code = "/*eslint quotes:[2, \"double\"]*/ alert('test');";
+
+ it("should report a violation", () => {
+ const config = { rules: { quotes: [2, "single"] } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "quotes");
+ assert.strictEqual(messages[0].message, "Strings must use doublequote.");
+ assert.include(messages[0].nodeType, "Literal");
+ });
+ });
+
+ describe("when evaluating code with comments to enable configurable rule using string severity", () => {
+ const code = "/*eslint quotes:[\"error\", \"double\"]*/ alert('test');";
+
+ it("should report a violation", () => {
+ const config = { rules: { quotes: [2, "single"] } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "quotes");
+ assert.strictEqual(messages[0].message, "Strings must use doublequote.");
+ assert.include(messages[0].nodeType, "Literal");
+ });
+ });
+
+ describe("when evaluating code with incorrectly formatted comments to disable rule", () => {
+ it("should report a violation", () => {
+ const code = "/*eslint no-alert:'1'*/ alert('test');";
+
+ const config = { rules: { "no-alert": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+
+ /*
+ * Incorrectly formatted comment threw error;
+ * message from caught exception
+ * may differ amongst UAs, so verifying
+ * first part only as defined in the
+ * parseJsonConfig function in lib/eslint.js
+ */
+ assert.match(messages[0].message, /^Failed to parse JSON from ' "no-alert":'1'':/u);
+ assert.strictEqual(messages[0].line, 1);
+ assert.strictEqual(messages[0].column, 1);
+
+ assert.strictEqual(messages[1].ruleId, "no-alert");
+ assert.strictEqual(messages[1].message, "Unexpected alert.");
+ assert.include(messages[1].nodeType, "CallExpression");
+ });
+
+ it("should report a violation", () => {
+ const code = "/*eslint no-alert:abc*/ alert('test');";
+
+ const config = { rules: { "no-alert": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+
+ /*
+ * Incorrectly formatted comment threw error;
+ * message from caught exception
+ * may differ amongst UAs, so verifying
+ * first part only as defined in the
+ * parseJsonConfig function in lib/eslint.js
+ */
+ assert.match(messages[0].message, /^Failed to parse JSON from ' "no-alert":abc':/u);
+ assert.strictEqual(messages[0].line, 1);
+ assert.strictEqual(messages[0].column, 1);
+
+ assert.strictEqual(messages[1].ruleId, "no-alert");
+ assert.strictEqual(messages[1].message, "Unexpected alert.");
+ assert.include(messages[1].nodeType, "CallExpression");
+ });
+
+ it("should report a violation", () => {
+ const code = "/*eslint no-alert:0 2*/ alert('test');";
+
+ const config = { rules: { "no-alert": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+
+ /*
+ * Incorrectly formatted comment threw error;
+ * message from caught exception
+ * may differ amongst UAs, so verifying
+ * first part only as defined in the
+ * parseJsonConfig function in lib/eslint.js
+ */
+ assert.match(messages[0].message, /^Failed to parse JSON from ' "no-alert":0 2':/u);
+ assert.strictEqual(messages[0].line, 1);
+ assert.strictEqual(messages[0].column, 1);
+
+ assert.strictEqual(messages[1].ruleId, "no-alert");
+ assert.strictEqual(messages[1].message, "Unexpected alert.");
+ assert.include(messages[1].nodeType, "CallExpression");
+ });
+ });
+
+ describe("when evaluating code with comments which have colon in its value", () => {
+ const code = String.raw`
+/* eslint max-len: [2, 100, 2, {ignoreUrls: true, ignorePattern: "data:image\\/|\\s*require\\s*\\(|^\\s*loader\\.lazy|-\\*-"}] */
+alert('test');
+`;
+
+ it("should not parse errors, should report a violation", () => {
+ const messages = linter.verify(code, {}, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "max-len");
+ assert.strictEqual(messages[0].message, "This line has a length of 129. Maximum allowed is 100.");
+ assert.include(messages[0].nodeType, "Program");
+ });
+ });
+
+ describe("when evaluating code with comments that contain escape sequences", () => {
+ const code = String.raw`
+/* eslint max-len: ["error", 1, { ignoreComments: true, ignorePattern: "console\\.log\\(" }] */
+console.log("test");
+consolexlog("test2");
+var a = "test2";
+`;
+
+ it("should validate correctly", () => {
+ const config = { rules: {} };
+ const messages = linter.verify(code, config, filename);
+ const [message1, message2] = messages;
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(message1.ruleId, "max-len");
+ assert.strictEqual(message1.message, "This line has a length of 21. Maximum allowed is 1.");
+ assert.strictEqual(message1.line, 4);
+ assert.strictEqual(message1.column, 1);
+ assert.include(message1.nodeType, "Program");
+ assert.strictEqual(message2.ruleId, "max-len");
+ assert.strictEqual(message2.message, "This line has a length of 16. Maximum allowed is 1.");
+ assert.strictEqual(message2.line, 5);
+ assert.strictEqual(message2.column, 1);
+ assert.include(message2.nodeType, "Program");
+ });
+ });
+
+ });
+
+ describe("/*eslint-disable*/ and /*eslint-enable*/", () => {
+ it("should report a violation", () => {
+ const code = [
+ "/*eslint-disable no-alert */",
+ "alert('test');",
+ "console.log('test');" // here
+ ].join("\n");
+ const config = { rules: { "no-alert": 1, "no-console": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+
+ assert.strictEqual(messages[0].ruleId, "no-console");
+ });
+
+ it("should report no violation", () => {
+ const code = [
+ "/* eslint-disable quotes */",
+ "console.log(\"foo\");",
+ "/* eslint-enable quotes */"
+ ].join("\n");
+ const config = { rules: { quotes: 2 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should report a violation", () => {
+ const code = [
+ "/*eslint-disable no-alert, no-console */",
+ "alert('test');",
+ "console.log('test');",
+ "/*eslint-enable*/",
+
+ "alert('test');", // here
+ "console.log('test');" // here
+ ].join("\n");
+ const config = { rules: { "no-alert": 1, "no-console": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].line, 5);
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ assert.strictEqual(messages[1].line, 6);
+ });
+
+ it("should report a violation", () => {
+ const code = [
+ "/*eslint-disable no-alert */",
+ "alert('test');",
+ "console.log('test');",
+ "/*eslint-enable no-console */",
+
+ "alert('test');" // here
+ ].join("\n");
+ const config = { rules: { "no-alert": 1, "no-console": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+
+ assert.strictEqual(messages[0].ruleId, "no-console");
+ });
+
+
+ it("should report a violation", () => {
+ const code = [
+ "/*eslint-disable no-alert, no-console */",
+ "alert('test');",
+ "console.log('test');",
+ "/*eslint-enable no-alert*/",
+
+ "alert('test');", // here
+ "console.log('test');"
+ ].join("\n");
+ const config = { rules: { "no-alert": 1, "no-console": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].line, 5);
+ });
+
+
+ it("should report a violation", () => {
+ const code = [
+ "/*eslint-disable no-alert */",
+
+ "/*eslint-disable no-console */",
+ "alert('test');",
+ "console.log('test');",
+ "/*eslint-enable */",
+
+ "alert('test');", // here
+ "console.log('test');", // here
+
+ "/*eslint-enable */",
+
+ "alert('test');", // here
+ "console.log('test');", // here
+
+ "/*eslint-enable*/"
+ ].join("\n");
+ const config = { rules: { "no-alert": 1, "no-console": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 4);
+
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].line, 6);
+
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ assert.strictEqual(messages[1].line, 7);
+
+ assert.strictEqual(messages[2].ruleId, "no-alert");
+ assert.strictEqual(messages[2].line, 9);
+
+ assert.strictEqual(messages[3].ruleId, "no-console");
+ assert.strictEqual(messages[3].line, 10);
+
+ });
+
+ it("should report a violation", () => {
+ const code = [
+ "/*eslint-disable no-alert, no-console */",
+ "alert('test');",
+ "console.log('test');",
+
+ "/*eslint-enable no-alert */",
+
+ "alert('test');", // here
+ "console.log('test');",
+
+ "/*eslint-enable no-console */",
+
+ "alert('test');", // here
+ "console.log('test');", // here
+ "/*eslint-enable no-console */"
+ ].join("\n");
+ const config = { rules: { "no-alert": 1, "no-console": 1 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 3);
+
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].line, 5);
+
+ assert.strictEqual(messages[1].ruleId, "no-alert");
+ assert.strictEqual(messages[1].line, 8);
+
+ assert.strictEqual(messages[2].ruleId, "no-console");
+ assert.strictEqual(messages[2].line, 9);
+
+ });
+
+ it("should report a violation when severity is warn", () => {
+ const code = [
+ "/*eslint-disable no-alert, no-console */",
+ "alert('test');",
+ "console.log('test');",
+
+ "/*eslint-enable no-alert */",
+
+ "alert('test');", // here
+ "console.log('test');",
+
+ "/*eslint-enable no-console */",
+
+ "alert('test');", // here
+ "console.log('test');", // here
+ "/*eslint-enable no-console */"
+ ].join("\n");
+ const config = { rules: { "no-alert": "warn", "no-console": "warn" } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 3);
+
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[0].line, 5);
+
+ assert.strictEqual(messages[1].ruleId, "no-alert");
+ assert.strictEqual(messages[1].line, 8);
+
+ assert.strictEqual(messages[2].ruleId, "no-console");
+ assert.strictEqual(messages[2].line, 9);
+
+ });
+
+ it("should report no violation", () => {
+ const code = [
+ "/*eslint-disable no-unused-vars */",
+ "var foo; // eslint-disable-line no-unused-vars",
+ "var bar;",
+ "/* eslint-enable no-unused-vars */" // here
+ ].join("\n");
+ const config = { rules: { "no-unused-vars": 2 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ });
+
+ describe("/*eslint-disable-line*/", () => {
+
+ it("should report a violation", () => {
+ const code = [
+ "alert('test'); // eslint-disable-line no-alert",
+ "console.log('test');" // here
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+
+ assert.strictEqual(messages[0].ruleId, "no-console");
+ });
+
+ it("should report a violation", () => {
+ const code = [
+ "alert('test'); // eslint-disable-line no-alert",
+ "console.log('test'); // eslint-disable-line no-console",
+ "alert('test');" // here
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ });
+
+ it("should report a violation if eslint-disable-line in a block comment is not on a single line", () => {
+ const code = [
+ "/* eslint-disable-line",
+ "*",
+ "*/ console.log('test');" // here
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-console": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ });
+
+ it("should not disable rule and add an extra report if eslint-disable-line in a block comment is not on a single line", () => {
+ const code = [
+ "alert('test'); /* eslint-disable-line ",
+ "no-alert */"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1
+ }
+ };
+
+ const messages = linter.verify(code, config);
+
+ assert.deepStrictEqual(messages, [
+ {
+ ruleId: "no-alert",
+ severity: 1,
+ line: 1,
+ column: 1,
+ endLine: 1,
+ endColumn: 14,
+ message: "Unexpected alert.",
+ messageId: "unexpected",
+ nodeType: "CallExpression"
+ },
+ {
+ ruleId: null,
+ severity: 2,
+ message: "eslint-disable-line comment should not span multiple lines.",
+ line: 1,
+ column: 16,
+ endLine: 2,
+ endColumn: 12,
+ nodeType: null
+ }
+ ]);
+ });
+
+ it("should not report a violation for eslint-disable-line in block comment", () => {
+ const code = [
+ "alert('test'); // eslint-disable-line no-alert",
+ "alert('test'); /*eslint-disable-line no-alert*/"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not report a violation", () => {
+ const code = [
+ "alert('test'); // eslint-disable-line no-alert",
+ "console.log('test'); // eslint-disable-line no-console"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not report a violation", () => {
+ const code = [
+ "alert('test') // eslint-disable-line no-alert, quotes, semi",
+ "console.log('test'); // eslint-disable-line"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ quotes: [1, "double"],
+ semi: [1, "always"],
+ "no-console": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not report a violation", () => {
+ const code = [
+ "alert('test') /* eslint-disable-line no-alert, quotes, semi */",
+ "console.log('test'); /* eslint-disable-line */"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ quotes: [1, "double"],
+ semi: [1, "always"],
+ "no-console": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should ignore violations of multiple rules when specified in mixed comments", () => {
+ const code = [
+ " alert(\"test\"); /* eslint-disable-line no-alert */ // eslint-disable-line quotes"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ quotes: [1, "single"]
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should report no violation", () => {
+ const code = [
+ "var foo1; // eslint-disable-line no-unused-vars",
+ "var foo2; // eslint-disable-line no-unused-vars",
+ "var foo3; // eslint-disable-line no-unused-vars",
+ "var foo4; // eslint-disable-line no-unused-vars",
+ "var foo5; // eslint-disable-line no-unused-vars"
+ ].join("\n");
+ const config = { rules: { "no-unused-vars": 2 } };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ });
+
+ describe("/*eslint-disable-next-line*/", () => {
+ it("should ignore violation of specified rule on next line", () => {
+ const code = [
+ "// eslint-disable-next-line no-alert",
+ "alert('test');",
+ "console.log('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-console");
+ });
+
+ it("should ignore violation of specified rule if eslint-disable-next-line is a block comment", () => {
+ const code = [
+ "/* eslint-disable-next-line no-alert */",
+ "alert('test');",
+ "console.log('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-console");
+ });
+ it("should ignore violation of specified rule if eslint-disable-next-line is a block comment", () => {
+ const code = [
+ "/* eslint-disable-next-line no-alert */",
+ "alert('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should not ignore violation if block comment is not on a single line", () => {
+ const code = [
+ "/* eslint-disable-next-line",
+ "no-alert */alert('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(messages[1].ruleId, "no-alert");
+ });
+
+ it("should ignore violations only of specified rule", () => {
+ const code = [
+ "// eslint-disable-next-line no-console",
+ "alert('test');",
+ "console.log('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ });
+
+ it("should ignore violations of multiple rules when specified", () => {
+ const code = [
+ "// eslint-disable-next-line no-alert, quotes",
+ "alert(\"test\");",
+ "console.log('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ quotes: [1, "single"],
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-console");
+ });
+
+ it("should ignore violations of multiple rules when specified in mixed comments", () => {
+ const code = [
+ "/* eslint-disable-next-line no-alert */ // eslint-disable-next-line quotes",
+ "alert(\"test\");"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ quotes: [1, "single"]
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should ignore violations of only the specified rule on next line", () => {
+ const code = [
+ "// eslint-disable-next-line quotes",
+ "alert(\"test\");",
+ "console.log('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ quotes: [1, "single"],
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ });
+
+ it("should ignore violations of specified rule on next line only", () => {
+ const code = [
+ "alert('test');",
+ "// eslint-disable-next-line no-alert",
+ "alert('test');",
+ "console.log('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ });
+
+ it("should ignore all rule violations on next line if none specified", () => {
+ const code = [
+ "// eslint-disable-next-line",
+ "alert(\"test\");",
+ "console.log('test')"
+ ].join("\n");
+ const config = {
+ rules: {
+ semi: [1, "never"],
+ quotes: [1, "single"],
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-console");
+ });
+
+ it("should ignore violations if eslint-disable-next-line is a block comment", () => {
+ const code = [
+ "alert('test');",
+ "/* eslint-disable-next-line no-alert */",
+ "alert('test');",
+ "console.log('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ });
+
+ it("should report a violation", () => {
+ const code = [
+ "/* eslint-disable-next-line",
+ "*",
+ "*/",
+ "console.log('test');" // here
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ });
+
+ it("should not ignore violations if comment is of the type hashbang", () => {
+ const code = [
+ "#! eslint-disable-next-line no-alert",
+ "alert('test');",
+ "console.log('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1,
+ "no-console": 1
+ }
+ };
+ const messages = linter.verify(code, config, filename);
+
+ assert.strictEqual(messages.length, 2);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ assert.strictEqual(messages[1].ruleId, "no-console");
+ });
+ });
+
+ describe("descriptions in directive comments", () => {
+ it("should ignore the part preceded by '--' in '/*eslint*/'.", () => {
+ const aaa = sinon.stub().returns({});
+ const bbb = sinon.stub().returns({});
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ aaa: { create: aaa },
+ bbb: { create: bbb }
+ }
+ }
+ }
+ };
+
+ const messages = linter.verify(`
+ /*eslint test/aaa:error -- test/bbb:error */
+ console.log("hello")
+ `, config);
+
+ // Don't include syntax error of the comment.
+ assert.deepStrictEqual(messages, []);
+
+ // Use only `aaa`.
+ assert.strictEqual(aaa.callCount, 1);
+ assert.strictEqual(bbb.callCount, 0);
+ });
+
+ it("should ignore the part preceded by '--' in '/*globals*/'.", () => {
+ const messages = linter.verify(`
+ /*globals aaa -- bbb */
+ var aaa = {}
+ var bbb = {}
+ `, {
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { "no-redeclare": "error" }
+ });
+
+ // Don't include `bbb`
+ assert.deepStrictEqual(
+ messages,
+ [{
+ column: 31,
+ endColumn: 34,
+ line: 2,
+ endLine: 2,
+ message: "'aaa' is already defined by a variable declaration.",
+ messageId: "redeclaredBySyntax",
+ nodeType: "Block",
+ ruleId: "no-redeclare",
+ severity: 2
+ }]
+ );
+ });
+
+ it("should ignore the part preceded by '--' in '/*exported*/'.", () => {
+ const messages = linter.verify(`
+ /*exported aaa -- bbb */
+ var aaa = {}
+ var bbb = {}
+ `, {
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: { "no-unused-vars": "error" }
+ });
+
+ // Don't include `aaa`
+ assert.deepStrictEqual(
+ messages,
+ [{
+ column: 25,
+ endColumn: 28,
+ endLine: 4,
+ line: 4,
+ message: "'bbb' is assigned a value but never used.",
+ messageId: "unusedVar",
+ nodeType: "Identifier",
+ ruleId: "no-unused-vars",
+ severity: 2
+ }]
+ );
+ });
+
+ it("should ignore the part preceded by '--' in '/*eslint-disable*/'.", () => {
+ const messages = linter.verify(`
+ /*eslint-disable no-redeclare -- no-unused-vars */
+ var aaa = {}
+ var aaa = {}
+ `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } });
+
+ // Do include `no-unused-vars` but not `no-redeclare`
+ assert.deepStrictEqual(
+ messages,
+ [{
+ column: 25,
+ endLine: 4,
+ endColumn: 28,
+ line: 4,
+ message: "'aaa' is assigned a value but never used.",
+ messageId: "unusedVar",
+ nodeType: "Identifier",
+ ruleId: "no-unused-vars",
+ severity: 2
+ }]
+ );
+ });
+
+ it("should ignore the part preceded by '--' in '/*eslint-enable*/'.", () => {
+ const messages = linter.verify(`
+ /*eslint-disable no-redeclare, no-unused-vars */
+ /*eslint-enable no-redeclare -- no-unused-vars */
+ var aaa = {}
+ var aaa = {}
+ `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } });
+
+ // Do include `no-redeclare` but not `no-unused-vars`
+ assert.deepStrictEqual(
+ messages,
+ [{
+ column: 25,
+ endLine: 5,
+ endColumn: 28,
+ line: 5,
+ message: "'aaa' is already defined.",
+ messageId: "redeclared",
+ nodeType: "Identifier",
+ ruleId: "no-redeclare",
+ severity: 2
+ }]
+ );
+ });
+
+ it("should ignore the part preceded by '--' in '//eslint-disable-line'.", () => {
+ const messages = linter.verify(`
+ var aaa = {} //eslint-disable-line no-redeclare -- no-unused-vars
+ var aaa = {} //eslint-disable-line no-redeclare -- no-unused-vars
+ `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } });
+
+ // Do include `no-unused-vars` but not `no-redeclare`
+ assert.deepStrictEqual(
+ messages,
+ [{
+ column: 25,
+ endLine: 3,
+ endColumn: 28,
+ line: 3,
+ message: "'aaa' is assigned a value but never used.",
+ messageId: "unusedVar",
+ nodeType: "Identifier",
+ ruleId: "no-unused-vars",
+ severity: 2
+ }]
+ );
+ });
+
+ it("should ignore the part preceded by '--' in '/*eslint-disable-line*/'.", () => {
+ const messages = linter.verify(`
+ var aaa = {} /*eslint-disable-line no-redeclare -- no-unused-vars */
+ var aaa = {} /*eslint-disable-line no-redeclare -- no-unused-vars */
+ `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } });
+
+ // Do include `no-unused-vars` but not `no-redeclare`
+ assert.deepStrictEqual(
+ messages,
+ [{
+ column: 25,
+ endLine: 3,
+ endColumn: 28,
+ line: 3,
+ message: "'aaa' is assigned a value but never used.",
+ messageId: "unusedVar",
+ nodeType: "Identifier",
+ ruleId: "no-unused-vars",
+ severity: 2
+ }]
+ );
+ });
+
+ it("should ignore the part preceded by '--' in '//eslint-disable-next-line'.", () => {
+ const messages = linter.verify(`
+ //eslint-disable-next-line no-redeclare -- no-unused-vars
+ var aaa = {}
+ //eslint-disable-next-line no-redeclare -- no-unused-vars
+ var aaa = {}
+ `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } });
+
+ // Do include `no-unused-vars` but not `no-redeclare`
+ assert.deepStrictEqual(
+ messages,
+ [{
+ column: 25,
+ endLine: 5,
+ endColumn: 28,
+ line: 5,
+ message: "'aaa' is assigned a value but never used.",
+ messageId: "unusedVar",
+ nodeType: "Identifier",
+ ruleId: "no-unused-vars",
+ severity: 2
+ }]
+ );
+ });
+
+ it("should ignore the part preceded by '--' in '/*eslint-disable-next-line*/'.", () => {
+ const messages = linter.verify(`
+ /*eslint-disable-next-line no-redeclare -- no-unused-vars */
+ var aaa = {}
+ /*eslint-disable-next-line no-redeclare -- no-unused-vars */
+ var aaa = {}
+ `, { rules: { "no-redeclare": "error", "no-unused-vars": "error" } });
+
+ // Do include `no-unused-vars` but not `no-redeclare`
+ assert.deepStrictEqual(
+ messages,
+ [{
+ column: 25,
+ endLine: 5,
+ endColumn: 28,
+ line: 5,
+ message: "'aaa' is assigned a value but never used.",
+ messageId: "unusedVar",
+ nodeType: "Identifier",
+ ruleId: "no-unused-vars",
+ severity: 2
+ }]
+ );
+ });
+
+ it("should not ignore the part preceded by '--' if the '--' is not surrounded by whitespaces.", () => {
+ const rule = sinon.stub().returns({});
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "a--rule": { create: rule }
+ }
+ }
+ }
+ };
+
+ const messages = linter.verify(`
+ /*eslint test/a--rule:error */
+ console.log("hello")
+ `, config);
+
+ // Don't include syntax error of the comment.
+ assert.deepStrictEqual(messages, []);
+
+ // Use `a--rule`.
+ assert.strictEqual(rule.callCount, 1);
+ });
+
+ it("should ignore the part preceded by '--' even if the '--' is longer than 2.", () => {
+ const aaa = sinon.stub().returns({});
+ const bbb = sinon.stub().returns({});
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ aaa: { create: aaa },
+ bbb: { create: bbb }
+ }
+ }
+ }
+ };
+
+ const messages = linter.verify(`
+ /*eslint test/aaa:error -------- test/bbb:error */
+ console.log("hello")
+ `, config);
+
+ // Don't include syntax error of the comment.
+ assert.deepStrictEqual(messages, []);
+
+ // Use only `aaa`.
+ assert.strictEqual(aaa.callCount, 1);
+ assert.strictEqual(bbb.callCount, 0);
+ });
+
+ it("should ignore the part preceded by '--' with line breaks.", () => {
+ const aaa = sinon.stub().returns({});
+ const bbb = sinon.stub().returns({});
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ aaa: { create: aaa },
+ bbb: { create: bbb }
+ }
+ }
+ }
+ };
+
+ const messages = linter.verify(`
+ /*eslint test/aaa:error
+ --------
+ test/bbb:error */
+ console.log("hello")
+ `, config);
+
+ // Don't include syntax error of the comment.
+ assert.deepStrictEqual(messages, []);
+
+ // Use only `aaa`.
+ assert.strictEqual(aaa.callCount, 1);
+ assert.strictEqual(bbb.callCount, 0);
+ });
+ });
+
+ describe("allowInlineConfig option", () => {
+ describe("when evaluating code with comments to change config when allowInlineConfig is enabled", () => {
+ it("should report a violation for disabling rules", () => {
+ const code = [
+ "alert('test'); // eslint-disable-line no-alert"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, {
+ filename,
+ allowInlineConfig: false
+ });
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ });
+
+ it("should report a violation for global variable declarations", () => {
+ let ok = false;
+ const code = [
+ "/* global foo */"
+ ].join("\n");
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ test(context) {
+ return {
+ Program() {
+ const scope = context.getScope();
+ const sourceCode = context.getSourceCode();
+ const comments = sourceCode.getAllComments();
+
+ assert.strictEqual(1, comments.length);
+
+ const foo = getVariable(scope, "foo");
+
+ assert.notOk(foo);
+
+ ok = true;
+ }
+ };
+ }
+ }
+ }
+ },
+ rules: {
+ "test/test": 2
+ }
+ };
+
+ linter.verify(code, config, { allowInlineConfig: false });
+ assert(ok);
+ });
+
+ it("should report a violation for eslint-disable", () => {
+ const code = [
+ "/* eslint-disable */",
+ "alert('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, {
+ filename,
+ allowInlineConfig: false
+ });
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ });
+
+ it("should not report a violation for rule changes", () => {
+ const code = [
+ "/*eslint no-alert:2*/",
+ "alert('test');"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 0
+ }
+ };
+
+ const messages = linter.verify(code, config, {
+ filename,
+ allowInlineConfig: false
+ });
+
+ assert.strictEqual(messages.length, 0);
+ });
+
+ it("should report a violation for disable-line", () => {
+ const code = [
+ "alert('test'); // eslint-disable-line"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 2
+ }
+ };
+
+ const messages = linter.verify(code, config, {
+ filename,
+ allowInlineConfig: false
+ });
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].ruleId, "no-alert");
+ });
+
+ });
+
+ describe("when evaluating code with 'noInlineConfig'", () => {
+ for (const directive of [
+ "globals foo",
+ "global foo",
+ "exported foo",
+ "eslint eqeqeq: error",
+ "eslint-disable eqeqeq",
+ "eslint-disable-line eqeqeq",
+ "eslint-disable-next-line eqeqeq",
+ "eslint-enable eqeqeq"
+ ]) {
+ // eslint-disable-next-line no-loop-func -- No closures
+ it(`should warn '/* ${directive} */' if 'noInlineConfig' was given.`, () => {
+ const messages = linter.verify(`/* ${directive} */`, {
+ linterOptions: {
+ noInlineConfig: true
+ }
+ });
+
+ assert.deepStrictEqual(messages.length, 1);
+ assert.deepStrictEqual(messages[0].fatal, void 0);
+ assert.deepStrictEqual(messages[0].ruleId, null);
+ assert.deepStrictEqual(messages[0].severity, 1);
+ assert.deepStrictEqual(messages[0].message, `'/*${directive.split(" ")[0]}*/' has no effect because you have 'noInlineConfig' setting in your config.`);
+ });
+ }
+
+ for (const directive of [
+ "eslint-disable-line eqeqeq",
+ "eslint-disable-next-line eqeqeq"
+ ]) {
+ // eslint-disable-next-line no-loop-func -- No closures
+ it(`should warn '// ${directive}' if 'noInlineConfig' was given.`, () => {
+ const messages = linter.verify(`// ${directive}`, {
+ linterOptions: {
+ noInlineConfig: true
+ }
+ });
+
+ assert.deepStrictEqual(messages.length, 1);
+ assert.deepStrictEqual(messages[0].fatal, void 0);
+ assert.deepStrictEqual(messages[0].ruleId, null);
+ assert.deepStrictEqual(messages[0].severity, 1);
+ assert.deepStrictEqual(messages[0].message, `'//${directive.split(" ")[0]}' has no effect because you have 'noInlineConfig' setting in your config.`);
+ });
+ }
+
+ it("should not warn if 'noInlineConfig' and '--no-inline-config' were given.", () => {
+ const messages = linter.verify("/* globals foo */", {
+ linterOptions: {
+ noInlineConfig: true
+ }
+ }, { allowInlineConfig: false });
+
+ assert.deepStrictEqual(messages.length, 0);
+ });
+ });
+
+ describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => {
+ it("should not report a violation", () => {
+ const code = [
+ "alert('test'); // eslint-disable-line no-alert"
+ ].join("\n");
+ const config = {
+ rules: {
+ "no-alert": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, {
+ filename,
+ allowInlineConfig: true
+ });
+
+ assert.strictEqual(messages.length, 0);
+ });
+ });
+
+ });
+
+ describe("reportUnusedDisableDirectives option", () => {
+ it("reports problems for unused eslint-disable comments", () => {
+ assert.deepStrictEqual(
+ linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: true }),
+ [
+ {
+ ruleId: null,
+ message: "Unused eslint-disable directive (no problems were reported).",
+ line: 1,
+ column: 1,
+ fix: {
+ range: [0, 20],
+ text: " "
+ },
+ severity: 2,
+ nodeType: null
+ }
+ ]
+ );
+ });
+
+ it("reports problems for unused eslint-disable comments (error)", () => {
+ assert.deepStrictEqual(
+ linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "error" }),
+ [
+ {
+ ruleId: null,
+ message: "Unused eslint-disable directive (no problems were reported).",
+ line: 1,
+ column: 1,
+ fix: {
+ range: [0, 20],
+ text: " "
+ },
+ severity: 2,
+ nodeType: null
+ }
+ ]
+ );
+ });
+
+ it("reports problems for unused eslint-disable comments (warn)", () => {
+ assert.deepStrictEqual(
+ linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "warn" }),
+ [
+ {
+ ruleId: null,
+ message: "Unused eslint-disable directive (no problems were reported).",
+ line: 1,
+ column: 1,
+ fix: {
+ range: [0, 20],
+ text: " "
+ },
+ severity: 1,
+ nodeType: null
+ }
+ ]
+ );
+ });
+
+ it("reports problems for unused eslint-disable comments (in config)", () => {
+ assert.deepStrictEqual(
+ linter.verify("/* eslint-disable */", {
+ linterOptions: {
+ reportUnusedDisableDirectives: true
+ }
+ }),
+ [
+ {
+ ruleId: null,
+ message: "Unused eslint-disable directive (no problems were reported).",
+ line: 1,
+ column: 1,
+ fix: {
+ range: [0, 20],
+ text: " "
+ },
+ severity: 1,
+ nodeType: null
+ }
+ ]
+ );
+ });
+
+ it("reports problems for partially unused eslint-disable comments (in config)", () => {
+ const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare";
+ const config = {
+ linterOptions: {
+ reportUnusedDisableDirectives: true
+ },
+ rules: {
+ "no-alert": 1,
+ "no-redeclare": 1
+ }
+ };
+
+ const messages = linter.verify(code, config, {
+ filename,
+ allowInlineConfig: true
+ });
+
+ assert.deepStrictEqual(
+ messages,
+ [
+ {
+ ruleId: null,
+ message: "Unused eslint-disable directive (no problems were reported from 'no-redeclare').",
+ line: 1,
+ column: 16,
+ fix: {
+ range: [46, 60],
+ text: ""
+ },
+ severity: 1,
+ nodeType: null
+ }
+ ]
+ );
+ });
+
+ describe("autofix", () => {
+ const alwaysReportsRule = {
+ create(context) {
+ return {
+ Program(node) {
+ context.report({ message: "bad code", loc: node.loc.end });
+ }
+ };
+ }
+ };
+
+ const neverReportsRule = {
+ create() {
+ return {};
+ }
+ };
+
+ const ruleCount = 3;
+ const usedRules = Array.from(
+ { length: ruleCount },
+ (_, index) => `used${index ? `-${index}` : ""}` // "used", "used-1", "used-2"
+ );
+ const unusedRules = usedRules.map(name => `un${name}`); // "unused", "unused-1", "unused-2"
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {}
+ }
+ },
+ linterOptions: {
+ reportUnusedDisableDirectives: true
+ },
+ rules: {
+ ...Object.fromEntries(usedRules.map(name => [`test/${name}`, "error"])),
+ ...Object.fromEntries(unusedRules.map(name => [`test/${name}`, "error"]))
+ }
+ };
+
+ beforeEach(() => {
+ config.plugins.test.rules = {
+ ...Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule])),
+ ...Object.fromEntries(unusedRules.map(name => [name, neverReportsRule]))
+ };
+ });
+
+ const tests = [
+
+ //-----------------------------------------------
+ // Removing the entire comment
+ //-----------------------------------------------
+
+ {
+ code: "// eslint-disable-line test/unused",
+ output: " "
+ },
+ {
+ code: "foo// eslint-disable-line test/unused",
+ output: "foo "
+ },
+ {
+ code: "// eslint-disable-line ,test/unused,",
+ output: " "
+ },
+ {
+ code: "// eslint-disable-line test/unused-1, test/unused-2",
+ output: " "
+ },
+ {
+ code: "// eslint-disable-line ,test/unused-1,, test/unused-2,, -- comment",
+ output: " "
+ },
+ {
+ code: "// eslint-disable-next-line test/unused\n",
+ output: " \n"
+ },
+ {
+ code: "// eslint-disable-next-line test/unused\nfoo",
+ output: " \nfoo"
+ },
+ {
+ code: "/* eslint-disable \ntest/unused\n*/",
+ output: " "
+ },
+
+ //-----------------------------------------------
+ // Removing only individual rules
+ //-----------------------------------------------
+
+ // content before the first rule should not be changed
+ {
+ code: "//eslint-disable-line test/unused, test/used",
+ output: "//eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused, test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused, test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "/*\neslint-disable test/unused, test/used*/",
+ output: "/*\neslint-disable test/used*/"
+ },
+ {
+ code: "/*\n eslint-disable test/unused, test/used*/",
+ output: "/*\n eslint-disable test/used*/"
+ },
+ {
+ code: "/*\r\neslint-disable test/unused, test/used*/",
+ output: "/*\r\neslint-disable test/used*/"
+ },
+ {
+ code: "/*\u2028eslint-disable test/unused, test/used*/",
+ output: "/*\u2028eslint-disable test/used*/"
+ },
+ {
+ code: "/*\u00A0eslint-disable test/unused, test/used*/",
+ output: "/*\u00A0eslint-disable test/used*/"
+ },
+ {
+ code: "// eslint-disable-line test/unused, test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "/* eslint-disable\ntest/unused, test/used*/",
+ output: "/* eslint-disable\ntest/used*/"
+ },
+ {
+ code: "/* eslint-disable\n test/unused, test/used*/",
+ output: "/* eslint-disable\n test/used*/"
+ },
+ {
+ code: "/* eslint-disable\r\ntest/unused, test/used*/",
+ output: "/* eslint-disable\r\ntest/used*/"
+ },
+ {
+ code: "/* eslint-disable\u2028test/unused, test/used*/",
+ output: "/* eslint-disable\u2028test/used*/"
+ },
+ {
+ code: "/* eslint-disable\u00A0test/unused, test/used*/",
+ output: "/* eslint-disable\u00A0test/used*/"
+ },
+
+ // when removing the first rule, the comma and all whitespace up to the next rule (or next lone comma) should also be removed
+ {
+ code: "// eslint-disable-line test/unused,test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused, test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused , test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused, test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused ,test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "/* eslint-disable test/unused\n,\ntest/used */",
+ output: "/* eslint-disable test/used */"
+ },
+ {
+ code: "/* eslint-disable test/unused \n \n,\n\n test/used */",
+ output: "/* eslint-disable test/used */"
+ },
+ {
+ code: "/* eslint-disable test/unused\u2028,\u2028test/used */",
+ output: "/* eslint-disable test/used */"
+ },
+ {
+ code: "// eslint-disable-line test/unused\u00A0,\u00A0test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused,,test/used",
+ output: "// eslint-disable-line ,test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused, ,test/used",
+ output: "// eslint-disable-line ,test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused,, test/used",
+ output: "// eslint-disable-line , test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused,test/used ",
+ output: "// eslint-disable-line test/used "
+ },
+ {
+ code: "// eslint-disable-next-line test/unused,test/used\n",
+ output: "// eslint-disable-next-line test/used\n"
+ },
+
+ // when removing a rule in the middle, one comma and all whitespace between commas should also be removed
+ {
+ code: "// eslint-disable-line test/used-1,test/unused,test/used-2",
+ output: "// eslint-disable-line test/used-1,test/used-2"
+ },
+ {
+ code: "// eslint-disable-line test/used-1, test/unused,test/used-2",
+ output: "// eslint-disable-line test/used-1,test/used-2"
+ },
+ {
+ code: "// eslint-disable-line test/used-1,test/unused ,test/used-2",
+ output: "// eslint-disable-line test/used-1,test/used-2"
+ },
+ {
+ code: "// eslint-disable-line test/used-1, test/unused ,test/used-2",
+ output: "// eslint-disable-line test/used-1,test/used-2"
+ },
+ {
+ code: "/* eslint-disable test/used-1,\ntest/unused\n,test/used-2 */",
+ output: "/* eslint-disable test/used-1,test/used-2 */"
+ },
+ {
+ code: "/* eslint-disable test/used-1,\n\n test/unused \n \n ,test/used-2 */",
+ output: "/* eslint-disable test/used-1,test/used-2 */"
+ },
+ {
+ code: "/* eslint-disable test/used-1,\u2028test/unused\u2028,test/used-2 */",
+ output: "/* eslint-disable test/used-1,test/used-2 */"
+ },
+ {
+ code: "// eslint-disable-line test/used-1,\u00A0test/unused\u00A0,test/used-2",
+ output: "// eslint-disable-line test/used-1,test/used-2"
+ },
+
+ // when removing a rule in the middle, content around commas should not be changed
+ {
+ code: "// eslint-disable-line test/used-1, test/unused ,test/used-2",
+ output: "// eslint-disable-line test/used-1,test/used-2"
+ },
+ {
+ code: "// eslint-disable-line test/used-1,test/unused, test/used-2",
+ output: "// eslint-disable-line test/used-1, test/used-2"
+ },
+ {
+ code: "// eslint-disable-line test/used-1 ,test/unused,test/used-2",
+ output: "// eslint-disable-line test/used-1 ,test/used-2"
+ },
+ {
+ code: "// eslint-disable-line test/used-1 ,test/unused, test/used-2",
+ output: "// eslint-disable-line test/used-1 , test/used-2"
+ },
+ {
+ code: "// eslint-disable-line test/used-1 , test/unused , test/used-2",
+ output: "// eslint-disable-line test/used-1 , test/used-2"
+ },
+ {
+ code: "/* eslint-disable test/used-1\n,test/unused,\ntest/used-2 */",
+ output: "/* eslint-disable test/used-1\n,\ntest/used-2 */"
+ },
+ {
+ code: "/* eslint-disable test/used-1\u2028,test/unused,\u2028test/used-2 */",
+ output: "/* eslint-disable test/used-1\u2028,\u2028test/used-2 */"
+ },
+ {
+ code: "// eslint-disable-line test/used-1\u00A0,test/unused,\u00A0test/used-2",
+ output: "// eslint-disable-line test/used-1\u00A0,\u00A0test/used-2"
+ },
+ {
+ code: "// eslint-disable-line , test/unused ,test/used",
+ output: "// eslint-disable-line ,test/used"
+ },
+ {
+ code: "/* eslint-disable\n, test/unused ,test/used */",
+ output: "/* eslint-disable\n,test/used */"
+ },
+ {
+ code: "/* eslint-disable test/used-1,\n,test/unused,test/used-2 */",
+ output: "/* eslint-disable test/used-1,\n,test/used-2 */"
+ },
+ {
+ code: "/* eslint-disable test/used-1,test/unused,\n,test/used-2 */",
+ output: "/* eslint-disable test/used-1,\n,test/used-2 */"
+ },
+ {
+ code: "/* eslint-disable test/used-1,\n,test/unused,\n,test/used-2 */",
+ output: "/* eslint-disable test/used-1,\n,\n,test/used-2 */"
+ },
+ {
+ code: "// eslint-disable-line test/used, test/unused,",
+ output: "// eslint-disable-line test/used,"
+ },
+ {
+ code: "// eslint-disable-next-line test/used, test/unused,\n",
+ output: "// eslint-disable-next-line test/used,\n"
+ },
+ {
+ code: "// eslint-disable-line test/used, test/unused, ",
+ output: "// eslint-disable-line test/used, "
+ },
+ {
+ code: "// eslint-disable-line test/used, test/unused, -- comment",
+ output: "// eslint-disable-line test/used, -- comment"
+ },
+ {
+ code: "/* eslint-disable test/used, test/unused,\n*/",
+ output: "/* eslint-disable test/used,\n*/"
+ },
+
+ // when removing the last rule, the comma and all whitespace up to the previous rule (or previous lone comma) should also be removed
+ {
+ code: "// eslint-disable-line test/used,test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used, test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used ,test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used , test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used, test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used ,test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "/* eslint-disable test/used\n,\ntest/unused */",
+ output: "/* eslint-disable test/used */"
+ },
+ {
+ code: "/* eslint-disable test/used \n \n,\n\n test/unused */",
+ output: "/* eslint-disable test/used */"
+ },
+ {
+ code: "/* eslint-disable test/used\u2028,\u2028test/unused */",
+ output: "/* eslint-disable test/used */"
+ },
+ {
+ code: "// eslint-disable-line test/used\u00A0,\u00A0test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used,,test/unused",
+ output: "// eslint-disable-line test/used,"
+ },
+ {
+ code: "// eslint-disable-line test/used, ,test/unused",
+ output: "// eslint-disable-line test/used,"
+ },
+ {
+ code: "/* eslint-disable test/used,\n,test/unused */",
+ output: "/* eslint-disable test/used, */"
+ },
+ {
+ code: "/* eslint-disable test/used\n, ,test/unused */",
+ output: "/* eslint-disable test/used\n, */"
+ },
+
+ // content after the last rule should not be changed
+ {
+ code: "// eslint-disable-line test/used,test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used,test/unused ",
+ output: "// eslint-disable-line test/used "
+ },
+ {
+ code: "// eslint-disable-line test/used,test/unused ",
+ output: "// eslint-disable-line test/used "
+ },
+ {
+ code: "// eslint-disable-line test/used,test/unused -- comment",
+ output: "// eslint-disable-line test/used -- comment"
+ },
+ {
+ code: "// eslint-disable-next-line test/used,test/unused\n",
+ output: "// eslint-disable-next-line test/used\n"
+ },
+ {
+ code: "// eslint-disable-next-line test/used,test/unused \n",
+ output: "// eslint-disable-next-line test/used \n"
+ },
+ {
+ code: "/* eslint-disable test/used,test/unused\u2028*/",
+ output: "/* eslint-disable test/used\u2028*/"
+ },
+ {
+ code: "// eslint-disable-line test/used,test/unused\u00A0",
+ output: "// eslint-disable-line test/used\u00A0"
+ },
+
+ // multiply rules to remove
+ {
+ code: "// eslint-disable-line test/used, test/unused-1, test/unused-2",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused-1, test/used, test/unused-2",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused-1, test/unused-2, test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used-1, test/unused-1, test/used-2, test/unused-2",
+ output: "// eslint-disable-line test/used-1, test/used-2"
+ },
+ {
+ code: "// eslint-disable-line test/unused-1, test/used-1, test/unused-2, test/used-2",
+ output: "// eslint-disable-line test/used-1, test/used-2"
+ },
+ {
+ code: `
+ /* eslint-disable test/unused-1,
+ test/used-1,
+ test/unused-2,
+ test/used-2
+ */
+ `,
+ output: `
+ /* eslint-disable test/used-1,
+ test/used-2
+ */
+ `
+ },
+ {
+ code: `
+ /* eslint-disable
+ test/unused-1,
+ test/used-1,
+ test/unused-2,
+ test/used-2
+ */
+ `,
+ output: `
+ /* eslint-disable
+ test/used-1,
+ test/used-2
+ */
+ `
+ },
+ {
+ code: `
+ /* eslint-disable
+ test/used-1,
+ test/unused-1,
+ test/used-2,
+ test/unused-2
+ */
+ `,
+ output: `
+ /* eslint-disable
+ test/used-1,
+ test/used-2
+ */
+ `
+ },
+ {
+ code: `
+ /* eslint-disable
+ test/used-1,
+ test/unused-1,
+ test/used-2,
+ test/unused-2,
+ */
+ `,
+ output: `
+ /* eslint-disable
+ test/used-1,
+ test/used-2,
+ */
+ `
+ },
+ {
+ code: `
+ /* eslint-disable
+ ,test/unused-1
+ ,test/used-1
+ ,test/unused-2
+ ,test/used-2
+ */
+ `,
+ output: `
+ /* eslint-disable
+ ,test/used-1
+ ,test/used-2
+ */
+ `
+ },
+ {
+ code: `
+ /* eslint-disable
+ ,test/used-1
+ ,test/unused-1
+ ,test/used-2
+ ,test/unused-2
+ */
+ `,
+ output: `
+ /* eslint-disable
+ ,test/used-1
+ ,test/used-2
+ */
+ `
+ },
+ {
+ code: `
+ /* eslint-disable
+ test/used-1,
+ test/unused-1,
+ test/used-2,
+ test/unused-2
+
+ -- comment
+ */
+ `,
+ output: `
+ /* eslint-disable
+ test/used-1,
+ test/used-2
+
+ -- comment
+ */
+ `
+ },
+
+ // duplicates in the list
+ {
+ code: "// eslint-disable-line test/unused, test/unused, test/used",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/unused, test/used, test/unused",
+ output: "// eslint-disable-line test/used"
+ },
+ {
+ code: "// eslint-disable-line test/used, test/unused, test/unused, test/used",
+ output: "// eslint-disable-line test/used, test/used"
+ }
+ ];
+
+ for (const { code, output } of tests) {
+ // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach()
+ it(code, () => {
+ assert.strictEqual(
+ linter.verifyAndFix(code, config).output,
+ output
+ );
+ });
+ }
+ });
+ });
+
+ });
+
+ describe("Default Global Variables", () => {
+ const code = "x";
+
+ it("builtin global variables should be available in the global scope", () => {
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.notStrictEqual(getVariable(scope, "Object"), null);
+ assert.notStrictEqual(getVariable(scope, "Array"), null);
+ assert.notStrictEqual(getVariable(scope, "undefined"), null);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: "script"
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce, "Rule should have been called.");
+ });
+
+ it("ES6 global variables should be available by default", () => {
+ let spy;
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ checker: context => {
+ spy = sinon.spy(() => {
+ const scope = context.getScope();
+
+ assert.notStrictEqual(getVariable(scope, "Promise"), null);
+ assert.notStrictEqual(getVariable(scope, "Symbol"), null);
+ assert.notStrictEqual(getVariable(scope, "WeakMap"), null);
+ });
+
+ return { Program: spy };
+ }
+ }
+ }
+ },
+ languageOptions: {
+ sourceType: "script"
+ },
+ rules: {
+ "test/checker": "error"
+ }
+ };
+
+ linter.verify(code, config);
+ assert(spy && spy.calledOnce);
+ });
+
+ });
+
+ describe("Suggestions", () => {
+ it("provides suggestion information for tools to use", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "rule-with-suggestions": {
+ meta: { hasSuggestions: true },
+ create: context => ({
+ Program(node) {
+ context.report({
+ node,
+ message: "Incorrect spacing",
+ suggest: [{
+ desc: "Insert space at the beginning",
+ fix: fixer => fixer.insertTextBefore(node, " ")
+ }, {
+ desc: "Insert space at the end",
+ fix: fixer => fixer.insertTextAfter(node, " ")
+ }]
+ });
+ }
+ })
+ }
+ }
+ }
+ },
+ rules: {
+ "test/rule-with-suggestions": "error"
+ }
+ };
+
+ const messages = linter.verify("var a = 1;", config);
+
+ assert.deepStrictEqual(messages[0].suggestions, [{
+ desc: "Insert space at the beginning",
+ fix: {
+ range: [0, 0],
+ text: " "
+ }
+ }, {
+ desc: "Insert space at the end",
+ fix: {
+ range: [10, 10],
+ text: " "
+ }
+ }]);
+ });
+
+ it("supports messageIds for suggestions", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "rule-with-suggestions": {
+ meta: {
+ messages: {
+ suggestion1: "Insert space at the beginning",
+ suggestion2: "Insert space at the end"
+ },
+ hasSuggestions: true
+ },
+ create: context => ({
+ Program(node) {
+ context.report({
+ node,
+ message: "Incorrect spacing",
+ suggest: [{
+ messageId: "suggestion1",
+ fix: fixer => fixer.insertTextBefore(node, " ")
+ }, {
+ messageId: "suggestion2",
+ fix: fixer => fixer.insertTextAfter(node, " ")
+ }]
+ });
+ }
+ })
+ }
+ }
+ }
+ },
+ rules: {
+ "test/rule-with-suggestions": "error"
+ }
+ };
+
+ const messages = linter.verify("var a = 1;", config);
+
+ assert.deepStrictEqual(messages[0].suggestions, [{
+ messageId: "suggestion1",
+ desc: "Insert space at the beginning",
+ fix: {
+ range: [0, 0],
+ text: " "
+ }
+ }, {
+ messageId: "suggestion2",
+ desc: "Insert space at the end",
+ fix: {
+ range: [10, 10],
+ text: " "
+ }
+ }]);
+ });
+
+ it("should throw an error if suggestion is passed but `meta.hasSuggestions` property is not enabled", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "rule-with-suggestions": {
+ meta: { docs: {}, schema: [] },
+ create: context => ({
+ Program(node) {
+ context.report({
+ node,
+ message: "hello world",
+ suggest: [{ desc: "convert to foo", fix: fixer => fixer.insertTextBefore(node, " ") }]
+ });
+ }
+ })
+ }
+ }
+ }
+ },
+ rules: {
+ "test/rule-with-suggestions": "error"
+ }
+ };
+
+ assert.throws(() => {
+ linter.verify("0", config);
+ }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`.");
+ });
+
+ it("should throw an error if suggestion is passed but `meta.hasSuggestions` property is not enabled and the rule has the obsolete `meta.docs.suggestion` property", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "rule-with-meta-docs-suggestion": {
+ meta: { docs: { suggestion: true }, schema: [] },
+ create: context => ({
+ Program(node) {
+ context.report({
+ node,
+ message: "hello world",
+ suggest: [{ desc: "convert to foo", fix: fixer => fixer.insertTextBefore(node, " ") }]
+ });
+ }
+ })
+ }
+ }
+ }
+ },
+ rules: {
+ "test/rule-with-meta-docs-suggestion": "error"
+ }
+ };
+
+ assert.throws(() => {
+ linter.verify("0", config);
+ }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint.");
+ });
+ });
+
+
+ describe("Error Conditions", () => {
+ describe("when evaluating broken code", () => {
+ const code = BROKEN_TEST_CODE;
+
+ it("should report a violation with a useful parse error prefix", () => {
+ const messages = linter.verify(code);
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.isNull(messages[0].ruleId);
+ assert.strictEqual(messages[0].line, 1);
+ assert.strictEqual(messages[0].column, 4);
+ assert.isTrue(messages[0].fatal);
+ assert.match(messages[0].message, /^Parsing error:/u);
+ });
+
+ it("should report source code where the issue is present", () => {
+ const inValidCode = [
+ "var x = 20;",
+ "if (x ==4 {",
+ " x++;",
+ "}"
+ ];
+ const messages = linter.verify(inValidCode.join("\n"));
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].severity, 2);
+ assert.isTrue(messages[0].fatal);
+ assert.match(messages[0].message, /^Parsing error:/u);
+ });
+ });
+
+ describe("when using a rule which has been replaced", () => {
+ const code = TEST_CODE;
+
+ it("should report the new rule", () => {
+
+ assert.throws(() => {
+ linter.verify(code, { rules: { "no-comma-dangle": 2 } });
+ }, /Key "rules": Key "no-comma-dangle": Rule "no-comma-dangle" was removed and replaced by "comma-dangle"/u);
+
+ });
+ });
+
+ });
+ });
+
+ describe("getSourceCode()", () => {
+ const code = TEST_CODE;
+
+ it("should retrieve SourceCode object after reset", () => {
+ linter.verify(code, {}, filename, true);
+
+ const sourceCode = linter.getSourceCode();
+
+ assert.isObject(sourceCode);
+ assert.strictEqual(sourceCode.text, code);
+ assert.isObject(sourceCode.ast);
+ });
+
+ it("should retrieve SourceCode object without reset", () => {
+ linter.verify(code, {}, filename);
+
+ const sourceCode = linter.getSourceCode();
+
+ assert.isObject(sourceCode);
+ assert.strictEqual(sourceCode.text, code);
+ assert.isObject(sourceCode.ast);
+ });
+
+ });
+
+ describe("defineRule()", () => {
+ it("should throw an error when called in flat config mode", () => {
+ assert.throws(() => {
+ linter.defineRule("foo", () => {});
+ }, /This method cannot be used with flat config/u);
+ });
+ });
+
+ describe("defineRules()", () => {
+ it("should throw an error when called in flat config mode", () => {
+ assert.throws(() => {
+ linter.defineRules({});
+ }, /This method cannot be used with flat config/u);
+ });
+ });
+
+ describe("defineParser()", () => {
+ it("should throw an error when called in flat config mode", () => {
+ assert.throws(() => {
+ linter.defineParser("foo", {});
+ }, /This method cannot be used with flat config/u);
+ });
+ });
+
+ describe("getRules()", () => {
+ it("should throw an error when called in flat config mode", () => {
+ assert.throws(() => {
+ linter.getRules();
+ }, /This method cannot be used with flat config/u);
+ });
+ });
+
+ describe("version", () => {
+ it("should return current version number", () => {
+ const version = linter.version;
+
+ assert.isString(version);
+ assert.isTrue(parseInt(version[0], 10) >= 3);
+ });
+ });
+
+ describe("verifyAndFix()", () => {
+ it("Fixes the code", () => {
+ const messages = linter.verifyAndFix("var a", {
+ rules: {
+ semi: 2
+ }
+ }, { filename: "test.js" });
+
+ assert.strictEqual(messages.output, "var a;", "Fixes were applied correctly");
+ assert.isTrue(messages.fixed);
+ });
+
+ it("does not require a third argument", () => {
+ const fixResult = linter.verifyAndFix("var a", {
+ rules: {
+ semi: 2
+ }
+ });
+
+ assert.deepStrictEqual(fixResult, {
+ fixed: true,
+ messages: [],
+ output: "var a;"
+ });
+ });
+
+ it("does not include suggestions in autofix results", () => {
+ const fixResult = linter.verifyAndFix("var foo = /\\#/", {
+ rules: {
+ semi: 2,
+ "no-useless-escape": 2
+ }
+ });
+
+ assert.strictEqual(fixResult.output, "var foo = /\\#/;");
+ assert.strictEqual(fixResult.fixed, true);
+ assert.strictEqual(fixResult.messages[0].suggestions.length > 0, true);
+ });
+
+ it("does not apply autofixes when fix argument is `false`", () => {
+ const fixResult = linter.verifyAndFix("var a", {
+ rules: {
+ semi: 2
+ }
+ }, { fix: false });
+
+ assert.strictEqual(fixResult.fixed, false);
+ });
+
+ it("stops fixing after 10 passes", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "add-spaces": {
+ meta: {
+ fixable: "whitespace"
+ },
+ create(context) {
+ return {
+ Program(node) {
+ context.report({
+ node,
+ message: "Add a space before this node.",
+ fix: fixer => fixer.insertTextBefore(node, " ")
+ });
+ }
+ };
+ }
+ }
+ }
+ }
+ },
+ rules: {
+ "test/add-spaces": "error"
+ }
+ };
+
+ const fixResult = linter.verifyAndFix("a", config);
+
+ assert.strictEqual(fixResult.fixed, true);
+ assert.strictEqual(fixResult.output, `${" ".repeat(10)}a`);
+ assert.strictEqual(fixResult.messages.length, 1);
+ });
+
+ it("should throw an error if fix is passed but meta has no `fixable` property", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "test-rule": {
+ meta: {
+ docs: {},
+ schema: []
+ },
+ create: context => ({
+ Program(node) {
+ context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" }));
+ }
+ })
+ }
+ }
+ }
+ },
+ rules: {
+ "test/test-rule": "error"
+ }
+ };
+
+
+ assert.throws(() => {
+ linter.verify("0", config);
+ }, /Fixable rules must set the `meta\.fixable` property to "code" or "whitespace".\nOccurred while linting <input>:1\nRule: "test\/test-rule"$/u);
+ });
+
+ it("should throw an error if fix is passed and there is no metadata", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "test-rule": {
+ create: context => ({
+ Program(node) {
+ context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" }));
+ }
+ })
+ }
+ }
+ }
+ },
+ rules: {
+ "test/test-rule": "error"
+ }
+ };
+
+ assert.throws(() => {
+ linter.verify("0", config);
+ }, /Fixable rules must set the `meta\.fixable` property/u);
+ });
+
+ it("should throw an error if fix is passed from a legacy-format rule", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "test-rule": context => ({
+ Program(node) {
+ context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" }));
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ "test/test-rule": "error"
+ }
+ };
+
+
+ assert.throws(() => {
+ linter.verify("0", config);
+ }, /Fixable rules must set the `meta\.fixable` property/u);
+ });
+ });
+
+ describe("Mutability", () => {
+ let linter1 = null;
+ let linter2 = null;
+
+ beforeEach(() => {
+ linter1 = new Linter();
+ linter2 = new Linter();
+ });
+
+ describe("rules", () => {
+ it("with no changes, same rules are loaded", () => {
+ assert.sameDeepMembers(Array.from(linter1.getRules().keys()), Array.from(linter2.getRules().keys()));
+ });
+
+ it("loading rule in one doesn't change the other", () => {
+ linter1.defineRule("mock-rule", () => ({}));
+
+ assert.isTrue(linter1.getRules().has("mock-rule"), "mock rule is present");
+ assert.isFalse(linter2.getRules().has("mock-rule"), "mock rule is not present");
+ });
+ });
+ });
+
+
+ describe("processors", () => {
+ let receivedFilenames = [];
+ let receivedPhysicalFilenames = [];
+ const extraConfig = {
+ plugins: {
+ test: {
+ rules: {
+ "report-original-text": {
+ meta: {
+
+ },
+ create(context) {
+ return {
+ Program(ast) {
+ receivedFilenames.push(context.getFilename());
+ receivedPhysicalFilenames.push(context.getPhysicalFilename());
+ context.report({ node: ast, message: context.getSourceCode().text });
+ }
+ };
+ }
+ }
+ }
+ }
+ }
+ };
+
+ beforeEach(() => {
+ receivedFilenames = [];
+ receivedPhysicalFilenames = [];
+ });
+
+ describe("preprocessors", () => {
+ it("should receive text and filename.", () => {
+ const code = "foo bar baz";
+ const preprocess = sinon.spy(text => text.split(" "));
+ const configs = createFlatConfigArray({});
+
+ configs.normalizeSync();
+
+ linter.verify(code, configs, { filename, preprocess });
+
+ assert.strictEqual(preprocess.calledOnce, true, "preprocess wasn't called");
+ assert.deepStrictEqual(preprocess.args[0], [code, filename], "preprocess was called with the wrong arguments");
+ });
+
+ it("should run preprocess only once", () => {
+ const logs = [];
+ const config = {
+ files: ["*.md"],
+ processor: {
+ preprocess(text, filenameForText) {
+ logs.push({
+ text,
+ filename: filenameForText
+ });
+
+ return [{ text: "bar", filename: "0.js" }];
+ },
+ postprocess() {
+ return [];
+ }
+ }
+ };
+
+ linter.verify("foo", config, "a.md");
+ assert.strictEqual(logs.length, 1, "preprocess() should only be called once.");
+ });
+
+ it("should apply a preprocessor to the code, and lint each code sample separately", () => {
+ const code = "foo bar baz";
+ const configs = createFlatConfigArray([
+ extraConfig,
+ { rules: { "test/report-original-text": "error" } }
+ ]);
+
+ configs.normalizeSync();
+
+ const problems = linter.verify(
+ code,
+ configs,
+ {
+ filename,
+
+ // Apply a preprocessor that splits the source text into spaces and lints each word individually
+ preprocess(input) {
+ return input.split(" ");
+ }
+ }
+ );
+
+ assert.strictEqual(problems.length, 3);
+ assert.deepStrictEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]);
+ });
+
+ it("should apply a preprocessor to the code even if the preprocessor returned code block objects.", () => {
+ const code = "foo bar baz";
+ const configs = createFlatConfigArray([
+ extraConfig,
+ { rules: { "test/report-original-text": "error" } }
+ ]);
+
+ configs.normalizeSync();
+
+ const problems = linter.verify(
+ code,
+ configs,
+ {
+ filename,
+
+ // Apply a preprocessor that splits the source text into spaces and lints each word individually
+ preprocess(input) {
+ return input.split(" ").map(text => ({
+ filename: "block.js",
+ text
+ }));
+ }
+ }
+ );
+
+ assert.strictEqual(problems.length, 3);
+ assert.deepStrictEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]);
+
+ // filename
+ assert.strictEqual(receivedFilenames.length, 3);
+ assert(/^filename\.js[/\\]0_block\.js/u.test(receivedFilenames[0]));
+ assert(/^filename\.js[/\\]1_block\.js/u.test(receivedFilenames[1]));
+ assert(/^filename\.js[/\\]2_block\.js/u.test(receivedFilenames[2]));
+
+ // physical filename
+ assert.strictEqual(receivedPhysicalFilenames.length, 3);
+ assert.strictEqual(receivedPhysicalFilenames.every(name => name === filename), true);
+ });
+
+ it("should receive text even if a SourceCode object was given.", () => {
+ const code = "foo";
+ const preprocess = sinon.spy(text => text.split(" "));
+ const configs = createFlatConfigArray([
+ extraConfig
+ ]);
+
+ configs.normalizeSync();
+
+ linter.verify(code, configs);
+ const sourceCode = linter.getSourceCode();
+
+ linter.verify(sourceCode, configs, { filename, preprocess });
+
+ assert.strictEqual(preprocess.calledOnce, true);
+ assert.deepStrictEqual(preprocess.args[0], [code, filename]);
+ });
+
+ it("should receive text even if a SourceCode object was given (with BOM).", () => {
+ const code = "\uFEFFfoo";
+ const preprocess = sinon.spy(text => text.split(" "));
+ const configs = createFlatConfigArray([
+ extraConfig
+ ]);
+
+ configs.normalizeSync();
+
+ linter.verify(code, configs);
+ const sourceCode = linter.getSourceCode();
+
+ linter.verify(sourceCode, configs, { filename, preprocess });
+
+ assert.strictEqual(preprocess.calledOnce, true);
+ assert.deepStrictEqual(preprocess.args[0], [code, filename]);
+ });
+ });
+
+ describe("postprocessors", () => {
+ it("should receive result and filename.", () => {
+ const code = "foo bar baz";
+ const preprocess = sinon.spy(text => text.split(" "));
+ const postprocess = sinon.spy(text => [text]);
+ const configs = createFlatConfigArray([
+ extraConfig
+ ]);
+
+ configs.normalizeSync();
+
+ linter.verify(code, configs, { filename, postprocess, preprocess });
+
+ assert.strictEqual(postprocess.calledOnce, true);
+ assert.deepStrictEqual(postprocess.args[0], [[[], [], []], filename]);
+ });
+
+ it("should apply a postprocessor to the reported messages", () => {
+ const code = "foo bar baz";
+ const configs = createFlatConfigArray([
+ extraConfig,
+ { rules: { "test/report-original-text": "error" } }
+ ]);
+
+ configs.normalizeSync();
+
+ const problems = linter.verify(
+ code,
+ configs,
+ {
+ preprocess: input => input.split(" "),
+
+ /*
+ * Apply a postprocessor that updates the locations of the reported problems
+ * to make sure they correspond to the locations in the original text.
+ */
+ postprocess(problemLists) {
+ problemLists.forEach(problemList => assert.strictEqual(problemList.length, 1));
+ return problemLists.reduce(
+ (combinedList, problemList, index) =>
+ combinedList.concat(
+ problemList.map(
+ problem =>
+ Object.assign(
+ {},
+ problem,
+ {
+ message: problem.message.toUpperCase(),
+ column: problem.column + index * 4
+ }
+ )
+ )
+ ),
+ []
+ );
+ }
+ }
+ );
+
+ assert.strictEqual(problems.length, 3);
+ assert.deepStrictEqual(problems.map(problem => problem.message), ["FOO", "BAR", "BAZ"]);
+ assert.deepStrictEqual(problems.map(problem => problem.column), [1, 5, 9]);
+ });
+
+ it("should use postprocessed problem ranges when applying autofixes", () => {
+ const code = "foo bar baz";
+ const configs = createFlatConfigArray([
+ extraConfig,
+ {
+ plugins: {
+ test2: {
+ rules: {
+ "capitalize-identifiers": {
+ meta: {
+ fixable: "code"
+ },
+ create(context) {
+ return {
+ Identifier(node) {
+ if (node.name !== node.name.toUpperCase()) {
+ context.report({
+ node,
+ message: "Capitalize this identifier",
+ fix: fixer => fixer.replaceText(node, node.name.toUpperCase())
+ });
+ }
+ }
+ };
+ }
+ }
+ }
+ }
+ }
+ },
+ { rules: { "test2/capitalize-identifiers": "error" } }
+ ]);
+
+ configs.normalizeSync();
+
+ const fixResult = linter.verifyAndFix(
+ code,
+ configs,
+ {
+
+ /*
+ * Apply a postprocessor that updates the locations of autofixes
+ * to make sure they correspond to locations in the original text.
+ */
+ preprocess: input => input.split(" "),
+ postprocess(problemLists) {
+ return problemLists.reduce(
+ (combinedProblems, problemList, blockIndex) =>
+ combinedProblems.concat(
+ problemList.map(problem =>
+ Object.assign(problem, {
+ fix: {
+ text: problem.fix.text,
+ range: problem.fix.range.map(
+ rangeIndex => rangeIndex + blockIndex * 4
+ )
+ }
+ }))
+ ),
+ []
+ );
+ }
+ }
+ );
+
+ assert.strictEqual(fixResult.fixed, true);
+ assert.strictEqual(fixResult.messages.length, 0);
+ assert.strictEqual(fixResult.output, "FOO BAR BAZ");
+ });
+ });
+ });
+
+ describe("Edge cases", () => {
+
+ describe("Modules", () => {
+ const moduleConfig = {
+ languageOptions: {
+ sourceType: "module",
+ ecmaVersion: 6
+ }
+ };
+
+ it("should properly parse import statements when sourceType is module", () => {
+ const code = "import foo from 'foo';";
+ const messages = linter.verify(code, moduleConfig);
+
+ assert.strictEqual(messages.length, 0, "Unexpected linting error.");
+ });
+
+ it("should properly parse import all statements when sourceType is module", () => {
+ const code = "import * as foo from 'foo';";
+ const messages = linter.verify(code, moduleConfig);
+
+ assert.strictEqual(messages.length, 0, "Unexpected linting error.");
+ });
+
+ it("should properly parse default export statements when sourceType is module", () => {
+ const code = "export default function initialize() {}";
+ const messages = linter.verify(code, moduleConfig);
+
+ assert.strictEqual(messages.length, 0, "Unexpected linting error.");
+ });
+
+ });
+
+
+ // https://github.com/eslint/eslint/issues/9687
+ it("should report an error when invalid languageOptions found", () => {
+ let messages = linter.verify("", { languageOptions: { ecmaVersion: 222 } });
+
+ assert.deepStrictEqual(messages.length, 1);
+ assert.ok(messages[0].message.includes("Invalid ecmaVersion"));
+
+ assert.throws(() => {
+ linter.verify("", { languageOptions: { sourceType: "foo" } });
+ }, /Expected "script", "module", or "commonjs"./u);
+
+
+ messages = linter.verify("", { languageOptions: { ecmaVersion: 5, sourceType: "module" } });
+
+ assert.deepStrictEqual(messages.length, 1);
+ assert.ok(messages[0].message.includes("sourceType 'module' is not supported when ecmaVersion < 2015"));
+ });
+
+ it("should not crash when invalid parentheses syntax is encountered", () => {
+ linter.verify("left = (aSize.width/2) - ()");
+ });
+
+ it("should not crash when let is used inside of switch case", () => {
+ linter.verify("switch(foo) { case 1: let bar=2; }", { languageOptions: { ecmaVersion: 6 } });
+ });
+
+ it("should not crash when parsing destructured assignment", () => {
+ linter.verify("var { a='a' } = {};", { languageOptions: { ecmaVersion: 6 } });
+ });
+
+ it("should report syntax error when a keyword exists in object property shorthand", () => {
+ const messages = linter.verify("let a = {this}", { languageOptions: { ecmaVersion: 6 } });
+
+ assert.strictEqual(messages.length, 1);
+ assert.strictEqual(messages[0].fatal, true);
+ });
+
+ it("should not crash when we reuse the SourceCode object", () => {
+ const config = {
+ languageOptions: {
+ ecmaVersion: 6,
+ parserOptions: {
+ ecmaFeatures: { jsx: true }
+ }
+ }
+ };
+
+ linter.verify("function render() { return <div className='test'>{hello}</div> }", config);
+ linter.verify(linter.getSourceCode(), config);
+ });
+
+ it("should reuse the SourceCode object", () => {
+ let ast1 = null,
+ ast2 = null;
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "save-ast1": () => ({
+ Program(node) {
+ ast1 = node;
+ }
+ }),
+
+ "save-ast2": () => ({
+ Program(node) {
+ ast2 = node;
+ }
+ })
+
+ }
+ }
+ },
+ languageOptions: {
+ ecmaVersion: 6,
+ parserOptions: {
+ ecmaFeatures: { jsx: true }
+ }
+ }
+ };
+
+
+ linter.verify("function render() { return <div className='test'>{hello}</div> }", { ...config, rules: { "test/save-ast1": "error" } });
+ linter.verify(linter.getSourceCode(), { ...config, rules: { "test/save-ast2": "error" } });
+
+ assert(ast1 !== null);
+ assert(ast2 !== null);
+ assert(ast1 === ast2);
+ });
+
+ it("should not modify config object passed as argument", () => {
+ const config = {};
+
+ Object.freeze(config);
+ linter.verify("var", config);
+ });
+
+ it("should pass 'id' to rule contexts with the rule id", () => {
+
+ const spy = sinon.spy(context => {
+ assert.strictEqual(context.id, "test/foo-bar-baz");
+ return {};
+ });
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "foo-bar-baz": spy
+ }
+ }
+ },
+ rules: {
+ "test/foo-bar-baz": "error"
+ }
+ };
+
+
+ linter.verify("x", config);
+ assert(spy.calledOnce);
+ });
+
+
+ describe("when evaluating an empty string", () => {
+ it("runs rules", () => {
+
+ const config = {
+ plugins: {
+ test: {
+ rules: {
+ "no-programs": context => ({
+ Program(node) {
+ context.report({ node, message: "No programs allowed." });
+ }
+ })
+ }
+ }
+ },
+ rules: {
+ "test/no-programs": "error"
+ }
+ };
+
+ assert.strictEqual(
+ linter.verify("", config).length,
+ 1
+ );
+ });
+ });
+
+ });
+
+});