From f591d2c88bf15af72e3a207b34fa872b4b90464b Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 29 Aug 2023 12:31:58 -0400 Subject: [PATCH 1/9] chore: Upgrade config-array (#17512) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b58827056a8..d52a2aef60ce 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", From 926a28684282aeec37680bbc52a66973b8055f54 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Tue, 29 Aug 2023 19:50:25 -0700 Subject: [PATCH 2/9] test: replace Karma with Webdriver.IO (#17126) * test: replace Karma with Webdriver.IO The current test framework for browser testing (Karma) is not maintained anymore and WebdriverIO provides a more modern stack that allows to test in different browser. This patch replaces these test frameworks. fixes: #17009 * update webdriverio deps * PR feedback * adjust tests * build eslint before running tests * make test file an esm file * revert more esm changes * make it work * remove return value * custom log dir for wdio tests * auto detect chromedriver * bump timeout, store logs * bump timeout again * update wdio deps * update wdio deps * set log level to trace * update wdio deps and unskip tests * no need to have this be an async test * update deps * make path spec file explicit * remove Chromedriver deps * removed wdio command --- .github/workflows/ci.yml | 8 +- .gitignore | 2 + Makefile.js | 8 +- karma.conf.js | 125 ----------- lib/config/rule-validator.js | 3 +- package.json | 14 +- tests/lib/linter/linter.js | 14 +- wdio.conf.js | 387 +++++++++++++++++++++++++++++++++++ 8 files changed, 416 insertions(+), 145 deletions(-) delete mode 100644 karma.conf.js create mode 100644 wdio.conf.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27dae4c1262e..92e2a7eed35e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,12 @@ jobs: - name: Install Packages run: npm install - name: Test - run: node Makefile karma + run: node Makefile wdio - name: Fuzz Test run: node Makefile fuzz + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: logs + path: | + wdio-logs/*.log diff --git a/.gitignore b/.gitignore index 075a4d740c70..148181e07769 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ test.js coverage/ build/ +logs +wdio-logs npm-debug.log yarn-error.log .pnpm-debug.log diff --git a/Makefile.js b/Makefile.js index 717cc7859467..528fd5aa1e72 100644 --- a/Makefile.js +++ b/Makefile.js @@ -628,12 +628,10 @@ target.mocha = () => { } }; -target.karma = () => { +target.wdio = () => { echo("Running unit tests on browsers"); - target.webpack("production"); - - const lastReturn = exec(`${getBinFile("karma")} start karma.conf.js`); + const lastReturn = exec(`${getBinFile("wdio")} run wdio.conf.js`); if (lastReturn.code !== 0) { exit(1); @@ -643,7 +641,7 @@ target.karma = () => { target.test = function() { target.checkRuleFiles(); target.mocha(); - target.karma(); + target.wdio(); target.fuzz({ amount: 150, fuzzBrokenAutofixes: false }); target.checkLicenses(); }; diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index 606d13f88f62..000000000000 --- a/karma.conf.js +++ /dev/null @@ -1,125 +0,0 @@ -"use strict"; -const os = require("os"); -const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); - -if (os.platform === "linux" && os.arch() === "arm64") { - - // For arm64 architecture, install chromium-browser using "apt-get install chromium-browser" - process.env.CHROME_BIN = "/usr/bin/chromium-browser"; -} else { - process.env.CHROME_BIN = require("puppeteer").executablePath(); -} - -module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: "", - - // next three sections allow console.log to work - client: { - captureConsole: true - }, - - browserConsoleLogOptions: { - terminal: true, - level: "log" - }, - - /* - * frameworks to use - * available frameworks: https://npmjs.org/browse/keyword/karma-adapter - */ - frameworks: ["mocha", "webpack"], - - - // list of files / patterns to load in the browser - files: [ - "tests/lib/linter/linter.js" - ], - - - // list of files to exclude - exclude: [ - ], - - - /* - * preprocess matching files before serving them to the browser - * available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - */ - preprocessors: { - "tests/lib/linter/linter.js": ["webpack"] - }, - webpack: { - mode: "none", - plugins: [ - new NodePolyfillPlugin() - ], - resolve: { - alias: { - "../../../lib/linter$": "../../../build/eslint.js" - } - }, - stats: "errors-only" - }, - webpackMiddleware: { - logLevel: "error" - }, - - - /* - * test results reporter to use - * possible values: "dots", "progress" - * available reporters: https://npmjs.org/browse/keyword/karma-reporter - */ - reporters: ["mocha"], - - mochaReporter: { - output: "minimal" - }, - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - /* - * level of logging - * possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - */ - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: false, - - - /* - * start these browsers - * available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - */ - browsers: ["HeadlessChrome"], - customLaunchers: { - HeadlessChrome: { - base: "ChromeHeadless", - flags: ["--no-sandbox"] - } - }, - - /* - * Continuous Integration mode - * if true, Karma captures browsers, runs the tests and exits - */ - singleRun: true, - - /* - * Concurrency level - * how many browser should be started simultaneous - */ - concurrency: Infinity - }); -}; diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index 0b5858fb30f3..eee5b40bd07b 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -9,7 +9,8 @@ // Requirements //----------------------------------------------------------------------------- -const ajv = require("../shared/ajv")(); +const ajvImport = require("../shared/ajv"); +const ajv = ajvImport(); const { parseRuleId, getRuleFromConfig, diff --git a/package.json b/package.json index d52a2aef60ce..4c22a4a94f09 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,11 @@ "devDependencies": { "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", + "@wdio/browser-runner": "^8.14.6", + "@wdio/cli": "^8.14.6", + "@wdio/concise-reporter": "^8.14.0", + "@wdio/globals": "^8.14.6", + "@wdio/mocha-framework": "^8.14.0", "babel-loader": "^8.0.5", "c8": "^7.12.0", "chai": "^4.0.1", @@ -124,11 +129,6 @@ "glob": "^7.1.6", "got": "^11.8.3", "gray-matter": "^4.0.3", - "karma": "^6.1.1", - "karma-chrome-launcher": "^3.1.0", - "karma-mocha": "^2.0.1", - "karma-mocha-reporter": "^2.2.5", - "karma-webpack": "^5.0.0", "lint-staged": "^11.0.0", "load-perf": "^0.2.0", "markdownlint": "^0.25.1", @@ -148,12 +148,14 @@ "pirates": "^4.0.5", "progress": "^2.0.3", "proxyquire": "^2.0.1", - "puppeteer": "^13.7.0", "recast": "^0.20.4", "regenerator-runtime": "^0.13.2", + "rollup-plugin-node-polyfills": "^0.2.1", "semver": "^7.5.3", "shelljs": "^0.8.2", "sinon": "^11.0.0", + "vite-plugin-commonjs": "^0.8.2", + "webdriverio": "^8.14.6", "webpack": "^5.23.0", "webpack-cli": "^4.5.0", "yorkie": "^2.0.0" diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index f86e98ba8e0d..65957f82a756 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -9,7 +9,7 @@ // Requirements //------------------------------------------------------------------------------ -const assert = require("chai").assert, +const { assert } = require("chai"), sinon = require("sinon"), espree = require("espree"), esprima = require("esprima"), @@ -7263,12 +7263,12 @@ var a = "test2"; it("should have file path passed to it", () => { const code = "/* this is code */"; - const parseSpy = sinon.spy(testParsers.stubParser, "parse"); + const parseSpy = { parse: sinon.spy() }; - linter.defineParser("stub-parser", testParsers.stubParser); + linter.defineParser("stub-parser", parseSpy); linter.verify(code, { parser: "stub-parser" }, filename, true); - sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); + sinon.assert.calledWithMatch(parseSpy.parse, "", { filePath: filename }); }); it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { @@ -8068,16 +8068,16 @@ describe("Linter with FlatConfigArray", () => { it("should have file path passed to it", () => { const code = "/* this is code */"; - const parseSpy = sinon.spy(testParsers.stubParser, "parse"); + const parseSpy = { parse: sinon.spy() }; const config = { languageOptions: { - parser: testParsers.stubParser + parser: parseSpy } }; linter.verify(code, config, filename, true); - sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); + sinon.assert.calledWithMatch(parseSpy.parse, "", { filePath: filename }); }); it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { diff --git a/wdio.conf.js b/wdio.conf.js new file mode 100644 index 000000000000..f32d757133e6 --- /dev/null +++ b/wdio.conf.js @@ -0,0 +1,387 @@ +"use strict"; + +const path = require("path"); +const commonjs = require("vite-plugin-commonjs").default; + +exports.config = { + + /* + * + * ==================== + * Runner Configuration + * ==================== + * WebdriverIO supports running e2e tests as well as unit and component tests. + */ + runner: ["browser", { + viteConfig: { + resolve: { + alias: { + util: "rollup-plugin-node-polyfills/polyfills/util", + path: "rollup-plugin-node-polyfills/polyfills/path", + assert: "rollup-plugin-node-polyfills/polyfills/assert" + } + }, + plugins: [ + commonjs(), + { + name: "wdio:import-fix", + enforce: "pre", + transform(source, id) { + if (!id.endsWith("/tests/lib/linter/linter.js")) { + return source; + } + + return source.replace( + 'const { Linter } = require("../../../lib/linter");', + 'const { Linter } = require("../../../build/eslint");\n' + + 'process.cwd = () => "/";' + ); + } + } + ] + } + }], + + /* + * + * ================== + * Specify Test Files + * ================== + * Define which test specs should run. The pattern is relative to the directory + * of the configuration file being run. + * + * The specs are defined as an array of spec files (optionally using wildcards + * that will be expanded). The test for each spec file will be run in a separate + * worker process. In order to have a group of spec files run in the same worker + * process simply enclose them in an array within the specs array. + * + * If you are calling `wdio` from an NPM script (see https://docs.npmjs.com/cli/run-script), + * then the current working directory is where your `package.json` resides, so `wdio` + * will be called from there. + * + */ + specs: [ + path.join(__dirname, "tests", "lib", "linter", "linter.js") + ], + + // Patterns to exclude. + exclude: [], + + /* + * + * ============ + * Capabilities + * ============ + * Define your capabilities here. WebdriverIO can run multiple capabilities at the same + * time. Depending on the number of capabilities, WebdriverIO launches several test + * sessions. Within your capabilities you can overwrite the spec and exclude options in + * order to group specific specs to a specific capability. + * + * First, you can define how many instances should be started at the same time. Let"s + * say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have + * set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec + * files and you set maxInstances to 10, all spec files will get tested at the same time + * and 30 processes will get spawned. The property handles how many capabilities + * from the same test should run tests. + * + */ + maxInstances: 10, + + /* + * + * If you have trouble getting all important capabilities together, check out the + * Sauce Labs platform configurator - a great tool to configure your capabilities: + * https://saucelabs.com/platform/platform-configurator + * + */ + capabilities: [{ + browserName: "chrome", + "goog:chromeOptions": { + args: process.env.CI ? ["headless", "disable-gpu"] : [] + } + }], + + /* + * + * =================== + * Test Configurations + * =================== + * Define all options that are relevant for the WebdriverIO instance here + * + * Level of logging verbosity: trace | debug | info | warn | error | silent + */ + logLevel: "trace", + outputDir: "./wdio-logs", + + /* + * + * Set specific log levels per logger + * loggers: + * - webdriver, webdriverio + * - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service + * - @wdio/mocha-framework, @wdio/jasmine-framework + * - @wdio/local-runner + * - @wdio/sumologic-reporter + * - @wdio/cli, @wdio/config, @wdio/utils + * Level of logging verbosity: trace | debug | info | warn | error | silent + * logLevels: { + * webdriver: 'info', + * '@wdio/appium-service': 'info' + * }, + * + * If you only want to run your tests until a specific amount of tests have failed use + * bail (default is 0 - don't bail, run all tests). + */ + bail: 0, + + /* + * + * Set a base URL in order to shorten url command calls. If your `url` parameter starts + * with `/`, the base url gets prepended, not including the path portion of your baseUrl. + * If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url + * gets prepended directly. + */ + baseUrl: "", + + /* + * + * Default timeout for all waitFor* commands. + */ + waitforTimeout: 10000, + + /* + * + * Default timeout in milliseconds for request + * if browser driver or grid doesn't send response + */ + connectionRetryTimeout: 120000, + + /* + * + * Default request retries count + */ + connectionRetryCount: 3, + + /* + * Framework you want to run your specs with. + * The following are supported: Mocha, Jasmine, and Cucumber + * see also: https://webdriver.io/docs/frameworks + * + * Make sure you have the wdio adapter package for the specific framework installed + * before running any tests. + */ + framework: "mocha", + + /* + * + * The number of times to retry the entire specfile when it fails as a whole + * specFileRetries: 1, + * + * Delay in seconds between the spec file retry attempts + * specFileRetriesDelay: 0, + * + * Whether or not retried specfiles should be retried immediately or deferred to the end of the queue + * specFileRetriesDeferred: false, + * + * Test reporter for stdout. + * The only one supported by default is 'dot' + * see also: https://webdriver.io/docs/dot-reporter + */ + reporters: ["concise"], + + /* + * + * Options to be passed to Mocha. + * See the full list at http://mochajs.org/ + */ + mochaOpts: { + ui: "bdd", + timeout: 5 * 60 * 1000, // 5min + grep: "@skipWeb", + invert: true + } + + /* + * + * ===== + * Hooks + * ===== + * WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance + * it and to build services around it. You can either apply a single function or an array of + * methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got + * resolved to continue. + */ + /** + * Gets executed once before all workers get launched. + * @param {Object} config wdio configuration object + * @param {Array} capabilities list of capabilities details + */ + /* + * onPrepare: function (config, capabilities) { + * }, + */ + /** + * Gets executed before a worker process is spawned and can be used to initialise specific service + * for that worker as well as modify runtime environments in an async fashion. + * @param {string} cid capability id (e.g 0-0) + * @param {Object} caps object containing capabilities for session that will be spawn in the worker + * @param {Object} specs specs to be run in the worker process + * @param {Object} args object that will be merged with the main configuration once worker is initialized + * @param {Object} execArgv list of string arguments passed to the worker process + */ + /* + * onWorkerStart: function (cid, caps, specs, args, execArgv) { + * }, + */ + /** + * Gets executed just after a worker process has exited. + * @param {string} cid capability id (e.g 0-0) + * @param {number} exitCode 0 - success, 1 - fail + * @param {Object} specs specs to be run in the worker process + * @param {number} retries number of retries used + */ + /* + * onWorkerEnd: function (cid, exitCode, specs, retries) { + * }, + */ + /** + * Gets executed just before initialising the webdriver session and test framework. It allows you + * to manipulate configurations depending on the capability or spec. + * @param {Object} config wdio configuration object + * @param {Array} capabilities list of capabilities details + * @param {Array} specs List of spec file paths that are to be run + * @param {string} cid worker id (e.g. 0-0) + */ + /* + * beforeSession: function (config, capabilities, specs, cid) { + * }, + */ + /** + * Gets executed before test execution begins. At this point you can access to all global + * variables like `browser`. It is the perfect place to define custom commands. + * @param {Array} capabilities list of capabilities details + * @param {Array} specs List of spec file paths that are to be run + * @param {Object} browser instance of created browser/device session + */ + /* + * before: function (capabilities, specs) { + * }, + */ + /** + * Runs before a WebdriverIO command gets executed. + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + */ + /* + * beforeCommand: function (commandName, args) { + * }, + */ + /** + * Hook that gets executed before the suite starts + * @param {Object} suite suite details + */ + /* + * beforeSuite: function (suite) { + * }, + */ + /** + * Function to be executed before a test (in Mocha/Jasmine) starts. + */ + /* + * beforeTest: function (test, context) { + * }, + */ + /** + * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling + * beforeEach in Mocha) + */ + /* + * beforeHook: function (test, context) { + * }, + */ + /** + * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling + * afterEach in Mocha) + */ + /* + * afterHook: function (test, context, { error, result, duration, passed, retries }) { + * }, + */ + /** + * Function to be executed after a test (in Mocha/Jasmine only) + * @param {Object} test test object + * @param {Object} context scope object the test was executed with + * @param {Error} result.error error object in case the test fails, otherwise `undefined` + * @param {any} result.result return object of test function + * @param {number} result.duration duration of test + * @param {boolean} result.passed true if test has passed, otherwise false + * @param {Object} result.retries informations to spec related retries, e.g. `{ attempts: 0, limit: 0 }` + */ + /* + * afterTest: function(test, context, { error, result, duration, passed, retries }) { + * }, + */ + + + /** + * Hook that gets executed after the suite has ended + * @param {Object} suite suite details + */ + /* + * afterSuite: function (suite) { + * }, + */ + /** + * Runs after a WebdriverIO command gets executed + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + * @param {number} result 0 - command success, 1 - command error + * @param {Object} error error object if any + */ + /* + * afterCommand: function (commandName, args, result, error) { + * }, + */ + /** + * Gets executed after all tests are done. You still have access to all global variables from + * the test. + * @param {number} result 0 - test pass, 1 - test fail + * @param {Array} capabilities list of capabilities details + * @param {Array} specs List of spec file paths that ran + */ + /* + * after: function (result, capabilities, specs) { + * }, + */ + /** + * Gets executed right after terminating the webdriver session. + * @param {Object} config wdio configuration object + * @param {Array} capabilities list of capabilities details + * @param {Array} specs List of spec file paths that ran + */ + /* + * afterSession: function (config, capabilities, specs) { + * }, + */ + /** + * Gets executed after all workers got shut down and the process is about to exit. An error + * thrown in the onComplete hook will result in the test run failing. + * @param {Object} exitCode 0 - success, 1 - fail + * @param {Object} config wdio configuration object + * @param {Array} capabilities list of capabilities details + * @param {Object} results object containing test results + */ + /* + * onComplete: function(exitCode, config, capabilities, results) { + * }, + */ + /** + * Gets executed when a refresh happens. + * @param {string} oldSessionId session ID of the old session + * @param {string} newSessionId session ID of the new session + */ + /* + * onReload: function(oldSessionId, newSessionId) { + * } + */ +}; From a40fa509922b36bb986eb1be9394591f84f62d9e Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Fri, 1 Sep 2023 02:39:45 +0200 Subject: [PATCH 3/9] chore: use eslint-plugin-jsdoc's flat config (#17516) --- packages/eslint-config-eslint/base.js | 3 +-- packages/eslint-config-eslint/package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/eslint-config-eslint/base.js b/packages/eslint-config-eslint/base.js index 1365108390d5..083336919e18 100644 --- a/packages/eslint-config-eslint/base.js +++ b/packages/eslint-config-eslint/base.js @@ -9,7 +9,6 @@ const unicorn = require("eslint-plugin-unicorn"); * the plugins' configs are not updated to support the flat config, * need to manually update the `plugins` property */ -jsdoc.configs.recommended.plugins = { jsdoc }; eslintComments.configs.recommended.plugins = { "eslint-comments": eslintComments }; // extends eslint recommended config @@ -262,7 +261,7 @@ const jsConfigs = [js.configs.recommended, { }]; // extends eslint-plugin-jsdoc's recommended config -const jsdocConfigs = [jsdoc.configs.recommended, { +const jsdocConfigs = [jsdoc.configs["flat/recommended"], { settings: { jsdoc: { mode: "typescript", diff --git a/packages/eslint-config-eslint/package.json b/packages/eslint-config-eslint/package.json index 7d8a747537c8..f8c7fab87686 100644 --- a/packages/eslint-config-eslint/package.json +++ b/packages/eslint-config-eslint/package.json @@ -30,7 +30,7 @@ "dependencies": { "@eslint/js": "^8.42.0", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-jsdoc": "^46.2.5", + "eslint-plugin-jsdoc": "^46.5.1", "eslint-plugin-n": "^16.0.0", "eslint-plugin-unicorn": "^42.0.0" }, From cd7da5cc3154f86f7ca45fb58929d27a7af359ed Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Fri, 1 Sep 2023 08:06:33 +0000 Subject: [PATCH 4/9] docs: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ce3a4a5a832..818a06e25ed6 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,7 @@ The following companies, organizations, and individuals support ESLint's ongoing

Chrome Frameworks Fund Automattic

Gold Sponsors

Salesforce Airbnb

Silver Sponsors

Sentry Liftoff American Express

Bronze Sponsors

-

ThemeIsle Nx (by Nrwl) Anagram Solver Icons8: free icons, photos, illustrations, and music Discord GitHub Transloadit Ignition HeroCoders QuickBooks Tool hub

+

ThemeIsle Nx (by Nrwl) Anagram Solver Icons8 Discord GitHub Transloadit Ignition HeroCoders QuickBooks Tool hub

## Technology Sponsors From 032c4b1476a7b8cfd917a66772d2221950ea87eb Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Fri, 1 Sep 2023 14:26:02 -0400 Subject: [PATCH 5/9] docs: add typescript template (#17500) * docs: add typescript template * fix: formatting * chore: update docs with AndreaPontrandolfo's findings * Update no-import-assign.md * chore: update no-redeclare * fix: update TS code numbers * feat: cleanup typescript error codes * Update docs/src/_includes/layouts/doc.html Co-authored-by: Nicholas C. Zakas * Update docs/src/rules/no-redeclare.md Co-authored-by: Nicholas C. Zakas * Update docs/src/rules/no-import-assign.md Co-authored-by: Nicholas C. Zakas * Update docs/src/rules/no-invalid-this.md Co-authored-by: Nicholas C. Zakas * fix: formatting error --------- Co-authored-by: Nicholas C. Zakas --- docs/src/_includes/layouts/doc.html | 21 +++++++++++++++++++-- docs/src/rules/constructor-super.md | 3 +-- docs/src/rules/getter-return.md | 1 + docs/src/rules/no-const-assign.md | 1 + docs/src/rules/no-dupe-args.md | 1 + docs/src/rules/no-dupe-class-members.md | 3 +-- docs/src/rules/no-dupe-keys.md | 1 + docs/src/rules/no-func-assign.md | 1 + docs/src/rules/no-import-assign.md | 2 ++ docs/src/rules/no-invalid-this.md | 2 ++ docs/src/rules/no-new-symbol.md | 1 + docs/src/rules/no-obj-calls.md | 1 + docs/src/rules/no-redeclare.md | 2 ++ docs/src/rules/no-setter-return.md | 1 + docs/src/rules/no-this-before-super.md | 1 + docs/src/rules/no-undef.md | 1 + docs/src/rules/no-unreachable.md | 1 + docs/src/rules/no-unsafe-negation.md | 1 + 18 files changed, 39 insertions(+), 6 deletions(-) diff --git a/docs/src/_includes/layouts/doc.html b/docs/src/_includes/layouts/doc.html index 4050a901063d..58d8986a5dcc 100644 --- a/docs/src/_includes/layouts/doc.html +++ b/docs/src/_includes/layouts/doc.html @@ -19,6 +19,22 @@ {% set added_version = rule_versions.added[title] %} {% set removed_version = rule_versions.removed[title] %} + {% if handled_by_typescript %} + {% set handled_by_typescript_content %} +

Handled by TypeScript

+

+ It is safe to disable this rule when using TypeScript because TypeScript's compiler enforces this check. +

+ {% if extra_typescript_info %} +

+ {{ extra_typescript_info | markdown | safe }} +

+ {% endif %} + {% endset %} + + {% set all_content = [all_content, handled_by_typescript_content] | join %} + {% endif %} + {% if related_rules %} {% set related_rules_content %} @@ -48,7 +64,7 @@

Further Reading

{% set all_content = [all_content, further_reading_content] | join %} {% endif %} - + {% if rule_meta %} {% set resources_content %}

Resources

@@ -76,7 +92,7 @@

{{ title }}

{% endif %} {% include 'components/docs-toc.html' %} - + {{ all_content | safe }} @@ -102,6 +118,7 @@

{{ title }}

{% include "partials/docs-footer.html" %} + diff --git a/docs/src/rules/constructor-super.md b/docs/src/rules/constructor-super.md index c172b0a7c279..7c19df77dbab 100644 --- a/docs/src/rules/constructor-super.md +++ b/docs/src/rules/constructor-super.md @@ -1,6 +1,7 @@ --- title: constructor-super rule_type: problem +handled_by_typescript: true --- Constructors of derived classes must call `super()`. @@ -69,5 +70,3 @@ class A extends B { ## When Not To Use It If you don't want to be notified about invalid/missing `super()` callings in constructors, you can safely disable this rule. - -It is safe to disable this rule when using TypeScript because TypeScript's compiler enforces this check (`ts(2335) & ts(2377)`). diff --git a/docs/src/rules/getter-return.md b/docs/src/rules/getter-return.md index 0c8937d14ccb..9d316303d025 100644 --- a/docs/src/rules/getter-return.md +++ b/docs/src/rules/getter-return.md @@ -1,6 +1,7 @@ --- title: getter-return rule_type: problem +handled_by_typescript: true further_reading: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get - https://leanpub.com/understandinges6/read/#leanpub-auto-accessor-properties diff --git a/docs/src/rules/no-const-assign.md b/docs/src/rules/no-const-assign.md index f9f0ed172bb8..ca62132a757e 100644 --- a/docs/src/rules/no-const-assign.md +++ b/docs/src/rules/no-const-assign.md @@ -1,6 +1,7 @@ --- title: no-const-assign rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-dupe-args.md b/docs/src/rules/no-dupe-args.md index 79f791c4666c..3cb9133c83fa 100644 --- a/docs/src/rules/no-dupe-args.md +++ b/docs/src/rules/no-dupe-args.md @@ -1,6 +1,7 @@ --- title: no-dupe-args rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-dupe-class-members.md b/docs/src/rules/no-dupe-class-members.md index d50a7fe74e11..216f3c9fecd3 100644 --- a/docs/src/rules/no-dupe-class-members.md +++ b/docs/src/rules/no-dupe-class-members.md @@ -1,6 +1,7 @@ --- title: no-dupe-class-members rule_type: problem +handled_by_typescript: true --- @@ -101,5 +102,3 @@ class Foo { This rule should not be used in ES3/5 environments. In ES2015 (ES6) or later, if you don't want to be notified about duplicate names in class members, you can safely disable this rule. - -It is safe to disable this rule when using TypeScript because TypeScript's compiler enforces this check (`ts(2300) & ts(2393)`). diff --git a/docs/src/rules/no-dupe-keys.md b/docs/src/rules/no-dupe-keys.md index 75fc9491fb62..1527bf8f1ec7 100644 --- a/docs/src/rules/no-dupe-keys.md +++ b/docs/src/rules/no-dupe-keys.md @@ -1,6 +1,7 @@ --- title: no-dupe-keys rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-func-assign.md b/docs/src/rules/no-func-assign.md index ffbcb46c6b81..a0f146203742 100644 --- a/docs/src/rules/no-func-assign.md +++ b/docs/src/rules/no-func-assign.md @@ -1,6 +1,7 @@ --- title: no-func-assign rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-import-assign.md b/docs/src/rules/no-import-assign.md index ca4b912de817..b0a7432bb801 100644 --- a/docs/src/rules/no-import-assign.md +++ b/docs/src/rules/no-import-assign.md @@ -1,6 +1,8 @@ --- title: no-import-assign rule_type: problem +handled_by_typescript: true +extra_typescript_info: Note that the compiler will not catch the `Object.assign()` case. Thus, if you use `Object.assign()` in your codebase, this rule will still provide some value. --- diff --git a/docs/src/rules/no-invalid-this.md b/docs/src/rules/no-invalid-this.md index f3aa6ed763ed..9e4a2aedffae 100644 --- a/docs/src/rules/no-invalid-this.md +++ b/docs/src/rules/no-invalid-this.md @@ -1,6 +1,8 @@ --- title: no-invalid-this rule_type: suggestion +handled_by_typescript: true +extra_typescript_info: Note that, technically, TypeScript will only catch this if you have the `strict` or `noImplicitThis` flags enabled. These are enabled in most TypeScript projects, since they are considered to be best practice. --- diff --git a/docs/src/rules/no-new-symbol.md b/docs/src/rules/no-new-symbol.md index 44c34a4eef00..d557811f30c4 100644 --- a/docs/src/rules/no-new-symbol.md +++ b/docs/src/rules/no-new-symbol.md @@ -1,6 +1,7 @@ --- title: no-new-symbol rule_type: problem +handled_by_typescript: true further_reading: - https://www.ecma-international.org/ecma-262/6.0/#sec-symbol-objects --- diff --git a/docs/src/rules/no-obj-calls.md b/docs/src/rules/no-obj-calls.md index 8d72e4ce2770..2fde92e5cbf5 100644 --- a/docs/src/rules/no-obj-calls.md +++ b/docs/src/rules/no-obj-calls.md @@ -1,6 +1,7 @@ --- title: no-obj-calls rule_type: problem +handled_by_typescript: true further_reading: - https://es5.github.io/#x15.8 --- diff --git a/docs/src/rules/no-redeclare.md b/docs/src/rules/no-redeclare.md index e66f0570fc66..009ba889fcc3 100644 --- a/docs/src/rules/no-redeclare.md +++ b/docs/src/rules/no-redeclare.md @@ -1,6 +1,8 @@ --- title: no-redeclare rule_type: suggestion +handled_by_typescript: true +extra_typescript_info: Note that while TypeScript will catch `let` redeclares and `const` redeclares, it will not catch `var` redeclares. Thus, if you use the legacy `var` keyword in your TypeScript codebase, this rule will still provide some value. related_rules: - no-shadow --- diff --git a/docs/src/rules/no-setter-return.md b/docs/src/rules/no-setter-return.md index ceb4558c15ac..50353c754dab 100644 --- a/docs/src/rules/no-setter-return.md +++ b/docs/src/rules/no-setter-return.md @@ -1,6 +1,7 @@ --- title: no-setter-return rule_type: problem +handled_by_typescript: true related_rules: - getter-return further_reading: diff --git a/docs/src/rules/no-this-before-super.md b/docs/src/rules/no-this-before-super.md index f1425b5ed35b..c1a654796cb6 100644 --- a/docs/src/rules/no-this-before-super.md +++ b/docs/src/rules/no-this-before-super.md @@ -1,6 +1,7 @@ --- title: no-this-before-super rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-undef.md b/docs/src/rules/no-undef.md index 0bc7a28279fb..72ea516f966b 100644 --- a/docs/src/rules/no-undef.md +++ b/docs/src/rules/no-undef.md @@ -1,6 +1,7 @@ --- title: no-undef rule_type: problem +handled_by_typescript: true related_rules: - no-global-assign - no-redeclare diff --git a/docs/src/rules/no-unreachable.md b/docs/src/rules/no-unreachable.md index 4f762084b41e..15f77f8173ee 100644 --- a/docs/src/rules/no-unreachable.md +++ b/docs/src/rules/no-unreachable.md @@ -1,6 +1,7 @@ --- title: no-unreachable rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-unsafe-negation.md b/docs/src/rules/no-unsafe-negation.md index 522e4ab4d1a6..f3b495223168 100644 --- a/docs/src/rules/no-unsafe-negation.md +++ b/docs/src/rules/no-unsafe-negation.md @@ -1,6 +1,7 @@ --- title: no-unsafe-negation rule_type: problem +handled_by_typescript: true --- From acb7df35b9a7485f26bc6b3e1f9083d1c585dce9 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 2 Sep 2023 22:00:32 +0530 Subject: [PATCH 6/9] feat: add new `enforce` option to `lines-between-class-members` (#17462) * feat: add new \`enforce\` option (`lines-between-class-members`) * test: add cases for enforce with exceptAfterSingleLine option * docs: add `enforce` option * docs: fix example * fix: update schema to make enforce option required * refactor: remove redundant if condition * refactor: remove redundant if condition * docs: add suggestions Co-authored-by: Milos Djermanovic * test: add cases where multiple config objects match a pair --------- Co-authored-by: Milos Djermanovic --- docs/src/rules/lines-between-class-members.md | 185 +- lib/rules/lines-between-class-members.js | 99 +- .../lib/rules/lines-between-class-members.js | 2471 ++++++++++++++++- 3 files changed, 2714 insertions(+), 41 deletions(-) diff --git a/docs/src/rules/lines-between-class-members.md b/docs/src/rules/lines-between-class-members.md index 8daf2a2f78db..55627501096a 100644 --- a/docs/src/rules/lines-between-class-members.md +++ b/docs/src/rules/lines-between-class-members.md @@ -69,14 +69,19 @@ class MyClass { ### Options -This rule has a string option and an object option. +This rule has two options, first option can be string or object, second option is object. -String option: +First option can be string `"always"` or `"never"` or an object with a property named `enforce`: * `"always"`(default) require an empty line after class members * `"never"` disallows an empty line after class members +* `Object`: An object with a property named `enforce`. The enforce property should be an array of objects, each specifying the configuration for enforcing empty lines between specific pairs of class members. + * **enforce**: You can supply any number of configurations. If a member pair matches multiple configurations, the last matched configuration will be used. If a member pair does not match any configurations, it will be ignored. Each object should have the following properties: + * **blankLine**: Can be set to either `"always"` or `"never"`, indicating whether a blank line should be required or disallowed between the specified members. + * **prev**: Specifies the type of the preceding class member. It can be `"method"` for class methods, `"field"` for class fields, or `"*"` for any class member. + * **next**: Specifies the type of the following class member. It follows the same options as `prev`. -Object option: +Second option is an object with a property named `exceptAfterSingleLine`: * `"exceptAfterSingleLine": false`(default) **do not** skip checking empty lines after single-line class members * `"exceptAfterSingleLine": true` skip checking empty lines after single-line class members @@ -129,6 +134,146 @@ class Foo{ ::: +Examples of **incorrect** code for this rule with the array of configurations option: + +::: incorrect + +```js +// disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + +::: incorrect + +```js +// requires blank lines around fields, disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + +Examples of **correct** code for this rule with the array of configurations option: + +::: correct + +```js +// disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} +} +``` + +::: + +::: correct + +```js +// requires blank lines around fields, disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} +} +``` + +::: + Examples of **correct** code for this rule with the object option: ::: correct @@ -148,6 +293,40 @@ class Foo{ ::: +::: correct + +```js +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { exceptAfterSingleLine: true } +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + ## When Not To Use It If you don't want to enforce empty lines between class members, you can disable this rule. diff --git a/lib/rules/lines-between-class-members.js b/lib/rules/lines-between-class-members.js index dee4bab5f54b..3d0a5e6738e9 100644 --- a/lib/rules/lines-between-class-members.js +++ b/lib/rules/lines-between-class-members.js @@ -10,6 +10,21 @@ const astUtils = require("./utils/ast-utils"); +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Types of class members. + * Those have `test` method to check it matches to the given class member. + * @private + */ +const ClassMemberTypes = { + "*": { test: () => true }, + field: { test: node => node.type === "PropertyDefinition" }, + method: { test: node => node.type === "MethodDefinition" } +}; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -29,7 +44,32 @@ module.exports = { schema: [ { - enum: ["always", "never"] + anyOf: [ + { + type: "object", + properties: { + enforce: { + type: "array", + items: { + type: "object", + properties: { + blankLine: { enum: ["always", "never"] }, + prev: { enum: ["method", "field", "*"] }, + next: { enum: ["method", "field", "*"] } + }, + additionalProperties: false, + required: ["blankLine", "prev", "next"] + }, + minItems: 1 + } + }, + additionalProperties: false, + required: ["enforce"] + }, + { + enum: ["always", "never"] + } + ] }, { type: "object", @@ -55,6 +95,7 @@ module.exports = { options[0] = context.options[0] || "always"; options[1] = context.options[1] || { exceptAfterSingleLine: false }; + const configureList = typeof options[0] === "object" ? options[0].enforce : [{ blankLine: options[0], prev: "*", next: "*" }]; const sourceCode = context.sourceCode; /** @@ -144,6 +185,38 @@ module.exports = { return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0; } + /** + * Checks whether the given node matches the given type. + * @param {ASTNode} node The class member node to check. + * @param {string} type The class member type to check. + * @returns {boolean} `true` if the class member node matched the type. + * @private + */ + function match(node, type) { + return ClassMemberTypes[type].test(node); + } + + /** + * Finds the last matched configuration from the configureList. + * @param {ASTNode} prevNode The previous node to match. + * @param {ASTNode} nextNode The current node to match. + * @returns {string|null} Padding type or `null` if no matches were found. + * @private + */ + function getPaddingType(prevNode, nextNode) { + for (let i = configureList.length - 1; i >= 0; --i) { + const configure = configureList[i]; + const matched = + match(prevNode, configure.prev) && + match(nextNode, configure.next); + + if (matched) { + return configure.blankLine; + } + } + return null; + } + return { ClassBody(node) { const body = node.body; @@ -158,22 +231,34 @@ module.exports = { const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1; const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding); const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0); + const paddingType = getPaddingType(body[i], body[i + 1]); + + if (paddingType === "never" && isPadded) { + context.report({ + node: body[i + 1], + messageId: "never", - if ((options[0] === "always" && !skip && !isPadded) || - (options[0] === "never" && isPadded)) { + fix(fixer) { + if (hasTokenInPadding) { + return null; + } + return fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n"); + } + }); + } else if (paddingType === "always" && !skip && !isPadded) { context.report({ node: body[i + 1], - messageId: isPadded ? "never" : "always", + messageId: "always", + fix(fixer) { if (hasTokenInPadding) { return null; } - return isPadded - ? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n") - : fixer.insertTextAfter(curLineLastToken, "\n"); + return fixer.insertTextAfter(curLineLastToken, "\n"); } }); } + } } }; diff --git a/tests/lib/rules/lines-between-class-members.js b/tests/lib/rules/lines-between-class-members.js index feb9c085e37e..2ee17f713c3a 100644 --- a/tests/lib/rules/lines-between-class-members.js +++ b/tests/lib/rules/lines-between-class-members.js @@ -50,24 +50,810 @@ ruleTester.run("lines-between-class-members", rule, { "class C {\naaa;\n\n#bbb;\n\nccc(){}\n\n#ddd(){}\n}", { code: "class foo{ bar(){}\nbaz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n/*comments*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n//comments\nbaz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}/* comments\n\n*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}/* \ncomments\n*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n/* \ncomments\n*/\nbaz(){}}", options: ["never"] }, + { + code: "class foo{ bar(){}\n/*comments*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}\n//comments\nbaz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}/* comments\n\n*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}/* \ncomments\n*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}\n/* \ncomments\n*/\nbaz(){}}", + options: ["never"] + }, { code: "class foo{ bar(){}\n\nbaz(){}}", options: ["always"] }, - { code: "class foo{ bar(){}\n\n/*comments*/baz(){}}", options: ["always"] }, - { code: "class foo{ bar(){}\n\n//comments\nbaz(){}}", options: ["always"] }, + { + code: "class foo{ bar(){}\n\n/*comments*/baz(){}}", + options: ["always"] + }, + { + code: "class foo{ bar(){}\n\n//comments\nbaz(){}}", + options: ["always"] + }, - { code: "class foo{ bar(){}\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }] }, - { code: "class foo{ bar(){\n}\n\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }] }, - { code: "class foo{\naaa;\n#bbb;\nccc(){\n}\n\n#ddd(){\n}\n}", options: ["always", { exceptAfterSingleLine: true }] }, + { + code: "class foo{ bar(){}\nbaz(){}}", + options: ["always", { exceptAfterSingleLine: true }] + }, + { + code: "class foo{ bar(){\n}\n\nbaz(){}}", + options: ["always", { exceptAfterSingleLine: true }] + }, + { + code: "class foo{\naaa;\n#bbb;\nccc(){\n}\n\n#ddd(){\n}\n}", + options: ["always", { exceptAfterSingleLine: true }] + }, // semicolon-less style (semicolons are at the beginning of lines) { code: "class C { foo\n\n;bar }", options: ["always"] }, - { code: "class C { foo\n;bar }", options: ["always", { exceptAfterSingleLine: true }] }, - { code: "class C { foo\n;bar }", options: ["never"] } + { + code: "class C { foo\n;bar }", + options: ["always", { exceptAfterSingleLine: true }] + }, + { code: "class C { foo\n;bar }", options: ["never"] }, + + // enforce option with blankLine: "always" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "method" } + ] + } + ] + }, + + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "always", prev: "*", next: "*" }] } + ] + }, + + // enforce option - blankLine: "never" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "method" } + ] + } + ] + }, + + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [{ blankLine: "never", prev: "field", next: "*" }] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [{ blankLine: "never", prev: "*", next: "field" }] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "never", prev: "*", next: "*" }] } + ] + }, + + // enforce option - multiple configurations + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods, disallows blank lines between fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + + // requires blank lines around fields, disallows blank lines between methods + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "never", prev: "*", next: "method" }, + { blankLine: "never", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" }, + + // This should take precedence over the above + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + + // enforce with exceptAfterSingleLine option + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { + exceptAfterSingleLine: true + } + ] + } ], invalid: [ { @@ -75,97 +861,116 @@ ruleTester.run("lines-between-class-members", rule, { output: "class foo{ bar(){}\n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){}\n\nbaz(){}}", output: "class foo{ bar(){}\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){\n}\nbaz(){}}", output: "class foo{ bar(){\n}\n\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){\n}\n/* comment */\nbaz(){}}", output: "class foo{ bar(){\n}\n\n/* comment */\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){}\n\n// comment\nbaz(){}}", output: "class foo{ bar(){}\n// comment\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n/* comment */\nbaz(){}}", output: "class foo{ bar(){}\n/* comment */\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n/* comment-2 */\nbaz(){}}", output: "class foo{ bar(){}\n/* comment-1 */\n/* comment-2 */\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n/* comment */\n\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n// comment\n\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n/* comment-2 */\n\n/* comment-3 */\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n;\n\n/* comment-3 */\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class A {\nfoo() {}// comment\n;\n/* comment */\nbar() {}\n}", output: "class A {\nfoo() {}// comment\n\n;\n/* comment */\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class A {\nfoo() {}\n/* comment */;\n;\n/* comment */\nbar() {}\n}", output: "class A {\nfoo() {}\n\n/* comment */;\n;\n/* comment */\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){};\nbaz(){}}", output: "class foo{ bar(){};\n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){} // comment \nbaz(){}}", output: "class foo{ bar(){} // comment \n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class A {\nfoo() {}\n/* comment */;\n;\nbar() {}\n}", output: "class A {\nfoo() {}\n\n/* comment */;\n;\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\nfield1\nfield2\n}", output: "class C {\nfield1\n\nfield2\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\n#field1\n#field2\n}", output: "class C {\n#field1\n\n#field2\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\nfield1\n\nfield2\n}", output: "class C {\nfield1\nfield2\n}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class C {\nfield1 = () => {\n}\nfield2\nfield3\n}", output: "class C {\nfield1 = () => {\n}\n\nfield2\nfield3\n}", options: ["always", { exceptAfterSingleLine: true }], @@ -208,6 +1013,1610 @@ ruleTester.run("lines-between-class-members", rule, { output: "class C { foo\n\n;;bar }", options: ["always"], errors: [alwaysError] + }, + + // enforce option with blankLine: "always" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 11, + column: 17 + }, + { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 13, + column: 17 + }, + { + messageId: "always", + line: 16, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "*" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 11, + column: 17 + }, + { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 9, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "*" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "always", prev: "*", next: "*" }] } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + + // enforce option - blankLine: "never" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 11, + column: 17 + }, + { + messageId: "never", + line: 15, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "*" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; +method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 12, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 10, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; +method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "*" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 10, + column: 17 + }, + { + messageId: "never", + line: 12, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; +method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 12, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 10, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; +#fieldB = 'Field B'; +method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "never", prev: "*", next: "*" }] } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 10, + column: 17 + }, + { + messageId: "never", + line: 12, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + + // enforce option - multiple configurations + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods, disallows blank lines between fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "never", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 11, + column: 17 + }, { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + + // requires blank lines around fields, disallows blank lines between methods + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "never", + line: 11, + column: 17 + }, { + messageId: "never", + line: 15, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "never", prev: "*", next: "method" }, + { blankLine: "never", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" }, + + // This should take precedence over the above + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + + // enforce with exceptAfterSingleLine option + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { + exceptAfterSingleLine: true + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] } ] }); From 32b2327aafdd3b911fabab69ed75c9ff97658c60 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Sat, 2 Sep 2023 15:00:31 -0400 Subject: [PATCH 7/9] feat: Emit deprecation warnings in RuleTester (#17527) * feat: Emit deprecation warnings in RuleTester Emits deprecation warnings when using methods on `context` that are deprecated. Refs #17520 * Revert flat-rule-tester * Fix linting error --- lib/rule-tester/rule-tester.js | 62 +++++++++++++++++++++++++- tests/lib/rule-tester/rule-tester.js | 66 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 030a986867f3..c9c18664528c 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -164,6 +164,30 @@ const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters 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" +}; + /** * Clones a given value deeply. * Note: This ignores `parent` property. @@ -335,6 +359,22 @@ 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" + ); + } +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -566,7 +606,27 @@ class RuleTester { freezeDeeply(context.settings); freezeDeeply(context.parserOptions); - return (typeof rule === "function" ? rule : rule.create)(context); + const newContext = Object.freeze( + 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 + } + ])) + ) + ); + + return (typeof rule === "function" ? rule : rule.create)(newContext); } })); diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index c607e4476a02..e3c3d5a0061b 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -2489,6 +2489,72 @@ describe("RuleTester", () => { assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); + + 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" + }).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 203a971c0abc3a95ae02ff74104a01e569707060 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 01:30:03 +0200 Subject: [PATCH 8/9] ci: bump actions/checkout from 3 to 4 (#17530) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout 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/ci.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/update-readme.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92e2a7eed35e..7602b8380378 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: name: Verify Files runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: 'lts/*' @@ -53,7 +53,7 @@ jobs: node: "lts/*" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} @@ -68,7 +68,7 @@ jobs: name: Browser Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: '16' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e2995e66d404..637f06e2e517 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index e6399920b51d..43b783d3fcad 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: token: ${{ secrets.WORKFLOW_PUSH_BOT_TOKEN }} From de86b3b2e58edd5826200c23255d8325abe375e1 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 6 Sep 2023 15:32:41 +0530 Subject: [PATCH 9/9] docs: update `no-promise-executor-return` examples (#17529) * docs: update `no-promise-executor-return` examples * docs: update example --- docs/src/rules/no-promise-executor-return.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/rules/no-promise-executor-return.md b/docs/src/rules/no-promise-executor-return.md index d82e44734771..f163d9ebf0b4 100644 --- a/docs/src/rules/no-promise-executor-return.md +++ b/docs/src/rules/no-promise-executor-return.md @@ -38,6 +38,7 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-promise-executor-return: "error"*/ +/*eslint-env es6*/ new Promise((resolve, reject) => { if (someCondition) { @@ -75,6 +76,7 @@ Examples of **correct** code for this rule: ```js /*eslint no-promise-executor-return: "error"*/ +/*eslint-env es6*/ // Turn return inline into two lines new Promise((resolve, reject) => { @@ -123,6 +125,7 @@ Examples of **correct** code for this rule with the `{ "allowVoid": true }` opti ```js /*eslint no-promise-executor-return: ["error", { allowVoid: true }]*/ +/*eslint-env es6*/ new Promise((resolve, reject) => { if (someCondition) {