From 60a531a9c0811ddf718e26b9136e133f580b6c36 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 15 Dec 2023 22:25:33 +0000 Subject: [PATCH 01/25] chore: package.json update for @eslint/js release --- packages/js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/package.json b/packages/js/package.json index 2c7aacae88a..8dde9da4c4a 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/js", - "version": "8.55.0", + "version": "8.56.0", "description": "ESLint JavaScript language implementation", "main": "./src/index.js", "scripts": {}, From ba6af85c7d8ba55d37f8663aee949d148e441c1a Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 15 Dec 2023 23:37:48 +0100 Subject: [PATCH 02/25] chore: upgrade @eslint/js@8.56.0 (#17864) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be595d3c84c..8da0303bbc0 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.55.0", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", From 085978b3ce051e454c2244bf36a9493f7fa95d41 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 15 Dec 2023 22:54:03 +0000 Subject: [PATCH 03/25] Build: changelog update for 8.56.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b0969f950..1ee57ce4b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +v8.56.0 - December 15, 2023 + +* [`ba6af85`](https://github.com/eslint/eslint/commit/ba6af85c7d8ba55d37f8663aee949d148e441c1a) chore: upgrade @eslint/js@8.56.0 (#17864) (Milos Djermanovic) +* [`60a531a`](https://github.com/eslint/eslint/commit/60a531a9c0811ddf718e26b9136e133f580b6c36) chore: package.json update for @eslint/js release (Jenkins) +* [`0dd9704`](https://github.com/eslint/eslint/commit/0dd9704c4751e1cd02039f7d6485fee09bbccbf6) feat: Support custom severity when reporting unused disable directives (#17212) (Bryan Mishkin) +* [`31a7e3f`](https://github.com/eslint/eslint/commit/31a7e3fde491e36496b54e8905c766b31162d776) feat: fix no-restricted-properties false negatives with unknown objects (#17818) (Arka Pratim Chaudhuri) +* [`ba87a06`](https://github.com/eslint/eslint/commit/ba87a0651a65b52c3ac442b512dd9f4c2b4c5f57) chore: update dependency markdownlint to ^0.32.0 (#17783) (renovate[bot]) +* [`7d5e5f6`](https://github.com/eslint/eslint/commit/7d5e5f68849ae80caec0fc96ecceebccd348deec) fix: `TypeError: fs.exists is not a function` on read-only file system (#17846) (Francesco Trotta) +* [`9271d10`](https://github.com/eslint/eslint/commit/9271d10d9eabeafb0129a090f29191bfd14273c0) chore: add GitHub issue template for docs issues (#17845) (Josh Goldberg ✨) +* [`70a686b`](https://github.com/eslint/eslint/commit/70a686b3c1feac5eca98bbff9bd67175f550d5db) chore: Convert rule tests to FlatRuleTester (#17819) (Nicholas C. Zakas) +* [`9007719`](https://github.com/eslint/eslint/commit/90077199fe519953f9af8664bf947db4e4958514) docs: update link in ways-to-extend.md (#17839) (Amel SELMANE) +* [`f3a599d`](https://github.com/eslint/eslint/commit/f3a599d34c7080fc0b2c9a60b5e54dc98c22867c) chore: upgrade eslint-plugin-unicorn to v49.0.0 (#17837) (唯然) +* [`905d4b7`](https://github.com/eslint/eslint/commit/905d4b75ab2df42aba30622cee0f66b511397e2c) chore: upgrade eslint-plugin-eslint-plugin v5.2.1 (#17838) (唯然) +* [`4d7c3ce`](https://github.com/eslint/eslint/commit/4d7c3ce246e6b499f472342ef59496a47cc033d6) chore: update eslint-plugin-n v16.4.0 (#17836) (唯然) +* [`3a22236`](https://github.com/eslint/eslint/commit/3a22236f8d10af8a5bcafe56092651d3d681c99d) docs: Update README (GitHub Actions Bot) +* [`54c3ca6`](https://github.com/eslint/eslint/commit/54c3ca6f2dcd2a7afd53f42fc32055a25587259e) docs: fix migration-guide example (#17829) (Tanuj Kanti) +* [`4391b71`](https://github.com/eslint/eslint/commit/4391b71e62b15e54b0493f0dce1ea053ebbc0689) docs: check config comments in rule examples (#17815) (Francesco Trotta) +* [`fd28363`](https://github.com/eslint/eslint/commit/fd2836342c2be4751b05fe0ba7cece17d1edecc8) docs: remove mention about ESLint stylistic rules in readme (#17810) (Zwyx) +* [`fd0c60c`](https://github.com/eslint/eslint/commit/fd0c60c3be1f213e5a6d69d8a3248e963619e155) ci: unpin Node.js 21.2.0 (#17821) (Francesco Trotta) +* [`48ed5a6`](https://github.com/eslint/eslint/commit/48ed5a6dad478a14d3e823f137455c523f373e0b) docs: Update README (GitHub Actions Bot) +* [`74739c8`](https://github.com/eslint/eslint/commit/74739c849bbb6547b0e555ed8bb2ba1cbe0fdce4) fix: suggestion with invalid syntax in no-promise-executor-return rule (#17812) (Bryan Mishkin) + v8.55.0 - December 1, 2023 * [`eb8950c`](https://github.com/eslint/eslint/commit/eb8950c3b811c9163b9aae23af8b6266ad98b295) chore: upgrade @eslint/js@8.55.0 (#17811) (Milos Djermanovic) From 8e8e9f8476d701e4e981b9b4d9957e5d4855e530 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 15 Dec 2023 22:54:04 +0000 Subject: [PATCH 04/25] 8.56.0 --- docs/package.json | 2 +- docs/src/use/formatters/html-formatter-example.html | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package.json b/docs/package.json index c79c756bf18..163db5f943b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "docs-eslint", "private": true, - "version": "8.55.0", + "version": "8.56.0", "description": "", "main": "index.js", "keywords": [], diff --git a/docs/src/use/formatters/html-formatter-example.html b/docs/src/use/formatters/html-formatter-example.html index 6bbd626e114..3cb873a3440 100644 --- a/docs/src/use/formatters/html-formatter-example.html +++ b/docs/src/use/formatters/html-formatter-example.html @@ -118,7 +118,7 @@

ESLint Report

- 9 problems (5 errors, 4 warnings) - Generated on Fri Dec 01 2023 21:46:40 GMT+0000 (Coordinated Universal Time) + 9 problems (5 errors, 4 warnings) - Generated on Fri Dec 15 2023 22:54:05 GMT+0000 (Coordinated Universal Time)
diff --git a/package.json b/package.json index 8da0303bbc0..aa5335dbd85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "8.55.0", + "version": "8.56.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From cc0c9f707aa9da7965b98151868b3c249c7f8f30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 19:59:54 +0100 Subject: [PATCH 05/25] ci: bump github/codeql-action from 2 to 3 (#17873) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 637f06e2e51..d505542058a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From b577e8a55750c5e842074f62f1babb1836c4571c Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Wed, 20 Dec 2023 11:31:32 +0100 Subject: [PATCH 06/25] fix: allow circular references in config (#17752) * fix for circular references in config * restore current logic --- lib/config/flat-config-schema.js | 35 +++- tests/lib/config/flat-config-schema.js | 228 +++++++++++++++++++++++++ tests/lib/eslint/flat-eslint.js | 66 +++++++ 3 files changed, 321 insertions(+), 8 deletions(-) create mode 100644 tests/lib/config/flat-config-schema.js diff --git a/lib/config/flat-config-schema.js b/lib/config/flat-config-schema.js index 3b6612b4984..55546134ec3 100644 --- a/lib/config/flat-config-schema.js +++ b/lib/config/flat-config-schema.js @@ -62,13 +62,17 @@ function isUndefined(value) { return typeof value === "undefined"; } +// A unique empty object to be used internally as a mapping key in `deepMerge`. +const EMPTY_OBJECT = {}; + /** * Deeply merges two objects. * @param {Object} first The base object. - * @param {Object} second The overrides object. + * @param {any} second The overrides value. + * @param {Map>} [mergeMap] Maps the combination of first and second arguments to a merged result. * @returns {Object} An object with properties from both first and second. */ -function deepMerge(first = {}, second = {}) { +function deepMerge(first, second = {}, mergeMap = new Map()) { /* * If the second value is an array, just return it. We don't merge @@ -78,8 +82,23 @@ function deepMerge(first = {}, second = {}) { return second; } + let secondMergeMap = mergeMap.get(first); + + if (secondMergeMap) { + const result = secondMergeMap.get(second); + + if (result) { + + // If this combination of first and second arguments has been already visited, return the previously created result. + return result; + } + } else { + secondMergeMap = new Map(); + mergeMap.set(first, secondMergeMap); + } + /* - * First create a result object where properties from the second object + * First create a result object where properties from the second value * overwrite properties from the first. This sets up a baseline to use * later rather than needing to inspect and change every property * individually. @@ -89,6 +108,9 @@ function deepMerge(first = {}, second = {}) { ...second }; + // Store the pending result for this combination of first and second arguments. + secondMergeMap.set(second, result); + for (const key of Object.keys(second)) { // avoid hairy edge case @@ -100,13 +122,10 @@ function deepMerge(first = {}, second = {}) { const secondValue = second[key]; if (isNonNullObject(firstValue)) { - result[key] = deepMerge(firstValue, secondValue); + result[key] = deepMerge(firstValue, secondValue, mergeMap); } else if (isUndefined(firstValue)) { if (isNonNullObject(secondValue)) { - result[key] = deepMerge( - Array.isArray(secondValue) ? [] : {}, - secondValue - ); + result[key] = deepMerge(EMPTY_OBJECT, secondValue, mergeMap); } else if (!isUndefined(secondValue)) { result[key] = secondValue; } diff --git a/tests/lib/config/flat-config-schema.js b/tests/lib/config/flat-config-schema.js new file mode 100644 index 00000000000..1cde41ab16b --- /dev/null +++ b/tests/lib/config/flat-config-schema.js @@ -0,0 +1,228 @@ +/** + * @fileoverview Tests for flatConfigSchema + * @author Francesco Trotta + */ + +"use strict"; + +const { flatConfigSchema } = require("../../../lib/config/flat-config-schema"); +const { assert } = require("chai"); + +describe("merge", () => { + + const { merge } = flatConfigSchema.settings; + + it("merges two objects", () => { + const first = { foo: 42 }; + const second = { bar: "baz" }; + const result = merge(first, second); + + assert.deepStrictEqual(result, { ...first, ...second }); + }); + + it("overrides an object with an array", () => { + const first = { foo: 42 }; + const second = ["bar", "baz"]; + const result = merge(first, second); + + assert.strictEqual(result, second); + }); + + it("merges an array with an object", () => { + const first = ["foo", "bar"]; + const second = { baz: 42 }; + const result = merge(first, second); + + assert.deepStrictEqual(result, { 0: "foo", 1: "bar", baz: 42 }); + }); + + it("overrides an array with another array", () => { + const first = ["foo", "bar"]; + const second = ["baz", "qux"]; + const result = merge(first, second); + + assert.strictEqual(result, second); + }); + + it("returns an emtpy object if both values are undefined", () => { + const result = merge(void 0, void 0); + + assert.deepStrictEqual(result, {}); + }); + + it("returns an object equal to the first one if the second one is undefined", () => { + const first = { foo: 42, bar: "baz" }; + const result = merge(first, void 0); + + assert.deepStrictEqual(result, first); + assert.notStrictEqual(result, first); + }); + + it("returns an object equal to the second one if the first one is undefined", () => { + const second = { foo: 42, bar: "baz" }; + const result = merge(void 0, second); + + assert.deepStrictEqual(result, second); + assert.notStrictEqual(result, second); + }); + + it("merges two objects in a property", () => { + const first = { foo: { bar: "baz" } }; + const second = { foo: { qux: 42 } }; + const result = merge(first, second); + + assert.deepStrictEqual(result, { foo: { bar: "baz", qux: 42 } }); + }); + + it("does not override a value in a property with undefined", () => { + const first = { foo: { bar: "baz" } }; + const second = { foo: void 0 }; + const result = merge(first, second); + + assert.deepStrictEqual(result, first); + assert.notStrictEqual(result, first); + }); + + it("does not change the prototype of a merged object", () => { + const first = { foo: 42 }; + const second = { bar: "baz", ["__proto__"]: { qux: true } }; + const result = merge(first, second); + + assert.strictEqual(Object.getPrototypeOf(result), Object.prototype); + }); + + it("does not merge the '__proto__' property", () => { + const first = { ["__proto__"]: { foo: 42 } }; + const second = { ["__proto__"]: { bar: "baz" } }; + const result = merge(first, second); + + assert.deepStrictEqual(result, second); + assert.notStrictEqual(result, second); + }); + + it("throws an error if a value in a property is overriden with null", () => { + const first = { foo: { bar: "baz" } }; + const second = { foo: null }; + + assert.throws(() => merge(first, second), TypeError); + }); + + it("does not override a value in a property with a primitive", () => { + const first = { foo: { bar: "baz" } }; + const second = { foo: 42 }; + const result = merge(first, second); + + assert.deepStrictEqual(result, first); + assert.notStrictEqual(result, first); + }); + + it("merges an object in a property with a string", () => { + const first = { foo: { bar: "baz" } }; + const second = { foo: "qux" }; + const result = merge(first, second); + + assert.deepStrictEqual(result, { foo: { 0: "q", 1: "u", 2: "x", bar: "baz" } }); + }); + + it("merges objects with self-references", () => { + const first = { foo: 42 }; + + first.first = first; + const second = { bar: "baz" }; + + second.second = second; + const result = merge(first, second); + + assert.strictEqual(result.first, first); + assert.deepStrictEqual(result.second, second); + + const expected = { foo: 42, bar: "baz" }; + + expected.first = first; + expected.second = second; + assert.deepStrictEqual(result, expected); + }); + + it("merges objects with overlapping self-references", () => { + const first = { foo: 42 }; + + first.reference = first; + const second = { bar: "baz" }; + + second.reference = second; + + const result = merge(first, second); + + assert.strictEqual(result.reference, result); + + const expected = { foo: 42, bar: "baz" }; + + expected.reference = expected; + assert.deepStrictEqual(result, expected); + }); + + it("merges objects with cross-references", () => { + const first = { foo: 42 }; + const second = { bar: "baz" }; + + first.second = second; + second.first = first; + + const result = merge(first, second); + + assert.deepStrictEqual(result.first, first); + assert.strictEqual(result.second, second); + + const expected = { foo: 42, bar: "baz" }; + + expected.first = first; + expected.second = second; + assert.deepStrictEqual(result, expected); + }); + + it("merges objects with overlapping cross-references", () => { + const first = { foo: 42 }; + const second = { bar: "baz" }; + + first.reference = second; + second.reference = first; + + const result = merge(first, second); + + assert.strictEqual(result, result.reference.reference); + + const expected = { foo: 42, bar: "baz", reference: { foo: 42, bar: "baz" } }; + + expected.reference.reference = expected; + assert.deepStrictEqual(result, expected); + }); + + it("produces the same results for the same combinations of property values", () => { + const firstCommon = { foo: 42 }; + const secondCommon = { bar: "baz" }; + const first = { + a: firstCommon, + b: firstCommon, + c: { foo: "different" }, + d: firstCommon + }; + const second = { + a: secondCommon, + b: { bar: "something else" }, + c: secondCommon, + d: secondCommon + }; + const result = merge(first, second); + + assert.deepStrictEqual(result.a, result.d); + + const expected = { + a: { foo: 42, bar: "baz" }, + b: { foo: 42, bar: "something else" }, + c: { foo: "different", bar: "baz" }, + d: { foo: 42, bar: "baz" } + }; + + assert.deepStrictEqual(result, expected); + }); +}); diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js index d71da6936e9..ee417830788 100644 --- a/tests/lib/eslint/flat-eslint.js +++ b/tests/lib/eslint/flat-eslint.js @@ -6322,6 +6322,72 @@ describe("FlatESLint", () => { }); } + describe("config with circular references", () => { + it("in 'settings'", async () => { + let resolvedSettings = null; + + const circular = {}; + + circular.self = circular; + + const eslint = new FlatESLint({ + overrideConfigFile: true, + baseConfig: { + settings: { + sharedData: circular + }, + rules: { + "test-plugin/test-rule": 1 + } + }, + plugins: { + "test-plugin": { + rules: { + "test-rule": { + create(context) { + resolvedSettings = context.settings; + return { }; + } + } + } + } + } + }); + + await eslint.lintText("debugger;"); + + assert.deepStrictEqual(resolvedSettings.sharedData, circular); + }); + + it("in 'parserOptions'", async () => { + let resolvedParserOptions = null; + + const circular = {}; + + circular.self = circular; + + const eslint = new FlatESLint({ + overrideConfigFile: true, + baseConfig: { + languageOptions: { + parser: { + parse(text, parserOptions) { + resolvedParserOptions = parserOptions; + } + }, + parserOptions: { + testOption: circular + } + } + } + }); + + await eslint.lintText("debugger;"); + + assert.deepStrictEqual(resolvedParserOptions.testOption, circular); + }); + }); + }); describe("shouldUseFlatConfig", () => { From e1e827ffcbd73faa40dbac3b97529452e9c67108 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 20 Dec 2023 14:02:44 +0100 Subject: [PATCH 07/25] feat!: Require Node.js `^18.18.0 || ^20.9.0 || >=21.1.0` (#17725) * feat!: Require Node.js `^18.18.0 || ^20.7.0 || >=21.1.0` Drops support for Node.js 12/14/16/17/19 Fixes #17595 * update getting-started * fix lint errors * 20.7.0 -> 20.9.0 --- .github/workflows/ci.yml | 2 +- README.md | 2 +- docs/src/integrate/integration-tutorial.md | 2 +- docs/src/use/getting-started.md | 4 ++-- package.json | 2 +- packages/js/package.json | 2 +- tests/lib/eslint/eslint.js | 16 +--------------- tests/lib/eslint/flat-eslint.js | 16 +--------------- 8 files changed, 9 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff0f1953dcc..9798d7c7eb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [21.x, 20.x, 19.x, 18.x, 17.x, 16.x, 14.x, 12.x, "12.22.0"] + node: [21.x, 20.x, 18.x, "18.18.0"] include: - os: windows-latest node: "lts/*" diff --git a/README.md b/README.md index 227d40c7cc5..67538267a0f 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ ESLint is a tool for identifying and reporting on patterns found in ECMAScript/J ## Installation and Usage -Prerequisites: [Node.js](https://nodejs.org/) (`^12.22.0`, `^14.17.0`, or `>=16.0.0`) built with SSL support. (If you are using an official Node.js distribution, SSL is always built in.) +Prerequisites: [Node.js](https://nodejs.org/) (`^18.18.0`, `^20.9.0`, or `>=21.1.0`) built with SSL support. (If you are using an official Node.js distribution, SSL is always built in.) You can install and configure ESLint using this command: diff --git a/docs/src/integrate/integration-tutorial.md b/docs/src/integrate/integration-tutorial.md index 08d77c7253b..403582f19fe 100644 --- a/docs/src/integrate/integration-tutorial.md +++ b/docs/src/integrate/integration-tutorial.md @@ -33,7 +33,7 @@ This tutorial assumes you are familiar with JavaScript and Node.js. To follow this tutorial, you'll need to have the following: -* Node.js (v12.22.0 or higher) +* Node.js (`^18.18.0`, `^20.9.0`, or `>=21.1.0`) * npm * A text editor diff --git a/docs/src/use/getting-started.md b/docs/src/use/getting-started.md index 623011aaf48..c53211415bf 100644 --- a/docs/src/use/getting-started.md +++ b/docs/src/use/getting-started.md @@ -1,7 +1,7 @@ --- title: Getting Started with ESLint eleventyNavigation: - key: getting started + key: getting started parent: use eslint title: Getting Started order: 1 @@ -14,7 +14,7 @@ ESLint is completely pluggable. Every single rule is a plugin and you can add mo ## Prerequisites -To use ESLint, you must have [Node.js](https://nodejs.org/en/) (`^12.22.0`, `^14.17.0`, or `>=16.0.0`) installed and built with SSL support. (If you are using an official Node.js distribution, SSL is always built in.) +To use ESLint, you must have [Node.js](https://nodejs.org/en/) (`^18.18.0`, `^20.9.0`, or `>=21.1.0`) installed and built with SSL support. (If you are using an official Node.js distribution, SSL is always built in.) ## Quick start diff --git a/package.json b/package.json index aa5335dbd85..71ca464eb06 100644 --- a/package.json +++ b/package.json @@ -174,6 +174,6 @@ ], "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } } diff --git a/packages/js/package.json b/packages/js/package.json index 8dde9da4c4a..c34e7fb7eb3 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -26,6 +26,6 @@ ], "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } } diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 040722fcf64..4e7499da0d4 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -2404,21 +2404,7 @@ describe("ESLint", () => { */ function deleteCacheDir() { try { - - /* - * `fs.rmdir(path, { recursive: true })` is deprecated and will be removed. - * Use `fs.rm(path, { recursive: true })` instead. - * When supporting Node.js 14.14.0+, the compatibility condition can be removed for `fs.rmdir`. - */ - // eslint-disable-next-line n/no-unsupported-features/node-builtins -- just checking if it exists - if (typeof fs.rm === "function") { - - // eslint-disable-next-line n/no-unsupported-features/node-builtins -- conditionally used - fs.rmSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); - } else { - fs.rmdirSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); - } - + fs.rmSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); } catch { /* diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js index ee417830788..365a1223896 100644 --- a/tests/lib/eslint/flat-eslint.js +++ b/tests/lib/eslint/flat-eslint.js @@ -2299,21 +2299,7 @@ describe("FlatESLint", () => { */ function deleteCacheDir() { try { - - /* - * `fs.rmdir(path, { recursive: true })` is deprecated and will be removed. - * Use `fs.rm(path, { recursive: true })` instead. - * When supporting Node.js 14.14.0+, the compatibility condition can be removed for `fs.rmdir`. - */ - // eslint-disable-next-line n/no-unsupported-features/node-builtins -- just checking if it exists - if (typeof fs.rm === "function") { - - // eslint-disable-next-line n/no-unsupported-features/node-builtins -- conditionally used - fs.rmSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); - } else { - fs.rmdirSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); - } - + fs.rmSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); } catch { /* From 5304da03d94dc8cb19060e2efc9206784c4cec0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 20 Dec 2023 08:14:12 -0500 Subject: [PATCH 08/25] feat!: remove formatters except html, json(-with-metadata), and stylish (#17531) * feat!: remove formatters other than json(-with-metadata) and stylish * Add back html * Reset formatters-meta order, and add removedFormatters Set * Apply suggestions from code review Co-authored-by: Milos Djermanovic * Consolidated removed formatters set * chore: undo formatters/index.md changes * Moved back to per-file sets --------- Co-authored-by: Milos Djermanovic --- docs/src/use/command-line-interface.md | 8 +- lib/cli-engine/cli-engine.js | 13 +- lib/cli-engine/formatters/checkstyle.js | 60 ----- lib/cli-engine/formatters/compact.js | 60 ----- .../formatters/formatters-meta.json | 30 +-- lib/cli-engine/formatters/jslint-xml.js | 41 --- lib/cli-engine/formatters/junit.js | 82 ------ lib/cli-engine/formatters/tap.js | 95 ------- lib/cli-engine/formatters/unix.js | 58 ----- lib/cli-engine/formatters/visualstudio.js | 63 ----- lib/eslint/flat-eslint.js | 12 +- package.json | 3 +- tests/lib/cli-engine/cli-engine.js | 2 +- tests/lib/cli-engine/formatters/checkstyle.js | 171 ------------- tests/lib/cli-engine/formatters/compact.js | 146 ----------- tests/lib/cli-engine/formatters/jslint-xml.js | 176 ------------- tests/lib/cli-engine/formatters/junit.js | 216 ---------------- tests/lib/cli-engine/formatters/tap.js | 235 ------------------ tests/lib/cli-engine/formatters/unix.js | 146 ----------- .../lib/cli-engine/formatters/visualstudio.js | 146 ----------- tests/lib/cli.js | 9 +- tests/lib/eslint/eslint.js | 2 +- tests/lib/eslint/flat-eslint.js | 2 +- tests/lib/options.js | 8 +- 24 files changed, 40 insertions(+), 1744 deletions(-) delete mode 100644 lib/cli-engine/formatters/checkstyle.js delete mode 100644 lib/cli-engine/formatters/compact.js delete mode 100644 lib/cli-engine/formatters/jslint-xml.js delete mode 100644 lib/cli-engine/formatters/junit.js delete mode 100644 lib/cli-engine/formatters/tap.js delete mode 100644 lib/cli-engine/formatters/unix.js delete mode 100644 lib/cli-engine/formatters/visualstudio.js delete mode 100644 tests/lib/cli-engine/formatters/checkstyle.js delete mode 100644 tests/lib/cli-engine/formatters/compact.js delete mode 100644 tests/lib/cli-engine/formatters/jslint-xml.js delete mode 100644 tests/lib/cli-engine/formatters/junit.js delete mode 100644 tests/lib/cli-engine/formatters/tap.js delete mode 100644 tests/lib/cli-engine/formatters/unix.js delete mode 100644 tests/lib/cli-engine/formatters/visualstudio.js diff --git a/docs/src/use/command-line-interface.md b/docs/src/use/command-line-interface.md index e07ff80ed04..dbbbe9ca526 100644 --- a/docs/src/use/command-line-interface.md +++ b/docs/src/use/command-line-interface.md @@ -503,16 +503,16 @@ An npm-installed formatter is resolved with or without `eslint-formatter-` prefi When specified, the given format is output to the console. If you'd like to save that output into a file, you can do so on the command line like so: ```shell -# Saves the output into the `results.txt` file. -npx eslint -f compact file.js > results.txt +# Saves the output into the `results.json` file. +npx eslint -f json file.js > results.json ``` ##### `-f`, `--format` example -Use the built-in `compact` formatter: +Use the built-in `json` formatter: ```shell -npx eslint --format compact file.js +npx eslint --format json file.js ``` Use a local custom formatter: diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 49c8902c161..35dd2849cd6 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -41,6 +41,17 @@ const hash = require("./hash"); const LintResultCache = require("./lint-result-cache"); const debug = require("debug")("eslint:cli-engine"); +const removedFormatters = new Set([ + "checkstyle", + "codeframe", + "compact", + "jslint-xml", + "junit", + "table", + "tap", + "unix", + "visualstudio" +]); const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]); //------------------------------------------------------------------------------ @@ -1047,7 +1058,7 @@ class CLIEngine { try { return require(formatterPath); } catch (ex) { - if (format === "table" || format === "codeframe") { + if (removedFormatters.has(format)) { ex.message = `The ${format} formatter is no longer part of core ESLint. Install it manually with \`npm install -D eslint-formatter-${format}\``; } else { ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`; diff --git a/lib/cli-engine/formatters/checkstyle.js b/lib/cli-engine/formatters/checkstyle.js deleted file mode 100644 index f19b6fc0957..00000000000 --- a/lib/cli-engine/formatters/checkstyle.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @fileoverview CheckStyle XML reporter - * @author Ian Christian Myers - */ -"use strict"; - -const xmlEscape = require("../xml-escape"); - -//------------------------------------------------------------------------------ -// Helper Functions -//------------------------------------------------------------------------------ - -/** - * Returns the severity of warning or error - * @param {Object} message message object to examine - * @returns {string} severity level - * @private - */ -function getMessageType(message) { - if (message.fatal || message.severity === 2) { - return "error"; - } - return "warning"; - -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = function(results) { - - let output = ""; - - output += ""; - output += ""; - - results.forEach(result => { - const messages = result.messages; - - output += ``; - - messages.forEach(message => { - output += [ - `` - ].join(" "); - }); - - output += ""; - - }); - - output += ""; - - return output; -}; diff --git a/lib/cli-engine/formatters/compact.js b/lib/cli-engine/formatters/compact.js deleted file mode 100644 index 2b540bde236..00000000000 --- a/lib/cli-engine/formatters/compact.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @fileoverview Compact reporter - * @author Nicholas C. Zakas - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Helper Functions -//------------------------------------------------------------------------------ - -/** - * Returns the severity of warning or error - * @param {Object} message message object to examine - * @returns {string} severity level - * @private - */ -function getMessageType(message) { - if (message.fatal || message.severity === 2) { - return "Error"; - } - return "Warning"; - -} - - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = function(results) { - - let output = "", - total = 0; - - results.forEach(result => { - - const messages = result.messages; - - total += messages.length; - - messages.forEach(message => { - - output += `${result.filePath}: `; - output += `line ${message.line || 0}`; - output += `, col ${message.column || 0}`; - output += `, ${getMessageType(message)}`; - output += ` - ${message.message}`; - output += message.ruleId ? ` (${message.ruleId})` : ""; - output += "\n"; - - }); - - }); - - if (total > 0) { - output += `\n${total} problem${total !== 1 ? "s" : ""}`; - } - - return output; -}; diff --git a/lib/cli-engine/formatters/formatters-meta.json b/lib/cli-engine/formatters/formatters-meta.json index bcd35e50428..4cc61ae3c62 100644 --- a/lib/cli-engine/formatters/formatters-meta.json +++ b/lib/cli-engine/formatters/formatters-meta.json @@ -1,20 +1,8 @@ [ - { - "name": "checkstyle", - "description": "Outputs results to the [Checkstyle](https://checkstyle.sourceforge.io/) format." - }, - { - "name": "compact", - "description": "Human-readable output format. Mimics the default output of JSHint." - }, { "name": "html", "description": "Outputs results to HTML. The `html` formatter is useful for visual presentation in the browser." }, - { - "name": "jslint-xml", - "description": "Outputs results to format compatible with the [JSLint Jenkins plugin](https://plugins.jenkins.io/jslint/)." - }, { "name": "json-with-metadata", "description": "Outputs JSON-serialized results. The `json-with-metadata` provides the same linting results as the [`json`](#json) formatter with additional metadata about the rules applied. The linting results are included in the `results` property and the rules metadata is included in the `metadata` property.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint." @@ -23,24 +11,8 @@ "name": "json", "description": "Outputs JSON-serialized results. The `json` formatter is useful when you want to programmatically work with the CLI's linting results.\n\nAlternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint." }, - { - "name": "junit", - "description": "Outputs results to format compatible with the [JUnit Jenkins plugin](https://plugins.jenkins.io/junit/)." - }, { "name": "stylish", "description": "Human-readable output format. This is the default formatter." - }, - { - "name": "tap", - "description": "Outputs results to the [Test Anything Protocol (TAP)](https://testanything.org/) specification format." - }, - { - "name": "unix", - "description": "Outputs results to a format similar to many commands in UNIX-like systems. Parsable with tools such as [grep](https://www.gnu.org/software/grep/manual/grep.html), [sed](https://www.gnu.org/software/sed/manual/sed.html), and [awk](https://www.gnu.org/software/gawk/manual/gawk.html)." - }, - { - "name": "visualstudio", - "description": "Outputs results to format compatible with the integrated terminal of the [Visual Studio](https://visualstudio.microsoft.com/) IDE. When using Visual Studio, you can click on the linting results in the integrated terminal to go to the issue in the source code." } -] \ No newline at end of file +] diff --git a/lib/cli-engine/formatters/jslint-xml.js b/lib/cli-engine/formatters/jslint-xml.js deleted file mode 100644 index 0ca1cbaed1a..00000000000 --- a/lib/cli-engine/formatters/jslint-xml.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @fileoverview JSLint XML reporter - * @author Ian Christian Myers - */ -"use strict"; - -const xmlEscape = require("../xml-escape"); - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = function(results) { - - let output = ""; - - output += ""; - output += ""; - - results.forEach(result => { - const messages = result.messages; - - output += ``; - - messages.forEach(message => { - output += [ - `` - ].join(" "); - }); - - output += ""; - - }); - - output += ""; - - return output; -}; diff --git a/lib/cli-engine/formatters/junit.js b/lib/cli-engine/formatters/junit.js deleted file mode 100644 index a994b4b1980..00000000000 --- a/lib/cli-engine/formatters/junit.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @fileoverview jUnit Reporter - * @author Jamund Ferguson - */ -"use strict"; - -const xmlEscape = require("../xml-escape"); -const path = require("path"); - -//------------------------------------------------------------------------------ -// Helper Functions -//------------------------------------------------------------------------------ - -/** - * Returns the severity of warning or error - * @param {Object} message message object to examine - * @returns {string} severity level - * @private - */ -function getMessageType(message) { - if (message.fatal || message.severity === 2) { - return "Error"; - } - return "Warning"; - -} - -/** - * Returns a full file path without extension - * @param {string} filePath input file path - * @returns {string} file path without extension - * @private - */ -function pathWithoutExt(filePath) { - return path.join(path.dirname(filePath), path.basename(filePath, path.extname(filePath))); -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = function(results) { - - let output = ""; - - output += "\n"; - output += "\n"; - - results.forEach(result => { - - const messages = result.messages; - const classname = pathWithoutExt(result.filePath); - - if (messages.length > 0) { - output += `\n`; - messages.forEach(message => { - const type = message.fatal ? "error" : "failure"; - - output += ``; - output += `<${type} message="${xmlEscape(message.message || "")}">`; - output += ""; - output += ``; - output += "\n"; - }); - output += "\n"; - } else { - output += `\n`; - output += `\n`; - output += "\n"; - } - - }); - - output += "\n"; - - return output; -}; diff --git a/lib/cli-engine/formatters/tap.js b/lib/cli-engine/formatters/tap.js deleted file mode 100644 index e4148a3b392..00000000000 --- a/lib/cli-engine/formatters/tap.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @fileoverview TAP reporter - * @author Jonathan Kingston - */ -"use strict"; - -const yaml = require("js-yaml"); - -//------------------------------------------------------------------------------ -// Helper Functions -//------------------------------------------------------------------------------ - -/** - * Returns a canonical error level string based upon the error message passed in. - * @param {Object} message Individual error message provided by eslint - * @returns {string} Error level string - */ -function getMessageType(message) { - if (message.fatal || message.severity === 2) { - return "error"; - } - return "warning"; -} - -/** - * Takes in a JavaScript object and outputs a TAP diagnostics string - * @param {Object} diagnostic JavaScript object to be embedded as YAML into output. - * @returns {string} diagnostics string with YAML embedded - TAP version 13 compliant - */ -function outputDiagnostics(diagnostic) { - const prefix = " "; - let output = `${prefix}---\n`; - - output += prefix + yaml.dump(diagnostic).split("\n").join(`\n${prefix}`); - output += "...\n"; - return output; -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = function(results) { - let output = `TAP version 13\n1..${results.length}\n`; - - results.forEach((result, id) => { - const messages = result.messages; - let testResult = "ok"; - let diagnostics = {}; - - if (messages.length > 0) { - messages.forEach(message => { - const severity = getMessageType(message); - const diagnostic = { - message: message.message, - severity, - data: { - line: message.line || 0, - column: message.column || 0, - ruleId: message.ruleId || "" - } - }; - - // This ensures a warning message is not flagged as error - if (severity === "error") { - testResult = "not ok"; - } - - /* - * If we have multiple messages place them under a messages key - * The first error will be logged as message key - * This is to adhere to TAP 13 loosely defined specification of having a message key - */ - if ("message" in diagnostics) { - if (typeof diagnostics.messages === "undefined") { - diagnostics.messages = []; - } - diagnostics.messages.push(diagnostic); - } else { - diagnostics = diagnostic; - } - }); - } - - output += `${testResult} ${id + 1} - ${result.filePath}\n`; - - // If we have an error include diagnostics - if (messages.length > 0) { - output += outputDiagnostics(diagnostics); - } - - }); - - return output; -}; diff --git a/lib/cli-engine/formatters/unix.js b/lib/cli-engine/formatters/unix.js deleted file mode 100644 index c6c4ebbdb9f..00000000000 --- a/lib/cli-engine/formatters/unix.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @fileoverview unix-style formatter. - * @author oshi-shinobu - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Helper Functions -//------------------------------------------------------------------------------ - -/** - * Returns a canonical error level string based upon the error message passed in. - * @param {Object} message Individual error message provided by eslint - * @returns {string} Error level string - */ -function getMessageType(message) { - if (message.fatal || message.severity === 2) { - return "Error"; - } - return "Warning"; - -} - - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = function(results) { - - let output = "", - total = 0; - - results.forEach(result => { - - const messages = result.messages; - - total += messages.length; - - messages.forEach(message => { - - output += `${result.filePath}:`; - output += `${message.line || 0}:`; - output += `${message.column || 0}:`; - output += ` ${message.message} `; - output += `[${getMessageType(message)}${message.ruleId ? `/${message.ruleId}` : ""}]`; - output += "\n"; - - }); - - }); - - if (total > 0) { - output += `\n${total} problem${total !== 1 ? "s" : ""}`; - } - - return output; -}; diff --git a/lib/cli-engine/formatters/visualstudio.js b/lib/cli-engine/formatters/visualstudio.js deleted file mode 100644 index 0d49431db87..00000000000 --- a/lib/cli-engine/formatters/visualstudio.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @fileoverview Visual Studio compatible formatter - * @author Ronald Pijnacker - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Helper Functions -//------------------------------------------------------------------------------ - -/** - * Returns the severity of warning or error - * @param {Object} message message object to examine - * @returns {string} severity level - * @private - */ -function getMessageType(message) { - if (message.fatal || message.severity === 2) { - return "error"; - } - return "warning"; - -} - - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = function(results) { - - let output = "", - total = 0; - - results.forEach(result => { - - const messages = result.messages; - - total += messages.length; - - messages.forEach(message => { - - output += result.filePath; - output += `(${message.line || 0}`; - output += message.column ? `,${message.column}` : ""; - output += `): ${getMessageType(message)}`; - output += message.ruleId ? ` ${message.ruleId}` : ""; - output += ` : ${message.message}`; - output += "\n"; - - }); - - }); - - if (total === 0) { - output += "no problems"; - } else { - output += `\n${total} problem${total !== 1 ? "s" : ""}`; - } - - return output; -}; diff --git a/lib/eslint/flat-eslint.js b/lib/eslint/flat-eslint.js index 785f41edf8d..5ebe9bc73c1 100644 --- a/lib/eslint/flat-eslint.js +++ b/lib/eslint/flat-eslint.js @@ -93,9 +93,19 @@ const LintResultCache = require("../cli-engine/lint-result-cache"); const FLAT_CONFIG_FILENAME = "eslint.config.js"; const debug = require("debug")("eslint:flat-eslint"); -const removedFormatters = new Set(["table", "codeframe"]); const privateMembers = new WeakMap(); const importedConfigFileModificationTime = new Map(); +const removedFormatters = new Set([ + "checkstyle", + "codeframe", + "compact", + "jslint-xml", + "junit", + "table", + "tap", + "unix", + "visualstudio" +]); /** * It will calculate the error and warning count for collection of messages per file diff --git a/package.json b/package.json index 71ca464eb06..8cbd2fcf320 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,6 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -132,6 +131,7 @@ "glob": "^7.1.6", "got": "^11.8.3", "gray-matter": "^4.0.3", + "js-yaml": "^4.1.0", "lint-staged": "^11.0.0", "load-perf": "^0.2.0", "markdown-it": "^12.2.0", @@ -147,7 +147,6 @@ "metascraper-logo-favicon": "^5.25.7", "metascraper-title": "^5.25.7", "mocha": "^8.3.2", - "mocha-junit-reporter": "^2.0.0", "node-polyfill-webpack-plugin": "^1.0.3", "npm-license": "^0.3.3", "pirates": "^4.0.5", diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 231501b364e..3546d611948 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -4794,7 +4794,7 @@ describe("CLIEngine", () => { it("should return a function when a bundled formatter is requested", () => { const engine = new CLIEngine(), - formatter = engine.getFormatter("compact"); + formatter = engine.getFormatter("json"); assert.isFunction(formatter); }); diff --git a/tests/lib/cli-engine/formatters/checkstyle.js b/tests/lib/cli-engine/formatters/checkstyle.js deleted file mode 100644 index 218b15310ab..00000000000 --- a/tests/lib/cli-engine/formatters/checkstyle.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * @fileoverview Tests for checkstyle reporter. - * @author Ian Christian Myers - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - formatter = require("../../../../lib/cli-engine/formatters/checkstyle"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("formatter:checkstyle", () => { - describe("when passed a single message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename: line x, col y, Error - z for errors", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - - it("should return a string in the format filename: line x, col y, Warning - z for warnings", () => { - code[0].messages[0].severity = 1; - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed a message with XML control characters", () => { - const code = [{ - filePath: "<>&\"'.js", - messages: [{ - fatal: true, - message: "Unexpected <>&\"'\b\t\n\f\r牛逼.", - line: "<", - column: ">", - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename: line x, col y, Error - z", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed a fatal error message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Unexpected foo.", - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename: line x, col y, Error - z", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed multiple messages", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }, { - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed multiple files with 1 message each", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }, { - filePath: "bar.js", - messages: [{ - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passing single message without rule id", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10 - }] - }]; - - it("should return a string in the format filename: line x, col y, Error - z for errors", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passing single message without line and column", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - ruleId: "foo" - }] - }]; - - it("should return line and column as 0 instead of undefined", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); -}); diff --git a/tests/lib/cli-engine/formatters/compact.js b/tests/lib/cli-engine/formatters/compact.js deleted file mode 100644 index 1a179d9f5d5..00000000000 --- a/tests/lib/cli-engine/formatters/compact.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * @fileoverview Tests for options. - * @author Nicholas C. Zakas - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - formatter = require("../../../../lib/cli-engine/formatters/compact"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("formatter:compact", () => { - describe("when passed no messages", () => { - const code = [{ - filePath: "foo.js", - messages: [] - }]; - - it("should return nothing", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed a single message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename: line x, col y, Error - z for errors", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js: line 5, col 10, Error - Unexpected foo. (foo)\n\n1 problem"); - }); - - it("should return a string in the format filename: line x, col y, Warning - z for warnings", () => { - code[0].messages[0].severity = 1; - const result = formatter(code); - - assert.strictEqual(result, "foo.js: line 5, col 10, Warning - Unexpected foo. (foo)\n\n1 problem"); - }); - }); - - describe("when passed a fatal error message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Unexpected foo.", - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename: line x, col y, Error - z", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js: line 5, col 10, Error - Unexpected foo. (foo)\n\n1 problem"); - }); - }); - - describe("when passed multiple messages", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }, { - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js: line 5, col 10, Error - Unexpected foo. (foo)\nfoo.js: line 6, col 11, Warning - Unexpected bar. (bar)\n\n2 problems"); - }); - }); - - describe("when passed multiple files with 1 message each", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }, { - filePath: "bar.js", - messages: [{ - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js: line 5, col 10, Error - Unexpected foo. (foo)\nbar.js: line 6, col 11, Warning - Unexpected bar. (bar)\n\n2 problems"); - }); - }); - - describe("when passed one file not found message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Couldn't find foo.js." - }] - }]; - - it("should return a string without line and column", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js: line 0, col 0, Error - Couldn't find foo.js.\n\n1 problem"); - }); - }); -}); diff --git a/tests/lib/cli-engine/formatters/jslint-xml.js b/tests/lib/cli-engine/formatters/jslint-xml.js deleted file mode 100644 index eca587a42d4..00000000000 --- a/tests/lib/cli-engine/formatters/jslint-xml.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * @fileoverview Tests for JSLint XML reporter. - * @author Ian Christian Myers - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - formatter = require("../../../../lib/cli-engine/formatters/jslint-xml"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("formatter:jslint-xml", () => { - describe("when passed a single message", () => { - - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo", - source: "foo" - }] - }]; - - it("should return a string in JSLint XML format with 1 issue in 1 file", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed a fatal error message", () => { - - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Unexpected foo.", - line: 5, - column: 10, - ruleId: "foo", - source: "foo" - }] - }]; - - it("should return a string in JSLint XML format with 1 issue in 1 file", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed multiple messages", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo", - source: "foo" - }, { - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar", - source: "bar" - }] - }]; - - it("should return a string in JSLint XML format with 2 issues in 1 file", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed multiple files with 1 message each", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo", - source: "foo" - }] - }, { - filePath: "bar.js", - messages: [{ - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar", - source: "bar" - }] - }]; - - it("should return a string in JSLint XML format with 2 issues in 2 files", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passing a single message with illegal characters", () => { - - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected <&\"'>\b\t\n\f\r牛逼 foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo", - source: "foo" - }] - }]; - - it("should return a string in JSLint XML format with 1 issue in 1 file", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passing a single message with no source", () => { - - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in JSLint XML format with 1 issue in 1 file", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passing a single message without rule id", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - severity: 2, - line: 5, - column: 10 - }] - }]; - - it("should return a string in JSLint XML format with 1 issue in 1 file", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); -}); diff --git a/tests/lib/cli-engine/formatters/junit.js b/tests/lib/cli-engine/formatters/junit.js deleted file mode 100644 index 26a2445b3b3..00000000000 --- a/tests/lib/cli-engine/formatters/junit.js +++ /dev/null @@ -1,216 +0,0 @@ -/** - * @fileoverview Tests for jUnit Formatter. - * @author Jamund Ferguson - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - formatter = require("../../../../lib/cli-engine/formatters/junit"), - process = require("process"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -const suppliedFilePath = (process.platform === "win32") ? "C:\\path\\to\\foo.js" : "/path/to/foo.js"; -const expectedClassName = (process.platform === "win32") ? "C:\\path\\to\\foo" : "/path/to/foo"; - -describe("formatter:junit", () => { - describe("when there are no problems", () => { - const code = []; - - it("should not complain about anything", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ""); - }); - }); - - describe("when passed a single message", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a single with a message and the line and col number in the body (error)", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - - it("should return a single with a message and the line and col number in the body (warning)", () => { - code[0].messages[0].severity = 1; - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); - - describe("when passed a fatal error message", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [{ - fatal: true, - message: "Unexpected foo.", - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a single and an ", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); - - describe("when passed a fatal error message with no line or column", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [{ - fatal: true, - message: "Unexpected foo." - }] - }]; - - it("should return a single and an ", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); - - describe("when passed a fatal error message with no line, column, or message text", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [{ - fatal: true - }] - }]; - - it("should return a single and an ", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); - - describe("when passed multiple messages", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }, { - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a multiple 's", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); - - describe("when passed special characters", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [{ - message: "Unexpected \b\t\n\f\r牛逼.", - severity: 1, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should make them go away", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); - - describe("when passed multiple files with 1 message each", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [{ - message: "Unexpected foo.", - severity: 1, - line: 5, - column: 10, - ruleId: "foo" - }] - }, { - filePath: "bar.js", - messages: [{ - message: "Unexpected bar.", - severity: 2, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return 2 's", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); - - describe("when passed multiple files should print even if no errors", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [{ - message: "Unexpected foo.", - severity: 1, - line: 5, - column: 10, - ruleId: "foo" - }] - }, { - filePath: "bar.js", - messages: [] - }]; - - it("should return 2 ", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); - - describe("when passed a file with no errors", () => { - const code = [{ - filePath: suppliedFilePath, - messages: [] - }]; - - it("should print a passing ", () => { - const result = formatter(code); - - assert.strictEqual(result.replace(/\n/gu, ""), ``); - }); - }); -}); diff --git a/tests/lib/cli-engine/formatters/tap.js b/tests/lib/cli-engine/formatters/tap.js deleted file mode 100644 index 8852ee72d60..00000000000 --- a/tests/lib/cli-engine/formatters/tap.js +++ /dev/null @@ -1,235 +0,0 @@ -/** - * @fileoverview Tests for options. - * @author Jonathan Kingston - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - formatter = require("../../../../lib/cli-engine/formatters/tap"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("formatter:tap", () => { - describe("when passed no messages", () => { - const code = [{ - filePath: "foo.js", - messages: [] - }]; - - it("should return nothing", () => { - const result = formatter(code); - - assert.strictEqual(result, "TAP version 13\n1..1\nok 1 - foo.js\n"); - }); - }); - - describe("when passed a single message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string with YAML severity, line and column", () => { - const result = formatter(code); - - assert.strictEqual(result, "TAP version 13\n1..1\nnot ok 1 - foo.js\n ---\n message: Unexpected foo.\n severity: error\n data:\n line: 5\n column: 10\n ruleId: foo\n ...\n"); - }); - - it("should return a string with line: x, column: y, severity: warning for warnings", () => { - code[0].messages[0].severity = 1; - const result = formatter(code); - - assert.include(result, "line: 5"); - assert.include(result, "column: 10"); - assert.include(result, "ruleId: foo"); - assert.include(result, "severity: warning"); - assert.include(result, "1..1"); - }); - }); - - describe("when passed a fatal error message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Unexpected foo.", - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return an error string", () => { - const result = formatter(code); - - assert.include(result, "not ok"); - assert.include(result, "error"); - }); - }); - - describe("when passed a message with a severity of 1", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 1, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a warning string", () => { - const result = formatter(code); - - assert.include(result, "ok"); - assert.notInclude(result, "not ok"); - assert.include(result, "warning"); - }); - }); - - describe("when passed multiple messages with a severity of 1", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Foo.", - severity: 1, - line: 5, - column: 10, - ruleId: "foo" - }, { - message: "Bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }, { - message: "Baz.", - severity: 1, - line: 7, - column: 12, - ruleId: "baz" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.include(result, "ok"); - assert.notInclude(result, "not ok"); - assert.include(result, "messages"); - assert.include(result, "Foo."); - assert.include(result, "line: 5"); - assert.include(result, "column: 10"); - assert.include(result, "Bar."); - assert.include(result, "line: 6"); - assert.include(result, "column: 11"); - assert.include(result, "Baz."); - assert.include(result, "line: 7"); - assert.include(result, "column: 12"); - }); - }); - - describe("when passed multiple messages with different error severity", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }, { - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }, { - message: "Unexpected baz.", - severity: 1, - line: 7, - column: 12, - ruleId: "baz" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.include(result, "not ok"); - assert.include(result, "messages"); - assert.include(result, "Unexpected foo."); - assert.include(result, "line: 5"); - assert.include(result, "column: 10"); - assert.include(result, "Unexpected bar."); - assert.include(result, "line: 6"); - assert.include(result, "column: 11"); - assert.include(result, "Unexpected baz."); - assert.include(result, "line: 7"); - assert.include(result, "column: 12"); - }); - }); - - describe("when passed multiple files with 1 message each", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }, { - filePath: "bar.js", - messages: [{ - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.include(result, "not ok 1"); - assert.include(result, "ok 2"); - assert.notInclude(result, "not ok 2"); - }); - }); - - describe("when passed one file not found message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Couldn't find foo.js." - }] - }]; - - it("should return a string without line and column", () => { - const result = formatter(code); - - assert.include(result, "line: 0"); - assert.include(result, "column: 0"); - assert.include(result, "severity: error"); - assert.include(result, "1..1"); - }); - }); -}); diff --git a/tests/lib/cli-engine/formatters/unix.js b/tests/lib/cli-engine/formatters/unix.js deleted file mode 100644 index 5d9a320dc65..00000000000 --- a/tests/lib/cli-engine/formatters/unix.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * @fileoverview Tests for unix-style formatter. - * @author oshi-shinobu - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - formatter = require("../../../../lib/cli-engine/formatters/unix"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("formatter:compact", () => { - describe("when passed no messages", () => { - const code = [{ - filePath: "foo.js", - messages: [] - }]; - - it("should return nothing", () => { - const result = formatter(code); - - assert.strictEqual(result, ""); - }); - }); - - describe("when passed a single message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename:line:column: error [Error/rule_id]", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js:5:10: Unexpected foo. [Error/foo]\n\n1 problem"); - }); - - it("should return a string in the format filename:line:column: warning [Warning/rule_id]", () => { - code[0].messages[0].severity = 1; - const result = formatter(code); - - assert.strictEqual(result, "foo.js:5:10: Unexpected foo. [Warning/foo]\n\n1 problem"); - }); - }); - - describe("when passed a fatal error message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Unexpected foo.", - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename:line:column: error [Error/rule_id]", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js:5:10: Unexpected foo. [Error/foo]\n\n1 problem"); - }); - }); - - describe("when passed multiple messages", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }, { - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js:5:10: Unexpected foo. [Error/foo]\nfoo.js:6:11: Unexpected bar. [Warning/bar]\n\n2 problems"); - }); - }); - - describe("when passed multiple files with 1 message each", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }, { - filePath: "bar.js", - messages: [{ - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js:5:10: Unexpected foo. [Error/foo]\nbar.js:6:11: Unexpected bar. [Warning/bar]\n\n2 problems"); - }); - }); - - describe("when passed one file not found message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Couldn't find foo.js." - }] - }]; - - it("should return a string without line and column", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js:0:0: Couldn't find foo.js. [Error]\n\n1 problem"); - }); - }); -}); diff --git a/tests/lib/cli-engine/formatters/visualstudio.js b/tests/lib/cli-engine/formatters/visualstudio.js deleted file mode 100644 index c4ce358f039..00000000000 --- a/tests/lib/cli-engine/formatters/visualstudio.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * @fileoverview Tests for VisualStudio format. - * @author Ronald Pijnacker - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - formatter = require("../../../../lib/cli-engine/formatters/visualstudio"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("formatter:visualstudio", () => { - describe("when passed no messages", () => { - const code = [{ - filePath: "foo.js", - messages: [] - }]; - - it("should return nothing", () => { - const result = formatter(code); - - assert.strictEqual(result, "no problems"); - }); - }); - - describe("when passed a single message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename(x,y): error z for errors", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js(5,10): error foo : Unexpected foo.\n\n1 problem"); - }); - - it("should return a string in the format filename(x,y): warning z for warnings", () => { - code[0].messages[0].severity = 1; - const result = formatter(code); - - assert.strictEqual(result, "foo.js(5,10): warning foo : Unexpected foo.\n\n1 problem"); - }); - }); - - describe("when passed a fatal error message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Unexpected foo.", - line: 5, - column: 10, - ruleId: "foo" - }] - }]; - - it("should return a string in the format filename(x,y): error z", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js(5,10): error foo : Unexpected foo.\n\n1 problem"); - }); - }); - - describe("when passed multiple messages", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }, { - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js(5,10): error foo : Unexpected foo.\nfoo.js(6,11): warning bar : Unexpected bar.\n\n2 problems"); - }); - }); - - describe("when passed multiple files with 1 message each", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - message: "Unexpected foo.", - severity: 2, - line: 5, - column: 10, - ruleId: "foo" - }] - }, { - filePath: "bar.js", - messages: [{ - message: "Unexpected bar.", - severity: 1, - line: 6, - column: 11, - ruleId: "bar" - }] - }]; - - it("should return a string with multiple entries", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js(5,10): error foo : Unexpected foo.\nbar.js(6,11): warning bar : Unexpected bar.\n\n2 problems"); - }); - }); - - describe("when passed one file not found message", () => { - const code = [{ - filePath: "foo.js", - messages: [{ - fatal: true, - message: "Couldn't find foo.js." - }] - }]; - - it("should return a string without line and column", () => { - const result = formatter(code); - - assert.strictEqual(result, "foo.js(0): error : Couldn't find foo.js.\n\n1 problem"); - }); - }); -}); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index c69498231d0..f2576cc5e28 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -241,7 +241,7 @@ describe("cli", () => { it(`should execute without any errors with configType:${configType}`, async () => { const filePath = getFixturePath("passing.js"); const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc"; - const exit = await cli.execute(`${flag} -f checkstyle ${filePath}`, null, useFlatConfig); + const exit = await cli.execute(`${flag} -f json ${filePath}`, null, useFlatConfig); assert.strictEqual(exit, 0); }); @@ -570,7 +570,7 @@ describe("cli", () => { it(`should only print error with configType:${configType}`, async () => { const filePath = getFixturePath("single-quoted.js"); - const cliArgs = `--no-ignore --quiet -f compact --rule 'quotes: [2, double]' --rule 'no-unused-vars: 1' ${filePath}`; + const cliArgs = `--no-ignore --quiet -f stylish --rule 'quotes: [2, double]' --rule 'no-undef: 1' ${filePath}`; await cli.execute(cliArgs, null, useFlatConfig); @@ -578,13 +578,12 @@ describe("cli", () => { const formattedOutput = log.info.firstCall.args[0]; - assert.include(formattedOutput, "Error"); - assert.notInclude(formattedOutput, "Warning"); + assert.include(formattedOutput, "(1 error, 0 warnings)"); }); it(`should print nothing if there are no errors with configType:${configType}`, async () => { const filePath = getFixturePath("single-quoted.js"); - const cliArgs = `--quiet -f compact --rule 'quotes: [1, double]' --rule 'no-unused-vars: 1' ${filePath}`; + const cliArgs = `--no-ignore --quiet -f stylish --rule 'quotes: [1, double]' --rule 'no-undef: 1' ${filePath}`; await cli.execute(cliArgs, null, useFlatConfig); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 4e7499da0d4..754dec5253b 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -5064,7 +5064,7 @@ describe("ESLint", () => { describe("loadFormatter()", () => { it("should return a formatter object when a bundled formatter is requested", async () => { const engine = new ESLint(); - const formatter = await engine.loadFormatter("compact"); + const formatter = await engine.loadFormatter("json"); assert.strictEqual(typeof formatter, "object"); assert.strictEqual(typeof formatter.format, "function"); diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js index 365a1223896..d131d8d1b04 100644 --- a/tests/lib/eslint/flat-eslint.js +++ b/tests/lib/eslint/flat-eslint.js @@ -4399,7 +4399,7 @@ describe("FlatESLint", () => { describe("loadFormatter()", () => { it("should return a formatter object when a bundled formatter is requested", async () => { const engine = new FlatESLint(); - const formatter = await engine.loadFormatter("compact"); + const formatter = await engine.loadFormatter("json"); assert.strictEqual(typeof formatter, "object"); assert.strictEqual(typeof formatter.format, "function"); diff --git a/tests/lib/options.js b/tests/lib/options.js index b663e8623e3..8387f86296b 100644 --- a/tests/lib/options.js +++ b/tests/lib/options.js @@ -69,10 +69,10 @@ describe("options", () => { describe("--format", () => { it("should return a string for .format when passed a string", () => { - const currentOptions = options.parse("--format compact"); + const currentOptions = options.parse("--format json"); assert.isString(currentOptions.format); - assert.strictEqual(currentOptions.format, "compact"); + assert.strictEqual(currentOptions.format, "json"); }); it("should return stylish for .format when not passed", () => { @@ -85,10 +85,10 @@ describe("options", () => { describe("-f", () => { it("should return a string for .format when passed a string", () => { - const currentOptions = options.parse("-f compact"); + const currentOptions = options.parse("-f json"); assert.isString(currentOptions.format); - assert.strictEqual(currentOptions.format, "compact"); + assert.strictEqual(currentOptions.format, "json"); }); }); From f71c328e2786e2d73f168e43c7f96de172484a49 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 20 Dec 2023 08:28:01 -0500 Subject: [PATCH 09/25] feat!: Swap FlatESLint-ESLint, FlatRuleTester-RuleTester in API (#17823) feat!: Swap FlatESLint-ESLint, FlatRuleTeser-RuleTester in API Refs #13481 --- lib/api.js | 8 ++++---- lib/rule-tester/index.js | 3 ++- tests/lib/api.js | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/api.js b/lib/api.js index 3dde0985505..fd43d9ef332 100644 --- a/lib/api.js +++ b/lib/api.js @@ -9,9 +9,9 @@ // Requirements //----------------------------------------------------------------------------- -const { ESLint } = require("./eslint"); +const { FlatESLint } = require("./eslint/flat-eslint"); const { Linter } = require("./linter"); -const { RuleTester } = require("./rule-tester"); +const { FlatRuleTester } = require("./rule-tester"); const { SourceCode } = require("./source-code"); //----------------------------------------------------------------------------- @@ -20,7 +20,7 @@ const { SourceCode } = require("./source-code"); module.exports = { Linter, - ESLint, - RuleTester, + ESLint: FlatESLint, + RuleTester: FlatRuleTester, SourceCode }; diff --git a/lib/rule-tester/index.js b/lib/rule-tester/index.js index f52d14027c5..27d4c402fc8 100644 --- a/lib/rule-tester/index.js +++ b/lib/rule-tester/index.js @@ -1,5 +1,6 @@ "use strict"; module.exports = { - RuleTester: require("./rule-tester") + RuleTester: require("./rule-tester"), + FlatRuleTester: require("./flat-rule-tester") }; diff --git a/tests/lib/api.js b/tests/lib/api.js index 074d206e52e..abcbea5aef1 100644 --- a/tests/lib/api.js +++ b/tests/lib/api.js @@ -18,6 +18,10 @@ const assert = require("chai").assert, describe("api", () => { + it("should have ESLint exposed", () => { + assert.isFunction(api.ESLint); + }); + it("should have RuleTester exposed", () => { assert.isFunction(api.RuleTester); }); From ae78ff16558a1a2ca07b2b9cd294157d1bdcce2e Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 20 Dec 2023 08:45:23 -0500 Subject: [PATCH 10/25] feat!: Remove deprecated context methods (#17698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat!: Remove deprecated context methods fixes #17520 * Fix broken tests * Remove BASE_TRAVERSAL_CONTEXT * Remove deprecated methods from RuleTester * Removed more methods/properties * Fix lint errors * Update docs/src/extend/custom-rules.md Co-authored-by: 唯然 * Update docs/src/extend/custom-rules.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/custom-rules.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/custom-rules.md Co-authored-by: Milos Djermanovic * Remove unneeded deprecated notices * Restore parserServices tests --------- Co-authored-by: 唯然 Co-authored-by: Milos Djermanovic --- docs/src/extend/custom-rules.md | 31 +- lib/linter/linter.js | 77 +- lib/rule-tester/rule-tester.js | 94 +- package.json | 2 +- tests/lib/linter/linter.js | 15186 ++++++++------------ tests/lib/rule-tester/flat-rule-tester.js | 1 - tests/lib/rule-tester/rule-tester.js | 111 - tests/lib/source-code/source-code.js | 18 +- 8 files changed, 6164 insertions(+), 9356 deletions(-) diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index 35df20541b6..f196100c3b6 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -131,28 +131,14 @@ The `context` object has the following properties: * `sourceCode`: (`object`) A `SourceCode` object that you can use to work with the source that was passed to ESLint (see [Accessing the Source Code](#accessing-the-source-code)). * `settings`: (`object`) The [shared settings](../use/configure/configuration-files#adding-shared-settings) from the configuration. * `parserPath`: (`string`) The name of the `parser` from the configuration. -* `parserServices`: (**Deprecated:** Use `SourceCode#parserServices` instead.) Contains parser-provided services for rules. The default parser does not provide any services. However, if a rule is intended to be used with a custom parser, it could use `parserServices` to access anything provided by that parser. (For example, a TypeScript parser could provide the ability to get the computed type of a given node.) * `parserOptions`: The parser options configured for this run (more details [here](../use/configure/language-options#specifying-parser-options)). Additionally, the `context` object has the following methods: -* `getAncestors()`: (**Deprecated:** Use `SourceCode#getAncestors(node)` instead.) Returns an array of the ancestors of the currently-traversed node, starting at the root of the AST and continuing through the direct parent of the current node. This array does not include the currently-traversed node itself. * `getCwd()`: (**Deprecated:** Use `context.cwd` instead.) Returns the `cwd` option passed to the [Linter](../integrate/nodejs-api#linter). It is a path to a directory that should be considered the current working directory. -* `getDeclaredVariables(node)`: (**Deprecated:** Use `SourceCode#getDeclaredVariables(node)` instead.) Returns a list of [variables](./scope-manager-interface#variable-interface) declared by the given node. This information can be used to track references to variables. - * If the node is a `VariableDeclaration`, all variables declared in the declaration are returned. - * If the node is a `VariableDeclarator`, all variables declared in the declarator are returned. - * If the node is a `FunctionDeclaration` or `FunctionExpression`, the variable for the function name is returned, in addition to variables for the function parameters. - * If the node is an `ArrowFunctionExpression`, variables for the parameters are returned. - * If the node is a `ClassDeclaration` or a `ClassExpression`, the variable for the class name is returned. - * If the node is a `CatchClause`, the variable for the exception is returned. - * If the node is an `ImportDeclaration`, variables for all of its specifiers are returned. - * If the node is an `ImportSpecifier`, `ImportDefaultSpecifier`, or `ImportNamespaceSpecifier`, the declared variable is returned. - * Otherwise, if the node does not declare any variables, an empty array is returned. * `getFilename()`: (**Deprecated:** Use `context.filename` instead.) Returns the filename associated with the source. * `getPhysicalFilename()`: (**Deprecated:** Use `context.physicalFilename` instead.) When linting a file, it returns the full path of the file on disk without any code block information. When linting text, it returns the value passed to `—stdin-filename` or `` if not specified. -* `getScope()`: (**Deprecated:** Use `SourceCode#getScope(node)` instead.) Returns the [scope](./scope-manager-interface#scope-interface) of the currently-traversed node. This information can be used to track references to variables. * `getSourceCode()`: (**Deprecated:** Use `context.sourceCode` instead.) Returns a `SourceCode` object that you can use to work with the source that was passed to ESLint (see [Accessing the Source Code](#accessing-the-source-code)). -* `markVariableAsUsed(name)`: (**Deprecated:** Use `SourceCode#markVariableAsUsed(name, node)` instead.) Marks a variable with the given name in the current scope as used. This affects the [no-unused-vars](../rules/no-unused-vars) rule. Returns `true` if a variable with the given name was found and marked as used, otherwise `false`. * `report(descriptor)`. Reports a problem in the code (see the [dedicated section](#reporting-problems)). **Note:** Earlier versions of ESLint supported additional methods on the `context` object. Those methods were removed in the new format and should not be relied upon. @@ -551,6 +537,19 @@ Once you have an instance of `SourceCode`, you can use the following methods on * `getLocFromIndex(index)`: Returns an object with `line` and `column` properties, corresponding to the location of the given source index. `line` is 1-based and `column` is 0-based. * `getIndexFromLoc(loc)`: Returns the index of a given location in the source code, where `loc` is an object with a 1-based `line` key and a 0-based `column` key. * `commentsExistBetween(nodeOrToken1, nodeOrToken2)`: Returns `true` if comments exist between two nodes. +* `getAncestors(node)`: Returns an array of the ancestors of the given node, starting at the root of the AST and continuing through the direct parent of the given node. This array does not include the given node itself. +* `getDeclaredVariables(node)`: Returns a list of [variables](./scope-manager-interface#variable-interface) declared by the given node. This information can be used to track references to variables. + * If the node is a `VariableDeclaration`, all variables declared in the declaration are returned. + * If the node is a `VariableDeclarator`, all variables declared in the declarator are returned. + * If the node is a `FunctionDeclaration` or `FunctionExpression`, the variable for the function name is returned, in addition to variables for the function parameters. + * If the node is an `ArrowFunctionExpression`, variables for the parameters are returned. + * If the node is a `ClassDeclaration` or a `ClassExpression`, the variable for the class name is returned. + * If the node is a `CatchClause`, the variable for the exception is returned. + * If the node is an `ImportDeclaration`, variables for all of its specifiers are returned. + * If the node is an `ImportSpecifier`, `ImportDefaultSpecifier`, or `ImportNamespaceSpecifier`, the declared variable is returned. + * Otherwise, if the node does not declare any variables, an empty array is returned. +* `getScope(node)`: Returns the [scope](./scope-manager-interface#scope-interface) of the given node. This information can be used to track references to variables. +* `markVariableAsUsed(name, refNode)`: Marks a variable with the given name in a scope indicated by the given reference node as used. This affects the [no-unused-vars](../rules/no-unused-vars) rule. Returns `true` if a variable with the given name was found and marked as used, otherwise `false`. `skipOptions` is an object which has 3 properties; `skip`, `includeComments`, and `filter`. Default is `{skip: 0, includeComments: false, filter: null}`. @@ -787,8 +786,6 @@ To learn more about JSON Schema, we recommend looking at some examples on the [J The `SourceCode#getScope(node)` method returns the scope of the given node. It is a useful method for finding information about the variables in a given scope and how they are used in other scopes. -**Deprecated:** The `context.getScope()` is deprecated; make sure to use `SourceCode#getScope(node)` instead. - #### Scope types The following table contains a list of AST node types and the scope type that they correspond to. For more information about the scope types, refer to the [`Scope` object documentation](./scope-manager-interface#scope-interface). @@ -836,8 +833,6 @@ For examples of using `SourceCode#getScope()` to track variables, refer to the s ### Marking Variables as Used -**Deprecated:** The `context.markVariableAsUsed()` method is deprecated in favor of `sourceCode.markVariableAsUsed()`. - Certain ESLint rules, such as [`no-unused-vars`](../rules/no-unused-vars), check to see if a variable has been used. ESLint itself only knows about the standard rules of variable access and so custom ways of accessing variables may not register as "used". To help with this, you can use the `sourceCode.markVariableAsUsed()` method. This method takes two arguments: the name of the variable to mark as used and an option reference node indicating the scope in which you are working. Here's an example: diff --git a/lib/linter/linter.js b/lib/linter/linter.js index f74d0ecd13f..998d01ad6bf 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -899,43 +899,6 @@ function createRuleListeners(rule, ruleContext) { } } -// methods that exist on SourceCode object -const DEPRECATED_SOURCECODE_PASSTHROUGHS = { - getSource: "getText", - getSourceLines: "getLines", - getAllComments: "getAllComments", - getNodeByRangeIndex: "getNodeByRangeIndex", - getComments: "getComments", - getCommentsBefore: "getCommentsBefore", - getCommentsAfter: "getCommentsAfter", - getCommentsInside: "getCommentsInside", - getJSDocComment: "getJSDocComment", - getFirstToken: "getFirstToken", - getFirstTokens: "getFirstTokens", - getLastToken: "getLastToken", - getLastTokens: "getLastTokens", - getTokenAfter: "getTokenAfter", - getTokenBefore: "getTokenBefore", - getTokenByRangeStart: "getTokenByRangeStart", - getTokens: "getTokens", - getTokensAfter: "getTokensAfter", - getTokensBefore: "getTokensBefore", - getTokensBetween: "getTokensBetween" -}; - - -const BASE_TRAVERSAL_CONTEXT = Object.freeze( - Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce( - (contextInfo, methodName) => - Object.assign(contextInfo, { - [methodName](...args) { - return this.sourceCode[DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]](...args); - } - }), - {} - ) -); - /** * Runs the given rules on the given SourceCode object * @param {SourceCode} sourceCode A SourceCode object for the given text @@ -972,30 +935,22 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO * properties once for each rule. */ const sharedTraversalContext = Object.freeze( - Object.assign( - Object.create(BASE_TRAVERSAL_CONTEXT), - { - getAncestors: () => sourceCode.getAncestors(currentNode), - getDeclaredVariables: node => sourceCode.getDeclaredVariables(node), - getCwd: () => cwd, - cwd, - getFilename: () => filename, - filename, - getPhysicalFilename: () => physicalFilename || filename, - physicalFilename: physicalFilename || filename, - getScope: () => sourceCode.getScope(currentNode), - getSourceCode: () => sourceCode, - sourceCode, - markVariableAsUsed: name => sourceCode.markVariableAsUsed(name, currentNode), - parserOptions: { - ...languageOptions.parserOptions - }, - parserPath: parserName, - languageOptions, - parserServices: sourceCode.parserServices, - settings - } - ) + { + getCwd: () => cwd, + cwd, + getFilename: () => filename, + filename, + getPhysicalFilename: () => physicalFilename || filename, + physicalFilename: physicalFilename || filename, + getSourceCode: () => sourceCode, + sourceCode, + parserOptions: { + ...languageOptions.parserOptions + }, + parserPath: parserName, + languageOptions, + settings + } ); const lintingProblems = []; diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 3bc80ab1837..ad410094f4e 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -171,35 +171,6 @@ const forbiddenMethods = [ const hasOwnProperty = Function.call.bind(Object.hasOwnProperty); -const DEPRECATED_SOURCECODE_PASSTHROUGHS = { - getSource: "getText", - getSourceLines: "getLines", - getAllComments: "getAllComments", - getNodeByRangeIndex: "getNodeByRangeIndex", - - // getComments: "getComments", -- already handled by a separate error - getCommentsBefore: "getCommentsBefore", - getCommentsAfter: "getCommentsAfter", - getCommentsInside: "getCommentsInside", - getJSDocComment: "getJSDocComment", - getFirstToken: "getFirstToken", - getFirstTokens: "getFirstTokens", - getLastToken: "getLastToken", - getLastTokens: "getLastTokens", - getTokenAfter: "getTokenAfter", - getTokenBefore: "getTokenBefore", - getTokenByRangeStart: "getTokenByRangeStart", - getTokens: "getTokens", - getTokensAfter: "getTokensAfter", - getTokensBefore: "getTokensBefore", - getTokensBetween: "getTokensBetween", - - getScope: "getScope", - getAncestors: "getAncestors", - getDeclaredVariables: "getDeclaredVariables", - markVariableAsUsed: "markVariableAsUsed" -}; - /** * Clones a given value deeply. * Note: This ignores `parent` property. @@ -384,22 +355,6 @@ function emitMissingSchemaWarning(ruleName) { } } -/** - * Emit a deprecation warning if a rule uses a deprecated `context` method. - * @param {string} ruleName Name of the rule. - * @param {string} methodName The name of the method on `context` that was used. - * @returns {void} - */ -function emitDeprecatedContextMethodWarning(ruleName, methodName) { - if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) { - emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true; - process.emitWarning( - `"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`, - "DeprecationWarning" - ); - } -} - /** * Emit a deprecation warning if rule uses CodePath#currentSegments. * @param {string} ruleName Name of the rule. @@ -415,22 +370,6 @@ function emitCodePathCurrentSegmentsWarning(ruleName) { } } -/** - * Emit a deprecation warning if `context.parserServices` is used. - * @param {string} ruleName Name of the rule. - * @returns {void} - */ -function emitParserServicesWarning(ruleName) { - if (!emitParserServicesWarning[`warned-${ruleName}`]) { - emitParserServicesWarning[`warned-${ruleName}`] = true; - process.emitWarning( - `"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`, - "DeprecationWarning" - ); - } -} - - //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -662,38 +601,7 @@ class RuleTester { freezeDeeply(context.settings); freezeDeeply(context.parserOptions); - // wrap all deprecated methods - const newContext = Object.create( - context, - Object.fromEntries(Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).map(methodName => [ - methodName, - { - value(...args) { - - // emit deprecation warning - emitDeprecatedContextMethodWarning(ruleName, methodName); - - // call the original method - return context[methodName].call(this, ...args); - }, - enumerable: true - } - ])) - ); - - // emit warning about context.parserServices - const parserServices = context.parserServices; - - Object.defineProperty(newContext, "parserServices", { - get() { - emitParserServicesWarning(ruleName); - return parserServices; - } - }); - - Object.freeze(newContext); - - return (typeof rule === "function" ? rule : rule.create)(newContext); + return (typeof rule === "function" ? rule : rule.create)(context); } })); diff --git a/package.json b/package.json index 8cbd2fcf320..473da26112f 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-eslint-plugin": "^5.2.1", "eslint-plugin-internal-rules": "file:tools/internal-rules", - "eslint-plugin-jsdoc": "^46.2.5", + "eslint-plugin-jsdoc": "^46.9.0", "eslint-plugin-n": "^16.4.0", "eslint-plugin-unicorn": "^49.0.0", "eslint-release": "^3.2.0", diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index dec2aabb67d..e78d13ca0ef 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -134,71 +134,6 @@ describe("Linter", () => { }); }); - 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 {}; - }); - - linter.defineRule("checker", { create: spy }); - linter.verify(code, { rules: { checker: "error" } }); - 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 {}; - }); - - linter.defineRule("checker", { create: spy }); - linter.verify(code, { rules: { checker: "error" } }); - 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 {}; - }); - - linter.defineRule("checker", { create: spy }); - linter.verify(code, { rules: { checker: "error" } }); - 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 {}; - }); - - linter.defineRule("checker", { create: spy }); - linter.verify(code, { rules: { checker: "error" } }); - 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 {}; - }); - - linter.defineRule("checker", { create: spy }); - linter.verify(code, { rules: { checker: "error" } }); - assert(spy.calledOnce); - }); - - - }); - describe("getSourceCode()", () => { const code = TEST_CODE; @@ -349,2002 +284,2090 @@ describe("Linter", () => { }); }); - describe("context.getSource()", () => { + describe("when evaluating code", () => { const code = TEST_CODE; - it("should retrieve all text when used without parameters", () => { - + it("events for each node type should fire", () => { const config = { rules: { checker: "error" } }; - let spy; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - assert.strictEqual(context.getSource(), TEST_CODE); - }); - return { Program: spy }; - } - }); - - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); - - it("should retrieve all text for root node", () => { - const config = { rules: { checker: "error" } }; - let spy; + // spies for various AST node types + const spyLiteral = sinon.spy(), + spyVariableDeclarator = sinon.spy(), + spyVariableDeclaration = sinon.spy(), + spyIdentifier = sinon.spy(), + spyBinaryExpression = sinon.spy(); linter.defineRule("checker", { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node), TEST_CODE); - }); - return { Program: spy }; - } + create: () => ({ + Literal: spyLiteral, + VariableDeclarator: spyVariableDeclarator, + VariableDeclaration: spyVariableDeclaration, + Identifier: spyIdentifier, + BinaryExpression: spyBinaryExpression + }) }); - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - it("should clamp to valid range when retrieving characters before start of source", () => { - const config = { rules: { checker: "error" } }; - let spy; + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + sinon.assert.calledOnce(spyVariableDeclaration); + sinon.assert.calledOnce(spyVariableDeclarator); + sinon.assert.calledOnce(spyIdentifier); + sinon.assert.calledTwice(spyLiteral); + sinon.assert.calledOnce(spyBinaryExpression); + }); - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node, 2, 0), TEST_CODE); - }); - return { Program: spy }; - } + it("should throw an error if a rule reports a problem without a message", () => { + linter.defineRule("invalid-report", { + create: context => ({ + Program(node) { + context.report({ node }); + } + }) }); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.throws( + () => linter.verify("foo", { rules: { "invalid-report": "error" } }), + TypeError, + "Missing `message` property in report() call; add a message that describes the linting problem." + ); }); + }); - it("should retrieve all text for binary expression", () => { - const config = { rules: { checker: "error" } }; - let spy; + describe("when config has shared settings for rules", () => { + const code = "test-rule"; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node), "6 * 7"); - }); - return { BinaryExpression: spy }; - } + it("should pass settings to all rules", () => { + linter.defineRule(code, { + create: context => ({ + Literal(node) { + context.report(node, context.settings.info); + } + }) }); - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); + const config = { rules: {}, settings: { info: "Hello" } }; - it("should retrieve all text plus two characters before for binary expression", () => { - const config = { rules: { checker: "error" } }; - let spy; + config.rules[code] = 1; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node, 2), "= 6 * 7"); - }); - return { BinaryExpression: spy }; - } - }); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hello"); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve all text plus one character after for binary expression", () => { - const config = { rules: { checker: "error" } }; - let spy; - - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node, 0, 1), "6 * 7;"); - }); - return { BinaryExpression: spy }; - } + it("should not have any settings if they were not passed in", () => { + linter.defineRule(code, { + create: context => ({ + Literal(node) { + if (Object.getOwnPropertyNames(context.settings).length !== 0) { + context.report(node, "Settings should be empty"); + } + } + }) }); - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); + const config = { rules: {} }; - it("should retrieve all text plus two characters before and one character after for binary expression", () => { - const config = { rules: { checker: "error" } }; - let spy; + config.rules[code] = 1; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node, 2, 1), "= 6 * 7;"); - }); - return { BinaryExpression: spy }; - } - }); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - }); - describe("when calling context.getAncestors", () => { - const code = TEST_CODE; - - it("should retrieve all ancestors when used", () => { - - const config = { rules: { checker: "error" } }; - let spy; + describe("when config has parseOptions", () => { - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const ancestors = context.getAncestors(); + it("should pass ecmaFeatures to all rules when provided on config", () => { - assert.strictEqual(ancestors.length, 3); - }); - return { BinaryExpression: spy }; + const parserOptions = { + ecmaFeatures: { + jsx: true, + globalReturn: true } + }; + + linter.defineRule("test-rule", { + create: sinon.mock().withArgs( + sinon.match({ parserOptions }) + ).returns({}) }); - linter.verify(code, config, filename, true); - assert(spy && spy.calledOnce); - }); + const config = { rules: { "test-rule": 2 }, parserOptions }; - it("should retrieve empty ancestors for root node", () => { - const config = { rules: { checker: "error" } }; - let spy; + linter.verify("0", config, filename); + }); - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const ancestors = context.getAncestors(); + it("should pass parserOptions to all rules when default parserOptions is used", () => { - assert.strictEqual(ancestors.length, 0); - }); + const parserOptions = {}; - return { Program: spy }; - } + linter.defineRule("test-rule", { + create: sinon.mock().withArgs( + sinon.match({ parserOptions }) + ).returns({}) }); - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); - }); - - describe("when calling context.getNodeByRangeIndex", () => { - const code = TEST_CODE; - - it("should retrieve a node starting at the given index", () => { - const config = { rules: { checker: "error" } }; - const spy = sinon.spy(context => { - assert.strictEqual(context.getNodeByRangeIndex(4).type, "Identifier"); - return {}; - }); + const config = { rules: { "test-rule": 2 } }; - linter.defineRule("checker", { create: spy }); - linter.verify(code, config); - assert(spy.calledOnce); + linter.verify("0", config, filename); }); - it("should retrieve a node containing the given index", () => { - const config = { rules: { checker: "error" } }; - const spy = sinon.spy(context => { - assert.strictEqual(context.getNodeByRangeIndex(6).type, "Identifier"); - return {}; - }); + }); - linter.defineRule("checker", { create: spy }); - linter.verify(code, config); - assert(spy.calledOnce); - }); + describe("when a custom parser is defined using defineParser", () => { - it("should retrieve a node that is exactly the given index", () => { - const config = { rules: { checker: "error" } }; - const spy = sinon.spy(context => { - const node = context.getNodeByRangeIndex(13); + 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!"; + } + } + } + }; + } + }; - assert.strictEqual(node.type, "Literal"); - assert.strictEqual(node.value, 6); - return {}; - }); + linter.defineParser("test-parser", parser); + const config = { rules: {}, parser: "test-parser" }; + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.defineRule("checker", { create: spy }); - linter.verify(code, config); - assert(spy.calledOnce); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve a node ending with the given index", () => { - const config = { rules: { checker: "error" } }; - const spy = sinon.spy(context => { - assert.strictEqual(context.getNodeByRangeIndex(9).type, "Identifier"); - return {}; - }); - - linter.defineRule("checker", { create: spy }); - linter.verify(code, config); - assert(spy.calledOnce); - }); + }); - it("should retrieve the deepest node containing the given index", () => { - const config = { rules: { checker: "error" } }; - const spy = sinon.spy(context => { - const node1 = context.getNodeByRangeIndex(14); + describe("when config has parser", () => { - assert.strictEqual(node1.type, "BinaryExpression"); + it("should pass parser as parserPath to all rules when provided on config", () => { - const node2 = context.getNodeByRangeIndex(3); + const alternateParser = "esprima"; - assert.strictEqual(node2.type, "VariableDeclaration"); - return {}; + linter.defineParser("esprima", esprima); + linter.defineRule("test-rule", { + create: sinon.mock().withArgs( + sinon.match({ parserPath: alternateParser }) + ).returns({}) }); - linter.defineRule("checker", { create: spy }); - linter.verify(code, config); - assert(spy.calledOnce); + const config = { rules: { "test-rule": 2 }, parser: alternateParser }; + + linter.verify("0", config, filename); }); - it("should return null if the index is outside the range of any node", () => { - const config = { rules: { checker: "error" } }; - const spy = sinon.spy(context => { - const node1 = context.getNodeByRangeIndex(-1); + it("should use parseForESLint() in custom parser when custom parser is specified", () => { + const config = { rules: {}, parser: "enhanced-parser" }; - assert.isNull(node1); + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - const node2 = context.getNodeByRangeIndex(-99); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.isNull(node2); - return {}; + it("should expose parser services when using parseForESLint() and services are specified", () => { + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + linter.defineRule("test-service-rule", { + create: context => ({ + Literal(node) { + context.report({ + node, + message: context.sourceCode.parserServices.test.getMessage() + }); + } + }) }); - linter.defineRule("checker", { create: spy }); - linter.verify(code, config); - assert(spy.calledOnce); - }); - }); + const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); + assert.strictEqual(suppressedMessages.length, 0); + }); - describe("when calling context.getScope", () => { - const code = "function foo() { q: for(;;) { break q; } } function bar () { var q = t; } var baz = (() => { return 1; });"; + it("should use the same parserServices if source code object is reused", () => { + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + linter.defineRule("test-service-rule", { + create: context => ({ + Literal(node) { + context.report({ + node, + message: context.sourceCode.parserServices.test.getMessage() + }); + } + }) + }); - it("should retrieve the global scope correctly from a Program", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; + const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); + assert.strictEqual(suppressedMessages.length, 0); - assert.strictEqual(scope.type, "global"); - }); - return { Program: spy }; - } - }); + const messages2 = linter.verify(linter.getSourceCode(), config, filename); + const suppressedMessages2 = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages2.length, 1); + assert.strictEqual(messages2[0].message, "Hi!"); + assert.strictEqual(suppressedMessages2.length, 0); }); - it("should retrieve the function scope correctly from a FunctionDeclaration", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; - - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); - - assert.strictEqual(scope.type, "function"); - }); - return { FunctionDeclaration: spy }; - } + it("should pass parser as parserPath to all rules when default parser is used", () => { + linter.defineRule("test-rule", { + create: sinon.mock().withArgs( + sinon.match({ parserPath: "espree" }) + ).returns({}) }); - linter.verify(code, config); - assert(spy && spy.calledTwice); - }); + const config = { rules: { "test-rule": 2 } }; - it("should retrieve the function scope correctly from a LabeledStatement", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; + linter.verify("0", config, filename); + }); - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + }); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block.id.name, "foo"); - }); - return { LabeledStatement: spy }; - } - }); - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); + describe("when passing in configuration values for rules", () => { + const code = "var answer = 6 * 7"; - it("should retrieve the function scope correctly from within an ArrowFunctionExpression", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; + it("should be configurable by only setting the integer value", () => { + const rule = "semi", + config = { rules: {} }; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + config.rules[rule] = 1; - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block.type, "ArrowFunctionExpression"); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - return { ReturnStatement: spy }; - } - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, rule); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve the function scope correctly from within an SwitchStatement", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; + it("should be configurable by only setting the string value", () => { + const rule = "semi", + config = { rules: {} }; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + config.rules[rule] = "warn"; - assert.strictEqual(scope.type, "switch"); - assert.strictEqual(scope.block.type, "SwitchStatement"); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - return { SwitchStatement: spy }; - } - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 1); + assert.strictEqual(messages[0].ruleId, rule); - linter.verify("switch(foo){ case 'a': var b = 'foo'; }", config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve the function scope correctly from within a BlockStatement", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; + it("should be configurable by passing in values as an array", () => { + const rule = "semi", + config = { rules: {} }; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + config.rules[rule] = [1]; - assert.strictEqual(scope.type, "block"); - assert.strictEqual(scope.block.type, "BlockStatement"); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - return { BlockStatement: spy }; - } - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, rule); - linter.verify("var x; {let y = 1}", config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve the function scope correctly from within a nested block statement", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; + it("should be configurable by passing in string value as an array", () => { + const rule = "semi", + config = { rules: {} }; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + config.rules[rule] = ["warn"]; - assert.strictEqual(scope.type, "block"); - assert.strictEqual(scope.block.type, "BlockStatement"); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - return { BlockStatement: spy }; - } - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 1); + assert.strictEqual(messages[0].ruleId, rule); - linter.verify("if (true) { let x = 1 }", config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve the function scope correctly from within a FunctionDeclaration", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; - - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + it("should not be configurable by setting other value", () => { + const rule = "semi", + config = { rules: {} }; - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block.type, "FunctionDeclaration"); - }); + config.rules[rule] = "1"; - return { FunctionDeclaration: spy }; - } - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify("function foo() {}", config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve the function scope correctly from within a FunctionExpression", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; - let spy; + it("should process empty config", () => { + const config = {}; + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block.type, "FunctionExpression"); - }); + describe("when evaluating code containing /*global */ and /*globals */ blocks", () => { - return { FunctionExpression: spy }; - } + it("variables should be available in global scope", () => { + const config = { rules: { checker: "error" }, globals: { Array: "off", ConfigGlobal: "writeable" } }; + const code = ` + /*global a b:true c:false d:readable e:writeable Math:off */ + function foo() {} + /*globals f:true*/ + /* global ConfigGlobal : readable */ + `; + let spy; + + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); + 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 }; + } }); - linter.verify("(function foo() {})();", config); + 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("should retrieve the catch scope correctly from within a CatchClause", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6 } }; + it("variables should be available in global scope", () => { + const config = { rules: { checker: "error" } }; let spy; linter.defineRule("checker", { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + a = getVariable(scope, "a"), + b = getVariable(scope, "b"), + c = getVariable(scope, "c"); - assert.strictEqual(scope.type, "catch"); - assert.strictEqual(scope.block.type, "CatchClause"); + 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 { CatchClause: spy }; + return { Program: spy }; } }); - linter.verify("try {} catch (err) {}", config); + linter.verify(code, config); assert(spy && spy.calledOnce); }); + }); + + describe("when evaluating code containing a /*global */ block with specific variables", () => { + const code = "/* global toString hasOwnProperty valueOf: true */"; + + it("should not throw an error if comment block has global variables which are Object.prototype contains", () => { + const config = { rules: { checker: "error" } }; + + linter.verify(code, config); + }); + }); - it("should retrieve module scope correctly from an ES6 module", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, sourceType: "module" } }; + describe("when evaluating code containing /*eslint-env */ block", () => { + it("variables should be available in global scope", () => { + const code = `/*${ESLINT_ENV} node*/ function f() {} /*${ESLINT_ENV} browser, foo*/`; + const config = { rules: { checker: "error" } }; let spy; linter.defineRule("checker", { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + exports = getVariable(scope, "exports"), + window = getVariable(scope, "window"); - assert.strictEqual(scope.type, "module"); + assert.strictEqual(exports.writeable, true); + assert.strictEqual(window.writeable, false); }); - return { AssignmentExpression: spy }; + return { Program: spy }; } }); - linter.verify("var foo = {}; foo.bar = 1;", config); + linter.verify(code, config); assert(spy && spy.calledOnce); }); - it("should retrieve function scope correctly when globalReturn is true", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, ecmaFeatures: { globalReturn: true } } }; + it("variables should be available in global scope with quoted items", () => { + const code = `/*${ESLINT_ENV} 'node'*/ function f() {} /*${ESLINT_ENV} "browser", "mocha"*/`; + const config = { rules: { checker: "error" } }; let spy; linter.defineRule("checker", { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + exports = getVariable(scope, "exports"), + window = getVariable(scope, "window"), + it = getVariable(scope, "it"); - assert.strictEqual(scope.type, "function"); + assert.strictEqual(exports.writeable, true); + assert.strictEqual(window.writeable, false); + assert.strictEqual(it.writeable, false); }); - return { AssignmentExpression: spy }; + return { Program: spy }; } }); - linter.verify("var foo = {}; foo.bar = 1;", config); + linter.verify(code, config); assert(spy && spy.calledOnce); }); }); - describe("marking variables as used", () => { - it("should mark variables in current scope as used", () => { - const code = "var a = 1, b = 2;"; + describe("when evaluating code containing /*eslint-env */ block with sloppy whitespace", () => { + const code = `/* ${ESLINT_ENV} ,, node , no-browser ,, */`; + + it("variables should be available in global scope", () => { + const config = { rules: { checker: "error" } }; let spy; linter.defineRule("checker", { create(context) { - spy = sinon.spy(() => { - assert.isTrue(context.markVariableAsUsed("a")); - - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + exports = getVariable(scope, "exports"), + window = getVariable(scope, "window"); - assert.isTrue(getVariable(scope, "a").eslintUsed); - assert.notOk(getVariable(scope, "b").eslintUsed); + assert.strictEqual(exports.writeable, true); + assert.strictEqual(window, null); }); - return { "Program:exit": spy }; + return { Program: spy }; } }); - linter.verify(code, { rules: { 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; }"; + }); + + describe("when evaluating code containing /*exported */ block", () => { + + 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'"; + const config = { rules: { checker: "error" } }; let spy; linter.defineRule("checker", { create(context) { - spy = sinon.spy(() => { - assert.isTrue(context.markVariableAsUsed("a")); - - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + horse = getVariable(scope, "horse"); - assert.isTrue(getVariable(scope, "a").eslintUsed); - assert.notOk(getVariable(scope, "b").eslintUsed); + assert.strictEqual(horse.eslintUsed, true); }); - return { ReturnStatement: spy }; + return { Program: spy }; } }); - linter.verify(code, { rules: { 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; + + it("undefined variables should not be exported", () => { + const code = "/* exported horse */\n\nhorse = 'circus'"; + const config = { rules: { checker: "error" } }; + let spy; linter.defineRule("checker", { create(context) { - returnSpy = sinon.spy(() => { - assert.isTrue(context.markVariableAsUsed("a")); - }); - exitSpy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + horse = getVariable(scope, "horse"); - assert.isTrue(getVariable(scope, "a").eslintUsed); - assert.notOk(getVariable(scope, "b").eslintUsed); + assert.strictEqual(horse, null); }); - return { ReturnStatement: returnSpy, "Program:exit": exitSpy }; + return { Program: spy }; } }); - linter.verify(code, { rules: { checker: "error" } }); - assert(returnSpy && returnSpy.calledOnce); - assert(exitSpy && exitSpy.calledOnce); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); - it("should mark variables in Node.js environment as used", () => { - const code = "var a = 1, b = 2;"; + it("variables should be exported in strict mode", () => { + const code = "/* exported horse */\n'use strict';\nvar horse = 'circus'"; + const config = { rules: { checker: "error" } }; let spy; linter.defineRule("checker", { create(context) { - spy = sinon.spy(() => { - const globalScope = context.getScope(), - childScope = globalScope.childScopes[0]; - - assert.isTrue(context.markVariableAsUsed("a")); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + horse = getVariable(scope, "horse"); - assert.isTrue(getVariable(childScope, "a").eslintUsed); - assert.isUndefined(getVariable(childScope, "b").eslintUsed); + assert.strictEqual(horse.eslintUsed, true); }); - return { "Program:exit": spy }; + return { Program: spy }; } }); - linter.verify(code, { rules: { checker: "error" }, env: { node: true } }); + linter.verify(code, config); assert(spy && spy.calledOnce); }); - it("should mark variables in modules as used", () => { - const code = "var a = 1, b = 2;"; + it("variables should not be exported in the es6 module environment", () => { + const code = "/* exported horse */\nvar horse = 'circus'"; + const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, sourceType: "module" } }; let spy; linter.defineRule("checker", { create(context) { - spy = sinon.spy(() => { - const globalScope = context.getScope(), - childScope = globalScope.childScopes[0]; - - assert.isTrue(context.markVariableAsUsed("a")); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + horse = getVariable(scope, "horse"); - assert.isTrue(getVariable(childScope, "a").eslintUsed); - assert.isUndefined(getVariable(childScope, "b").eslintUsed); + assert.strictEqual(horse, null); // there is no global scope at all }); - return { "Program:exit": spy }; + return { Program: spy }; } }); - linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, sourceType: "module" } }, filename, true); + 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;"; + it("variables should not be exported when in the node environment", () => { + const code = "/* exported horse */\nvar horse = 'circus'"; + const config = { rules: { checker: "error" }, env: { node: true } }; let spy; linter.defineRule("checker", { create(context) { - spy = sinon.spy(() => { - assert.isFalse(context.markVariableAsUsed("c")); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), + horse = getVariable(scope, "horse"); + + assert.strictEqual(horse, null); // there is no global scope at all }); - return { "Program:exit": spy }; + return { Program: spy }; } }); - linter.verify(code, { rules: { checker: "error" } }); + linter.verify(code, config); assert(spy && spy.calledOnce); }); }); - describe("when evaluating code", () => { - const code = TEST_CODE; + describe("when evaluating code containing a line comment", () => { + const code = "//global a \n function f() {}"; - it("events for each node type should fire", () => { + it("should not introduce a global variable", () => { const config = { rules: { checker: "error" } }; - - // spies for various AST node types - const spyLiteral = sinon.spy(), - spyVariableDeclarator = sinon.spy(), - spyVariableDeclaration = sinon.spy(), - spyIdentifier = sinon.spy(), - spyBinaryExpression = sinon.spy(); + let spy; linter.defineRule("checker", { - create: () => ({ - Literal: spyLiteral, - VariableDeclarator: spyVariableDeclarator, - VariableDeclaration: spyVariableDeclaration, - Identifier: spyIdentifier, - BinaryExpression: spyBinaryExpression - }) - }); - - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + create(context) { + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - sinon.assert.calledOnce(spyVariableDeclaration); - sinon.assert.calledOnce(spyVariableDeclarator); - sinon.assert.calledOnce(spyIdentifier); - sinon.assert.calledTwice(spyLiteral); - sinon.assert.calledOnce(spyBinaryExpression); - }); + assert.strictEqual(getVariable(scope, "a"), null); + }); - it("should throw an error if a rule reports a problem without a message", () => { - linter.defineRule("invalid-report", { - create: context => ({ - Program(node) { - context.report({ node }); - } - }) + return { Program: spy }; + } }); - assert.throws( - () => linter.verify("foo", { rules: { "invalid-report": "error" } }), - TypeError, - "Missing `message` property in report() call; add a message that describes the linting problem." - ); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); }); - describe("when config has shared settings for rules", () => { - const code = "test-rule"; + describe("when evaluating code containing normal block comments", () => { + const code = "/**/ /*a*/ /*b:true*/ /*foo c:false*/"; - it("should pass settings to all rules", () => { - linter.defineRule(code, { - create: context => ({ - Literal(node) { - context.report(node, context.settings.info); - } - }) - }); + it("should not introduce a global variable", () => { + const config = { rules: { checker: "error" } }; + let spy; - const config = { rules: {}, settings: { info: "Hello" } }; + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); - config.rules[code] = 1; + assert.strictEqual(getVariable(scope, "a"), null); + assert.strictEqual(getVariable(scope, "b"), null); + assert.strictEqual(getVariable(scope, "foo"), null); + assert.strictEqual(getVariable(scope, "c"), null); + }); - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + return { Program: spy }; + } + }); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Hello"); - assert.strictEqual(suppressedMessages.length, 0); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); + }); - it("should not have any settings if they were not passed in", () => { - linter.defineRule(code, { - create: context => ({ - Literal(node) { - if (Object.getOwnPropertyNames(context.settings).length !== 0) { - context.report(node, "Settings should be empty"); - } - } - }) - }); + describe("when evaluating any code", () => { + const code = "x"; - const config = { rules: {} }; + it("builtin global variables should be available in the global scope", () => { + const config = { rules: { checker: "error" } }; + let spy; - config.rules[code] = 1; + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.notStrictEqual(getVariable(scope, "Object"), null); + assert.notStrictEqual(getVariable(scope, "Array"), null); + assert.notStrictEqual(getVariable(scope, "undefined"), null); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + return { Program: spy }; + } + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); }); - }); - describe("when config has parseOptions", () => { + it("ES6 global variables should not be available by default", () => { + const config = { rules: { checker: "error" } }; + let spy; - it("should pass ecmaFeatures to all rules when provided on config", () => { + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); - const parserOptions = { - ecmaFeatures: { - jsx: true, - globalReturn: true - } - }; + assert.strictEqual(getVariable(scope, "Promise"), null); + assert.strictEqual(getVariable(scope, "Symbol"), null); + assert.strictEqual(getVariable(scope, "WeakMap"), null); + }); - linter.defineRule("test-rule", { - create: sinon.mock().withArgs( - sinon.match({ parserOptions }) - ).returns({}) + return { Program: spy }; + } }); - const config = { rules: { "test-rule": 2 }, parserOptions }; - - linter.verify("0", config, filename); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); - it("should pass parserOptions to all rules when default parserOptions is used", () => { + it("ES6 global variables should be available in the es6 environment", () => { + const config = { rules: { checker: "error" }, env: { es6: true } }; + let spy; - const parserOptions = {}; + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); - linter.defineRule("test-rule", { - create: sinon.mock().withArgs( - sinon.match({ parserOptions }) - ).returns({}) - }); + assert.notStrictEqual(getVariable(scope, "Promise"), null); + assert.notStrictEqual(getVariable(scope, "Symbol"), null); + assert.notStrictEqual(getVariable(scope, "WeakMap"), null); + }); - const config = { rules: { "test-rule": 2 } }; + return { Program: spy }; + } + }); - linter.verify("0", config, filename); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); - }); + it("ES6 global variables can be disabled when the es6 environment is enabled", () => { + const config = { rules: { checker: "error" }, globals: { Promise: "off", Symbol: "off", WeakMap: "off" }, env: { es6: true } }; + let spy; - describe("when a custom parser is defined using defineParser", () => { + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); - 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!"; - } - } - } - }; - } - }; + assert.strictEqual(getVariable(scope, "Promise"), null); + assert.strictEqual(getVariable(scope, "Symbol"), null); + assert.strictEqual(getVariable(scope, "WeakMap"), null); + }); - linter.defineParser("test-parser", parser); - const config = { rules: {}, parser: "test-parser" }; - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + return { Program: spy }; + } + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); - }); - describe("when config has parser", () => { + describe("at any time", () => { + const code = "new-rule"; - it("should pass parser as parserPath to all rules when provided on config", () => { + it("can add a rule dynamically", () => { + linter.defineRule(code, { + create: context => ({ + Literal(node) { + context.report(node, "message"); + } + }) + }); - const alternateParser = "esprima"; + const config = { rules: {} }; - linter.defineParser("esprima", esprima); - linter.defineRule("test-rule", { - create: sinon.mock().withArgs( - sinon.match({ parserPath: alternateParser }) - ).returns({}) - }); + config.rules[code] = 1; - const config = { rules: { "test-rule": 2 }, parser: alternateParser }; + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify("0", config, filename); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, code); + assert.strictEqual(messages[0].nodeType, "Literal"); + + assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("should use parseForESLint() in custom parser when custom parser is specified", () => { - const config = { rules: {}, parser: "enhanced-parser" }; + describe("at any time", () => { + const code = ["new-rule-0", "new-rule-1"]; + + it("can add multiple rules dynamically", () => { + const config = { rules: {} }; + const newRules = {}; + + code.forEach(item => { + config.rules[item] = 1; + newRules[item] = { + create(context) { + return { + Literal(node) { + context.report(node, "message"); + } + }; + } + }; + }); + linter.defineRules(newRules); - linter.defineParser("enhanced-parser", testParsers.enhancedParser); const messages = linter.verify("0", config, filename); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + assert.strictEqual(messages.length, code.length); + code.forEach(item => { + assert.ok(messages.some(message => message.ruleId === item)); + }); + messages.forEach(message => { + assert.strictEqual(message.nodeType, "Literal"); + }); + assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("should expose parser services when using parseForESLint() and services are specified", () => { - linter.defineParser("enhanced-parser", testParsers.enhancedParser); - linter.defineRule("test-service-rule", { + describe("at any time", () => { + const code = "filename-rule"; + + it("has access to the filename", () => { + linter.defineRule(code, { create: context => ({ Literal(node) { - assert.strictEqual(context.parserServices, context.sourceCode.parserServices); - context.report({ - node, - message: context.sourceCode.parserServices.test.getMessage() - }); + assert.strictEqual(context.getFilename(), context.filename); + context.report(node, context.filename); } }) }); - const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; + const config = { rules: {} }; + + config.rules[code] = 1; + const messages = linter.verify("0", config, filename); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Hi!"); + assert.strictEqual(messages[0].message, filename); assert.strictEqual(suppressedMessages.length, 0); }); - it("should use the same parserServices if source code object is reused", () => { - linter.defineParser("enhanced-parser", testParsers.enhancedParser); - linter.defineRule("test-service-rule", { + it("has access to the physicalFilename", () => { + linter.defineRule(code, { create: context => ({ Literal(node) { - assert.strictEqual(context.parserServices, context.sourceCode.parserServices); - context.report({ - node, - message: context.sourceCode.parserServices.test.getMessage() - }); + assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); + context.report(node, context.physicalFilename); } }) }); - const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; + const config = { rules: {} }; + + config.rules[code] = 1; + const messages = linter.verify("0", config, filename); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Hi!"); + assert.strictEqual(messages[0].message, filename); assert.strictEqual(suppressedMessages.length, 0); - - const messages2 = linter.verify(linter.getSourceCode(), config, filename); - const suppressedMessages2 = linter.getSuppressedMessages(); - - assert.strictEqual(messages2.length, 1); - assert.strictEqual(messages2[0].message, "Hi!"); - assert.strictEqual(suppressedMessages2.length, 0); }); - it("should pass parser as parserPath to all rules when default parser is used", () => { - linter.defineRule("test-rule", { - create: sinon.mock().withArgs( - sinon.match({ parserPath: "espree" }) - ).returns({}) - }); - - const config = { rules: { "test-rule": 2 } }; + it("defaults filename to ''", () => { + linter.defineRule(code, { + create: context => ({ + Literal(node) { + assert.strictEqual(context.getFilename(), context.filename); + context.report(node, context.filename); + } + }) + }); - linter.verify("0", config, filename); - }); + const config = { rules: {} }; - }); + config.rules[code] = 1; + const messages = linter.verify("0", config); + const suppressedMessages = linter.getSuppressedMessages(); - describe("when passing in configuration values for rules", () => { - const code = "var answer = 6 * 7"; + assert.strictEqual(messages[0].message, ""); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - it("should be configurable by only setting the integer value", () => { - const rule = "semi", - config = { rules: {} }; + describe("when evaluating code with comments to enable rules", () => { - config.rules[rule] = 1; + it("should report a violation", () => { + const code = "/*eslint no-alert:1*/ alert('test');"; + const config = { rules: {} }; - const messages = linter.verify(code, config, filename, true); + const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, rule); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].message, "Unexpected alert."); + assert.include(messages[0].nodeType, "CallExpression"); assert.strictEqual(suppressedMessages.length, 0); }); - it("should be configurable by only setting the string value", () => { - const rule = "semi", - config = { rules: {} }; - - config.rules[rule] = "warn"; + it("should enable rule configured using a string severity that contains uppercase letters", () => { + const code = "/*eslint no-alert: \"Error\"*/ alert('test');"; + const config = { rules: {} }; - const messages = linter.verify(code, config, filename, true); + const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 1); - assert.strictEqual(messages[0].ruleId, rule); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Unexpected alert."); + assert.include(messages[0].nodeType, "CallExpression"); assert.strictEqual(suppressedMessages.length, 0); }); - it("should be configurable by passing in values as an array", () => { - const rule = "semi", - config = { rules: {} }; - - config.rules[rule] = [1]; + it("rules should not change initial config", () => { + const config = { 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); + let suppressedMessages = linter.getSuppressedMessages(); - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + messages = linter.verify(codeB, config, filename, false); + suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, rule); assert.strictEqual(suppressedMessages.length, 0); }); - it("should be configurable by passing in string value as an array", () => { - const rule = "semi", - config = { rules: {} }; - - config.rules[rule] = ["warn"]; + it("rules should not change initial config", () => { + const config = { 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); + let suppressedMessages = linter.getSuppressedMessages(); - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + messages = linter.verify(codeB, config, filename, false); + suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 1); - assert.strictEqual(messages[0].ruleId, rule); assert.strictEqual(suppressedMessages.length, 0); }); - it("should not be configurable by setting other value", () => { - const rule = "semi", - config = { rules: {} }; - - config.rules[rule] = "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'; }"; - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + let messages = linter.verify(codeA, config, filename, false); + let suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); - }); - it("should process empty config", () => { - const config = {}; - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + messages = linter.verify(codeB, config, filename, false); + suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + assert.strictEqual(messages.length, 1); assert.strictEqual(suppressedMessages.length, 0); }); - }); - - describe("when evaluating code containing /*global */ and /*globals */ blocks", () => { - it("variables should be available in global scope", () => { - const config = { rules: { checker: "error" }, globals: { Array: "off", ConfigGlobal: "writeable" } }; - const code = ` - /*global a b:true c:false d:readable e:writeable Math:off */ - function foo() {} - /*globals f:true*/ - /* global ConfigGlobal : readable */ - `; - let spy; + it("rules should not change initial config", () => { + const config = { rules: { "no-unused-vars": [2, { vars: "all" }] } }; + const codeA = "/*eslint no-unused-vars: [0, {\"vars\": \"local\"}]*/ var a = 44;"; + const codeB = "var b = 55;"; - linter.defineRule("checker", { - create(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"); + let messages = linter.verify(codeA, config, filename, false); + let suppressedMessages = linter.getSuppressedMessages(); - 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); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); - return { Program: spy }; - } - }); + messages = linter.verify(codeB, config, filename, false); + suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 1); + assert.strictEqual(suppressedMessages.length, 0); }); }); - 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", () => { - const config = { rules: { checker: "error" } }; - let spy; - - linter.defineRule("checker", { - create(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); - }); + describe("when evaluating code with invalid comments to enable rules", () => { + it("should report a violation when the config is not a valid rule configuration", () => { + const messages = linter.verify("/*eslint no-alert:true*/ alert('test');", {}); + const suppressedMessages = linter.getSuppressedMessages(); - return { Program: spy }; - } - }); + assert.deepStrictEqual( + messages, + [ + { + 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 + } + ] + ); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 0); }); - }); - describe("when evaluating code containing a /*global */ block with specific variables", () => { - const code = "/* global toString hasOwnProperty valueOf: true */"; + it("should report a violation when the config violates a rule's schema", () => { + const messages = linter.verify("/* eslint no-alert: [error, {nonExistentPropertyName: true}]*/", {}); + const suppressedMessages = linter.getSuppressedMessages(); - it("should not throw an error if comment block has global variables which are Object.prototype contains", () => { - const config = { rules: { checker: "error" } }; + assert.deepStrictEqual( + messages, + [ + { + 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 + } + ] + ); - linter.verify(code, config); + assert.strictEqual(suppressedMessages.length, 0); }); - }); - describe("when evaluating code containing /*eslint-env */ block", () => { - it("variables should be available in global scope", () => { - const code = `/*${ESLINT_ENV} node*/ function f() {} /*${ESLINT_ENV} browser, foo*/`; - const config = { rules: { checker: "error" } }; - let spy; + it("should apply valid configuration even if there is an invalid configuration present", () => { + const code = [ + "/* eslint no-unused-vars: [ */ // <-- this one is invalid JSON", + "/* eslint no-undef: [\"error\"] */ // <-- this one is fine, and thus should apply", + "foo(); // <-- expected no-undef error here" + ].join("\n"); - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), - exports = getVariable(scope, "exports"), - window = getVariable(scope, "window"); + const messages = linter.verify(code); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(exports.writeable, true); - assert.strictEqual(window.writeable, false); - }); + // different engines have different JSON parsing error messages + assert.match(messages[0].message, /Failed to parse JSON from ' "no-unused-vars": \['/u); + assert.strictEqual(messages[0].severity, 2); + assert.isTrue(messages[0].fatal); + assert.isNull(messages[0].ruleId); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 1); + assert.isNull(messages[0].nodeType); - return { Program: spy }; + assert.deepStrictEqual( + messages[1], + { + severity: 2, + ruleId: "no-undef", + message: "'foo' is not defined.", + messageId: "undef", + line: 3, + column: 1, + endLine: 3, + endColumn: 4, + nodeType: "Identifier" } - }); + ); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 0); }); - it("variables should be available in global scope with quoted items", () => { - const code = `/*${ESLINT_ENV} 'node'*/ function f() {} /*${ESLINT_ENV} "browser", "mocha"*/`; - const config = { rules: { checker: "error" } }; - let spy; + }); - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), - exports = getVariable(scope, "exports"), - window = getVariable(scope, "window"), - it = getVariable(scope, "it"); + describe("when evaluating code with comments to disable rules", () => { + const code = "/*eslint no-alert:0*/ alert('test');"; - assert.strictEqual(exports.writeable, true); - assert.strictEqual(window.writeable, false); - assert.strictEqual(it.writeable, false); - }); + it("should not report a violation", () => { + const config = { rules: { "no-alert": 1 } }; - return { Program: spy }; - } - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); }); - describe("when evaluating code containing /*eslint-env */ block with sloppy whitespace", () => { - const code = `/* ${ESLINT_ENV} ,, node , no-browser ,, */`; - - it("variables should be available in global scope", () => { - const config = { rules: { checker: "error" } }; - let spy; + describe("when evaluating code with comments to disable rules", () => { + let code, messages, suppressedMessages; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), - exports = getVariable(scope, "exports"), - window = getVariable(scope, "window"); + it("should report an error when disabling a non-existent rule in inline comment", () => { + code = "/*eslint foo:0*/ ;"; + messages = linter.verify(code, {}, filename); + suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + assert.strictEqual(suppressedMessages.length, 0); - assert.strictEqual(exports.writeable, true); - assert.strictEqual(window, null); - }); + code = "/*eslint-disable foo*/ ;"; + messages = linter.verify(code, {}, filename); + suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + assert.strictEqual(suppressedMessages.length, 0); - return { Program: spy }; - } - }); + code = "/*eslint-disable-line foo*/ ;"; + messages = linter.verify(code, {}, filename); + suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + assert.strictEqual(suppressedMessages.length, 0); - linter.verify(code, config); - assert(spy && spy.calledOnce); + code = "/*eslint-disable-next-line foo*/ ;"; + messages = linter.verify(code, {}, filename); + suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + assert.strictEqual(suppressedMessages.length, 0); }); - }); - describe("when evaluating code containing /*exported */ block", () => { + it("should not report an error, when disabling a non-existent rule in config", () => { + messages = linter.verify("", { rules: { foo: 0 } }, filename); + suppressedMessages = linter.getSuppressedMessages(); - it("we should behave nicely when no matching variable is found", () => { - const code = "/* exported horse */"; - const config = { rules: {} }; + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - linter.verify(code, config, filename, true); + it("should report an error, when config a non-existent rule in config", () => { + messages = linter.verify("", { rules: { foo: 1 } }, filename); + suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + assert.strictEqual(suppressedMessages.length, 0); + + messages = linter.verify("", { rules: { foo: 2 } }, filename); + suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); + assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("variables should be exported", () => { - const code = "/* exported horse */\n\nvar horse = 'circus'"; - const config = { rules: { checker: "error" } }; - let spy; + describe("when evaluating code with comments to enable multiple rules", () => { + const code = "/*eslint no-alert:1 no-console:1*/ alert('test'); console.log('test');"; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); + it("should report a violation", () => { + const config = { rules: {} }; - assert.strictEqual(horse.eslintUsed, true); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - return { Program: spy }; - } - }); + 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"); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("undefined variables should not be exported", () => { - const code = "/* exported horse */\n\nhorse = 'circus'"; - const config = { rules: { checker: "error" } }; - let spy; + 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');"; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); + it("should report a violation", () => { + const config = { rules: { "no-console": 1, "no-alert": 0 } }; - assert.strictEqual(horse, null); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - return { Program: spy }; - } - }); + 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"); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("variables should be exported in strict mode", () => { - const code = "/* exported horse */\n'use strict';\nvar horse = 'circus'"; - const config = { rules: { checker: "error" } }; - let spy; + describe("when evaluating code with comments to disable and enable configurable rule as part of plugin", () => { - linter.defineRule("checker", { + beforeEach(() => { + linter.defineRule("test-plugin/test-rule", { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); - - assert.strictEqual(horse.eslintUsed, true); - }); - - return { Program: spy }; + return { + Literal(node) { + if (node.value === "trigger violation") { + context.report(node, "Reporting violation."); + } + } + }; } }); - - 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'"; - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, sourceType: "module" } }; - let spy; + it("should not report a violation when inline comment enables plugin rule and there's no violation", () => { + const config = { rules: {} }; + const code = "/*eslint test-plugin/test-rule: 2*/ var a = \"no violation\";"; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), - horse = getVariable(scope, "horse"); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(horse, null); // there is no global scope at all - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - return { Program: spy }; - } - }); + 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 = { rules: { "test-plugin/test-rule": 1 } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - it("variables should not be exported when in the node environment", () => { - const code = "/* exported horse */\nvar horse = 'circus'"; - const config = { rules: { checker: "error" }, env: { node: true } }; - let spy; + it("should report a violation when the report is right before the comment", () => { + const code = " /* eslint-disable */ "; linter.defineRule("checker", { - create(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 }; - } + create: context => ({ + Program() { + context.report({ loc: { line: 1, column: 0 }, message: "foo" }); + } + }) }); + const problems = linter.verify(code, { rules: { checker: "error" } }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(problems.length, 1); + assert.strictEqual(problems[0].message, "foo"); + assert.strictEqual(suppressedMessages.length, 0); }); - }); - - describe("when evaluating code containing a line comment", () => { - const code = "//global a \n function f() {}"; - it("should not introduce a global variable", () => { - const config = { rules: { checker: "error" } }; - let spy; + it("should not report a violation when the report is right at the start of the comment", () => { + const code = " /* eslint-disable */ "; linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); - - assert.strictEqual(getVariable(scope, "a"), null); - }); - - return { Program: spy }; - } + create: context => ({ + Program() { + context.report({ loc: { line: 1, column: 1 }, message: "foo" }); + } + }) }); + const problems = linter.verify(code, { rules: { checker: "error" } }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); - }); + assert.strictEqual(problems.length, 0); - describe("when evaluating code containing normal block comments", () => { - const code = "/**/ /*a*/ /*b:true*/ /*foo c:false*/"; + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].message, "foo"); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions[0].justification, ""); + }); - it("should not introduce a global variable", () => { - const config = { rules: { checker: "error" } }; - let spy; + it("rules should not change initial config", () => { + const config = { rules: { "test-plugin/test-rule": 2 } }; + const codeA = "/*eslint test-plugin/test-rule: 0*/ var a = \"trigger violation\";"; + const codeB = "var a = \"trigger violation\";"; - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + let messages = linter.verify(codeA, config, filename, false); + let suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(getVariable(scope, "a"), null); - assert.strictEqual(getVariable(scope, "b"), null); - assert.strictEqual(getVariable(scope, "foo"), null); - assert.strictEqual(getVariable(scope, "c"), null); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); - return { Program: spy }; - } - }); + messages = linter.verify(codeB, config, filename, false); + suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 1); + assert.strictEqual(suppressedMessages.length, 0); }); }); - describe("when evaluating any code", () => { - const code = "x"; - - it("builtin global variables should be available in the global scope", () => { - const config = { rules: { checker: "error" } }; - let spy; + describe("when evaluating code with comments to enable and disable all reporting", () => { + it("should report a violation", () => { - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + const code = [ + "/*eslint-disable */", + "alert('test');", + "/*eslint-enable */", + "alert('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1 } }; - assert.notStrictEqual(getVariable(scope, "Object"), null); - assert.notStrictEqual(getVariable(scope, "Array"), null); - assert.notStrictEqual(getVariable(scope, "undefined"), null); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - return { Program: spy }; - } - }); + 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); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].message, "Unexpected alert."); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions[0].justification, ""); }); - it("ES6 global variables should not be available by default", () => { - const config = { rules: { checker: "error" } }; - let spy; - - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + it("should not report a violation", () => { + const code = [ + "/*eslint-disable */", + "alert('test');", + "alert('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1 } }; - assert.strictEqual(getVariable(scope, "Promise"), null); - assert.strictEqual(getVariable(scope, "Symbol"), null); - assert.strictEqual(getVariable(scope, "WeakMap"), null); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - return { Program: spy }; - } - }); + assert.strictEqual(messages.length, 0); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); }); - it("ES6 global variables should be available in the es6 environment", () => { - const config = { rules: { checker: "error" }, env: { es6: true } }; - let spy; - - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + 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 } }; - assert.notStrictEqual(getVariable(scope, "Promise"), null); - assert.notStrictEqual(getVariable(scope, "Symbol"), null); - assert.notStrictEqual(getVariable(scope, "WeakMap"), null); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - return { Program: spy }; - } - }); + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].column, 21); + assert.strictEqual(messages[1].column, 19); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].column, 1); + assert.strictEqual(suppressedMessages[1].column, 56); }); - it("ES6 global variables can be disabled when the es6 environment is enabled", () => { - const config = { rules: { checker: "error" }, globals: { Promise: "off", Symbol: "off", WeakMap: "off" }, env: { es6: true } }; - let spy; + it("should report a violation", () => { - linter.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + const code = [ + "/*eslint-disable */", + "alert('test');", + "/*eslint-disable */", + "alert('test');", + "/*eslint-enable*/", + "alert('test');", + "/*eslint-enable*/" + ].join("\n"); - assert.strictEqual(getVariable(scope, "Promise"), null); - assert.strictEqual(getVariable(scope, "Symbol"), null); - assert.strictEqual(getVariable(scope, "WeakMap"), null); - }); + const config = { rules: { "no-alert": 1 } }; - return { Program: spy }; - } - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 1); + assert.strictEqual(suppressedMessages.length, 2); }); - }); - - describe("at any time", () => { - const code = "new-rule"; - it("can add a rule dynamically", () => { - linter.defineRule(code, { - create: context => ({ - Literal(node) { - context.report(node, "message"); - } - }) - }); - const config = { rules: {} }; + it("should not report a violation", () => { + const code = [ + "/*eslint-disable */", + "(function(){ var b = 44;})()", + "/*eslint-enable */;any();" + ].join("\n"); - config.rules[code] = 1; + const config = { rules: { "no-unused-vars": 1 } }; - const messages = linter.verify("0", config, filename); + const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, code); - assert.strictEqual(messages[0].nodeType, "Literal"); - - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); }); - }); - describe("at any time", () => { - const code = ["new-rule-0", "new-rule-1"]; + it("should not report a violation", () => { + const code = [ + "(function(){ /*eslint-disable */ var b = 44;})()", + "/*eslint-enable */;any();" + ].join("\n"); - it("can add multiple rules dynamically", () => { - const config = { rules: {} }; - const newRules = {}; + const config = { rules: { "no-unused-vars": 1 } }; - code.forEach(item => { - config.rules[item] = 1; - newRules[item] = { - create(context) { - return { - Literal(node) { - context.report(node, "message"); - } - }; - } - }; - }); - linter.defineRules(newRules); - - const messages = linter.verify("0", config, filename); + const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, code.length); - code.forEach(item => { - assert.ok(messages.some(message => message.ruleId === item)); - }); - messages.forEach(message => { - assert.strictEqual(message.nodeType, "Literal"); - }); - - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); }); }); - describe("at any time", () => { - const code = "filename-rule"; + describe("when evaluating code with comments to ignore reporting on specific rules on a specific line", () => { - it("has access to the filename", () => { - linter.defineRule(code, { - create: context => ({ - Literal(node) { - assert.strictEqual(context.getFilename(), context.filename); - context.report(node, context.filename); + 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 config = { rules: {} }; - - config.rules[code] = 1; + }; - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages[0].message, filename); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); - it("has access to the physicalFilename", () => { - linter.defineRule(code, { - create: context => ({ - Literal(node) { - assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); - context.report(node, context.physicalFilename); - } - }) + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); }); - const config = { rules: {} }; - - config.rules[code] = 1; + 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("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages[0].message, filename); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); - it("defaults filename to ''", () => { - linter.defineRule(code, { - create: context => ({ - Literal(node) { - assert.strictEqual(context.getFilename(), context.filename); - context.report(node, context.filename); - } - }) + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); }); - const config = { rules: {} }; + 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 + } + }; - config.rules[code] = 1; + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - const messages = linter.verify("0", config); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.strictEqual(messages[0].message, ""); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); + 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 + } + }; - describe("when evaluating code with comments to enable rules", () => { + const messages = linter.verify(code, config); + const suppressedMessages = linter.getSuppressedMessages(); - it("should report a violation", () => { - const code = "/*eslint no-alert:1*/ alert('test');"; - const config = { rules: {} }; + 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 + } + ]); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 0); + }); - 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("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 + } + }; - assert.strictEqual(suppressedMessages.length, 0); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("should enable rule configured using a string severity that contains uppercase letters", () => { - const code = "/*eslint no-alert: \"Error\"*/ alert('test');"; - const config = { rules: {} }; + assert.strictEqual(messages.length, 0); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); + }); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-alert"); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, "Unexpected alert."); - assert.include(messages[0].nodeType, "CallExpression"); + 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 + } + }; - assert.strictEqual(suppressedMessages.length, 0); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("rules should not change initial config", () => { - const config = { 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); - let suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 0); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + }); - messages = linter.verify(codeB, config, filename, false); - suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); + 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 + } + }; - assert.strictEqual(suppressedMessages.length, 0); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("rules should not change initial config", () => { - const config = { 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); - let suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 5); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.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 + } + }; - messages = linter.verify(codeB, config, filename, false); - suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 5); + }); - 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'; }"; + 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); + const suppressedMessages = linter.getSuppressedMessages(); - let messages = linter.verify(codeA, config, filename, false); - let suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 0); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + }); - messages = linter.verify(codeB, config, filename, false); - suppressedMessages = linter.getSuppressedMessages(); + it("should report a violation with quoted rule names in eslint-disable-line", () => { + const code = [ + "alert('test'); // eslint-disable-line 'no-alert'", + "console.log('test');", // here + "alert('test'); // eslint-disable-line \"no-alert\"" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; - assert.strictEqual(messages.length, 1); - assert.strictEqual(suppressedMessages.length, 0); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("rules should not change initial config", () => { - const config = { rules: { "no-unused-vars": [2, { vars: "all" }] } }; - const codeA = "/*eslint no-unused-vars: [0, {\"vars\": \"local\"}]*/ var a = 44;"; - const codeB = "var b = 55;"; + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + assert.strictEqual(messages[0].line, 2); - let messages = linter.verify(codeA, config, filename, false); - let suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 1); + assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].line, 3); + }); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.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); + const suppressedMessages = linter.getSuppressedMessages(); - messages = linter.verify(codeB, config, filename, false); - suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); - assert.strictEqual(messages.length, 1); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + }); - describe("when evaluating code with invalid comments to enable rules", () => { - it("should report a violation when the config is not a valid rule configuration", () => { - const messages = linter.verify("/*eslint no-alert:true*/ alert('test');", {}); - const suppressedMessages = linter.getSuppressedMessages(); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - assert.deepStrictEqual( - messages, - [ - { - 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 + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + }); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 0); - it("should report a violation when the config violates a rule's schema", () => { - const messages = linter.verify("/* eslint no-alert: [error, {nonExistentPropertyName: true}]*/", {}); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + }); - assert.deepStrictEqual( - messages, - [ - { - 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 + it("should not ignore violation if code is not on next 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); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); - it("should apply valid configuration even if there is an invalid configuration present", () => { - const code = [ - "/* eslint no-unused-vars: [ */ // <-- this one is invalid JSON", - "/* eslint no-undef: [\"error\"] */ // <-- this one is fine, and thus should apply", - "foo(); // <-- expected no-undef error here" - ].join("\n"); + assert.strictEqual(suppressedMessages.length, 0); + }); - const messages = linter.verify(code); - const suppressedMessages = linter.getSuppressedMessages(); + it("should ignore violation if block comment span multiple lines", () => { + 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); + const suppressedMessages = linter.getSuppressedMessages(); - // different engines have different JSON parsing error messages - assert.match(messages[0].message, /Failed to parse JSON from ' "no-unused-vars": \['/u); - assert.strictEqual(messages[0].severity, 2); - assert.isTrue(messages[0].fatal); - assert.isNull(messages[0].ruleId); - assert.strictEqual(messages[0].line, 1); - assert.strictEqual(messages[0].column, 1); - assert.isNull(messages[0].nodeType); + assert.strictEqual(messages.length, 0); - assert.deepStrictEqual( - messages[1], - { - severity: 2, - ruleId: "no-undef", - message: "'foo' is not defined.", - messageId: "undef", - line: 3, - column: 1, - endLine: 3, - endColumn: 4, - nodeType: "Identifier" - } - ); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + }); - assert.strictEqual(suppressedMessages.length, 0); - }); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - }); + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); - describe("when evaluating code with comments to disable rules", () => { - const code = "/*eslint no-alert:0*/ alert('test');"; + assert.strictEqual(suppressedMessages.length, 0); + }); - it("should not report a violation", () => { - const config = { rules: { "no-alert": 1 } }; + 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); + const suppressedMessages = linter.getSuppressedMessages(); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "quotes"); + }); - describe("when evaluating code with comments to disable rules", () => { - let code, messages, suppressedMessages; + it("should ignore violations of multiple rules when specified in multiple lines", () => { + 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); - it("should report an error when disabling a non-existent rule in inline comment", () => { - code = "/*eslint foo:0*/ ;"; - messages = linter.verify(code, {}, filename); - suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + }); - code = "/*eslint-disable foo*/ ;"; - messages = linter.verify(code, {}, filename); - suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); - assert.strictEqual(suppressedMessages.length, 0); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - code = "/*eslint-disable-line foo*/ ;"; - messages = linter.verify(code, {}, filename); - suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(messages.length, 0); - code = "/*eslint-disable-next-line foo*/ ;"; - messages = linter.verify(code, {}, filename); - suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "quotes"); + }); - it("should not report an error, when disabling a non-existent rule in config", () => { - messages = linter.verify("", { rules: { foo: 0 } }, filename); - suppressedMessages = linter.getSuppressedMessages(); + it("should ignore violations of multiple rules when specified in mixed single line and multi line 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); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 0); + }); - it("should report an error, when config a non-existent rule in config", () => { - messages = linter.verify("", { rules: { foo: 1 } }, filename); - suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); - assert.strictEqual(suppressedMessages.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); + const suppressedMessages = linter.getSuppressedMessages(); - messages = linter.verify("", { rules: { foo: 2 } }, filename); - suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, "Definition for rule 'foo' was not found."); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); - describe("when evaluating code with comments to enable multiple rules", () => { - const code = "/*eslint no-alert:1 no-console:1*/ alert('test'); console.log('test');"; + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "quotes"); + }); - it("should report a violation", () => { - const config = { rules: {} }; + 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); + const suppressedMessages = linter.getSuppressedMessages(); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); - 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"); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + }); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - 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');"; + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); - it("should report a violation", () => { - const config = { rules: { "no-console": 1, "no-alert": 0 } }; + assert.strictEqual(suppressedMessages.length, 3); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "quotes"); + assert.strictEqual(suppressedMessages[2].ruleId, "semi"); + }); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - 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(suppressedMessages.length, 0); - }); - }); - - describe("when evaluating code with comments to disable and enable configurable rule as part of plugin", () => { + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); - beforeEach(() => { - linter.defineRule("test-plugin/test-rule", { - create(context) { - return { - Literal(node) { - if (node.value === "trigger violation") { - context.report(node, "Reporting violation."); - } - } - }; - } + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); }); - }); - - it("should not report a violation when inline comment enables plugin rule and there's no violation", () => { - const config = { rules: {} }; - const code = "/*eslint test-plugin/test-rule: 2*/ var a = \"no violation\";"; - - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.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 = { rules: { "test-plugin/test-rule": 1 } }; - - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should report a violation when the report is right before the comment", () => { - const code = " /* eslint-disable */ "; - linter.defineRule("checker", { - create: context => ({ - Program() { - context.report({ loc: { line: 1, column: 0 }, message: "foo" }); + 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 problems = linter.verify(code, { rules: { checker: "error" } }); - const suppressedMessages = linter.getSuppressedMessages(); + }; - assert.strictEqual(problems.length, 1); - assert.strictEqual(problems[0].message, "foo"); - assert.strictEqual(suppressedMessages.length, 0); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("should not report a violation when the report is right at the start of the comment", () => { - const code = " /* eslint-disable */ "; + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[1].ruleId, "no-console"); - linter.defineRule("checker", { - create: context => ({ - Program() { - context.report({ loc: { line: 1, column: 1 }, message: "foo" }); - } - }) + assert.strictEqual(suppressedMessages.length, 0); }); - const problems = linter.verify(code, { rules: { checker: "error" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(problems.length, 0); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].message, "foo"); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - assert.strictEqual(suppressedMessages[0].suppressions[0].justification, ""); - }); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - it("rules should not change initial config", () => { - const config = { rules: { "test-plugin/test-rule": 2 } }; - const codeA = "/*eslint test-plugin/test-rule: 0*/ var a = \"trigger violation\";"; - const codeB = "var a = \"trigger violation\";"; + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[1].ruleId, "no-console"); - let messages = linter.verify(codeA, config, filename, false); - let suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + it("should ignore violation of specified rule on next line with quoted rule names", () => { + const code = [ + "// eslint-disable-next-line 'no-alert'", + "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); + const suppressedMessages = linter.getSuppressedMessages(); - messages = linter.verify(codeB, config, filename, false); - suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); - assert.strictEqual(messages.length, 1); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); + }); }); }); - describe("when evaluating code with comments to enable and disable all reporting", () => { - it("should report a violation", () => { + describe("when evaluating code with comments to enable and disable reporting of specific rules", () => { + it("should report a violation", () => { const code = [ - "/*eslint-disable */", + "/*eslint-disable no-alert */", "alert('test');", - "/*eslint-enable */", - "alert('test');" + "console.log('test');" // here ].join("\n"); - const config = { rules: { "no-alert": 1 } }; + const config = { rules: { "no-alert": 1, "no-console": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); 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); + assert.strictEqual(messages[0].ruleId, "no-console"); assert.strictEqual(suppressedMessages.length, 1); assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].message, "Unexpected alert."); - assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - assert.strictEqual(suppressedMessages[0].suppressions[0].justification, ""); }); - it("should not report a violation", () => { + it("should report no violation", () => { const code = [ - "/*eslint-disable */", - "alert('test');", - "alert('test');" + "/*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-alert": 1 } }; + const config = { rules: { "no-unused-vars": 2 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); @@ -2352,1872 +2375,1532 @@ describe("Linter", () => { assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-unused-vars"); assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].ruleId, "no-unused-vars"); assert.strictEqual(suppressedMessages[1].line, 3); - assert.strictEqual(suppressedMessages[1].suppressions.length, 1); }); - it("should not report a violation", () => { + it("should report no violation", () => { const code = [ - " alert('test1');/*eslint-disable */\n", - "alert('test');", - " alert('test');\n", - "/*eslint-enable */alert('test2');" - ].join(""); - const config = { rules: { "no-alert": 1 } }; + "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); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].column, 21); - assert.strictEqual(messages[1].column, 19); - - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].column, 1); - assert.strictEqual(suppressedMessages[1].column, 56); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 5); }); - it("should report a violation", () => { - + it("should report no violation", () => { const code = [ - "/*eslint-disable */", - "alert('test');", - "/*eslint-disable */", - "alert('test');", - "/*eslint-enable*/", - "alert('test');", - "/*eslint-enable*/" + "/* eslint-disable quotes */", + "console.log(\"foo\");", + "/* eslint-enable quotes */" ].join("\n"); - - const config = { rules: { "no-alert": 1 } }; + const config = { rules: { quotes: 2 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - - it("should not report a violation", () => { + it("should report a violation", () => { const code = [ - "/*eslint-disable */", - "(function(){ var b = 44;})()", - "/*eslint-enable */;any();" - ].join("\n"); + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", + "/*eslint-enable*/", - const config = { rules: { "no-unused-vars": 1 } }; + "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); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 1); - }); + 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 not report a violation", () => { - const code = [ - "(function(){ /*eslint-disable */ var b = 44;})()", - "/*eslint-enable */;any();" - ].join("\n"); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[1].line, 3); + }); - const config = { rules: { "no-unused-vars": 1 } }; + 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); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].line, 5); }); - }); - describe("when evaluating code with comments to ignore reporting on specific rules on a specific line", () => { - 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 - } - }; + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", + "/*eslint-enable no-alert*/", - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + "alert('test');", // here + "console.log('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); + 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 = [ - "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 - } - }; + assert.strictEqual(suppressedMessages.length, 3); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[2].line, 6); + }); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-alert"); + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert */", - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); - }); + "/*eslint-disable no-console */", + "alert('test');", + "console.log('test');", + "/*eslint-enable */", - 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 - } - }; + "alert('test');", // here + "console.log('test');", // here - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + "/*eslint-enable */", - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[1].ruleId, "no-console"); - assert.strictEqual(suppressedMessages.length, 0); - }); + "alert('test');", // here + "console.log('test');", // here - 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 - } - }; + "/*eslint-enable*/" + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; - const messages = linter.verify(code, config); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - 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 - } - ]); + 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); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 3); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[1].line, 4); + }); - 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 - } - }; + it("should report a violation", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + "/*eslint-enable no-alert */", - assert.strictEqual(messages.length, 0); + "alert('test');", // here + "console.log('test');", - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); - }); + "/*eslint-enable no-console */", - 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 - } - }; + "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); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + 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); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); - }); + assert.strictEqual(suppressedMessages.length, 3); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[2].line, 6); + }); - 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); - const suppressedMessages = linter.getSuppressedMessages(); + it("should report a violation when severity is warn", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 5); - }); + "/*eslint-enable no-alert */", - 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 - } - }; + "alert('test');", // here + "console.log('test');", - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + "/*eslint-enable no-console */", - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 5); - }); + "alert('test');", // here + "console.log('test');", // here + "/*eslint-enable no-console */" + ].join("\n"); + const config = { rules: { "no-alert": "warn", "no-console": "warn" } }; - 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); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + 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); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); + assert.strictEqual(suppressedMessages.length, 3); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[2].line, 6); + }); - it("should report a violation with quoted rule names in eslint-disable-line", () => { - const code = [ - "alert('test'); // eslint-disable-line 'no-alert'", - "console.log('test');", // here - "alert('test'); // eslint-disable-line \"no-alert\"" - ].join("\n"); - const config = { - rules: { - "no-alert": 1, - "no-console": 1 - } - }; + it("should report a violation with quoted rule names in eslint-disable", () => { + const code = [ + "/*eslint-disable 'no-alert' */", + "alert('test');", + "console.log('test');", // here + "/*eslint-enable */", + "/*eslint-disable \"no-console\" */", + "alert('test');", // here + "console.log('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); - assert.strictEqual(messages[0].line, 2); + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-console"); + assert.strictEqual(messages[1].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].line, 1); - assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].line, 3); - }); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); }); - 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); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); + it("should report a violation with quoted rule names in eslint-enable", () => { + 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\"*/", + "console.log('test');" // here + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - 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); - const suppressedMessages = linter.getSuppressedMessages(); + 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, 8); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); + assert.strictEqual(suppressedMessages.length, 3); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[2].line, 6); + }); + }); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); - 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); - const suppressedMessages = linter.getSuppressedMessages(); + 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');"; - assert.strictEqual(messages.length, 0); + it("should report a violation", () => { + const config = { rules: { "no-console": 1, "no-alert": 0 } }; - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("should not ignore violation if code is not on next 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); - const suppressedMessages = linter.getSuppressedMessages(); + 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.length, 1); - assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - assert.strictEqual(suppressedMessages.length, 0); - }); + describe("when evaluating code with comments to enable configurable rule", () => { + const code = "/*eslint quotes:[2, \"double\"]*/ alert('test');"; - it("should ignore violation if block comment span multiple lines", () => { - 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); - const suppressedMessages = linter.getSuppressedMessages(); + it("should report a violation", () => { + const config = { rules: { quotes: [2, "single"] } }; - assert.strictEqual(messages.length, 0); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); + 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"); - 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); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].ruleId, "no-alert"); - assert.strictEqual(messages[1].ruleId, "no-console"); + describe("when evaluating code with comments to enable configurable rule using string severity", () => { + const code = "/*eslint quotes:[\"error\", \"double\"]*/ alert('test');"; - assert.strictEqual(suppressedMessages.length, 0); - }); + it("should report a violation", () => { + const config = { rules: { quotes: [2, "single"] } }; - 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); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); + 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"); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].ruleId, "quotes"); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - it("should ignore violations of multiple rules when specified in multiple lines", () => { - 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); + describe("when evaluating code with incorrectly formatted comments to disable rule", () => { + it("should report a violation", () => { + const code = "/*eslint no-alert:'1'*/ alert('test');"; - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); - }); + const config = { rules: { "no-alert": 1 } }; - 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); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + assert.strictEqual(messages.length, 2); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].ruleId, "quotes"); - }); + /* + * 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); - it("should ignore violations of multiple rules when specified in mixed single line and multi line 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[1].ruleId, "no-alert"); + assert.strictEqual(messages[1].message, "Unexpected alert."); + assert.include(messages[1].nodeType, "CallExpression"); - assert.strictEqual(messages.length, 0); - }); + assert.strictEqual(suppressedMessages.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); - const suppressedMessages = linter.getSuppressedMessages(); + it("should report a violation", () => { + const code = "/*eslint no-alert:abc*/ alert('test');"; - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].ruleId, "no-alert"); - assert.strictEqual(messages[1].ruleId, "no-console"); + const config = { rules: { "no-alert": 1 } }; - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "quotes"); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - 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); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 2); - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].ruleId, "no-alert"); - assert.strictEqual(messages[1].ruleId, "no-console"); + /* + * 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(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); + assert.strictEqual(messages[1].ruleId, "no-alert"); + assert.strictEqual(messages[1].message, "Unexpected alert."); + assert.include(messages[1].nodeType, "CallExpression"); - 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); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); + it("should report a violation", () => { + const code = "/*eslint no-alert:0 2*/ alert('test');"; - assert.strictEqual(suppressedMessages.length, 3); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].ruleId, "quotes"); - assert.strictEqual(suppressedMessages[2].ruleId, "semi"); - }); + const config = { rules: { "no-alert": 1 } }; - 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); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].ruleId, "no-alert"); - assert.strictEqual(messages[1].ruleId, "no-console"); + assert.strictEqual(messages.length, 2); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); + /* + * 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); - 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); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[1].ruleId, "no-console"); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - 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); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages[1].ruleId, "no-alert"); + assert.strictEqual(messages[1].message, "Unexpected alert."); + assert.include(messages[1].nodeType, "CallExpression"); - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].ruleId, "no-alert"); - assert.strictEqual(messages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - assert.strictEqual(suppressedMessages.length, 0); - }); + 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 ignore violation of specified rule on next line with quoted rule names", () => { - const code = [ - "// eslint-disable-next-line 'no-alert'", - "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); - const suppressedMessages = linter.getSuppressedMessages(); + it("should not parse errors, should report a violation", () => { + const messages = linter.verify(code, {}, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); + 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"); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); - }); + assert.strictEqual(suppressedMessages.length, 0); }); }); - describe("when evaluating code with comments to enable and disable reporting of specific rules", () => { - - 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 } }; + 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 suppressedMessages = linter.getSuppressedMessages(); + const [message1, message2] = messages; - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); + 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"); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages.length, 0); }); + }); - 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 } }; + describe("when evaluating a file with a hashbang", () => { - const messages = linter.verify(code, config, filename); + 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); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + 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); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-unused-vars"); - assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[1].ruleId, "no-unused-vars"); - assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages.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 } }; + 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.sourceCode.getAllComments(); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(comments.length, 1); + assert.strictEqual(comments[0].type, "Shebang"); + return {}; + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 5); + linter.defineRule("checker", { create: spy }); + linter.verify(code, config); + assert(spy.calledOnce); }); - it("should report no violation", () => { - const code = [ - "/* eslint-disable quotes */", - "console.log(\"foo\");", - "/* eslint-enable quotes */" - ].join("\n"); - const config = { rules: { quotes: 2 } }; + it("should comment hashbang without breaking offset", () => { + const code = "#!/usr/bin/env node\n'123';"; + const config = { rules: { checker: "error" } }; + let spy; - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(node => { + assert.strictEqual(context.sourceCode.getText(node), "'123';"); + }); + return { ExpressionStatement: spy }; + } + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); - 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 } }; + describe("when evaluating broken code", () => { + const code = BROKEN_TEST_CODE; - const messages = linter.verify(code, config, filename); + it("should report a violation with a useful parse error prefix", () => { + const messages = linter.verify(code); const suppressedMessages = linter.getSuppressedMessages(); - 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); + 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); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages.length, 0); }); - 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); + 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")); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-console"); + assert.strictEqual(messages[0].severity, 2); + assert.isTrue(messages[0].fatal); + assert.match(messages[0].message, /^Parsing error:/u); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].line, 5); + assert.strictEqual(suppressedMessages.length, 0); }); + }); + describe("when using an invalid (undefined) rule", () => { + linter = new Linter(); - 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 code = TEST_CODE; + let results, result, warningResult, arrayOptionResults, objectOptionResults, resultsMultiple; - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + 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 } }); + }); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-alert"); - assert.strictEqual(messages[0].line, 5); + it("should report a problem", () => { + assert.isNotNull(result); + assert.isArray(results); + assert.isObject(result); + assert.property(result, "ruleId"); + assert.strictEqual(result.ruleId, "foobar"); + }); - assert.strictEqual(suppressedMessages.length, 3); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[1].line, 3); - assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[2].line, 6); + it("should report that the rule does not exist", () => { + assert.property(result, "message"); + assert.strictEqual(result.message, "Definition for rule 'foobar' was not found."); }); + it("should report at the correct severity", () => { + assert.property(result, "severity"); + assert.strictEqual(result.severity, 2); + assert.strictEqual(warningResult.severity, 2); // this is 2, since the rulename is very likely to be wrong + }); - it("should report a violation", () => { - const code = [ - "/*eslint-disable no-alert */", + it("should accept any valid rule configuration", () => { + assert.isObject(arrayOptionResults[0]); + assert.isObject(objectOptionResults[0]); + }); - "/*eslint-disable no-console */", - "alert('test');", - "console.log('test');", - "/*eslint-enable */", + it("should report multiple missing rules", () => { + assert.isArray(resultsMultiple); - "alert('test');", // here - "console.log('test');", // here + assert.deepStrictEqual( + resultsMultiple[1], + { + ruleId: "barfoo", + message: "Definition for rule 'barfoo' was not found.", + line: 1, + column: 1, + endLine: 1, + endColumn: 2, + severity: 2, + nodeType: null + } + ); + }); + }); - "/*eslint-enable */", + describe("when using a rule which has been replaced", () => { + const code = TEST_CODE; - "alert('test');", // here - "console.log('test');", // here + it("should report the new rule", () => { + const results = linter.verify(code, { rules: { "no-comma-dangle": 2 } }); + const suppressedMessages = linter.getSuppressedMessages(); - "/*eslint-enable*/" - ].join("\n"); - const config = { rules: { "no-alert": 1, "no-console": 1 } }; + assert.strictEqual(results[0].ruleId, "no-comma-dangle"); + assert.strictEqual(results[0].message, "Rule 'no-comma-dangle' was removed and replaced by: comma-dangle"); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - 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); + describe("when calling getRules", () => { + it("should return all loaded rules", () => { + const rules = linter.getRules(); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].line, 3); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[1].line, 4); + assert.isAbove(rules.size, 230); + assert.isObject(rules.get("no-alert")); }); + }); - it("should report a violation", () => { - const code = [ - "/*eslint-disable no-alert, no-console */", - "alert('test');", - "console.log('test');", + describe("when calling version", () => { + it("should return current version number", () => { + const version = linter.version; - "/*eslint-enable no-alert */", + assert.isString(version); + assert.isTrue(parseInt(version[0], 10) >= 3); + }); + }); - "alert('test');", // here - "console.log('test');", + describe("when evaluating an empty string", () => { + it("runs rules", () => { + linter.defineRule("no-programs", { + create: context => ({ + Program(node) { + context.report({ node, message: "No programs allowed." }); + } + }) + }); - "/*eslint-enable no-console */", + assert.strictEqual( + linter.verify("", { rules: { "no-programs": "error" } }).length, + 1 + ); + }); + }); - "alert('test');", // here - "console.log('test');", // here - "/*eslint-enable no-console */" - ].join("\n"); - const config = { rules: { "no-alert": 1, "no-console": 1 } }; + describe("when evaluating code without comments to environment", () => { + it("should report a violation when using typed array", () => { + const code = "var array = new Uint8Array();"; + + const config = { rules: { "no-undef": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - 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); - - assert.strictEqual(suppressedMessages.length, 3); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[1].line, 3); - assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[2].line, 6); + assert.strictEqual(messages.length, 1); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should report a violation when severity is warn", () => { - const code = [ - "/*eslint-disable no-alert, no-console */", - "alert('test');", - "console.log('test');", + it("should report a violation when using Promise", () => { + const code = "new Promise();"; - "/*eslint-enable no-alert */", + const config = { rules: { "no-undef": 1 } }; - "alert('test');", // here - "console.log('test');", + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - "/*eslint-enable no-console */", + assert.strictEqual(messages.length, 1); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - "alert('test');", // here - "console.log('test');", // here - "/*eslint-enable no-console */" - ].join("\n"); - const config = { rules: { "no-alert": "warn", "no-console": "warn" } }; + describe("when evaluating code with comments to environment", () => { + it("should not support legacy config", () => { + const code = "/*jshint mocha:true */ describe();"; + + const config = { rules: { "no-undef": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - 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); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-undef"); + assert.strictEqual(messages[0].nodeType, "Identifier"); + assert.strictEqual(messages[0].line, 1); - assert.strictEqual(suppressedMessages.length, 3); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[1].line, 3); - assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[2].line, 6); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should report a violation with quoted rule names in eslint-disable", () => { - const code = [ - "/*eslint-disable 'no-alert' */", - "alert('test');", - "console.log('test');", // here - "/*eslint-enable */", - "/*eslint-disable \"no-console\" */", - "alert('test');", // here - "console.log('test');" - ].join("\n"); - const config = { rules: { "no-alert": 1, "no-console": 1 } }; + it("should not report a violation", () => { + const code = "/*eslint-env es6 */ new Promise();"; + + const config = { rules: { "no-undef": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].ruleId, "no-console"); - assert.strictEqual(messages[1].ruleId, "no-alert"); - - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should report a violation with quoted rule names in eslint-enable", () => { - 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\"*/", - "console.log('test');" // here - ].join("\n"); - const config = { rules: { "no-alert": 1, "no-console": 1 } }; + // https://github.com/eslint/eslint/issues/14652 + it("should not report a violation", () => { + const codes = [ + "/*eslint-env es6\n */ new Promise();", + "/*eslint-env browser,\nes6 */ window;Promise;", + "/*eslint-env\nbrowser,es6 */ window;Promise;" + ]; + const config = { rules: { "no-undef": 1 } }; - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + for (const code of codes) { + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - 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, 8); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + } - assert.strictEqual(suppressedMessages.length, 3); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - assert.strictEqual(suppressedMessages[0].line, 2); - assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[1].line, 3); - assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); - assert.strictEqual(suppressedMessages[2].line, 6); }); - }); - 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 not report a violation", () => { + const code = `/*${ESLINT_ENV} mocha,node */ require();describe();`; - it("should report a violation", () => { - const config = { rules: { "no-console": 1, "no-alert": 0 } }; + const config = { rules: { "no-undef": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - 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.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - }); - describe("when evaluating code with comments to enable configurable rule", () => { - const code = "/*eslint quotes:[2, \"double\"]*/ alert('test');"; + it("should not report a violation", () => { + const code = "/*eslint-env mocha */ suite();test();"; - it("should report a violation", () => { - const config = { rules: { quotes: [2, "single"] } }; + const config = { rules: { "no-undef": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - 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"); - + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - }); - describe("when evaluating code with comments to enable configurable rule using string severity", () => { - const code = "/*eslint quotes:[\"error\", \"double\"]*/ alert('test');"; + it("should not report a violation", () => { + const code = `/*${ESLINT_ENV} amd */ define();require();`; - it("should report a violation", () => { - const config = { rules: { quotes: [2, "single"] } }; + const config = { rules: { "no-undef": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - 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"); - + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - }); - describe("when evaluating code with incorrectly formatted comments to disable rule", () => { - it("should report a violation", () => { - const code = "/*eslint no-alert:'1'*/ alert('test');"; + it("should not report a violation", () => { + const code = `/*${ESLINT_ENV} jasmine */ expect();spyOn();`; - const config = { rules: { "no-alert": 1 } }; + const config = { rules: { "no-undef": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - 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"); - + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - it("should report a violation", () => { - const code = "/*eslint no-alert:abc*/ alert('test');"; + it("should not report a violation", () => { + const code = `/*globals require: true */ /*${ESLINT_ENV} node */ require = 1;`; - const config = { rules: { "no-alert": 1 } }; + const config = { rules: { "no-undef": 1 } }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - 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"); - + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - it("should report a violation", () => { - const code = "/*eslint no-alert:0 2*/ alert('test');"; + it("should not report a violation", () => { + const code = `/*${ESLINT_ENV} node */ process.exit();`; - const config = { rules: { "no-alert": 1 } }; + const config = { rules: {} }; const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - 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"); - + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - }); - 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 report a violation", () => { + const code = `/*eslint no-process-exit: 0 */ /*${ESLINT_ENV} node */ process.exit();`; - it("should not parse errors, should report a violation", () => { - const messages = linter.verify(code, {}, filename); - const suppressedMessages = linter.getSuppressedMessages(); + const config = { rules: { "no-undef": 1 } }; - 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"); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); }); - 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"; -`; + 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 + } + }; - it("should validate correctly", () => { - const config = { rules: {} }; - const messages = linter.verify(code, config, filename); + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); const suppressedMessages = linter.getSuppressedMessages(); - 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"); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); assert.strictEqual(suppressedMessages.length, 0); }); - }); - describe("when evaluating a file with a hashbang", () => { + it("should report a violation for global variable declarations", () => { + const code = [ + "/* global foo */" + ].join("\n"); + const config = { + rules: { + test: 2 + } + }; + let ok = false; - 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); - const suppressedMessages = linter.getSuppressedMessages(); + linter.defineRules({ + test: { + create: context => ({ + Program(node) { + const scope = context.sourceCode.getScope(node); + const sourceCode = context.sourceCode; + const comments = sourceCode.getAllComments(); - 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); + assert.strictEqual(context.getSourceCode(), sourceCode); + assert.strictEqual(1, comments.length); - assert.strictEqual(suppressedMessages.length, 0); - }); + const foo = getVariable(scope, "foo"); - 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(); + assert.notOk(foo); - assert.strictEqual(comments.length, 1); - assert.strictEqual(comments[0].type, "Shebang"); - return {}; + ok = true; + } + }) + } }); - linter.defineRule("checker", { create: spy }); - linter.verify(code, config); - assert(spy.calledOnce); + linter.verify(code, config, { allowInlineConfig: false }); + assert(ok); }); - 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", { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node), "'123';"); - }); - return { ExpressionStatement: spy }; + 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 }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + + assert.strictEqual(suppressedMessages.length, 0); }); - }); + 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 + } + }; - describe("when evaluating broken code", () => { - const code = BROKEN_TEST_CODE; + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + const suppressedMessages = linter.getSuppressedMessages(); - it("should report a violation with a useful parse error prefix", () => { - const messages = linter.verify(code); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.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 + }); const suppressedMessages = linter.getSuppressedMessages(); 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); + assert.strictEqual(messages[0].ruleId, "no-alert"); assert.strictEqual(suppressedMessages.length, 0); }); - 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")); + it("should report a violation for env changes", () => { + const code = [ + `/*${ESLINT_ENV} browser*/ window` + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + } + }; + const messages = linter.verify(code, config, { allowInlineConfig: false }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.isTrue(messages[0].fatal); - assert.match(messages[0].message, /^Parsing error:/u); + assert.strictEqual(messages[0].ruleId, "no-undef"); assert.strictEqual(suppressedMessages.length, 0); }); }); - describe("when using an invalid (undefined) rule", () => { - linter = new Linter(); + describe("when evaluating code with 'noInlineComment'", () => { + 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-env es6" + ]) { + // eslint-disable-next-line no-loop-func -- No closures + it(`should warn '/* ${directive} */' if 'noInlineConfig' was given.`, () => { + const messages = linter.verify(`/* ${directive} */`, { noInlineConfig: true }); + const suppressedMessages = linter.getSuppressedMessages(); - const code = TEST_CODE; - let results, result, warningResult, arrayOptionResults, objectOptionResults, resultsMultiple; + 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.`); - 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 } }); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); + } - it("should report a problem", () => { - assert.isNotNull(result); - assert.isArray(results); - assert.isObject(result); - assert.property(result, "ruleId"); - assert.strictEqual(result.ruleId, "foobar"); - }); + 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}`, { noInlineConfig: true }); + const suppressedMessages = linter.getSuppressedMessages(); - it("should report that the rule does not exist", () => { - assert.property(result, "message"); - assert.strictEqual(result.message, "Definition for rule 'foobar' was not found."); - }); + 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 report at the correct severity", () => { - assert.property(result, "severity"); - assert.strictEqual(result.severity, 2); - assert.strictEqual(warningResult.severity, 2); // this is 2, since the rulename is very likely to be wrong - }); + assert.strictEqual(suppressedMessages.length, 0); + }); + } - it("should accept any valid rule configuration", () => { - assert.isObject(arrayOptionResults[0]); - assert.isObject(objectOptionResults[0]); + it("should not warn if 'noInlineConfig' and '--no-inline-config' were given.", () => { + const messages = linter.verify("/* globals foo */", { noInlineConfig: true }, { allowInlineConfig: false }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("should report multiple missing rules", () => { - assert.isArray(resultsMultiple); + describe("when receiving cwd in options during instantiation", () => { + const code = "a;\nb;"; + const config = { rules: { checker: "error" } }; - assert.deepStrictEqual( - resultsMultiple[1], - { - ruleId: "barfoo", - message: "Definition for rule 'barfoo' was not found.", - line: 1, - column: 1, - endLine: 1, - endColumn: 2, - severity: 2, - nodeType: null + it("should get cwd correctly in the context", () => { + const cwd = "cwd"; + const linterWithOption = new Linter({ cwd }); + let spy; + + linterWithOption.defineRule("checker", { + create(context) { + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), context.cwd); + assert.strictEqual(context.cwd, cwd); + }); + return { Program: spy }; } - ); + }); + + linterWithOption.verify(code, config); + assert(spy && spy.calledOnce); }); - }); - describe("when using a rule which has been replaced", () => { - const code = TEST_CODE; + it("should assign process.cwd() to it if cwd is undefined", () => { + let spy; + const linterWithOption = new Linter({ }); - it("should report the new rule", () => { - const results = linter.verify(code, { rules: { "no-comma-dangle": 2 } }); - const suppressedMessages = linter.getSuppressedMessages(); + linterWithOption.defineRule("checker", { + create(context) { - assert.strictEqual(results[0].ruleId, "no-comma-dangle"); - assert.strictEqual(results[0].message, "Rule 'no-comma-dangle' was removed and replaced by: comma-dangle"); + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), context.cwd); + assert.strictEqual(context.cwd, process.cwd()); + }); + return { Program: spy }; + } + }); - assert.strictEqual(suppressedMessages.length, 0); + linterWithOption.verify(code, config); + assert(spy && spy.calledOnce); }); - }); - describe("when calling getRules", () => { - it("should return all loaded rules", () => { - const rules = linter.getRules(); + it("should assign process.cwd() to it if the option is undefined", () => { + let spy; - assert.isAbove(rules.size, 230); - assert.isObject(rules.get("no-alert")); - }); - }); + linter.defineRule("checker", { + create(context) { - describe("when calling version", () => { - it("should return current version number", () => { - const version = linter.version; + spy = sinon.spy(() => { + assert.strictEqual(context.getCwd(), context.cwd); + assert.strictEqual(context.cwd, process.cwd()); + }); + return { Program: spy }; + } + }); - assert.isString(version); - assert.isTrue(parseInt(version[0], 10) >= 3); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); }); - describe("when evaluating an empty string", () => { - it("runs rules", () => { - linter.defineRule("no-programs", { - create: context => ({ - Program(node) { - context.report({ node, message: "No programs allowed." }); - } - }) - }); + describe("reportUnusedDisable option", () => { + it("reports problems for unused eslint-disable comments", () => { + const messages = linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual( - linter.verify("", { rules: { "no-programs": "error" } }).length, - 1 + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + } + ] ); - }); - }); - describe("when evaluating code without comments to environment", () => { - it("should report a violation when using typed array", () => { - const code = "var array = new Uint8Array();"; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); assert.strictEqual(suppressedMessages.length, 0); }); - it("should report a violation when using Promise", () => { - const code = "new Promise();"; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); + it("reports problems for multiple eslint-disable comments, including unused ones", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //eslint-disable-line no-alert -- j2" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 2); }); - }); - - describe("when evaluating code with comments to environment", () => { - it("should not support legacy config", () => { - const code = "/*jshint mocha:true */ describe();"; - - const config = { rules: { "no-undef": 1 } }; - const messages = linter.verify(code, config, filename); + it("reports problems for eslint-disable-line and eslint-disable-next-line comments, including unused ones", () => { + const code = [ + "// eslint-disable-next-line no-alert -- j1 */", + "alert(\"test\"); //eslint-disable-line no-alert -- j2" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-undef"); - assert.strictEqual(messages[0].nodeType, "Identifier"); - assert.strictEqual(messages[0].line, 1); - - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 2); }); - it("should not report a violation", () => { - const code = "/*eslint-env es6 */ new Promise();"; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); + it("reports problems for multiple unused eslint-disable comments with multiple ruleIds", () => { + const code = [ + "/* eslint no-undef: 2, no-void: 2 */", + "/* eslint-disable no-undef -- j1 */", + "void foo; //eslint-disable-line no-undef, no-void -- j2" + ].join("\n"); + const config = { + rules: { + "no-undef": 2, + "no-void": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - // https://github.com/eslint/eslint/issues/14652 - it("should not report a violation", () => { - const codes = [ - "/*eslint-env es6\n */ new Promise();", - "/*eslint-env browser,\nes6 */ window;Promise;", - "/*eslint-env\nbrowser,es6 */ window;Promise;" - ]; - const config = { rules: { "no-undef": 1 } }; - - for (const code of codes) { - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - } - + assert.strictEqual(messages.length, 1); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-void"); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].ruleId, "no-undef"); + assert.strictEqual(suppressedMessages[1].suppressions.length, 2); }); - it("should not report a violation", () => { - const code = `/*${ESLINT_ENV} mocha,node */ require();describe();`; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); + it("reports problems for unused eslint-disable comments (error)", () => { + const messages = linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "error" }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should not report a violation", () => { - const code = "/*eslint-env mocha */ suite();test();"; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); - assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - it("should not report a violation", () => { - const code = `/*${ESLINT_ENV} amd */ define();require();`; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); + it("reports problems for unused eslint-disable comments (warn)", () => { + const messages = linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "warn" }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 1, + nodeType: null + } + ] + ); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should not report a violation", () => { - const code = `/*${ESLINT_ENV} jasmine */ expect();spyOn();`; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); + it("reports problems for unused eslint-enable comments", () => { + const messages = linter.verify("/* eslint-enable */", {}, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 1, + column: 1, + fix: { + range: [0, 19], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should not report a violation", () => { - const code = `/*globals require: true */ /*${ESLINT_ENV} node */ require = 1;`; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); + it("reports problems for unused eslint-enable comments with ruleId", () => { + const messages = linter.verify("/* eslint-enable no-alert */", {}, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'no-alert').", + line: 1, + column: 1, + fix: { + range: [0, 28], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should not report a violation", () => { - const code = `/*${ESLINT_ENV} node */ process.exit();`; - - const config = { rules: {} }; - - const messages = linter.verify(code, config, filename); + it("reports problems for unused eslint-enable comments with mismatch ruleId", () => { + const code = [ + "/* eslint-disable no-alert */", + "alert(\"test\");", + "/* eslint-enable no-console */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should not report a violation", () => { - const code = `/*eslint no-process-exit: 0 */ /*${ESLINT_ENV} node */ process.exit();`; - - const config = { rules: { "no-undef": 1 } }; - - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'no-console').", + line: 3, + column: 1, + fix: { + range: [45, 75], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); }); - }); - describe("when evaluating code with comments to change config when allowInlineConfig is enabled", () => { - it("should report a violation for disabling rules", () => { + it("reports problems for unused eslint-enable comments with used eslint-enable comments", () => { const code = [ - "alert('test'); // eslint-disable-line no-alert" + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\");", + "/* eslint-disable no-alert -- j2 */", + "alert(\"test\");", + "/* eslint-enable no-alert -- j3 */", + "/* eslint-enable -- j4 */" ].join("\n"); const config = { rules: { - "no-alert": 1 + "no-alert": 2 } }; - - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: false - }); + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 6, + column: 1, + fix: { + range: [137, 162], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 2); }); - it("should report a violation for global variable declarations", () => { + it("reports problems for unused eslint-disable comments with used eslint-enable comments", () => { const code = [ - "/* global foo */" + "/* eslint-disable no-alert -- j1 */", + "console.log(\"test\"); //", + "/* eslint-enable no-alert -- j2 */" ].join("\n"); const config = { rules: { - test: 2 + "no-alert": 2 } }; - let ok = false; - - linter.defineRules({ - test: { - create: context => ({ - Program() { - const scope = context.getScope(); - const sourceCode = context.sourceCode; - const comments = sourceCode.getAllComments(); + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(context.getSourceCode(), sourceCode); - assert.strictEqual(1, comments.length); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'no-alert').", + line: 1, + column: 1, + fix: { + range: [0, 35], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); - const foo = getVariable(scope, "foo"); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.notOk(foo); + it("reports problems for unused eslint-disable comments (in config)", () => { + const messages = linter.verify("/* eslint-disable */", { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); - ok = true; - } - }) - } - }); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 1, + nodeType: null + } + ] + ); - linter.verify(code, config, { allowInlineConfig: false }); - assert(ok); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should report a violation for eslint-disable", () => { - const code = [ - "/* eslint-disable */", - "alert('test');" - ].join("\n"); + it("reports problems for partially unused eslint-disable comments (in config)", () => { + const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare"; const config = { + reportUnusedDisableDirectives: true, rules: { - "no-alert": 1 + "no-alert": 1, + "no-redeclare": 1 } }; const messages = linter.verify(code, config, { filename, - allowInlineConfig: false + allowInlineConfig: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-alert"); + 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 + } + ] + ); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); }); - it("should not report a violation for rule changes", () => { - const code = [ - "/*eslint no-alert:2*/", - "alert('test');" - ].join("\n"); + it("reports no problems for no-fallthrough despite comment pattern match", () => { + const code = "switch (foo) { case 0: a(); \n// eslint-disable-next-line no-fallthrough\n case 1: }"; const config = { + reportUnusedDisableDirectives: true, rules: { - "no-alert": 0 + "no-fallthrough": 2 } }; const messages = linter.verify(code, config, { filename, - allowInlineConfig: false + allowInlineConfig: true }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-fallthrough"); }); - it("should report a violation for disable-line", () => { + it("reports problems for multiple eslint-enable comments with same ruleId", () => { const code = [ - "alert('test'); // eslint-disable-line" + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable no-alert -- j3 */" ].join("\n"); const config = { rules: { "no-alert": 2 } }; - - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: false - }); + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-alert"); - - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(messages[0].line, 4); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); }); - it("should report a violation for env changes", () => { + it("reports problems for multiple eslint-enable comments without ruleId (Rule is already enabled)", () => { const code = [ - `/*${ESLINT_ENV} browser*/ window` + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable -- j3 */" ].join("\n"); const config = { rules: { - "no-undef": 2 + "no-alert": 2 } }; - const messages = linter.verify(code, config, { allowInlineConfig: false }); + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "no-undef"); - - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(messages[0].line, 4); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); }); - }); - describe("when evaluating code with 'noInlineComment'", () => { - 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-env es6" - ]) { - // eslint-disable-next-line no-loop-func -- No closures - it(`should warn '/* ${directive} */' if 'noInlineConfig' was given.`, () => { - const messages = linter.verify(`/* ${directive} */`, { noInlineConfig: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - 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.`); - - assert.strictEqual(suppressedMessages.length, 0); - }); - } - - 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}`, { noInlineConfig: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - 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.`); - - assert.strictEqual(suppressedMessages.length, 0); - }); - } - - it("should not warn if 'noInlineConfig' and '--no-inline-config' were given.", () => { - const messages = linter.verify("/* globals foo */", { noInlineConfig: true }, { allowInlineConfig: false }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); - - describe("when receiving cwd in options during instantiation", () => { - const code = "a;\nb;"; - const config = { rules: { checker: "error" } }; - - it("should get cwd correctly in the context", () => { - const cwd = "cwd"; - const linterWithOption = new Linter({ cwd }); - let spy; - - linterWithOption.defineRule("checker", { - create(context) { - spy = sinon.spy(() => { - assert.strictEqual(context.getCwd(), context.cwd); - assert.strictEqual(context.cwd, cwd); - }); - return { Program: spy }; - } - }); - - linterWithOption.verify(code, config); - assert(spy && spy.calledOnce); - }); - - it("should assign process.cwd() to it if cwd is undefined", () => { - let spy; - const linterWithOption = new Linter({ }); - - linterWithOption.defineRule("checker", { - create(context) { - - spy = sinon.spy(() => { - assert.strictEqual(context.getCwd(), context.cwd); - assert.strictEqual(context.cwd, process.cwd()); - }); - return { Program: spy }; - } - }); - - linterWithOption.verify(code, config); - assert(spy && spy.calledOnce); - }); - - it("should assign process.cwd() to it if the option is undefined", () => { - let spy; - - linter.defineRule("checker", { - create(context) { - - spy = sinon.spy(() => { - assert.strictEqual(context.getCwd(), context.cwd); - assert.strictEqual(context.cwd, process.cwd()); - }); - return { Program: spy }; - } - }); - - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); - }); - - describe("reportUnusedDisable option", () => { - it("reports problems for unused eslint-disable comments", () => { - const messages = linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " - }, - severity: 2, - nodeType: null - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("reports problems for multiple eslint-disable comments, including unused ones", () => { + it("reports problems for multiple eslint-enable comments with ruleId (Rule is already enabled by eslint-enable comments without ruleId)", () => { const code = [ "/* eslint-disable no-alert -- j1 */", - "alert(\"test\"); //eslint-disable-line no-alert -- j2" + "alert(\"test\"); //", + "/* eslint-enable -- j3 */", + "/* eslint-enable no-alert -- j2 */" ].join("\n"); const config = { rules: { @@ -4228,531 +3911,134 @@ var a = "test2"; const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 4); assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].suppressions.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); }); - it("reports problems for eslint-disable-line and eslint-disable-next-line comments, including unused ones", () => { + it("reports problems for eslint-enable comments without ruleId (Two rules are already enabled)", () => { const code = [ - "// eslint-disable-next-line no-alert -- j1 */", - "alert(\"test\"); //eslint-disable-line no-alert -- j2" + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable no-console -- j3 */", + "/* eslint-enable -- j4 */" ].join("\n"); const config = { rules: { - "no-alert": 2 + "no-alert": 2, + "no-console": 2 } }; const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].suppressions.length, 2); + assert.strictEqual(messages[0].line, 6); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); }); - it("reports problems for multiple unused eslint-disable comments with multiple ruleIds", () => { + it("reports problems for multiple eslint-enable comments with ruleId (Two rules are already enabled by eslint-enable comments without ruleId)", () => { const code = [ - "/* eslint no-undef: 2, no-void: 2 */", - "/* eslint-disable no-undef -- j1 */", - "void foo; //eslint-disable-line no-undef, no-void -- j2" + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable -- j2 */", + "/* eslint-enable no-console -- j3 */", + "/* eslint-enable no-alert -- j4 */" ].join("\n"); const config = { rules: { - "no-undef": 2, - "no-void": 2 + "no-alert": 2, + "no-console": 2 } }; const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].line, 5); + assert.strictEqual(messages[1].line, 6); assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].ruleId, "no-void"); assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - assert.strictEqual(suppressedMessages[1].ruleId, "no-undef"); - assert.strictEqual(suppressedMessages[1].suppressions.length, 2); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); }); - it("reports problems for unused eslint-disable comments (error)", () => { - const messages = linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "error" }); + it("reports problems for multiple eslint-enable comments", () => { + const code = [ + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable no-console -- j2 */", + "/* eslint-enable -- j3 */", + "/* eslint-enable no-alert -- j4 */", + "/* eslint-enable -- j5 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2, + "no-console": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].line, 6); + assert.strictEqual(messages[1].line, 7); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); + }); + + describe("autofix", () => { + const alwaysReportsRule = { + create(context) { + return { + Program(node) { + context.report({ message: "bad code", loc: node.loc.end }); }, - severity: 2, - nodeType: null - } - ] - ); + "Identifier[name=bad]"(node) { + context.report({ message: "bad id", loc: node.loc }); + } + }; + } + }; - assert.strictEqual(suppressedMessages.length, 0); - }); + const neverReportsRule = { + create() { + return {}; + } + }; - it("reports problems for unused eslint-disable comments (warn)", () => { - const messages = linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "warn" }); - const suppressedMessages = linter.getSuppressedMessages(); + 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" - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " - }, - severity: 1, - nodeType: null - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("reports problems for unused eslint-enable comments", () => { - const messages = linter.verify("/* eslint-enable */", {}, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", - line: 1, - column: 1, - fix: { - range: [0, 19], - text: " " - }, - severity: 2, - nodeType: null - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("reports problems for unused eslint-enable comments with ruleId", () => { - const messages = linter.verify("/* eslint-enable no-alert */", {}, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'no-alert').", - line: 1, - column: 1, - fix: { - range: [0, 28], - text: " " - }, - severity: 2, - nodeType: null - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("reports problems for unused eslint-enable comments with mismatch ruleId", () => { - const code = [ - "/* eslint-disable no-alert */", - "alert(\"test\");", - "/* eslint-enable no-console */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'no-console').", - line: 3, - column: 1, - fix: { - range: [45, 75], - text: " " - }, - severity: 2, - nodeType: null - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 1); - }); - - it("reports problems for unused eslint-enable comments with used eslint-enable comments", () => { - const code = [ - "/* eslint-disable no-alert -- j1 */", - "alert(\"test\");", - "/* eslint-disable no-alert -- j2 */", - "alert(\"test\");", - "/* eslint-enable no-alert -- j3 */", - "/* eslint-enable -- j4 */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", - line: 6, - column: 1, - fix: { - range: [137, 162], - text: " " - }, - severity: 2, - nodeType: null - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - assert.strictEqual(suppressedMessages[1].suppressions.length, 2); - }); - - it("reports problems for unused eslint-disable comments with used eslint-enable comments", () => { - const code = [ - "/* eslint-disable no-alert -- j1 */", - "console.log(\"test\"); //", - "/* eslint-enable no-alert -- j2 */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'no-alert').", - line: 1, - column: 1, - fix: { - range: [0, 35], - text: " " - }, - severity: 2, - nodeType: null - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("reports problems for unused eslint-disable comments (in config)", () => { - const messages = linter.verify("/* eslint-disable */", { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " - }, - severity: 1, - nodeType: null - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("reports problems for partially unused eslint-disable comments (in config)", () => { - const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare"; const config = { reportUnusedDisableDirectives: true, rules: { - "no-alert": 1, - "no-redeclare": 1 + ...Object.fromEntries(usedRules.map(name => [name, "error"])), + ...Object.fromEntries(unusedRules.map(name => [name, "error"])) } }; - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true + beforeEach(() => { + linter.defineRules(Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule]))); + linter.defineRules(Object.fromEntries(unusedRules.map(name => [name, neverReportsRule]))); }); - const suppressedMessages = linter.getSuppressedMessages(); - - 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 - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); - - it("reports no problems for no-fallthrough despite comment pattern match", () => { - const code = "switch (foo) { case 0: a(); \n// eslint-disable-next-line no-fallthrough\n case 1: }"; - const config = { - reportUnusedDisableDirectives: true, - rules: { - "no-fallthrough": 2 - } - }; - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true - }); - const suppressedMessages = linter.getSuppressedMessages(); + const tests = [ - assert.strictEqual(messages.length, 0); - - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-fallthrough"); - }); - - it("reports problems for multiple eslint-enable comments with same ruleId", () => { - const code = [ - "/* eslint-disable no-alert -- j1 */", - "alert(\"test\"); //", - "/* eslint-enable no-alert -- j2 */", - "/* eslint-enable no-alert -- j3 */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].line, 4); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - }); - - it("reports problems for multiple eslint-enable comments without ruleId (Rule is already enabled)", () => { - const code = [ - "/* eslint-disable no-alert -- j1 */", - "alert(\"test\"); //", - "/* eslint-enable no-alert -- j2 */", - "/* eslint-enable -- j3 */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].line, 4); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - }); - - it("reports problems for multiple eslint-enable comments with ruleId (Rule is already enabled by eslint-enable comments without ruleId)", () => { - const code = [ - "/* eslint-disable no-alert -- j1 */", - "alert(\"test\"); //", - "/* eslint-enable -- j3 */", - "/* eslint-enable no-alert -- j2 */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].line, 4); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - }); - - it("reports problems for eslint-enable comments without ruleId (Two rules are already enabled)", () => { - const code = [ - "/* eslint-disable no-alert, no-console -- j1 */", - "alert(\"test\"); //", - "console.log(\"test\"); //", - "/* eslint-enable no-alert -- j2 */", - "/* eslint-enable no-console -- j3 */", - "/* eslint-enable -- j4 */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2, - "no-console": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].line, 6); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - assert.strictEqual(suppressedMessages[1].suppressions.length, 1); - }); - - it("reports problems for multiple eslint-enable comments with ruleId (Two rules are already enabled by eslint-enable comments without ruleId)", () => { - const code = [ - "/* eslint-disable no-alert, no-console -- j1 */", - "alert(\"test\"); //", - "console.log(\"test\"); //", - "/* eslint-enable -- j2 */", - "/* eslint-enable no-console -- j3 */", - "/* eslint-enable no-alert -- j4 */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2, - "no-console": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].line, 5); - assert.strictEqual(messages[1].line, 6); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - assert.strictEqual(suppressedMessages[1].suppressions.length, 1); - }); - - it("reports problems for multiple eslint-enable comments", () => { - const code = [ - "/* eslint-disable no-alert, no-console -- j1 */", - "alert(\"test\"); //", - "console.log(\"test\"); //", - "/* eslint-enable no-console -- j2 */", - "/* eslint-enable -- j3 */", - "/* eslint-enable no-alert -- j4 */", - "/* eslint-enable -- j5 */" - ].join("\n"); - const config = { - rules: { - "no-alert": 2, - "no-console": 2 - } - }; - const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].line, 6); - assert.strictEqual(messages[1].line, 7); - assert.strictEqual(suppressedMessages.length, 2); - assert.strictEqual(suppressedMessages[0].suppressions.length, 1); - assert.strictEqual(suppressedMessages[1].suppressions.length, 1); - }); - - describe("autofix", () => { - const alwaysReportsRule = { - create(context) { - return { - Program(node) { - context.report({ message: "bad code", loc: node.loc.end }); - }, - "Identifier[name=bad]"(node) { - context.report({ message: "bad id", loc: node.loc }); - } - }; - } - }; - - 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 = { - reportUnusedDisableDirectives: true, - rules: { - ...Object.fromEntries(usedRules.map(name => [name, "error"])), - ...Object.fromEntries(unusedRules.map(name => [name, "error"])) - } - }; - - beforeEach(() => { - linter.defineRules(Object.fromEntries(usedRules.map(name => [name, alwaysReportsRule]))); - linter.defineRules(Object.fromEntries(unusedRules.map(name => [name, neverReportsRule]))); - }); - - const tests = [ - - //----------------------------------------------- - // Removing the entire comment - //----------------------------------------------- + //----------------------------------------------- + // Removing the entire comment + //----------------------------------------------- { code: "// eslint-disable-line unused", @@ -5490,5081 +4776,2648 @@ var a = "test2"; bad /* eslint-enable ,used-1 - ,used-2 - */ - ` - }, - { - code: ` - /* eslint-disable used-1, used-2*/ - bad - /* eslint-enable - used-1, - unused-1, - used-2, - unused-2 - - -- comment - */ - `, - output: ` - /* eslint-disable used-1, used-2*/ - bad - /* eslint-enable - used-1, - used-2 - - -- comment - */ - ` - }, - - // duplicates in the list - { - code: "// eslint-disable-line unused, unused, used", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line unused, used, unused", - output: "// eslint-disable-line used" - }, - { - code: "// eslint-disable-line used, unused, unused, used", - output: "// eslint-disable-line used, 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 - ); - }); - - // Test for quoted rule names - for (const testcaseForLiteral of [ - { code: code.replace(/((?:un)?used[\w-]*)/gu, '"$1"'), output: output.replace(/((?:un)?used[\w-]*)/gu, '"$1"') }, - { code: code.replace(/((?:un)?used[\w-]*)/gu, "'$1'"), output: output.replace(/((?:un)?used[\w-]*)/gu, "'$1'") } - ]) { - // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() - it(testcaseForLiteral.code, () => { - assert.strictEqual( - linter.verifyAndFix(testcaseForLiteral.code, config).output, - testcaseForLiteral.output - ); - }); - } - } - }); - }); - - describe("config.noInlineConfig + options.allowInlineConfig", () => { - - it("should report both a rule violation and a warning about inline config", () => { - const code = [ - "/* eslint-disable */ // <-- this should be inline config warning", - "foo(); // <-- this should be no-undef error" - ].join("\n"); - const config = { - rules: { - "no-undef": 2 - }, - noInlineConfig: true - }; - - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 2); - assert.deepStrictEqual( - messages, - [ - { - ruleId: null, - message: "'/*eslint-disable*/' has no effect because you have 'noInlineConfig' setting in your config.", - line: 1, - column: 1, - endLine: 1, - endColumn: 21, - severity: 1, - nodeType: null - }, - { - ruleId: "no-undef", - messageId: "undef", - message: "'foo' is not defined.", - line: 2, - endLine: 2, - column: 1, - endColumn: 4, - severity: 2, - nodeType: "Identifier" - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should report both a rule violation without warning about inline config when noInlineConfig is true and allowInlineConfig is false", () => { - const code = [ - "/* eslint-disable */ // <-- this should be inline config warning", - "foo(); // <-- this should be no-undef error" - ].join("\n"); - const config = { - rules: { - "no-undef": 2 - }, - noInlineConfig: true - }; - - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: false - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.deepStrictEqual( - messages, - [ - { - ruleId: "no-undef", - messageId: "undef", - message: "'foo' is not defined.", - line: 2, - endLine: 2, - column: 1, - endColumn: 4, - severity: 2, - nodeType: "Identifier" - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should report both a rule violation without warning about inline config when both are false", () => { - const code = [ - "/* eslint-disable */ // <-- this should be inline config warning", - "foo(); // <-- this should be no-undef error" - ].join("\n"); - const config = { - rules: { - "no-undef": 2 - }, - noInlineConfig: false - }; - - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: false - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.deepStrictEqual( - messages, - [ - { - ruleId: "no-undef", - messageId: "undef", - message: "'foo' is not defined.", - line: 2, - endLine: 2, - column: 1, - endColumn: 4, - severity: 2, - nodeType: "Identifier" - } - ] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should report one suppresed problem when noInlineConfig is false and allowInlineConfig is true", () => { - const code = [ - "/* eslint-disable */ // <-- this should be inline config warning", - "foo(); // <-- this should be no-undef error" - ].join("\n"); - const config = { - rules: { - "no-undef": 2 - }, - noInlineConfig: false - }; - - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 1); - assert.deepStrictEqual( - suppressedMessages, - [ - { - ruleId: "no-undef", - messageId: "undef", - message: "'foo' is not defined.", - line: 2, - endLine: 2, - column: 1, - endColumn: 4, - severity: 2, - nodeType: "Identifier", - suppressions: [ - { - justification: "", - kind: "directive" - } - ] - } - ] - ); - - }); - }); - - - 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 - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); - }); - }); - - describe("when evaluating code with hashbang", () => { - 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", { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node), "'123';"); - }); - return { ExpressionStatement: spy }; - } - }); - - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); - }); - - describe("verify()", () => { - describe("filenames", () => { - it("should allow filename to be passed on options object", () => { - const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.filename, "foo.js"); - return {}; - }); - - linter.defineRule("checker", { create: filenameChecker }); - linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); - assert(filenameChecker.calledOnce); - }); - - it("should allow filename to be passed as third argument", () => { - const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.filename, "bar.js"); - return {}; - }); - - linter.defineRule("checker", { create: filenameChecker }); - linter.verify("foo;", { rules: { checker: "error" } }, "bar.js"); - assert(filenameChecker.calledOnce); - }); - - it("should default filename to when options object doesn't have filename", () => { - const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.filename, ""); - return {}; - }); - - linter.defineRule("checker", { create: filenameChecker }); - linter.verify("foo;", { rules: { checker: "error" } }, {}); - assert(filenameChecker.calledOnce); - }); - - it("should default filename to when only two arguments are passed", () => { - const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.filename, ""); - return {}; - }); - - linter.defineRule("checker", { create: filenameChecker }); - linter.verify("foo;", { rules: { checker: "error" } }); - assert(filenameChecker.calledOnce); - }); - }); - - describe("physicalFilenames", () => { - it("should be same as `filename` passed on options object, if no processors are used", () => { - const physicalFilenameChecker = sinon.spy(context => { - assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); - assert.strictEqual(context.physicalFilename, "foo.js"); - return {}; - }); - - linter.defineRule("checker", { create: physicalFilenameChecker }); - linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); - assert(physicalFilenameChecker.calledOnce); - }); - - it("should default physicalFilename to when options object doesn't have filename", () => { - const physicalFilenameChecker = sinon.spy(context => { - assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); - assert.strictEqual(context.physicalFilename, ""); - return {}; - }); - - linter.defineRule("checker", { create: physicalFilenameChecker }); - linter.verify("foo;", { rules: { checker: "error" } }, {}); - assert(physicalFilenameChecker.calledOnce); - }); - - it("should default physicalFilename to when only two arguments are passed", () => { - const physicalFilenameChecker = sinon.spy(context => { - assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); - assert.strictEqual(context.physicalFilename, ""); - return {}; - }); - - linter.defineRule("checker", { create: physicalFilenameChecker }); - linter.verify("foo;", { rules: { checker: "error" } }); - assert(physicalFilenameChecker.calledOnce); - }); - }); - - 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); - const suppressedMessages = linter.getSuppressedMessages(); - - 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); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - describe("ecmaVersion", () => { - - it("should not support ES6 when no ecmaVersion provided", () => { - const messages = linter.verify("let x = 0;"); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("supports ECMAScript version 'latest'", () => { - const messages = linter.verify("let x = /[\\q{abc|d}&&[A--B]]/v;", { - parserOptions: { ecmaVersion: "latest" } - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("the 'latest' is equal to espree.latestEcmaVersion", () => { - let ecmaVersion = null; - const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; - - linter.defineRule("ecma-version", { - create: context => ({ - Program() { - ecmaVersion = context.parserOptions.ecmaVersion; - } - }) - }); - linter.verify("", config); - 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", { - create: 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", { - create: 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", { - create: 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", { - create: 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", () => { - let blockScope = null; - - linter.defineRule("block-scope", { - create: context => ({ - BlockStatement() { - blockScope = context.getScope(); - } - }) - }); - linter.defineParser("custom-parser", { - parse: (...args) => espree.parse(...args) - }); - - // Use standard parser - linter.verify("{}", { - rules: { "block-scope": 2 }, - parserOptions: { ecmaVersion: "latest" } - }); - - assert.strictEqual(blockScope.type, "block"); - - linter.verify("{}", { - rules: { "block-scope": 2 }, - parserOptions: {} // ecmaVersion defaults to 5 - }); - assert.strictEqual(blockScope.type, "global"); - - // Use custom parser - linter.verify("{}", { - rules: { "block-scope": 2 }, - parser: "custom-parser", - parserOptions: { ecmaVersion: "latest" } - }); - - assert.strictEqual(blockScope.type, "block"); - - linter.verify("{}", { - rules: { "block-scope": 2 }, - parser: "custom-parser", - parserOptions: {} // ecmaVersion defaults to 5 - }); - assert.strictEqual(blockScope.type, "global"); - }); - - describe("it should properly parse let declaration when", () => { - it("the ECMAScript version number is 6", () => { - const messages = linter.verify("let x = 5;", { - parserOptions: { - ecmaVersion: 6 - } - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("the ECMAScript version number is 2015", () => { - const messages = linter.verify("let x = 5;", { - parserOptions: { - ecmaVersion: 2015 - } - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); - - it("should fail to parse exponentiation operator when the ECMAScript version number is 2015", () => { - const messages = linter.verify("x ** y;", { - parserOptions: { - ecmaVersion: 2015 - } - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(suppressedMessages.length, 0); - }); - - describe("should properly parse exponentiation operator when", () => { - it("the ECMAScript version number is 7", () => { - const messages = linter.verify("x ** y;", { - parserOptions: { - ecmaVersion: 7 - } - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("the ECMAScript version number is 2016", () => { - const messages = linter.verify("x ** y;", { - parserOptions: { - ecmaVersion: 2016 - } - }); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); - }); - - it("should properly parse object spread when ecmaVersion is 2018", () => { - - const messages = linter.verify("var x = { ...y };", { - parserOptions: { - ecmaVersion: 2018 - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should properly parse global return when passed ecmaFeatures", () => { - - const messages = linter.verify("return;", { - parserOptions: { - ecmaFeatures: { - globalReturn: true - } - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should properly parse global return when in Node.js environment", () => { - - const messages = linter.verify("return;", { - env: { - node: true - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should not parse global return when in Node.js environment with globalReturn explicitly off", () => { - - const messages = linter.verify("return;", { - env: { - node: true - }, - parserOptions: { - ecmaFeatures: { - globalReturn: false - } - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should not parse global return when Node.js environment is false", () => { - - const messages = linter.verify("return;", {}, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should properly parse sloppy-mode code when impliedStrict is false", () => { - - const messages = linter.verify("var private;", {}, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should not parse sloppy-mode code when impliedStrict is true", () => { - - const messages = linter.verify("var private;", { - parserOptions: { - ecmaFeatures: { - impliedStrict: true - } - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Parsing error: The keyword 'private' is reserved"); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should properly parse valid code when impliedStrict is true", () => { - - const messages = linter.verify("var foo;", { - parserOptions: { - ecmaFeatures: { - impliedStrict: true - } - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should properly parse JSX when passed ecmaFeatures", () => { - - const messages = linter.verify("var x =
;", { - parserOptions: { - ecmaFeatures: { - jsx: true - } - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should report an error when JSX code is encountered and JSX is not enabled", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, {}, "filename"); - const suppressedMessages = linter.getSuppressedMessages(); - - 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 <"); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should not report an error when JSX code is encountered and JSX is enabled", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, { parserOptions: { ecmaFeatures: { jsx: true } } }, "filename"); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }, "filename"); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.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); - const suppressedMessages = linter.getSuppressedMessages(); - - 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); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - 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); - const suppressedMessages = linter.getSuppressedMessages(); - - 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); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - 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); - const suppressedMessages = linter.getSuppressedMessages(); - - 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); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - 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); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.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); - const suppressedMessages = linter.getSuppressedMessages(); - - 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); - - assert.strictEqual(suppressedMessages.length, 0); - }); - }); - - 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); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.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); - const suppressedMessages = linter.getSuppressedMessages(); - - 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); - - assert.strictEqual(suppressedMessages.length, 0); - }); - }); - - it("should be able to use es6 features if there is a comment which has \"eslint-env es6\"", () => { - const code = [ - "/* eslint-env es6 */", - "var arrow = () => 0;", - "var binary = 0b1010;", - "{ let a = 0; const b = 1; }", - "class A {}", - "function defaultParams(a = 0) {}", - "var {a = 1, b = 2} = {};", - "for (var a of []) {}", - "function* generator() { yield 0; }", - "var computed = {[a]: 0};", - "var duplicate = {dup: 0, dup: 1};", - "var method = {foo() {}};", - "var property = {a, b};", - "var octal = 0o755;", - "var u = /^.$/u.test('𠮷');", - "var y = /hello/y.test('hello');", - "function restParam(a, ...rest) {}", - "class B { superInFunc() { super.foo(); } }", - "var template = `hello, ${a}`;", - "var unicode = '\\u{20BB7}';" - ].join("\n"); - - const messages = linter.verify(code, null, "eslint-env es6"); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should be able to return in global if there is a comment which enables the node environment with a comment", () => { - const messages = linter.verify(`/* ${ESLINT_ENV} node */ return;`, null, "node environment"); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should attach a \"/*global\" comment node to declared variables", () => { - const code = "/* global foo */\n/* global bar, baz */"; - let ok = false; - - linter.defineRules({ - test: { - create: context => ({ - Program() { - const scope = context.getScope(); - const sourceCode = context.sourceCode; - const comments = sourceCode.getAllComments(); - - assert.strictEqual(context.getSourceCode(), sourceCode); - 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; - } - }) - } - }); - - linter.verify(code, { rules: { test: 2 } }); - 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" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - 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" - } - ]); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should not crash when we reuse the SourceCode object", () => { - linter.verify("function render() { return
{hello}
}", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }); - linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }); - }); - - it("should reuse the SourceCode object", () => { - let ast1 = null, - ast2 = null; - - linter.defineRule("save-ast1", { - create: () => ({ - Program(node) { - ast1 = node; - } - }) - }); - linter.defineRule("save-ast2", { - create: () => ({ - Program(node) { - ast2 = node; - } - }) - }); - - linter.verify("function render() { return
{hello}
}", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast1": 2 } }); - linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast2": 2 } }); - - assert(ast1 !== null); - assert(ast2 !== null); - assert(ast1 === ast2); - }); - - it("should allow 'await' as a property name in modules", () => { - const result = linter.verify( - "obj.await", - { parserOptions: { ecmaVersion: 6, sourceType: "module" } } - ); - const suppressedMessages = linter.getSuppressedMessages(); - - assert(result.length === 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - - 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, "foo-bar-baz"); - return {}; - }); - - linter.defineRule("foo-bar-baz", { create: spy }); - linter.verify("x", { rules: { "foo-bar-baz": "error" } }); - assert(spy.calledOnce); - }); - - describe("descriptions in directive comments", () => { - it("should ignore the part preceded by '--' in '/*eslint*/'.", () => { - const aaa = sinon.stub().returns({}); - const bbb = sinon.stub().returns({}); - - linter.defineRule("aaa", { create: aaa }); - linter.defineRule("bbb", { create: bbb }); - const messages = linter.verify(` - /*eslint aaa:error -- bbb:error */ - console.log("hello") - `, {}); - const suppressedMessages = linter.getSuppressedMessages(); - - // Don't include syntax error of the comment. - assert.deepStrictEqual(messages, []); - - // Use only `aaa`. - assert.strictEqual(aaa.callCount, 1); - assert.strictEqual(bbb.callCount, 0); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should ignore the part preceded by '--' in '/*eslint-env*/'.", () => { - const messages = linter.verify(` - /*eslint-env es2015 -- es2017 */ - var Promise = {} - var Atomics = {} - `, { rules: { "no-redeclare": "error" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // Don't include `Atomics` - assert.deepStrictEqual( - messages, - [{ - column: 25, - endColumn: 32, - endLine: 3, - line: 3, - message: "'Promise' is already defined as a built-in global variable.", - messageId: "redeclaredAsBuiltin", - nodeType: "Identifier", - ruleId: "no-redeclare", - severity: 2 - }] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should ignore the part preceded by '--' in '/*global*/'.", () => { - const messages = linter.verify(` - /*global aaa -- bbb */ - var aaa = {} - var bbb = {} - `, { rules: { "no-redeclare": "error" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // Don't include `bbb` - assert.deepStrictEqual( - messages, - [{ - column: 30, - endColumn: 33, - line: 2, - endLine: 2, - message: "'aaa' is already defined by a variable declaration.", - messageId: "redeclaredBySyntax", - nodeType: "Block", - ruleId: "no-redeclare", - severity: 2 - }] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should ignore the part preceded by '--' in '/*globals*/'.", () => { - const messages = linter.verify(` - /*globals aaa -- bbb */ - var aaa = {} - var bbb = {} - `, { rules: { "no-redeclare": "error" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // 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 - }] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should ignore the part preceded by '--' in '/*exported*/'.", () => { - const messages = linter.verify(` - /*exported aaa -- bbb */ - var aaa = {} - var bbb = {} - `, { rules: { "no-unused-vars": "error" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // 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 - }] - ); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - 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" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // 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 - }] - ); - - assert.deepStrictEqual( - suppressedMessages, - [{ - column: 25, - endLine: 4, - endColumn: 28, - line: 4, - message: "'aaa' is already defined.", - messageId: "redeclared", - nodeType: "Identifier", - ruleId: "no-redeclare", - severity: 2, - suppressions: [{ kind: "directive", justification: "no-unused-vars" }] - }] - ); - }); - - 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" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // 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 - }] - ); - - assert.deepStrictEqual( - suppressedMessages, - [{ - 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, - suppressions: [{ kind: "directive", justification: "" }] - }] - ); - }); - - 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" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // 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 - }] - ); - - assert.deepStrictEqual( - suppressedMessages, - [{ - column: 25, - endLine: 3, - endColumn: 28, - line: 3, - message: "'aaa' is already defined.", - messageId: "redeclared", - nodeType: "Identifier", - ruleId: "no-redeclare", - severity: 2, - suppressions: [{ kind: "directive", justification: "no-unused-vars" }] - }] - ); - }); - - 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" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // 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 - }] - ); - - assert.deepStrictEqual( - suppressedMessages, - [{ - column: 25, - endLine: 3, - endColumn: 28, - line: 3, - message: "'aaa' is already defined.", - messageId: "redeclared", - nodeType: "Identifier", - ruleId: "no-redeclare", - severity: 2, - suppressions: [{ kind: "directive", justification: "no-unused-vars" }] - }] - ); - }); - - 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" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // 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 - }] - ); - - assert.deepStrictEqual( - suppressedMessages, - [{ - column: 25, - endLine: 5, - endColumn: 28, - line: 5, - message: "'aaa' is already defined.", - messageId: "redeclared", - nodeType: "Identifier", - ruleId: "no-redeclare", - severity: 2, - suppressions: [{ kind: "directive", justification: "no-unused-vars" }] - }] - ); - }); - - 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" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - // 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 - }] - ); - - assert.deepStrictEqual( - suppressedMessages, - [{ - column: 25, - endLine: 5, - endColumn: 28, - line: 5, - message: "'aaa' is already defined.", - messageId: "redeclared", - nodeType: "Identifier", - ruleId: "no-redeclare", - severity: 2, - suppressions: [{ kind: "directive", justification: "no-unused-vars" }] - }] - ); - }); - - it("should not ignore the part preceded by '--' if the '--' is not surrounded by whitespaces.", () => { - const rule = sinon.stub().returns({}); - - linter.defineRule("a--rule", { create: rule }); - const messages = linter.verify(` - /*eslint a--rule:error */ - console.log("hello") - `, {}); - const suppressedMessages = linter.getSuppressedMessages(); - - // Don't include syntax error of the comment. - assert.deepStrictEqual(messages, []); - - // Use `a--rule`. - assert.strictEqual(rule.callCount, 1); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should ignore the part preceded by '--' even if the '--' is longer than 2.", () => { - const aaa = sinon.stub().returns({}); - const bbb = sinon.stub().returns({}); - - linter.defineRule("aaa", { create: aaa }); - linter.defineRule("bbb", { create: bbb }); - const messages = linter.verify(` - /*eslint aaa:error -------- bbb:error */ - console.log("hello") - `, {}); - const suppressedMessages = linter.getSuppressedMessages(); - - // Don't include syntax error of the comment. - assert.deepStrictEqual(messages, []); - - // Use only `aaa`. - assert.strictEqual(aaa.callCount, 1); - assert.strictEqual(bbb.callCount, 0); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should ignore the part preceded by '--' with line breaks.", () => { - const aaa = sinon.stub().returns({}); - const bbb = sinon.stub().returns({}); - - linter.defineRule("aaa", { create: aaa }); - linter.defineRule("bbb", { create: bbb }); - const messages = linter.verify(` - /*eslint aaa:error - -------- - bbb:error */ - console.log("hello") - `, {}); - const suppressedMessages = linter.getSuppressedMessages(); - - // Don't include syntax error of the comment. - assert.deepStrictEqual(messages, []); - - // Use only `aaa`. - assert.strictEqual(aaa.callCount, 1); - assert.strictEqual(bbb.callCount, 0); - - assert.strictEqual(suppressedMessages.length, 0); - }); - }); - }); - - describe("context.getScope()", () => { - - /** - * Get the scope on the node `astSelector` specified. - * @param {string} code 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(code, astSelector, ecmaVersion = 5) { - let node, scope; - - linter.defineRule("get-scope", { - create: context => ({ - [astSelector](node0) { - node = node0; - scope = context.getScope(); - } - }) - }); - linter.verify( - code, - { - parserOptions: { ecmaVersion }, - rules: { "get-scope": 2 } - } - ); - - 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; - - linter.defineRules({ - test: { - create: context => ({ - Program() { - scope = context.getScope(); - ok = true; - } - }) - } - }); - linter.verify(code, { rules: { test: 2 }, globals: { e: true, f: false } }); - 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>} expectedNamesList An array of expected variable names. The expected variable names is an array of string. - * @returns {void} - */ - function verify(code, type, expectedNamesList) { - linter.defineRules({ - test: { - create(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; - } - } - }); - linter.verify(code, { - rules: { test: 2 }, - parserOptions: { - ecmaVersion: 6, - sourceType: "module" - } - }); - - // 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("suggestions", () => { - it("provides suggestion information for tools to use", () => { - linter.defineRule("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, " ") - }] - }); - } - }) - }); - - const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - 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: " " - } - }]); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("supports messageIds for suggestions", () => { - linter.defineRule("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, " ") - }] - }); - } - }) - }); - - const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); - const suppressedMessages = linter.getSuppressedMessages(); - - 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: " " - } - }]); - - assert.strictEqual(suppressedMessages.length, 0); - }); - - it("should throw an error if suggestion is passed but `meta.hasSuggestions` property is not enabled", () => { - linter.defineRule("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, " ") }] - }); - } - }) - }); - - assert.throws(() => { - linter.verify("0", { rules: { "rule-with-suggestions": "error" } }); - }, "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", () => { - linter.defineRule("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, " ") }] - }); - } - }) - }); - - assert.throws(() => { - linter.verify("0", { rules: { "rule-with-meta-docs-suggestion": "error" } }); - }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint."); - }); - }); - - 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", { - create: () => ({}) - }); - - 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 = []; - - beforeEach(() => { - receivedFilenames = []; - receivedPhysicalFilenames = []; - - // A rule that always reports the AST with a message equal to the source text - linter.defineRule("report-original-text", { - create: context => ({ - Program(ast) { - assert.strictEqual(context.getFilename(), context.filename); - assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); - - receivedFilenames.push(context.filename); - receivedPhysicalFilenames.push(context.physicalFilename); - - context.report({ node: ast, message: context.sourceCode.text }); - } - }) - }); - }); - - describe("preprocessors", () => { - it("should receive text and filename.", () => { - const code = "foo bar baz"; - const preprocess = sinon.spy(text => text.split(" ")); - - linter.verify(code, {}, { filename, preprocess }); - - assert.strictEqual(preprocess.calledOnce, true); - assert.deepStrictEqual(preprocess.args[0], [code, filename]); - }); - - it("should apply a preprocessor to the code, and lint each code sample separately", () => { - const code = "foo bar baz"; - const problems = linter.verify( - code, - { rules: { "report-original-text": "error" } }, - { - - // 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 problems = linter.verify( - code, - { rules: { "report-original-text": "error" } }, - { - 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(" ")); - - linter.verify(code, {}); - const sourceCode = linter.getSourceCode(); - - linter.verify(sourceCode, {}, { 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(" ")); - - linter.verify(code, {}); - const sourceCode = linter.getSourceCode(); - - linter.verify(sourceCode, {}, { filename, preprocess }); - - assert.strictEqual(preprocess.calledOnce, true); - assert.deepStrictEqual(preprocess.args[0], [code, filename]); - }); - - it("should catch preprocess error.", () => { - const code = "foo"; - const preprocess = sinon.spy(() => { - throw Object.assign(new SyntaxError("Invalid syntax"), { - lineNumber: 1, - column: 1 - }); - }); - - const messages = linter.verify(code, {}, { filename, preprocess }); - - assert.strictEqual(preprocess.calledOnce, true); - assert.deepStrictEqual(preprocess.args[0], [code, filename]); - assert.deepStrictEqual(messages, [ - { - ruleId: null, - fatal: true, - severity: 2, - message: "Preprocessing error: Invalid syntax", - line: 1, - column: 1, - nodeType: null - } - ]); - }); - }); - - 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]); - - linter.verify(code, {}, { 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 problems = linter.verify( - code, - { rules: { "report-original-text": "error" } }, - { - 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"; - - linter.defineRule("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()) - }); - } - } - }; - } - }); - - const fixResult = linter.verifyAndFix( - code, - { rules: { "capitalize-identifiers": "error" } }, - { - - /* - * 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("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", () => { - - linter.defineRule("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, " ") - }); - } - }; - } - }); - - const fixResult = linter.verifyAndFix("a", { rules: { "add-spaces": "error" } }); - - 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", () => { - linter.defineRule("test-rule", { - meta: { - docs: {}, - schema: [] - }, - create: context => ({ - Program(node) { - context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); - } - }) - }); - - assert.throws(() => { - linter.verify("0", { rules: { "test-rule": "error" } }); - }, /Fixable rules must set the `meta\.fixable` property to "code" or "whitespace".\nOccurred while linting :1\nRule: "test-rule"$/u); - }); - - it("should throw an error if fix is passed and there is no metadata", () => { - linter.defineRule("test-rule", { - create: context => ({ - Program(node) { - context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); - } - }) - }); - - assert.throws(() => { - linter.verify("0", { rules: { "test-rule": "error" } }); - }, /Fixable rules must set the `meta\.fixable` property/u); - }); + ,used-2 + */ + ` + }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + unused-1, + used-2, + unused-2 - it("should throw an error if fix is passed from a legacy-format rule", () => { - linter.defineRule("test-rule", { - create: context => ({ - Program(node) { - context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); - } - }) - }); + -- comment + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + used-2 - assert.throws(() => { - linter.verify("0", { rules: { "test-rule": "error" } }); - }, /Fixable rules must set the `meta\.fixable` property/u); - }); - }); + -- comment + */ + ` + }, - describe("Edge cases", () => { + // duplicates in the list + { + code: "// eslint-disable-line unused, unused, used", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line unused, used, unused", + output: "// eslint-disable-line used" + }, + { + code: "// eslint-disable-line used, unused, unused, used", + output: "// eslint-disable-line used, used" + } + ]; - it("should properly parse import statements when sourceType is module", () => { - const code = "import foo from 'foo';"; - const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, sourceType: "module" } }); - const suppressedMessages = linter.getSuppressedMessages(); + 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 + ); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + // Test for quoted rule names + for (const testcaseForLiteral of [ + { code: code.replace(/((?:un)?used[\w-]*)/gu, '"$1"'), output: output.replace(/((?:un)?used[\w-]*)/gu, '"$1"') }, + { code: code.replace(/((?:un)?used[\w-]*)/gu, "'$1'"), output: output.replace(/((?:un)?used[\w-]*)/gu, "'$1'") } + ]) { + // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() + it(testcaseForLiteral.code, () => { + assert.strictEqual( + linter.verifyAndFix(testcaseForLiteral.code, config).output, + testcaseForLiteral.output + ); + }); + } + } }); + }); - it("should properly parse import all statements when sourceType is module", () => { - const code = "import * as foo from 'foo';"; - const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, sourceType: "module" } }); - const suppressedMessages = linter.getSuppressedMessages(); + describe("config.noInlineConfig + options.allowInlineConfig", () => { - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + it("should report both a rule violation and a warning about inline config", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + noInlineConfig: true + }; - it("should properly parse default export statements when sourceType is module", () => { - const code = "export default function initialize() {}"; - const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, sourceType: "module" } }); + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - - // https://github.com/eslint/eslint/issues/9687 - it("should report an error when invalid parserOptions found", () => { - let messages = linter.verify("", { parserOptions: { ecmaVersion: 222 } }); - let suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual(messages.length, 1); - assert.ok(messages[0].message.includes("Invalid ecmaVersion")); - assert.strictEqual(suppressedMessages.length, 0); - - messages = linter.verify("", { parserOptions: { sourceType: "foo" } }); - suppressedMessages = linter.getSuppressedMessages(); - - assert.deepStrictEqual(messages.length, 1); - assert.ok(messages[0].message.includes("Invalid sourceType")); - assert.strictEqual(suppressedMessages.length, 0); - - messages = linter.verify("", { parserOptions: { ecmaVersion: 5, sourceType: "module" } }); - suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 2); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "'/*eslint-disable*/' has no effect because you have 'noInlineConfig' setting in your config.", + line: 1, + column: 1, + endLine: 1, + endColumn: 21, + severity: 1, + nodeType: null + }, + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" + } + ] + ); - assert.deepStrictEqual(messages.length, 1); - assert.ok(messages[0].message.includes("sourceType 'module' is not supported when ecmaVersion < 2015")); assert.strictEqual(suppressedMessages.length, 0); }); - 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; }", { parserOptions: { ecmaVersion: 6 } }); - }); - - it("should not crash when parsing destructured assignment", () => { - linter.verify("var { a='a' } = {};", { parserOptions: { ecmaVersion: 6 } }); - }); + it("should report both a rule violation without warning about inline config when noInlineConfig is true and allowInlineConfig is false", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + noInlineConfig: true + }; - it("should report syntax error when a keyword exists in object property shorthand", () => { - const messages = linter.verify("let a = {this}", { parserOptions: { ecmaVersion: 6 } }); + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].fatal, true); + assert.deepStrictEqual( + messages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" + } + ] + ); assert.strictEqual(suppressedMessages.length, 0); }); - it("should not rewrite env setting in core (https://github.com/eslint/eslint/issues/4814)", () => { - - /* - * This test focuses on the instance of https://github.com/eslint/eslint/blob/v2.0.0-alpha-2/conf/environments.js#L26-L28 - * This `verify()` takes the instance and runs https://github.com/eslint/eslint/blob/v2.0.0-alpha-2/lib/eslint.js#L416 - */ - linter.defineRule("test", { - create: () => ({}) - }); - linter.verify("var a = 0;", { - env: { node: true }, - parserOptions: { ecmaVersion: 6, sourceType: "module" }, - rules: { test: 2 } - }); - - // This `verify()` takes the instance and tests that the instance was not modified. - let ok = false; - - linter.defineRule("test", { - create(context) { - assert( - context.parserOptions.ecmaFeatures.globalReturn, - "`ecmaFeatures.globalReturn` of the node environment should not be modified." - ); - ok = true; - return {}; - } - }); - linter.verify("var a = 0;", { - env: { node: true }, - rules: { test: 2 } - }); - - assert(ok); - }); - - it("should throw when rule's create() function does not return an object", () => { - const config = { rules: { checker: "error" } }; - - linter.defineRule("checker", { - create: () => null - }); // returns null + it("should report both a rule violation without warning about inline config when both are false", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + noInlineConfig: false + }; - assert.throws(() => { - linter.verify("abc", config, filename); - }, "The create() function for rule 'checker' did not return an object."); + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.defineRule("checker", { - create() {} - }); // returns undefined + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual( + messages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" + } + ] + ); - assert.throws(() => { - linter.verify("abc", config, filename); - }, "The create() function for rule 'checker' did not return an object."); + assert.strictEqual(suppressedMessages.length, 0); }); - }); - - describe("Custom parser", () => { - const errorPrefix = "Parsing error: "; + it("should report one suppresed problem when noInlineConfig is false and allowInlineConfig is true", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + noInlineConfig: false + }; - it("should have file path passed to it", () => { - const code = "/* this is code */"; - const parseSpy = { parse: sinon.spy() }; + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.defineParser("stub-parser", parseSpy); - linter.verify(code, { parser: "stub-parser" }, filename, true); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); + assert.deepStrictEqual( + suppressedMessages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier", + suppressions: [ + { + justification: "", + kind: "directive" + } + ] + } + ] + ); - sinon.assert.calledWithMatch(parseSpy.parse, "", { filePath: filename }); }); + }); - it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { - const code = "var myDivElement =
;"; - linter.defineParser("esprima", esprima); - const messages = linter.verify(code, { parser: "esprima", parserOptions: { jsx: true } }, "filename"); + 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 + }); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); }); + }); - it("should return an error when the custom parser can't be found", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, { parser: "esprima-xyz" }, "filename"); - const suppressedMessages = linter.getSuppressedMessages(); + describe("when evaluating code with hashbang", () => { + it("should comment hashbang without breaking offset", () => { + const code = "#!/usr/bin/env node\n'123';"; + const config = { rules: { checker: "error" } }; + let spy; - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, "Configured parser 'esprima-xyz' was not found."); + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(node => { + assert.strictEqual(context.sourceCode.getText(node), "'123';"); + }); + return { ExpressionStatement: spy }; + } + }); - assert.strictEqual(suppressedMessages.length, 0); + linter.verify(code, config); + assert(spy && spy.calledOnce); }); + }); - 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'"; + describe("verify()", () => { + describe("filenames", () => { + it("should allow filename to be passed on options object", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.filename, "foo.js"); + return {}; + }); - linter.defineParser("unknown-logical-operator", testParsers.unknownLogicalOperator); + linter.defineRule("checker", { create: filenameChecker }); + linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); + assert(filenameChecker.calledOnce); + }); - // This shouldn't throw - const messages = linter.verify(code, { parser: "unknown-logical-operator" }, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + it("should allow filename to be passed as third argument", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.filename, "bar.js"); + return {}; + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + linter.defineRule("checker", { create: filenameChecker }); + linter.verify("foo;", { rules: { checker: "error" } }, "bar.js"); + assert(filenameChecker.calledOnce); + }); - 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"; + it("should default filename to when options object doesn't have filename", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.filename, ""); + return {}; + }); - linter.defineParser("unknown-logical-operator-nested", testParsers.unknownLogicalOperatorNested); + linter.defineRule("checker", { create: filenameChecker }); + linter.verify("foo;", { rules: { checker: "error" } }, {}); + assert(filenameChecker.calledOnce); + }); - // This shouldn't throw - const messages = linter.verify(code, { parser: "unknown-logical-operator-nested" }, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + it("should default filename to when only two arguments are passed", () => { + const filenameChecker = sinon.spy(context => { + assert.strictEqual(context.filename, ""); + return {}; + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + linter.defineRule("checker", { create: filenameChecker }); + linter.verify("foo;", { rules: { checker: "error" } }); + assert(filenameChecker.calledOnce); + }); }); - it("should not throw or return errors when the custom parser returns unknown AST nodes", () => { - const code = "foo && bar %% baz"; - - const nodes = []; + describe("physicalFilenames", () => { + it("should be same as `filename` passed on options object, if no processors are used", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); + assert.strictEqual(context.physicalFilename, "foo.js"); + return {}; + }); - linter.defineRule("collect-node-types", { - create: () => ({ - "*"(node) { - nodes.push(node.type); - } - }) + linter.defineRule("checker", { create: physicalFilenameChecker }); + linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); + assert(physicalFilenameChecker.calledOnce); }); - linter.defineParser("non-js-parser", testParsers.nonJSParser); + it("should default physicalFilename to when options object doesn't have filename", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); + assert.strictEqual(context.physicalFilename, ""); + return {}; + }); - const messages = linter.verify(code, { - parser: "non-js-parser", - rules: { - "collect-node-types": "error" - } - }, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + linter.defineRule("checker", { create: physicalFilenameChecker }); + linter.verify("foo;", { rules: { checker: "error" } }, {}); + assert(physicalFilenameChecker.calledOnce); + }); - assert.strictEqual(messages.length, 0); - assert.isTrue(nodes.length > 0); + it("should default physicalFilename to when only two arguments are passed", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); + assert.strictEqual(context.physicalFilename, ""); + return {}; + }); - assert.strictEqual(suppressedMessages.length, 0); + linter.defineRule("checker", { create: physicalFilenameChecker }); + linter.verify("foo;", { rules: { checker: "error" } }); + assert(physicalFilenameChecker.calledOnce); + }); }); - it("should strip leading line: prefix from parser error", () => { - linter.defineParser("line-error", testParsers.lineError); - const messages = linter.verify(";", { parser: "line-error" }, "filename"); + 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); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError); + 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); assert.strictEqual(suppressedMessages.length, 0); }); - it("should not modify a parser error message without a leading line: prefix", () => { - linter.defineParser("no-line-error", testParsers.noLineError); - const messages = linter.verify(";", { parser: "no-line-error" }, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError); + describe("ecmaVersion", () => { - assert.strictEqual(suppressedMessages.length, 0); - }); + it("should not support ES6 when no ecmaVersion provided", () => { + const messages = linter.verify("let x = 0;"); + const suppressedMessages = linter.getSuppressedMessages(); - describe("if a parser provides 'visitorKeys'", () => { - let types = []; - let sourceCode; - let scopeManager; - let firstChildNodes = []; + assert.strictEqual(messages.length, 1); + assert.strictEqual(suppressedMessages.length, 0); + }); - beforeEach(() => { - types = []; - firstChildNodes = []; - linter.defineRule("collect-node-types", { - create: () => ({ - "*"(node) { - types.push(node.type); - } - }) + it("supports ECMAScript version 'latest'", () => { + const messages = linter.verify("let x = /[\\q{abc|d}&&[A--B]]/v;", { + parserOptions: { ecmaVersion: "latest" } }); - linter.defineRule("save-scope-manager", { - create(context) { - scopeManager = context.sourceCode.scopeManager; + const suppressedMessages = linter.getSuppressedMessages(); - return {}; - } - }); - linter.defineRule("esquery-option", { - create: () => ({ - ":first-child"(node) { - firstChildNodes.push(node); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("the 'latest' is equal to espree.latestEcmaVersion", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; + + linter.defineRule("ecma-version", { + create: context => ({ + Program() { + ecmaVersion = context.parserOptions.ecmaVersion; } }) }); - linter.defineParser("enhanced-parser2", testParsers.enhancedParser2); - linter.verify("@foo class A {}", { - parser: "enhanced-parser2", - rules: { - "collect-node-types": "error", - "save-scope-manager": "error", - "esquery-option": "error" - } - }); - - sourceCode = linter.getSourceCode(); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion, "ecmaVersion should be 13"); }); - it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { - assert.deepStrictEqual( - types, - ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] - ); - }); + it("the 'latest' is not normalized for custom parsers", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parser: "custom-parser", parserOptions: { ecmaVersion: "latest" } }; - 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"] - ); + linter.defineParser("custom-parser", testParsers.enhancedParser); + linter.defineRule("ecma-version", { + create: context => ({ + Program() { + ecmaVersion = context.parserOptions.ecmaVersion; + } + }) + }); + linter.verify("", config); + assert.strictEqual(ecmaVersion, "latest", "ecmaVersion should be latest"); }); - it("should use the same visitorKeys if the source code object is reused", () => { - const types2 = []; + it("the 'latest' is equal to espree.latestEcmaVersion on languageOptions", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; - linter.defineRule("collect-node-types", { - create: () => ({ - "*"(node) { - types2.push(node.type); + linter.defineRule("ecma-version", { + create: context => ({ + Program() { + ecmaVersion = context.languageOptions.ecmaVersion; } }) }); - linter.verify(sourceCode, { - rules: { - "collect-node-types": "error" - } - }); - - 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]] - ); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion + 2009, "ecmaVersion should be 2022"); }); - }); - describe("if a parser provides 'scope'", () => { - let scope = null; - let sourceCode = null; + 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" } }; - beforeEach(() => { - linter.defineParser("enhanced-parser3", testParsers.enhancedParser3); - linter.defineRule("save-scope1", { + linter.defineParser("custom-parser", testParsers.stubParser); + linter.defineRule("ecma-version", { create: context => ({ Program() { - scope = context.getScope(); + ecmaVersion = context.languageOptions.ecmaVersion; } }) }); - linter.verify("@foo class A {}", { parser: "enhanced-parser3", rules: { "save-scope1": 2 } }); - - 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" - ); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion + 2009, "ecmaVersion should be 2022"); }); - it("should use the same scope if the source code object is reused", () => { - let scope2 = null; + 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.defineRule("save-scope2", { + linter.defineParser("custom-parser", testParsers.enhancedParser); + linter.defineRule("ecma-version", { create: context => ({ Program() { - scope2 = context.getScope(); + ecmaVersion = context.languageOptions.ecmaVersion; } }) }); - linter.verify(sourceCode, { rules: { "save-scope2": 2 } }, "test.js"); - - assert(scope2 !== null); - assert(scope2 === scope); + linter.verify("", config); + assert.strictEqual(ecmaVersion, 5, "ecmaVersion should be 5"); }); - }); - - it("should not pass any default parserOptions to the parser", () => { - linter.defineParser("throws-with-options", testParsers.throwsWithOptions); - const messages = linter.verify(";", { parser: "throws-with-options" }, "filename"); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); - - describe("merging 'parserOptions'", () => { - it("should deeply merge 'parserOptions' from an environment with 'parserOptions' from the provided config", () => { - const code = "return
"; - const config = { - env: { - node: true // ecmaFeatures: { globalReturn: true } - }, - parserOptions: { - ecmaFeatures: { - jsx: true - } - } - }; - - const messages = linter.verify(code, config); - const suppressedMessages = linter.getSuppressedMessages(); - - // no parsing errors - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); -}); -describe("Linter with FlatConfigArray", () => { + it("should pass normalized ecmaVersion to eslint-scope", () => { + let blockScope = null; - let linter; - const filename = "filename.js"; + linter.defineRule("block-scope", { + create: context => ({ + BlockStatement(node) { + blockScope = context.sourceCode.getScope(node); + } + }) + }); + linter.defineParser("custom-parser", { + parse: (...args) => espree.parse(...args) + }); - /** - * 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: "" }); - } + // Use standard parser + linter.verify("{}", { + rules: { "block-scope": 2 }, + parserOptions: { ecmaVersion: "latest" } + }); - beforeEach(() => { - linter = new Linter({ configType: "flat" }); - }); + assert.strictEqual(blockScope.type, "block"); - describe("Static Members", () => { - describe("version", () => { - it("should return same version as instance property", () => { - assert.strictEqual(Linter.version, linter.version); - }); - }); - }); + linter.verify("{}", { + rules: { "block-scope": 2 }, + parserOptions: {} // ecmaVersion defaults to 5 + }); + assert.strictEqual(blockScope.type, "global"); - describe("Config Options", () => { + // Use custom parser + linter.verify("{}", { + rules: { "block-scope": 2 }, + parser: "custom-parser", + parserOptions: { ecmaVersion: "latest" } + }); - describe("languageOptions", () => { + assert.strictEqual(blockScope.type, "block"); - describe("ecmaVersion", () => { + linter.verify("{}", { + rules: { "block-scope": 2 }, + parser: "custom-parser", + parserOptions: {} // ecmaVersion defaults to 5 + }); + assert.strictEqual(blockScope.type, "global"); + }); - 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" + describe("it should properly parse let declaration when", () => { + it("the ECMAScript version number is 6", () => { + const messages = linter.verify("let x = 5;", { + parserOptions: { + ecmaVersion: 6 } }); const suppressedMessages = linter.getSuppressedMessages(); - 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."); - + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - 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" + it("the ECMAScript version number is 2015", () => { + const messages = linter.verify("let x = 5;", { + parserOptions: { + ecmaVersion: 2015 } }); const suppressedMessages = linter.getSuppressedMessages(); - 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."); - + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("should add globals for ES6 when ecmaVersion is 6", () => { - const messages = linter.verify("new Map()", { - languageOptions: { - ecmaVersion: 6 - }, - rules: { - "no-undef": "error" + it("should fail to parse exponentiation operator when the ECMAScript version number is 2015", () => { + const messages = linter.verify("x ** y;", { + parserOptions: { + ecmaVersion: 2015 + } + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(suppressedMessages.length, 0); + }); + + describe("should properly parse exponentiation operator when", () => { + it("the ECMAScript version number is 7", () => { + const messages = linter.verify("x ** y;", { + parserOptions: { + ecmaVersion: 7 } }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0, "There should be no linting errors."); + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - it("should allow destructuring when ecmaVersion is 6", () => { - const messages = linter.verify("let {a} = b", { - languageOptions: { - ecmaVersion: 6 + it("the ECMAScript version number is 2016", () => { + const messages = linter.verify("x ** y;", { + parserOptions: { + ecmaVersion: 2016 } }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0, "There should be no linting errors."); + assert.strictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); + }); + }); - it("ecmaVersion should be normalized to year name for ES 6", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create: context => ({ - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, 2015); - } - }) - } - } - } - }, - languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; + it("should properly parse object spread when ecmaVersion is 2018", () => { - linter.verify("foo", config, filename); - }); + const messages = linter.verify("var x = { ...y };", { + parserOptions: { + ecmaVersion: 2018 + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("ecmaVersion should be normalized to latest year by default", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create: context => ({ - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); - } - }) - } - } - } - }, - rules: { "test/checker": "error" } - }; + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - linter.verify("foo", config, filename); - }); + it("should properly parse global return when passed ecmaFeatures", () => { - it("ecmaVersion should not be normalized to year name for ES 5", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create: context => ({ - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, 5); - } - }) - } - } - } - }, - languageOptions: { - ecmaVersion: 5 - }, - rules: { "test/checker": "error" } - }; + const messages = linter.verify("return;", { + parserOptions: { + ecmaFeatures: { + globalReturn: true + } + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify("foo", config, filename); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - it("ecmaVersion should be normalized to year name for 'latest'", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create: context => ({ - Program() { - assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); - } - }) - } - } - } - }, - languageOptions: { - ecmaVersion: "latest" - }, - rules: { "test/checker": "error" } - }; + it("should properly parse global return when in Node.js environment", () => { - linter.verify("foo", config, filename); - }); + const messages = linter.verify("return;", { + env: { + node: true + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - }); + it("should not parse global return when in Node.js environment with globalReturn explicitly off", () => { - describe("sourceType", () => { + const messages = linter.verify("return;", { + env: { + node: true + }, + parserOptions: { + ecmaFeatures: { + globalReturn: false + } + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("should be module by default", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create: context => ({ - Program() { - assert.strictEqual(context.languageOptions.sourceType, "module"); - } - }) - } - } - } - }, - rules: { "test/checker": "error" } - }; + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should not parse global return when Node.js environment is false", () => { + + const messages = linter.verify("return;", {}, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should properly parse sloppy-mode code when impliedStrict is false", () => { + + const messages = linter.verify("var private;", {}, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should not parse sloppy-mode code when impliedStrict is true", () => { + + const messages = linter.verify("var private;", { + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify("import foo from 'bar'", config, filename); - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Parsing error: The keyword 'private' is reserved"); - it("should default to commonjs when passed a .cjs filename", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create: context => ({ - Program() { - assert.strictEqual(context.languageOptions.sourceType, "commonjs"); - } - }) - } - } - } - }, - rules: { "test/checker": "error" } - }; + assert.strictEqual(suppressedMessages.length, 0); + }); - linter.verify("import foo from 'bar'", config, `${filename}.cjs`); - }); + it("should properly parse valid code when impliedStrict is true", () => { + const messages = linter.verify("var foo;", { + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - 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, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - 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 properly parse JSX when passed ecmaFeatures", () => { - it("should not error when import is used in a module", () => { - const messages = linter.verify("import foo from 'bar';", { - languageOptions: { - ecmaVersion: 6, - sourceType: "module" - } - }); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify("var x =
;", { + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0, "There should no linting errors."); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - it("should error when return is used at the top-level outside of commonjs", () => { - const messages = linter.verify("return", { - languageOptions: { - ecmaVersion: 6, - sourceType: "script" - } - }); - const suppressedMessages = linter.getSuppressedMessages(); + it("should report an error when JSX code is encountered and JSX is not enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, {}, "filename"); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1, "There should be one parsing error."); - assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); + 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 <"); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - it("should not error when top-level return is used in commonjs", () => { - const messages = linter.verify("return", { - languageOptions: { - ecmaVersion: 6, - sourceType: "commonjs" - } - }); - const suppressedMessages = linter.getSuppressedMessages(); + it("should not report an error when JSX code is encountered and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, { parserOptions: { ecmaFeatures: { jsx: true } } }, "filename"); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0, "There should no linting errors."); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - it("should error when accessing a Node.js global outside of commonjs", () => { - const messages = linter.verify("require()", { - languageOptions: { - ecmaVersion: 6 - }, - rules: { - "no-undef": "error" - } - }); - const suppressedMessages = linter.getSuppressedMessages(); + it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }, "filename"); + const suppressedMessages = linter.getSuppressedMessages(); - 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."); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.strictEqual(suppressedMessages.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); + const suppressedMessages = linter.getSuppressedMessages(); - 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" - } - }); - const suppressedMessages = linter.getSuppressedMessages(); + 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); - assert.strictEqual(messages.length, 0, "There should be no linting errors."); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - it("should allow 'await' as a property name in modules", () => { - const result = linter.verify( - "obj.await", - { - languageOptions: { - ecmaVersion: 6, - sourceType: "module" - } - } - ); - const suppressedMessages = linter.getSuppressedMessages(); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - assert(result.length === 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + 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); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - describe("parser", () => { + 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); + const suppressedMessages = linter.getSuppressedMessages(); - 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!"; - } - } - } - }; - } - }; + 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); - const config = { - languageOptions: { - parser - } - }; + assert.strictEqual(suppressedMessages.length, 0); + }); + 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); + const suppressedMessages = linter.getSuppressedMessages(); - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.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)]; - it("should pass parser as context.languageOptions.parser to all rules when provided on config", () => { + ecmaVersions.forEach(ecmaVersion => { + const code = "var enum;"; + const messages = linter.verify(code, { parserOptions: { ecmaVersion } }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - const config = { - plugins: { - test: { - rules: { - "test-rule": { - create: sinon.mock().withArgs( - sinon.match({ languageOptions: { parser: esprima } }) - ).returns({}) - } - } - } - }, - languageOptions: { - parser: esprima - }, - rules: { - "test/test-rule": 2 - } - }; + 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); - linter.verify("0", config, filename); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - it("should use parseForESLint() in custom parser when custom parser is specified", () => { - const config = { - languageOptions: { - parser: testParsers.enhancedParser - } - }; + it("should allow the use of reserved words as property names in ES > 3", () => { + const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)]; - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + ecmaVersions.forEach(ecmaVersion => { + const code = "obj.enum; obj.function; var obj = { enum: 1, function: 2 };"; + const messages = linter.verify(code, { parserOptions: { ecmaVersion } }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - it("should expose parser services when using parseForESLint() and services are specified", () => { + it("should not allow `allowReserved: true` in ES > 3", () => { + const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)]; - const config = { - plugins: { - test: { - rules: { - "test-service-rule": { - create: context => ({ - Literal(node) { - assert.strictEqual(context.parserServices, context.sourceCode.parserServices); - context.report({ - node, - message: context.sourceCode.parserServices.test.getMessage() - }); - } - }) - } - } - } - }, - languageOptions: { - parser: testParsers.enhancedParser - }, - rules: { - "test/test-service-rule": 2 - } - }; + ecmaVersions.forEach(ecmaVersion => { + const code = ""; + const messages = linter.verify(code, { parserOptions: { ecmaVersion, allowReserved: true } }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + 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); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Hi!"); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - assert.strictEqual(suppressedMessages.length, 0); - }); + it("should be able to use es6 features if there is a comment which has \"eslint-env es6\"", () => { + const code = [ + "/* eslint-env es6 */", + "var arrow = () => 0;", + "var binary = 0b1010;", + "{ let a = 0; const b = 1; }", + "class A {}", + "function defaultParams(a = 0) {}", + "var {a = 1, b = 2} = {};", + "for (var a of []) {}", + "function* generator() { yield 0; }", + "var computed = {[a]: 0};", + "var duplicate = {dup: 0, dup: 1};", + "var method = {foo() {}};", + "var property = {a, b};", + "var octal = 0o755;", + "var u = /^.$/u.test('𠮷');", + "var y = /hello/y.test('hello');", + "function restParam(a, ...rest) {}", + "class B { superInFunc() { super.foo(); } }", + "var template = `hello, ${a}`;", + "var unicode = '\\u{20BB7}';" + ].join("\n"); - it("should use the same parserServices if source code object is reused", () => { + const messages = linter.verify(code, null, "eslint-env es6"); + const suppressedMessages = linter.getSuppressedMessages(); - const config = { - plugins: { - test: { - rules: { - "test-service-rule": { - create: context => ({ - Literal(node) { - assert.strictEqual(context.parserServices, context.sourceCode.parserServices); - context.report({ - node, - message: context.sourceCode.parserServices.test.getMessage() - }); - } - }) - } - } - } - }, - languageOptions: { - parser: testParsers.enhancedParser - }, - rules: { - "test/test-service-rule": 2 - } - }; + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + it("should be able to return in global if there is a comment which enables the node environment with a comment", () => { + const messages = linter.verify(`/* ${ESLINT_ENV} node */ return;`, null, "node environment"); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Hi!"); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - const messages2 = linter.verify(linter.getSourceCode(), config, filename); - const suppressedMessages2 = linter.getSuppressedMessages(); + it("should attach a \"/*global\" comment node to declared variables", () => { + const code = "/* global foo */\n/* global bar, baz */"; + let ok = false; - assert.strictEqual(messages2.length, 1); - assert.strictEqual(messages2[0].message, "Hi!"); - assert.strictEqual(suppressedMessages2.length, 0); - }); + linter.defineRules({ + test: { + create: context => ({ + Program(node) { + const scope = context.sourceCode.getScope(node); + const sourceCode = context.sourceCode; + const comments = sourceCode.getAllComments(); - it("should pass parser as context.languageOptions.parser to all rules when default parser is used", () => { + assert.strictEqual(context.getSourceCode(), sourceCode); + assert.strictEqual(2, comments.length); - // references to Espree get messed up in a browser context, so wrap it - const fakeParser = { - parse: espree.parse - }; + const foo = getVariable(scope, "foo"); - const spy = sinon.spy(context => { - assert.strictEqual(context.languageOptions.parser, fakeParser); - return {}; - }); + assert.strictEqual(foo.eslintExplicitGlobal, true); + assert.strictEqual(foo.eslintExplicitGlobalComments[0], comments[0]); - const config = { - plugins: { - test: { - rules: { - "test-rule": { create: spy } - } - } - }, - languageOptions: { - parser: fakeParser - }, - rules: { - "test/test-rule": 2 - } - }; + const bar = getVariable(scope, "bar"); - linter.verify("0", config, filename); - assert.isTrue(spy.calledOnce); - }); + assert.strictEqual(bar.eslintExplicitGlobal, true); + assert.strictEqual(bar.eslintExplicitGlobalComments[0], comments[1]); + const baz = getVariable(scope, "baz"); - describe("Custom Parsers", () => { + assert.strictEqual(baz.eslintExplicitGlobal, true); + assert.strictEqual(baz.eslintExplicitGlobalComments[0], comments[1]); - const errorPrefix = "Parsing error: "; + ok = true; + } + }) + } + }); - it("should have file path passed to it", () => { - const code = "/* this is code */"; - const parseSpy = { parse: sinon.spy() }; - const config = { - languageOptions: { - parser: parseSpy - } - }; + linter.verify(code, { rules: { test: 2 } }); + assert(ok); + }); - linter.verify(code, config, filename, true); + 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" } }); + const suppressedMessages = linter.getSuppressedMessages(); - sinon.assert.calledWithMatch(parseSpy.parse, "", { filePath: filename }); - }); + 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" + } + ]); - it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { - const code = "var myDivElement =
;"; - const config = { - languageOptions: { - parser: esprima, - parserOptions: { - jsx: true - } - } - }; + assert.strictEqual(suppressedMessages.length, 0); + }); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + it("should not crash when we reuse the SourceCode object", () => { + linter.verify("function render() { return
{hello}
}", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }); + linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } } }); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + it("should reuse the SourceCode object", () => { + let ast1 = null, + ast2 = null; - 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 - } - }; + linter.defineRule("save-ast1", { + create: () => ({ + Program(node) { + ast1 = node; + } + }) + }); + linter.defineRule("save-ast2", { + create: () => ({ + Program(node) { + ast2 = node; + } + }) + }); - // This shouldn't throw - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + linter.verify("function render() { return
{hello}
}", { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast1": 2 } }); + linter.verify(linter.getSourceCode(), { parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, rules: { "save-ast2": 2 } }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert(ast1 !== null); + assert(ast2 !== null); + assert(ast1 === ast2); + }); - 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 - } - }; + it("should allow 'await' as a property name in modules", () => { + const result = linter.verify( + "obj.await", + { parserOptions: { ecmaVersion: 6, sourceType: "module" } } + ); + const suppressedMessages = linter.getSuppressedMessages(); - // This shouldn't throw - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert(result.length === 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.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": { - create: () => ({ - "*"(node) { - nodes.push(node.type); - } - }) - } - } - } - }, - languageOptions: { - parser: testParsers.nonJSParser - }, - rules: { - "test/collect-node-types": "error" - } - }; + it("should not modify config object passed as argument", () => { + const config = {}; - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + Object.freeze(config); + linter.verify("var", config); + }); - assert.strictEqual(messages.length, 0); - assert.isTrue(nodes.length > 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + it("should pass 'id' to rule contexts with the rule id", () => { + const spy = sinon.spy(context => { + assert.strictEqual(context.id, "foo-bar-baz"); + return {}; + }); - it("should strip leading line: prefix from parser error", () => { - const messages = linter.verify(";", { - languageOptions: { - parser: testParsers.lineError - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); + linter.defineRule("foo-bar-baz", { create: spy }); + linter.verify("x", { rules: { "foo-bar-baz": "error" } }); + assert(spy.calledOnce); + }); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError); + describe("descriptions in directive comments", () => { + it("should ignore the part preceded by '--' in '/*eslint*/'.", () => { + const aaa = sinon.stub().returns({}); + const bbb = sinon.stub().returns({}); - assert.strictEqual(suppressedMessages.length, 0); - }); + linter.defineRule("aaa", { create: aaa }); + linter.defineRule("bbb", { create: bbb }); + const messages = linter.verify(` + /*eslint aaa:error -- bbb:error */ + console.log("hello") + `, {}); + const suppressedMessages = linter.getSuppressedMessages(); - it("should not modify a parser error message without a leading line: prefix", () => { - const messages = linter.verify(";", { - languageOptions: { - parser: testParsers.noLineError - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); + // Don't include syntax error of the comment. + assert.deepStrictEqual(messages, []); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError); + // Use only `aaa`. + assert.strictEqual(aaa.callCount, 1); + assert.strictEqual(bbb.callCount, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - describe("if a parser provides 'visitorKeys'", () => { - let types = []; - let sourceCode; - let scopeManager; - let firstChildNodes = []; + it("should ignore the part preceded by '--' in '/*eslint-env*/'.", () => { + const messages = linter.verify(` + /*eslint-env es2015 -- es2017 */ + var Promise = {} + var Atomics = {} + `, { rules: { "no-redeclare": "error" } }); + const suppressedMessages = linter.getSuppressedMessages(); - beforeEach(() => { - types = []; - firstChildNodes = []; - const config = { - plugins: { - test: { - rules: { - "collect-node-types": { - create: () => ({ - "*"(node) { - types.push(node.type); - } - }) - }, - "save-scope-manager": { - create(context) { - scopeManager = context.sourceCode.scopeManager; + // Don't include `Atomics` + assert.deepStrictEqual( + messages, + [{ + column: 25, + endColumn: 32, + endLine: 3, + line: 3, + message: "'Promise' is already defined as a built-in global variable.", + messageId: "redeclaredAsBuiltin", + nodeType: "Identifier", + ruleId: "no-redeclare", + severity: 2 + }] + ); - return {}; - } - }, - "esquery-option": { - create: () => ({ - ":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" - } - }; + assert.strictEqual(suppressedMessages.length, 0); + }); - linter.verify("@foo class A {}", config); + it("should ignore the part preceded by '--' in '/*global*/'.", () => { + const messages = linter.verify(` + /*global aaa -- bbb */ + var aaa = {} + var bbb = {} + `, { rules: { "no-redeclare": "error" } }); + const suppressedMessages = linter.getSuppressedMessages(); - sourceCode = linter.getSourceCode(); - }); + // Don't include `bbb` + assert.deepStrictEqual( + messages, + [{ + column: 30, + endColumn: 33, + line: 2, + endLine: 2, + message: "'aaa' is already defined by a variable declaration.", + messageId: "redeclaredBySyntax", + nodeType: "Block", + ruleId: "no-redeclare", + severity: 2 + }] + ); - it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { - assert.deepStrictEqual( - types, - ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] - ); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should ignore the part preceded by '--' in '/*globals*/'.", () => { + const messages = linter.verify(` + /*globals aaa -- bbb */ + var aaa = {} + var bbb = {} + `, { rules: { "no-redeclare": "error" } }); + const suppressedMessages = linter.getSuppressedMessages(); - 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"] - ); - }); + // 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 use the same visitorKeys if the source code object is reused", () => { - const types2 = []; - const config = { - plugins: { - test: { - rules: { - "collect-node-types": { - create: () => ({ - "*"(node) { - types2.push(node.type); - } - }) - } - } - } - }, - rules: { - "test/collect-node-types": "error" - } - }; + assert.strictEqual(suppressedMessages.length, 0); + }); - linter.verify(sourceCode, config); + it("should ignore the part preceded by '--' in '/*exported*/'.", () => { + const messages = linter.verify(` + /*exported aaa -- bbb */ + var aaa = {} + var bbb = {} + `, { rules: { "no-unused-vars": "error" } }); + const suppressedMessages = linter.getSuppressedMessages(); - assert.deepStrictEqual( - types2, - ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] - ); - }); + // 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("esquery should use the visitorKeys (so 'visitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { - assert.deepStrictEqual( - firstChildNodes, - [sourceCode.ast.body[0], sourceCode.ast.body[0].experimentalDecorators[0]] - ); - }); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - describe("if a parser provides 'scope'", () => { - let scope = null; - let sourceCode = null; + 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" } }); + const suppressedMessages = linter.getSuppressedMessages(); - beforeEach(() => { - const config = { - plugins: { - test: { - rules: { - "save-scope1": { - create: context => ({ - Program() { - scope = context.getScope(); - } - }) - } - } - } - }, - languageOptions: { - parser: testParsers.enhancedParser3 - }, - rules: { - "test/save-scope1": "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 + }] + ); - linter.verify("@foo class A {}", config); + assert.deepStrictEqual( + suppressedMessages, + [{ + column: 25, + endLine: 4, + endColumn: 28, + line: 4, + message: "'aaa' is already defined.", + messageId: "redeclared", + nodeType: "Identifier", + ruleId: "no-redeclare", + severity: 2, + suppressions: [{ kind: "directive", justification: "no-unused-vars" }] + }] + ); + }); - sourceCode = linter.getSourceCode(); - }); + 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" } }); + const suppressedMessages = linter.getSuppressedMessages(); - 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" - ); - }); + // 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 use the same scope if the source code object is reused", () => { - let scope2 = null; - const config = { - plugins: { - test: { - rules: { - "save-scope2": { - create: context => ({ - Program() { - scope2 = context.getScope(); - } - }) - } - } - } - }, - rules: { - "test/save-scope2": "error" - } - }; + assert.deepStrictEqual( + suppressedMessages, + [{ + 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, + suppressions: [{ kind: "directive", justification: "" }] + }] + ); + }); - linter.verify(sourceCode, config, "test.js"); + 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" } }); + const suppressedMessages = linter.getSuppressedMessages(); - assert(scope2 !== null); - assert(scope2 === scope); - }); - }); + // 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 pass default languageOptions to the parser", () => { + assert.deepStrictEqual( + suppressedMessages, + [{ + column: 25, + endLine: 3, + endColumn: 28, + line: 3, + message: "'aaa' is already defined.", + messageId: "redeclared", + nodeType: "Identifier", + ruleId: "no-redeclare", + severity: 2, + suppressions: [{ kind: "directive", justification: "no-unused-vars" }] + }] + ); + }); + + 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" } }); + const suppressedMessages = linter.getSuppressedMessages(); - const spy = sinon.spy((code, options) => espree.parse(code, options)); + // 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 + }] + ); - linter.verify(";", { - languageOptions: { - parser: { - parse: spy - } - } - }, "filename.js"); + assert.deepStrictEqual( + suppressedMessages, + [{ + column: 25, + endLine: 3, + endColumn: 28, + line: 3, + message: "'aaa' is already defined.", + messageId: "redeclared", + nodeType: "Identifier", + ruleId: "no-redeclare", + severity: 2, + suppressions: [{ kind: "directive", justification: "no-unused-vars" }] + }] + ); + }); - assert(spy.calledWithMatch(";", { - ecmaVersion: espree.latestEcmaVersion + 2009, - sourceType: "module" - })); - }); - }); + 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" } }); + const suppressedMessages = linter.getSuppressedMessages(); + // 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 + }] + ); + assert.deepStrictEqual( + suppressedMessages, + [{ + column: 25, + endLine: 5, + endColumn: 28, + line: 5, + message: "'aaa' is already defined.", + messageId: "redeclared", + nodeType: "Identifier", + ruleId: "no-redeclare", + severity: 2, + suppressions: [{ kind: "directive", justification: "no-unused-vars" }] + }] + ); }); - describe("parseOptions", () => { + 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" } }); + const suppressedMessages = linter.getSuppressedMessages(); - it("should pass ecmaFeatures to all rules when provided on config", () => { + // 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 + }] + ); - const parserOptions = { - ecmaFeatures: { - jsx: true - } - }; + assert.deepStrictEqual( + suppressedMessages, + [{ + column: 25, + endLine: 5, + endColumn: 28, + line: 5, + message: "'aaa' is already defined.", + messageId: "redeclared", + nodeType: "Identifier", + ruleId: "no-redeclare", + severity: 2, + suppressions: [{ kind: "directive", justification: "no-unused-vars" }] + }] + ); + }); - const config = { - plugins: { - test: { - rules: { - "test-rule": { - create: sinon.mock().withArgs( - sinon.match({ languageOptions: { parserOptions } }) - ).returns({}) - } - } - } - }, - languageOptions: { - parserOptions - }, - rules: { - "test/test-rule": 2 - } - }; + it("should not ignore the part preceded by '--' if the '--' is not surrounded by whitespaces.", () => { + const rule = sinon.stub().returns({}); - linter.verify("0", config, filename); - }); + linter.defineRule("a--rule", { create: rule }); + const messages = linter.verify(` + /*eslint a--rule:error */ + console.log("hello") + `, {}); + const suppressedMessages = linter.getSuppressedMessages(); - it("should switch globalReturn to false if sourceType is module", () => { + // Don't include syntax error of the comment. + assert.deepStrictEqual(messages, []); - const config = { - plugins: { - test: { - rules: { - "test-rule": { - create: sinon.mock().withArgs( - sinon.match({ - languageOptions: { - parserOptions: { - ecmaFeatures: { - globalReturn: false - } - } - } - }) - ).returns({}) - } - } - } - }, - languageOptions: { - sourceType: "module", - parserOptions: { - ecmaFeatures: { - globalReturn: true - } - } - }, - rules: { - "test/test-rule": 2 - } - }; + // Use `a--rule`. + assert.strictEqual(rule.callCount, 1); - linter.verify("0", config, filename); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - it("should not parse sloppy-mode code when impliedStrict is true", () => { + 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 messages = linter.verify("var private;", { - languageOptions: { - parserOptions: { - ecmaFeatures: { - impliedStrict: true - } - } - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); + linter.defineRule("aaa", { create: aaa }); + linter.defineRule("bbb", { create: bbb }); + const messages = linter.verify(` + /*eslint aaa:error -------- bbb:error */ + console.log("hello") + `, {}); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Parsing error: The keyword 'private' is reserved"); + // Don't include syntax error of the comment. + assert.deepStrictEqual(messages, []); - assert.strictEqual(suppressedMessages.length, 0); - }); + // Use only `aaa`. + assert.strictEqual(aaa.callCount, 1); + assert.strictEqual(bbb.callCount, 0); - it("should properly parse valid code when impliedStrict is true", () => { + assert.strictEqual(suppressedMessages.length, 0); + }); - const messages = linter.verify("var foo;", { - languageOptions: { - parserOptions: { - ecmaFeatures: { - impliedStrict: true - } - } - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); + it("should ignore the part preceded by '--' with line breaks.", () => { + const aaa = sinon.stub().returns({}); + const bbb = sinon.stub().returns({}); + + linter.defineRule("aaa", { create: aaa }); + linter.defineRule("bbb", { create: bbb }); + const messages = linter.verify(` + /*eslint aaa:error + -------- + bbb:error */ + console.log("hello") + `, {}); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + // Don't include syntax error of the comment. + assert.deepStrictEqual(messages, []); - it("should properly parse JSX when passed ecmaFeatures", () => { + // Use only `aaa`. + assert.strictEqual(aaa.callCount, 1); + assert.strictEqual(bbb.callCount, 0); - const messages = linter.verify("var x =
;", { - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true - } - } + assert.strictEqual(suppressedMessages.length, 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; + + linter.defineRules({ + test: { + create: context => ({ + Program(node) { + scope = context.sourceCode.getScope(node); + ok = true; } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); + }) + } + }); + linter.verify(code, { rules: { test: 2 }, globals: { e: true, f: false } }); + assert(ok); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + afterEach(() => { + scope = null; + }); - it("should report an error when JSX code is encountered and JSX is not enabled", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, {}, filename); - const suppressedMessages = linter.getSuppressedMessages(); + 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); + }); - 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("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")); + }); - assert.strictEqual(suppressedMessages.length, 0); - }); + 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("should not report an error when JSX code is encountered and JSX is enabled", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, { - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true - } - } - } - }, filename); - const suppressedMessages = linter.getSuppressedMessages(); + 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")); + }); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + 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")); + }); + }); - it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, { - languageOptions: { - ecmaVersion: 6, - parserOptions: { - ecmaFeatures: { - jsx: true - } - } - } + describe("suggestions", () => { + it("provides suggestion information for tools to use", () => { + linter.defineRule("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, " ") + }] + }); + } + }) + }); - }, "filename.js"); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); + 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("should not allow the use of reserved words as variable names in ES3", () => { - const code = "var char;"; - const messages = linter.verify(code, { - languageOptions: { - ecmaVersion: 3, - sourceType: "script" - } - }, filename); + assert.strictEqual(suppressedMessages.length, 0); + }); - 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("supports messageIds for suggestions", () => { + linter.defineRule("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, " ") + }] + }); + } + }) + }); - 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, { - languageOptions: { - ecmaVersion: 3, - sourceType: "script" - } - }, filename); + const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); + const suppressedMessages = linter.getSuppressedMessages(); + + 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: " " + } + }]); - 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); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - 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, { - languageOptions: { - ecmaVersion: 3, - sourceType: "script" - } - }, filename); + it("should throw an error if suggestion is passed but `meta.hasSuggestions` property is not enabled", () => { + linter.defineRule("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, " ") }] + }); + } + }) + }); - 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); - }); + assert.throws(() => { + linter.verify("0", { rules: { "rule-with-suggestions": "error" } }); + }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`."); + }); - 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, { - languageOptions: { - ecmaVersion: 3, - sourceType: "script", - parserOptions: { - allowReserved: true - } - } - }, filename); + 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", () => { + linter.defineRule("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, " ") }] + }); + } + }) + }); - assert.strictEqual(messages.length, 0); - }); + assert.throws(() => { + linter.verify("0", { rules: { "rule-with-meta-docs-suggestion": "error" } }); + }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint."); + }); + }); - 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)]; + describe("mutability", () => { + let linter1 = null; + let linter2 = null; - ecmaVersions.forEach(ecmaVersion => { - const code = "var enum;"; - const messages = linter.verify(code, { - languageOptions: { - ...(ecmaVersion ? { ecmaVersion } : {}), - sourceType: "script" - } - }, filename); + beforeEach(() => { + linter1 = new Linter(); + linter2 = new Linter(); + }); - 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); - }); + 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", { + create: () => ({}) }); - it("should allow the use of reserved words as property names in ES > 3", () => { - const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)]; + assert.isTrue(linter1.getRules().has("mock-rule"), "mock rule is present"); + assert.isFalse(linter2.getRules().has("mock-rule"), "mock rule is not present"); + }); + }); + }); - ecmaVersions.forEach(ecmaVersion => { - const code = "obj.enum; obj.function; var obj = { enum: 1, function: 2 };"; - const messages = linter.verify(code, { - languageOptions: { - ...(ecmaVersion ? { ecmaVersion } : {}), - sourceType: "script" - } - }, filename); + describe("processors", () => { + let receivedFilenames = []; + let receivedPhysicalFilenames = []; - assert.strictEqual(messages.length, 0); - }); - }); + beforeEach(() => { + receivedFilenames = []; + receivedPhysicalFilenames = []; - it("should not allow `allowReserved: true` in ES > 3", () => { - const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)]; + // A rule that always reports the AST with a message equal to the source text + linter.defineRule("report-original-text", { + create: context => ({ + Program(ast) { + assert.strictEqual(context.getFilename(), context.filename); + assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); - ecmaVersions.forEach(ecmaVersion => { - const code = ""; - const messages = linter.verify(code, { - languageOptions: { - ...(ecmaVersion ? { ecmaVersion } : {}), - sourceType: "script", - parserOptions: { - allowReserved: true - } - } - }, filename); + receivedFilenames.push(context.filename); + receivedPhysicalFilenames.push(context.physicalFilename); - 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); - }); - }); + context.report({ node: ast, message: context.sourceCode.text }); + } + }) }); }); - describe("settings", () => { - const ruleId = "test-rule"; + describe("preprocessors", () => { + it("should receive text and filename.", () => { + const code = "foo bar baz"; + const preprocess = sinon.spy(text => text.split(" ")); - it("should pass settings to all rules", () => { + linter.verify(code, {}, { filename, preprocess }); - const config = { - plugins: { - test: { - rules: { - [ruleId]: { - create: context => ({ - Literal(node) { - context.report(node, context.settings.info); - } - }) - } - } - } - }, - settings: { - info: "Hello" - }, - rules: { - [`test/${ruleId}`]: 1 - } - }; + assert.strictEqual(preprocess.calledOnce, true); + assert.deepStrictEqual(preprocess.args[0], [code, filename]); + }); - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + it("should apply a preprocessor to the code, and lint each code sample separately", () => { + const code = "foo bar baz"; + const problems = linter.verify( + code, + { rules: { "report-original-text": "error" } }, + { - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Hello"); + // Apply a preprocessor that splits the source text into spaces and lints each word individually + preprocess(input) { + return input.split(" "); + } + } + ); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(problems.length, 3); + assert.deepStrictEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]); }); - it("should not have any settings if they were not passed in", () => { + it("should apply a preprocessor to the code even if the preprocessor returned code block objects.", () => { + const code = "foo bar baz"; + const problems = linter.verify( + code, + { rules: { "report-original-text": "error" } }, + { + filename, - const config = { - plugins: { - test: { - rules: { - [ruleId]: { - create: context => ({ - Literal(node) { - if (Object.getOwnPropertyNames(context.settings).length !== 0) { - context.report(node, "Settings should be empty"); - } - } - }) - } - } + // 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 + })); } - }, - settings: { - }, - rules: { - [`test/${ruleId}`]: 1 } - }; - - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + ); - assert.strictEqual(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); + assert.strictEqual(problems.length, 3); + assert.deepStrictEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]); - describe("rules", () => { - const code = "var answer = 6 * 7"; + // 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])); - it("should be configurable by only setting the integer value", () => { - const rule = "semi", - config = { rules: {} }; + // physical filename + assert.strictEqual(receivedPhysicalFilenames.length, 3); + assert.strictEqual(receivedPhysicalFilenames.every(name => name === filename), true); + }); - config.rules[rule] = 1; + it("should receive text even if a SourceCode object was given.", () => { + const code = "foo"; + const preprocess = sinon.spy(text => text.split(" ")); - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + linter.verify(code, {}); + const sourceCode = linter.getSourceCode(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, rule); + linter.verify(sourceCode, {}, { filename, preprocess }); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(preprocess.calledOnce, true); + assert.deepStrictEqual(preprocess.args[0], [code, filename]); }); - it("should be configurable by only setting the string value", () => { - const rule = "semi", - config = { rules: {} }; - - config.rules[rule] = "warn"; + it("should receive text even if a SourceCode object was given (with BOM).", () => { + const code = "\uFEFFfoo"; + const preprocess = sinon.spy(text => text.split(" ")); - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + linter.verify(code, {}); + const sourceCode = linter.getSourceCode(); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 1); - assert.strictEqual(messages[0].ruleId, rule); + linter.verify(sourceCode, {}, { filename, preprocess }); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(preprocess.calledOnce, true); + assert.deepStrictEqual(preprocess.args[0], [code, filename]); }); - it("should be configurable by passing in values as an array", () => { - const rule = "semi", - config = { rules: {} }; + it("should catch preprocess error.", () => { + const code = "foo"; + const preprocess = sinon.spy(() => { + throw Object.assign(new SyntaxError("Invalid syntax"), { + lineNumber: 1, + column: 1 + }); + }); - config.rules[rule] = [1]; + const messages = linter.verify(code, {}, { filename, preprocess }); - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(preprocess.calledOnce, true); + assert.deepStrictEqual(preprocess.args[0], [code, filename]); + assert.deepStrictEqual(messages, [ + { + ruleId: null, + fatal: true, + severity: 2, + message: "Preprocessing error: Invalid syntax", + line: 1, + column: 1, + nodeType: null + } + ]); + }); + }); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, rule); + 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]); - assert.strictEqual(suppressedMessages.length, 0); - }); + linter.verify(code, {}, { filename, postprocess, preprocess }); - it("should be configurable by passing in string value as an array", () => { - const rule = "semi", - config = { rules: {} }; + assert.strictEqual(postprocess.calledOnce, true); + assert.deepStrictEqual(postprocess.args[0], [[[], [], []], filename]); + }); - config.rules[rule] = ["warn"]; + it("should apply a postprocessor to the reported messages", () => { + const code = "foo bar baz"; - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + const problems = linter.verify( + code, + { rules: { "report-original-text": "error" } }, + { + preprocess: input => input.split(" "), - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 1); - assert.strictEqual(messages[0].ruleId, rule); + /* + * 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(suppressedMessages.length, 0); + 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 not be configurable by setting other value", () => { - const rule = "semi", - config = { rules: {} }; + it("should use postprocessed problem ranges when applying autofixes", () => { + const code = "foo bar baz"; - config.rules[rule] = "1"; + linter.defineRule("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()) + }); + } + } + }; + } + }); - assert.throws(() => { - linter.verify(code, config, filename, true); - }, /Key "rules": Key "semi": Expected severity/u); - }); + const fixResult = linter.verifyAndFix( + code, + { rules: { "capitalize-identifiers": "error" } }, + { - it("should process empty config", () => { - const config = {}; - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + /* + * 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(messages.length, 0); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(fixResult.fixed, true); + assert.strictEqual(fixResult.messages.length, 0); + assert.strictEqual(fixResult.output, "FOO BAR BAZ"); }); }); - }); - 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"] } }; + describe("verifyAndFix", () => { + it("Fixes the code", () => { + const messages = linter.verifyAndFix("var a", { + rules: { + semi: 2 + } + }, { filename: "test.js" }); - const messages = linter.verify(code, config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.output, "var a;", "Fixes were applied correctly"); + assert.isTrue(messages.fixed); + }); - 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); + it("does not require a third argument", () => { + const fixResult = linter.verifyAndFix("var a", { + rules: { + semi: 2 + } + }); - assert.strictEqual(suppressedMessages.length, 0); + assert.deepStrictEqual(fixResult, { + fixed: true, + messages: [], + output: "var a;" + }); }); - it("should report ignored file when filename isn't matched in the config array", () => { + it("does not include suggestions in autofix results", () => { + const fixResult = linter.verifyAndFix("var foo = /\\#/", { + rules: { + semi: 2, + "no-useless-escape": 2 + } + }); - const code = "foo()\n alert('test')"; - const config = { rules: { "no-mixed-spaces-and-tabs": 1, "eol-last": 1, semi: [1, "always"] } }; + assert.strictEqual(fixResult.output, "var foo = /\\#/;"); + assert.strictEqual(fixResult.fixed, true); + assert.strictEqual(fixResult.messages[0].suggestions.length > 0, true); + }); - const messages = linter.verify(code, config, "filename.ts"); + it("does not apply autofixes when fix argument is `false`", () => { + const fixResult = linter.verifyAndFix("var a", { + rules: { + semi: 2 + } + }, { fix: false }); - assert.strictEqual(messages.length, 1); - assert.deepStrictEqual(messages[0], { - ruleId: null, - severity: 1, - message: "No matching configuration found for filename.ts.", - line: 0, - column: 0, - nodeType: null - }); + assert.strictEqual(fixResult.fixed, false); }); - // https://github.com/eslint/eslint/issues/17669 - it("should use `cwd` constructor option as config `basePath` when config is not an instance of FlatConfigArray", () => { - const rule = { + it("stops fixing after 10 passes", () => { + + linter.defineRule("add-spaces", { + meta: { + fixable: "whitespace" + }, create(context) { return { Program(node) { - context.report({ node, message: "Bad program." }); + context.report({ + node, + message: "Add a space before this node.", + fix: fixer => fixer.insertTextBefore(node, " ") + }); } }; } - }; + }); - const code = "foo"; - const config = [ - { - plugins: { - test: { - rules: { - "test-rule-1": rule, - "test-rule-2": rule, - "test-rule-3": rule - } - } - } - }, - { - rules: { - "test/test-rule-1": 2 - } - }, - { - files: ["**/*.ts"], - rules: { - "test/test-rule-2": 2 - } + const fixResult = linter.verifyAndFix("a", { rules: { "add-spaces": "error" } }); + + 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", () => { + linter.defineRule("test-rule", { + meta: { + docs: {}, + schema: [] }, - { - files: ["bar/file.ts"], - rules: { - "test/test-rule-3": 2 + create: context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); } - } - ]; - - const linterWithOptions = new Linter({ - configType: "flat", - cwd: "/foo" + }) }); - let messages; + assert.throws(() => { + linter.verify("0", { rules: { "test-rule": "error" } }); + }, /Fixable rules must set the `meta\.fixable` property to "code" or "whitespace".\nOccurred while linting :1\nRule: "test-rule"$/u); + }); - messages = linterWithOptions.verify(code, config, "/file.js"); - assert.strictEqual(messages.length, 1); - assert.deepStrictEqual(messages[0], { - ruleId: null, - severity: 1, - message: "No matching configuration found for /file.js.", - line: 0, - column: 0, - nodeType: null + it("should throw an error if fix is passed and there is no metadata", () => { + linter.defineRule("test-rule", { + create: context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + } + }) }); - messages = linterWithOptions.verify(code, config, "/file.ts"); - assert.strictEqual(messages.length, 1); - assert.deepStrictEqual(messages[0], { - ruleId: null, - severity: 1, - message: "No matching configuration found for /file.ts.", - line: 0, - column: 0, - nodeType: null - }); + assert.throws(() => { + linter.verify("0", { rules: { "test-rule": "error" } }); + }, /Fixable rules must set the `meta\.fixable` property/u); + }); - messages = linterWithOptions.verify(code, config, "/bar/foo/file.js"); - assert.strictEqual(messages.length, 1); - assert.deepStrictEqual(messages[0], { - ruleId: null, - severity: 1, - message: "No matching configuration found for /bar/foo/file.js.", - line: 0, - column: 0, - nodeType: null + it("should throw an error if fix is passed from a legacy-format rule", () => { + linter.defineRule("test-rule", { + create: context => ({ + Program(node) { + context.report(node, "hello world", {}, () => ({ range: [1, 1], text: "" })); + } + }) }); - messages = linterWithOptions.verify(code, config, "/bar/foo/file.ts"); - assert.strictEqual(messages.length, 1); - assert.deepStrictEqual(messages[0], { - ruleId: null, - severity: 1, - message: "No matching configuration found for /bar/foo/file.ts.", - line: 0, - column: 0, - nodeType: null - }); + assert.throws(() => { + linter.verify("0", { rules: { "test-rule": "error" } }); + }, /Fixable rules must set the `meta\.fixable` property/u); + }); + }); - messages = linterWithOptions.verify(code, config, "/foo/file.js"); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].ruleId, "test/test-rule-1"); + describe("Edge cases", () => { - messages = linterWithOptions.verify(code, config, "/foo/file.ts"); - assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].ruleId, "test/test-rule-1"); - assert.strictEqual(messages[1].ruleId, "test/test-rule-2"); + it("should properly parse import statements when sourceType is module", () => { + const code = "import foo from 'foo';"; + const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, sourceType: "module" } }); + const suppressedMessages = linter.getSuppressedMessages(); - messages = linterWithOptions.verify(code, config, "/foo/bar/file.ts"); - assert.strictEqual(messages.length, 3); - assert.strictEqual(messages[0].ruleId, "test/test-rule-1"); - assert.strictEqual(messages[1].ruleId, "test/test-rule-2"); - assert.strictEqual(messages[2].ruleId, "test/test-rule-3"); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - describe("Plugins", () => { - - it("should not load rule definition when rule isn't used", () => { + it("should properly parse import all statements when sourceType is module", () => { + const code = "import * as foo from 'foo';"; + const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, sourceType: "module" } }); + const suppressedMessages = linter.getSuppressedMessages(); - const spy = sinon.spy(); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } - } - } - } - }; + it("should properly parse default export statements when sourceType is module", () => { + const code = "export default function initialize() {}"; + const messages = linter.verify(code, { parserOptions: { ecmaVersion: 6, sourceType: "module" } }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify("code", config, filename); - assert.isTrue(spy.notCalled, "Rule should not have been called"); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - describe("Rule Internals", () => { + // https://github.com/eslint/eslint/issues/9687 + it("should report an error when invalid parserOptions found", () => { + let messages = linter.verify("", { parserOptions: { ecmaVersion: 222 } }); + let suppressedMessages = linter.getSuppressedMessages(); - const code = TEST_CODE; + assert.deepStrictEqual(messages.length, 1); + assert.ok(messages[0].message.includes("Invalid ecmaVersion")); + assert.strictEqual(suppressedMessages.length, 0); - it("should throw an error when an error occurs inside of a rule visitor", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create: () => ({ - Program() { - throw new Error("Intentional error."); - } + messages = linter.verify("", { parserOptions: { sourceType: "foo" } }); + suppressedMessages = linter.getSuppressedMessages(); - }) - } - } - } - }, - rules: { "test/checker": "error" } - }; + assert.deepStrictEqual(messages.length, 1); + assert.ok(messages[0].message.includes("Invalid sourceType")); + assert.strictEqual(suppressedMessages.length, 0); - assert.throws(() => { - linter.verify(code, config, filename); - }, `Intentional error.\nOccurred while linting ${filename}:1\nRule: "test/checker"`); - }); + messages = linter.verify("", { parserOptions: { ecmaVersion: 5, sourceType: "module" } }); + suppressedMessages = linter.getSuppressedMessages(); - it("should not call rule visitor with a `this` value", () => { - const spy = sinon.spy(); - const config = { - plugins: { - test: { - rules: { - checker: { - create: () => ({ - Program: spy - }) - } - } - } - }, - rules: { "test/checker": "error" } - }; + assert.deepStrictEqual(messages.length, 1); + assert.ok(messages[0].message.includes("sourceType 'module' is not supported when ecmaVersion < 2015")); + assert.strictEqual(suppressedMessages.length, 0); + }); + + 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; }", { parserOptions: { ecmaVersion: 6 } }); + }); + + it("should not crash when parsing destructured assignment", () => { + linter.verify("var { a='a' } = {};", { parserOptions: { ecmaVersion: 6 } }); + }); - linter.verify("foo", config); - assert(spy.calledOnce); - assert.strictEqual(spy.firstCall.thisValue, void 0); + it("should report syntax error when a keyword exists in object property shorthand", () => { + const messages = linter.verify("let a = {this}", { parserOptions: { ecmaVersion: 6 } }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].fatal, true); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should not rewrite env setting in core (https://github.com/eslint/eslint/issues/4814)", () => { + + /* + * This test focuses on the instance of https://github.com/eslint/eslint/blob/v2.0.0-alpha-2/conf/environments.js#L26-L28 + * This `verify()` takes the instance and runs https://github.com/eslint/eslint/blob/v2.0.0-alpha-2/lib/eslint.js#L416 + */ + linter.defineRule("test", { + create: () => ({}) + }); + linter.verify("var a = 0;", { + env: { node: true }, + parserOptions: { ecmaVersion: 6, sourceType: "module" }, + rules: { test: 2 } }); - it("should not call unrecognized rule visitor when present in a rule", () => { - const spy = sinon.spy(); - const config = { - plugins: { - test: { - rules: { - checker: { - create: () => ({ - newListener: spy - }) - } - } - } - }, - rules: { - "test/checker": "error", - "no-undef": "error" - } - }; + // This `verify()` takes the instance and tests that the instance was not modified. + let ok = false; - linter.verify("foo", config); - assert(spy.notCalled); + linter.defineRule("test", { + create(context) { + assert( + context.parserOptions.ecmaFeatures.globalReturn, + "`ecmaFeatures.globalReturn` of the node environment should not be modified." + ); + ok = true; + return {}; + } + }); + linter.verify("var a = 0;", { + env: { node: true }, + rules: { test: 2 } }); - it("should have all the `parent` properties on nodes when the rule visitors are created", () => { - const spy = sinon.spy(context => { - assert.strictEqual(context.getSourceCode(), context.sourceCode); - const ast = context.sourceCode.ast; + assert(ok); + }); - 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); + it("should throw when rule's create() function does not return an object", () => { + const config = { rules: { checker: "error" } }; - return {}; - }); + linter.defineRule("checker", { + create: () => null + }); // returns null - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } - } - } - }, - rules: { "test/checker": "error" } - }; + assert.throws(() => { + linter.verify("abc", config, filename); + }, "The create() function for rule 'checker' did not return an object."); - linter.verify("foo + bar", config); - assert(spy.calledOnce); - }); + linter.defineRule("checker", { + create() {} + }); // returns undefined - it("events for each node type should fire", () => { + assert.throws(() => { + linter.verify("abc", config, filename); + }, "The create() function for rule 'checker' did not return an object."); + }); + }); - // spies for various AST node types - const spyLiteral = sinon.spy(), - spyVariableDeclarator = sinon.spy(), - spyVariableDeclaration = sinon.spy(), - spyIdentifier = sinon.spy(), - spyBinaryExpression = sinon.spy(); + describe("Custom parser", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create() { - return { - Literal: spyLiteral, - VariableDeclarator: spyVariableDeclarator, - VariableDeclaration: spyVariableDeclaration, - Identifier: spyIdentifier, - BinaryExpression: spyBinaryExpression - }; - } - } - } - } - }, - rules: { "test/checker": "error" } - }; + const errorPrefix = "Parsing error: "; - const messages = linter.verify(code, config, filename, true); - const suppressedMessages = linter.getSuppressedMessages(); + it("should have file path passed to it", () => { + const code = "/* this is code */"; + const parseSpy = { parse: sinon.spy() }; - 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); + linter.defineParser("stub-parser", parseSpy); + linter.verify(code, { parser: "stub-parser" }, filename, true); - assert.strictEqual(suppressedMessages.length, 0); - }); + sinon.assert.calledWithMatch(parseSpy.parse, "", { filePath: filename }); + }); - it("should throw an error if a rule reports a problem without a message", () => { + it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { + const code = "var myDivElement =
;"; - const config = { - plugins: { - test: { - rules: { - "invalid-report": { - create: context => ({ - Program(node) { - context.report({ node }); - } - }) - } - } - } - }, - rules: { "test/invalid-report": "error" } - }; + linter.defineParser("esprima", esprima); + const messages = linter.verify(code, { parser: "esprima", parserOptions: { jsx: true } }, "filename"); + const suppressedMessages = linter.getSuppressedMessages(); - assert.throws( - () => linter.verify("foo", config), - TypeError, - "Missing `message` property in report() call; add a message that describes the linting problem." - ); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should return an error when the custom parser can't be found", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, { parser: "esprima-xyz" }, "filename"); + const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Configured parser 'esprima-xyz' was not found."); + assert.strictEqual(suppressedMessages.length, 0); }); - describe("Rule Context", () => { + 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'"; - describe("context.getFilename()", () => { - const ruleId = "filename-rule"; + linter.defineParser("unknown-logical-operator", testParsers.unknownLogicalOperator); - it("has access to the filename", () => { + // This shouldn't throw + const messages = linter.verify(code, { parser: "unknown-logical-operator" }, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - const config = { - plugins: { - test: { - rules: { - [ruleId]: { - create: context => ({ - Literal(node) { - context.report(node, context.getFilename()); - } - }) - } - } - } - }, - rules: { - [`test/${ruleId}`]: 1 - } - }; + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + 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"; - assert.strictEqual(messages[0].message, filename); - assert.strictEqual(suppressedMessages.length, 0); - }); + linter.defineParser("unknown-logical-operator-nested", testParsers.unknownLogicalOperatorNested); - it("defaults filename to ''", () => { + // This shouldn't throw + const messages = linter.verify(code, { parser: "unknown-logical-operator-nested" }, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - const config = { - plugins: { - test: { - rules: { - [ruleId]: { - create: context => ({ - Literal(node) { - context.report(node, context.getFilename()); - } - }) - } - } - } - }, - rules: { - [`test/${ruleId}`]: 1 - } - }; + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + it("should not throw or return errors when the custom parser returns unknown AST nodes", () => { + const code = "foo && bar %% baz"; - const messages = linter.verify("0", config); - const suppressedMessages = linter.getSuppressedMessages(); + const nodes = []; - assert.strictEqual(messages[0].message, ""); - assert.strictEqual(suppressedMessages.length, 0); - }); + linter.defineRule("collect-node-types", { + create: () => ({ + "*"(node) { + nodes.push(node.type); + } + }) }); - describe("context.filename", () => { - const ruleId = "filename-rule"; - - it("has access to the filename", () => { - - const config = { - plugins: { - test: { - rules: { - [ruleId]: { - create: context => ({ - Literal(node) { - assert.strictEqual(context.getFilename(), context.filename); - context.report(node, context.filename); - } - }) - } - } - } - }, - rules: { - [`test/${ruleId}`]: 1 - } - }; + linter.defineParser("non-js-parser", testParsers.nonJSParser); - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + const messages = linter.verify(code, { + parser: "non-js-parser", + rules: { + "collect-node-types": "error" + } + }, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages[0].message, filename); - assert.strictEqual(suppressedMessages.length, 0); - }); + assert.strictEqual(messages.length, 0); + assert.isTrue(nodes.length > 0); - it("defaults filename to ''", () => { + assert.strictEqual(suppressedMessages.length, 0); + }); - const config = { - plugins: { - test: { - rules: { - [ruleId]: { - create: context => ({ - Literal(node) { - assert.strictEqual(context.getFilename(), context.filename); - context.report(node, context.filename); - } - }) - } - } - } - }, - rules: { - [`test/${ruleId}`]: 1 - } - }; + it("should strip leading line: prefix from parser error", () => { + linter.defineParser("line-error", testParsers.lineError); + const messages = linter.verify(";", { parser: "line-error" }, "filename"); + const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError); - const messages = linter.verify("0", config); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.strictEqual(messages[0].message, ""); - assert.strictEqual(suppressedMessages.length, 0); - }); - }); + it("should not modify a parser error message without a leading line: prefix", () => { + linter.defineParser("no-line-error", testParsers.noLineError); + const messages = linter.verify(";", { parser: "no-line-error" }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - describe("context.getPhysicalFilename()", () => { + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError); - const ruleId = "filename-rule"; + assert.strictEqual(suppressedMessages.length, 0); + }); - it("has access to the physicalFilename", () => { + describe("if a parser provides 'visitorKeys'", () => { + let types = []; + let sourceCode; + let scopeManager; + let firstChildNodes = []; - const config = { - plugins: { - test: { - rules: { - [ruleId]: { - create: context => ({ - Literal(node) { - context.report(node, context.getPhysicalFilename()); - } - }) - } - } - } - }, - rules: { - [`test/${ruleId}`]: 1 + beforeEach(() => { + types = []; + firstChildNodes = []; + linter.defineRule("collect-node-types", { + create: () => ({ + "*"(node) { + types.push(node.type); } - }; - - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); + }) + }); + linter.defineRule("save-scope-manager", { + create(context) { + scopeManager = context.sourceCode.scopeManager; - assert.strictEqual(messages[0].message, filename); - assert.strictEqual(suppressedMessages.length, 0); + return {}; + } + }); + linter.defineRule("esquery-option", { + create: () => ({ + ":first-child"(node) { + firstChildNodes.push(node); + } + }) + }); + linter.defineParser("enhanced-parser2", testParsers.enhancedParser2); + linter.verify("@foo class A {}", { + parser: "enhanced-parser2", + rules: { + "collect-node-types": "error", + "save-scope-manager": "error", + "esquery-option": "error" + } }); + sourceCode = linter.getSourceCode(); }); - describe("context.physicalFilename", () => { + it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { + assert.deepStrictEqual( + types, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); - const ruleId = "filename-rule"; + 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("has access to the physicalFilename", () => { + it("should use the same visitorKeys if the source code object is reused", () => { + const types2 = []; - const config = { - plugins: { - test: { - rules: { - [ruleId]: { - create: context => ({ - Literal(node) { - assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); - context.report(node, context.physicalFilename); - } - }) - } - } - } - }, - rules: { - [`test/${ruleId}`]: 1 + linter.defineRule("collect-node-types", { + create: () => ({ + "*"(node) { + types2.push(node.type); } - }; - - const messages = linter.verify("0", config, filename); - const suppressedMessages = linter.getSuppressedMessages(); - - assert.strictEqual(messages[0].message, filename); - assert.strictEqual(suppressedMessages.length, 0); + }) + }); + linter.verify(sourceCode, { + rules: { + "collect-node-types": "error" + } }); + assert.deepStrictEqual( + types2, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); }); - 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 {}; - }); + it("esquery should use the visitorKeys (so 'visitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { + assert.deepStrictEqual( + firstChildNodes, + [sourceCode.ast.body[0], sourceCode.ast.body[0].experimentalDecorators[0]] + ); + }); + }); - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } - } - } - }, - rules: { "test/checker": "error" } - }; + describe("if a parser provides 'scope'", () => { + let scope = null; + let sourceCode = null; - linter.verify(code, config); - assert(spy.calledOnce); + beforeEach(() => { + linter.defineParser("enhanced-parser3", testParsers.enhancedParser3); + linter.defineRule("save-scope1", { + create: context => ({ + Program(node) { + scope = context.sourceCode.getScope(node); + } + }) }); + linter.verify("@foo class A {}", { parser: "enhanced-parser3", rules: { "save-scope1": 2 } }); - 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 {}; - }); + 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" + ); + }); - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } - } - } - }, - rules: { "test/checker": "error" } - }; + it("should use the same scope if the source code object is reused", () => { + let scope2 = null; - linter.verify(code, config); - assert(spy.calledOnce); + linter.defineRule("save-scope2", { + create: context => ({ + Program(node) { + scope2 = context.sourceCode.getScope(node); + } + }) }); + linter.verify(sourceCode, { rules: { "save-scope2": 2 } }, "test.js"); - 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 {}; - }); + assert(scope2 !== null); + assert(scope2 === scope); + }); + }); - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } - } - } - }, - rules: { "test/checker": "error" } - }; + it("should not pass any default parserOptions to the parser", () => { + linter.defineParser("throws-with-options", testParsers.throwsWithOptions); + const messages = linter.verify(";", { parser: "throws-with-options" }, "filename"); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy.calledOnce); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - 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 {}; - }); + describe("merging 'parserOptions'", () => { + it("should deeply merge 'parserOptions' from an environment with 'parserOptions' from the provided config", () => { + const code = "return
"; + const config = { + env: { + node: true // ecmaFeatures: { globalReturn: true } + }, + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + }; - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } - } - } - }, - rules: { "test/checker": "error" } - }; + const messages = linter.verify(code, config); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy.calledOnce); - }); + // no parsing errors + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); +}); - 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 {}; - }); +describe("Linter with FlatConfigArray", () => { - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } - } - } - }, - rules: { "test/checker": "error" } - }; + let linter; + const filename = "filename.js"; - linter.verify(code, config); - assert(spy.calledOnce); - }); + /** + * 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("context.getSource()", () => { - const code = TEST_CODE; + describe("Config Options", () => { - it("should retrieve all text when used without parameters", () => { + describe("languageOptions", () => { - let spy; + describe("ecmaVersion", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - assert.strictEqual(context.getSource(), TEST_CODE); - }); - return { Program: spy }; - } - } - } - } + 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: { "test/checker": "error" } - }; - - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); + rules: { + "no-undef": "error" + } + }); + const suppressedMessages = linter.getSuppressedMessages(); - it("should retrieve all text for root node", () => { + 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."); - let spy; + assert.strictEqual(suppressedMessages.length, 0); + }); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node), TEST_CODE); - }); - return { Program: spy }; - } - } - } - } + 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: { "test/checker": "error" } - }; + rules: { + "no-undef": "error" + } + }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); - }); + 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 clamp to valid range when retrieving characters before start of source", () => { - let spy; + assert.strictEqual(suppressedMessages.length, 0); + }); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node, 2, 0), TEST_CODE); - }); - return { Program: spy }; - } - } - } - } + it("should add globals for ES6 when ecmaVersion is 6", () => { + const messages = linter.verify("new Map()", { + languageOptions: { + ecmaVersion: 6 }, - rules: { "test/checker": "error" } - }; + rules: { + "no-undef": "error" + } + }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 0, "There should be no linting errors."); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve all text for binary expression", () => { - let spy; + it("should allow destructuring when ecmaVersion is 6", () => { + const messages = linter.verify("let {a} = b", { + languageOptions: { + ecmaVersion: 6 + } + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0, "There should be no linting errors."); + assert.strictEqual(suppressedMessages.length, 0); + }); + it("ecmaVersion should be normalized to year name for ES 6", () => { const config = { plugins: { test: { rules: { checker: { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node), "6 * 7"); - }); - return { BinaryExpression: spy }; - } + create: context => ({ + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, 2015); + } + }) } } } }, + languageOptions: { + ecmaVersion: 6 + }, rules: { "test/checker": "error" } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + linter.verify("foo", config, filename); }); - it("should retrieve all text plus two characters before for binary expression", () => { - let spy; - + it("ecmaVersion should be normalized to latest year by default", () => { const config = { plugins: { test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node, 2), "= 6 * 7"); - }); - return { BinaryExpression: spy }; - } + rules: { + checker: { + create: context => ({ + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); + } + }) } } } @@ -10572,82 +7425,73 @@ describe("Linter with FlatConfigArray", () => { rules: { "test/checker": "error" } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + linter.verify("foo", config, filename); }); - it("should retrieve all text plus one character after for binary expression", () => { - let spy; - + it("ecmaVersion should not be normalized to year name for ES 5", () => { const config = { plugins: { test: { rules: { checker: { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node, 0, 1), "6 * 7;"); - }); - return { BinaryExpression: spy }; - } + create: context => ({ + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, 5); + } + }) } } } }, + languageOptions: { + ecmaVersion: 5 + }, rules: { "test/checker": "error" } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + linter.verify("foo", config, filename); }); - it("should retrieve all text plus two characters before and one character after for binary expression", () => { - let spy; - + it("ecmaVersion should be normalized to year name for 'latest'", () => { const config = { plugins: { test: { rules: { checker: { - create(context) { - spy = sinon.spy(node => { - assert.strictEqual(context.getSource(node, 2, 1), "= 6 * 7;"); - }); - return { BinaryExpression: spy }; - } + create: context => ({ + Program() { + assert.strictEqual(context.languageOptions.ecmaVersion, espree.latestEcmaVersion + 2009); + } + }) } } } }, + languageOptions: { + ecmaVersion: "latest" + }, rules: { "test/checker": "error" } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + linter.verify("foo", config, filename); }); - }); - - describe("context.getAncestors()", () => { - const code = TEST_CODE; - it("should retrieve all ancestors when used", () => { + }); - let spy; + describe("sourceType", () => { + it("should be module by default", () => { const config = { plugins: { test: { rules: { checker: { - create(context) { - spy = sinon.spy(() => { - const ancestors = context.getAncestors(); - - assert.strictEqual(ancestors.length, 3); - }); - return { BinaryExpression: spy }; - } + create: context => ({ + Program() { + assert.strictEqual(context.languageOptions.sourceType, "module"); + } + }) } } } @@ -10655,27 +7499,20 @@ describe("Linter with FlatConfigArray", () => { rules: { "test/checker": "error" } }; - linter.verify(code, config, filename, true); - assert(spy && spy.calledOnce); + linter.verify("import foo from 'bar'", config, filename); }); - it("should retrieve empty ancestors for root node", () => { - let spy; - + it("should default to commonjs when passed a .cjs filename", () => { const config = { plugins: { test: { rules: { checker: { - create(context) { - spy = sinon.spy(() => { - const ancestors = context.getAncestors(); - - assert.strictEqual(ancestors.length, 0); - }); - - return { Program: spy }; - } + create: context => ({ + Program() { + assert.strictEqual(context.languageOptions.sourceType, "commonjs"); + } + }) } } } @@ -10683,110 +7520,272 @@ describe("Linter with FlatConfigArray", () => { rules: { "test/checker": "error" } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + linter.verify("import foo from 'bar'", config, `${filename}.cjs`); }); - }); - 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 {}; + it("should error when import is used in a script", () => { + const messages = linter.verify("import foo from 'bar';", { + languageOptions: { + ecmaVersion: 6, + sourceType: "script" + } }); - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } - } - } + 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" + } + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0, "There should no linting errors."); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should error when return is used at the top-level outside of commonjs", () => { + const messages = linter.verify("return", { + languageOptions: { + ecmaVersion: 6, + sourceType: "script" + } + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1, "There should be one parsing error."); + assert.strictEqual(messages[0].message, "Parsing error: 'return' outside of function"); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should not error when top-level return is used in commonjs", () => { + const messages = linter.verify("return", { + languageOptions: { + ecmaVersion: 6, + sourceType: "commonjs" + } + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0, "There should no linting errors."); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should error when accessing a Node.js global outside of commonjs", () => { + const messages = linter.verify("require()", { + languageOptions: { + ecmaVersion: 6 }, - rules: { "test/checker": "error" } - }; + rules: { + "no-undef": "error" + } + }); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify(code, config); - assert(spy.calledOnce); + 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."); + + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve a node containing the given index", () => { - const spy = sinon.spy(context => { - assert.strictEqual(context.getNodeByRangeIndex(6).type, "Identifier"); - return {}; + 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" + } }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0, "There should be no linting errors."); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should allow 'await' as a property name in modules", () => { + const result = linter.verify( + "obj.await", + { + languageOptions: { + ecmaVersion: 6, + sourceType: "module" + } + } + ); + const suppressedMessages = linter.getSuppressedMessages(); + + assert(result.length === 0); + assert.strictEqual(suppressedMessages.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 = { + languageOptions: { + parser + } + }; + + + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should pass parser as context.languageOptions.parser to all rules when provided on config", () => { const config = { plugins: { test: { rules: { - checker: { create: spy } + "test-rule": { + create: sinon.mock().withArgs( + sinon.match({ languageOptions: { parser: esprima } }) + ).returns({}) + } } } }, - rules: { "test/checker": "error" } + languageOptions: { + parser: esprima + }, + rules: { + "test/test-rule": 2 + } }; - linter.verify(code, config); - assert(spy.calledOnce); + linter.verify("0", config, filename); + }); + + it("should use parseForESLint() in custom parser when custom parser is specified", () => { + const config = { + languageOptions: { + parser: testParsers.enhancedParser + } + }; + + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - 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 {}; - }); + it("should expose parser services when using parseForESLint() and services are specified", () => { const config = { plugins: { test: { rules: { - checker: { create: spy } + "test-service-rule": { + create: context => ({ + Literal(node) { + context.report({ + node, + message: context.sourceCode.parserServices.test.getMessage() + }); + } + }) + } } } }, - rules: { "test/checker": "error" } + languageOptions: { + parser: testParsers.enhancedParser + }, + rules: { + "test/test-service-rule": 2 + } }; - linter.verify(code, config); - assert(spy.calledOnce); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); + + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve a node ending with the given index", () => { - const spy = sinon.spy(context => { - assert.strictEqual(context.getNodeByRangeIndex(9).type, "Identifier"); - return {}; - }); + it("should use the same parserServices if source code object is reused", () => { const config = { plugins: { test: { rules: { - checker: { create: spy } + "test-service-rule": { + create: context => ({ + Literal(node) { + context.report({ + node, + message: context.sourceCode.parserServices.test.getMessage() + }); + } + }) + } } } }, - rules: { "test/checker": "error" } + languageOptions: { + parser: testParsers.enhancedParser + }, + rules: { + "test/test-service-rule": 2 + } }; - linter.verify(code, config); - assert(spy.calledOnce); - }); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - it("should retrieve the deepest node containing the given index", () => { - const spy = sinon.spy(context => { - const node1 = context.getNodeByRangeIndex(14); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); + assert.strictEqual(suppressedMessages.length, 0); + + const messages2 = linter.verify(linter.getSourceCode(), config, filename); + const suppressedMessages2 = linter.getSuppressedMessages(); + + assert.strictEqual(messages2.length, 1); + assert.strictEqual(messages2[0].message, "Hi!"); + assert.strictEqual(suppressedMessages2.length, 0); + }); - assert.strictEqual(node1.type, "BinaryExpression"); + it("should pass parser as context.languageOptions.parser to all rules when default parser is used", () => { - const node2 = context.getNodeByRangeIndex(3); + // references to Espree get messed up in a browser context, so wrap it + const fakeParser = { + parse: espree.parse + }; - assert.strictEqual(node2.type, "VariableDeclaration"); + const spy = sinon.spy(context => { + assert.strictEqual(context.languageOptions.parser, fakeParser); return {}; }); @@ -10794,1271 +7793,1334 @@ describe("Linter with FlatConfigArray", () => { plugins: { test: { rules: { - checker: { create: spy } + "test-rule": { create: spy } } } }, - rules: { "test/checker": "error" } + languageOptions: { + parser: fakeParser + }, + rules: { + "test/test-rule": 2 + } }; - linter.verify(code, config); - assert(spy.calledOnce); + linter.verify("0", config, filename); + assert.isTrue(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); + describe("Custom Parsers", () => { - const node2 = context.getNodeByRangeIndex(-99); + const errorPrefix = "Parsing error: "; - assert.isNull(node2); - return {}; + it("should have file path passed to it", () => { + const code = "/* this is code */"; + const parseSpy = { parse: sinon.spy() }; + const config = { + languageOptions: { + parser: parseSpy + } + }; + + linter.verify(code, config, filename, true); + + sinon.assert.calledWithMatch(parseSpy.parse, "", { filePath: filename }); }); - const config = { - plugins: { - test: { - rules: { - checker: { create: spy } + it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const config = { + languageOptions: { + parser: esprima, + parserOptions: { + jsx: true } } - }, - rules: { "test/checker": "error" } - }; + }; - linter.verify(code, config); - assert(spy.calledOnce); - }); - }); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - describe("context.getScope()", () => { - const codeToTestScope = "function foo() { q: for(;;) { break q; } } function bar () { var q = t; } var baz = (() => { return 1; });"; + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - it("should retrieve the global scope correctly from a Program", () => { - let spy; + 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 + } + }; - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + // This shouldn't throw + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(scope.type, "global"); - }); - return { Program: spy }; + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.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); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.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": { + create: () => ({ + "*"(node) { + nodes.push(node.type); + } + }) } } } + }, + languageOptions: { + parser: testParsers.nonJSParser + }, + rules: { + "test/collect-node-types": "error" } - }, - languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; + }; - linter.verify(codeToTestScope, config); - assert(spy && spy.calledOnce); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - it("should retrieve the function scope correctly from a FunctionDeclaration", () => { - let spy; + assert.strictEqual(messages.length, 0); + assert.isTrue(nodes.length > 0); + assert.strictEqual(suppressedMessages.length, 0); + }); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + it("should strip leading line: prefix from parser error", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.lineError + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should not modify a parser error message without a leading line: prefix", () => { + const messages = linter.verify(";", { + languageOptions: { + parser: testParsers.noLineError + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + 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": { + create: () => ({ + "*"(node) { + types.push(node.type); + } + }) + }, + "save-scope-manager": { + create(context) { + scopeManager = context.sourceCode.scopeManager; - assert.strictEqual(scope.type, "function"); - }); - return { FunctionDeclaration: spy }; + return {}; + } + }, + "esquery-option": { + create: () => ({ + ":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" } - } - }, - languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; + }; - linter.verify(codeToTestScope, config); - assert(spy && spy.calledTwice); - }); + linter.verify("@foo class A {}", config); - it("should retrieve the function scope correctly from a LabeledStatement", () => { - let spy; + sourceCode = linter.getSourceCode(); + }); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { + assert.deepStrictEqual( + types, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block.id.name, "foo"); - }); - return { LabeledStatement: spy }; + 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": { + create: () => ({ + "*"(node) { + types2.push(node.type); + } + }) + } } } + }, + rules: { + "test/collect-node-types": "error" } - } - }, - languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; - + }; - linter.verify(codeToTestScope, config); - assert(spy && spy.calledOnce); - }); + linter.verify(sourceCode, config); - it("should retrieve the function scope correctly from within an ArrowFunctionExpression", () => { - let spy; + assert.deepStrictEqual( + types2, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + it("esquery should use the visitorKeys (so 'visitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { + assert.deepStrictEqual( + firstChildNodes, + [sourceCode.ast.body[0], sourceCode.ast.body[0].experimentalDecorators[0]] + ); + }); + }); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block.type, "ArrowFunctionExpression"); - }); + describe("if a parser provides 'scope'", () => { + let scope = null; + let sourceCode = null; - return { ReturnStatement: spy }; + beforeEach(() => { + const config = { + plugins: { + test: { + rules: { + "save-scope1": { + create: context => ({ + Program(node) { + scope = context.sourceCode.getScope(node); + } + }) + } } } + }, + languageOptions: { + parser: testParsers.enhancedParser3 + }, + rules: { + "test/save-scope1": "error" } - } - }, - 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; + linter.verify("@foo class A {}", config); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + sourceCode = linter.getSourceCode(); + }); - assert.strictEqual(scope.type, "switch"); - assert.strictEqual(scope.block.type, "SwitchStatement"); - }); + 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" + ); + }); - return { SwitchStatement: spy }; + it("should use the same scope if the source code object is reused", () => { + let scope2 = null; + const config = { + plugins: { + test: { + rules: { + "save-scope2": { + create: context => ({ + Program(node) { + scope2 = context.sourceCode.getScope(node); + } + }) + } } } + }, + rules: { + "test/save-scope2": "error" } - } - }, - languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; + }; - linter.verify("switch(foo){ case 'a': var b = 'foo'; }", config); - assert(spy && spy.calledOnce); - }); + linter.verify(sourceCode, config, "test.js"); - it("should retrieve the function scope correctly from within a BlockStatement", () => { - let spy; + assert(scope2 !== null); + assert(scope2 === scope); + }); + }); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + it("should pass default languageOptions to the parser", () => { - assert.strictEqual(scope.type, "block"); - assert.strictEqual(scope.block.type, "BlockStatement"); - }); + const spy = sinon.spy((code, options) => espree.parse(code, options)); - return { BlockStatement: spy }; - } - } + linter.verify(";", { + languageOptions: { + parser: { + parse: spy } } - }, - languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; - + }, "filename.js"); - linter.verify("var x; {let y = 1}", config); - assert(spy && spy.calledOnce); + assert(spy.calledWithMatch(";", { + ecmaVersion: espree.latestEcmaVersion + 2009, + sourceType: "module" + })); + }); }); - it("should retrieve the function scope correctly from within a nested block statement", () => { - let spy; - - const config = { - plugins: { - test: { - rules: { - checker: { - create(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" } - }; + }); + describe("parseOptions", () => { - linter.verify("if (true) { let x = 1 }", config); - assert(spy && spy.calledOnce); - }); + it("should pass ecmaFeatures to all rules when provided on config", () => { - it("should retrieve the function scope correctly from within a FunctionDeclaration", () => { - let spy; + const parserOptions = { + ecmaFeatures: { + jsx: true + } + }; const config = { plugins: { test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); - - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block.type, "FunctionDeclaration"); - }); - - return { FunctionDeclaration: spy }; - } + rules: { + "test-rule": { + create: sinon.mock().withArgs( + sinon.match({ languageOptions: { parserOptions } }) + ).returns({}) } } } }, languageOptions: { - ecmaVersion: 6 + parserOptions }, - rules: { "test/checker": "error" } + rules: { + "test/test-rule": 2 + } }; - - linter.verify("function foo() {}", config); - assert(spy && spy.calledOnce); + linter.verify("0", config, filename); }); - it("should retrieve the function scope correctly from within a FunctionExpression", () => { - let spy; + it("should switch globalReturn to false if sourceType is module", () => { const config = { plugins: { test: { rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); - - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block.type, "FunctionExpression"); - }); - - return { FunctionExpression: spy }; - } + "test-rule": { + create: sinon.mock().withArgs( + sinon.match({ + languageOptions: { + parserOptions: { + ecmaFeatures: { + globalReturn: false + } + } + } + }) + ).returns({}) } } } }, languageOptions: { - ecmaVersion: 6 + sourceType: "module", + parserOptions: { + ecmaFeatures: { + globalReturn: true + } + } }, - rules: { "test/checker": "error" } + rules: { + "test/test-rule": 2 + } }; - - linter.verify("(function foo() {})();", config); - assert(spy && spy.calledOnce); + linter.verify("0", config, filename); }); - it("should retrieve the catch scope correctly from within a CatchClause", () => { - let spy; + it("should not parse sloppy-mode code when impliedStrict is true", () => { - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + const messages = linter.verify("var private;", { + languageOptions: { + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + } + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(scope.type, "catch"); - assert.strictEqual(scope.block.type, "CatchClause"); - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Parsing error: The keyword 'private' is reserved"); - return { CatchClause: spy }; - } - } + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should properly parse valid code when impliedStrict is true", () => { + + const messages = linter.verify("var foo;", { + languageOptions: { + parserOptions: { + ecmaFeatures: { + impliedStrict: true } } - }, + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should properly parse JSX when passed ecmaFeatures", () => { + + const messages = linter.verify("var x =
;", { languageOptions: { - ecmaVersion: 6 - }, - rules: { "test/checker": "error" } - }; + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify("try {} catch (err) {}", config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve module scope correctly from an ES6 module", () => { - let spy; + it("should report an error when JSX code is encountered and JSX is not enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, {}, filename); + const suppressedMessages = linter.getSuppressedMessages(); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + 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 <"); - assert.strictEqual(scope.type, "module"); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - return { AssignmentExpression: spy }; - } - } + it("should not report an error when JSX code is encountered and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, { + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true } } - }, + } + }, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, { languageOptions: { ecmaVersion: 6, - sourceType: "module" - }, - rules: { "test/checker": "error" } - }; + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + }, "filename.js"); + const suppressedMessages = linter.getSuppressedMessages(); - linter.verify("var foo = {}; foo.bar = 1;", config); - assert(spy && spy.calledOnce); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should retrieve function scope correctly when sourceType is commonjs", () => { - let spy; + it("should not allow the use of reserved words as variable names in ES3", () => { + const code = "var char;"; + const messages = linter.verify(code, { + languageOptions: { + ecmaVersion: 3, + sourceType: "script" + } + }, filename); - const config = { - plugins: { - test: { - rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + 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); + }); - assert.strictEqual(scope.type, "function"); - }); + 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, { + languageOptions: { + ecmaVersion: 3, + sourceType: "script" + } + }, filename); - return { AssignmentExpression: spy }; - } - } - } - } - }, + 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, { languageOptions: { - ecmaVersion: 6, - sourceType: "commonjs" - }, - rules: { "test/checker": "error" } - }; + ecmaVersion: 3, + sourceType: "script" + } + }, filename); - linter.verify("var foo = {}; foo.bar = 1;", config); - assert(spy && spy.calledOnce); + 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); }); - describe("Scope Internals", () => { + 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, { + languageOptions: { + ecmaVersion: 3, + sourceType: "script", + parserOptions: { + allowReserved: true + } + } + }, filename); + + assert.strictEqual(messages.length, 0); + }); - /** - * 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; + 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)]; - const config = { - plugins: { - test: { - rules: { - "get-scope": { - create: context => ({ - [astSelector](node0) { - node = node0; - scope = context.getScope(); - } - }) - } - } - } - }, + ecmaVersions.forEach(ecmaVersion => { + const code = "var enum;"; + const messages = linter.verify(code, { languageOptions: { - ecmaVersion, + ...(ecmaVersion ? { ecmaVersion } : {}), sourceType: "script" - }, - rules: { "test/get-scope": "error" } - }; + } + }, filename); - linter.verify( - codeToEvaluate, - config - ); + 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); + }); + }); - return { node, scope }; - } + it("should allow the use of reserved words as property names in ES > 3", () => { + const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)]; - it("should return 'function' scope on FunctionDeclaration (ES5)", () => { - const { node, scope } = getScope("function f() {}", "FunctionDeclaration"); + ecmaVersions.forEach(ecmaVersion => { + const code = "obj.enum; obj.function; var obj = { enum: 1, function: 2 };"; + const messages = linter.verify(code, { + languageOptions: { + ...(ecmaVersion ? { ecmaVersion } : {}), + sourceType: "script" + } + }, filename); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node); + assert.strictEqual(messages.length, 0); }); + }); - 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 not allow `allowReserved: true` in ES > 3", () => { + const ecmaVersions = [void 0, ...espree.supportedEcmaVersions.filter(ecmaVersion => ecmaVersion > 3)]; - it("should return 'function' scope on the body of FunctionDeclaration (ES5)", () => { - const { node, scope } = getScope("function f() {}", "BlockStatement"); + ecmaVersions.forEach(ecmaVersion => { + const code = ""; + const messages = linter.verify(code, { + languageOptions: { + ...(ecmaVersion ? { ecmaVersion } : {}), + sourceType: "script", + parserOptions: { + allowReserved: true + } + } + }, filename); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node.parent); + 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 return 'function' scope on the body of FunctionDeclaration (ES2015)", () => { - const { node, scope } = getScope("function f() {}", "BlockStatement", 2015); + describe("settings", () => { + const ruleId = "test-rule"; - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node.parent); - }); + it("should pass settings to all rules", () => { - it("should return 'function' scope on BlockStatement in functions (ES5)", () => { - const { node, scope } = getScope("function f() { { var b; } }", "BlockStatement > BlockStatement"); + const config = { + plugins: { + test: { + rules: { + [ruleId]: { + create: context => ({ + Literal(node) { + context.report(node, context.settings.info); + } + }) + } + } + } + }, + settings: { + info: "Hello" + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node.parent.parent); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]); - }); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - 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(messages.length, 1); + assert.strictEqual(messages[0].message, "Hello"); - 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"]); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - 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); + it("should not have any settings if they were not passed in", () => { - 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"]); - }); + const config = { + plugins: { + test: { + rules: { + [ruleId]: { + create: context => ({ + Literal(node) { + if (Object.getOwnPropertyNames(context.settings).length !== 0) { + context.report(node, "Settings should be empty"); + } + } + }) + } + } + } + }, + settings: { + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; - it("should return 'function' scope on SwitchStatement in functions (ES5)", () => { - const { node, scope } = getScope("function f() { switch (a) { case 0: var b; } }", "SwitchStatement"); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node.parent.parent); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - it("should return 'switch' scope on SwitchStatement in functions (ES2015)", () => { - const { node, scope } = getScope("function f() { switch (a) { case 0: let b; } }", "SwitchStatement", 2015); + describe("rules", () => { + const code = "var answer = 6 * 7"; - assert.strictEqual(scope.type, "switch"); - assert.strictEqual(scope.block, node); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["b"]); - }); + it("should be configurable by only setting the integer value", () => { + const rule = "semi", + config = { rules: {} }; - it("should return 'function' scope on SwitchCase in functions (ES5)", () => { - const { node, scope } = getScope("function f() { switch (a) { case 0: var b; } }", "SwitchCase"); + config.rules[rule] = 1; - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node.parent.parent.parent); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "b"]); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - 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(messages.length, 1); + assert.strictEqual(messages[0].ruleId, rule); - assert.strictEqual(scope.type, "switch"); - assert.strictEqual(scope.block, node.parent); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["b"]); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - it("should return 'catch' scope on CatchClause in functions (ES5)", () => { - const { node, scope } = getScope("function f() { try {} catch (e) { var a; } }", "CatchClause"); + it("should be configurable by only setting the string value", () => { + const rule = "semi", + config = { rules: {} }; - assert.strictEqual(scope.type, "catch"); - assert.strictEqual(scope.block, node); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]); - }); + config.rules[rule] = "warn"; - it("should return 'catch' scope on CatchClause in functions (ES2015)", () => { - const { node, scope } = getScope("function f() { try {} catch (e) { let a; } }", "CatchClause", 2015); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(scope.type, "catch"); - assert.strictEqual(scope.block, node); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]); - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 1); + assert.strictEqual(messages[0].ruleId, rule); - 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(suppressedMessages.length, 0); + }); - assert.strictEqual(scope.type, "catch"); - assert.strictEqual(scope.block, node.parent); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["e"]); - }); + it("should be configurable by passing in values as an array", () => { + const rule = "semi", + config = { rules: {} }; - 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); + config.rules[rule] = [1]; - assert.strictEqual(scope.type, "block"); - assert.strictEqual(scope.block, node); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["a"]); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - 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(messages.length, 1); + assert.strictEqual(messages[0].ruleId, rule); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node.parent.parent); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "i"]); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - 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); + it("should be configurable by passing in string value as an array", () => { + const rule = "semi", + config = { rules: {} }; - assert.strictEqual(scope.type, "for"); - assert.strictEqual(scope.block, node); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["i"]); - }); + config.rules[rule] = ["warn"]; - 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"); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node.parent.parent.parent); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "i"]); - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 1); + assert.strictEqual(messages[0].ruleId, rule); - 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(suppressedMessages.length, 0); + }); - 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 not be configurable by setting other value", () => { + const rule = "semi", + config = { rules: {} }; - it("should return 'function' scope on ForInStatement in functions (ES5)", () => { - const { node, scope } = getScope("function f() { for (var key in obj) {} }", "ForInStatement"); + config.rules[rule] = "1"; - assert.strictEqual(scope.type, "function"); - assert.strictEqual(scope.block, node.parent.parent); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["arguments", "key"]); - }); + assert.throws(() => { + linter.verify(code, config, filename, true); + }, /Key "rules": Key "semi": Expected severity/u); + }); - it("should return 'for' scope on ForInStatement in functions (ES2015)", () => { - const { node, scope } = getScope("function f() { for (let key in obj) {} }", "ForInStatement", 2015); + it("should process empty config", () => { + const config = {}; + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(scope.type, "for"); - assert.strictEqual(scope.block, node); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["key"]); - }); + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); - 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"]); - }); + describe("verify()", () => { - 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); + it("should report warnings in order by line and column when called", () => { - 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"]); - }); + const code = "foo()\n alert('test')"; + const config = { rules: { "no-mixed-spaces-and-tabs": 1, "eol-last": 1, semi: [1, "always"] } }; - it("should return 'for' scope on ForOfStatement in functions (ES2015)", () => { - const { node, scope } = getScope("function f() { for (let x of xs) {} }", "ForOfStatement", 2015); + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(scope.type, "for"); - assert.strictEqual(scope.block, node); - assert.deepStrictEqual(scope.variables.map(v => v.name), ["x"]); - }); + 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); - 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(suppressedMessages.length, 0); + }); - 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 report ignored file when filename isn't matched in the config array", () => { - it("should shadow the same name variable by the iteration variable.", () => { - const { node, scope } = getScope("let x; for (let x of x) {}", "ForOfStatement", 2015); + const code = "foo()\n alert('test')"; + const config = { rules: { "no-mixed-spaces-and-tabs": 1, "eol-last": 1, semi: [1, "always"] } }; - 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]); - }); - }); + const messages = linter.verify(code, config, "filename.ts"); - 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; + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual(messages[0], { + ruleId: null, + severity: 1, + message: "No matching configuration found for filename.ts.", + line: 0, + column: 0, + nodeType: null + }); + }); - beforeEach(() => { - let ok = false; + // https://github.com/eslint/eslint/issues/17669 + it("should use `cwd` constructor option as config `basePath` when config is not an instance of FlatConfigArray", () => { + const rule = { + create(context) { + return { + Program(node) { + context.report({ node, message: "Bad program." }); + } + }; + } + }; - const config = { - plugins: { - test: { - rules: { - test: { - create: context => ({ - Program() { - scope = context.getScope(); - ok = true; - } - }) - } - } - } - }, - languageOptions: { - globals: { e: true, f: false }, - sourceType: "script", - ecmaVersion: 5 - }, + const code = "foo"; + const config = [ + { + plugins: { + test: { rules: { - "test/test": 2 + "test-rule-1": rule, + "test-rule-2": rule, + "test-rule-3": rule } - }; - - linter.verify(code, config); - assert(ok); - }); - - afterEach(() => { - scope = null; - }); + } + } + }, + { + rules: { + "test/test-rule-1": 2 + } + }, + { + files: ["**/*.ts"], + rules: { + "test/test-rule-2": 2 + } + }, + { + files: ["bar/file.ts"], + rules: { + "test/test-rule-3": 2 + } + } + ]; - 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); - }); + const linterWithOptions = new Linter({ + configType: "flat", + cwd: "/foo" + }); - 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")); - }); + let messages; - 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")); - }); + messages = linterWithOptions.verify(code, config, "/file.js"); + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual(messages[0], { + ruleId: null, + severity: 1, + message: "No matching configuration found for /file.js.", + line: 0, + column: 0, + nodeType: null + }); - 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")); - }); + messages = linterWithOptions.verify(code, config, "/file.ts"); + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual(messages[0], { + ruleId: null, + severity: 1, + message: "No matching configuration found for /file.ts.", + line: 0, + column: 0, + nodeType: null + }); - 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")); - }); - }); + messages = linterWithOptions.verify(code, config, "/bar/foo/file.js"); + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual(messages[0], { + ruleId: null, + severity: 1, + message: "No matching configuration found for /bar/foo/file.js.", + line: 0, + column: 0, + nodeType: null }); - describe("context.getDeclaredVariables(node)", () => { + messages = linterWithOptions.verify(code, config, "/bar/foo/file.ts"); + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual(messages[0], { + ruleId: null, + severity: 1, + message: "No matching configuration found for /bar/foo/file.ts.", + line: 0, + column: 0, + nodeType: null + }); - /** - * 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>} 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: { + messages = linterWithOptions.verify(code, config, "/foo/file.js"); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "test/test-rule-1"); - rules: { - test: { - create(context) { + messages = linterWithOptions.verify(code, config, "/foo/file.ts"); + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "test/test-rule-1"); + assert.strictEqual(messages[1].ruleId, "test/test-rule-2"); - /** - * 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 - }; + messages = linterWithOptions.verify(code, config, "/foo/bar/file.ts"); + assert.strictEqual(messages.length, 3); + assert.strictEqual(messages[0].ruleId, "test/test-rule-1"); + assert.strictEqual(messages[1].ruleId, "test/test-rule-2"); + assert.strictEqual(messages[2].ruleId, "test/test-rule-3"); + }); - rule[type] = function(node) { - const expectedNames = expectedNamesList.shift(); - const variables = context.getDeclaredVariables(node); + describe("Plugins", () => { - 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; - } + it("should not load rule definition when rule isn't used", () => { - } - } + const spy = sinon.spy(); + const config = { + plugins: { + test: { + rules: { + checker: { create: spy } } - }, - 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); - }); + linter.verify("code", config, filename); + assert.isTrue(spy.notCalled, "Rule should not have been called"); + }); + }); - it("VariableDeclaration (on for-in/of loop)", () => { + describe("Rule Internals", () => { - // 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"] - ]; + const code = TEST_CODE; - verify(code, "VariableDeclaration", namesList); - }); + it("should throw an error when an error occurs inside of a rule visitor", () => { + const config = { + plugins: { + test: { + rules: { + checker: { + create: () => ({ + Program() { + throw new Error("Intentional error."); + } - it("VariableDeclarator", () => { + }) + } + } + } + }, + rules: { "test/checker": "error" } + }; - // 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"] - ]; + assert.throws(() => { + linter.verify(code, config, filename); + }, `Intentional error.\nOccurred while linting ${filename}:1\nRule: "test/checker"`); + }); - verify(code, "VariableDeclarator", namesList); - }); + it("should not call rule visitor with a `this` value", () => { + const spy = sinon.spy(); + const config = { + plugins: { + test: { + rules: { + checker: { + create: () => ({ + Program: spy + }) + } + } + } + }, + rules: { "test/checker": "error" } + }; - 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"] - ]; + linter.verify("foo", config); + assert(spy.calledOnce); + assert.strictEqual(spy.firstCall.thisValue, void 0); + }); - verify(code, "FunctionDeclaration", namesList); - }); + it("should not call unrecognized rule visitor when present in a rule", () => { + const spy = sinon.spy(); + const config = { + plugins: { + test: { + rules: { + checker: { + create: () => ({ + newListener: spy + }) + } + } + } + }, + rules: { + "test/checker": "error", + "no-undef": "error" + } + }; - 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"] - ]; + linter.verify("foo", config); + assert(spy.notCalled); + }); - verify(code, "FunctionExpression", namesList); - }); + it("should have all the `parent` properties on nodes when the rule visitors are created", () => { + const spy = sinon.spy(context => { + assert.strictEqual(context.getSourceCode(), context.sourceCode); + const ast = context.sourceCode.ast; - 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"] - ]; + 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); - verify(code, "ArrowFunctionExpression", namesList); + return {}; }); - 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"] - ]; + const config = { + plugins: { + test: { + rules: { + checker: { create: spy } + } + } + }, + rules: { "test/checker": "error" } + }; - verify(code, "ClassDeclaration", namesList); - }); + linter.verify("foo + bar", config); + assert(spy.calledOnce); + }); - it("ClassExpression", () => { - const code = "\n (class A { foo(x) { let y; } });\n (class B { foo(x) { let y; } });\n "; - const namesList = [ - ["A"], - ["B"] - ]; + it("events for each node type should fire", () => { - verify(code, "ClassExpression", namesList); - }); + // spies for various AST node types + const spyLiteral = sinon.spy(), + spyVariableDeclarator = sinon.spy(), + spyVariableDeclaration = sinon.spy(), + spyIdentifier = sinon.spy(), + spyBinaryExpression = sinon.spy(); - 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"] - ]; + const config = { + plugins: { + test: { + rules: { + checker: { + create() { + return { + Literal: spyLiteral, + VariableDeclarator: spyVariableDeclarator, + VariableDeclaration: spyVariableDeclaration, + Identifier: spyIdentifier, + BinaryExpression: spyBinaryExpression + }; + } + } + } + } + }, + rules: { "test/checker": "error" } + }; - verify(code, "CatchClause", namesList); - }); + const messages = linter.verify(code, config, filename, true); + const suppressedMessages = linter.getSuppressedMessages(); - 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"] - ]; + 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); - verify(code, "ImportDeclaration", namesList); - }); + assert.strictEqual(suppressedMessages.length, 0); + }); - 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"] - ]; + it("should throw an error if a rule reports a problem without a message", () => { - verify(code, "ImportSpecifier", namesList); - }); + const config = { + plugins: { + test: { + rules: { + "invalid-report": { + create: context => ({ + Program(node) { + context.report({ node }); + } + }) + } + } + } + }, + rules: { "test/invalid-report": "error" } + }; - 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"] - ]; + assert.throws( + () => linter.verify("foo", config), + TypeError, + "Missing `message` property in report() call; add a message that describes the linting problem." + ); + }); - 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("Rule Context", () => { - describe("context.markVariableAsUsed()", () => { + describe("context.getFilename()", () => { + const ruleId = "filename-rule"; - it("should mark variables in current scope as used", () => { - const code = "var a = 1, b = 2;"; - let spy; + it("has access to the filename", () => { const config = { plugins: { test: { rules: { - checker: { - create(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 }; - } + [ruleId]: { + create: context => ({ + Literal(node) { + context.report(node, context.getFilename()); + } + }) } } } }, - languageOptions: { - sourceType: "script" - }, - rules: { "test/checker": "error" } + rules: { + [`test/${ruleId}`]: 1 + } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages[0].message, filename); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should mark variables in function args as used", () => { - const code = "function abc(a, b) { return 1; }"; - let spy; + it("defaults filename to ''", () => { const config = { plugins: { test: { rules: { - checker: { - create(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 }; - } + [ruleId]: { + create: context => ({ + Literal(node) { + context.report(node, context.getFilename()); + } + }) } } } }, - rules: { "test/checker": "error" } + rules: { + [`test/${ruleId}`]: 1 + } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + + const messages = linter.verify("0", config); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages[0].message, ""); + assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("should mark variables in higher scopes as used", () => { - const code = "var a, b; function abc() { return 1; }"; - let returnSpy, exitSpy; + describe("context.filename", () => { + const ruleId = "filename-rule"; + + it("has access to the filename", () => { const config = { plugins: { test: { rules: { - checker: { - create(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 }; - } + [ruleId]: { + create: context => ({ + Literal(node) { + assert.strictEqual(context.getFilename(), context.filename); + context.report(node, context.filename); + } + }) } } } }, - languageOptions: { - sourceType: "script" - }, - rules: { "test/checker": "error" } + rules: { + [`test/${ruleId}`]: 1 + } }; - linter.verify(code, config); - assert(returnSpy && returnSpy.calledOnce); - assert(exitSpy && exitSpy.calledOnce); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages[0].message, filename); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should mark variables as used when sourceType is commonjs", () => { - const code = "var a = 1, b = 2;"; - let spy; + it("defaults filename to ''", () => { const config = { plugins: { test: { rules: { - checker: { - create(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 }; - } + [ruleId]: { + create: context => ({ + Literal(node) { + assert.strictEqual(context.getFilename(), context.filename); + context.report(node, context.filename); + } + }) } } } }, - languageOptions: { - sourceType: "commonjs" - }, - rules: { "test/checker": "error" } + rules: { + [`test/${ruleId}`]: 1 + } }; - linter.verify(code, config); - assert(spy && spy.calledOnce, "Spy wasn't called."); + + const messages = linter.verify("0", config); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages[0].message, ""); + assert.strictEqual(suppressedMessages.length, 0); }); + }); - it("should mark variables in modules as used", () => { - const code = "var a = 1, b = 2;"; - let spy; + describe("context.getPhysicalFilename()", () => { + + const ruleId = "filename-rule"; + + it("has access to the physicalFilename", () => { const config = { plugins: { test: { rules: { - checker: { - create(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 }; - } + [ruleId]: { + create: context => ({ + Literal(node) { + context.report(node, context.getPhysicalFilename()); + } + }) } } } }, - languageOptions: { - ecmaVersion: 6, - sourceType: "module" - }, - rules: { "test/checker": "error" } + rules: { + [`test/${ruleId}`]: 1 + } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages[0].message, filename); + assert.strictEqual(suppressedMessages.length, 0); }); - it("should return false if the given variable is not found", () => { - const code = "var a = 1, b = 2;"; - let spy; + }); + + describe("context.physicalFilename", () => { + + const ruleId = "filename-rule"; + + it("has access to the physicalFilename", () => { const config = { plugins: { test: { rules: { - checker: { - create(context) { - spy = sinon.spy(() => { - assert.isFalse(context.markVariableAsUsed("c")); - }); - - return { "Program:exit": spy }; - } + [ruleId]: { + create: context => ({ + Literal(node) { + assert.strictEqual(context.getPhysicalFilename(), context.physicalFilename); + context.report(node, context.physicalFilename); + } + }) } } } }, - rules: { "test/checker": "error" } + rules: { + [`test/${ruleId}`]: 1 + } }; - linter.verify(code, config); - assert(spy && spy.calledOnce); + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages[0].message, filename); + assert.strictEqual(suppressedMessages.length, 0); }); + }); describe("context.getCwd()", () => { @@ -12307,7 +9369,7 @@ describe("Linter with FlatConfigArray", () => { it("should have a comment with the hashbang in it", () => { const spy = sinon.spy(context => { - const comments = context.getAllComments(); + const comments = context.sourceCode.getAllComments(); assert.strictEqual(comments.length, 1); assert.strictEqual(comments[0].type, "Shebang"); @@ -12525,8 +9587,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); const a = getVariable(scope, "a"), b = getVariable(scope, "b"), c = getVariable(scope, "c"), @@ -12584,8 +9646,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), a = getVariable(scope, "a"), b = getVariable(scope, "b"), c = getVariable(scope, "c"); @@ -12624,8 +9686,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); assert.strictEqual(getVariable(scope, "a"), null); }); @@ -12657,8 +9719,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); assert.strictEqual(getVariable(scope, "a"), null); assert.strictEqual(getVariable(scope, "b"), null); @@ -12690,8 +9752,8 @@ describe("Linter with FlatConfigArray", () => { rules: { test: { create: context => ({ - Program() { - const scope = context.getScope(); + Program(node) { + const scope = context.sourceCode.getScope(node); const sourceCode = context.sourceCode; const comments = sourceCode.getAllComments(); @@ -12781,8 +9843,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), horse = getVariable(scope, "horse"); assert.strictEqual(horse.eslintUsed, true); @@ -12813,8 +9875,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), horse = getVariable(scope, "horse"); assert.strictEqual(horse, null); @@ -12845,8 +9907,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), horse = getVariable(scope, "horse"); assert.strictEqual(horse.eslintUsed, true); @@ -12877,8 +9939,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), horse = getVariable(scope, "horse"); assert.strictEqual(horse, null); // there is no global scope at all @@ -12910,8 +9972,8 @@ describe("Linter with FlatConfigArray", () => { rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(), + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node), horse = getVariable(scope, "horse"); assert.strictEqual(horse, null); // there is no global scope at all @@ -15140,8 +12202,8 @@ var a = "test2"; rules: { test: { create: context => ({ - Program() { - const scope = context.getScope(); + Program(node) { + const scope = context.sourceCode.getScope(node); const sourceCode = context.sourceCode; const comments = sourceCode.getAllComments(); @@ -16982,8 +14044,8 @@ var a = "test2"; rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); assert.notStrictEqual(getVariable(scope, "Object"), null); assert.notStrictEqual(getVariable(scope, "Array"), null); @@ -17017,8 +14079,8 @@ var a = "test2"; rules: { checker: { create(context) { - spy = sinon.spy(() => { - const scope = context.getScope(); + spy = sinon.spy(node => { + const scope = context.sourceCode.getScope(node); assert.notStrictEqual(getVariable(scope, "Promise"), null); assert.notStrictEqual(getVariable(scope, "Symbol"), null); diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index 679a87b99da..882c8adf5da 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -1277,7 +1277,6 @@ describe("FlatRuleTester", () => { const disallowHiRule = { create: context => ({ Literal(node) { - assert.strictEqual(context.parserServices, context.sourceCode.parserServices); const disallowed = context.sourceCode.parserServices.test.getMessage(); // returns "Hi!" diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index a28b345501f..6530b670cee 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2482,117 +2482,6 @@ describe("RuleTester", () => { ); }); - it("should pass-through services from parseForESLint to the rule and log deprecation notice", () => { - const enhancedParserPath = require.resolve("../../fixtures/parsers/enhanced-parser"); - const disallowHiRule = { - create: context => ({ - Literal(node) { - assert.strictEqual(context.parserServices, context.sourceCode.parserServices); - - const disallowed = context.sourceCode.parserServices.test.getMessage(); // returns "Hi!" - - if (node.value === disallowed) { - context.report({ node, message: `Don't use '${disallowed}'` }); - } - } - }) - }; - - ruleTester.run("no-hi", disallowHiRule, { - valid: [ - { - code: "'Hello!'", - parser: enhancedParserPath - } - ], - invalid: [ - { - code: "'Hi!'", - parser: enhancedParserPath, - errors: [{ message: "Don't use 'Hi!'" }] - } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"no-hi\" rule is using `context.parserServices`, which is deprecated and will be removed in ESLint v9. Please use `sourceCode.parserServices` instead.", - "DeprecationWarning" - ] - ); - - }); - Object.entries({ - getSource: "getText", - getSourceLines: "getLines", - getAllComments: "getAllComments", - getNodeByRangeIndex: "getNodeByRangeIndex", - getCommentsBefore: "getCommentsBefore", - getCommentsAfter: "getCommentsAfter", - getCommentsInside: "getCommentsInside", - getJSDocComment: "getJSDocComment", - getFirstToken: "getFirstToken", - getFirstTokens: "getFirstTokens", - getLastToken: "getLastToken", - getLastTokens: "getLastTokens", - getTokenAfter: "getTokenAfter", - getTokenBefore: "getTokenBefore", - getTokenByRangeStart: "getTokenByRangeStart", - getTokens: "getTokens", - getTokensAfter: "getTokensAfter", - getTokensBefore: "getTokensBefore", - getTokensBetween: "getTokensBetween", - getScope: "getScope", - getAncestors: "getAncestors", - getDeclaredVariables: "getDeclaredVariables", - markVariableAsUsed: "markVariableAsUsed" - }).forEach(([methodName, replacementName]) => { - - it(`should log a deprecation warning when calling \`context.${methodName}\``, () => { - const ruleToCheckDeprecation = { - meta: { - type: "problem", - schema: [] - }, - create(context) { - return { - Program(node) { - - // special case - if (methodName === "getTokensBetween") { - context[methodName](node, node); - } else { - context[methodName](node); - } - - context.report({ node, message: "bad" }); - } - }; - } - }; - - ruleTester.run("deprecated-method", ruleToCheckDeprecation, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - `"deprecated-method" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${replacementName}()\` instead.`, - "DeprecationWarning" - ] - ); - }); - - }); - - }); /** diff --git a/tests/lib/source-code/source-code.js b/tests/lib/source-code/source-code.js index 763db27bc46..03a74080ac4 100644 --- a/tests/lib/source-code/source-code.js +++ b/tests/lib/source-code/source-code.js @@ -3646,10 +3646,10 @@ describe("SourceCode", () => { create(context) { const sourceCode = context.sourceCode; - spy = sinon.spy(() => { + spy = sinon.spy(node => { assert.isTrue(sourceCode.markVariableAsUsed("a")); - const scope = context.getScope(); + const scope = sourceCode.getScope(node); assert.isTrue(getVariable(scope, "a").eslintUsed); assert.notOk(getVariable(scope, "b").eslintUsed); @@ -3674,7 +3674,7 @@ describe("SourceCode", () => { spy = sinon.spy(node => { assert.isTrue(sourceCode.markVariableAsUsed("a", node)); - const scope = context.getScope(); + const scope = sourceCode.getScope(node); assert.isTrue(getVariable(scope, "a").eslintUsed); assert.notOk(getVariable(scope, "b").eslintUsed); @@ -3699,8 +3699,8 @@ describe("SourceCode", () => { returnSpy = sinon.spy(node => { assert.isTrue(sourceCode.markVariableAsUsed("a", node)); }); - exitSpy = sinon.spy(() => { - const scope = context.getScope(); + exitSpy = sinon.spy(node => { + const scope = sourceCode.getScope(node); assert.isTrue(getVariable(scope, "a").eslintUsed); assert.notOk(getVariable(scope, "b").eslintUsed); @@ -3723,8 +3723,8 @@ describe("SourceCode", () => { create(context) { const sourceCode = context.sourceCode; - spy = sinon.spy(() => { - const globalScope = context.getScope(), + spy = sinon.spy(node => { + const globalScope = sourceCode.getScope(node), childScope = globalScope.childScopes[0]; assert.isTrue(sourceCode.markVariableAsUsed("a")); @@ -3749,8 +3749,8 @@ describe("SourceCode", () => { create(context) { const sourceCode = context.sourceCode; - spy = sinon.spy(() => { - const globalScope = context.getScope(), + spy = sinon.spy(node => { + const globalScope = sourceCode.getScope(node), childScope = globalScope.childScopes[0]; assert.isTrue(sourceCode.markVariableAsUsed("a")); From c314fd612587c42cfbe6acbe286629c4178be3f7 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 20 Dec 2023 14:53:56 +0100 Subject: [PATCH 11/25] feat!: Remove `SourceCode#getComments()` (#17715) Fixes #14744 --- docs/src/extend/custom-rules.md | 1 - lib/rule-tester/flat-rule-tester.js | 15 +- lib/rule-tester/rule-tester.js | 15 +- lib/source-code/source-code.js | 78 -- tests/lib/rule-tester/flat-rule-tester.js | 33 - tests/lib/rule-tester/rule-tester.js | 34 - tests/lib/source-code/source-code.js | 838 ---------------------- 7 files changed, 2 insertions(+), 1012 deletions(-) diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index f196100c3b6..bc7d936f28f 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -867,7 +867,6 @@ ESLint analyzes code paths while traversing AST. You can access code path object Please note that the following `SourceCode` methods have been deprecated and will be removed in a future version of ESLint: -* `getComments()`: Replaced by `SourceCode#getCommentsBefore()`, `SourceCode#getCommentsAfter()`, and `SourceCode#getCommentsInside()`. * `getTokenOrCommentBefore()`: Replaced by `SourceCode#getTokenBefore()` with the `{ includeComments: true }` option. * `getTokenOrCommentAfter()`: Replaced by `SourceCode#getTokenAfter()` with the `{ includeComments: true }` option. * `isSpaceBetweenTokens()`: Replaced by `SourceCode#isSpaceBetween()` diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 51cb73b5f80..01163c48e4d 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -274,17 +274,6 @@ function wrapParser(parser) { }; } -/** - * Function to replace `SourceCode.prototype.getComments`. - * @returns {void} - * @throws {Error} Deprecation message. - */ -function getCommentsDeprecation() { - throw new Error( - "`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead." - ); -} - /** * Emit a deprecation warning if rule uses CodePath#currentSegments. * @param {string} ruleName Name of the rule. @@ -727,12 +716,11 @@ class FlatRuleTester { } // Verify the code. - const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); let messages; try { - SourceCode.prototype.getComments = getCommentsDeprecation; Object.defineProperty(CodePath.prototype, "currentSegments", { get() { emitCodePathCurrentSegmentsWarning(ruleName); @@ -746,7 +734,6 @@ class FlatRuleTester { messages = linter.verify(code, configs, filename); } finally { - SourceCode.prototype.getComments = getComments; Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index ad410094f4e..626d9dcd3ce 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -301,17 +301,6 @@ function wrapParser(parser) { }; } -/** - * Function to replace `SourceCode.prototype.getComments`. - * @returns {void} - * @throws {Error} Deprecation message. - */ -function getCommentsDeprecation() { - throw new Error( - "`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead." - ); -} - /** * Function to replace forbidden `SourceCode` methods. * @param {string} methodName The name of the method to forbid. @@ -720,12 +709,11 @@ class RuleTester { validate(config, "rule-tester", id => (id === ruleName ? rule : null)); // Verify the code. - const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); let messages; try { - SourceCode.prototype.getComments = getCommentsDeprecation; Object.defineProperty(CodePath.prototype, "currentSegments", { get() { emitCodePathCurrentSegmentsWarning(ruleName); @@ -739,7 +727,6 @@ class RuleTester { messages = linter.verify(code, config, filename); } finally { - SourceCode.prototype.getComments = getComments; Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; diff --git a/lib/source-code/source-code.js b/lib/source-code/source-code.js index 236f6b5c6cc..ce8d89e7843 100644 --- a/lib/source-code/source-code.js +++ b/lib/source-code/source-code.js @@ -420,9 +420,6 @@ class SourceCode extends TokenStore { } this.lines.push(this.text.slice(this.lineStartIndices[this.lineStartIndices.length - 1])); - // Cache for comments found using getComments(). - this._commentCache = new WeakMap(); - // don't allow further modification of this object Object.freeze(this); Object.freeze(this.lines); @@ -472,81 +469,6 @@ class SourceCode extends TokenStore { return this.ast.comments; } - /** - * Gets all comments for the given node. - * @param {ASTNode} node The AST node to get the comments for. - * @returns {Object} An object containing a leading and trailing array - * of comments indexed by their position. - * @public - * @deprecated replaced by getCommentsBefore(), getCommentsAfter(), and getCommentsInside(). - */ - getComments(node) { - if (this._commentCache.has(node)) { - return this._commentCache.get(node); - } - - const comments = { - leading: [], - trailing: [] - }; - - /* - * Return all comments as leading comments of the Program node when - * there is no executable code. - */ - if (node.type === "Program") { - if (node.body.length === 0) { - comments.leading = node.comments; - } - } else { - - /* - * Return comments as trailing comments of nodes that only contain - * comments (to mimic the comment attachment behavior present in Espree). - */ - if ((node.type === "BlockStatement" || node.type === "ClassBody") && node.body.length === 0 || - node.type === "ObjectExpression" && node.properties.length === 0 || - node.type === "ArrayExpression" && node.elements.length === 0 || - node.type === "SwitchStatement" && node.cases.length === 0 - ) { - comments.trailing = this.getTokens(node, { - includeComments: true, - filter: isCommentToken - }); - } - - /* - * Iterate over tokens before and after node and collect comment tokens. - * Do not include comments that exist outside of the parent node - * to avoid duplication. - */ - let currentToken = this.getTokenBefore(node, { includeComments: true }); - - while (currentToken && isCommentToken(currentToken)) { - if (node.parent && node.parent.type !== "Program" && (currentToken.start < node.parent.start)) { - break; - } - comments.leading.push(currentToken); - currentToken = this.getTokenBefore(currentToken, { includeComments: true }); - } - - comments.leading.reverse(); - - currentToken = this.getTokenAfter(node, { includeComments: true }); - - while (currentToken && isCommentToken(currentToken)) { - if (node.parent && node.parent.type !== "Program" && (currentToken.end > node.parent.end)) { - break; - } - comments.trailing.push(currentToken); - currentToken = this.getTokenAfter(currentToken, { includeComments: true }); - } - } - - this._commentCache.set(node, comments); - return comments; - } - /** * Retrieves the JSDoc comment for a given node. * @param {ASTNode} node The AST node to get the comment for. diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index 882c8adf5da..c6d0730b3c6 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -2664,39 +2664,6 @@ describe("FlatRuleTester", () => { }); - describe("SourceCode#getComments()", () => { - const useGetCommentsRule = { - create: context => ({ - Program(node) { - const sourceCode = context.sourceCode; - - sourceCode.getComments(node); - } - }) - }; - - it("should throw if called from a valid test case", () => { - assert.throws(() => { - ruleTester.run("use-get-comments", useGetCommentsRule, { - valid: [""], - invalid: [] - }); - }, /`SourceCode#getComments\(\)` is deprecated/u); - }); - - it("should throw if called from an invalid test case", () => { - assert.throws(() => { - ruleTester.run("use-get-comments", useGetCommentsRule, { - valid: [], - invalid: [{ - code: "", - errors: [{}] - }] - }); - }, /`SourceCode#getComments\(\)` is deprecated/u); - }); - }); - describe("SourceCode forbidden methods", () => { [ diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 6530b670cee..23a748bc536 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2784,40 +2784,6 @@ describe("RuleTester", () => { }); - describe("SourceCode#getComments()", () => { - const useGetCommentsRule = { - create: context => ({ - Program(node) { - const sourceCode = context.sourceCode; - - sourceCode.getComments(node); - } - }) - }; - - it("should throw if called from a valid test case", () => { - assert.throws(() => { - ruleTester.run("use-get-comments", useGetCommentsRule, { - valid: [""], - invalid: [] - }); - }, /`SourceCode#getComments\(\)` is deprecated/u); - }); - - it("should throw if called from an invalid test case", () => { - assert.throws(() => { - ruleTester.run("use-get-comments", useGetCommentsRule, { - valid: [], - invalid: [{ - code: "", - errors: [{}] - }] - }); - }, /`SourceCode#getComments\(\)` is deprecated/u); - }); - }); - - describe("SourceCode forbidden methods", () => { [ diff --git a/tests/lib/source-code/source-code.js b/tests/lib/source-code/source-code.js index 03a74080ac4..6d3e07a2f54 100644 --- a/tests/lib/source-code/source-code.js +++ b/tests/lib/source-code/source-code.js @@ -1053,844 +1053,6 @@ describe("SourceCode", () => { }); - describe("getComments()", () => { - const config = { rules: { checker: "error" }, parserOptions: { ecmaVersion: 6, sourceType: "module" } }; - let unusedAssertionFuncs; - - - beforeEach(() => { - unusedAssertionFuncs = new Set(); - }); - - /** - * Check comment count - * @param {int} leading Leading comment count - * @param {int} trailing Trailing comment count - * @returns {Function} function to execute - * @private - */ - function assertCommentCount(leading, trailing) { - - /** - * Asserts the comment count for a node - * @param {ASTNode} node the node being traversed - * @returns {void} - */ - function assertionFunc(node) { - unusedAssertionFuncs.delete(assertionFunc); - const sourceCode = linter.getSourceCode(); - const comments = sourceCode.getComments(node); - - assert.strictEqual(comments.leading.length, leading); - assert.strictEqual(comments.trailing.length, trailing); - } - unusedAssertionFuncs.add(assertionFunc); - return assertionFunc; - } - - afterEach(() => { - assert.strictEqual( - unusedAssertionFuncs.size, - 0, - "An assertion function was created with assertCommentCount, but the function was not called." - ); - }); - - it("should return comments around nodes", () => { - const code = [ - "// Leading comment for VariableDeclaration", - "var a = 42;", - "/* Trailing comment for VariableDeclaration */" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - VariableDeclaration: assertCommentCount(1, 1), - VariableDeclarator: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - Literal: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return trailing comments inside a block", () => { - const code = [ - "{", - " a();", - " // Trailing comment for ExpressionStatement", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - ExpressionStatement: assertCommentCount(0, 1), - CallExpression: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments within a conditional", () => { - const code = [ - "/* Leading comment for IfStatement */", - "if (/* Leading comment for Identifier */ a) {}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - IfStatement: assertCommentCount(1, 0), - Identifier: assertCommentCount(1, 0), - BlockStatement: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should not return comments within a previous node", () => { - const code = [ - "function a() {", - " var b = {", - " // Trailing comment for ObjectExpression", - " };", - " return b;", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - VariableDeclaration: assertCommentCount(0, 0), - VariableDeclarator: assertCommentCount(0, 0), - ObjectExpression: assertCommentCount(0, 1), - ReturnStatement: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments only for children of parent node", () => { - const code = [ - "var foo = {", - " bar: 'bar'", - " // Trailing comment for Property", - "};", - "var baz;" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - VariableDeclaration: assertCommentCount(0, 0), - VariableDeclarator: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - ObjectExpression: assertCommentCount(0, 0), - Property: assertCommentCount(0, 1), - Literal: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments for an export default anonymous class", () => { - const code = [ - "/**", - " * Leading comment for ExportDefaultDeclaration", - " */", - "export default class {", - " /**", - " * Leading comment for MethodDefinition", - " */", - " method1(){", - " }", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - ExportDefaultDeclaration: assertCommentCount(1, 0), - ClassDeclaration: assertCommentCount(0, 0), - ClassBody: assertCommentCount(0, 0), - MethodDefinition: assertCommentCount(1, 0), - Identifier: assertCommentCount(0, 0), - FunctionExpression: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return leading comments", () => { - const code = [ - "// Leading comment for first VariableDeclaration", - "var a;", - "// Leading comment for previous VariableDeclaration and trailing comment for next VariableDeclaration", - "var b;" - ].join("\n"); - let varDeclCount = 0; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - - VariableDeclaration(node) { - if (varDeclCount === 0) { - assertCommentCount(1, 1)(node); - } else { - assertCommentCount(1, 0)(node); - } - varDeclCount++; - }, - - VariableDeclarator: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return shebang comments", () => { - const code = [ - "#!/usr/bin/env node", // Leading comment for following VariableDeclaration - "var a;", - "// Leading comment for previous VariableDeclaration and trailing comment for next VariableDeclaration", - "var b;" - ].join("\n"); - let varDeclCount = 0; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - - VariableDeclaration(node) { - if (varDeclCount === 0) { - assertCommentCount(1, 1)(node); - } else { - assertCommentCount(1, 0)(node); - } - varDeclCount++; - }, - - Identifier: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should include shebang comment when program only contains shebang", () => { - const code = "#!/usr/bin/env node"; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(1, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return mixture of line and block comments", () => { - const code = [ - "// Leading comment for VariableDeclaration", - "var zzz /* Trailing comment for Identifier */ = 777;", - "// Trailing comment for VariableDeclaration" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - VariableDeclaration: assertCommentCount(1, 1), - VariableDeclarator: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 1), - Literal: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments surrounding a call expression", () => { - const code = [ - "function a() {", - " /* Leading comment for ExpressionStatement */", - " foo();", - " /* Trailing comment for ExpressionStatement */", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - ExpressionStatement: assertCommentCount(1, 1), - CallExpression: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments surrounding a debugger statement", () => { - const code = [ - "function a() {", - " /* Leading comment for DebuggerStatement */", - " debugger;", - " /* Trailing comment for DebuggerStatement */", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - DebuggerStatement: assertCommentCount(1, 1) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments surrounding a return statement", () => { - const code = [ - "function a() {", - " /* Leading comment for ReturnStatement */", - " return;", - " /* Trailing comment for ReturnStatement */", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - ReturnStatement: assertCommentCount(1, 1) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments surrounding a throw statement", () => { - const code = [ - "function a() {", - " /* Leading comment for ThrowStatement */", - " throw 55;", - " /* Trailing comment for ThrowStatement */", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - ThrowStatement: assertCommentCount(1, 1) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments surrounding a while loop", () => { - const code = [ - "function f() {", - " /* Leading comment for WhileStatement */", - " while (true) {}", - " /* Trailing comment for WhileStatement and leading comment for VariableDeclaration */", - " var each;", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - WhileStatement: assertCommentCount(1, 1), - Literal: assertCommentCount(0, 0), - VariableDeclaration: assertCommentCount(1, 0), - VariableDeclarator: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return switch case fallthrough comments in functions", () => { - const code = [ - "function bar(foo) {", - " switch(foo) {", - " /* Leading comment for SwitchCase */", - " case 1:", - " // falls through", // Trailing comment for previous SwitchCase and leading comment for next SwitchCase - " case 2:", - " doIt();", - " }", - "}" - ].join("\n"); - let switchCaseCount = 0; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - SwitchStatement: assertCommentCount(0, 0), - - SwitchCase(node) { - if (switchCaseCount === 0) { - assertCommentCount(1, 1)(node); - } else { - assertCommentCount(1, 0)(node); - } - switchCaseCount++; - }, - - Literal: assertCommentCount(0, 0), - ExpressionStatement: assertCommentCount(0, 0), - CallExpression: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return switch case fallthrough comments", () => { - const code = [ - "switch(foo) {", - " /* Leading comment for SwitchCase */", - "case 1:", - " // falls through", // Trailing comment for previous SwitchCase and leading comment for next SwitchCase - "case 2:", - " doIt();", - "}" - ].join("\n"); - let switchCaseCount = 0; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - SwitchStatement: assertCommentCount(0, 0), - - SwitchCase(node) { - if (switchCaseCount === 0) { - assertCommentCount(1, 1)(node); - } else { - assertCommentCount(1, 0)(node); - } - switchCaseCount++; - }, - - Literal: assertCommentCount(0, 0), - ExpressionStatement: assertCommentCount(0, 0), - CallExpression: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return switch case no-default comments in functions", () => { - const code = [ - "function bar(a) {", - " switch (a) {", - " case 2:", - " break;", - " case 1:", - " break;", - " // no default", // Trailing comment for SwitchCase - " }", - "}" - ].join("\n"); - let breakStatementCount = 0; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - SwitchStatement: assertCommentCount(0, 0), - - SwitchCase(node) { - if (breakStatementCount === 0) { - assertCommentCount(0, 0)(node); - } else { - assertCommentCount(0, 1)(node); - } - breakStatementCount++; - }, - - BreakStatement: assertCommentCount(0, 0), - Literal: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return switch case no-default comments", () => { - const code = [ - "switch (a) {", - " case 1:", - " break;", - " // no default", // Trailing comment for SwitchCase - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - SwitchStatement: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - SwitchCase: assertCommentCount(0, 1), - BreakStatement: assertCommentCount(0, 0), - Literal: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return switch case no-default comments in nested functions", () => { - const code = [ - "module.exports = function(context) {", - " function isConstant(node) {", - " switch (node.type) {", - " case 'SequenceExpression':", - " return isConstant(node.expressions[node.expressions.length - 1]);", - " // no default", // Trailing comment for SwitchCase - " }", - " return false;", - " }", - "};" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - ExpressionStatement: assertCommentCount(0, 0), - AssignmentExpression: assertCommentCount(0, 0), - MemberExpression: assertCommentCount(0, 0), - Identifier: assertCommentCount(0, 0), - FunctionExpression: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(0, 0), - SwitchStatement: assertCommentCount(0, 0), - SwitchCase: assertCommentCount(0, 1), - ReturnStatement: assertCommentCount(0, 0), - CallExpression: assertCommentCount(0, 0), - BinaryExpression: assertCommentCount(0, 0), - Literal: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return leading comments if the code only contains comments", () => { - const code = [ - "//comment", - "/*another comment*/" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(2, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return trailing comments if a block statement only contains comments", () => { - const code = [ - "{", - " //comment", - " /*another comment*/", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - BlockStatement: assertCommentCount(0, 2) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return trailing comments if a class body only contains comments", () => { - const code = [ - "class Foo {", - " //comment", - " /*another comment*/", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - ClassDeclaration: assertCommentCount(0, 0), - ClassBody: assertCommentCount(0, 2) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return trailing comments if an object only contains comments", () => { - const code = [ - "({", - " //comment", - " /*another comment*/", - "})" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - ExpressionStatement: assertCommentCount(0, 0), - ObjectExpression: assertCommentCount(0, 2) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return trailing comments if an array only contains comments", () => { - const code = [ - "[", - " //comment", - " /*another comment*/", - "]" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - ExpressionStatement: assertCommentCount(0, 0), - ArrayExpression: assertCommentCount(0, 2) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return trailing comments if a switch statement only contains comments", () => { - const code = [ - "switch (foo) {", - " //comment", - " /*another comment*/", - "}" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - SwitchStatement: assertCommentCount(0, 2), - Identifier: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments for multiple declarations with a single variable", () => { - const code = [ - "// Leading comment for VariableDeclaration", - "var a, // Leading comment for next VariableDeclarator", - " b, // Leading comment for next VariableDeclarator", - " c; // Trailing comment for VariableDeclaration", - "// Trailing comment for VariableDeclaration" - ].join("\n"); - let varDeclCount = 0; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - VariableDeclaration: assertCommentCount(1, 2), - - VariableDeclarator(node) { - if (varDeclCount === 0) { - assertCommentCount(0, 0)(node); - } else if (varDeclCount === 1) { - assertCommentCount(1, 0)(node); - } else { - assertCommentCount(1, 0)(node); - } - varDeclCount++; - }, - - Identifier: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return comments when comments exist between var keyword and VariableDeclarator", () => { - const code = [ - "var // Leading comment for VariableDeclarator", - " // Leading comment for VariableDeclarator", - " a;" - ].join("\n"); - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - VariableDeclaration: assertCommentCount(0, 0), - VariableDeclarator: assertCommentCount(2, 0), - Identifier: assertCommentCount(0, 0) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return attached comments between tokens to the correct nodes for empty function declarations", () => { - const code = "/* 1 */ function /* 2 */ foo(/* 3 */) /* 4 */ { /* 5 */ } /* 6 */"; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - FunctionDeclaration: assertCommentCount(1, 1), - Identifier: assertCommentCount(1, 0), - BlockStatement: assertCommentCount(1, 1) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return attached comments between tokens to the correct nodes for empty class declarations", () => { - const code = "/* 1 */ class /* 2 */ Foo /* 3 */ extends /* 4 */ Bar /* 5 */ { /* 6 */ } /* 7 */"; - let idCount = 0; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - ClassDeclaration: assertCommentCount(1, 1), - - Identifier(node) { - if (idCount === 0) { - assertCommentCount(1, 1)(node); - } else { - assertCommentCount(1, 1)(node); - } - idCount++; - }, - - ClassBody: assertCommentCount(1, 1) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - - it("should return attached comments between tokens to the correct nodes for empty switch statements", () => { - const code = "/* 1 */ switch /* 2 */ (/* 3 */ foo /* 4 */) /* 5 */ { /* 6 */ } /* 7 */"; - - linter.defineRule("checker", { - create() { - return ({ - Program: assertCommentCount(0, 0), - SwitchStatement: assertCommentCount(1, 6), - Identifier: assertCommentCount(1, 1) - }); - } - }); - - assert.isEmpty(linter.verify(code, config)); - }); - }); - describe("getLines()", () => { it("should get proper lines when using \\n as a line break", () => { From c2cf85a7447777e6b499cbb5c49de919bb5c817f Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 20 Dec 2023 15:02:14 +0100 Subject: [PATCH 12/25] feat!: drop support for string configurations in flat config array (#17717) Drops support for "eslint:recommended" and "eslint:all" string configurations in flat config arrays. Fixes #17488 --- lib/config/flat-config-array.js | 20 ------------- tests/lib/config/flat-config-array.js | 41 ++++----------------------- 2 files changed, 6 insertions(+), 55 deletions(-) diff --git a/lib/config/flat-config-array.js b/lib/config/flat-config-array.js index 689dc429f50..6103374dcd6 100644 --- a/lib/config/flat-config-array.js +++ b/lib/config/flat-config-array.js @@ -13,7 +13,6 @@ const { ConfigArray, ConfigArraySymbol } = require("@humanwhocodes/config-array" const { flatConfigSchema } = require("./flat-config-schema"); const { RuleValidator } = require("./rule-validator"); const { defaultConfig } = require("./default-config"); -const jsPlugin = require("@eslint/js"); //----------------------------------------------------------------------------- // Helpers @@ -134,25 +133,6 @@ class FlatConfigArray extends ConfigArray { * @returns {Object} The preprocessed config. */ [ConfigArraySymbol.preprocessConfig](config) { - if (config === "eslint:recommended") { - - // if we are in a Node.js environment warn the user - if (typeof process !== "undefined" && process.emitWarning) { - process.emitWarning("The 'eslint:recommended' string configuration is deprecated and will be replaced by the @eslint/js package's 'recommended' config."); - } - - return jsPlugin.configs.recommended; - } - - if (config === "eslint:all") { - - // if we are in a Node.js environment warn the user - if (typeof process !== "undefined" && process.emitWarning) { - process.emitWarning("The 'eslint:all' string configuration is deprecated and will be replaced by the @eslint/js package's 'all' config."); - } - - return jsPlugin.configs.all; - } /* * If `shouldIgnore` is false, we remove any ignore patterns specified diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 79f80bce425..6bc5a92278d 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -11,10 +11,6 @@ const { FlatConfigArray } = require("../../../lib/config/flat-config-array"); const assert = require("chai").assert; -const { - all: allConfig, - recommended: recommendedConfig -} = require("@eslint/js").configs; const stringify = require("json-stable-stringify-without-jsonify"); const espree = require("espree"); @@ -127,24 +123,6 @@ async function assertInvalidConfig(values, message) { }, message); } -/** - * Normalizes the rule configs to an array with severity to match - * how Flat Config merges rule options. - * @param {Object} rulesConfig The rules config portion of a config. - * @returns {Array} The rules config object. - */ -function normalizeRuleConfig(rulesConfig) { - const rulesConfigCopy = { - ...rulesConfig - }; - - for (const ruleId of Object.keys(rulesConfigCopy)) { - rulesConfigCopy[ruleId] = [2]; - } - - return rulesConfigCopy; -} - //----------------------------------------------------------------------------- // Tests //----------------------------------------------------------------------------- @@ -630,24 +608,17 @@ describe("FlatConfigArray", () => { }); - describe("Special configs", () => { - it("eslint:recommended is replaced with an actual config", async () => { - const configs = new FlatConfigArray(["eslint:recommended"]); - - await configs.normalize(); - const config = configs.getConfig("foo.js"); + describe("Config array elements", () => { + it("should error on 'eslint:recommended' string config", async () => { - assert.deepStrictEqual(config.rules, normalizeRuleConfig(recommendedConfig.rules)); + await assertInvalidConfig(["eslint:recommended"], "All arguments must be objects."); }); - it("eslint:all is replaced with an actual config", async () => { - const configs = new FlatConfigArray(["eslint:all"]); + it("should error on 'eslint:all' string config", async () => { - await configs.normalize(); - const config = configs.getConfig("foo.js"); - - assert.deepStrictEqual(config.rules, normalizeRuleConfig(allConfig.rules)); + await assertInvalidConfig(["eslint:all"], "All arguments must be objects."); }); + }); describe("Config Properties", () => { From 6ee3e9eb5df7bdfdaa1746214793ed511112be76 Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Wed, 20 Dec 2023 15:12:28 +0100 Subject: [PATCH 13/25] feat!: Update `eslint:recommended` configuration (#17716) Adds new rules no-constant-binary-expression, no-empty-static-block and no-unused-private-class-members. Removes deprecated rules no-extra-semi and no-mixed-spaces-and-tabs. Fixes #17596 --- lib/rules/no-constant-binary-expression.js | 2 +- lib/rules/no-empty-static-block.js | 2 +- lib/rules/no-extra-semi.js | 2 +- lib/rules/no-mixed-spaces-and-tabs.js | 2 +- lib/rules/no-unused-private-class-members.js | 2 +- packages/eslint-config-eslint/base.js | 1 + packages/eslint-config-eslint/eslintrc.js | 1 + packages/js/src/configs/eslint-recommended.js | 5 +++-- 8 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-constant-binary-expression.js b/lib/rules/no-constant-binary-expression.js index 845255a0cc2..1c0e39fdcbc 100644 --- a/lib/rules/no-constant-binary-expression.js +++ b/lib/rules/no-constant-binary-expression.js @@ -440,7 +440,7 @@ module.exports = { type: "problem", docs: { description: "Disallow expressions where the operation doesn't affect the value", - recommended: false, + recommended: true, url: "https://eslint.org/docs/latest/rules/no-constant-binary-expression" }, schema: [], diff --git a/lib/rules/no-empty-static-block.js b/lib/rules/no-empty-static-block.js index 81fc449b8cf..558c4fad455 100644 --- a/lib/rules/no-empty-static-block.js +++ b/lib/rules/no-empty-static-block.js @@ -15,7 +15,7 @@ module.exports = { docs: { description: "Disallow empty static blocks", - recommended: false, + recommended: true, url: "https://eslint.org/docs/latest/rules/no-empty-static-block" }, diff --git a/lib/rules/no-extra-semi.js b/lib/rules/no-extra-semi.js index af7eb888845..1daf2242249 100644 --- a/lib/rules/no-extra-semi.js +++ b/lib/rules/no-extra-semi.js @@ -26,7 +26,7 @@ module.exports = { docs: { description: "Disallow unnecessary semicolons", - recommended: true, + recommended: false, url: "https://eslint.org/docs/latest/rules/no-extra-semi" }, diff --git a/lib/rules/no-mixed-spaces-and-tabs.js b/lib/rules/no-mixed-spaces-and-tabs.js index 7698b5da7fa..18e6114a045 100644 --- a/lib/rules/no-mixed-spaces-and-tabs.js +++ b/lib/rules/no-mixed-spaces-and-tabs.js @@ -18,7 +18,7 @@ module.exports = { docs: { description: "Disallow mixed spaces and tabs for indentation", - recommended: true, + recommended: false, url: "https://eslint.org/docs/latest/rules/no-mixed-spaces-and-tabs" }, diff --git a/lib/rules/no-unused-private-class-members.js b/lib/rules/no-unused-private-class-members.js index 037be7d3eaa..bc05cd25180 100644 --- a/lib/rules/no-unused-private-class-members.js +++ b/lib/rules/no-unused-private-class-members.js @@ -16,7 +16,7 @@ module.exports = { docs: { description: "Disallow unused private class members", - recommended: false, + recommended: true, url: "https://eslint.org/docs/latest/rules/no-unused-private-class-members" }, diff --git a/packages/eslint-config-eslint/base.js b/packages/eslint-config-eslint/base.js index e7be7fc0de6..2e2ea40657a 100644 --- a/packages/eslint-config-eslint/base.js +++ b/packages/eslint-config-eslint/base.js @@ -81,6 +81,7 @@ const jsConfigs = [js.configs.recommended, { "no-eval": "error", "no-extend-native": "error", "no-extra-bind": "error", + "no-extra-semi": "error", "no-floating-decimal": "error", "no-implied-eval": "error", "no-invalid-this": "error", diff --git a/packages/eslint-config-eslint/eslintrc.js b/packages/eslint-config-eslint/eslintrc.js index 31ce7f252c0..81840082371 100644 --- a/packages/eslint-config-eslint/eslintrc.js +++ b/packages/eslint-config-eslint/eslintrc.js @@ -223,6 +223,7 @@ module.exports = { "no-eval": "error", "no-extend-native": "error", "no-extra-bind": "error", + "no-extra-semi": "error", "no-floating-decimal": "error", "no-implied-eval": "error", "no-invalid-this": "error", diff --git a/packages/js/src/configs/eslint-recommended.js b/packages/js/src/configs/eslint-recommended.js index 248c613caed..6109474f3a4 100644 --- a/packages/js/src/configs/eslint-recommended.js +++ b/packages/js/src/configs/eslint-recommended.js @@ -20,6 +20,7 @@ module.exports = Object.freeze({ "no-compare-neg-zero": "error", "no-cond-assign": "error", "no-const-assign": "error", + "no-constant-binary-expression": "error", "no-constant-condition": "error", "no-control-regex": "error", "no-debugger": "error", @@ -32,9 +33,9 @@ module.exports = Object.freeze({ "no-empty": "error", "no-empty-character-class": "error", "no-empty-pattern": "error", + "no-empty-static-block": "error", "no-ex-assign": "error", "no-extra-boolean-cast": "error", - "no-extra-semi": "error", "no-fallthrough": "error", "no-func-assign": "error", "no-global-assign": "error", @@ -44,7 +45,6 @@ module.exports = Object.freeze({ "no-irregular-whitespace": "error", "no-loss-of-precision": "error", "no-misleading-character-class": "error", - "no-mixed-spaces-and-tabs": "error", "no-new-symbol": "error", "no-nonoctal-decimal-escape": "error", "no-obj-calls": "error", @@ -64,6 +64,7 @@ module.exports = Object.freeze({ "no-unsafe-negation": "error", "no-unsafe-optional-chaining": "error", "no-unused-labels": "error", + "no-unused-private-class-members": "error", "no-unused-vars": "error", "no-useless-backreference": "error", "no-useless-catch": "error", From e5f02c70084c4f80900c0875b08f665e1f030af2 Mon Sep 17 00:00:00 2001 From: MHO <17177411+mho22@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:27:16 +0100 Subject: [PATCH 14/25] fix!: no-sequences rule schema correction (#17878) chore: no-sequences rule schema correction --- lib/rules/no-sequences.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rules/no-sequences.js b/lib/rules/no-sequences.js index cd21fc7842c..5822a24542f 100644 --- a/lib/rules/no-sequences.js +++ b/lib/rules/no-sequences.js @@ -35,6 +35,7 @@ module.exports = { }, schema: [{ + type: "object", properties: { allowInParentheses: { type: "boolean", From e563c52e35d25f726d423cc3b1dffcd80027fd99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 20 Dec 2023 09:37:26 -0500 Subject: [PATCH 15/25] feat!: `no-invalid-regexp` make allowConstructorFlags case-sensitive (#17533) * feat!: `no-invalid-regexp` make allowConstructorFlags case-sensitive * Mention case sensitivity in docs --- docs/src/rules/no-invalid-regexp.md | 2 +- lib/rules/no-invalid-regexp.js | 2 +- tests/lib/rules/no-invalid-regexp.js | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/src/rules/no-invalid-regexp.md b/docs/src/rules/no-invalid-regexp.md index 565b13cdaa8..8ad2a5fbabc 100644 --- a/docs/src/rules/no-invalid-regexp.md +++ b/docs/src/rules/no-invalid-regexp.md @@ -53,7 +53,7 @@ If you want to allow additional constructor flags for any reason, you can specif This rule has an object option for exceptions: -* `"allowConstructorFlags"` is an array of flags +* `"allowConstructorFlags"` is a case-sensitive array of flags ### allowConstructorFlags diff --git a/lib/rules/no-invalid-regexp.js b/lib/rules/no-invalid-regexp.js index 3c42a68e8a3..99fa6295af2 100644 --- a/lib/rules/no-invalid-regexp.js +++ b/lib/rules/no-invalid-regexp.js @@ -55,7 +55,7 @@ module.exports = { const temp = options.allowConstructorFlags.join("").replace(validFlags, ""); if (temp) { - allowedFlags = new RegExp(`[${temp}]`, "giu"); + allowedFlags = new RegExp(`[${temp}]`, "gu"); } } diff --git a/tests/lib/rules/no-invalid-regexp.js b/tests/lib/rules/no-invalid-regexp.js index 063cf2ef713..3fd444a991a 100644 --- a/tests/lib/rules/no-invalid-regexp.js +++ b/tests/lib/rules/no-invalid-regexp.js @@ -183,6 +183,24 @@ ruleTester.run("no-invalid-regexp", rule, { type: "NewExpression" }] }, + { + code: "RegExp('.', 'a');", + options: [{ allowConstructorFlags: ["A"] }], + errors: [{ + messageId: "regexMessage", + data: { message: "Invalid flags supplied to RegExp constructor 'a'" }, + type: "CallExpression" + }] + }, + { + code: "RegExp('.', 'A');", + options: [{ allowConstructorFlags: ["a"] }], + errors: [{ + messageId: "regexMessage", + data: { message: "Invalid flags supplied to RegExp constructor 'A'" }, + type: "CallExpression" + }] + }, { code: "new RegExp('.', 'az');", options: [{ allowConstructorFlags: ["z"] }], From b3e0bb03cc814e78b06a1acc4e5347b4c90d72bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 20 Dec 2023 11:23:31 -0500 Subject: [PATCH 16/25] feat!: assert suggestion messages are unique in rule testers (#17532) * feat!: assert suggestion messages are unique in rule testers * Apply suggestions from code review Co-authored-by: Milos Djermanovic * Remove unnecessary . addition * Remove unused getSuggestionMessage functions * Update tests/lib/rule-tester/flat-rule-tester.js Co-authored-by: Milos Djermanovic --------- Co-authored-by: Milos Djermanovic --- lib/rule-tester/flat-rule-tester.js | 16 ++++ lib/rule-tester/rule-tester.js | 16 ++++ .../testers/rule-tester/suggestions.js | 92 +++++++++++++++++++ tests/lib/rule-tester/flat-rule-tester.js | 33 +++++++ tests/lib/rule-tester/rule-tester.js | 33 +++++++ 5 files changed, 190 insertions(+) diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 01163c48e4d..c790dab69e7 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -853,6 +853,22 @@ class FlatRuleTester { const result = runRuleForItem(item); const messages = result.messages; + for (const message of messages) { + if (hasOwnProperty(message, "suggestions")) { + + /** @type {Map} */ + const seenMessageIndices = new Map(); + + for (let i = 0; i < message.suggestions.length; i += 1) { + const suggestionMessage = message.suggestions[i].desc; + const previous = seenMessageIndices.get(suggestionMessage); + + assert.ok(!seenMessageIndices.has(suggestionMessage), `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`); + seenMessageIndices.set(suggestionMessage, i); + } + } + } + if (typeof item.errors === "number") { if (item.errors === 0) { diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 626d9dcd3ce..bd46fcc807a 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -845,6 +845,22 @@ class RuleTester { const result = runRuleForItem(item); const messages = result.messages; + for (const message of messages) { + if (hasOwnProperty(message, "suggestions")) { + + /** @type {Map} */ + const seenMessageIndices = new Map(); + + for (let i = 0; i < message.suggestions.length; i += 1) { + const suggestionMessage = message.suggestions[i].desc; + const previous = seenMessageIndices.get(suggestionMessage); + + assert.ok(!seenMessageIndices.has(suggestionMessage), `Suggestion message '${suggestionMessage}' reported from suggestion ${i} was previously reported by suggestion ${previous}. Suggestion messages should be unique within an error.`); + seenMessageIndices.set(suggestionMessage, i); + } + } + } + if (typeof item.errors === "number") { if (item.errors === 0) { diff --git a/tests/fixtures/testers/rule-tester/suggestions.js b/tests/fixtures/testers/rule-tester/suggestions.js index ecedde04637..57cb84f1653 100644 --- a/tests/fixtures/testers/rule-tester/suggestions.js +++ b/tests/fixtures/testers/rule-tester/suggestions.js @@ -59,6 +59,98 @@ module.exports.withMessageIds = { } }; +module.exports.withDuplicateDescriptions = { + meta: { + hasSuggestions: true + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + message: "Avoid using identifiers name 'foo'.", + suggest: [{ + desc: "Rename 'foo' to 'bar'", + fix: fixer => fixer.replaceText(node, "bar") + }, { + desc: "Rename 'foo' to 'bar'", + fix: fixer => fixer.replaceText(node, "baz") + }] + }); + } + } + }; + } +}; + +module.exports.withDuplicateMessageIdsNoData = { + meta: { + messages: { + avoidFoo: "Avoid using identifiers named '{{ name }}'.", + renameFoo: "Rename identifier" + }, + hasSuggestions: true + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + data: { + name: "foo" + }, + suggest: [{ + messageId: "renameFoo", + fix: fixer => fixer.replaceText(node, "bar") + }, { + messageId: "renameFoo", + fix: fixer => fixer.replaceText(node, "baz") + }] + }); + } + } + }; + } +}; + +module.exports.withDuplicateMessageIdsWithData = { + meta: { + messages: { + avoidFoo: "Avoid using identifiers named foo.", + renameFoo: "Rename identifier 'foo' to '{{ newName }}'" + }, + hasSuggestions: true + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + suggest: [{ + messageId: "renameFoo", + data: { + newName: "bar" + }, + fix: fixer => fixer.replaceText(node, "bar") + }, { + messageId: "renameFoo", + data: { + newName: "bar" + }, + fix: fixer => fixer.replaceText(node, "baz") + }] + }); + } + } + }; + } +}; + module.exports.withoutHasSuggestionsProperty = { create(context) { return { diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index c6d0730b3c6..fe97bd2c851 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -2313,6 +2313,39 @@ describe("FlatRuleTester", () => { }, /Invalid suggestion property name 'outpt'/u); }); + it("should fail if a rule produces two suggestions with the same description", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-descriptions", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateDescriptions, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId without data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-no-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsNoData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId with data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-with-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsWithData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => { assert.throws(() => { ruleTester.run("suggestions-missing-hasSuggestions-property", require("../../fixtures/testers/rule-tester/suggestions").withoutHasSuggestionsProperty, { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 23a748bc536..5af50dd7dea 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2187,6 +2187,39 @@ describe("RuleTester", () => { }, /Invalid suggestion property name 'outpt'/u); }); + it("should fail if a rule produces two suggestions with the same description", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-descriptions", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateDescriptions, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId without data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-no-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsNoData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId with data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-with-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsWithData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => { assert.throws(() => { ruleTester.run("suggestions-missing-hasSuggestions-property", require("../../fixtures/testers/rule-tester/suggestions").withoutHasSuggestionsProperty, { From 5aa9c499da48b2d3187270d5d8ece71ad7521f56 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Wed, 20 Dec 2023 11:36:18 -0500 Subject: [PATCH 17/25] feat!: check for parsing errors in suggestion fixes (#16639) * feat!: check for parsing errors in suggestion fixes * revert RuleTester changes --- lib/rule-tester/flat-rule-tester.js | 15 ++++++++- tests/lib/rule-tester/flat-rule-tester.js | 41 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index c790dab69e7..0a127c749a0 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -764,7 +764,9 @@ class FlatRuleTester { messages, output, beforeAST, - afterAST: cloneDeeplyExcludesParent(afterAST) + afterAST: cloneDeeplyExcludesParent(afterAST), + configs, + filename }; } @@ -1050,6 +1052,17 @@ class FlatRuleTester { if (hasOwnProperty(expectedSuggestion, "output")) { const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output; + // Verify if suggestion fix makes a syntax error or not. + const errorMessageInSuggestion = + linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal); + + assert(!errorMessageInSuggestion, [ + "A fatal parsing error occurred in suggestion fix.", + `Error: ${errorMessageInSuggestion && errorMessageInSuggestion.message}`, + "Suggestion output:", + codeWithAppliedSuggestion + ].join("\n")); + assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); } }); diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index fe97bd2c851..5f47ce1fe4b 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -2020,6 +2020,47 @@ describe("FlatRuleTester", () => { }, "Error should have 2 suggestions. Instead found 1 suggestions"); }); + it("should throw if suggestion fix made a syntax error.", () => { + assert.throw(() => { + ruleTester.run( + "foo", + { + meta: { hasSuggestions: true }, + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: "make a syntax error", + suggest: [ + { + desc: "make a syntax error", + fix(fixer) { + return fixer.replaceText(node, "one two"); + } + } + ] + }); + } + }; + } + }, + { + valid: [""], + invalid: [{ + code: "one()", + errors: [{ + suggestions: [{ + desc: "make a syntax error", + output: "one two()" + }] + }] + }] + } + ); + }, /A fatal parsing error occurred in suggestion fix\.\nError: .+\nSuggestion output:\n.+/u); + }); + it("should throw if the suggestion description doesn't match", () => { assert.throws(() => { ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { From 60dea3e3abd6c0b6aab25437b2d0501b0d30b70c Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Wed, 20 Dec 2023 23:29:36 +0100 Subject: [PATCH 18/25] feat!: deprecate no-new-symbol, recommend no-new-native-nonconstructor (#17710) * deprecate `no-new-symbol`, recommend `no-new-native-nonconstructor` * deprecated in ESLint v9.0.0 --------- Co-authored-by: Nicholas C. Zakas --- docs/src/_data/rules.json | 13 ++++++------- docs/src/_data/rules_meta.json | 10 +++++++--- docs/src/rules/no-new-symbol.md | 2 +- lib/rules/no-new-native-nonconstructor.js | 2 +- lib/rules/no-new-symbol.js | 9 ++++++++- packages/js/src/configs/eslint-all.js | 1 - packages/js/src/configs/eslint-recommended.js | 2 +- 7 files changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/src/_data/rules.json b/docs/src/_data/rules.json index 3fe9ddcc3d5..b2502bfedbf 100644 --- a/docs/src/_data/rules.json +++ b/docs/src/_data/rules.json @@ -228,13 +228,6 @@ { "name": "no-new-native-nonconstructor", "description": "Disallow `new` operators with global non-constructor functions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-new-symbol", - "description": "Disallow `new` operators with the `Symbol` object", "recommended": true, "fixable": false, "hasSuggestions": false @@ -1737,6 +1730,12 @@ "fixable": false, "hasSuggestions": false }, + { + "name": "no-new-symbol", + "replacedBy": [ + "no-new-native-nonconstructor" + ] + }, { "name": "no-path-concat", "replacedBy": [], diff --git a/docs/src/_data/rules_meta.json b/docs/src/_data/rules_meta.json index 06db19be509..7f310da9f23 100644 --- a/docs/src/_data/rules_meta.json +++ b/docs/src/_data/rules_meta.json @@ -1433,7 +1433,7 @@ "type": "problem", "docs": { "description": "Disallow `new` operators with global non-constructor functions", - "recommended": false, + "recommended": true, "url": "https://eslint.org/docs/latest/rules/no-new-native-nonconstructor" } }, @@ -1463,9 +1463,13 @@ "type": "problem", "docs": { "description": "Disallow `new` operators with the `Symbol` object", - "recommended": true, + "recommended": false, "url": "https://eslint.org/docs/latest/rules/no-new-symbol" - } + }, + "deprecated": true, + "replacedBy": [ + "no-new-native-nonconstructor" + ] }, "no-new-wrappers": { "type": "suggestion", diff --git a/docs/src/rules/no-new-symbol.md b/docs/src/rules/no-new-symbol.md index d557811f30c..2be75ee9e24 100644 --- a/docs/src/rules/no-new-symbol.md +++ b/docs/src/rules/no-new-symbol.md @@ -6,7 +6,7 @@ further_reading: - https://www.ecma-international.org/ecma-262/6.0/#sec-symbol-objects --- - +This rule was **deprecated** in ESLint v9.0.0 and replaced by the [no-new-native-nonconstructor](no-new-native-nonconstructor) rule. `Symbol` is not intended to be used with the `new` operator, but to be called as a function. diff --git a/lib/rules/no-new-native-nonconstructor.js b/lib/rules/no-new-native-nonconstructor.js index ee70d281d75..699390dd5a1 100644 --- a/lib/rules/no-new-native-nonconstructor.js +++ b/lib/rules/no-new-native-nonconstructor.js @@ -22,7 +22,7 @@ module.exports = { docs: { description: "Disallow `new` operators with global non-constructor functions", - recommended: false, + recommended: true, url: "https://eslint.org/docs/latest/rules/no-new-native-nonconstructor" }, diff --git a/lib/rules/no-new-symbol.js b/lib/rules/no-new-symbol.js index 99830220244..85f9e62a6d7 100644 --- a/lib/rules/no-new-symbol.js +++ b/lib/rules/no-new-symbol.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to disallow use of the new operator with the `Symbol` object * @author Alberto Rodríguez + * @deprecated in ESLint v9.0.0 */ "use strict"; @@ -16,10 +17,16 @@ module.exports = { docs: { description: "Disallow `new` operators with the `Symbol` object", - recommended: true, + recommended: false, url: "https://eslint.org/docs/latest/rules/no-new-symbol" }, + deprecated: true, + + replacedBy: [ + "no-new-native-nonconstructor" + ], + schema: [], messages: { diff --git a/packages/js/src/configs/eslint-all.js b/packages/js/src/configs/eslint-all.js index f2f7a664af6..2bf3564e453 100644 --- a/packages/js/src/configs/eslint-all.js +++ b/packages/js/src/configs/eslint-all.js @@ -114,7 +114,6 @@ module.exports = Object.freeze({ "no-new": "error", "no-new-func": "error", "no-new-native-nonconstructor": "error", - "no-new-symbol": "error", "no-new-wrappers": "error", "no-nonoctal-decimal-escape": "error", "no-obj-calls": "error", diff --git a/packages/js/src/configs/eslint-recommended.js b/packages/js/src/configs/eslint-recommended.js index 6109474f3a4..81b2750d84f 100644 --- a/packages/js/src/configs/eslint-recommended.js +++ b/packages/js/src/configs/eslint-recommended.js @@ -45,7 +45,7 @@ module.exports = Object.freeze({ "no-irregular-whitespace": "error", "no-loss-of-precision": "error", "no-misleading-character-class": "error", - "no-new-symbol": "error", + "no-new-native-nonconstructor": "error", "no-nonoctal-decimal-escape": "error", "no-obj-calls": "error", "no-octal": "error", From c7eca43202be98f6ff253b46c9a38602eeb92ea0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:59:03 -0500 Subject: [PATCH 19/25] chore: update dependency markdownlint-cli to ^0.38.0 (#17865) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 473da26112f..00a045572b9 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "markdown-it": "^12.2.0", "markdown-it-container": "^3.0.0", "markdownlint": "^0.32.0", - "markdownlint-cli": "^0.37.0", + "markdownlint-cli": "^0.38.0", "marked": "^4.0.8", "memfs": "^3.0.1", "metascraper": "^5.25.7", From e501ab9b9f1955822c45dced841849dc214b6604 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 20 Dec 2023 18:19:51 -0500 Subject: [PATCH 20/25] feat!: Remove CodePath#currentSegments (#17756) fixes #17457 --- docs/src/extend/code-path-analysis.md | 3 +- lib/linter/code-path-analysis/code-path.js | 14 --- lib/rule-tester/flat-rule-tester.js | 31 ++---- lib/rule-tester/rule-tester.js | 42 ++++--- .../code-path-analysis/code-path-analyzer.js | 31 ------ tests/lib/rule-tester/flat-rule-tester.js | 39 ------- tests/lib/rule-tester/rule-tester.js | 104 ++++++++++++++++-- 7 files changed, 132 insertions(+), 132 deletions(-) diff --git a/docs/src/extend/code-path-analysis.md b/docs/src/extend/code-path-analysis.md index 87911957490..36d827bee77 100644 --- a/docs/src/extend/code-path-analysis.md +++ b/docs/src/extend/code-path-analysis.md @@ -37,7 +37,6 @@ This has references of both the initial segment and the final segments of a code * `finalSegments` (`CodePathSegment[]`) - The final segments which includes both returned and thrown. * `returnedSegments` (`CodePathSegment[]`) - The final segments which includes only returned. * `thrownSegments` (`CodePathSegment[]`) - The final segments which includes only thrown. -* `currentSegments` (`CodePathSegment[]`) - **Deprecated.** Segments of the current traversal position. * `upper` (`CodePath|null`) - The code path of the upper function/global scope. * `childCodePaths` (`CodePath[]`) - Code paths of functions this code path contains. @@ -635,7 +634,7 @@ last(); ``` If there is not `catch` block, `finally` block has two current segments. -At this time, `CodePath.currentSegments.length` is `2`. +At this time when running the previous example to find unreachable nodes, `currentSegments.length` is `2`. One is the normal path, and another is the leaving path (`throw` or `return`). :::img-container diff --git a/lib/linter/code-path-analysis/code-path.js b/lib/linter/code-path-analysis/code-path.js index 3bf570d754b..90cafef68f0 100644 --- a/lib/linter/code-path-analysis/code-path.js +++ b/lib/linter/code-path-analysis/code-path.js @@ -125,20 +125,6 @@ class CodePath { return this.internal.thrownForkContext; } - /** - * Tracks the traversal of the code path through each segment. This array - * starts empty and segments are added or removed as the code path is - * traversed. This array always ends up empty at the end of a code path - * traversal. The `CodePathState` uses this to track its progress through - * the code path. - * This is a passthrough to the underlying `CodePathState`. - * @type {CodePathSegment[]} - * @deprecated - */ - get currentSegments() { - return this.internal.currentSegments; - } - /** * Traverses all segments in this code path. * diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 0a127c749a0..5e08eab93fd 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -16,8 +16,7 @@ const equal = require("fast-deep-equal"), Traverser = require("../shared/traverser"), { getRuleOptionsSchema } = require("../config/flat-config-helpers"), - { Linter, SourceCodeFixer, interpolate } = require("../linter"), - CodePath = require("../linter/code-path-analysis/code-path"); + { Linter, SourceCodeFixer, interpolate } = require("../linter"); const { FlatConfigArray } = require("../config/flat-config-array"); const { defaultConfig } = require("../config/default-config"); @@ -275,18 +274,14 @@ function wrapParser(parser) { } /** - * Emit a deprecation warning if rule uses CodePath#currentSegments. - * @param {string} ruleName Name of the rule. + * Function to replace `SourceCode.prototype.getComments`. * @returns {void} + * @throws {Error} Deprecation message. */ -function emitCodePathCurrentSegmentsWarning(ruleName) { - if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { - emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; - process.emitWarning( - `"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`, - "DeprecationWarning" - ); - } +function getCommentsDeprecation() { + throw new Error( + "`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead." + ); } /** @@ -716,17 +711,11 @@ class FlatRuleTester { } // Verify the code. - const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; - const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); + const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; let messages; try { - Object.defineProperty(CodePath.prototype, "currentSegments", { - get() { - emitCodePathCurrentSegmentsWarning(ruleName); - return originalCurrentSegments.get.call(this); - } - }); + SourceCode.prototype.getComments = getCommentsDeprecation; forbiddenMethods.forEach(methodName => { SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype); @@ -734,7 +723,7 @@ class FlatRuleTester { messages = linter.verify(code, configs, filename); } finally { - Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); + SourceCode.prototype.getComments = getComments; SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; SourceCode.prototype.finalize = finalize; diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index bd46fcc807a..74ca7c11493 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -48,8 +48,7 @@ const equal = require("fast-deep-equal"), Traverser = require("../../lib/shared/traverser"), { getRuleOptionsSchema, validate } = require("../shared/config-validator"), - { Linter, SourceCodeFixer, interpolate } = require("../linter"), - CodePath = require("../linter/code-path-analysis/code-path"); + { Linter, SourceCodeFixer, interpolate } = require("../linter"); const ajv = require("../shared/ajv")({ strictDefaults: true }); @@ -345,20 +344,37 @@ function emitMissingSchemaWarning(ruleName) { } /** - * Emit a deprecation warning if rule uses CodePath#currentSegments. + * Emit a deprecation warning if a rule uses a deprecated `context` method. * @param {string} ruleName Name of the rule. + * @param {string} methodName The name of the method on `context` that was used. * @returns {void} */ -function emitCodePathCurrentSegmentsWarning(ruleName) { - if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { - emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; +function emitDeprecatedContextMethodWarning(ruleName, methodName) { + if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) { + emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true; process.emitWarning( - `"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`, + `"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`, "DeprecationWarning" ); } } +/** + * Emit a deprecation warning if `context.parserServices` is used. + * @param {string} ruleName Name of the rule. + * @returns {void} + */ +function emitParserServicesWarning(ruleName) { + if (!emitParserServicesWarning[`warned-${ruleName}`]) { + emitParserServicesWarning[`warned-${ruleName}`] = true; + process.emitWarning( + `"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`, + "DeprecationWarning" + ); + } +} + + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -709,17 +725,11 @@ class RuleTester { validate(config, "rule-tester", id => (id === ruleName ? rule : null)); // Verify the code. - const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; - const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); + const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; let messages; try { - Object.defineProperty(CodePath.prototype, "currentSegments", { - get() { - emitCodePathCurrentSegmentsWarning(ruleName); - return originalCurrentSegments.get.call(this); - } - }); + SourceCode.prototype.getComments = getCommentsDeprecation; forbiddenMethods.forEach(methodName => { SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName); @@ -727,7 +737,7 @@ class RuleTester { messages = linter.verify(code, config, filename); } finally { - Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); + SourceCode.prototype.getComments = getComments; SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; SourceCode.prototype.finalize = finalize; diff --git a/tests/lib/linter/code-path-analysis/code-path-analyzer.js b/tests/lib/linter/code-path-analysis/code-path-analyzer.js index dbed9b46194..e580c14d134 100644 --- a/tests/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/tests/lib/linter/code-path-analysis/code-path-analyzer.js @@ -137,37 +137,6 @@ describe("CodePathAnalyzer", () => { assert(actual[1].thrownSegments[0] instanceof CodePathSegment); }); - it("should have `currentSegments` as CodePathSegment[]", () => { - assert(Array.isArray(actual[0].currentSegments)); - assert(Array.isArray(actual[1].currentSegments)); - assert(actual[0].currentSegments.length === 0); - assert(actual[1].currentSegments.length === 0); - - // there is the current segment in progress. - linter.defineRule("test", { - create() { - let codePath = null; - - return { - onCodePathStart(cp) { - codePath = cp; - }, - ReturnStatement() { - assert(codePath.currentSegments.length === 1); - assert(codePath.currentSegments[0] instanceof CodePathSegment); - }, - ThrowStatement() { - assert(codePath.currentSegments.length === 1); - assert(codePath.currentSegments[0] instanceof CodePathSegment); - } - }; - } - }); - linter.verify( - "function foo(a) { if (a) return 0; else throw new Error(); }", - { rules: { test: 2 } } - ); - }); }); describe("interface of code path segments", () => { diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index 5f47ce1fe4b..d5d11e84333 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -2399,45 +2399,6 @@ describe("FlatRuleTester", () => { }); }); - describe("deprecations", () => { - let processStub; - - beforeEach(() => { - processStub = sinon.stub(process, "emitWarning"); - }); - - afterEach(() => { - processStub.restore(); - }); - - it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { - - const useCurrentSegmentsRule = { - create: () => ({ - onCodePathStart(codePath) { - codePath.currentSegments.forEach(() => { }); - } - }) - }; - - ruleTester.run("use-current-segments", useCurrentSegmentsRule, { - valid: ["foo"], - invalid: [] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - "\"use-current-segments\" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples", - "DeprecationWarning" - ] - ); - - }); - - }); - /** * Asserts that a particular value will be emitted from an EventEmitter. * @param {EventEmitter} emitter The emitter that should emit a value diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 5af50dd7dea..ba253da8f00 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2490,31 +2490,117 @@ describe("RuleTester", () => { assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); - it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { + it("should pass-through services from parseForESLint to the rule and log deprecation notice", () => { + const enhancedParserPath = require.resolve("../../fixtures/parsers/enhanced-parser"); + const disallowHiRule = { + create: context => ({ + Literal(node) { + assert.strictEqual(context.parserServices, context.sourceCode.parserServices); + + const disallowed = context.sourceCode.parserServices.test.getMessage(); // returns "Hi!" - const useCurrentSegmentsRule = { - create: () => ({ - onCodePathStart(codePath) { - codePath.currentSegments.forEach(() => {}); + if (node.value === disallowed) { + context.report({ node, message: `Don't use '${disallowed}'` }); + } } }) }; - ruleTester.run("use-current-segments", useCurrentSegmentsRule, { - valid: ["foo"], - invalid: [] + ruleTester.run("no-hi", disallowHiRule, { + valid: [ + { + code: "'Hello!'", + parser: enhancedParserPath + } + ], + invalid: [ + { + code: "'Hi!'", + parser: enhancedParserPath, + errors: [{ message: "Don't use 'Hi!'" }] + } + ] }); assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); assert.deepStrictEqual( processStub.getCall(0).args, [ - "\"use-current-segments\" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples", + "\"no-hi\" rule is using `context.parserServices`, which is deprecated and will be removed in ESLint v9. Please use `sourceCode.parserServices` instead.", "DeprecationWarning" ] ); + + }); + Object.entries({ + getSource: "getText", + getSourceLines: "getLines", + getAllComments: "getAllComments", + getNodeByRangeIndex: "getNodeByRangeIndex", + getCommentsBefore: "getCommentsBefore", + getCommentsAfter: "getCommentsAfter", + getCommentsInside: "getCommentsInside", + getJSDocComment: "getJSDocComment", + getFirstToken: "getFirstToken", + getFirstTokens: "getFirstTokens", + getLastToken: "getLastToken", + getLastTokens: "getLastTokens", + getTokenAfter: "getTokenAfter", + getTokenBefore: "getTokenBefore", + getTokenByRangeStart: "getTokenByRangeStart", + getTokens: "getTokens", + getTokensAfter: "getTokensAfter", + getTokensBefore: "getTokensBefore", + getTokensBetween: "getTokensBetween", + getScope: "getScope", + getAncestors: "getAncestors", + getDeclaredVariables: "getDeclaredVariables", + markVariableAsUsed: "markVariableAsUsed" + }).forEach(([methodName, replacementName]) => { + + it(`should log a deprecation warning when calling \`context.${methodName}\``, () => { + const ruleToCheckDeprecation = { + meta: { + type: "problem", + schema: [] + }, + create(context) { + return { + Program(node) { + + // special case + if (methodName === "getTokensBetween") { + context[methodName](node, node); + } else { + context[methodName](node); + } + + context.report({ node, message: "bad" }); + } + }; + } + }; + + ruleTester.run("deprecated-method", ruleToCheckDeprecation, { + valid: [], + invalid: [ + { code: "var foo = bar;", options: [], errors: 1 } + ] + }); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + `"deprecated-method" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${replacementName}()\` instead.`, + "DeprecationWarning" + ] + ); + }); + }); + }); /** From 8fe8c5626b98840d6a8580004f6ceffeff56264f Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 20 Dec 2023 18:20:22 -0500 Subject: [PATCH 21/25] feat!: Update shouldUseFlatConfig and CLI so flat config is default (#17748) * feat!: Update shouldUseFlatConfig so flat config is default Refs # 13481 * Fix tests * Update CLI * Fix test * Fix tests * Fix lint warning * Update bin tests to search for eslint.config.js * Update tests/lib/cli.js Co-authored-by: Milos Djermanovic * Update tests/lib/cli.js Co-authored-by: Milos Djermanovic * Update tests/lib/cli.js Co-authored-by: Milos Djermanovic * Merge tests for deprecation warnings * Update lib/cli.js Co-authored-by: Milos Djermanovic * Update tests/lib/cli.js Co-authored-by: Milos Djermanovic --------- Co-authored-by: Milos Djermanovic --- lib/cli.js | 8 ++++-- lib/eslint/flat-eslint.js | 14 +---------- tests/bin/eslint.js | 8 +++--- tests/lib/cli.js | 43 ++++++++++++++++++++------------- tests/lib/eslint/flat-eslint.js | 2 +- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index 1d909ec1cf2..5f3443fda69 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -304,10 +304,10 @@ const cli = { * Executes the CLI based on an array of arguments that is passed in. * @param {string|Array|Object} args The arguments to process. * @param {string} [text] The text to lint (used for TTY). - * @param {boolean} [allowFlatConfig] Whether or not to allow flat config. + * @param {boolean} [allowFlatConfig=true] Whether or not to allow flat config. * @returns {Promise} The exit code for the operation. */ - async execute(args, text, allowFlatConfig) { + async execute(args, text, allowFlatConfig = true) { if (Array.isArray(args)) { debug("CLI args: %o", args.slice(2)); } @@ -323,6 +323,10 @@ const cli = { debug("Using flat config?", usingFlatConfig); + if (allowFlatConfig && !usingFlatConfig) { + process.emitWarning("You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details.", "ESLintRCWarning"); + } + const CLIOptions = createCLIOptions(usingFlatConfig); /** @type {ParsedCLIOptions} */ diff --git a/lib/eslint/flat-eslint.js b/lib/eslint/flat-eslint.js index 5ebe9bc73c1..c96f6f84b8c 100644 --- a/lib/eslint/flat-eslint.js +++ b/lib/eslint/flat-eslint.js @@ -1127,19 +1127,7 @@ class FlatESLint { * @returns {Promise} Whether flat config should be used. */ async function shouldUseFlatConfig() { - switch (process.env.ESLINT_USE_FLAT_CONFIG) { - case "true": - return true; - case "false": - return false; - default: - - /* - * If neither explicitly enabled nor disabled, then use the presence - * of a flat config file to determine enablement. - */ - return !!(await findFlatConfigFile(process.cwd())); - } + return (process.env.ESLINT_USE_FLAT_CONFIG !== "false"); } //------------------------------------------------------------------------------ diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index 0622d48e973..c4c67b9594d 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -160,10 +160,8 @@ describe("bin/eslint.js", () => { "gives a detailed error message if no config file is found in /", () => { if ( - fs.readdirSync("/").some( - fileName => - /^\.eslintrc(?:\.(?:js|yaml|yml|json))?$/u - .test(fileName) + fs.readdirSync("/").includes( + "eslint.config.js" ) ) { return Promise.resolve(true); @@ -176,7 +174,7 @@ describe("bin/eslint.js", () => { const stderrPromise = getOutput(child).then(output => { assert.match( output.stderr, - /ESLint couldn't find a configuration file/u + /Could not find config file/u ); }); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index f2576cc5e28..457e94eac99 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -142,11 +142,15 @@ describe("cli", () => { const originalEnv = process.env; const originalCwd = process.cwd; + let processStub; + beforeEach(() => { + processStub = sinon.stub(process, "emitWarning"); process.env = { ...originalEnv }; }); afterEach(() => { + processStub.restore(); process.env = originalEnv; process.cwd = originalCwd; }); @@ -167,6 +171,12 @@ describe("cli", () => { const exitCode = await cli.execute(`--no-ignore --ext .js ${getFixturePath("files")}`, null, useFlatConfig); assert.strictEqual(exitCode, 0); + + + if (useFlatConfig) { + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.strictEqual(processStub.getCall(0).args[1], "ESLintRCWarning"); + } }); it(`should use it when ESLINT_USE_FLAT_CONFIG=true and useFlatConfig is true even if an eslint.config.js is not present:${configType}`, async () => { @@ -237,10 +247,11 @@ describe("cli", () => { describe("Formatters", () => { + const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc"; + describe("when given a valid built-in formatter name", () => { it(`should execute without any errors with configType:${configType}`, async () => { const filePath = getFixturePath("passing.js"); - const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc"; const exit = await cli.execute(`${flag} -f json ${filePath}`, null, useFlatConfig); assert.strictEqual(exit, 0); @@ -261,7 +272,6 @@ describe("cli", () => { it(`should execute without any errors with configType:${configType}`, async () => { const filePath = getFixturePath("passing.js"); - const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc"; const exit = await cli.execute(`--no-ignore -f json-with-metadata ${filePath} ${flag}`, null, useFlatConfig); assert.strictEqual(exit, 0); @@ -289,7 +299,6 @@ describe("cli", () => { }); describe("when the --max-warnings option is passed", () => { - const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc"; describe("and there are too many warnings", () => { it(`should provide \`maxWarningsExceeded\` metadata to the formatter with configType:${configType}`, async () => { @@ -330,7 +339,7 @@ describe("cli", () => { describe("when given an invalid built-in formatter name", () => { it(`should execute with error: with configType:${configType}`, async () => { const filePath = getFixturePath("passing.js"); - const exit = await cli.execute(`-f fakeformatter ${filePath}`); + const exit = await cli.execute(`-f fakeformatter ${filePath} ${flag}`, null, useFlatConfig); assert.strictEqual(exit, 2); }); @@ -340,7 +349,7 @@ describe("cli", () => { it(`should execute without any errors with configType:${configType}`, async () => { const formatterPath = getFixturePath("formatters", "simple.js"); const filePath = getFixturePath("passing.js"); - const exit = await cli.execute(`-f ${formatterPath} ${filePath}`); + const exit = await cli.execute(`-f ${formatterPath} ${filePath} ${flag}`, null, useFlatConfig); assert.strictEqual(exit, 0); }); @@ -371,7 +380,7 @@ describe("cli", () => { it(`should execute without any errors with configType:${configType}`, async () => { const formatterPath = getFixturePath("formatters", "async.js"); const filePath = getFixturePath("passing.js"); - const exit = await cli.execute(`-f ${formatterPath} ${filePath}`); + const exit = await cli.execute(`-f ${formatterPath} ${filePath} ${flag}`, null, useFlatConfig); assert.strictEqual(log.info.getCall(0).args[0], "from async formatter"); assert.strictEqual(exit, 0); @@ -1480,7 +1489,7 @@ describe("cli", () => { describe("when given a config file", () => { it("should load the specified config file", async () => { - const configPath = getFixturePath(".eslintrc"); + const configPath = getFixturePath("eslint.config.js"); const filePath = getFixturePath("passing.js"); await cli.execute(`--config ${configPath} ${filePath}`); @@ -1498,7 +1507,7 @@ describe("cli", () => { const filePath = getFixturePath("globals-browser.js"); const code = `--config ${configPath} ${filePath}`; - const exit = await cli.execute(code); + const exit = await cli.execute(code, null, false); assert.strictEqual(exit, 0); }); @@ -1510,7 +1519,7 @@ describe("cli", () => { const filePath = getFixturePath("globals-node.js"); const code = `--config ${configPath} ${filePath}`; - const exit = await cli.execute(code); + const exit = await cli.execute(code, null, false); assert.strictEqual(exit, 0); }); @@ -1522,7 +1531,7 @@ describe("cli", () => { const filePath = getFixturePath("globals-nashorn.js"); const code = `--config ${configPath} ${filePath}`; - const exit = await cli.execute(code); + const exit = await cli.execute(code, null, false); assert.strictEqual(exit, 0); }); @@ -1534,7 +1543,7 @@ describe("cli", () => { const filePath = getFixturePath("globals-webextensions.js"); const code = `--config ${configPath} ${filePath}`; - const exit = await cli.execute(code); + const exit = await cli.execute(code, null, false); assert.strictEqual(exit, 0); }); @@ -1549,7 +1558,7 @@ describe("cli", () => { const code = `--rulesdir ${rulesPath} --config ${configPath} --no-ignore ${filePath}`; await stdAssert.rejects(async () => { - const exit = await cli.execute(code); + const exit = await cli.execute(code, null, false); assert.strictEqual(exit, 2); }, /Error while loading rule 'custom-rule': Boom!/u); @@ -1561,7 +1570,7 @@ describe("cli", () => { const filePath = getFixturePath("rules", "test", "test-custom-rule.js"); const code = `--rulesdir ${rulesPath} --config ${configPath} --no-ignore ${filePath}`; - await cli.execute(code); + await cli.execute(code, null, false); assert.isTrue(log.info.calledOnce); assert.isTrue(log.info.neverCalledWith("")); @@ -1573,7 +1582,7 @@ describe("cli", () => { const configPath = getFixturePath("rules", "multi-rulesdirs.json"); const filePath = getFixturePath("rules", "test-multi-rulesdirs.js"); const code = `--rulesdir ${rulesPath} --rulesdir ${rulesPath2} --config ${configPath} --no-ignore ${filePath}`; - const exit = await cli.execute(code); + const exit = await cli.execute(code, null, false); const call = log.info.getCall(0); @@ -1591,7 +1600,7 @@ describe("cli", () => { describe("when executing with no-eslintrc flag", () => { it("should ignore a local config file", async () => { const filePath = getFixturePath("eslintrc", "quotes.js"); - const exit = await cli.execute(`--no-eslintrc --no-ignore ${filePath}`); + const exit = await cli.execute(`--no-eslintrc --no-ignore ${filePath}`, null, false); assert.isTrue(log.info.notCalled); assert.strictEqual(exit, 0); @@ -1601,7 +1610,7 @@ describe("cli", () => { describe("when executing without no-eslintrc flag", () => { it("should load a local config file", async () => { const filePath = getFixturePath("eslintrc", "quotes.js"); - const exit = await cli.execute(`--no-ignore ${filePath}`); + const exit = await cli.execute(`--no-ignore ${filePath}`, null, false); assert.isTrue(log.info.calledOnce); assert.strictEqual(exit, 1); @@ -1615,7 +1624,7 @@ describe("cli", () => { getFixturePath("globals-node.js") ]; - await cli.execute(`--no-eslintrc --config ./packages/js/src/configs/eslint-recommended.js --no-ignore ${files.join(" ")}`); + await cli.execute(`--no-eslintrc --config ./packages/js/src/configs/eslint-recommended.js --no-ignore ${files.join(" ")}`, null, false); assert.strictEqual(log.info.args[0][0].split("\n").length, 10); }); diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js index d131d8d1b04..15599cba8fe 100644 --- a/tests/lib/eslint/flat-eslint.js +++ b/tests/lib/eslint/flat-eslint.js @@ -6446,6 +6446,6 @@ describe("shouldUseFlatConfig", () => { }); describe("when the env variable `ESLINT_USE_FLAT_CONFIG` is unset", () => { - testShouldUseFlatConfig(true, false); + testShouldUseFlatConfig(true, true); }); }); From 29bc729df100721740748bb15027fa5431853757 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 20 Dec 2023 18:43:16 -0500 Subject: [PATCH 22/25] Revert "feat!: Remove CodePath#currentSegments" (#17890) Revert "feat!: Remove CodePath#currentSegments (#17756)" This reverts commit e501ab9b9f1955822c45dced841849dc214b6604. --- docs/src/extend/code-path-analysis.md | 3 +- lib/linter/code-path-analysis/code-path.js | 14 +++ lib/rule-tester/flat-rule-tester.js | 31 ++++-- lib/rule-tester/rule-tester.js | 42 +++---- .../code-path-analysis/code-path-analyzer.js | 31 ++++++ tests/lib/rule-tester/flat-rule-tester.js | 39 +++++++ tests/lib/rule-tester/rule-tester.js | 104 ++---------------- 7 files changed, 132 insertions(+), 132 deletions(-) diff --git a/docs/src/extend/code-path-analysis.md b/docs/src/extend/code-path-analysis.md index 36d827bee77..87911957490 100644 --- a/docs/src/extend/code-path-analysis.md +++ b/docs/src/extend/code-path-analysis.md @@ -37,6 +37,7 @@ This has references of both the initial segment and the final segments of a code * `finalSegments` (`CodePathSegment[]`) - The final segments which includes both returned and thrown. * `returnedSegments` (`CodePathSegment[]`) - The final segments which includes only returned. * `thrownSegments` (`CodePathSegment[]`) - The final segments which includes only thrown. +* `currentSegments` (`CodePathSegment[]`) - **Deprecated.** Segments of the current traversal position. * `upper` (`CodePath|null`) - The code path of the upper function/global scope. * `childCodePaths` (`CodePath[]`) - Code paths of functions this code path contains. @@ -634,7 +635,7 @@ last(); ``` If there is not `catch` block, `finally` block has two current segments. -At this time when running the previous example to find unreachable nodes, `currentSegments.length` is `2`. +At this time, `CodePath.currentSegments.length` is `2`. One is the normal path, and another is the leaving path (`throw` or `return`). :::img-container diff --git a/lib/linter/code-path-analysis/code-path.js b/lib/linter/code-path-analysis/code-path.js index 90cafef68f0..3bf570d754b 100644 --- a/lib/linter/code-path-analysis/code-path.js +++ b/lib/linter/code-path-analysis/code-path.js @@ -125,6 +125,20 @@ class CodePath { return this.internal.thrownForkContext; } + /** + * Tracks the traversal of the code path through each segment. This array + * starts empty and segments are added or removed as the code path is + * traversed. This array always ends up empty at the end of a code path + * traversal. The `CodePathState` uses this to track its progress through + * the code path. + * This is a passthrough to the underlying `CodePathState`. + * @type {CodePathSegment[]} + * @deprecated + */ + get currentSegments() { + return this.internal.currentSegments; + } + /** * Traverses all segments in this code path. * diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 5e08eab93fd..0a127c749a0 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -16,7 +16,8 @@ const equal = require("fast-deep-equal"), Traverser = require("../shared/traverser"), { getRuleOptionsSchema } = require("../config/flat-config-helpers"), - { Linter, SourceCodeFixer, interpolate } = require("../linter"); + { Linter, SourceCodeFixer, interpolate } = require("../linter"), + CodePath = require("../linter/code-path-analysis/code-path"); const { FlatConfigArray } = require("../config/flat-config-array"); const { defaultConfig } = require("../config/default-config"); @@ -274,14 +275,18 @@ function wrapParser(parser) { } /** - * Function to replace `SourceCode.prototype.getComments`. + * Emit a deprecation warning if rule uses CodePath#currentSegments. + * @param {string} ruleName Name of the rule. * @returns {void} - * @throws {Error} Deprecation message. */ -function getCommentsDeprecation() { - throw new Error( - "`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead." - ); +function emitCodePathCurrentSegmentsWarning(ruleName) { + if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { + emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; + process.emitWarning( + `"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`, + "DeprecationWarning" + ); + } } /** @@ -711,11 +716,17 @@ class FlatRuleTester { } // Verify the code. - const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); let messages; try { - SourceCode.prototype.getComments = getCommentsDeprecation; + Object.defineProperty(CodePath.prototype, "currentSegments", { + get() { + emitCodePathCurrentSegmentsWarning(ruleName); + return originalCurrentSegments.get.call(this); + } + }); forbiddenMethods.forEach(methodName => { SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype); @@ -723,7 +734,7 @@ class FlatRuleTester { messages = linter.verify(code, configs, filename); } finally { - SourceCode.prototype.getComments = getComments; + Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; SourceCode.prototype.finalize = finalize; diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 74ca7c11493..bd46fcc807a 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -48,7 +48,8 @@ const equal = require("fast-deep-equal"), Traverser = require("../../lib/shared/traverser"), { getRuleOptionsSchema, validate } = require("../shared/config-validator"), - { Linter, SourceCodeFixer, interpolate } = require("../linter"); + { Linter, SourceCodeFixer, interpolate } = require("../linter"), + CodePath = require("../linter/code-path-analysis/code-path"); const ajv = require("../shared/ajv")({ strictDefaults: true }); @@ -344,37 +345,20 @@ function emitMissingSchemaWarning(ruleName) { } /** - * Emit a deprecation warning if a rule uses a deprecated `context` method. + * Emit a deprecation warning if rule uses CodePath#currentSegments. * @param {string} ruleName Name of the rule. - * @param {string} methodName The name of the method on `context` that was used. * @returns {void} */ -function emitDeprecatedContextMethodWarning(ruleName, methodName) { - if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) { - emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true; +function emitCodePathCurrentSegmentsWarning(ruleName) { + if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { + emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; process.emitWarning( - `"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`, + `"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`, "DeprecationWarning" ); } } -/** - * Emit a deprecation warning if `context.parserServices` is used. - * @param {string} ruleName Name of the rule. - * @returns {void} - */ -function emitParserServicesWarning(ruleName) { - if (!emitParserServicesWarning[`warned-${ruleName}`]) { - emitParserServicesWarning[`warned-${ruleName}`] = true; - process.emitWarning( - `"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`, - "DeprecationWarning" - ); - } -} - - //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -725,11 +709,17 @@ class RuleTester { validate(config, "rule-tester", id => (id === ruleName ? rule : null)); // Verify the code. - const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const { applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); let messages; try { - SourceCode.prototype.getComments = getCommentsDeprecation; + Object.defineProperty(CodePath.prototype, "currentSegments", { + get() { + emitCodePathCurrentSegmentsWarning(ruleName); + return originalCurrentSegments.get.call(this); + } + }); forbiddenMethods.forEach(methodName => { SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName); @@ -737,7 +727,7 @@ class RuleTester { messages = linter.verify(code, config, filename); } finally { - SourceCode.prototype.getComments = getComments; + Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); SourceCode.prototype.applyInlineConfig = applyInlineConfig; SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; SourceCode.prototype.finalize = finalize; diff --git a/tests/lib/linter/code-path-analysis/code-path-analyzer.js b/tests/lib/linter/code-path-analysis/code-path-analyzer.js index e580c14d134..dbed9b46194 100644 --- a/tests/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/tests/lib/linter/code-path-analysis/code-path-analyzer.js @@ -137,6 +137,37 @@ describe("CodePathAnalyzer", () => { assert(actual[1].thrownSegments[0] instanceof CodePathSegment); }); + it("should have `currentSegments` as CodePathSegment[]", () => { + assert(Array.isArray(actual[0].currentSegments)); + assert(Array.isArray(actual[1].currentSegments)); + assert(actual[0].currentSegments.length === 0); + assert(actual[1].currentSegments.length === 0); + + // there is the current segment in progress. + linter.defineRule("test", { + create() { + let codePath = null; + + return { + onCodePathStart(cp) { + codePath = cp; + }, + ReturnStatement() { + assert(codePath.currentSegments.length === 1); + assert(codePath.currentSegments[0] instanceof CodePathSegment); + }, + ThrowStatement() { + assert(codePath.currentSegments.length === 1); + assert(codePath.currentSegments[0] instanceof CodePathSegment); + } + }; + } + }); + linter.verify( + "function foo(a) { if (a) return 0; else throw new Error(); }", + { rules: { test: 2 } } + ); + }); }); describe("interface of code path segments", () => { diff --git a/tests/lib/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index d5d11e84333..5f47ce1fe4b 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -2399,6 +2399,45 @@ describe("FlatRuleTester", () => { }); }); + describe("deprecations", () => { + let processStub; + + beforeEach(() => { + processStub = sinon.stub(process, "emitWarning"); + }); + + afterEach(() => { + processStub.restore(); + }); + + it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { + + const useCurrentSegmentsRule = { + create: () => ({ + onCodePathStart(codePath) { + codePath.currentSegments.forEach(() => { }); + } + }) + }; + + ruleTester.run("use-current-segments", useCurrentSegmentsRule, { + valid: ["foo"], + invalid: [] + }); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "\"use-current-segments\" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples", + "DeprecationWarning" + ] + ); + + }); + + }); + /** * Asserts that a particular value will be emitted from an EventEmitter. * @param {EventEmitter} emitter The emitter that should emit a value diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index ba253da8f00..5af50dd7dea 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2490,117 +2490,31 @@ describe("RuleTester", () => { assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); - it("should pass-through services from parseForESLint to the rule and log deprecation notice", () => { - const enhancedParserPath = require.resolve("../../fixtures/parsers/enhanced-parser"); - const disallowHiRule = { - create: context => ({ - Literal(node) { - assert.strictEqual(context.parserServices, context.sourceCode.parserServices); - - const disallowed = context.sourceCode.parserServices.test.getMessage(); // returns "Hi!" + it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { - if (node.value === disallowed) { - context.report({ node, message: `Don't use '${disallowed}'` }); - } + const useCurrentSegmentsRule = { + create: () => ({ + onCodePathStart(codePath) { + codePath.currentSegments.forEach(() => {}); } }) }; - ruleTester.run("no-hi", disallowHiRule, { - valid: [ - { - code: "'Hello!'", - parser: enhancedParserPath - } - ], - invalid: [ - { - code: "'Hi!'", - parser: enhancedParserPath, - errors: [{ message: "Don't use 'Hi!'" }] - } - ] + ruleTester.run("use-current-segments", useCurrentSegmentsRule, { + valid: ["foo"], + invalid: [] }); assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); assert.deepStrictEqual( processStub.getCall(0).args, [ - "\"no-hi\" rule is using `context.parserServices`, which is deprecated and will be removed in ESLint v9. Please use `sourceCode.parserServices` instead.", + "\"use-current-segments\" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples", "DeprecationWarning" ] ); - - }); - Object.entries({ - getSource: "getText", - getSourceLines: "getLines", - getAllComments: "getAllComments", - getNodeByRangeIndex: "getNodeByRangeIndex", - getCommentsBefore: "getCommentsBefore", - getCommentsAfter: "getCommentsAfter", - getCommentsInside: "getCommentsInside", - getJSDocComment: "getJSDocComment", - getFirstToken: "getFirstToken", - getFirstTokens: "getFirstTokens", - getLastToken: "getLastToken", - getLastTokens: "getLastTokens", - getTokenAfter: "getTokenAfter", - getTokenBefore: "getTokenBefore", - getTokenByRangeStart: "getTokenByRangeStart", - getTokens: "getTokens", - getTokensAfter: "getTokensAfter", - getTokensBefore: "getTokensBefore", - getTokensBetween: "getTokensBetween", - getScope: "getScope", - getAncestors: "getAncestors", - getDeclaredVariables: "getDeclaredVariables", - markVariableAsUsed: "markVariableAsUsed" - }).forEach(([methodName, replacementName]) => { - - it(`should log a deprecation warning when calling \`context.${methodName}\``, () => { - const ruleToCheckDeprecation = { - meta: { - type: "problem", - schema: [] - }, - create(context) { - return { - Program(node) { - - // special case - if (methodName === "getTokensBetween") { - context[methodName](node, node); - } else { - context[methodName](node); - } - - context.report({ node, message: "bad" }); - } - }; - } - }; - - ruleTester.run("deprecated-method", ruleToCheckDeprecation, { - valid: [], - invalid: [ - { code: "var foo = bar;", options: [], errors: 1 } - ] - }); - - assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); - assert.deepStrictEqual( - processStub.getCall(0).args, - [ - `"deprecated-method" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${replacementName}()\` instead.`, - "DeprecationWarning" - ] - ); - }); - }); - }); /** From cd1ac2041f48f2b6d743ebf671d0279a70de6eea Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Thu, 21 Dec 2023 08:06:49 +0000 Subject: [PATCH 23/25] docs: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67538267a0f..0323a681180 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Platinum Sponsors

Chrome Frameworks Fund Automattic

Gold Sponsors

Salesforce Airbnb

Silver Sponsors

-

Liftoff American Express Workleap

Bronze Sponsors

+

JetBrains Liftoff American Express Workleap

Bronze Sponsors

ThemeIsle Anagram Solver Icons8 Discord Transloadit Ignition Nx HeroCoders

From 12be3071d014814149e8e6d602f5c192178ca771 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 21 Dec 2023 08:06:45 -0500 Subject: [PATCH 24/25] fix!: Behavior of CLI when no arguments are passed (#17644) * fix: Behavior of CLI when no arguments are passed Fixes #14308 * Removed unnecessary check * Fix edge cases * Add --pass-on-no-patterns flag * Rename argument * Update lib/options.js Co-authored-by: Milos Djermanovic * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic * Update tests/lib/eslint/eslint.js Co-authored-by: Milos Djermanovic * Update lib/eslint/flat-eslint.js Co-authored-by: Milos Djermanovic * Update tests/lib/cli.js Co-authored-by: Milos Djermanovic * lintFiles() should not have default param * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic * Fix merge bug --------- Co-authored-by: Milos Djermanovic --- docs/src/use/command-line-interface.md | 22 ++++++++ lib/cli.js | 6 ++- lib/eslint/eslint-helpers.js | 33 ++++++++---- lib/eslint/eslint.js | 62 +++++++++++++++------- lib/eslint/flat-eslint.js | 40 ++++++++++++-- lib/options.js | 8 +++ tests/lib/cli.js | 7 +++ tests/lib/eslint/eslint.js | 50 ++++++++++++++++++ tests/lib/eslint/flat-eslint.js | 73 ++++++++++++++++++++++++++ tests/lib/options.js | 10 +++- 10 files changed, 275 insertions(+), 36 deletions(-) diff --git a/docs/src/use/command-line-interface.md b/docs/src/use/command-line-interface.md index dbbbe9ca526..472619a524b 100644 --- a/docs/src/use/command-line-interface.md +++ b/docs/src/use/command-line-interface.md @@ -36,6 +36,15 @@ Please note that when passing a glob as a parameter, it is expanded by your shel npx eslint "lib/**" ``` +If you are using a [flat configuration file](./configure/configuration-files-new) (`eslint.config.js`), you can also omit the file arguments and ESLint will use `.`. For instance, these two lines perform the same operation: + +```shell +npx eslint . +npx eslint +``` + +If you are not using a flat configuration file, running ESLint without file arguments results in an error. + **Note:** You can also use alternative package managers such as [Yarn](https://yarnpkg.com/) or [pnpm](https://pnpm.io/) to run ESLint. Please refer to your package manager's documentation for the correct syntax. ## Pass Multiple Values to an Option @@ -112,6 +121,7 @@ Miscellaneous: --no-error-on-unmatched-pattern Prevent errors when pattern is unmatched --exit-on-fatal-error Exit with exit code 2 in case of fatal error - default: false --no-warn-ignored Suppress warnings when the file list includes ignored files. *Flat Config Mode Only* + --pass-on-no-patterns Exit with exit code 0 in case no file patterns are passed --debug Output debugging information -h, --help Show help -v, --version Output the version number @@ -734,6 +744,18 @@ npx eslint --exit-on-fatal-error file.js npx eslint --no-warn-ignored --max-warnings 0 ignored-file.js ``` +#### `--pass-on-no-patterns` + +This option allows ESLint to exit with code 0 when no file or directory patterns are passed. Without this option, ESLint assumes you want to use `.` as the pattern. (When running in legacy eslintrc mode, ESLint will exit with code 1.) + +* **Argument Type**: No argument. + +##### `--pass-on-no-patterns` example + +```shell +npx eslint --pass-on-no-patterns +``` + #### `--debug` This option outputs debugging information to the console. Add this flag to an ESLint command line invocation in order to get extra debugging information while the command runs. diff --git a/lib/cli.js b/lib/cli.js index 5f3443fda69..db9e1fd2708 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -94,7 +94,8 @@ async function translateOptions({ resolvePluginsRelativeTo, rule, rulesdir, - warnIgnored + warnIgnored, + passOnNoPatterns }, configType) { let overrideConfig, overrideConfigFile; @@ -187,7 +188,8 @@ async function translateOptions({ fixTypes: fixType, ignore, overrideConfig, - overrideConfigFile + overrideConfigFile, + passOnNoPatterns }; if (configType === "flat") { diff --git a/lib/eslint/eslint-helpers.js b/lib/eslint/eslint-helpers.js index 685826ac69c..b367928ad18 100644 --- a/lib/eslint/eslint-helpers.js +++ b/lib/eslint/eslint-helpers.js @@ -105,20 +105,30 @@ class AllFilesIgnoredError extends Error { /** * Check if a given value is a non-empty string or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is a non-empty string. + * @param {any} value The value to check. + * @returns {boolean} `true` if `value` is a non-empty string. */ -function isNonEmptyString(x) { - return typeof x === "string" && x.trim() !== ""; +function isNonEmptyString(value) { + return typeof value === "string" && value.trim() !== ""; } /** * Check if a given value is an array of non-empty strings or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is an array of non-empty strings. + * @param {any} value The value to check. + * @returns {boolean} `true` if `value` is an array of non-empty strings. */ -function isArrayOfNonEmptyString(x) { - return Array.isArray(x) && x.every(isNonEmptyString); +function isArrayOfNonEmptyString(value) { + return Array.isArray(value) && value.length && value.every(isNonEmptyString); +} + +/** + * Check if a given value is an empty array or an array of non-empty strings. + * @param {any} value The value to check. + * @returns {boolean} `true` if `value` is an empty array or an array of non-empty + * strings. + */ +function isEmptyArrayOrArrayOfNonEmptyString(value) { + return Array.isArray(value) && value.every(isNonEmptyString); } //----------------------------------------------------------------------------- @@ -676,6 +686,7 @@ function processOptions({ overrideConfigFile = null, plugins = {}, warnIgnored = true, + passOnNoPatterns = false, ...unknownOptions }) { const errors = []; @@ -759,7 +770,7 @@ function processOptions({ if (typeof ignore !== "boolean") { errors.push("'ignore' must be a boolean."); } - if (!isArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) { + if (!isEmptyArrayOrArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) { errors.push("'ignorePatterns' must be an array of non-empty strings or null."); } if (typeof overrideConfig !== "object") { @@ -768,6 +779,9 @@ function processOptions({ if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null && overrideConfigFile !== true) { errors.push("'overrideConfigFile' must be a non-empty string, null, or true."); } + if (typeof passOnNoPatterns !== "boolean") { + errors.push("'passOnNoPatterns' must be a boolean."); + } if (typeof plugins !== "object") { errors.push("'plugins' must be an object or null."); } else if (plugins !== null && Object.keys(plugins).includes("")) { @@ -800,6 +814,7 @@ function processOptions({ globInputPaths, ignore, ignorePatterns, + passOnNoPatterns, warnIgnored }; } diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 15e6b3dee41..0a136add77c 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -67,6 +67,8 @@ const { version } = require("../../package.json"); * @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD. * @property {string[]} [rulePaths] An array of directories to load custom rules from. * @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files. + * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause + * the linting operation to short circuit and not report any failures. */ /** @@ -97,38 +99,48 @@ const privateMembersMap = new WeakMap(); /** * Check if a given value is a non-empty string or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is a non-empty string. + * @param {any} value The value to check. + * @returns {boolean} `true` if `value` is a non-empty string. */ -function isNonEmptyString(x) { - return typeof x === "string" && x.trim() !== ""; +function isNonEmptyString(value) { + return typeof value === "string" && value.trim() !== ""; } /** * Check if a given value is an array of non-empty strings or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is an array of non-empty strings. + * @param {any} value The value to check. + * @returns {boolean} `true` if `value` is an array of non-empty strings. */ -function isArrayOfNonEmptyString(x) { - return Array.isArray(x) && x.every(isNonEmptyString); +function isArrayOfNonEmptyString(value) { + return Array.isArray(value) && value.length && value.every(isNonEmptyString); +} + +/** + * Check if a given value is an empty array or an array of non-empty strings. + * @param {any} value The value to check. + * @returns {boolean} `true` if `value` is an empty array or an array of non-empty + * strings. + */ +function isEmptyArrayOrArrayOfNonEmptyString(value) { + return Array.isArray(value) && value.every(isNonEmptyString); } /** * Check if a given value is a valid fix type or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is valid fix type. + * @param {any} value The value to check. + * @returns {boolean} `true` if `value` is valid fix type. */ -function isFixType(x) { - return x === "directive" || x === "problem" || x === "suggestion" || x === "layout"; +function isFixType(value) { + return value === "directive" || value === "problem" || value === "suggestion" || value === "layout"; } /** * Check if a given value is an array of fix types or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is an array of fix types. + * @param {any} value The value to check. + * @returns {boolean} `true` if `value` is an array of fix types. */ -function isFixTypeArray(x) { - return Array.isArray(x) && x.every(isFixType); +function isFixTypeArray(value) { + return Array.isArray(value) && value.every(isFixType); } /** @@ -169,6 +181,7 @@ function processOptions({ resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. rulePaths = [], useEslintrc = true, + passOnNoPatterns = false, ...unknownOptions }) { const errors = []; @@ -225,7 +238,7 @@ function processOptions({ if (typeof errorOnUnmatchedPattern !== "boolean") { errors.push("'errorOnUnmatchedPattern' must be a boolean."); } - if (!isArrayOfNonEmptyString(extensions) && extensions !== null) { + if (!isEmptyArrayOrArrayOfNonEmptyString(extensions) && extensions !== null) { errors.push("'extensions' must be an array of non-empty strings or null."); } if (typeof fix !== "boolean" && typeof fix !== "function") { @@ -271,12 +284,15 @@ function processOptions({ ) { errors.push("'resolvePluginsRelativeTo' must be a non-empty string or null."); } - if (!isArrayOfNonEmptyString(rulePaths)) { + if (!isEmptyArrayOrArrayOfNonEmptyString(rulePaths)) { errors.push("'rulePaths' must be an array of non-empty strings."); } if (typeof useEslintrc !== "boolean") { errors.push("'useEslintrc' must be a boolean."); } + if (typeof passOnNoPatterns !== "boolean") { + errors.push("'passOnNoPatterns' must be a boolean."); + } if (errors.length > 0) { throw new ESLintInvalidOptionsError(errors); @@ -300,7 +316,8 @@ function processOptions({ reportUnusedDisableDirectives, resolvePluginsRelativeTo, rulePaths, - useEslintrc + useEslintrc, + passOnNoPatterns }; } @@ -541,10 +558,15 @@ class ESLint { * @returns {Promise} The results of linting the file patterns given. */ async lintFiles(patterns) { + const { cliEngine, options } = privateMembersMap.get(this); + + if (options.passOnNoPatterns && (patterns === "" || (Array.isArray(patterns) && patterns.length === 0))) { + return []; + } + if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) { throw new Error("'patterns' must be a non-empty string or an array of non-empty strings"); } - const { cliEngine } = privateMembersMap.get(this); return processCLIEngineLintReport( cliEngine, diff --git a/lib/eslint/flat-eslint.js b/lib/eslint/flat-eslint.js index c96f6f84b8c..eaca597c5ae 100644 --- a/lib/eslint/flat-eslint.js +++ b/lib/eslint/flat-eslint.js @@ -85,6 +85,8 @@ const LintResultCache = require("../cli-engine/lint-result-cache"); * when a string. * @property {Record} [plugins] An array of plugin implementations. * @property {boolean} warnIgnored Show warnings when the file list includes ignored files + * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause + * the linting operation to short circuit and not report any failures. */ //------------------------------------------------------------------------------ @@ -738,16 +740,46 @@ class FlatESLint { * @returns {Promise} The results of linting the file patterns given. */ async lintFiles(patterns) { - if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) { - throw new Error("'patterns' must be a non-empty string or an array of non-empty strings"); - } + let normalizedPatterns = patterns; const { cacheFilePath, lintResultCache, linter, options: eslintOptions } = privateMembers.get(this); + + /* + * Special cases: + * 1. `patterns` is an empty string + * 2. `patterns` is an empty array + * + * In both cases, we use the cwd as the directory to lint. + */ + if (patterns === "" || Array.isArray(patterns) && patterns.length === 0) { + + /* + * Special case: If `passOnNoPatterns` is true, then we just exit + * without doing any work. + */ + if (eslintOptions.passOnNoPatterns) { + return []; + } + + normalizedPatterns = ["."]; + } else { + + if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) { + throw new Error("'patterns' must be a non-empty string or an array of non-empty strings"); + } + + if (typeof patterns === "string") { + normalizedPatterns = [patterns]; + } + } + + debug(`Using file patterns: ${normalizedPatterns}`); + const configs = await calculateConfigArray(this, eslintOptions); const { allowInlineConfig, @@ -779,7 +811,7 @@ class FlatESLint { } const filePaths = await findFiles({ - patterns: typeof patterns === "string" ? [patterns] : patterns, + patterns: normalizedPatterns, cwd, globInputPaths, configs, diff --git a/lib/options.js b/lib/options.js index dd67c399e64..57cfd234a5f 100644 --- a/lib/options.js +++ b/lib/options.js @@ -57,6 +57,8 @@ const optionator = require("optionator"); * @property {boolean} quiet Report errors only * @property {boolean} [version] Output the version number * @property {boolean} warnIgnored Show warnings when the file list includes ignored files + * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause + * the linting operation to short circuit and not report any failures. * @property {string[]} _ Positional filenames or patterns */ @@ -370,6 +372,12 @@ module.exports = function(usingFlatConfig) { description: "Exit with exit code 2 in case of fatal error" }, warnIgnoredFlag, + { + option: "pass-on-no-patterns", + type: "Boolean", + default: false, + description: "Exit with exit code 0 in case no file patterns are passed" + }, { option: "debug", type: "Boolean", diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 457e94eac99..0134db91aca 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -823,6 +823,13 @@ describe("cli", () => { assert.strictEqual(exit, useFlatConfig ? 0 : 2); }); + it(`should not lint anything when no files are passed if --pass-on-no-patterns is passed with configType:${configType}`, async () => { + const exit = await cli.execute("--pass-on-no-patterns", null, useFlatConfig); + + assert.isFalse(log.info.called); + assert.strictEqual(exit, 0); + }); + it(`should suppress the warning if --no-warn-ignored is passed and an ignored file is passed via stdin with configType:${configType}`, async () => { const options = useFlatConfig ? `--config ${getFixturePath("eslint.config_with_ignores.js")}` diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 754dec5253b..42e64c13335 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -1075,6 +1075,43 @@ describe("ESLint", () => { await assert.rejects(async () => await eslint.lintFiles(["lib/cli.js"]), /Cannot find module 'test11'/u); }); + describe("Invalid inputs", () => { + + [ + ["an empty string", ""], + ["an empty array", []], + ["a string with a single space", " "], + ["an array with one empty string", [""]], + ["an array with two empty strings", ["", ""]] + + ].forEach(([name, value]) => { + + it(`should throw an error when passed ${name}`, async () => { + eslint = new ESLint({ + useEslintrc: false + }); + + await assert.rejects(async () => await eslint.lintFiles(value), /'patterns' must be a non-empty string or an array of non-empty strings/u); + }); + + if (value === "" || Array.isArray(value) && value.length === 0) { + it(`should not throw an error when passed ${name} and passOnNoPatterns: true`, async () => { + eslint = new ESLint({ + useEslintrc: false, + passOnNoPatterns: true + }); + + const results = await eslint.lintFiles(value); + + assert.strictEqual(results.length, 0); + }); + } + + }); + + + }); + it("should report zero messages when given a directory with a .js2 file", async () => { eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -1545,6 +1582,19 @@ describe("ESLint", () => { }, /All files matched by 'tests\/fixtures\/\*-quoted\.js' are ignored\./u); }); + it("should not throw an error when ignorePatterns is an empty array", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + ignorePatterns: [] + } + }); + + await assert.doesNotReject(async () => { + await eslint.lintFiles(["*.js"]); + }); + }); + it("should return a warning when an explicitly given file is ignored", async () => { eslint = new ESLint({ ignorePath: getFixturePath(".eslintignore"), diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js index 15599cba8fe..c29f0574f1d 100644 --- a/tests/lib/eslint/flat-eslint.js +++ b/tests/lib/eslint/flat-eslint.js @@ -960,6 +960,67 @@ describe("FlatESLint", () => { await assert.rejects(async () => await eslint.lintFiles(["lib/cli.js"]), /Expected object with parse\(\) or parseForESLint\(\) method/u); }); + describe("Invalid inputs", () => { + + [ + ["a string with a single space", " "], + ["an array with one empty string", [""]], + ["an array with two empty strings", ["", ""]], + ["undefined", void 0] + ].forEach(([name, value]) => { + + it(`should throw an error when passed ${name}`, async () => { + eslint = new FlatESLint({ + overrideConfigFile: true + }); + + await assert.rejects(async () => await eslint.lintFiles(value), /'patterns' must be a non-empty string or an array of non-empty strings/u); + }); + }); + + }); + + describe("Normalized inputs", () => { + + [ + ["an empty string", ""], + ["an empty array", []] + + ].forEach(([name, value]) => { + + it(`should normalize to '.' when ${name} is passed`, async () => { + eslint = new FlatESLint({ + ignore: false, + cwd: getFixturePath("files"), + overrideConfig: { files: ["**/*.js"] }, + overrideConfigFile: getFixturePath("eslint.config.js") + }); + const results = await eslint.lintFiles(value); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].filePath, getFixturePath("files/.bar.js")); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].filePath, getFixturePath("files/foo.js")); + assert.strictEqual(results[1].messages.length, 0); + assert.strictEqual(results[0].suppressedMessages.length, 0); + }); + + it(`should return an empty array when ${name} is passed with passOnNoPatterns: true`, async () => { + eslint = new FlatESLint({ + ignore: false, + cwd: getFixturePath("files"), + overrideConfig: { files: ["**/*.js"] }, + overrideConfigFile: getFixturePath("eslint.config.js"), + passOnNoPatterns: true + }); + const results = await eslint.lintFiles(value); + + assert.strictEqual(results.length, 0); + }); + }); + + }); + it("should report zero messages when given a directory with a .js2 file", async () => { eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), @@ -1530,6 +1591,18 @@ describe("FlatESLint", () => { }, /All files matched by 'tests\/fixtures\/\*-quoted\.js' are ignored\./u); }); + it("should not throw an error when ignorePatterns is an empty array", async () => { + eslint = new FlatESLint({ + overrideConfigFile: true, + ignorePatterns: [] + }); + + await assert.doesNotReject(async () => { + await eslint.lintFiles(["*.js"]); + }); + }); + + it("should return a warning when an explicitly given file is ignored", async () => { eslint = new FlatESLint({ overrideConfigFile: "eslint.config_with_ignores.js", diff --git a/tests/lib/options.js b/tests/lib/options.js index 8387f86296b..781f8250634 100644 --- a/tests/lib/options.js +++ b/tests/lib/options.js @@ -408,13 +408,21 @@ describe("options", () => { }); describe("--no-config-lookup", () => { - it("should return a string for .rulesdir when passed a string", () => { + it("should return a boolean for .configLookup when passed a string", () => { const currentOptions = flatOptions.parse("--no-config-lookup foo.js"); assert.isFalse(currentOptions.configLookup); }); }); + describe("--pass-on-no-patterns", () => { + it("should return a boolean for .passOnNoPatterns when passed a string", () => { + const currentOptions = flatOptions.parse("--pass-on-no-patterns"); + + assert.isTrue(currentOptions.passOnNoPatterns); + }); + }); + describe("--no-warn-ignored", () => { it("should return false when --no-warn-ignored is passed", () => { const currentOptions = flatOptions.parse("--no-warn-ignored"); From 595a1f689edb5250d8398af13c3e4bd19d284d92 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Thu, 21 Dec 2023 17:06:07 +0100 Subject: [PATCH 25/25] test: ensure that CLI tests run with FlatESLint (#17884) * test: ensure that CLI tests run with FlatESLint * mock `process.cwd()` in `beforeEach`/`afterEach` hooks --- tests/lib/cli.js | 62 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 0134db91aca..b5dfbe7dbee 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -16,7 +16,7 @@ const assert = require("chai").assert, stdAssert = require("assert"), - { ESLint } = require("../../lib/eslint"), + { ESLint, FlatESLint } = require("../../lib/eslint"), BuiltinRules = require("../../lib/rules"), path = require("path"), sinon = require("sinon"), @@ -54,10 +54,12 @@ describe("cli", () => { */ async function verifyESLintOpts(cmd, opts, configType) { + const ActiveESLint = configType === "flat" ? FlatESLint : ESLint; + // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match(opts)); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: sinon.spy() }); @@ -110,6 +112,7 @@ describe("cli", () => { ["eslintrc", "flat"].forEach(configType => { const useFlatConfig = configType === "flat"; + const ActiveESLint = configType === "flat" ? FlatESLint : ESLint; describe("execute()", () => { @@ -337,6 +340,17 @@ describe("cli", () => { }); describe("when given an invalid built-in formatter name", () => { + + const originalCwd = process.cwd; + + beforeEach(() => { + process.cwd = () => getFixturePath(); + }); + + afterEach(() => { + process.cwd = originalCwd; + }); + it(`should execute with error: with configType:${configType}`, async () => { const filePath = getFixturePath("passing.js"); const exit = await cli.execute(`-f fakeformatter ${filePath} ${flag}`, null, useFlatConfig); @@ -346,6 +360,17 @@ describe("cli", () => { }); describe("when given a valid formatter path", () => { + + const originalCwd = process.cwd; + + beforeEach(() => { + process.cwd = () => getFixturePath(); + }); + + afterEach(() => { + process.cwd = originalCwd; + }); + it(`should execute without any errors with configType:${configType}`, async () => { const formatterPath = getFixturePath("formatters", "simple.js"); const filePath = getFixturePath("passing.js"); @@ -377,6 +402,17 @@ describe("cli", () => { }); describe("when given an async formatter path", () => { + + const originalCwd = process.cwd; + + beforeEach(() => { + process.cwd = () => getFixturePath(); + }); + + afterEach(() => { + process.cwd = originalCwd; + }); + it(`should execute without any errors with configType:${configType}`, async () => { const formatterPath = getFixturePath("formatters", "async.js"); const filePath = getFixturePath("passing.js"); @@ -555,7 +591,7 @@ describe("cli", () => { it(`should allow defining variables with multiple flags with configType:${configType}`, async () => { const filePath = getFixturePath("undef.js"); - const exit = await cli.execute(`--global baz --global bat:true --no-ignore ${filePath}`); + const exit = await cli.execute(`--global baz --global bat:true --no-ignore ${filePath}`, null, useFlatConfig); assert.isTrue(log.info.notCalled); assert.strictEqual(exit, 0); @@ -987,7 +1023,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: false })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns([{ filePath: "./foo.js", output: "bar", @@ -1017,7 +1053,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: true })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.outputFixes = sinon.stub(); @@ -1048,7 +1084,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.outputFixes = sinon.mock().once(); @@ -1084,7 +1120,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.outputFixes = sinon.mock().withExactArgs(report); @@ -1120,7 +1156,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.getErrorResults = sinon.stub().returns([]); @@ -1170,7 +1206,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.outputFixes = sinon.mock().never(); @@ -1198,7 +1234,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match(expectedESLintOptions)); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.outputFixes = sinon.stub(); @@ -1233,7 +1269,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.outputFixes = sinon.mock().never(); @@ -1269,7 +1305,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.getErrorResults = sinon.stub().returns([]); @@ -1306,7 +1342,7 @@ describe("cli", () => { // create a fake ESLint class to test with const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ActiveESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintText").returns(report); sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); fakeESLint.outputFixes = sinon.mock().never();