diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..352444a0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,63 @@ +'use strict'; + +module.exports = { + root: true, + plugins: ['node'], + extends: [ + 'not-an-aardvark/node', + 'plugin:unicorn/recommended', + 'plugin:node/recommended', + ], + rules: { + 'comma-dangle': [ + 'error', + { + arrays: 'always-multiline', + objects: 'always-multiline', + functions: 'never', // disallow trailing commas in function(es2017) + }, + ], + 'require-jsdoc': 'error', + + 'unicorn/consistent-function-scoping': 'off', + 'unicorn/no-array-callback-reference': 'off', + 'unicorn/no-array-for-each': 'off', + 'unicorn/no-array-reduce': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prefer-module': 'off', + 'unicorn/prevent-abbreviations': 'off', + }, + overrides: [ + { + // Apply eslint-plugin rules to our own rules/tests (but not docs). + files: ['lib/**/*.js', 'tests/**/*.js'], + plugins: ['self'], + extends: ['plugin:self/all'], + rules: { + 'self/report-message-format': ['error', '^[^a-z].*.$'], + 'self/require-meta-docs-url': 'off', + }, + }, + { + files: ['tests/**/*.js'], + env: { mocha: true }, + }, + { + files: ['**/*.md'], + processor: 'markdown/markdown', + }, + { + // Markdown JS code samples in documentation: + files: ['**/*.md/*.js'], + plugins: ['markdown'], + noInlineConfig: true, + rules: { + 'no-undef': 'off', + 'no-unused-vars': 'off', + strict: 'off', + + 'unicorn/filename-case': 'off', + }, + }, + ], +}; diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 07136078..00000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,20 +0,0 @@ -plugins: - - node - - self -extends: - - not-an-aardvark/node - - plugin:node/recommended - - plugin:self/all -root: true -rules: - comma-dangle: - - error - - arrays: always-multiline - objects: always-multiline - functions: never # disallow trailing commas in function(es2017) - require-jsdoc: error - self/meta-property-ordering: off - self/require-meta-docs-url: off - self/report-message-format: - - error - - '^[^a-z].*\.$' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a701f2d5..93d483ae 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,11 +12,9 @@ jobs: fail-fast: false matrix: node-version: + - '16' - '14' - '12' - - '12.0' - - '10' - - '10.12' os: - ubuntu-latest steps: @@ -34,3 +32,12 @@ jobs: - uses: actions/setup-node@v2 - run: npm install - run: npm run lint + + eslint6: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + - run: npm install + - run: npm install --save-dev eslint@6 + - run: npm test diff --git a/.gitignore b/.gitignore index 218bab36..32543cf0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ +.idea +.nyc_output +.vscode node_modules/ npm-debug.log -.vscode -.idea yarn.lock diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 00000000..aec8fd53 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,3 @@ +{ + "line-length": false +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 00000000..1b5d3aed --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,3 @@ +CHANGELOG.md +LICENSE.md +node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b53748..7f0d4967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,84 @@ # Changelog +## v3.6.1 (2021-09-24) + +* Fix: Change autofix to suggestion in `require-meta-schema` rule ([#185](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/185)) ([afc1514](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/afc15149779647265b23ba8c4c181376eeb58795)) +* Fix: only autofix in require-meta-schema rule when no options present ([#184](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/184)) ([d2d165d](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/d2d165dd70686f77a7204f9e4bf7a048a5e42942)) + +## v3.6.0 (2021-09-24) + +* New: Add `requireSchemaPropertyWhenOptionless` option to `require-meta-schema` rule ([#180](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/180)) ([483f78f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/483f78fb69f074189b78916efa8bf89f084f2f8a)) +* Fix: Remove erroneous schema from require-meta-schema rule ([#178](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/178)) ([2f9b2b0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/2f9b2b0671ebbcd3e76cf746d83371b131e375ac)) +* Test: Add CI test for ESLint 6 compatibility ([#174](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/174)) ([30bb8e2](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/30bb8e2231c79f5010cf53763482edc70ffb4507)) + +## v3.5.3 (2021-07-30) + +* Fix: False negative in `prefer-message-ids` rule ([#173](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/173)) ([c5c4b62](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c5c4b62be720768aeb13c55b003566d2c38211cb)) + +## v3.5.2 (2021-07-29) + +* Fix: support eslint v6 ([#172](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/172)) ([f7384ad](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/f7384ad76a0511cde29444256e16e9d273384cb0)) + +## v3.5.1 (2021-07-27) + +* Fix: Fix false positive with empty array variable in `require-meta-has-suggestions` rule ([#171](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/171)) ([fffa881](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/fffa881fdaf1575834832e0e16df2cddb913008c)) + +## v3.5.0 (2021-07-27) + +* Update: Add `catchNoFixerButFixableProperty` option (default false) to catch non-fixable rules that enable the fixable property in `require-meta-fixable` rule ([#165](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/165)) ([da652aa](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/da652aa2c13f55627503067968cc843a4732eb26)) +* New: Add new rule `prefer-message-ids` ([#170](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/170)) ([95021dd](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/95021dd03baefe8f06d959d45fefd23c893fa832)) + +## v3.4.0 (2021-07-12) + +* Fix: Ensure `require-meta-*` rules test null/undefined property values ([#164](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/164)) ([990f8f6](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/990f8f6ab1bb7bf60939e2455ea3fae086bb90ae)) +* Chore: Improve test coverage in a few places ([#167](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/167)) ([cb9276e](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/cb9276e8a6490bb7023d5a6cb9fc4be5971341cf)) +* Docs: Improve consistency of `require-meta-*` rule violation messages ([#166](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/166)) ([1da1acc](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/1da1acc3a0193087947b803f9f1a6233362511af)) +* Update: Add autofixer to `require-meta-has-suggestions` rule ([#168](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/168)) ([a0a39c6](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/a0a39c6bc33b74b1f399a9099f98248c0c6d9577)) +* Docs: Fix incorrect CLI option link ([#169](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/169)) ([639da89](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/639da89998bc30e68ee5bfe8c8a00671fead5c99)) +* Fix: Improve detection of static `url` strings in `require-meta-docs-url` rule ([#162](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/162)) ([0459f12](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/0459f1288cda0174ec4622862d3e6d60eaff3889)) +* Update: Fix false positives/negatives in `require-meta-fixable` rule ([#158](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/158)) ([dc29b03](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/dc29b03e4fa75827b2b009e29b9836c801f5f9f0)) +* Docs: Fix typo ([#160](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/160)) ([88cb2bf](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/88cb2bf11e5f8162b19934f7152a1a2adaef8f27)) +* Fix: Avoid crash with non-static value of `hasSuggestions` in `require-meta-has-suggestions` rule ([#163](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/163)) ([5c83cd9](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/5c83cd9d0b49adfc408936511b560f957d3cbbcb)) +* Chore: Fully adopt `messageId` in `require-meta-docs-description` rule ([#161](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/161)) ([b0b170e](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/b0b170e11c7d86bfb34a3412edacedaed35268ab)) +* Docs: Update rule descriptions for consistency ([#159](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/159)) ([c10afb8](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c10afb833c294195e83af4d018c7bf425379fc13)) +* Fix: Use token utilities from eslint-utils ([#156](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/156)) ([5ac45f0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/5ac45f08bb943c097d3d10ee088a4a8f4f8e4de8)) +* Fix: Fix false negatives and reporting location in `require-meta-type` ([#155](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/155)) ([7c0d1d0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/7c0d1d06050d3e68de7623a81d4022886ae457a6)) +* Chore: remove unnecessary ignore pattern from internal js linting ([#154](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/154)) ([4aa9aca](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/4aa9aca1dc27e224bb55fb6f1d5bdb565c73c6a1)) + +## v3.3.0 (2021-07-02) + +* Chore: improve test coverage of `no-identical-tests` rule ([#153](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/153)) ([281d4e5](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/281d4e58d48fc5fad6ba033df9d412cfcb8ed99c)) +* Docs: ensure rule doc titles match rule descriptions ([#147](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/147)) ([c55a956](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c55a95616dd2e1e832fb082c4e686f4dc271d931)) +* Update: add the plugin name to plugins prop of presets ([#91](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/91)) ([e825c56](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/e825c5692361da151e75b69b9dca196d30b6d465)) +* Docs: indicate rules with suggestions ([#146](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/146)) ([cd65a5c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/cd65a5cedc9adc5874b789ccfffe2ac2c2041abb)) +* New: add new rule `no-only-tests` ([#145](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/145)) ([f0ac31c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/f0ac31c967eb05dd5da95e5ef15c26ce48f6d976)) +* Fix: update fixer-return rule to handle arrow function expressions ([#144](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/144)) ([6762a3f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/6762a3fa61fde2e9ae43576bd695d31da7ab5736)) + +## v3.2.0 (2021-06-23) + +* Fix: Improve detection of fix functions that never return a fix in `fixer-return` rule ([#143](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/143)) ([65cfb2c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/65cfb2cd78484f2072bb1f150d07c6fa299579ed)) +* Chore: enforce minimum code coverage ([#142](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/142)) ([f136e4c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/f136e4ce5917730bbb175e159e26af737aa76523)) +* Chore: enable meta-property-ordering rule internally ([#139](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/139)) ([6c83ec6](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/6c83ec65fa3c8c5a1771ab2c5bcb1946a0c1d78f)) +* Update: Flag a violation when rule options are used but an empty schema is present in `require-meta-schema` rule ([#138](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/138)) ([6ffddd7](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/6ffddd703773be7d2afd5e33fc2529b836b7a56c)) +* Docs: add rule documentation consistency tests ([#137](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/137)) ([c7f8bee](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c7f8beedfc9c4df64660b1d25ed0498f8551dfcf)) +* Docs: add eslint-plugin-markdown for JavaScript code samples in documentation ([#134](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/134)) ([15ffada](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/15ffadae375f9f835b3f3a18b5aa1bbc07e5efbe)) +* Chore: add eslint-plugin-unicorn ([#133](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/133)) ([d71c8b3](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/d71c8b3833e8bbde01236396b40c91f0dc11531f)) +* Build: Add `markdownlint` for doc formatting ([#130](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/130)) ([5b0ce68](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/5b0ce688d3f423aaecb704a3e29239097599c0dd)) +* Build: run tests under Node 16 ([#132](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/132)) ([1368388](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/136838824c3b0ad81710034e13761af53b3e7525)) +* Fix: improve detection of static arguments of context.report() in several rules ([#129](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/129)) ([6d5be9f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/6d5be9fb3e2e4c4c19d0c20a8f4a33867573e3fa)) + +## v3.1.0 (2021-06-15) + +* Chore: Switch from `.eslintrc.yml` to `.eslintrc.js` ([#127](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/127)) ([c767ea3](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c767ea33a69a35a0cb7007b90f41c38c5877e94f)) +* Docs: clarify some wording in `require-meta-docs-url` rule doc ([#126](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/126)) ([6fab4c2](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/6fab4c21cc961a349f68fd17f83ca5d8ef6d7123)) +* Docs: elaborate on output assertion requirement and benefits in `consistent-output` rule doc ([#123](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/123)) ([2fe92b7](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/2fe92b70fe2282e480a029471e2ba43f5c3cbc8f)) +* Docs: add explanation to `prefer-output-null` rule doc ([#124](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/124)) ([72fc89d](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/72fc89d3c212551f96f13f85b9720a6096975d3c)) +* Docs: fix --fix link ([#125](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/125)) ([ac2259c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/ac2259cb622326e34a4ec3f317aa3ad8bf8fe0e7)) +* Docs: add links for deprecated and new styles in `prefer-object-rule` rule ([#122](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/122)) ([6e351c6](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/6e351c6563b22157510dd9c772d2b98c468d856e)) +* Docs: mention allowed values in require-meta-type rule doc ([#121](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/121)) ([63d46e2](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/63d46e2f450c3517d95ca0c41d934aa345bedcd2)) +* New: add new rule require-meta-has-suggestions ([#105](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/105)) ([ff0ae38](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/ff0ae38e95d79ff316d77dbc5fd41b7d27c45bb0)) +* Docs: fix broken links in changelog ([b6ce109](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/b6ce109f42e4b7f42d6c1b7f6ac54b24d7fec54c)) + ## v3.0.3 (2021-05-10) * Docs: rm global-installed usage ([#116](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/116)) ([1f99c7c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/1f99c7ce827f576ffba8a76fc8d2bee534648f8a)) @@ -62,7 +141,7 @@ * New: require-meta-type (fixes [#67](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/67)) ([#68](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/68)) ([7f87941](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/7f8794159aae178fdd6f069ed9d4dee27367633a)) * Update: ensure report-message-format checks formatting in meta.messages ([#72](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/72)) ([1ffb48a](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/1ffb48aec79c278562729698bff93493ee5ac20e)) -* Upgrade: dev dependcies to latest ([#69](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/69)) ([9dad54f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/9dad54f6b3e148068f6322011f4c5c63bd4178c0)) +* Upgrade: dev dependencies to latest ([#69](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/69)) ([9dad54f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/9dad54f6b3e148068f6322011f4c5c63bd4178c0)) * Breaking: require node >= 6 & eslint >= 5 (fixes [#70](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/70)) ([#71](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/71)) ([c1778af](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c1778af090dc88f122101e3cf6ea653b5bc49778)) ## v1.4.1 (2018-10-24) diff --git a/README.md b/README.md index 34bf6976..3ae66758 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ An ESLint plugin for linting ESLint plugins You'll first need to install [ESLint](https://eslint.org): -``` -$ npm i eslint --save-dev +```sh +npm i eslint --save-dev ``` Next, install `eslint-plugin-eslint-plugin`: -``` -$ npm install eslint-plugin-eslint-plugin --save-dev +```sh +npm install eslint-plugin-eslint-plugin --save-dev ``` ## Usage @@ -28,7 +28,6 @@ Add `eslint-plugin` to the plugins section of your `.eslintrc` configuration fil } ``` - Then configure the rules you want to use under the rules section. ```json @@ -41,33 +40,37 @@ Then configure the rules you want to use under the rules section. ## Supported Rules -✔️ indicates that a rule is recommended for all users. -🛠 indicates that a rule is fixable. +* ✔️ if the rule belongs to the `recommended` configuration +* 🛠 if some problems reported by the rule are automatically fixable by the `--fix` [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) option +* 💡 if some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions) -Name | ✔️ | 🛠 | Description ------ | ----- | ----- | ----- -[consistent-output](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/consistent-output.md) | | | enforce consistent use of output assertions in rule tests -[fixer-return](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md) | ✔️ | | require fixer function to always return a value. -[meta-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/meta-property-ordering.md) | | 🛠 | enforce the order of meta properties -[no-deprecated-context-methods](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-context-methods.md) | | 🛠 | disallow usage of deprecated methods on rule context objects -[no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 | disallow use of the deprecated context.report() API -[no-identical-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-identical-tests.md) | ✔️ | 🛠 | disallow identical tests -[no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md) | ✔️ | | disallow missing placeholders in rule report messages -[no-unused-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-placeholders.md) | ✔️ | | disallow unused placeholders in rule report messages -[no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 | disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken -[prefer-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md) | | 🛠 | disallow rule exports where the export is a function. -[prefer-output-null](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-output-null.md) | | 🛠 | disallow invalid RuleTester test cases with the output the same as the code. -[prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) | | | disallow template literals as report messages -[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) | | | require using replaceText instead of replaceTextRange. -[report-message-format](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/report-message-format.md) | | | enforce a consistent format for rule report messages -[require-meta-docs-description](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-description.md) | | | require rules to implement a meta.docs.description property with the correct format -[require-meta-docs-url](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-url.md) | | 🛠 | require rules to implement a meta.docs.url property -[require-meta-fixable](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-fixable.md) | ✔️ | | require rules to implement a meta.fixable property -[require-meta-schema](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-schema.md) | | 🛠 | require rules to implement a meta.schema property -[require-meta-type](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-type.md) | | | require rules to implement a meta.type property -[test-case-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-property-ordering.md) | | 🛠 | require the properties of a test case to be placed in a consistent order -[test-case-shorthand-strings](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-shorthand-strings.md) | | 🛠 | enforce consistent usage of shorthand strings for test cases with no options +Name | ✔️ | 🛠 | 💡 | Description +----- | ----- | ----- | ----- | ----- +[consistent-output](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/consistent-output.md) | | | | enforce consistent use of `output` assertions in rule tests +[fixer-return](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md) | ✔️ | | | require fixer functions to return a fix +[meta-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/meta-property-ordering.md) | | 🛠 | | enforce the order of meta properties +[no-deprecated-context-methods](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-context-methods.md) | | 🛠 | | disallow usage of deprecated methods on rule context objects +[no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 | | disallow the version of `context.report()` with multiple arguments +[no-identical-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-identical-tests.md) | ✔️ | 🛠 | | disallow identical tests +[no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md) | ✔️ | | | disallow missing placeholders in rule report messages +[no-only-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-only-tests.md) | | | 💡 | disallow the test case property `only` +[no-unused-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-placeholders.md) | ✔️ | | | disallow unused placeholders in rule report messages +[no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 | | disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` +[prefer-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-message-ids.md) | | | | require using `messageId` instead of `message` to report rule violations +[prefer-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md) | | 🛠 | | disallow rule exports where the export is a function +[prefer-output-null](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-output-null.md) | | 🛠 | | disallow invalid RuleTester test cases where the `output` matches the `code` +[prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) | | | | require using placeholders for dynamic report messages +[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) | | | | require using `replaceText()` instead of `replaceTextRange()` +[report-message-format](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/report-message-format.md) | | | | enforce a consistent format for rule report messages +[require-meta-docs-description](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-description.md) | | | | require rules to implement a `meta.docs.description` property with the correct format +[require-meta-docs-url](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-url.md) | | 🛠 | | require rules to implement a `meta.docs.url` property +[require-meta-fixable](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-fixable.md) | ✔️ | | | require rules to implement a `meta.fixable` property +[require-meta-has-suggestions](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-has-suggestions.md) | | 🛠 | | require suggestable rules to implement a `meta.hasSuggestions` property +[require-meta-schema](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-schema.md) | | | 💡 | require rules to implement a `meta.schema` property +[require-meta-type](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-type.md) | | | | require rules to implement a `meta.type` property +[test-case-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-property-ordering.md) | | 🛠 | | require the properties of a test case to be placed in a consistent order +[test-case-shorthand-strings](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-shorthand-strings.md) | | 🛠 | | enforce consistent usage of shorthand strings for test cases with no options ## Supported Presets diff --git a/build/generate-readme-table.js b/build/generate-readme-table.js index d714ef51..1cb49b9b 100644 --- a/build/generate-readme-table.js +++ b/build/generate-readme-table.js @@ -17,11 +17,12 @@ const expectedTableLines = Object.keys(rules) `[${ruleId}](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/${ruleId}.md)`, rule.meta.docs.recommended ? '✔️' : '', rule.meta.fixable ? '🛠' : '', + rule.meta.hasSuggestions ? '💡' : '', rule.meta.docs.description, ].join(' | ')); return lines; - }, ['Name | ✔️ | 🛠 | Description', '----- | ----- | ----- | -----']) + }, ['Name | ✔️ | 🛠 | 💡 | Description', '----- | ----- | ----- | ----- | -----']) .join('\n'); const readmeContents = fs.readFileSync(README_LOCATION, 'utf8'); diff --git a/docs/rules/consistent-output.md b/docs/rules/consistent-output.md index 38827aa4..72db0bcc 100644 --- a/docs/rules/consistent-output.md +++ b/docs/rules/consistent-output.md @@ -1,7 +1,11 @@ -# Enforce consistent use of output assertions in rule tests (consistent-output) +# Enforce consistent use of `output` assertions in rule tests (consistent-output) When writing tests for a fixable rule with `RuleTester`, you can assert the autofix output of your test cases. However, it can be easy to forget to assert the output of a particular test case. +[As of ESLint 7](https://eslint.org/docs/user-guide/migrating-to-7.0.0#additional-validation-added-to-the-ruletester-class), test cases that trigger an autofix are required to provide the `output` property. + +Even test that do not trigger an autofix can benefit from asserting that they have no autofix using `output: null`. + ## Rule Details This rule aims to ensure that if any invalid test cases have output assertions, then all test cases have output assertions. @@ -17,15 +21,14 @@ new RuleTester().run('example-rule', rule, { { code: 'foo', output: 'bar', - errors: ['baz'] + errors: ['baz'], }, { code: 'bar', - errors: ['baz'] - } - ] + errors: ['baz'], + }, + ], }); - ``` Examples of **correct** code for this rule: @@ -39,21 +42,20 @@ new RuleTester().run('example-rule', rule, { { code: 'foo', output: 'bar', - errors: ['baz'] + errors: ['baz'], }, { code: 'bar', output: 'qux', - errors: ['baz'] + errors: ['baz'], }, { code: 'foo', - output: null, - errors: ['baz'] - } - ] + output: null, // asserts that there is no autofix + errors: ['baz'], + }, + ], }); - ``` ## Options diff --git a/docs/rules/fixer-return.md b/docs/rules/fixer-return.md index edb0b263..6926c327 100644 --- a/docs/rules/fixer-return.md +++ b/docs/rules/fixer-return.md @@ -1,23 +1,26 @@ -# Enforces always return from a fixer function (fixer-return) +# Require fixer functions to return a fix (fixer-return) -In a fixable rule, missing return from a fixer function will not apply fixes. +✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule. + +In a [fixable](https://eslint.org/docs/developer-guide/working-with-rules#applying-fixes) rule, a fixer function is useless if it never returns anything. ## Rule Details -This rule enforces that fixer functions always return a value. +This rule enforces that a fixer function returns a fix in at least one situation. Examples of **incorrect** code for this rule: ```js /* eslint eslint-plugin/fixer-return: error */ + module.exports = { - create: function(context) { - context.report( { - fix: function(fixer) { - fixer.foo(); - } - }); - } + create (context) { + context.report({ + fix (fixer) { + fixer.insertTextAfter(node, 'foo'); + }, + }); + }, }; ``` @@ -25,17 +28,31 @@ Examples of **correct** code for this rule: ```js /* eslint eslint-plugin/fixer-return: error */ + module.exports = { - create: function(context) { - context.report( { - fix: function(fixer) { - return fixer.foo(); - } - }); - } + create (context) { + context.report({ + fix (fixer) { + return fixer.insertTextAfter(node, 'foo'); + }, + }); + }, }; ``` -## When Not To Use It +```js +/* eslint eslint-plugin/fixer-return: error */ -If you don't want to enforce always return from a fixer function, do not enable this rule. +module.exports = { + create (context) { + context.report({ + fix (fixer) { + if (foo) { + return; // no autofix in this situation + } + return fixer.insertTextAfter(node, 'foo'); + }, + }); + }, +}; +``` diff --git a/docs/rules/meta-property-ordering.md b/docs/rules/meta-property-ordering.md index 2689e623..01b2ee9d 100644 --- a/docs/rules/meta-property-ordering.md +++ b/docs/rules/meta-property-ordering.md @@ -1,6 +1,6 @@ -# enforce ordering of meta properties in rule source (meta-property-ordering) +# Enforce the order of meta properties (meta-property-ordering) -(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule. +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. This rule enforces that meta properties of a rule are placed in a consistent order. @@ -23,23 +23,23 @@ Examples of **incorrect** code for this rule: // invalid; wrong order. module.exports = { meta: { - docs: "", - type: "problem", - fixable: "code", + docs: '', + type: 'problem', + fixable: 'code', }, - create() {}, -} + create () {}, +}; // invalid; extra properties must be placed afterwards. module.exports = { meta: { - type: "problem", - fooooooooo: "foo", - docs: "", - fixable: "code", + type: 'problem', + fooooooooo: 'foo', + docs: '', + fixable: 'code', }, - create() {}, -} + create () {}, +}; ``` Examples of **correct** code for this rule: @@ -52,13 +52,13 @@ Examples of **correct** code for this rule: // valid; module.exports = { meta: { - type: "bar", - docs: "foo", - messages: ["zoo"], - fooooooooo: "foo", + type: 'bar', + docs: 'foo', + messages: ['zoo'], + fooooooooo: 'foo', }, - create() {}, -} + create () {}, +}; ``` ## When Not To Use It diff --git a/docs/rules/no-deprecated-context-methods.md b/docs/rules/no-deprecated-context-methods.md index fe64b210..75bdfcba 100644 --- a/docs/rules/no-deprecated-context-methods.md +++ b/docs/rules/no-deprecated-context-methods.md @@ -1,6 +1,6 @@ -# Disallows usage of deprecated methods on rule context objects (no-deprecated-context-methods) +# Disallow usage of deprecated methods on rule context objects (no-deprecated-context-methods) -(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule. +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. This rule disallows the use of deprecated methods on rule `context` objects. @@ -35,29 +35,29 @@ Examples of **incorrect** code for this rule: ```js module.exports = { - create(context) { + create (context) { return { - Program(node) { + Program (node) { const firstToken = context.getFirstToken(node); - } - } - } -} + }, + }; + }, +}; ``` Examples of **correct** code for this rule: ```js module.exports = { - create(context) { + create (context) { const sourceCode = context.getSourceCode(); return { - Program(node) { + Program (node) { const firstToken = sourceCode.getFirstToken(node); - } - } - } + }, + }; + }, }; ``` diff --git a/docs/rules/no-deprecated-report-api.md b/docs/rules/no-deprecated-report-api.md index 507322c7..19231d59 100644 --- a/docs/rules/no-deprecated-report-api.md +++ b/docs/rules/no-deprecated-report-api.md @@ -1,43 +1,43 @@ -# disallow use of the deprecated context.report() API (no-deprecated-report-api) +# Disallow the version of `context.report()` with multiple arguments (no-deprecated-report-api) -(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule. +✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule. -ESLint has two APIs that rules can use to report problems. The [deprecated API](http://eslint.org/docs/developer-guide/working-with-rules-deprecated) accepts multiple arguments: `context.report(node, [loc], message)`. The ["new API"](http://eslint.org/docs/developer-guide/working-with-rules#contextreport) accepts a single argument: an object containing information about the reported problem. It is recommended that all rules use the new API. +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. + +ESLint has two APIs that rules can use to report problems. + +* The [deprecated API](http://eslint.org/docs/developer-guide/working-with-rules-deprecated) accepts multiple arguments: `context.report(node, [loc], message)`. +* The ["new API"](http://eslint.org/docs/developer-guide/working-with-rules#contextreport) accepts a single argument: an object containing information about the reported problem. + +It is recommended that all rules use the new API. ## Rule Details This rule aims to disallow use of the deprecated `context.report(node, [loc], message)` API. -The following patterns are considered warnings: +Examples of **incorrect** code for this rule: ```js module.exports = { - create(context) { - + create (context) { context.report(node, 'This node is bad.'); - - context.report(node, loc, 'This node is bad.'); - - } + }, }; ``` -The following patterns are not warnings: +Examples of **correct** code for this rule: ```js module.exports = { - create(context) { - + create (context) { context.report({ node, message: 'This node is bad.' }); context.report({ node, loc, message: 'This node is bad.' }); - - } + }, }; ``` - ## Further Reading * [Deprecated rule API](http://eslint.org/docs/developer-guide/working-with-rules-deprecated) diff --git a/docs/rules/no-identical-tests.md b/docs/rules/no-identical-tests.md index 77d90d93..5893c2d5 100644 --- a/docs/rules/no-identical-tests.md +++ b/docs/rules/no-identical-tests.md @@ -1,41 +1,41 @@ -# Disallow identical tests (no-identical-tests) - -(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule. - -When a rule has a lot of tests, it's sometimes difficult to tell if any tests are duplicates. This rule would warn if any test cases have the same properties. - -## Rule Details - -Examples of **incorrect** code for this rule: - -```js -/* eslint eslint-plugin/no-identical-tests: error */ - -new RuleTester().run('foo', bar, { -valid: [ - { code: 'foo' }, - { code: 'foo' } -], -invalid: [] -}); - -``` - -Examples of **correct** code for this rule: - -```js -/* eslint eslint-plugin/no-identical-tests: error */ - -new RuleTester().run('foo', bar, { -valid: [ - { code: 'foo' }, - { code: 'bar' } -], -invalid: [] -}); - -``` - -## When Not To Use It - -If you want to allow identical tests, do not enable this rule. +# Disallow identical tests (no-identical-tests) + +✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule. + +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. + +When a rule has a lot of tests, it's sometimes difficult to tell if any tests are duplicates. This rule would warn if any test cases have the same properties. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```js +/* eslint eslint-plugin/no-identical-tests: error */ + +new RuleTester().run('foo', bar, { + valid: [ + { code: 'foo' }, + { code: 'foo' }, + ], + invalid: [], +}); +``` + +Examples of **correct** code for this rule: + +```js +/* eslint eslint-plugin/no-identical-tests: error */ + +new RuleTester().run('foo', bar, { + valid: [ + { code: 'foo' }, + { code: 'bar' }, + ], + invalid: [], +}); +``` + +## When Not To Use It + +If you want to allow identical tests, do not enable this rule. diff --git a/docs/rules/no-missing-placeholders.md b/docs/rules/no-missing-placeholders.md index 52382336..5a6e6142 100644 --- a/docs/rules/no-missing-placeholders.md +++ b/docs/rules/no-missing-placeholders.md @@ -1,12 +1,14 @@ # Disallow missing placeholders in rule report messages (no-missing-placeholders) +✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule. + Report messages in rules can have placeholders surrounded by curly brackets. ```js context.report({ node, message: '{{disallowedNode}} nodes are not allowed.', - data: { disallowedNode: node.type } + data: { disallowedNode: node.type }, }); // Resulting message: e.g. 'IfStatement nodes are not allowed.' @@ -14,7 +16,6 @@ context.report({ However, if no `data` argument is provided, or no matching replacement key exists in the `data` argument, the raw curly brackets will end up in the report message. This is usually a mistake. - ## Rule Details This rule aims to disallow missing placeholders in rule report messages. @@ -22,52 +23,49 @@ This rule aims to disallow missing placeholders in rule report messages. Examples of **incorrect** code for this rule: ```js -/*eslint eslint-plugin/no-missing-placeholders: error*/ +/* eslint eslint-plugin/no-missing-placeholders: error*/ module.exports = { - create(context) { + create (context) { context.report({ node, - message: '{{something}} is wrong.' + message: '{{something}} is wrong.', }); context.report({ node, message: '{{something}} is wrong.', - data: { somethingElse: 'foo' } + data: { somethingElse: 'foo' }, }); context.report(node, '{{something}} is wrong.', { somethingElse: 'foo' }); - } + }, }; - ``` Examples of **correct** code for this rule: ```js -/*eslint eslint-plugin/no-missing-placeholders: error*/ +/* eslint eslint-plugin/no-missing-placeholders: error*/ module.exports = { - create(context) { + create (context) { context.report({ node, - message: 'something is wrong.' + message: 'something is wrong.', }); context.report({ node, message: '{{something}} is wrong.', - data: { something: 'foo' } + data: { something: 'foo' }, }); context.report(node, '{{something}} is wrong.', { something: 'foo' }); - } + }, }; - ``` - ## When Not To Use It If you want to use rule messages that actually contain double-curly bracket text, you should turn off this rule. diff --git a/docs/rules/no-only-tests.md b/docs/rules/no-only-tests.md new file mode 100644 index 00000000..0da555e6 --- /dev/null +++ b/docs/rules/no-only-tests.md @@ -0,0 +1,57 @@ +# Disallow the test case property `only` (no-only-tests) + +💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + +The [`only` property](https://eslint.org/docs/developer-guide/unit-tests#running-individual-tests) can be used as of [ESLint 7.29](https://eslint.org/blog/2021/06/eslint-v7.29.0-released#highlights) for running individual rule test cases with less-noisy debugging. This feature should be only used in development, as it prevents all the tests from running. Mistakenly checking-in a test case with this property can cause CI tests to incorrectly pass. + +## Rule Details + +This rule flags a violation when a test case is using `only`. Note that this rule is not autofixable since automatically deleting the property would prevent developers from being able to use it during development. + +Examples of **incorrect** code for this rule: + +```js +/* eslint eslint-plugin/no-only-tests: error */ + +const { RuleTester } = require('eslint'); +const ruleTester = new RuleTester(); + +ruleTester.run('my-rule', myRule, { + valid: [ + { + code: 'const valid = 42;', + only: true, + }, + RuleTester.only('const valid = 42;'), + ], + invalid: [ + { + code: 'const invalid = 42;', + only: true, + errors: [/* ... */], + }, + ], +}); +``` + +Examples of **correct** code for this rule: + +```js +/* eslint eslint-plugin/no-only-tests: error */ + +const { RuleTester } = require('eslint'); +const ruleTester = new RuleTester(); + +ruleTester.run('my-rule', myRule, { + valid: [ + 'const valid = 42;', + { code: 'const valid = 42;' }, + ], + invalid: [ + { + code: 'const invalid = 42;', + errors: [/* ... */], + }, + ], +}); +``` diff --git a/docs/rules/no-unused-placeholders.md b/docs/rules/no-unused-placeholders.md index cd6a6a93..00b96c64 100644 --- a/docs/rules/no-unused-placeholders.md +++ b/docs/rules/no-unused-placeholders.md @@ -1,50 +1,51 @@ # Disallow unused placeholders in rule report messages (no-unused-placeholders) +✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule. + This rule aims to disallow unused placeholders in rule report messages. ## Rule Details -Reports when a context.report call contains a data property that does not have a corresponding placeholder in the report message. +Reports when a `context.report()` call contains a data property that does not have a corresponding placeholder in the report message. Examples of **incorrect** code for this rule: ```js -/*eslint eslint-plugin/no-unused-placeholders: error*/ +/* eslint eslint-plugin/no-unused-placeholders: error*/ module.exports = { - create(context) { - + create (context) { context.report({ node, message: 'something is wrong.', - data: { something: 'foo' } + data: { something: 'foo' }, }); context.report(node, 'something is wrong.', { something: 'foo' }); - } + }, }; ``` Examples of **correct** code for this rule: ```js -/*eslint eslint-plugin/no-unused-placeholders: error*/ +/* eslint eslint-plugin/no-unused-placeholders: error*/ module.exports = { - create(context) { + create (context) { context.report({ node, - message: 'something is wrong.' + message: 'something is wrong.', }); context.report({ node, message: '{{something}} is wrong.', - data: { something: 'foo' } + data: { something: 'foo' }, }); context.report(node, '{{something}} is wrong.', { something: 'foo' }); - } + }, }; ``` diff --git a/docs/rules/no-useless-token-range.md b/docs/rules/no-useless-token-range.md index 93d61335..6bd4de80 100644 --- a/docs/rules/no-useless-token-range.md +++ b/docs/rules/no-useless-token-range.md @@ -1,10 +1,14 @@ -# Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken (no-useless-token-range) +# Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` (no-useless-token-range) + +✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule. + +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. AST nodes always start and end with tokens. As a result, the start index of the first token in a node is the same as the start index of the node itself, and the end index of the last token in a node is the same as the end index of the node itself. Using code like `sourceCode.getFirstToken(node).range[0]` unnecessarily hurts the performance of your rule, and makes your code less readable. ## Rule Details -This rule aims to avoid unnecessary calls to `sourceCode.getFirstToken` and `sourceCode.getLastToken`. +This rule aims to avoid unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`. Examples of **incorrect** code for this rule: @@ -12,12 +16,12 @@ Examples of **incorrect** code for this rule: /* eslint eslint-plugin/no-useless-token-range: error */ module.exports = { - create(context) { + create (context) { const sourceCode = context.getSourceCode(); const rangeStart = sourceCode.getFirstToken(node).range[0]; const rangeEnd = sourceCode.getLastToken(node).range[1]; - } + }, }; ``` @@ -27,12 +31,12 @@ Examples of **correct** code for this rule: /* eslint eslint-plugin/no-useless-token-range: error */ module.exports = { - create(context) { + create (context) { const sourceCode = context.getSourceCode(); const rangeStart = node.range[0]; const rangeEnd = node.range[1]; - } + }, }; ``` diff --git a/docs/rules/prefer-message-ids.md b/docs/rules/prefer-message-ids.md new file mode 100644 index 00000000..1c2ce5f1 --- /dev/null +++ b/docs/rules/prefer-message-ids.md @@ -0,0 +1,57 @@ +# Require using `messageId` instead of `message` to report rule violations (prefer-message-ids) + +When reporting a rule violation, it's preferred to provide the violation message with the `messageId` property instead of the `message` property. Message IDs provide the following benefits: + +* Rule violation messages can be stored in a central `meta.messages` object for convenient management +* Rule violation messages do not need to be repeated in both the rule file and rule test file + +## Rule Details + +This rule catches usages of the `message` property when reporting a rule violation. + +Examples of **incorrect** code for this rule: + +```js +/* eslint eslint-plugin/prefer-message-ids: error */ + +module.exports = { + create (context) { + return { + CallExpression (node) { + context.report({ + node, + message: 'Some error message.', + }); + }, + }; + }, +}; +``` + +Examples of **correct** code for this rule: + +```js +/* eslint eslint-plugin/prefer-message-ids: error */ + +module.exports = { + meta: { + messages: { + someMessageId: 'Some error message', + }, + }, + create (context) { + return { + CallExpression (node) { + context.report({ + node, + messageId: 'someMessageId', + }); + }, + }; + }, +}; +``` + +## Further Reading + +* [messageIds API](https://eslint.org/docs/developer-guide/working-with-rules#messageids) diff --git a/docs/rules/prefer-object-rule.md b/docs/rules/prefer-object-rule.md index 5ddd6def..095835ac 100644 --- a/docs/rules/prefer-object-rule.md +++ b/docs/rules/prefer-object-rule.md @@ -1,10 +1,10 @@ -# Disallow rule exports where the export is a function. (prefer-object-rule) +# Disallow rule exports where the export is a function (prefer-object-rule) -(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule. +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. ## Rule Details -The rule reports an error if it encounters a rule that's defined using the old style of just a `create` function. +The rule reports an error if it encounters a rule that's defined using the [deprecated style](https://eslint.org/docs/developer-guide/working-with-rules-deprecated) of just a `create` function instead of the newer [object style](https://eslint.org/docs/developer-guide/working-with-rules). Examples of **incorrect** code for this rule: @@ -12,15 +12,21 @@ Examples of **incorrect** code for this rule: /* eslint eslint-plugin/prefer-object-rule: error */ module.exports = function (context) { - return { Program() { context.report() } }; + return { Program () { + context.report(); + } }; }; -module.exports = function create(context) { - return { Program() { context.report() } }; +module.exports = function create (context) { + return { Program () { + context.report(); + } }; }; -module.exports = (context) => { - return { Program() { context.report() } }; +module.exports = context => { + return { Program () { + context.report(); + } }; }; ``` @@ -30,20 +36,26 @@ Examples of **correct** code for this rule: /* eslint eslint-plugin/prefer-object-rule: error */ module.exports = { - create(context) { - return { Program() { context.report() } }; + create (context) { + return { Program () { + context.report(); + } }; }, }; module.exports = { - create(context) { - return { Program() { context.report() } }; + create (context) { + return { Program () { + context.report(); + } }; }, }; module.exports = { - create: (context) => { - return { Program() { context.report() } }; + create: context => { + return { Program () { + context.report(); + } }; }, }; ``` diff --git a/docs/rules/prefer-output-null.md b/docs/rules/prefer-output-null.md index d247504d..e45256d8 100644 --- a/docs/rules/prefer-output-null.md +++ b/docs/rules/prefer-output-null.md @@ -1,6 +1,8 @@ -# Disallows invalid RuleTester test cases with the output the same as the code. (prefer-output-null) +# Disallow invalid RuleTester test cases where the `output` matches the `code` (prefer-output-null) -(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule. +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. + +Instead of repeating the test case `code`, using `output: null` is more concise and makes it easier to distinguish whether a test case provides an autofix. ## Rule Details @@ -12,10 +14,10 @@ Examples of **incorrect** code for this rule: /* eslint eslint-plugin/prefer-output-null: error */ new RuleTester().run('foo', bar, { - valid: [], - invalid: [ - { code: 'foo', output: 'foo', errors: [{ message: 'bar' }] }, - ] + valid: [], + invalid: [ + { code: 'foo', output: 'foo', errors: [{ message: 'bar' }] }, + ], }); ``` @@ -25,9 +27,9 @@ Examples of **correct** code for this rule: /* eslint eslint-plugin/prefer-output-null: error */ new RuleTester().run('foo', bar, { - valid: [], - invalid: [ - { code: 'foo', output: null, errors: [{ message: 'bar' }] }, - ] + valid: [], + invalid: [ + { code: 'foo', output: null, errors: [{ message: 'bar' }] }, + ], }); ``` diff --git a/docs/rules/prefer-placeholders.md b/docs/rules/prefer-placeholders.md index a383039b..e11f1c3b 100644 --- a/docs/rules/prefer-placeholders.md +++ b/docs/rules/prefer-placeholders.md @@ -1,4 +1,4 @@ -# disallow template literals as report messages (prefer-placeholders) +# Require using placeholders for dynamic report messages (prefer-placeholders) Report messages in rules can have placeholders surrounded by curly brackets. @@ -6,7 +6,7 @@ Report messages in rules can have placeholders surrounded by curly brackets. context.report({ node, message: '{{disallowedNode}} nodes are not allowed.', - data: { disallowedNode: node.type } + data: { disallowedNode: node.type }, }); ``` @@ -25,17 +25,17 @@ Examples of **incorrect** code for this rule: /* eslint eslint-plugin/prefer-placeholders: error */ module.exports = { - create(context) { + create (context) { context.report({ node, - message: `The node ${node.name} is not allowed to be used.` + message: `The node ${node.name} is not allowed to be used.`, }); context.report({ node, - message: 'The node ' + node.name + ' is not allowed to be used.' + message: 'The node ' + node.name + ' is not allowed to be used.', }); - } + }, }; ``` @@ -45,13 +45,13 @@ Examples of **correct** code for this rule: /* eslint eslint-plugin/prefer-placeholders: error */ module.exports = { - create(context) { + create (context) { context.report({ node, message: 'The node {{name}} is not allowed to be used.', - data: { name: node.name } + data: { name: node.name }, }); - } + }, }; ``` diff --git a/docs/rules/prefer-replace-text.md b/docs/rules/prefer-replace-text.md index fb8263d3..87d28551 100644 --- a/docs/rules/prefer-replace-text.md +++ b/docs/rules/prefer-replace-text.md @@ -1,22 +1,23 @@ -# prefer using replaceText instead of replaceTextRange. (prefer-replace-text) +# Require using `replaceText()` instead of `replaceTextRange()` (prefer-replace-text) ## Rule Details -The rule reports an error if `replaceTextRange`'s first argument is an array of identical array elements. It can be easily replaced by `replaceText` to improve readability. +The rule reports an error if `replaceTextRange()`'s first argument is an array of identical array elements. It can be easily replaced by `replaceText()` to improve readability. Examples of **incorrect** code for this rule: ```js /* eslint eslint-plugin/prefer-replace-text: error */ + module.exports = { - create(context) { + create (context) { context.report({ - fix(fixer) { -        // error, can be written: return fixer.replaceText([node, '']); + fix (fixer) { + // error, can be written: return fixer.replaceText([node, '']); return fixer.replaceTextRange([node.range[0], node.range[1]], ''); - } + }, }); - } + }, }; ``` @@ -24,26 +25,27 @@ Examples of **correct** code for this rule: ```js /* eslint eslint-plugin/prefer-replace-text: error */ + module.exports = { - create(context) { + create (context) { context.report({ - fix(fixer) { + fix (fixer) { return fixer.replaceText(node, ''); - } + }, }); - } + }, }; module.exports = { - create(context) { + create (context) { context.report({ - fix(fixer) { + fix (fixer) { // start = ... // end = ... return fixer.replaceTextRange([start, end], ''); - } + }, }); - } + }, }; ``` diff --git a/docs/rules/report-message-format.md b/docs/rules/report-message-format.md index 600ff070..57451c52 100644 --- a/docs/rules/report-message-format.md +++ b/docs/rules/report-message-format.md @@ -1,4 +1,4 @@ -# enforce a consistent format for rule report messages (report-message-format) +# Enforce a consistent format for rule report messages (report-message-format) It is sometimes desirable to maintain consistent formatting for all report messages. For example, you might want to mandate that all report messages begin with a capital letter and end with a period. @@ -25,41 +25,34 @@ For example, in order to mandate that all report messages begin with a capital l Note that since this rule uses static analysis and does not actually run your code, it will attempt to match report messages *before* placeholders are inserted. -The following patterns are considered warnings: +Examples of **incorrect** code for this rule: ```js /* eslint eslint-plugin/report-message-format: ["error", "^[A-Z].*\\.$"] */ module.exports = { meta: {}, - create(context) { - + create (context) { context.report(node, 'this message does not match the regular expression.'); context.report(node, 'Neither does this one'); - context.report(node, 'This will get reported, regardless of the value of the {{placeholder}}', { placeholder: foo }) - - } + context.report(node, 'This will get reported, regardless of the value of the {{placeholder}}', { placeholder: foo }); + }, }; - ``` -The following patterns are not warnings: +Examples of **correct** code for this rule: ```js - module.exports = { meta: {}, - create(context) { - + create (context) { context.report(node, 'This message matches the regular expression.'); context.report(node, 'So does this one.'); - - } + }, }; - ``` ## When Not To Use It diff --git a/docs/rules/require-meta-docs-description.md b/docs/rules/require-meta-docs-description.md index 24fa29d7..1ce3ccea 100644 --- a/docs/rules/require-meta-docs-description.md +++ b/docs/rules/require-meta-docs-description.md @@ -1,8 +1,9 @@ -# require rules to implement a meta.docs.description property (require-meta-docs-description) +# Require rules to implement a `meta.docs.description` property with the correct format (require-meta-docs-description) Defining a clear and consistent description for each rule helps developers understand what they're used for. In particular, each rule description should begin with an allowed prefix: + * `enforce` * `require` * `disallow` @@ -15,14 +16,15 @@ Examples of **incorrect** code for this rule: ```js /* eslint eslint-plugin/require-meta-docs-description: error */ + module.exports = { - meta: {}, - create: function(context) { /* ... */} + meta: {}, + create (context) {/* ... */}, }; module.exports = { - meta: { description: 'this rule does ...' }, // missing allowed prefix - create: function(context) { /* ... */} + meta: { description: 'this rule does ...' }, // missing allowed prefix + create (context) {/* ... */}, }; ``` @@ -30,9 +32,10 @@ Examples of **correct** code for this rule: ```js /* eslint eslint-plugin/require-meta-docs-description: error */ + module.exports = { - meta: { description: 'disallow unused variables' }, - create: function(context) { /* ... */} + meta: { description: 'disallow unused variables' }, + create (context) {/* ... */}, }; ``` @@ -40,7 +43,7 @@ module.exports = { This rule takes an optional object containing: -- `String` — `pattern` — A regular expression that the description must match. Use `'.+'` to allow anything. Defaults to `^(enforce|require|disallow)`. +* `String` — `pattern` — A regular expression that the description must match. Use `'.+'` to allow anything. Defaults to `^(enforce|require|disallow)`. ## Further Reading diff --git a/docs/rules/require-meta-docs-url.md b/docs/rules/require-meta-docs-url.md index e0875c2b..e71f1bf1 100644 --- a/docs/rules/require-meta-docs-url.md +++ b/docs/rules/require-meta-docs-url.md @@ -1,7 +1,8 @@ -# require rules to implement a meta.docs.url property (require-meta-docs-url) +# Require rules to implement a `meta.docs.url` property (require-meta-docs-url) -`meta.docs.url` property is the official location to store a URL to their documentation in the rule metadata. -Some integration tools will show the URL to users to understand rules. +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. + +A rule can store the URL to its documentation page in `meta.docs.url`. This enables integration tools / IDEs / editors to conveniently provide the link to developers so that they can better understand the rule. ## Rule Details @@ -17,11 +18,11 @@ This rule has an option. } ``` -- `pattern` (`string`) ... A pattern to enforce rule's document URL. It replaces `{{name}}` placeholder by each rule name. The rule name is the basename of each rule file. Default is undefined. +- `pattern` (`string`) ... A pattern to enforce rule's document URL. It replaces `{{name}}` placeholder by each rule name. The rule name is the basename of each rule file. Default is `undefined` which allows any URL. If you set the `pattern` option, this rule adds `meta.docs.url` property automatically when you execute `eslint --fix` command. -The following patterns are considered warnings: +Examples of **incorrect** code for this rule: ```js @@ -29,8 +30,7 @@ The following patterns are considered warnings: module.exports = { meta: {}, - create(context) { - } + create (context) {}, }; ``` @@ -42,11 +42,10 @@ module.exports = { module.exports = { meta: { docs: { - url: undefined - } + url: undefined, + }, }, - create(context) { - } + create (context) {}, }; ``` @@ -58,16 +57,15 @@ module.exports = { module.exports = { meta: { docs: { - url: "wrong URL" - } + url: 'wrong URL', + }, }, - create(context) { - } + create (context) {}, }; ``` -The following patterns are not warnings: +Examples of **correct** code for this rule: ```js @@ -76,11 +74,10 @@ The following patterns are not warnings: module.exports = { meta: { docs: { - url: "a URL" - } + url: 'a URL', + }, }, - create(context) { - } + create (context) {}, }; ``` @@ -92,11 +89,10 @@ module.exports = { module.exports = { meta: { docs: { - url: "path/to/rule-name.md" - } + url: 'path/to/rule-name.md', + }, }, - create(context) { - } + create (context) {}, }; ``` @@ -109,19 +105,16 @@ For example: **.eslintrc.js**: ```js -"use strict" - -const version = require("./package.json").version +// const version = require("./package.json").version; module.exports = { - plugins: ["eslint-plugin"], - // ... leaving out ... + plugins: ['eslint-plugin'], rules: { - "eslint-plugin/require-meta-docs-url": ["error", { + 'eslint-plugin/require-meta-docs-url': ['error', { pattern: `path/to/v${version}/docs/rules/{{name}}.md`, }], - } -} + }, +}; ``` **package.json**: @@ -134,8 +127,7 @@ module.exports = { "test": "... leaving out ...", "preversion": "npm test", "version": "eslint . --fix && git add ." - }, - // ... leaving out ... + } } ``` @@ -143,7 +135,7 @@ Then `npm version ` command will update every rule to the new version's UR > npm runs `preversion` script on the current version, runs `version` script on the new version, and commits and makes a tag. > -> Further reading: https://docs.npmjs.com/cli/version +> Further reading: ## When Not To Use It diff --git a/docs/rules/require-meta-fixable.md b/docs/rules/require-meta-fixable.md index fa59e65a..ef0ba9ea 100644 --- a/docs/rules/require-meta-fixable.md +++ b/docs/rules/require-meta-fixable.md @@ -1,108 +1,100 @@ -# require rules to implement a meta.fixable property (require-meta-fixable) +# Require rules to implement a `meta.fixable` property (require-meta-fixable) -A fixable ESLint rule must have a valid `meta.fixable` property. A rule reports a problem with a `fix()` function but does not export a `meta.fixable` property is likely to cause an unexpected error. +✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule. + +ESLint requires fixable rules to specify a valid `meta.fixable` property (with value `code` or `whitespace`). ## Rule Details -This rule aims to require ESLint rules to have a `meta.fixable` property if necessary. +This rule aims to require fixable ESLint rules to have a valid `meta.fixable` property. -The following patterns are considered warnings: +Examples of **incorrect** code for this rule: ```js - /* eslint eslint-plugin/require-meta-fixable: "error" */ module.exports = { - meta: {}, - create(context) { + meta: {}, // missing `fixable` property + create (context) { context.report({ node, message: 'foo', - fix(fixer) { + fix (fixer) { return fixer.remove(node); - } + }, }); - } + }, }; - ``` ```js - /* eslint eslint-plugin/require-meta-fixable: "error" */ module.exports = { meta: { fixable: 'not a valid meta.fixable value' }, - create(context) { + create (context) { context.report({ node, message: 'foo', - fix(fixer) { + fix (fixer) { return fixer.remove(node); - } + }, }); - } + }, }; - ``` ```js +/* eslint eslint-plugin/require-meta-fixable: ["error", { catchNoFixerButFixableProperty: true }] */ -/* eslint eslint-plugin/require-meta-fixable: "error" */ - -module.exports = function create(context) { - context.report({ - node, - message: 'foo', - fix(fixer) { - return fixer.remove(node); - } - }); +module.exports = { + meta: { fixable: 'code' }, // property enabled but no fixer detected + create (context) { + context.report({ node, message: 'foo' }); + }, }; - ``` -The following patterns are not warnings: +Examples of **correct** code for this rule: ```js - /* eslint eslint-plugin/require-meta-fixable: "error" */ module.exports = { meta: { fixable: 'code' }, - create(context) { + create (context) { context.report({ node, message: 'foo', - fix(fixer) { + fix (fixer) { return fixer.remove(node); - } + }, }); - } + }, }; - ``` ```js - /* eslint eslint-plugin/require-meta-fixable: "error" */ module.exports = { meta: {}, - create(context) { + create (context) { context.report({ node, - message: 'foo' + message: 'foo', }); - } + }, }; - ``` -## When Not To Use It +## Options + +This rule takes an optional object containing: -If you do not plan to implement autofixable rules, you can turn off this rule. +* `boolean` — `catchNoFixerButFixableProperty` — default `false` - Whether the rule should attempt to detect rules that do not have a fixer but enable the `meta.fixable` property. This option is off by default because it increases the chance of false positives since fixers can't always be detected when helper functions are used. ## Further Reading * [ESLint's autofix API](http://eslint.org/docs/developer-guide/working-with-rules#applying-fixes) +* [ESLint's rule basics mentioning `meta.fixable`](https://eslint.org/docs/developer-guide/working-with-rules#rule-basics) diff --git a/docs/rules/require-meta-has-suggestions.md b/docs/rules/require-meta-has-suggestions.md new file mode 100644 index 00000000..6d08487d --- /dev/null +++ b/docs/rules/require-meta-has-suggestions.md @@ -0,0 +1,88 @@ +# Require suggestable rules to implement a `meta.hasSuggestions` property (require-meta-has-suggestions) + +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. + +A suggestable ESLint rule should specify the `meta.hasSuggestions` property with a value of `true`. This makes it easier for both humans and tooling to tell whether a rule provides suggestions. [As of ESLint 8](https://eslint.org/blog/2021/06/whats-coming-in-eslint-8.0.0#rules-with-suggestions-now-require-the-metahassuggestions-property), an exception will be thrown if a suggestable rule is missing this property. + +Likewise, rules that do not report suggestions should not enable the `meta.hasSuggestions` property. + +## Rule Details + +This rule aims to require ESLint rules to have a `meta.hasSuggestions` property if necessary. + +Examples of **incorrect** code for this rule: + +```js +/* eslint eslint-plugin/require-meta-has-suggestions: "error" */ + +module.exports = { + meta: {}, // Missing `meta.hasSuggestions`. + create (context) { + context.report({ + node, + message: 'foo', + suggest: [ + { + desc: 'Insert space at the beginning', + fix: fixer => fixer.insertTextBefore(node, ' '), + }, + ], + }); + }, +}; +``` + +```js +/* eslint eslint-plugin/require-meta-has-suggestions: "error" */ + +module.exports = { + meta: { hasSuggestions: true }, // Has `meta.hasSuggestions` enabled but never provides suggestions. + create (context) { + context.report({ + node, + message: 'foo', + }); + }, +}; +``` + +Examples of **correct** code for this rule: + +```js +/* eslint eslint-plugin/require-meta-has-suggestions: "error" */ + +module.exports = { + meta: { hasSuggestions: true }, + create (context) { + context.report({ + node, + message: 'foo', + suggest: [ + { + desc: 'Insert space at the beginning', + fix: fixer => fixer.insertTextBefore(node, ' '), + }, + ], + }); + }, +}; +``` + +```js +/* eslint eslint-plugin/require-meta-has-suggestions: "error" */ + +module.exports = { + meta: {}, + create (context) { + context.report({ + node, + message: 'foo', + }); + }, +}; +``` + +## Further Reading + +* [ESLint's suggestion API](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions) +* [ESLint rule basics describing the `meta.hasSuggestions` property](https://eslint.org/docs/developer-guide/working-with-rules#rule-basics) diff --git a/docs/rules/require-meta-schema.md b/docs/rules/require-meta-schema.md index 8c4129cf..d6530131 100644 --- a/docs/rules/require-meta-schema.md +++ b/docs/rules/require-meta-schema.md @@ -1,6 +1,8 @@ -# require rules to implement a meta.schema property (require-meta-schema) +# Require rules to implement a `meta.schema` property (require-meta-schema) -Defining a schema for each rule allows eslint to validate that configuration options are passed correctly. Even when there are no options for a rule, a schema should still be defined (as an empty array) so that eslint can validate that no data is passed to the rule. +💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). + +Defining a schema for each rule allows eslint to validate that configuration options are passed correctly. Even when there are no options for a rule, a schema should still be defined (as an empty array) so that eslint can validate that no data is mistakenly passed to the rule. ## Rule Details @@ -10,14 +12,22 @@ Examples of **incorrect** code for this rule: ```js /* eslint eslint-plugin/require-meta-schema: error */ + +module.exports = { + meta: {}, + create (context) {/* ... */}, +}; + module.exports = { - meta: {}, - create: function(context) { /* ... */} + meta: { schema: null }, + create (context) {/* ... */}, }; module.exports = { - meta: { schema: null }, - create: function(context) { /* ... */} + meta: { schema: [] }, + create (context) { + const options = context.options; /* using options when schema is empty */ + }, }; ``` @@ -25,29 +35,36 @@ Examples of **correct** code for this rule: ```js /* eslint eslint-plugin/require-meta-schema: error */ + module.exports = { - meta: { schema: [] }, // ensures no options are passed to the rule - create: function(context) { /* ... */} + meta: { schema: [] }, // ensures no options are passed to the rule + create (context) {/* ... */}, }; module.exports = { - meta: { - schema: [ - { - type: 'object', - properties: { - exceptRange: { - type: 'boolean' - } - }, - additionalProperties: false - } - ] - }, - create: function(context) { /* ... */} + meta: { + schema: [ + { + type: 'object', + properties: { + exceptRange: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + }, + create (context) {/* ... */}, }; ``` +## Options + +This rule takes an optional object containing: + +* `boolean` — `requireSchemaPropertyWhenOptionless` — Whether the rule should require the `meta.schema` property to be specified (with `schema: []`) for rules that have no options. Defaults to `true`. + ## Further Reading * [working-with-rules#options-schemas](https://eslint.org/docs/developer-guide/working-with-rules#options-schemas) diff --git a/docs/rules/require-meta-type.md b/docs/rules/require-meta-type.md index e988fd39..b3a33484 100644 --- a/docs/rules/require-meta-type.md +++ b/docs/rules/require-meta-type.md @@ -1,29 +1,34 @@ -# require rules to implement a meta.type property (require-meta-type) +# Require rules to implement a `meta.type` property (require-meta-type) ESLint v5.9.0 introduces a new `--fix-type` option for the command line interface. This option allows users to filter the type of fixes applied when using `--fix`. -Fixes in custom rules will not be applied when using `--fix-type` unless they include a meta.type field. +Fixes in custom rules will not be applied when using `--fix-type` unless they include a `meta.type` field. ## Rule Details -This rule aims to require ESLint rules to have a valid `meta.type` property. +This rule aims to require ESLint rules to have a valid `meta.type` property with one of the following values: + +* `"problem"` means the rule is identifying code that either will cause an error or may cause a confusing behavior. Developers should consider this a high priority to resolve. +* `"suggestion"` means the rule is identifying something that could be done in a better way but no errors will occur if the code isn't changed. +* `"layout"` means the rule cares primarily about whitespace, semicolons, commas, and parentheses, all the parts of the program that determine how the code looks rather than how it executes. These rules work on parts of the code that aren't specified in the AST. Examples of **incorrect** code for this rule: ```js /* eslint eslint-plugin/require-meta-type: error */ + module.exports = { - meta: {}, - create: function(context) { - // ... - } + meta: {}, + create (context) { + // ... + }, }; module.exports = { - meta: {type: 'invalid'}, - create: function(context) { - // ... - } + meta: { type: 'invalid' }, + create (context) { + // ... + }, }; ``` @@ -31,11 +36,12 @@ Examples of **correct** code for this rule: ```js /* eslint eslint-plugin/require-meta-type: error */ + module.exports = { - meta: {type: 'problem'}, - create: function(context) { - // ... - } + meta: { type: 'problem' }, + create (context) { + // ... + }, }; ``` diff --git a/docs/rules/test-case-property-ordering.md b/docs/rules/test-case-property-ordering.md index 40acd2d4..4e62d531 100644 --- a/docs/rules/test-case-property-ordering.md +++ b/docs/rules/test-case-property-ordering.md @@ -1,6 +1,6 @@ -# enforce ordering of keys in test cases (test-case-property-ordering) +# Require the properties of a test case to be placed in a consistent order (test-case-property-ordering) -(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule. +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. This rule enforces that the properties of RuleTester test cases are arranged in a consistent order. @@ -21,20 +21,20 @@ Examples of **incorrect** code for this rule: ] */ // invalid; wrong order -{ - code: "foo", - options: ["baz"], - output: "bar", -} +const testCase1 = { + code: 'foo', + options: ['baz'], + output: 'bar', +}; // invalid; extra properties should need to be placed afterwards. -{ - code: "foo", +const testCase2 = { + code: 'foo', env: { es6: true }, - output: "bar", - options: ["baz"], -} + output: 'bar', + options: ['baz'], +}; ``` Examples of **correct** code for this rule: @@ -45,11 +45,11 @@ Examples of **correct** code for this rule: ] */ // valid; -{ - code: "foo", - output: "bar", - options: ["baz"], -} +const testCase1 = { + code: 'foo', + output: 'bar', + options: ['baz'], +}; ``` diff --git a/docs/rules/test-case-shorthand-strings.md b/docs/rules/test-case-shorthand-strings.md index ecbaeff5..bed82065 100644 --- a/docs/rules/test-case-shorthand-strings.md +++ b/docs/rules/test-case-shorthand-strings.md @@ -1,5 +1,7 @@ # Enforce consistent usage of shorthand strings for test cases with no options (test-case-shorthand-strings) +⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule. + When writing valid test cases for rules with `RuleTester`, one can optionally include a string as a test case instead of an object, if the the test case does not use any options. ```js @@ -11,12 +13,12 @@ ruleTester.run('example-rule', rule, { // longform object { - code: 'anotherValidTestCase;' - } + code: 'anotherValidTestCase;', + }, ], invalid: [ // ... - ] + ], }); ``` @@ -45,13 +47,13 @@ Examples of **incorrect** code for this rule with the default `as-needed` option ruleTester.run('example-rule', rule, { valid: [ { - code: 'validTestCase;' + code: 'validTestCase;', }, { - code: 'anotherValidTestCase;' - } + code: 'anotherValidTestCase;', + }, ], - invalid: [] + invalid: [], }); ``` @@ -66,10 +68,10 @@ ruleTester.run('example-rule', rule, { 'anotherValidTestCase;', { code: 'testCaseWithOption;', - options: ["foo"] - } + options: ['foo'], + }, ], - invalid: [] + invalid: [], }); ``` @@ -83,9 +85,9 @@ Examples of **incorrect** code for this rule with the `never` option: ruleTester.run('example-rule', rule, { valid: [ 'validTestCase;', - 'anotherValidTestCase;' + 'anotherValidTestCase;', ], - invalid: [] + invalid: [], }); ``` @@ -97,13 +99,13 @@ Examples of **correct** code for this rule with the `never` option: ruleTester.run('example-rule', rule, { valid: [ { - code: 'validTestCase;' + code: 'validTestCase;', }, { - code: 'anotherValidTestCase;' - } + code: 'anotherValidTestCase;', + }, ], - invalid: [] + invalid: [], }); ``` @@ -120,10 +122,10 @@ ruleTester.run('example-rule', rule, { 'anotherValidTestCase;', { code: 'testCaseWithOption', - options: ["foo"] - } + options: ['foo'], + }, ], - invalid: [] + invalid: [], }); ``` @@ -135,41 +137,41 @@ Examples of **correct** code for this rule with the `consistent` option: ruleTester.run('example-rule', rule, { valid: [ { - code: 'validTestCase;' + code: 'validTestCase;', }, { - code: 'anotherValidTestCase' + code: 'anotherValidTestCase', }, { code: 'testCaseWithOption', - options: ["foo"] - } + options: ['foo'], + }, ], - invalid: [] + invalid: [], }); ruleTester.run('example-rule', rule, { valid: [ 'validTestCase;', - 'anotherValidTestCase' + 'anotherValidTestCase', ], - invalid: [] + invalid: [], }); ruleTester.run('example-rule', rule, { valid: [ { - code: 'validTestCase;' + code: 'validTestCase;', }, { - code: 'anotherValidTestCase' - } + code: 'anotherValidTestCase', + }, ], - invalid: [] + invalid: [], }); ``` -#### `never` +#### `consistent-as-needed` Examples of **incorrect** code for this rule with the `consistent-as-needed` option: @@ -180,10 +182,10 @@ ruleTester.run('example-rule', rule, { valid: [ 'validTestCase;', { - code: 'anotherValidTestCase' - } + code: 'anotherValidTestCase', + }, ], - invalid: [] + invalid: [], }); ruleTester.run('example-rule', rule, { @@ -192,22 +194,22 @@ ruleTester.run('example-rule', rule, { 'anotherValidTestCase;', { code: 'testCaseWithOption;', - options: ['foo'] - } + options: ['foo'], + }, ], - invalid: [] + invalid: [], }); ruleTester.run('example-rule', rule, { valid: [ { - code: 'validTestCase;' + code: 'validTestCase;', }, { - code: 'anotherValidTestCase;' - } + code: 'anotherValidTestCase;', + }, ], - invalid: [] + invalid: [], }); ``` @@ -219,25 +221,25 @@ Examples of **correct** code for this rule with the `consistent-as-needed` optio ruleTester.run('example-rule', rule, { valid: [ 'validTestCase;', - 'anotherValidTestCase;' + 'anotherValidTestCase;', ], - invalid: [] + invalid: [], }); ruleTester.run('example-rule', rule, { valid: [ { - code: 'validTestCase;' + code: 'validTestCase;', }, { - code: 'anotherValidTestCase;' + code: 'anotherValidTestCase;', }, { code: 'testCaseWithOption;', - options: ['foo'] - } + options: ['foo'], + }, ], - invalid: [] + invalid: [], }); ``` diff --git a/lib/index.js b/lib/index.js index 7d486690..32e427a6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -43,20 +43,22 @@ function loadRule (ruleName) { } // import all rules in lib/rules -const allRules = fs +const allRules = Object.fromEntries(fs .readdirSync(`${__dirname}/rules`) .filter(fileName => fileName.endsWith('.js') && /^[^._]/.test(fileName)) .map(fileName => fileName.replace(/\.js$/, '')) - .reduce((rules, ruleName) => Object.assign(rules, { [ruleName]: loadRule(ruleName) }), {}); + .map(ruleName => [ruleName, loadRule(ruleName)])); module.exports.rules = allRules; +// eslint-disable-next-line unicorn/prefer-object-from-entries module.exports.configs = Object.keys(configFilters).reduce((configs, configName) => { return Object.assign(configs, { [configName]: { - rules: Object.keys(allRules) + plugins: ['eslint-plugin'], + rules: Object.fromEntries(Object.keys(allRules) .filter(ruleName => configFilters[configName](allRules[ruleName])) - .reduce((rules, ruleName) => Object.assign(rules, { [`${PLUGIN_NAME}/${ruleName}`]: 'error' }), {}), + .map(ruleName => [`${PLUGIN_NAME}/${ruleName}`, 'error'])), }, }); }, {}); diff --git a/lib/rules/consistent-output.js b/lib/rules/consistent-output.js index 35478e76..b62b6df5 100644 --- a/lib/rules/consistent-output.js +++ b/lib/rules/consistent-output.js @@ -1,5 +1,5 @@ /** - * @fileoverview Enforce consistent use of output assertions in rule tests + * @fileoverview Enforce consistent use of `output` assertions in rule tests * @author Teddy Katz */ @@ -13,12 +13,12 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'enforce consistent use of output assertions in rule tests', + description: 'enforce consistent use of `output` assertions in rule tests', category: 'Tests', recommended: false, }, - type: 'suggestion', fixable: null, // or "code" or "whitespace" schema: [ { @@ -26,6 +26,9 @@ module.exports = { enum: ['always', 'consistent'], }, ], + messages: { + missingOutput: 'This test case should have an output assertion.', + }, }, create (context) { @@ -39,7 +42,7 @@ module.exports = { utils.getTestInfo(context, ast).forEach(testRun => { const readableCases = testRun.invalid.filter(testCase => testCase.type === 'ObjectExpression'); const casesWithoutOutput = readableCases - .filter(testCase => testCase.properties.map(utils.getKeyName).indexOf('output') === -1); + .filter(testCase => !testCase.properties.map(utils.getKeyName).includes('output')); if ( (casesWithoutOutput.length < readableCases.length) || @@ -48,7 +51,7 @@ module.exports = { casesWithoutOutput.forEach(testCase => { context.report({ node: testCase, - message: 'This test case should have an output assertion.', + messageId: 'missingOutput', }); }); } diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js index 8d4898c1..0dd7fc02 100644 --- a/lib/rules/fixer-return.js +++ b/lib/rules/fixer-return.js @@ -1,5 +1,5 @@ /** - * @fileoverview Enforces always return from a fixer function + * @fileoverview require fixer functions to return a fix * @author 薛定谔的猫 */ @@ -10,6 +10,7 @@ // ------------------------------------------------------------------------------ const utils = require('../utils'); +const { getStaticValue } = require('eslint-utils'); // ------------------------------------------------------------------------------ // Rule Definition @@ -17,52 +18,79 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'problem', docs: { - description: 'require fixer function to always return a value.', + description: 'require fixer functions to return a fix', category: 'Possible Errors', recommended: true, }, - type: 'problem', fixable: null, schema: [], + messages: { + missingFix: 'Fixer function never returned a fix.', + }, }, create (context) { - const message = 'Expected fixer function to always return a value.'; let funcInfo = { upper: null, codePath: null, - hasReturn: false, - hasYield: false, + hasReturnWithFixer: false, + hasYieldWithFixer: false, shouldCheck: false, node: null, }; let contextIdentifiers; /** - * Checks whether or not the last code path segment is reachable. - * Then reports this function if the segment is reachable. - * - * If the last code path segment is reachable, there are paths which are not - * returned or thrown. + * As we exit the fix() function, ensure we have returned or yielded a real fix by this point. + * If not, report the function as a violation. * * @param {ASTNode} node - A node to check. + * @param {Location} loc - Optional location to report violation on. * @returns {void} */ - function checkLastSegment (node) { + function ensureFunctionReturnedFix (node, loc = (node.id || node).loc.start) { if ( - funcInfo.shouldCheck && - funcInfo.codePath.currentSegments.some(segment => segment.reachable) && - (!node.generator || !funcInfo.hasYield) + (node.generator && !funcInfo.hasYieldWithFixer) || // Generator function never yielded a fix + (!node.generator && !funcInfo.hasReturnWithFixer) // Non-generator function never returned a fix ) { context.report({ node, - loc: (node.id || node).loc.start, - message, + loc, + messageId: 'missingFix', }); } } + /** + * Check if a returned/yielded node is likely to be a fix or not. + * A fix is an object created by fixer.replaceText() for example and returned by the fix function. + * @param {ASTNode} node - node to check + * @param {Context} context + * @returns {boolean} + */ + function isFix (node) { + if (node.type === 'ArrayExpression' && node.elements.length === 0) { + // An empty array is not a fix. + return false; + } + + const staticValue = getStaticValue(node, context.getScope()); + if (!staticValue) { + // If we can't find a static value, assume it's a real fix value. + return true; + } + + if (Array.isArray(staticValue.value)) { + // If the static value is an array, then consider it a fix since fixes could have been added to it after creation. + return true; + } + + // Any other static values (booleans, numbers, etc) are not fixes. + return false; + } + return { Program (node) { contextIdentifiers = utils.getContextIdentifiers(context, node); @@ -71,7 +99,9 @@ module.exports = { // Stacks this function's information. onCodePathStart (codePath, node) { const parent = node.parent; - const shouldCheck = node.type === 'FunctionExpression' && + + // Whether we are inside the fixer function we care about. + const shouldCheck = ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) && parent.parent.type === 'ObjectExpression' && parent.parent.parent.type === 'CallExpression' && contextIdentifiers.has(parent.parent.parent.callee.object) && @@ -81,8 +111,8 @@ module.exports = { funcInfo = { upper: funcInfo, codePath, - hasYield: false, - hasReturn: false, + hasYieldWithFixer: false, + hasReturnWithFixer: false, shouldCheck, node, }; @@ -94,28 +124,44 @@ module.exports = { }, // Yield in generators - YieldExpression () { - if (funcInfo.shouldCheck) { - funcInfo.hasYield = true; + YieldExpression (node) { + if (funcInfo.shouldCheck && node.argument && isFix(node.argument)) { + funcInfo.hasYieldWithFixer = true; } }, // Checks the return statement is valid. ReturnStatement (node) { + if (funcInfo.shouldCheck && node.argument && isFix(node.argument)) { + funcInfo.hasReturnWithFixer = true; + } + }, + + // Ensure the current fixer function returned or yielded a fix. + 'FunctionExpression:exit' (node) { if (funcInfo.shouldCheck) { - funcInfo.hasReturn = true; + ensureFunctionReturnedFix(node); + } + }, - if (!node.argument) { - context.report({ - node, - message, - }); + // Ensure the current (arrow) fixer function returned a fix. + 'ArrowFunctionExpression:exit' (node) { + if (funcInfo.shouldCheck) { + const loc = context.getSourceCode().getTokenBefore(node.body).loc; // Show violation on arrow (=>). + if (node.expression) { + // When the return is implied (no curly braces around the body), we have to check the single body node directly. + if (!isFix(node.body)) { + context.report({ + node, + loc, + messageId: 'missingFix', + }); + } + } else { + ensureFunctionReturnedFix(node, loc); } } }, - - // Reports a given function if the last path is reachable. - 'FunctionExpression:exit': checkLastSegment, }; }, }; diff --git a/lib/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.js index 0677dc48..40a6791a 100644 --- a/lib/rules/meta-property-ordering.js +++ b/lib/rules/meta-property-ordering.js @@ -12,24 +12,26 @@ const { getKeyName, getRuleInfo } = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { description: 'enforce the order of meta properties', category: 'Rules', recommended: false, }, - type: 'suggestion', fixable: 'code', schema: [{ type: 'array', elements: { type: 'string' }, }], + messages: { + inconsistentOrder: 'The meta properties should be placed in a consistent order: [{{order}}].', + }, }, create (context) { const sourceCode = context.getSourceCode(); const info = getRuleInfo(sourceCode); - const message = 'The meta properties should be placed in a consistent order: [{{order}}].'; const order = context.options[0] || ['type', 'docs', 'fixable', 'hasSuggestions', 'schema', 'messages']; const orderMap = new Map(order.map((name, i) => [name, i])); @@ -51,7 +53,7 @@ module.exports = { const violatingProps = props.filter(prop => { const curr = orderMap.has(getKeyName(prop)) ? orderMap.get(getKeyName(prop)) - : Infinity; + : Number.POSITIVE_INFINITY; return last > (last = curr); }); @@ -67,7 +69,7 @@ module.exports = { for (const violatingProp of violatingProps) { context.report({ node: violatingProp, - message, + messageId: 'inconsistentOrder', data: { order: knownProps.map(getKeyName).join(', '), }, diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.js index 27c5afc6..76771527 100644 --- a/lib/rules/no-deprecated-context-methods.js +++ b/lib/rules/no-deprecated-context-methods.js @@ -36,14 +36,17 @@ const DEPRECATED_PASSTHROUGHS = { module.exports = { meta: { + type: 'suggestion', docs: { description: 'disallow usage of deprecated methods on rule context objects', category: 'Rules', recommended: false, }, - type: 'suggestion', fixable: 'code', schema: [], + messages: { + newFormat: 'Use `{{contextName}}.getSourceCode().{{replacement}}` instead of `{{contextName}}.{{original}}`.', + }, }, create (context) { @@ -55,7 +58,7 @@ module.exports = { return { 'Program:exit' () { - Array.from(utils.getContextIdentifiers(context, sourceCode.ast)) + [...utils.getContextIdentifiers(context, sourceCode.ast)] .filter( contextId => contextId.parent.type === 'MemberExpression' && @@ -66,7 +69,7 @@ module.exports = { contextId => context.report({ node: contextId.parent, - message: 'Use `{{contextName}}.getSourceCode().{{replacement}}` instead of `{{contextName}}.{{original}}`.', + messageId: 'newFormat', data: { contextName: contextId.name, original: contextId.parent.property.name, diff --git a/lib/rules/no-deprecated-report-api.js b/lib/rules/no-deprecated-report-api.js index 38c768f0..d20fbb3c 100644 --- a/lib/rules/no-deprecated-report-api.js +++ b/lib/rules/no-deprecated-report-api.js @@ -1,5 +1,5 @@ /** - * @fileoverview disallow use of the deprecated context.report() API + * @fileoverview Disallow the version of `context.report()` with multiple arguments * @author Teddy Katz */ @@ -13,14 +13,17 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'disallow use of the deprecated context.report() API', + description: 'disallow the version of `context.report()` with multiple arguments', category: 'Rules', recommended: true, }, - type: 'suggestion', fixable: 'code', // or "code" or "whitespace" schema: [], + messages: { + useNewAPI: 'Use the new-style context.report() API.', + }, }, create (context) { @@ -44,11 +47,11 @@ module.exports = { ) { context.report({ node: node.callee.property, - message: 'Use the new-style context.report() API.', + messageId: 'useNewAPI', fix (fixer) { const openingParen = sourceCode.getTokenBefore(node.arguments[0]); const closingParen = sourceCode.getLastToken(node); - const reportInfo = utils.getReportInfo(node.arguments); + const reportInfo = utils.getReportInfo(node.arguments, context); if (!reportInfo) { return null; diff --git a/lib/rules/no-identical-tests.js b/lib/rules/no-identical-tests.js index 4740bccd..e32f7188 100644 --- a/lib/rules/no-identical-tests.js +++ b/lib/rules/no-identical-tests.js @@ -13,21 +13,23 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'problem', docs: { description: 'disallow identical tests', category: 'Tests', recommended: true, }, - type: 'problem', fixable: 'code', schema: [], + messages: { + identical: 'This test case is identical to another case.', + }, }, create (context) { // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- - const message = 'This test case is identical to another case.'; const sourceCode = context.getSourceCode(); // ---------------------------------------------------------------------- @@ -46,8 +48,8 @@ module.exports = { return sourceCode.getText(testA) === sourceCode.getText(testB); } - const propertiesA = testA.properties || []; - const propertiesB = testB.properties || []; + const propertiesA = testA.properties; + const propertiesB = testB.properties; // if properties length not eq; return false; if (propertiesA.length !== propertiesB.length) { @@ -60,8 +62,8 @@ module.exports = { propertiesSetA.add(code); }); - for (let i = 0; i < propertiesB.length; i++) { - const code = sourceCode.getText(propertiesB[i]); + for (const element of propertiesB) { + const code = sourceCode.getText(element); if (!propertiesSetA.has(code)) { return false; } @@ -78,7 +80,7 @@ module.exports = { if (cache.some(item => eq(item, test))) { context.report({ node: test, - message, + messageId: 'identical', fix (fixer) { const start = sourceCode.getTokenBefore(test); const end = sourceCode.getTokenAfter(test); diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.js index c4cf742e..77aca1b2 100644 --- a/lib/rules/no-missing-placeholders.js +++ b/lib/rules/no-missing-placeholders.js @@ -6,6 +6,7 @@ 'use strict'; const utils = require('../utils'); +const { getStaticValue } = require('eslint-utils'); // ------------------------------------------------------------------------------ // Rule Definition @@ -13,14 +14,17 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'problem', docs: { description: 'disallow missing placeholders in rule report messages', category: 'Rules', recommended: true, }, - type: 'problem', fixable: null, schema: [], + messages: { + placeholderDoesNotExist: 'The placeholder {{{{missingKey}}}} does not exist.', + }, }, create (context) { @@ -40,28 +44,32 @@ module.exports = { contextIdentifiers.has(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node.arguments); + const reportInfo = utils.getReportInfo(node.arguments, context); + if (!reportInfo || !reportInfo.message) { + return; + } + const messageStaticValue = getStaticValue(reportInfo.message, context.getScope()); if ( - reportInfo && - reportInfo.message && - reportInfo.message.type === 'Literal' && - typeof reportInfo.message.value === 'string' && + ( + (reportInfo.message.type === 'Literal' && typeof reportInfo.message.value === 'string') || + (messageStaticValue && typeof messageStaticValue.value === 'string') + ) && (!reportInfo.data || reportInfo.data.type === 'ObjectExpression') ) { // Same regex as the one ESLint uses // https://github.com/eslint/eslint/blob/e5446449d93668ccbdb79d78cc69f165ce4fde07/lib/eslint.js#L990 - const PLACEHOLDER_MATCHER = /\{\{\s*([^{}]+?)\s*\}\}/g; + const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g; let match; - while ((match = PLACEHOLDER_MATCHER.exec(reportInfo.message.value))) { // eslint-disable-line no-extra-parens + while ((match = PLACEHOLDER_MATCHER.exec(reportInfo.message.value || messageStaticValue.value))) { // eslint-disable-line no-extra-parens const matchingProperty = reportInfo.data && reportInfo.data.properties.find(prop => utils.getKeyName(prop) === match[1]); if (!matchingProperty) { context.report({ node: reportInfo.message, - message: 'The placeholder {{{{missingKey}}}} does not exist.', + messageId: 'placeholderDoesNotExist', data: { missingKey: match[1] }, }); } diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.js new file mode 100644 index 00000000..78f0c6d6 --- /dev/null +++ b/lib/rules/no-only-tests.js @@ -0,0 +1,83 @@ +'use strict'; + +const utils = require('../utils'); +const { isCommaToken, isOpeningBraceToken, isClosingBraceToken } = require('eslint-utils'); + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'disallow the test case property `only`', + category: 'Tests', + recommended: false, + }, + schema: [], + messages: { + foundOnly: + 'The test case property `only` can be used during development, but should not be checked-in, since it prevents all the tests from running.', + removeOnly: 'Remove `only`.', + }, + hasSuggestions: true, + }, + + create (context) { + return { + Program (ast) { + for (const testRun of utils.getTestInfo(context, ast)) { + for (const test of [...testRun.valid, ...testRun.invalid]) { + if (test.type === 'ObjectExpression') { + // Test case object: { code: 'const x = 123;', ... } + + const onlyProperty = test.properties.find( + property => + property.key.type === 'Identifier' && + property.key.name === 'only' && + property.value.type === 'Literal' && + property.value.value + ); + + if (onlyProperty) { + context.report({ + node: onlyProperty, + messageId: 'foundOnly', + suggest: [ + { + messageId: 'removeOnly', + *fix (fixer) { + const sourceCode = context.getSourceCode(); + + const tokenBefore = sourceCode.getTokenBefore(onlyProperty); + const tokenAfter = sourceCode.getTokenAfter(onlyProperty); + if ( + (isCommaToken(tokenBefore) && isCommaToken(tokenAfter)) || // In middle of properties + (isOpeningBraceToken(tokenBefore) && isCommaToken(tokenAfter)) // At beginning of properties + ) { + yield fixer.remove(tokenAfter); // Remove extra comma. + } + if (isCommaToken(tokenBefore) && isClosingBraceToken(tokenAfter)) { // At end of properties + yield fixer.remove(tokenBefore); // Remove extra comma. + } + + yield fixer.remove(onlyProperty); + }, + }, + ], + }); + } + } else if ( + test.type === 'CallExpression' && + test.callee.type === 'MemberExpression' && + test.callee.object.type === 'Identifier' && + test.callee.object.name === 'RuleTester' && + test.callee.property.type === 'Identifier' && + test.callee.property.name === 'only' + ) { + // RuleTester.only('const x = 123;'); + context.report({ node: test.callee, messageId: 'foundOnly' }); + } + } + } + }, + }; + }, +}; diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js index af7b78d1..83c38a40 100644 --- a/lib/rules/no-unused-placeholders.js +++ b/lib/rules/no-unused-placeholders.js @@ -6,6 +6,7 @@ 'use strict'; const utils = require('../utils'); +const { getStaticValue } = require('eslint-utils'); // ------------------------------------------------------------------------------ // Rule Definition @@ -13,14 +14,17 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'problem', docs: { description: 'disallow unused placeholders in rule report messages', category: 'Rules', recommended: true, }, - type: 'problem', fixable: null, schema: [], + messages: { + placeholderUnused: 'The placeholder {{{{unusedKey}}}} is unused.', + }, }, create (context) { @@ -40,18 +44,23 @@ module.exports = { contextIdentifiers.has(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node.arguments); + const reportInfo = utils.getReportInfo(node.arguments, context); + if (!reportInfo || !reportInfo.message) { + return; + } + const messageStaticValue = getStaticValue(reportInfo.message, context.getScope()); if ( - reportInfo && - reportInfo.message && - reportInfo.message.type === 'Literal' && - typeof reportInfo.message.value === 'string' && - reportInfo.data && reportInfo.data.type === 'ObjectExpression' + ( + (reportInfo.message.type === 'Literal' && typeof reportInfo.message.value === 'string') || + (messageStaticValue && typeof messageStaticValue.value === 'string') + ) && + reportInfo.data && + reportInfo.data.type === 'ObjectExpression' ) { - const message = reportInfo.message.value; + const message = reportInfo.message.value || messageStaticValue.value; // https://github.com/eslint/eslint/blob/2874d75ed8decf363006db25aac2d5f8991bd969/lib/linter.js#L986 - const PLACEHOLDER_MATCHER = /\{\{\s*([^{}]+?)\s*\}\}/g; + const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g; const placeholdersInMessage = new Set(); message.replace(PLACEHOLDER_MATCHER, (fullMatch, term) => { @@ -63,7 +72,7 @@ module.exports = { if (!placeholdersInMessage.has(key)) { context.report({ node: reportInfo.message, - message: 'The placeholder {{{{unusedKey}}}} is unused.', + messageId: 'placeholderUnused', data: { unusedKey: key }, }); } diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.js index ce6f5b6d..0a8a35df 100644 --- a/lib/rules/no-useless-token-range.js +++ b/lib/rules/no-useless-token-range.js @@ -1,5 +1,5 @@ /** - * @fileoverview Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken + * @fileoverview Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` * @author Teddy Katz */ @@ -13,14 +13,17 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken', + description: 'disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`', category: 'Rules', recommended: true, }, - type: 'suggestion', fixable: 'code', schema: [], + messages: { + useReplacement: "Use '{{replacementText}}' instead.", + }, }, create (context) { @@ -33,7 +36,7 @@ module.exports = { /** * Determines whether a second argument to getFirstToken or getLastToken changes the output of the function. * This occurs when the second argument exists and is not an object literal, or has keys other than `includeComments`. - * @param {ASTNode} arg The second argument to `sourceCode.getFirstToken` or `sourceCode.getLastToken` + * @param {ASTNode} arg The second argument to `sourceCode.getFirstToken()` or `sourceCode.getLastToken()` * @returns {boolean} `true` if the argument affects the output of getFirstToken or getLastToken */ function affectsGetTokenOutput (arg) { @@ -103,7 +106,7 @@ module.exports = { return { 'Program:exit' (ast) { - Array.from(utils.getSourceCodeIdentifiers(context, ast)) + [...utils.getSourceCodeIdentifiers(context, ast)] .filter(identifier => identifier.parent.type === 'MemberExpression' && identifier.parent.object === identifier && identifier.parent.property.type === 'Identifier' && @@ -125,7 +128,7 @@ module.exports = { sourceCode.text.slice(identifier.parent.parent.range[1], fullRangeAccess.range[1]); context.report({ node: identifier.parent.parent, - message: "Use '{{replacementText}}' instead.", + messageId: 'useReplacement', data: { replacementText }, fix (fixer) { return fixer.replaceText(identifier.parent.parent, sourceCode.getText(identifier.parent.parent.arguments[0])); diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.js new file mode 100644 index 00000000..8fddd35e --- /dev/null +++ b/lib/rules/prefer-message-ids.js @@ -0,0 +1,83 @@ +'use strict'; + +const utils = require('../utils'); +const { getStaticValue } = require('eslint-utils'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'require using `messageId` instead of `message` to report rule violations', + category: 'Rules', + recommended: false, + }, + fixable: null, + schema: [], + messages: { + messagesMissing: '`meta.messages` must contain at least one violation message.', + foundMessage: 'Use `messageId` instead of `message`.', + }, + }, + + create (context) { + const sourceCode = context.getSourceCode(); + const info = utils.getRuleInfo(sourceCode); + + let contextIdentifiers; + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return { + Program (ast) { + contextIdentifiers = utils.getContextIdentifiers(context, ast); + + if (info === null || info.meta === null) { + return; + } + + const metaNode = info.meta; + const messagesNode = + metaNode && + metaNode.properties && + metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'messages'); + + if (!messagesNode) { + context.report({ node: metaNode, messageId: 'messagesMissing' }); + return; + } + + const staticValue = getStaticValue(messagesNode.value, context.getScope()); + if (!staticValue) { + return; + } + + if (typeof staticValue.value === 'object' && staticValue.value.constructor === Object && Object.keys(staticValue.value).length === 0) { + context.report({ node: messagesNode.value, messageId: 'messagesMissing' }); + } + }, + CallExpression (node) { + if ( + node.callee.type === 'MemberExpression' && + contextIdentifiers.has(node.callee.object) && + node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' + ) { + const reportInfo = utils.getReportInfo(node.arguments, context); + if (!reportInfo || !reportInfo.message) { + return; + } + + context.report({ + node: reportInfo.message.parent, + messageId: 'foundMessage', + }); + } + }, + }; + }, +}; diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.js index f8d3b499..72b9920e 100644 --- a/lib/rules/prefer-object-rule.js +++ b/lib/rules/prefer-object-rule.js @@ -12,17 +12,17 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'disallow rule exports where the export is a function.', + description: 'disallow rule exports where the export is a function', category: 'Rules', recommended: false, }, + fixable: 'code', + schema: [], messages: { preferObject: 'Rules should be declared using the object style.', }, - type: 'suggestion', - fixable: 'code', - schema: [], }, create (context) { @@ -52,6 +52,7 @@ module.exports = { token => token.type === 'Punctuator' && token.value === '(' ); + /* istanbul ignore if */ if (!openParenToken) { // this shouldn't happen, but guarding against crashes just in case return null; diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.js index 8e6c6306..f622f00d 100644 --- a/lib/rules/prefer-output-null.js +++ b/lib/rules/prefer-output-null.js @@ -1,5 +1,5 @@ /** - * @fileoverview disallows invalid RuleTester test cases with the output the same as the code. + * @fileoverview disallows invalid RuleTester test cases where the `output` matches the `code` * @author 薛定谔的猫 */ @@ -13,14 +13,17 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'disallow invalid RuleTester test cases with the output the same as the code.', + description: 'disallow invalid RuleTester test cases where the `output` matches the `code`', category: 'Tests', recommended: false, }, - type: 'suggestion', fixable: 'code', schema: [], + messages: { + useOutputNull: 'Use `output: null` to assert that a test case is not autofixed.', + }, }, create (context) { @@ -28,7 +31,6 @@ module.exports = { // Public // ---------------------------------------------------------------------- - const message = 'Use `output: null` to assert that a test case is not autofixed.'; const sourceCode = context.getSourceCode(); return { @@ -43,7 +45,7 @@ module.exports = { function getTestInfo (key) { if (test.type === 'ObjectExpression') { const res = test.properties.filter(item => item.key.name === key); - return res.length ? res[res.length - 1] : null; + return res.length > 0 ? res[res.length - 1] : null; } return key === 'code' ? test : null; } @@ -54,7 +56,7 @@ module.exports = { if (output && sourceCode.getText(output.value) === sourceCode.getText(code.value)) { context.report({ node: output, - message, + messageId: 'useOutputNull', fix: fixer => fixer.replaceText(output.value, 'null'), }); } diff --git a/lib/rules/prefer-placeholders.js b/lib/rules/prefer-placeholders.js index ec58277e..21cf23a2 100644 --- a/lib/rules/prefer-placeholders.js +++ b/lib/rules/prefer-placeholders.js @@ -1,11 +1,12 @@ /** - * @fileoverview disallow template literals as report messages + * @fileoverview require using placeholders for dynamic report messages * @author Teddy Katz */ 'use strict'; const utils = require('../utils'); +const { findVariable } = require('eslint-utils'); // ------------------------------------------------------------------------------ // Rule Definition @@ -13,19 +14,25 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'disallow template literals as report messages', + description: 'require using placeholders for dynamic report messages', category: 'Rules', recommended: false, }, - type: 'suggestion', fixable: null, schema: [], + messages: { + usePlaceholders: 'Use report message placeholders instead of string concatenation.', + }, }, create (context) { let contextIdentifiers; + const sourceCode = context.getSourceCode(); + const { scopeManager } = sourceCode; + // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- @@ -40,17 +47,43 @@ module.exports = { contextIdentifiers.has(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node.arguments); + const reportInfo = utils.getReportInfo(node.arguments, context); + + if (!reportInfo || !reportInfo.message) { + return; + } + + let messageNode = reportInfo.message; + + if (messageNode.type === 'Identifier') { + // See if we can find the variable declaration. + + const variable = findVariable( + scopeManager.acquire(messageNode) || scopeManager.globalScope, + messageNode + ); + + if ( + !variable || + !variable.defs || + !variable.defs[0] || + !variable.defs[0].node || + variable.defs[0].node.type !== 'VariableDeclarator' || + !variable.defs[0].node.init + ) { + return; + } + + messageNode = variable.defs[0].node.init; + } if ( - reportInfo && reportInfo.message && ( - (reportInfo.message.type === 'TemplateLiteral' && reportInfo.message.expressions.length) || - (reportInfo.message.type === 'BinaryExpression' && reportInfo.message.operator === '+') - ) + (messageNode.type === 'TemplateLiteral' && messageNode.expressions.length > 0) || + (messageNode.type === 'BinaryExpression' && messageNode.operator === '+') ) { context.report({ - node: reportInfo.message, - message: 'Use report message placeholders instead of string concatenation.', + node: messageNode, + messageId: 'usePlaceholders', }); } } diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js index 3b7278b2..9275883a 100644 --- a/lib/rules/prefer-replace-text.js +++ b/lib/rules/prefer-replace-text.js @@ -1,5 +1,5 @@ /** - * @fileoverview prefer using replaceText instead of replaceTextRange. + * @fileoverview prefer using `replaceText()` instead of `replaceTextRange()` * @author 薛定谔的猫 */ @@ -13,19 +13,21 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'require using replaceText instead of replaceTextRange.', + description: 'require using `replaceText()` instead of `replaceTextRange()`', category: 'Rules', recommended: false, }, - type: 'suggestion', fixable: null, schema: [], + messages: { + useReplaceText: 'Use replaceText instead of replaceTextRange.', + }, }, create (context) { const sourceCode = context.getSourceCode(); - const message = 'Use replaceText instead of replaceTextRange.'; let funcInfo = { upper: null, codePath: null, @@ -47,7 +49,7 @@ module.exports = { parent.parent.parent.type === 'CallExpression' && contextIdentifiers.has(parent.parent.parent.callee.object) && parent.parent.parent.callee.property.name === 'report' && - utils.getReportInfo(parent.parent.parent.arguments).fix === node; + utils.getReportInfo(parent.parent.parent.arguments, context).fix === node; funcInfo = { upper: funcInfo, @@ -74,7 +76,7 @@ module.exports = { if (isIdenticalNodeRange) { context.report({ node, - message, + messageId: 'useReplaceText', }); } } diff --git a/lib/rules/report-message-format.js b/lib/rules/report-message-format.js index cef98cfb..343f46af 100644 --- a/lib/rules/report-message-format.js +++ b/lib/rules/report-message-format.js @@ -5,6 +5,7 @@ 'use strict'; +const { getStaticValue } = require('eslint-utils'); const utils = require('../utils'); // ------------------------------------------------------------------------------ @@ -13,16 +14,19 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { description: 'enforce a consistent format for rule report messages', category: 'Rules', recommended: false, }, - type: 'suggestion', fixable: null, schema: [ { type: 'string' }, ], + messages: { + noMatch: "Report message does not match the pattern '{{pattern}}'.", + }, }, create (context) { @@ -35,13 +39,15 @@ module.exports = { * @returns {void} */ function processMessageNode (message) { + const staticValue = getStaticValue(message, context.getScope()); if ( (message.type === 'Literal' && typeof message.value === 'string' && !pattern.test(message.value)) || - (message.type === 'TemplateLiteral' && message.quasis.length === 1 && !pattern.test(message.quasis[0].value.cooked)) + (message.type === 'TemplateLiteral' && message.quasis.length === 1 && !pattern.test(message.quasis[0].value.cooked)) || + (staticValue && !pattern.test(staticValue.value)) ) { context.report({ node: message, - message: "Report message does not match the pattern '{{pattern}}'.", + messageId: 'noMatch', data: { pattern: context.options[0] || '' }, }); } @@ -75,7 +81,7 @@ module.exports = { contextIdentifiers.has(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node.arguments); + const reportInfo = utils.getReportInfo(node.arguments, context); const message = reportInfo && reportInfo.message; if (!message) { diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.js index ce85c672..31a148f8 100644 --- a/lib/rules/require-meta-docs-description.js +++ b/lib/rules/require-meta-docs-description.js @@ -11,12 +11,12 @@ const DEFAULT_PATTERN = new RegExp('^(enforce|require|disallow)'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'require rules to implement a meta.docs.description property with the correct format', + description: 'require rules to implement a `meta.docs.description` property with the correct format', category: 'Rules', recommended: false, // TODO: enable it in a major release. }, - type: 'suggestion', fixable: null, schema: [ { @@ -30,9 +30,10 @@ module.exports = { }, ], messages: { + extraWhitespace: '`meta.docs.description` must not have leading nor trailing whitespace.', + mismatch: '`meta.docs.description` must match the regexp {{pattern}}.', missing: '`meta.docs.description` is required.', wrongType: '`meta.docs.description` must be a non-empty string.', - extraWhitespace: '`meta.docs.description` must not have leading nor trailing whitespace.', }, }, @@ -77,7 +78,7 @@ module.exports = { } else if (!pattern.test(staticValue.value)) { context.report({ node: descriptionNode.value, - message: '`meta.docs.description` must match the regexp {{pattern}}.', + messageId: 'mismatch', data: { pattern }, }); } diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.js index 78431398..ed7d67a7 100644 --- a/lib/rules/require-meta-docs-url.js +++ b/lib/rules/require-meta-docs-url.js @@ -10,6 +10,7 @@ const path = require('path'); const util = require('../utils'); +const { getStaticValue } = require('eslint-utils'); // ----------------------------------------------------------------------------- // Rule Definition @@ -17,12 +18,12 @@ const util = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'require rules to implement a meta.docs.url property', + description: 'require rules to implement a `meta.docs.url` property', category: 'Rules', recommended: false, }, - type: 'suggestion', fixable: 'code', schema: [{ type: 'object', @@ -31,6 +32,11 @@ module.exports = { }, additionalProperties: false, }], + messages: { + mismatch: '`meta.docs.url` property must be `{{expectedUrl}}`.', + missing: '`meta.docs.url` property is missing.', + wrongType: '`meta.docs.url` property must be a string.', + }, }, /** @@ -48,24 +54,22 @@ module.exports = { : options.pattern.replace(/{{\s*name\s*}}/g, ruleName); /** - * Check whether a given node is the expected URL. - * @param {Node} node The node of property value to check. + * Check whether a given URL is the expected URL. + * @param {string} url The URL to check. * @returns {boolean} `true` if the node is the expected URL. */ - function isExpectedUrl (node) { + function isExpectedUrl (url) { return Boolean( - node && - node.type === 'Literal' && - typeof node.value === 'string' && + typeof url === 'string' && ( expectedUrl === undefined || - node.value === expectedUrl + url === expectedUrl ) ); } return { - Program (node) { + Program () { const info = util.getRuleInfo(sourceCode); if (info === null) { return; @@ -81,39 +85,45 @@ module.exports = { docsPropNode.value.properties && docsPropNode.value.properties.find(p => p.type === 'Property' && util.getKeyName(p) === 'url'); - if (isExpectedUrl(urlPropNode && urlPropNode.value)) { + const staticValue = urlPropNode ? getStaticValue(urlPropNode.value, context.getScope()) : undefined; + if (urlPropNode && !staticValue) { + // Ignore non-static values since we can't determine what they look like. + return; + } + + if (isExpectedUrl(staticValue && staticValue.value)) { return; } context.report({ - loc: - (urlPropNode && urlPropNode.value.loc) || - (docsPropNode && docsPropNode.value.loc) || - (metaNode && metaNode.loc) || - node.loc.start, + node: (urlPropNode && urlPropNode.value) || (docsPropNode && docsPropNode.value) || metaNode || info.create, - message: - !urlPropNode ? 'Rules should export a `meta.docs.url` property.' : - !expectedUrl ? '`meta.docs.url` property must be a string.' : - /* otherwise */ '`meta.docs.url` property must be `{{expectedUrl}}`.', + messageId: + !urlPropNode ? 'missing' : + // eslint-disable-next-line unicorn/no-nested-ternary + !expectedUrl ? 'wrongType' : + /* otherwise */ 'mismatch', data: { expectedUrl, }, fix (fixer) { - if (expectedUrl) { - const urlString = JSON.stringify(expectedUrl); - if (urlPropNode) { + if (!expectedUrl) { + return null; + } + + const urlString = JSON.stringify(expectedUrl); + if (urlPropNode) { + if (urlPropNode.value.type === 'Literal' || (urlPropNode.value.type === 'Identifier' && urlPropNode.value.name === 'undefined')) { return fixer.replaceText(urlPropNode.value, urlString); } - if (docsPropNode && docsPropNode.value.type === 'ObjectExpression') { - return util.insertProperty(fixer, docsPropNode.value, `url: ${urlString}`, sourceCode); - } - if (!docsPropNode && metaNode && metaNode.type === 'ObjectExpression') { - return util.insertProperty(fixer, metaNode, `docs: {\nurl: ${urlString}\n}`, sourceCode); - } + } else if (docsPropNode && docsPropNode.value.type === 'ObjectExpression') { + return util.insertProperty(fixer, docsPropNode.value, `url: ${urlString}`, sourceCode); + } else if (!docsPropNode && metaNode && metaNode.type === 'ObjectExpression') { + return util.insertProperty(fixer, metaNode, `docs: {\nurl: ${urlString}\n}`, sourceCode); } + return null; }, }); diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.js index bdf368b2..3223dedd 100644 --- a/lib/rules/require-meta-fixable.js +++ b/lib/rules/require-meta-fixable.js @@ -1,10 +1,11 @@ /** - * @fileoverview require rules to implement a meta.fixable property + * @fileoverview require rules to implement a `meta.fixable` property * @author Teddy Katz */ 'use strict'; +const { getStaticValue } = require('eslint-utils'); const utils = require('../utils'); // ------------------------------------------------------------------------------ @@ -13,16 +14,34 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'problem', docs: { - description: 'require rules to implement a meta.fixable property', + description: 'require rules to implement a `meta.fixable` property', category: 'Rules', recommended: true, }, - type: 'problem', - schema: [], + schema: [ + { + type: 'object', + properties: { + catchNoFixerButFixableProperty: { + type: 'boolean', + default: false, + }, + }, + additionalProperties: false, + }, + ], + messages: { + invalid: '`meta.fixable` must be either `code`, `whitespace`, or `null`.', + missing: '`meta.fixable` must be either `code` or `whitespace` for fixable rules.', + noFixerButFixableValue: '`meta.fixable` is enabled but no fixer detected.', + }, }, create (context) { + const catchNoFixerButFixableProperty = context.options[0] && context.options[0].catchNoFixerButFixableProperty; + const sourceCode = context.getSourceCode(); const ruleInfo = utils.getRuleInfo(sourceCode); let contextIdentifiers; @@ -62,18 +81,28 @@ module.exports = { ruleInfo.meta.properties.find(prop => utils.getKeyName(prop) === 'fixable'); if (metaFixableProp) { - const VALID_VALUES = new Set(['code', 'whitespace', null, undefined]); - const valueIsValid = metaFixableProp.value.type === 'Literal' - ? VALID_VALUES.has(metaFixableProp.value.value) - : metaFixableProp.value.type === 'TemplateLiteral' && metaFixableProp.value.quasis.length === 1 - ? VALID_VALUES.has(metaFixableProp.value.quasis[0].value.cooked) - : metaFixableProp.value.type === 'Identifier' && metaFixableProp.value.name === 'undefined'; + const staticValue = getStaticValue(metaFixableProp.value, context.getScope()); + if (!staticValue) { + // Ignore non-static values since we can't determine what they look like. + return; + } + + if (!['code', 'whitespace', null, undefined].includes(staticValue.value)) { + // `fixable` property has an invalid value. + context.report({ node: metaFixableProp.value, messageId: 'invalid' }); + return; + } - if (!valueIsValid) { - context.report({ node: metaFixableProp, message: '`meta.fixable` must be either `code`, `whitespace` or `null`.' }); + if (usesFixFunctions && !['code', 'whitespace'].includes(staticValue.value)) { + // Rule is fixable but `fixable` property does not have a fixable value. + context.report({ node: metaFixableProp.value, messageId: 'missing' }); + } else if (catchNoFixerButFixableProperty && !usesFixFunctions && ['code', 'whitespace'].includes(staticValue.value)) { + // Rule is NOT fixable but `fixable` property has a fixable value. + context.report({ node: metaFixableProp.value, messageId: 'noFixerButFixableValue' }); } - } else if (usesFixFunctions) { - context.report({ node: ruleInfo.create, message: 'Fixable rules must export a `meta.fixable` property.' }); + } else if (!metaFixableProp && usesFixFunctions) { + // Rule is fixable but is missing the `fixable` property. + context.report({ node: ruleInfo.meta || ruleInfo.create, messageId: 'missing' }); } }, }; diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.js new file mode 100644 index 00000000..da15e476 --- /dev/null +++ b/lib/rules/require-meta-has-suggestions.js @@ -0,0 +1,105 @@ +'use strict'; + +const utils = require('../utils'); +const { getStaticValue } = require('eslint-utils'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'require suggestable rules to implement a `meta.hasSuggestions` property', + category: 'Rules', + recommended: false, + }, + fixable: 'code', + schema: [], + messages: { + shouldBeSuggestable: '`meta.hasSuggestions` must be `true` for suggestable rules.', + shouldNotBeSuggestable: '`meta.hasSuggestions` cannot be `true` for non-suggestable rules.', + }, + }, + + create (context) { + const sourceCode = context.getSourceCode(); + const ruleInfo = utils.getRuleInfo(sourceCode); + let contextIdentifiers; + let ruleReportsSuggestions; + + return { + Program (node) { + contextIdentifiers = utils.getContextIdentifiers(context, node); + }, + CallExpression (node) { + if ( + node.callee.type === 'MemberExpression' && + contextIdentifiers.has(node.callee.object) && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'report' && + (node.arguments.length > 4 || ( + node.arguments.length === 1 && + node.arguments[0].type === 'ObjectExpression' + )) + ) { + const suggestProp = node.arguments[0].properties.find(prop => utils.getKeyName(prop) === 'suggest'); + if (suggestProp) { + const staticValue = getStaticValue(suggestProp.value, context.getScope()); + if ( + !staticValue || + (Array.isArray(staticValue.value) && staticValue.value.length > 0) || + (Array.isArray(staticValue.value) && staticValue.value.length === 0 && suggestProp.value.type === 'Identifier') // Array variable can have suggestions pushed to it. + ) { + // These are all considered reporting suggestions: + // suggest: [{...}] + // suggest: getSuggestions() + // suggest: MY_SUGGESTIONS + ruleReportsSuggestions = true; + } + } + } + }, + 'Program:exit' () { + const metaNode = ruleInfo && ruleInfo.meta; + const hasSuggestionsProperty = metaNode && metaNode.type === 'ObjectExpression' ? metaNode.properties.find(prop => utils.getKeyName(prop) === 'hasSuggestions') : undefined; + const hasSuggestionsStaticValue = hasSuggestionsProperty && getStaticValue(hasSuggestionsProperty.value, context.getScope()); + + if (ruleReportsSuggestions) { + if (!hasSuggestionsProperty) { + // Rule reports suggestions but is missing the `meta.hasSuggestions` property altogether. + context.report({ + node: metaNode ? metaNode : ruleInfo.create, + messageId: 'shouldBeSuggestable', + fix (fixer) { + if (metaNode && metaNode.type === 'ObjectExpression') { + if (metaNode.properties.length === 0) { + // If object is empty, just replace entire object. + return fixer.replaceText(metaNode, '{ hasSuggestions: true }'); + } + // Add new property to start of property list. + return fixer.insertTextBefore(metaNode.properties[0], 'hasSuggestions: true, '); + } + }, + }); + } else if (hasSuggestionsStaticValue && hasSuggestionsStaticValue.value !== true) { + // Rule reports suggestions but does not have `meta.hasSuggestions` property enabled. + context.report({ + node: hasSuggestionsProperty.value, + messageId: 'shouldBeSuggestable', + fix (fixer) { + if (hasSuggestionsProperty.value.type === 'Literal' || (hasSuggestionsProperty.value.type === 'Identifier' && hasSuggestionsProperty.value.name === 'undefined')) { + return fixer.replaceText(hasSuggestionsProperty.value, 'true'); + } + }, + }); + } + } else if (!ruleReportsSuggestions && hasSuggestionsProperty && hasSuggestionsStaticValue && hasSuggestionsStaticValue.value === true) { + // Rule does not report suggestions but has the `meta.hasSuggestions` property enabled. + context.report({ node: hasSuggestionsProperty.value, messageId: 'shouldNotBeSuggestable' }); + } + }, + }; + }, +}; diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js index 143563c8..976aea37 100644 --- a/lib/rules/require-meta-schema.js +++ b/lib/rules/require-meta-schema.js @@ -9,60 +9,66 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { - description: 'require rules to implement a meta.schema property', + description: 'require rules to implement a `meta.schema` property', category: 'Rules', recommended: false, // TODO: enable it in a major release. }, - type: 'suggestion', - fixable: 'code', schema: [ { type: 'object', properties: { - exceptRange: { + requireSchemaPropertyWhenOptionless: { type: 'boolean', + default: true, }, }, additionalProperties: false, }, ], messages: { + addEmptySchema: 'Add empty schema indicating the rule has no options.', + foundOptionsUsage: '`meta.schema` has no schema defined but rule has options.', missing: '`meta.schema` is required (use [] if rule has no schema).', wrongType: '`meta.schema` should be an array or object (use [] if rule has no schema).', }, + hasSuggestions: true, }, create (context) { const sourceCode = context.getSourceCode(); const { scopeManager } = sourceCode; const info = utils.getRuleInfo(sourceCode); + if (info === null || info.meta === null) { + return {}; + } + + let contextIdentifiers; + const metaNode = info.meta; + let schemaNode; + + // Options + const requireSchemaPropertyWhenOptionless = !context.options[0] || context.options[0].requireSchemaPropertyWhenOptionless; + + let hasEmptySchema = false; + let isUsingOptions = false; return { - Program () { - if (info === null || info.meta === null) { - return; - } + Program (ast) { + contextIdentifiers = utils.getContextIdentifiers(context, ast); - const metaNode = info.meta; - const schemaNode = + schemaNode = metaNode && metaNode.properties && metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'schema'); if (!schemaNode) { - context.report({ - node: metaNode, - messageId: 'missing', - fix (fixer) { - return utils.insertProperty(fixer, metaNode, 'schema: []', sourceCode); - }, - }); return; } let { value } = schemaNode; - if (value.type === 'Identifier') { + if (value.type === 'Identifier' && value.name !== 'undefined') { const variable = findVariable( scopeManager.acquire(value) || scopeManager.globalScope, value @@ -83,10 +89,49 @@ module.exports = { value = variable.defs[0].node.init; } + if ( + (value.type === 'ArrayExpression' && value.elements.length === 0) || + (value.type === 'ObjectExpression' && value.properties.length === 0) + ) { + // Schema is explicitly defined as having no options. + hasEmptySchema = true; + } + if (!['ArrayExpression', 'ObjectExpression'].includes(value.type)) { context.report({ node: value, messageId: 'wrongType' }); } }, + + 'Program:exit' () { + if (!schemaNode && requireSchemaPropertyWhenOptionless) { + context.report({ + node: metaNode, + messageId: 'missing', + suggest: isUsingOptions ? [] : [ + { + messageId: 'addEmptySchema', + fix (fixer) { + return utils.insertProperty(fixer, metaNode, 'schema: []', sourceCode); + }, + }, + ], + }); + } + }, + + MemberExpression (node) { + // Check if `context.options` was used when no options were defined in `meta.schema`. + if ( + (hasEmptySchema || !schemaNode) && + node.object.type === 'Identifier' && + contextIdentifiers.has(node.object) && + node.property.type === 'Identifier' && + node.property.name === 'options' + ) { + isUsingOptions = true; + context.report({ node: schemaNode || metaNode, messageId: 'foundOptionsUsage' }); + } + }, }; }, }; diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.js index f0bb1e86..330d3e94 100644 --- a/lib/rules/require-meta-type.js +++ b/lib/rules/require-meta-type.js @@ -1,10 +1,11 @@ /** - * @fileoverview require rules to implement a meta.type property + * @fileoverview require rules to implement a `meta.type` property * @author 薛定谔的猫 */ 'use strict'; +const { getStaticValue } = require('eslint-utils'); const utils = require('../utils'); const VALID_TYPES = new Set(['problem', 'suggestion', 'layout']); @@ -14,17 +15,17 @@ const VALID_TYPES = new Set(['problem', 'suggestion', 'layout']); module.exports = { meta: { + type: 'problem', docs: { - description: 'require rules to implement a meta.type property', + description: 'require rules to implement a `meta.type` property', category: 'Rules', recommended: false, // TODO: enable it in a major release. }, - type: 'problem', fixable: null, schema: [], messages: { missing: '`meta.type` is required (must be either `problem`, `suggestion`, or `layout`).', - unexpected: '`meta.type` must be either `problem`, `suggestion` or `layout`.', + unexpected: '`meta.type` must be either `problem`, `suggestion`, or `layout`.', }, }, @@ -52,10 +53,19 @@ module.exports = { metaNode.properties && metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'type'); - if (typeNode && typeNode.value.type === 'Literal' && !VALID_TYPES.has(typeNode.value.value)) { - context.report({ node: metaNode, messageId: 'unexpected' }); - } else if (!typeNode) { + if (!typeNode) { context.report({ node: metaNode, messageId: 'missing' }); + return; + } + + const staticValue = getStaticValue(typeNode.value, context.getScope()); + if (!staticValue) { + // Ignore non-static values since we can't determine what they look like. + return; + } + + if (!VALID_TYPES.has(staticValue.value)) { + context.report({ node: typeNode.value, messageId: 'unexpected' }); } }, }; diff --git a/lib/rules/test-case-property-ordering.js b/lib/rules/test-case-property-ordering.js index f7dda406..65ce61c3 100644 --- a/lib/rules/test-case-property-ordering.js +++ b/lib/rules/test-case-property-ordering.js @@ -13,24 +13,26 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { description: 'require the properties of a test case to be placed in a consistent order', category: 'Tests', recommended: false, }, - type: 'suggestion', fixable: 'code', schema: [{ type: 'array', elements: { type: 'string' }, }], + messages: { + inconsistentOrder: 'The properties of a test case should be placed in a consistent order: [{{order}}].', + }, }, create (context) { // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- - const message = 'The properties of a test case should be placed in a consistent order: [{{order}}].'; const order = context.options[0] || [ 'filename', 'code', @@ -58,14 +60,12 @@ module.exports = { // current < lastChecked to catch unordered; // and lastChecked === -1 to catch extra properties before. if (current > -1 && (current < lastChecked || lastChecked === -1)) { - let orderMsg = order.filter(item => keyNames.indexOf(item) > -1); - orderMsg = orderMsg.concat( - lastChecked === -1 ? keyNames.filter(item => order.indexOf(item) === -1) : [] - ); + let orderMsg = order.filter(item => keyNames.includes(item)); + orderMsg = [...orderMsg, ...lastChecked === -1 ? keyNames.filter(item => !order.includes(item)) : []]; context.report({ node: properties[i], - message, + messageId: 'inconsistentOrder', data: { order: orderMsg.join(', ') }, fix (fixer) { return orderMsg.map((key, index) => { diff --git a/lib/rules/test-case-shorthand-strings.js b/lib/rules/test-case-shorthand-strings.js index edec6d20..540571b1 100644 --- a/lib/rules/test-case-shorthand-strings.js +++ b/lib/rules/test-case-shorthand-strings.js @@ -13,14 +13,17 @@ const utils = require('../utils'); module.exports = { meta: { + type: 'suggestion', docs: { description: 'enforce consistent usage of shorthand strings for test cases with no options', category: 'Tests', recommended: false, }, - type: 'suggestion', - schema: [{ enum: ['as-needed', 'never', 'consistent', 'consistent-as-needed'] }], fixable: 'code', + schema: [{ enum: ['as-needed', 'never', 'consistent', 'consistent-as-needed'] }], + messages: { + useShorthand: 'Use {{preferred}} for this test case instead of {{actual}}.', + }, }, create (context) { @@ -62,7 +65,7 @@ module.exports = { }[shorthandOption]).forEach(badCaseInfo => { context.report({ node: badCaseInfo.node, - message: 'Use {{preferred}} for this test case instead of {{actual}}.', + messageId: 'useShorthand', data: { preferred: badCaseInfo.shorthand ? 'an object' : 'a string', actual: badCaseInfo.shorthand ? 'a string' : 'an object', diff --git a/lib/utils.js b/lib/utils.js index 829c16ad..024360bc 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,7 @@ 'use strict'; +const { getStaticValue } = require('eslint-utils'); + /** * Determines whether a node is a 'normal' (i.e. non-async, non-generator) function expression. * @param {ASTNode} node The node in question @@ -51,7 +53,7 @@ function isNormalFunctionExpressionReference (node, scopeManager) { } const definitions = createReference.resolved.defs; - if (!definitions || !definitions.length) { + if (!definitions || definitions.length === 0) { return false; } @@ -101,6 +103,7 @@ module.exports = { .map(statement => statement.expression) .filter(expression => expression.type === 'AssignmentExpression') .filter(expression => expression.left.type === 'MemberExpression') + // eslint-disable-next-line unicorn/prefer-object-from-entries .reduce((currentExports, node) => { if ( node.left.object.type === 'Identifier' && node.left.object.name === 'module' && @@ -117,6 +120,7 @@ module.exports = { // Check `module.exports = { create: function () {}, meta: {} }` exportsIsFunction = false; + // eslint-disable-next-line unicorn/prefer-object-from-entries return node.right.properties.reduce((parsedProps, prop) => { const keyValue = module.exports.getKeyName(prop); if (INTERESTING_KEYS.has(keyValue)) { @@ -173,7 +177,7 @@ module.exports = { getContextIdentifiers (context, ast) { const ruleInfo = module.exports.getRuleInfo({ ast }); - if (!ruleInfo || !ruleInfo.create.params.length || ruleInfo.create.params[0].type !== 'Identifier') { + if (!ruleInfo || ruleInfo.create.params.length === 0 || ruleInfo.create.params[0].type !== 'Identifier') { return new Set(); } @@ -260,19 +264,21 @@ module.exports = { /** * Gets information on a report, given the arguments passed to context.report(). * @param {ASTNode[]} reportArgs The arguments passed to context.report() + * @param {Context} context */ - getReportInfo (reportArgs) { + getReportInfo (reportArgs, context) { // If there is exactly one argument, the API expects an object. // Otherwise, if the second argument is a string, the arguments are interpreted as // ['node', 'message', 'data', 'fix']. // Otherwise, the arguments are interpreted as ['node', 'loc', 'message', 'data', 'fix']. - if (!reportArgs.length) { + if (reportArgs.length === 0) { return null; } if (reportArgs.length === 1) { if (reportArgs[0].type === 'ObjectExpression') { + // eslint-disable-next-line unicorn/prefer-object-from-entries return reportArgs[0].properties.reduce((reportInfo, property) => { const propName = module.exports.getKeyName(property); @@ -287,15 +293,17 @@ module.exports = { let keys; + const secondArgStaticValue = getStaticValue(reportArgs[1], context.getScope()); if ( - (reportArgs[1].type === 'Literal' && typeof reportArgs[1].value === 'string') || + (secondArgStaticValue && typeof secondArgStaticValue.value === 'string') || reportArgs[1].type === 'TemplateLiteral' ) { keys = ['node', 'message', 'data', 'fix']; } else if ( reportArgs[1].type === 'ObjectExpression' || reportArgs[1].type === 'ArrayExpression' || - (reportArgs[1].type === 'Literal' && typeof reportArgs[1].value !== 'string') + (reportArgs[1].type === 'Literal' && typeof reportArgs[1].value !== 'string') || + (secondArgStaticValue && ['object', 'number'].includes(typeof secondArgStaticValue.value)) ) { keys = ['node', 'loc', 'message', 'data', 'fix']; } else { @@ -303,9 +311,9 @@ module.exports = { return null; } - return keys + return Object.fromEntries(keys .slice(0, reportArgs.length) - .reduce((reportInfo, key, index) => Object.assign(reportInfo, { [key]: reportArgs[index] }), {}); + .map((key, index) => [key, reportArgs[index]])); }, /** @@ -315,7 +323,7 @@ module.exports = { * @returns {Set} A set of all identifiers referring to the `SourceCode` object. */ getSourceCodeIdentifiers (context, ast) { - return new Set(Array.from(module.exports.getContextIdentifiers(context, ast)) + return new Set([...module.exports.getContextIdentifiers(context, ast)] .filter(identifier => identifier.parent && identifier.parent.type === 'MemberExpression' && identifier === identifier.parent.object && @@ -327,10 +335,8 @@ module.exports = { identifier.parent.parent === identifier.parent.parent.parent.init && identifier.parent.parent.parent.id.type === 'Identifier' ) - .map(identifier => context.getDeclaredVariables(identifier.parent.parent.parent)) - .reduce((allVariables, variablesForIdentifier) => allVariables.concat(variablesForIdentifier), []) - .map(variable => variable.references) - .reduce((allRefs, refsForVariable) => allRefs.concat(refsForVariable), []) + .flatMap(identifier => context.getDeclaredVariables(identifier.parent.parent.parent)) + .flatMap(variable => variable.references) .map(ref => ref.identifier)); }, diff --git a/package.json b/package.json index 5a80300d..3194399c 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,17 @@ { "name": "eslint-plugin-eslint-plugin", - "version": "3.0.3", + "version": "3.6.1", "description": "An ESLint plugin for linting ESLint plugins", "author": "Teddy Katz", "main": "lib/index.js", "license": "MIT", "scripts": { - "lint": "eslint . --ignore-pattern \"!.*\"", + "lint": "npm-run-all --continue-on-error --aggregate-output --parallel lint:*", + "lint:docs": "markdownlint **/*.md", + "lint:js": "eslint .", "generate-readme-table": "node build/generate-readme-table.js", "generate-release": "node-release-script", - "test": "mocha tests --recursive" + "test": "nyc --all --check-coverage --include lib mocha tests --recursive" }, "files": [ "lib/" @@ -28,7 +30,13 @@ }, "homepage": "https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin#readme", "dependencies": { - "eslint-utils": "^2.1.0" + "eslint-utils": "^3.0.0" + }, + "nyc": { + "branches": 98, + "functions": 98, + "lines": 99, + "statements": 99 }, "devDependencies": { "@not-an-aardvark/node-release-script": "^0.1.0", @@ -36,18 +44,23 @@ "dirty-chai": "^2.0.1", "eslint": "^7.9.0", "eslint-config-not-an-aardvark": "^2.1.0", + "eslint-plugin-markdown": "^2.0.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-self": "^1.2.1", + "eslint-plugin-unicorn": "^36.0.0", "eslint-scope": "^5.1.1", "espree": "^7.3.0", "estraverse": "^5.0.0", "lodash": "^4.17.2", - "mocha": "^7.1.1" + "markdownlint-cli": "^0.27.1", + "mocha": "^7.1.1", + "npm-run-all": "^4.1.5", + "nyc": "^15.1.0" }, "peerDependencies": { - "eslint": "^7.0.0" + "eslint": ">=6.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "12.* || 14.* || >= 16.*" } } diff --git a/tests/.eslintrc.yml b/tests/.eslintrc.yml deleted file mode 100644 index 9808c3b2..00000000 --- a/tests/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -env: - mocha: true diff --git a/tests/lib/index.js b/tests/lib/index.js index 1361f073..2dee7b5a 100644 --- a/tests/lib/index.js +++ b/tests/lib/index.js @@ -3,9 +3,11 @@ const assert = require('chai').assert; const plugin = require('../..'); +const RULE_NAMES = Object.keys(plugin.rules); + describe('exported plugin', () => { describe('adds a meta.docs.url property to each rule', () => { - Object.keys(plugin.rules).forEach(ruleName => { + RULE_NAMES.forEach(ruleName => { it(ruleName, () => { assert.match( plugin.rules[ruleName].meta.docs.url, diff --git a/tests/lib/rule-setup.js b/tests/lib/rule-setup.js new file mode 100644 index 00000000..856f4a57 --- /dev/null +++ b/tests/lib/rule-setup.js @@ -0,0 +1,126 @@ +'use strict'; + +const { readdirSync, readFileSync } = require('fs'); +const path = require('path'); +const assert = require('chai').assert; +const plugin = require('../..'); + +const RULE_NAMES = Object.keys(plugin.rules); +const RULE_NAMES_RECOMMENDED = new Set(Object.keys(plugin.configs.recommended.rules)); + +const MESSAGES = { + fixable: + '⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.', + configRecommended: + '✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.', + hasSuggestions: '💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).', +}; + +/** + * @param {string} string - to operate on + * @returns the string with a capitalized first letter + */ +function capitalizeFirstLetter (string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +describe('rule setup is correct', () => { + it('should have a list of exported rules and rules directory that match', () => { + const filePath = path.join(__dirname, '..', 'lib', 'rules'); + const files = readdirSync(filePath); + + assert.deepStrictEqual( + RULE_NAMES, + files + .filter(file => !file.startsWith('.')) + .map(file => file.replace('.js', '')) + ); + }); + + it('should have tests for all rules', () => { + const filePath = path.join(__dirname, 'rules'); + const files = readdirSync(filePath); + + assert.deepStrictEqual( + RULE_NAMES, + files + .filter(file => !file.startsWith('.')) + .map(file => file.replace('.js', '')) + ); + }); + + it('should have documentation for all rules', () => { + const filePath = path.join(__dirname, '..', '..', 'docs', 'rules'); + const files = readdirSync(filePath); + + assert.deepStrictEqual( + RULE_NAMES, + files + .filter(file => !file.startsWith('.')) + .map(file => file.replace('.md', '')) + ); + }); + + describe('rule documentation files', () => { + for (const ruleName of RULE_NAMES) { + const rule = plugin.rules[ruleName]; + const filePath = path.join( + __dirname, + '..', + '..', + 'docs', + 'rules', + `${ruleName}.md` + ); + const fileContents = readFileSync(filePath, 'utf8'); + const lines = fileContents.split('\n'); + + describe(ruleName, () => { + it('should have the right contents (title, notices, etc)', () => { + // Title + assert.strictEqual(lines[0], `# ${capitalizeFirstLetter(rule.meta.docs.description)} (${ruleName})`, 'first line has rule description and name'); + assert.strictEqual(lines[1], '', 'second line is blank'); + + // Rule Details + assert.ok(fileContents.includes('## Rule Details'), 'includes "## Rule Details" header'); + + // Examples + assert.ok(fileContents.includes('Examples of **incorrect** code for this rule'), 'includes incorrect examples'); + assert.ok(fileContents.includes('Examples of **correct** code for this rule'), 'includes correct examples'); + + // Decide which notices should be shown at the top of the doc. + const expectedNotices = []; + const unexpectedNotices = []; + if (RULE_NAMES_RECOMMENDED.has('eslint-plugin/' + ruleName)) { + expectedNotices.push('configRecommended'); + } else { + unexpectedNotices.push('configRecommended'); + } + if (rule.meta.fixable) { + expectedNotices.push('fixable'); + } else { + unexpectedNotices.push('fixable'); + } + if (rule.meta.hasSuggestions) { + expectedNotices.push('hasSuggestions'); + } else { + unexpectedNotices.push('hasSuggestions'); + } + + // Ensure that expected notices are present in the correct order. + let currentLineNumber = 1; + for (const expectedNotice of expectedNotices) { + assert.strictEqual(lines[currentLineNumber], ''); + assert.strictEqual(lines[currentLineNumber + 1], MESSAGES[expectedNotice]); + currentLineNumber += 2; + } + + // Ensure that unexpected notices are not present. + for (const unexpectedNotice of unexpectedNotices) { + assert.ok(!fileContents.includes(MESSAGES[unexpectedNotice]), 'does not include notice: ' + MESSAGES[unexpectedNotice]); + } + }); + }); + } + }); +}); diff --git a/tests/lib/rules/consistent-output.js b/tests/lib/rules/consistent-output.js index 627eebf4..535dd9cf 100644 --- a/tests/lib/rules/consistent-output.js +++ b/tests/lib/rules/consistent-output.js @@ -1,5 +1,5 @@ /** - * @fileoverview Enforce consistent use of output assertions in rule tests + * @fileoverview Enforce consistent use of `output` assertions in rule tests * @author Teddy Katz */ @@ -12,7 +12,7 @@ const rule = require('../../../lib/rules/consistent-output'); const RuleTester = require('eslint').RuleTester; -const ERROR = { message: 'This test case should have an output assertion.', type: 'ObjectExpression' }; +const ERROR = { messageId: 'missingOutput', type: 'ObjectExpression' }; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.js index d8c6e2dd..11cf7ab6 100644 --- a/tests/lib/rules/fixer-return.js +++ b/tests/lib/rules/fixer-return.js @@ -1,5 +1,5 @@ /** - * @fileoverview enforces always return from a fixer function + * @fileoverview require fixer functions to return a fix * @author 薛定谔的猫 */ @@ -12,8 +12,6 @@ const rule = require('../../../lib/rules/fixer-return'); const RuleTester = require('eslint').RuleTester; -const ERROR = { message: 'Expected fixer function to always return a value.' }; - // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ @@ -46,6 +44,38 @@ ruleTester.run('fixer-return', rule, { } }; `, + // Not the right fix function. + ` + module.exports = { + create: function(context) { + context.report( { + notFix: function(fixer) { + } + }); + } + }; + `, + // Not the right fix function (arrow function with implied return) + ` + module.exports = { + create: function(context) { + context.report( { + notFix: fixer => undefined + }); + } + }; + `, + // Not the right fix function (arrow function) + ` + module.exports = { + create: function(context) { + context.report( { + notFix: fixer => {} + }); + } + }; + `, + // Arrow function (expression) ` module.exports = { create: function(context) { @@ -55,6 +85,17 @@ ruleTester.run('fixer-return', rule, { } }; `, + // Arrow function (with return) + ` + module.exports = { + create: function(context) { + context.report({ + fix: fixer => {return fixer.foo();} + }); + } + }; + `, + // Generator ` module.exports = { create: function (context) { @@ -66,36 +107,300 @@ ruleTester.run('fixer-return', rule, { } }; `, + // Yielded a fix object, but with one code branch that has no autofix. + ` + module.exports = { + create: function (context) { + context.report({ + fix: function* (fixer) { + if (foo) { + return; // no autofix in this case + } + yield fixer.foo(); + } + }); + } + }; + `, + // Return fix in variable. + ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + const fix = fixer.foo(); + return fix; + } + }); + } + }; + `, + // Return array variable. + ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + const fixers = []; + // ... fixers could be added to this array here + return fixers; + } + }); + } + }; + `, + // Return fix in array. + ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + return [fixer.foo()]; + } + }); + } + }; + `, + // With one code branch that has no autofix (return null). + ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + if (foo) { + return null; // no autofix in this case + } + return fixer.foo(); + } + }); + } + }; + `, + // With one code branch that has no autofix (return undefined). + ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + if (foo) { + return undefined; // no autofix in this case + } + return fixer.foo(); + } + }); + } + }; + `, + // With one code branch that has no autofix (return empty array). + ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + if (foo) { + return []; // no autofix in this case + } + return fixer.foo(); + } + }); + } + }; + `, + // With one code branch that has no autofix (return implicit undefined). + ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + if (foo) { + return; // no autofix in this case + } + return fixer.foo(); + } + }); + } + }; + `, ], invalid: [ { + // Fix but missing return + code: ` + module.exports = { + create: function(context) { + context.report({ + fix(fixer) { + fixer.foo(); + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 24 }], + }, + { + // Fix but missing return (arrow function, report on arrow) + code: ` + module.exports = { + create: function(context) { + context.report({ + fix: (fixer) => { + fixer.foo(); + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'ArrowFunctionExpression', line: 5, endLine: 5, column: 34, endColumn: 36 }], + }, + { + // With no autofix (arrow function, explicit return, report on arrow) + code: ` + module.exports = { + create: function(context) { + context.report({ + fix: (fixer) => { + return undefined; + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'ArrowFunctionExpression', line: 5, endLine: 5, column: 34, endColumn: 36 }], + }, + { + // With no autofix (arrow function, implied return, report on arrow) + code: ` + module.exports = { + create: function(context) { + context.report( { + fix: fixer => undefined + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'ArrowFunctionExpression', line: 5, endLine: 5, column: 32, endColumn: 34 }], + }, + { + // Fix but missing yield (generator) + code: ` + module.exports = { + create: function(context) { + context.report({ + *fix(fixer) { + fixer.foo(); + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 25 }], + }, + { + // With no autofix (only yield undefined) + code: ` + module.exports = { + create: function(context) { + context.report({ + *fix(fixer) { + yield undefined; + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 25 }], + }, + { + // With no autofix (only return null) + code: ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + return null; + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 26 }], + }, + { + // With no autofix (only return undefined) + code: ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + return undefined; + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 26 }], + }, + { + // With no autofix (only return undefined, but in variable) + code: ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + const returnValue = undefined; + return returnValue; + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 26 }], + }, + { + // With no autofix (only return implicit undefined) + code: ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + return; + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 26 }], + }, + { + // With no autofix (only return empty array) code: ` - module.exports = { - create: function(context) { - context.report({ - fix(fixer) { - fixer.foo(); - } - }); - } - }; - `, - errors: [ERROR], + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + return []; + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 26 }], }, { + // With no autofix (no return, empty function) code: ` - module.exports = { - create: function(context) { - context.report({ - *fix(fixer) { - fixer.foo(); - } - }); - } - }; - `, - errors: [ERROR], + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + } + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 26 }], }, ], }); diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.js index 29692646..39dd5d15 100644 --- a/tests/lib/rules/meta-property-ordering.js +++ b/tests/lib/rules/meta-property-ordering.js @@ -15,14 +15,6 @@ const RuleTester = require('eslint').RuleTester; // Tests // ------------------------------------------------------------------------------ -/** - * @param {string[]} order - * @returns {string} - */ -function getMessage (order) { - return `The meta properties should be placed in a consistent order: [${order.join(', ')}].`; -} - const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); ruleTester.run('test-case-property-ordering', rule, { valid: [ @@ -92,7 +84,7 @@ ruleTester.run('test-case-property-ordering', rule, { }, create() {}, };`, - errors: [{ message: getMessage(['type', 'docs', 'fixable']) }], + errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }], }, { code: ` @@ -107,27 +99,27 @@ ruleTester.run('test-case-property-ordering', rule, { create() {}, };`, errors: [ - { message: getMessage(['type', 'docs', 'fixable', 'schema']) }, - { message: getMessage(['type', 'docs', 'fixable', 'schema']) }, + { messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable', 'schema'].join(', ') } }, + { messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable', 'schema'].join(', ') } }, ], }, { code: ` module.exports = { - meta: {fixable, fooooooooo, doc, type}, + meta: {fixable, fooooooooo, docs, type}, create() {}, };`, output: ` module.exports = { - meta: {type, doc, fixable, fooooooooo}, + meta: {type, docs, fixable, fooooooooo}, create() {}, };`, - options: [['type', 'doc', 'fixable']], + options: [['type', 'docs', 'fixable']], errors: [ - { message: getMessage(['type', 'doc', 'fixable']) }, - { message: getMessage(['type', 'doc', 'fixable']) }, + { messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }, + { messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }, ], }, ], diff --git a/tests/lib/rules/no-deprecated-report-api.js b/tests/lib/rules/no-deprecated-report-api.js index 24a81272..d71e3df3 100644 --- a/tests/lib/rules/no-deprecated-report-api.js +++ b/tests/lib/rules/no-deprecated-report-api.js @@ -1,5 +1,5 @@ /** - * @fileoverview disallow use of the deprecated context.report() API + * @fileoverview Disallow the version of `context.report()` with multiple arguments * @author Teddy Katz */ @@ -11,7 +11,6 @@ const rule = require('../../../lib/rules/no-deprecated-report-api'); const RuleTester = require('eslint').RuleTester; -const ERROR = { message: 'Use the new-style context.report() API.', type: 'Identifier' }; // ------------------------------------------------------------------------------ // Tests @@ -62,6 +61,24 @@ ruleTester.run('no-deprecated-report-api', rule, { } }; `, + // With object as variable. + ` + const OBJ = {node, message}; + module.exports = { + create(context) { + context.report(OBJ); + } + }; + `, + // With object as variable but cannot determine its value statically. + ` + const OBJ = getObj(); + module.exports = { + create(context) { + context.report(OBJ); + } + }; + `, ], invalid: [ @@ -80,7 +97,7 @@ ruleTester.run('no-deprecated-report-api', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -97,7 +114,7 @@ ruleTester.run('no-deprecated-report-api', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -114,7 +131,7 @@ ruleTester.run('no-deprecated-report-api', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -125,7 +142,7 @@ ruleTester.run('no-deprecated-report-api', rule, { }; `, output: null, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -142,7 +159,7 @@ ruleTester.run('no-deprecated-report-api', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -159,7 +176,40 @@ ruleTester.run('no-deprecated-report-api', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], + }, + { + // With message string in variable. + code: ` + const MESSAGE = 'foo'; + module.exports = { + create(context) { + context.report(theNode, MESSAGE); + } + }; + `, + output: ` + const MESSAGE = 'foo'; + module.exports = { + create(context) { + context.report({node: theNode, message: MESSAGE}); + } + }; + `, + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], + }, + { + // With message in variable but no autofix since we can't statically determine its type. + code: ` + const MESSAGE = getMessage(); + module.exports = { + create(context) { + context.report(theNode, MESSAGE); + } + }; + `, + output: null, + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -170,7 +220,7 @@ ruleTester.run('no-deprecated-report-api', rule, { }; `, output: null, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -183,7 +233,7 @@ ruleTester.run('no-deprecated-report-api', rule, { context.report({node: theNode, message: \`blah\`, data: theData, fix: theFix}); }; `, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -196,7 +246,50 @@ ruleTester.run('no-deprecated-report-api', rule, { context.report({node: theNode, loc: 5, message: foo, data: bar}); }; `, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], + }, + { + // Location in variable as number. + code: ` + const LOC = 5; + module.exports.create = context => { + context.report(theNode, LOC, foo, bar); + }; + `, + output: ` + const LOC = 5; + module.exports.create = context => { + context.report({node: theNode, loc: LOC, message: foo, data: bar}); + }; + `, + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], + }, + { + // Location in variable as object. + code: ` + const LOC = { line: 1, column: 2 }; + module.exports.create = context => { + context.report(theNode, LOC, foo, bar); + }; + `, + output: ` + const LOC = { line: 1, column: 2 }; + module.exports.create = context => { + context.report({node: theNode, loc: LOC, message: foo, data: bar}); + }; + `, + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], + }, + { + // Location in variable but no autofix since we can't statically determine its type. + code: ` + const LOC = getLoc(); + module.exports.create = context => { + context.report(theNode, LOC, foo, bar); + }; + `, + output: null, + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -207,7 +300,7 @@ ruleTester.run('no-deprecated-report-api', rule, { }; `, output: null, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, { code: ` @@ -218,7 +311,7 @@ ruleTester.run('no-deprecated-report-api', rule, { }; `, output: null, - errors: [ERROR], + errors: [{ messageId: 'useNewAPI', type: 'Identifier' }], }, ], }); diff --git a/tests/lib/rules/no-identical-tests.js b/tests/lib/rules/no-identical-tests.js index 343c5cfd..d8c7bd74 100644 --- a/tests/lib/rules/no-identical-tests.js +++ b/tests/lib/rules/no-identical-tests.js @@ -12,7 +12,8 @@ const rule = require('../../../lib/rules/no-identical-tests'); const RuleTester = require('eslint').RuleTester; -const ERROR = { message: 'This test case is identical to another case.' }; +const ERROR_OBJECT_TEST = { messageId: 'identical', type: 'ObjectExpression' }; +const ERROR_STRING_TEST = { messageId: 'identical', type: 'Literal' }; // ------------------------------------------------------------------------------ // Tests @@ -46,6 +47,26 @@ ruleTester.run('no-identical-tests', rule, { invalid: [] }); `, + // Object and string test. + ` + new RuleTester().run('foo', bar, { + valid: [ + { code: 'foo' }, + 'foo', + ], + invalid: [] + }); + `, + // One test object with more properties than the other. + ` + new RuleTester().run('foo', bar, { + valid: [ + { code: 'foo' }, + { code: 'foo', options: [{}] }, + ], + invalid: [] + }); + `, ], invalid: [ @@ -67,7 +88,7 @@ ruleTester.run('no-identical-tests', rule, { invalid: [] }); `, - errors: [ERROR], + errors: [ERROR_OBJECT_TEST], }, { code: ` @@ -87,7 +108,7 @@ ruleTester.run('no-identical-tests', rule, { invalid: [] }); `, - errors: [ERROR], + errors: [ERROR_OBJECT_TEST], }, { code: ` @@ -112,7 +133,7 @@ ruleTester.run('no-identical-tests', rule, { ] }); `, - errors: [ERROR, ERROR], + errors: [ERROR_OBJECT_TEST, ERROR_OBJECT_TEST], }, { code: ` @@ -132,7 +153,28 @@ ruleTester.run('no-identical-tests', rule, { invalid: [] }); `, - errors: [ERROR], + errors: [ERROR_OBJECT_TEST], + }, + { + // Empty objects. + code: ` + new RuleTester().run('foo', bar, { + valid: [ + {}, + {}, + ], + invalid: [] + }); + `, + output: ` + new RuleTester().run('foo', bar, { + valid: [ + {}, + ], + invalid: [] + }); + `, + errors: [ERROR_OBJECT_TEST], }, { code: ` @@ -152,7 +194,7 @@ ruleTester.run('no-identical-tests', rule, { invalid: [] }); `, - errors: [ERROR], + errors: [ERROR_STRING_TEST], }, ], }); diff --git a/tests/lib/rules/no-missing-placeholders.js b/tests/lib/rules/no-missing-placeholders.js index c6574c9b..09b8ce36 100644 --- a/tests/lib/rules/no-missing-placeholders.js +++ b/tests/lib/rules/no-missing-placeholders.js @@ -17,8 +17,8 @@ const RuleTester = require('eslint').RuleTester; * @param {string} missingKey The placeholder that is missing * @returns {object} An expected error */ -function error (missingKey) { - return { type: 'Literal', message: `The placeholder {{${missingKey}}} does not exist.` }; +function error (missingKey, type = 'Literal') { + return { type, message: `The placeholder {{${missingKey}}} does not exist.` }; } // ------------------------------------------------------------------------------ @@ -114,6 +114,20 @@ ruleTester.run('no-missing-placeholders', rule, { } }; `, + // Message in variable. + ` + const MESSAGE = 'foo {{bar}}'; + module.exports = context => { + context.report(node, MESSAGE, { bar: 'baz' }); + }; + `, + // Message in variable but cannot statically determine its type. + ` + const MESSAGE = getMessage(); + module.exports = context => { + context.report(node, MESSAGE, { baz: 'qux' }); + }; + `, ], invalid: [ @@ -166,6 +180,16 @@ ruleTester.run('no-missing-placeholders', rule, { `, errors: [error('bar')], }, + { + // Message in variable. + code: ` + const MESSAGE = 'foo {{bar}}'; + module.exports = context => { + context.report(node, MESSAGE, { baz: 'qux' }); + }; + `, + errors: [error('bar', 'Identifier')], + }, { code: ` module.exports = context => { diff --git a/tests/lib/rules/no-only-tests.js b/tests/lib/rules/no-only-tests.js new file mode 100644 index 00000000..8fb6e51c --- /dev/null +++ b/tests/lib/rules/no-only-tests.js @@ -0,0 +1,251 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-only-tests'); +const RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +ruleTester.run('no-only-tests', rule, { + valid: [ + // No test cases with `only` + ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [ + 'foo', + { code: 'foo', foo: true }, + RuleTester.somethingElse(), + notRuleTester.only() + ], + invalid: [ + { code: 'bar', foo: true }, + ] + }); + `, + // `only` set to `false` + ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [ + { code: 'foo', only: false }, + ], + invalid: [ + { code: 'bar', only: false }, + ] + }); + `, + ], + + invalid: [ + { + // Valid test case with `only` + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [ + { code: 'foo', only: true }, + ], + invalid: [] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 6, + endLine: 6, + column: 28, + endColumn: 38, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [ + { code: 'foo' }, + ], + invalid: [] + });`, + }, + ], + }, + ], + }, + { + // Invalid test case with `only` (property at end of object, no trailing comma) + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', only: true }, + ] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 7, + endLine: 7, + column: 28, + endColumn: 38, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo' }, + ] + });`, + }, + ], + }, + ], + }, + { + // Invalid test case with `only` (property at end of object, with trailing comma) + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', only: true, }, + ] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 7, + endLine: 7, + column: 28, + endColumn: 38, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', }, + ] + });`, + }, + ], + }, + ], + }, + { + // Invalid test case with `only` (property in middle of object) + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', only: true, bar: true }, + ] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 7, + endLine: 7, + column: 28, + endColumn: 38, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', bar: true }, + ] + });`, + }, + ], + }, + ], + }, + { + // Invalid test case with `only` (property at beginning of object) + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { only: true, code: 'foo' }, + ] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 7, + endLine: 7, + column: 15, + endColumn: 25, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo' }, + ] + });`, + }, + ], + }, + ], + }, + + { + // Valid test case using `RuleTester.only` + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + new RuleTester().run('foo', bar, { + valid: [ + RuleTester.only('foo'), + ], + invalid: [] + }); + `, + output: null, + errors: [{ messageId: 'foundOnly', type: 'MemberExpression', line: 6, endLine: 6, column: 13, endColumn: 28, suggestions: [] }], + }, + ], +}); diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.js index febe8c4f..4646de9f 100644 --- a/tests/lib/rules/no-unused-placeholders.js +++ b/tests/lib/rules/no-unused-placeholders.js @@ -17,8 +17,8 @@ const RuleTester = require('eslint').RuleTester; * @param {string} unusedKey The placeholder that is unused * @returns {object} An expected error */ -function error (unusedKey) { - return { type: 'Literal', message: `The placeholder {{${unusedKey}}} is unused.` }; +function error (unusedKey, type = 'Literal') { + return { type, message: `The placeholder {{${unusedKey}}} is unused.` }; } // ------------------------------------------------------------------------------ @@ -88,6 +88,20 @@ ruleTester.run('no-unused-placeholders', rule, { context.report(node, 'foo {{bar}}', { bar: 'baz' }); }; `, + // With message as variable. + ` + const MESSAGE = 'foo {{bar}}'; + module.exports = context => { + context.report(node, MESSAGE, { bar: 'baz' }); + }; + `, + // With message as variable but cannot statically determine its type. + ` + const MESSAGE = getMessage(); + module.exports = context => { + context.report(node, MESSAGE, { bar: 'baz' }); + }; + `, ` module.exports = context => { context.report(node, { line: 1, column: 3 }, 'foo {{bar}}', { bar: 'baz' }); @@ -110,6 +124,22 @@ ruleTester.run('no-unused-placeholders', rule, { `, errors: [error('bar')], }, + { + // With message as variable. + code: ` + const MESSAGE = 'foo'; + module.exports = { + create(context) { + context.report({ + node, + message: MESSAGE, + data: { bar } + }); + } + }; + `, + errors: [error('bar', 'Identifier')], + }, { code: ` module.exports = { diff --git a/tests/lib/rules/no-useless-token-range.js b/tests/lib/rules/no-useless-token-range.js index c8d58b79..19671f10 100644 --- a/tests/lib/rules/no-useless-token-range.js +++ b/tests/lib/rules/no-useless-token-range.js @@ -1,5 +1,5 @@ /** - * @fileoverview Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken + * @fileoverview Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` * @author Teddy Katz */ diff --git a/tests/lib/rules/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.js new file mode 100644 index 00000000..cfabcef4 --- /dev/null +++ b/tests/lib/rules/prefer-message-ids.js @@ -0,0 +1,179 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/prefer-message-ids'); +const RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +ruleTester.run('prefer-message-ids', rule, { + valid: [ + ` + module.exports = { + create(context) { + context.report({ node }); + } + }; + `, + ` + module.exports = { + create(context) { + context.report({ node, messageId: 'foo' }); + } + }; + `, + ` + module.exports = { + create(context) { + foo.report({ node, message: 'foo' }); // unrelated function + } + }; + `, + ` + module.exports = { + create(context) { + context.foo({ node, message: 'foo' }); // unrelated function + } + }; + `, + ` + context.report({ node, message: 'foo' }); // outside rule + module.exports = { + create(context) { + } + }; + `, + ` + // Tests are still allowed to use 'message' which is helpful for verifying that dynamically-constructed messages (i.e. from placeholders) look correct. + new RuleTester().run('foo', bar, { + invalid: [ + { code: 'foo', errors: [{message: 'foo'}] }, + ] + }); + `, + + // `meta.messages` has a message + ` + module.exports = { + meta: { messages: { someMessageId: 'some message' } }, + create(context) { + context.report({ node, messageId: 'someMessageId' }); + } + }; + `, + // `meta.messages` has a message (in variable) + ` + const messages = { someMessageId: 'some message' }; + module.exports = { + meta: { messages }, + create(context) { + context.report({ node, messageId: 'someMessageId' }); + } + }; + `, + ], + + invalid: [ + { + code: ` + module.exports = { + create(context) { + context.report({ node, message: 'foo' }); + } + }; + `, + errors: [{ messageId: 'foundMessage', type: 'Property' }], + }, + { + // With message in variable. + code: ` + const MESSAGE = \`\${foo} is bad.\`; + module.exports = { + create(context) { + context.report({ + node, + message: MESSAGE + }); + } + }; + `, + errors: [{ messageId: 'foundMessage', type: 'Property' }], + }, + { + // With constructed message. + code: ` + module.exports = { + create(context) { + context.report({ + node, + message: foo + ' is bad.' + }); + } + }; + `, + errors: [{ messageId: 'foundMessage', type: 'Property' }], + }, + + { + // `meta.messages` missing + code: ` + module.exports = { + meta: { description: 'foo' }, + create(context) { } + }; + `, + errors: [{ messageId: 'messagesMissing', type: 'ObjectExpression' }], + }, + { + // `meta.messages` empty + code: ` + module.exports = { + meta: { + description: 'foo', + messages: {}, + }, + create(context) { } + }; + `, + errors: [{ messageId: 'messagesMissing', type: 'ObjectExpression' }], + }, + { + // `meta.messages` empty (in variable) + code: ` + const messages = {}; + module.exports = { + meta: { + description: 'foo', + messages, + }, + create(context) { } + }; + `, + errors: [{ messageId: 'messagesMissing', type: 'Identifier' }], + }, + { + // `meta.messages` missing and using `message` + code: ` + module.exports = { + meta: { + description: 'foo', + }, + create(context) { + context.report({ node, message: 'foo' }); + } + }; + `, + errors: [ + { messageId: 'messagesMissing', type: 'ObjectExpression' }, + { messageId: 'foundMessage', type: 'Property' }, + ], + }, + ], +}); diff --git a/tests/lib/rules/prefer-output-null.js b/tests/lib/rules/prefer-output-null.js index 2c85ee86..6eabb455 100644 --- a/tests/lib/rules/prefer-output-null.js +++ b/tests/lib/rules/prefer-output-null.js @@ -1,5 +1,5 @@ /** - * @fileoverview disallows invalid RuleTester test cases with the output the same as the code. + * @fileoverview disallows invalid RuleTester test cases where the `output` matches the `code` * @author 薛定谔的猫 */ @@ -12,7 +12,7 @@ const rule = require('../../../lib/rules/prefer-output-null'); const RuleTester = require('eslint').RuleTester; -const ERROR = { message: 'Use `output: null` to assert that a test case is not autofixed.' }; +const ERROR = { messageId: 'useOutputNull', type: 'Property' }; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/prefer-placeholders.js b/tests/lib/rules/prefer-placeholders.js index 140634d4..2de61b85 100644 --- a/tests/lib/rules/prefer-placeholders.js +++ b/tests/lib/rules/prefer-placeholders.js @@ -1,5 +1,5 @@ /** - * @fileoverview disallow template literals as report messages + * @fileoverview require using placeholders for dynamic report messages * @author Teddy Katz */ @@ -17,7 +17,7 @@ const RuleTester = require('eslint').RuleTester; // ------------------------------------------------------------------------------ const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); -const ERROR = { message: 'Use report message placeholders instead of string concatenation.' }; +const ERROR = { messageId: 'usePlaceholders' }; ruleTester.run('prefer-placeholders', rule, { @@ -60,6 +60,24 @@ ruleTester.run('prefer-placeholders', rule, { } }; `, + // With message in variable. + ` + const MESSAGE = 'foo is bad.'; + module.exports = { + create(context) { + context.report(node, MESSAGE); + } + }; + `, + // With message in variable but cannot statically determine its value. + ` + const MESSAGE = getMessage(); + module.exports = { + create(context) { + context.report(node, MESSAGE); + } + }; + `, ], invalid: [ @@ -76,6 +94,21 @@ ruleTester.run('prefer-placeholders', rule, { `, errors: [ERROR], }, + { + // With message in variable. + code: ` + const MESSAGE = \`\${foo} is bad.\`; + module.exports = { + create(context) { + context.report({ + node, + message: MESSAGE + }); + } + }; + `, + errors: [ERROR], + }, { code: ` module.exports = { diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js index 2c339f16..10777b26 100644 --- a/tests/lib/rules/prefer-replace-text.js +++ b/tests/lib/rules/prefer-replace-text.js @@ -1,5 +1,5 @@ /** - * @fileoverview prefer using replaceText instead of replaceTextRange + * @fileoverview prefer using `replaceText()` instead of `replaceTextRange()` * @author 薛定谔的猫 */ @@ -17,7 +17,7 @@ const RuleTester = require('eslint').RuleTester; // ------------------------------------------------------------------------------ const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); -const ERROR = { message: 'Use replaceText instead of replaceTextRange.' }; +const ERROR = { messageId: 'useReplaceText', type: 'CallExpression' }; ruleTester.run('prefer-placeholders', rule, { diff --git a/tests/lib/rules/report-message-format.js b/tests/lib/rules/report-message-format.js index 03813a0d..d93543d2 100644 --- a/tests/lib/rules/report-message-format.js +++ b/tests/lib/rules/report-message-format.js @@ -33,6 +33,30 @@ ruleTester.run('report-message-format', rule, { `, options: ['foo'], }, + { + // With message as variable. + code: ` + const MESSAGE = 'foo'; + module.exports = { + create(context) { + context.report(node, MESSAGE); + } + }; + `, + options: ['foo'], + }, + { + // With message as variable but cannot statically determine its type. + code: ` + const MESSAGE = getMessage(); + module.exports = { + create(context) { + context.report(node, MESSAGE); + } + }; + `, + options: ['foo'], + }, { code: ` module.exports = { @@ -140,6 +164,18 @@ ruleTester.run('report-message-format', rule, { `, options: ['foo'], }, + { + // With message as variable. + code: ` + const MESSAGE = 'bar'; + module.exports = { + create(context) { + context.report(node, MESSAGE); + } + }; + `, + options: ['foo'], + }, { code: ` module.exports = { diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js index 9a0e1dcd..e8c67109 100644 --- a/tests/lib/rules/require-meta-docs-description.js +++ b/tests/lib/rules/require-meta-docs-description.js @@ -14,6 +14,7 @@ const RuleTester = require('eslint').RuleTester; const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); ruleTester.run('require-meta-docs-description', rule, { valid: [ + 'foo()', ` module.exports = { meta: { docs: { description: 'disallow unused variables' } }, @@ -133,6 +134,26 @@ ruleTester.run('require-meta-docs-description', rule, { output: null, errors: [{ messageId: 'wrongType', type: 'Literal' }], }, + { + code: ` + module.exports = { + meta: { docs: { description: null } }, + create(context) {} + }; + `, + output: null, + errors: [{ messageId: 'wrongType', type: 'Literal' }], + }, + { + code: ` + module.exports = { + meta: { docs: { description: undefined } }, + create(context) {} + }; + `, + output: null, + errors: [{ messageId: 'wrongType', type: 'Identifier' }], + }, { code: ` const DESCRIPTION = true; diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js index 6513286e..b7195aef 100644 --- a/tests/lib/rules/require-meta-docs-url.js +++ b/tests/lib/rules/require-meta-docs-url.js @@ -19,7 +19,7 @@ const rule = require('../../../lib/rules/require-meta-docs-url'); const tester = new RuleTester({ parserOptions: { - ecmaVersion: 2018, + ecmaVersion: 2020, }, }); @@ -66,6 +66,46 @@ tester.run('require-meta-docs-url', rule, { pattern: 'path/to/{{name}}.md', }], }, + { + // `url` in variable. + filename: 'test-rule', + code: ` + const url = "path/to/test-rule.md"; + module.exports = { + meta: {docs: {url}}, + create() {} + } + `, + options: [{ + pattern: 'path/to/{{name}}.md', + }], + }, + { + // Can't determine `url` value statically. + filename: 'test-rule', + code: ` + module.exports = { + meta: {docs: {url: foo }}, + create() {} + } + `, + options: [{ + pattern: 'path/to/{{name}}.md', + }], + }, + { + // Can't determine `url` value statically. + filename: 'test-rule', + code: ` + module.exports = { + meta: {docs: {url: getUrl() }}, + create() {} + } + `, + options: [{ + pattern: 'path/to/{{name}}.md', + }], + }, ], invalid: [ @@ -74,7 +114,7 @@ tester.run('require-meta-docs-url', rule, { module.exports = function() {} `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], }, { code: ` @@ -84,7 +124,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Identifier' }], }, { code: ` @@ -94,7 +134,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Literal' }], }, { code: ` @@ -104,7 +144,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -116,7 +156,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -128,7 +168,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -140,7 +180,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Identifier' }], }, { code: ` @@ -152,7 +192,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -166,7 +206,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -180,7 +220,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -194,7 +234,47 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['`meta.docs.url` property must be a string.'], + errors: [{ messageId: 'wrongType', type: 'Literal' }], + }, + { + // `url` is null + code: ` + module.exports = { + meta: { + docs: { url: null } + }, + create() {} + } + `, + output: null, + errors: [{ messageId: 'wrongType', type: 'Literal' }], + }, + { + // `url` is undefined + code: ` + module.exports = { + meta: { + docs: { url: undefined } + }, + create() {} + } + `, + output: null, + errors: [{ messageId: 'wrongType', type: 'Identifier' }], + }, + { + // `url` in variable. + code: ` + const url = 100; + module.exports = { + meta: { + docs: { url } + }, + create() {} + } + `, + output: null, + errors: [{ messageId: 'wrongType', type: 'Identifier' }], }, { code: ` @@ -208,7 +288,7 @@ tester.run('require-meta-docs-url', rule, { } `, output: null, - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, // ------------------------------------------------------------------------- @@ -222,7 +302,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], }, { code: ` @@ -235,7 +315,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Identifier' }], }, { code: ` @@ -248,7 +328,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Literal' }], }, { code: ` @@ -261,7 +341,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -276,7 +356,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -291,7 +371,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -306,7 +386,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Identifier' }], }, { code: ` @@ -321,7 +401,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -338,7 +418,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -355,7 +435,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -372,7 +452,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['`meta.docs.url` property must be a string.'], + errors: [{ messageId: 'wrongType', type: 'Literal' }], }, { code: ` @@ -389,7 +469,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, // ------------------------------------------------------------------------- @@ -404,7 +484,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], }, { filename: 'test.js', @@ -418,7 +498,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Identifier' }], }, { filename: 'test.js', @@ -432,7 +512,7 @@ tester.run('require-meta-docs-url', rule, { options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Literal' }], }, { filename: 'test.js', @@ -455,7 +535,7 @@ url: "plugin-name/test.md" options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { filename: 'test.js', @@ -481,7 +561,7 @@ url: "plugin-name/test.md" options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { filename: 'test.js', @@ -507,7 +587,7 @@ url: "plugin-name/test.md" options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { filename: 'test.js', @@ -523,7 +603,7 @@ url: "plugin-name/test.md" options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'Identifier' }], }, { filename: 'test.js', @@ -548,7 +628,7 @@ url: "plugin-name/test.md" options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { filename: 'test.js', @@ -576,7 +656,7 @@ url: "plugin-name/test.md" options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { filename: 'test.js', @@ -604,7 +684,7 @@ url: "plugin-name/test.md", options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { filename: 'test.js', @@ -631,7 +711,73 @@ url: "plugin-name/test.md", options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['`meta.docs.url` property must be `plugin-name/test.md`.'], + errors: [{ message: '`meta.docs.url` property must be `plugin-name/test.md`.', type: 'Literal' }], + }, + { + // `url` in variable, can't autofix it. + filename: 'test.js', + code: ` + const url = 'wrong-url'; + module.exports = { + meta: { + docs: { url } + }, + create() {} + } + `, + output: null, + options: [{ + pattern: 'plugin-name/{{ name }}.md', + }], + errors: [{ message: '`meta.docs.url` property must be `plugin-name/test.md`.', type: 'Identifier' }], + }, + { + // `url` is `null`. + filename: 'test.js', + code: ` + module.exports = { + meta: { + docs: { url: null } + }, + create() {} + } + `, + output: ` + module.exports = { + meta: { + docs: { url: "plugin-name/test.md" } + }, + create() {} + } + `, + options: [{ + pattern: 'plugin-name/{{ name }}.md', + }], + errors: [{ message: '`meta.docs.url` property must be `plugin-name/test.md`.', type: 'Literal' }], + }, + { + // `url` is `undefined`. + filename: 'test.js', + code: ` + module.exports = { + meta: { + docs: { url: undefined } + }, + create() {} + } + `, + output: ` + module.exports = { + meta: { + docs: { url: "plugin-name/test.md" } + }, + create() {} + } + `, + options: [{ + pattern: 'plugin-name/{{ name }}.md', + }], + errors: [{ message: '`meta.docs.url` property must be `plugin-name/test.md`.', type: 'Identifier' }], }, { filename: 'test.js', @@ -659,7 +805,7 @@ url: "plugin-name/test.md" options: [{ pattern: 'plugin-name/{{ name }}.md', }], - errors: ['Rules should export a `meta.docs.url` property.'], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, ], }); diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.js index 875e7016..ce4761b9 100644 --- a/tests/lib/rules/require-meta-fixable.js +++ b/tests/lib/rules/require-meta-fixable.js @@ -1,5 +1,5 @@ /** - * @fileoverview require rules to implement a meta.fixable property + * @fileoverview require rules to implement a `meta.fixable` property * @author Teddy Katz */ @@ -12,9 +12,6 @@ const rule = require('../../../lib/rules/require-meta-fixable'); const RuleTester = require('eslint').RuleTester; -const MISSING_ERROR = { message: 'Fixable rules must export a `meta.fixable` property.', type: 'FunctionExpression' }; -const INVALID_ERROR = { message: '`meta.fixable` must be either `code`, `whitespace` or `null`.', type: 'Property' }; - // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ @@ -37,9 +34,11 @@ ruleTester.run('require-meta-fixable', rule, { } }; `, + // Value in variable. ` + const fixable = 'code'; module.exports = { - meta: { fixable: 'whitespace' }, + meta: { fixable }, create(context) { context.report({node, message, fix: foo}); } @@ -47,7 +46,7 @@ ruleTester.run('require-meta-fixable', rule, { `, ` module.exports = { - meta: { 'fixable': 'code' }, + meta: { fixable: 'whitespace' }, create(context) { context.report({node, message, fix: foo}); } @@ -55,7 +54,7 @@ ruleTester.run('require-meta-fixable', rule, { `, ` module.exports = { - meta: { ['fixable']: 'code' }, + meta: { 'fixable': 'code' }, create(context) { context.report({node, message, fix: foo}); } @@ -63,7 +62,7 @@ ruleTester.run('require-meta-fixable', rule, { `, ` module.exports = { - meta: { [\`fixable\`]: 'code' }, + meta: { ['fixable']: 'code' }, create(context) { context.report({node, message, fix: foo}); } @@ -71,9 +70,9 @@ ruleTester.run('require-meta-fixable', rule, { `, ` module.exports = { - meta: { fixable: 'code' }, + meta: { [\`fixable\`]: 'code' }, create(context) { - context.report({node, message}); + context.report({node, message, fix: foo}); } }; `, @@ -93,6 +92,13 @@ ruleTester.run('require-meta-fixable', rule, { } }; `, + // `fixable` uses variable but no static value available. + ` + module.exports = { + meta: { fixable: foo }, + create(context) { context.report({node, message, fix: foo}); } + }; + `, ` module.exports = { meta: {}, @@ -118,6 +124,53 @@ ruleTester.run('require-meta-fixable', rule, { ecmaVersion: 9, }, }, + + // catchNoFixerButFixableProperty = false (implicitly) + ` + module.exports = { + meta: { fixable: 'code' }, + create(context) { context.report({node, message}); } + }; + `, + ` + module.exports = { + meta: { fixable: 'whitespace' }, + create(context) { context.report({node, message}); } + }; + `, + // catchNoFixerButFixableProperty = false (explicitly) + { + code: ` + module.exports = { + meta: { fixable: 'code' }, + create(context) { context.report({node, message}); } + }; + `, + options: [{ catchNoFixerButFixableProperty: false }], + }, + { + code: ` + module.exports = { + meta: { fixable: 'whitespace' }, + create(context) { context.report({node, message}); } + }; + `, + options: [{ catchNoFixerButFixableProperty: false }], + }, + // catchNoFixerButFixableProperty = true + { + code: ` + module.exports = { + meta: { fixable: 'code' }, + create(context) { + foo + ? context.report({ node, message }) + : context.report({ node, message, fix }); + } + }; + `, + options: [{ catchNoFixerButFixableProperty: true }], + }, ], invalid: [ @@ -128,7 +181,15 @@ ruleTester.run('require-meta-fixable', rule, { create(context) { context.report({node, message, fix: foo}); } }; `, - errors: [MISSING_ERROR], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, + { + code: ` + module.exports = { + create(context) { context.report({node, message, fix: foo}); } + }; + `, + errors: [{ messageId: 'missing', type: 'FunctionExpression' }], }, { code: ` @@ -137,7 +198,7 @@ ruleTester.run('require-meta-fixable', rule, { create(context) { context.report(node, loc, message, data, fix); } }; `, - errors: [MISSING_ERROR], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -146,7 +207,7 @@ ruleTester.run('require-meta-fixable', rule, { create(context) { context.report({node, message}); } }; `, - errors: [INVALID_ERROR], + errors: [{ messageId: 'invalid', type: 'Literal' }], }, { code: ` @@ -155,16 +216,67 @@ ruleTester.run('require-meta-fixable', rule, { create(context) { context.report({node, message, fix: foo}); } }; `, - errors: [INVALID_ERROR], + errors: [{ messageId: 'invalid', type: 'Literal' }], + }, + { + code: ` + const fixable = 'invalid'; + module.exports = { + meta: { fixable }, + create(context) { context.report({node, message, fix: foo}); } + }; + `, + errors: [{ messageId: 'invalid', type: 'Identifier' }], + }, + { + code: ` + module.exports = { + meta: { fixable: null }, + create(context) { context.report({node, message, fix: foo}); } + }; + `, + errors: [{ messageId: 'missing', type: 'Literal' }], }, { code: ` module.exports = { - meta: { fixable: foo }, + meta: { fixable: undefined }, create(context) { context.report({node, message, fix: foo}); } }; `, - errors: [INVALID_ERROR], + errors: [{ messageId: 'missing', type: 'Identifier' }], + }, + + // catchNoFixerButFixableProperty = true + { + code: ` + module.exports = { + meta: { fixable: 'code' }, + create(context) { context.report({node, message}); } + }; + `, + options: [{ catchNoFixerButFixableProperty: true }], + errors: [{ messageId: 'noFixerButFixableValue', type: 'Literal' }], + }, + { + code: ` + module.exports = { + meta: { fixable: 'whitespace' }, + create(context) { context.report({node, message}); } + }; + `, + options: [{ catchNoFixerButFixableProperty: true }], + errors: [{ messageId: 'noFixerButFixableValue', type: 'Literal' }], + }, + { + code: ` + module.exports = { + meta: { fixable: null }, + create(context) { context.report({node, message, fix}); } + }; + `, + options: [{ catchNoFixerButFixableProperty: true }], + errors: [{ messageId: 'missing', type: 'Literal' }], }, ], }); diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.js new file mode 100644 index 00000000..0695e343 --- /dev/null +++ b/tests/lib/rules/require-meta-has-suggestions.js @@ -0,0 +1,331 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/require-meta-has-suggestions'); +const RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +ruleTester.run('require-meta-has-suggestions', rule, { + valid: [ + 'module.exports = context => {};', + // No suggestions reported, no violations reported, no meta object. + ` + module.exports = { + create(context) {} + }; + `, + // No suggestions reported, no violations reported, empty meta object. + ` + module.exports = { + meta: {}, + create(context) {} + }; + `, + // No suggestions reported, violation reported, empty meta object. + ` + module.exports = { + meta: {}, + create(context) { + context.report({node, message}); + } + }; + `, + // No suggestions reported, no suggestion property, non-object style of reporting. + ` + module.exports = { + meta: {}, + create(context) { + context.report(node, message); + } + }; + `, + // No suggestions reported (empty suggest array), no suggestion property. + ` + module.exports = { + meta: {}, + create(context) { + context.report({node, message, suggest:[]}); + } + }; + `, + // Suggestions reported (pushing to an array variable), suggestion property. + ` + const suggest = []; + suggest.push({}); + module.exports = { + meta: { hasSuggestions: true }, + create(context) { + context.report({node, message, suggest}); + } + }; + `, + // No suggestions reported, hasSuggestions property set to false. + ` + module.exports = { + meta: { hasSuggestions: false }, + create(context) { + context.report({node, message}); + } + }; + `, + // No suggestions reported, hasSuggestions property set to `null`. + ` + module.exports = { + meta: { hasSuggestions: null }, + create(context) { + context.report({node, message}); + } + }; + `, + // No suggestions reported, hasSuggestions property set to `undefined`. + ` + module.exports = { + meta: { hasSuggestions: undefined }, + create(context) { + context.report({node, message}); + } + }; + `, + // No suggestions reported, hasSuggestions property set to false (as variable). + ` + const hasSuggestions = false; + module.exports = { + meta: { hasSuggestions }, + create(context) { + context.report({node, message}); + } + }; + `, + // Provides suggestions, has hasSuggestions property. + ` + module.exports = { + meta: { hasSuggestions: true }, + create(context) { + context.report({node, message, suggest: [{}]}); + } + }; + `, + // Provides suggestions, has hasSuggestions property (as variable). + ` + const hasSuggestions = true; + module.exports = { + meta: { hasSuggestions }, + create(context) { + context.report({node, message, suggest: [{}]}); + } + }; + `, + // Provides *dynamic* suggestions, has hasSuggestions property. + ` + module.exports = { + meta: { hasSuggestions: true }, + create(context) { + context.report({node, message, suggest: getSuggestions()}); + } + }; + `, + // Provides suggestions, has hasSuggestions property with no static value available. + ` + module.exports = { + meta: { hasSuggestions: getHasSuggestions() }, + create(context) { + context.report({node, message, suggest: [{}]}); + } + }; + `, + // Provides suggestions, has hasSuggestions property in variable with no static value available + ` + const hasSuggestions = getHasSuggestions(); + module.exports = { + meta: { hasSuggestions }, + create(context) { + context.report({node, message, suggest: [{}]}); + } + }; + `, + // Does not provide suggestions, has hasSuggestions property with no static value available + ` + module.exports = { + meta: { hasSuggestions: getHasSuggestions() }, + create(context) { + context.report({node, message}); + } + }; + `, + // Spread syntax. + { + code: ` + const meta = {}; + module.exports = { + ...meta, + meta: {}, + create(context) { context.report(node, message, data, fix); } + }; + `, + parserOptions: { + ecmaVersion: 9, + }, + }, + ], + + invalid: [ + { + // Reports suggestions, no meta object, violation should be on `create` function. + code: ` + module.exports = { + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + output: null, + errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionExpression', line: 3, column: 17, endLine: 3, endColumn: 78 }], + }, + { + // Reports suggestions, no hasSuggestions property, violation should be on `meta` object, empty meta object. + code: ` + module.exports = { + meta: {}, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + output: ` + module.exports = { + meta: { hasSuggestions: true }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 3, column: 17, endLine: 3, endColumn: 19 }], + }, + { + // Reports suggestions, no hasSuggestions property, violation should be on `meta` object, non-empty meta object. + code: ` + module.exports = { + meta: { foo: 'bar' }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + output: ` + module.exports = { + meta: { hasSuggestions: true, foo: 'bar' }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 3, column: 17, endLine: 3, endColumn: 31 }], + }, + { + // Reports suggestions (in variable), no hasSuggestions property, violation should be on `meta` object. + code: ` + const SUGGESTIONS = [{}]; + module.exports = { + meta: {}, + create(context) { context.report({node, message, suggest: SUGGESTIONS}); } + }; + `, + output: ` + const SUGGESTIONS = [{}]; + module.exports = { + meta: { hasSuggestions: true }, + create(context) { context.report({node, message, suggest: SUGGESTIONS}); } + }; + `, + errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 4, column: 17, endLine: 4, endColumn: 19 }], + }, + { + // Reports suggestions (in variable, with pushing), no hasSuggestions property, violation should be on `meta` object. + code: ` + const suggest = []; + suggest.push({}); + module.exports = { + meta: {}, + create(context) { context.report({node, message, suggest}); } + }; + `, + output: ` + const suggest = []; + suggest.push({}); + module.exports = { + meta: { hasSuggestions: true }, + create(context) { context.report({node, message, suggest}); } + }; + `, + errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 5, column: 17, endLine: 5, endColumn: 19 }], + }, + { + // Reports suggestions, hasSuggestions property set to false, violation should be on `false` + code: ` + module.exports = { + meta: { hasSuggestions: false }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + output: ` + module.exports = { + meta: { hasSuggestions: true }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + errors: [{ messageId: 'shouldBeSuggestable', type: 'Literal', line: 3, column: 35, endLine: 3, endColumn: 40 }], + }, + { + // Reports suggestions, hasSuggestions property set to `null`, violation should be on `null` + code: ` + module.exports = { + meta: { hasSuggestions: null }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + output: ` + module.exports = { + meta: { hasSuggestions: true }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + errors: [{ messageId: 'shouldBeSuggestable', type: 'Literal', line: 3, column: 35, endLine: 3, endColumn: 39 }], + }, + { + // Reports suggestions, hasSuggestions property set to `undefined`, violation should be on `undefined` + code: ` + module.exports = { + meta: { hasSuggestions: undefined }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + output: ` + module.exports = { + meta: { hasSuggestions: true }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + errors: [{ messageId: 'shouldBeSuggestable', type: 'Identifier', line: 3, column: 35, endLine: 3, endColumn: 44 }], + }, + { + // Reports suggestions, hasSuggestions property set to false (as variable), violation should be on variable + code: ` + const hasSuggestions = false; + module.exports = { + meta: { hasSuggestions }, + create(context) { context.report({node, message, suggest: [{}]}); } + }; + `, + output: null, + errors: [{ messageId: 'shouldBeSuggestable', type: 'Identifier', line: 4, column: 19, endLine: 4, endColumn: 33 }], + }, + { + // Does not report suggestions, hasSuggestions property set to true, violation should be on `true` + code: ` + module.exports = { + meta: { hasSuggestions: true }, + create(context) { context.report({node, message}); } + }; + `, + output: null, + errors: [{ messageId: 'shouldNotBeSuggestable', type: 'Literal', line: 3, column: 35, endLine: 3, endColumn: 39 }], + }, + ], +}); diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js index 25621162..7450ea75 100644 --- a/tests/lib/rules/require-meta-schema.js +++ b/tests/lib/rules/require-meta-schema.js @@ -32,6 +32,27 @@ ruleTester.run('require-meta-schema', rule, { create(context) {} }; `, + // Schema with options and using `context.options`. + ` + module.exports = { + meta: { schema: { "enum": ["always", "never"] } }, + create(context) { const options = context.options; } + }; + `, + // Empty schema, using arbitrary property of `context`. + ` + module.exports = { + meta: { schema: [] }, + create(context) { const foo = context.foo; } + }; + `, + // Empty schema, using arbitrary `options` property. + ` + module.exports = { + meta: { schema: [] }, + create(context) { const options = foo.options; } + }; + `, ` const schema = []; module.exports = { @@ -62,6 +83,17 @@ ruleTester.run('require-meta-schema', rule, { create }; `, + + { + // requireSchemaPropertyWhenOptionless = false + code: ` + module.exports = { + meta: {}, + create(context) {} + }; + `, + options: [{ requireSchemaPropertyWhenOptionless: false }], + }, ], invalid: [ @@ -72,7 +104,43 @@ ruleTester.run('require-meta-schema', rule, { create(context) {} }; `, - output: ` + output: null, + errors: [ + { + messageId: 'missing', + type: 'ObjectExpression', + suggestions: [ + { + messageId: 'addEmptySchema', + output: ` + module.exports = { + meta: { +schema: [] +}, + create(context) {} + }; + `, + }], + }], + }, + { + // requireSchemaPropertyWhenOptionless = true. + code: ` + module.exports = { + meta: {}, + create(context) {} + }; + `, + output: null, + options: [{ requireSchemaPropertyWhenOptionless: true }], + errors: [ + { + messageId: 'missing', + type: 'ObjectExpression', + suggestions: [ + { + messageId: 'addEmptySchema', + output: ` module.exports = { meta: { schema: [] @@ -80,7 +148,10 @@ schema: [] create(context) {} }; `, - errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, + ], + }, + ], }, { code: ` @@ -89,16 +160,38 @@ schema: [] create(context) {} }; `, - output: ` + output: null, + errors: [ + { + messageId: 'missing', + type: 'ObjectExpression', + suggestions: [ + { + messageId: 'addEmptySchema', + output: ` module.exports = { meta: { type: 'problem', schema: [] }, create(context) {} }; `, - errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, + ], + }, + ], + }, + { + code: ` + module.exports = { + meta: { schema: null }, + create(context) {} + }; + `, + output: null, + errors: [{ messageId: 'wrongType', type: 'Literal', suggestions: [] }], }, { + // requireSchemaPropertyWhenOptionless = false. code: ` module.exports = { meta: { schema: null }, @@ -106,7 +199,18 @@ schema: [] }, }; `, output: null, - errors: [{ messageId: 'wrongType', type: 'Literal' }], + options: [{ requireSchemaPropertyWhenOptionless: false }], + errors: [{ messageId: 'wrongType', type: 'Literal', suggestions: [] }], + }, + { + code: ` + module.exports = { + meta: { schema: undefined }, + create(context) {} + }; + `, + output: null, + errors: [{ messageId: 'wrongType', type: 'Identifier', suggestions: [] }], }, { code: ` @@ -117,7 +221,67 @@ schema: [] }, }; `, output: null, - errors: [{ messageId: 'wrongType', type: 'Literal' }], + errors: [{ messageId: 'wrongType', type: 'Literal', suggestions: [] }], + }, + { + // Empty schema (array), but using rule options. + code: ` + module.exports = { + meta: { schema: [] }, + create(context) { const options = context.options; } + }; + `, + output: null, + errors: [{ messageId: 'foundOptionsUsage', type: 'Property', suggestions: [] }], + }, + { + // Empty schema (object), but using rule options. + code: ` + module.exports = { + meta: { schema: {} }, + create(context) { const options = context.options; } + }; + `, + output: null, + errors: [{ messageId: 'foundOptionsUsage', type: 'Property', suggestions: [] }], + }, + { + // Empty schema (object), but using rule options, requireSchemaPropertyWhenOptionless = false. + code: ` + module.exports = { + meta: { schema: {} }, + create(context) { const options = context.options; } + }; + `, + output: null, + options: [{ requireSchemaPropertyWhenOptionless: false }], + errors: [{ messageId: 'foundOptionsUsage', type: 'Property', suggestions: [] }], + }, + { + // No schema, but using rule options, requireSchemaPropertyWhenOptionless = false. + code: ` + module.exports = { + meta: {}, + create(context) { const options = context.options; } + }; + `, + output: null, + options: [{ requireSchemaPropertyWhenOptionless: false }], + errors: [{ messageId: 'foundOptionsUsage', type: 'ObjectExpression', suggestions: [] }], + }, + { + // No schema, but using rule options, should have no suggestions. + code: ` + module.exports = { + meta: {}, + create(context) { const options = context.options; } + }; + `, + output: null, + errors: [ + { messageId: 'foundOptionsUsage', type: 'ObjectExpression', suggestions: [] }, + { messageId: 'missing', type: 'ObjectExpression', suggestions: [] }, + ], }, ], }); diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.js index 41bd6d9f..25ca0992 100644 --- a/tests/lib/rules/require-meta-type.js +++ b/tests/lib/rules/require-meta-type.js @@ -1,5 +1,5 @@ /** - * @fileoverview require rules to implement a meta.type property + * @fileoverview require rules to implement a `meta.type` property * @author 唯然 */ @@ -37,6 +37,25 @@ ruleTester.run('require-meta-type', rule, { create(context) {} }; `, + ` + const type = 'problem'; + module.exports = { + meta: { type }, + create(context) {} + }; + `, + ` + module.exports = { + meta: { type: getType() }, + create(context) {} + }; + `, + ` + module.exports = { + meta: { type: FOO }, + create(context) {} + }; + `, `module.exports = { create(context) {} }`, @@ -60,7 +79,7 @@ ruleTester.run('require-meta-type', rule, { create(context) {} }; `, - errors: [{ messageId: 'missing' }], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -70,7 +89,7 @@ ruleTester.run('require-meta-type', rule, { create, }; `, - errors: [{ messageId: 'missing' }], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -80,7 +99,7 @@ ruleTester.run('require-meta-type', rule, { create, }; `, - errors: [{ messageId: 'missing' }], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -90,7 +109,7 @@ ruleTester.run('require-meta-type', rule, { create, }; `, - errors: [{ messageId: 'missing' }], + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], }, { code: ` @@ -99,7 +118,35 @@ ruleTester.run('require-meta-type', rule, { create(context) {} }; `, - errors: [{ messageId: 'unexpected' }], + errors: [{ messageId: 'unexpected', type: 'Literal' }], + }, + { + code: ` + const type = 'invalid-type'; + module.exports = { + meta: { type }, + create(context) {} + }; + `, + errors: [{ messageId: 'unexpected', type: 'Identifier' }], + }, + { + code: ` + module.exports = { + meta: { type: null }, + create(context) {} + }; + `, + errors: [{ messageId: 'unexpected', type: 'Literal' }], + }, + { + code: ` + module.exports = { + meta: { type: undefined }, + create(context) {} + }; + `, + errors: [{ messageId: 'unexpected', type: 'Identifier' }], }, ], }); diff --git a/tests/lib/rules/test-case-property-ordering.js b/tests/lib/rules/test-case-property-ordering.js index df29749b..3a5a3d6b 100644 --- a/tests/lib/rules/test-case-property-ordering.js +++ b/tests/lib/rules/test-case-property-ordering.js @@ -19,6 +19,14 @@ const RuleTester = require('eslint').RuleTester; const ruleTester = new RuleTester(); ruleTester.run('test-case-property-ordering', rule, { valid: [ + ` + new RuleTester().run('foo', bar, { + valid: [ + 'foo', + RuleTester.only('foo'), + ] + }); + `, ` new RuleTester().run('foo', bar, { valid: [ diff --git a/tests/lib/rules/test-case-shorthand-strings.js b/tests/lib/rules/test-case-shorthand-strings.js index c15eefba..cf29c26a 100644 --- a/tests/lib/rules/test-case-shorthand-strings.js +++ b/tests/lib/rules/test-case-shorthand-strings.js @@ -28,7 +28,7 @@ function getTestCases (cases) { `; } -const EXPECTED_SHORTHAND_ERROR = { message: 'Use a string for this test case instead of an object.' }; +const EXPECTED_SHORTHAND_ERROR = { message: 'Use a string for this test case instead of an object.', type: 'ObjectExpression' }; const UNEXPECTED_SHORTHAND_ERROR = { message: 'Use an object for this test case instead of a string.' }; // ------------------------------------------------------------------------------ diff --git a/tests/lib/utils.js b/tests/lib/utils.js index 8f5cb5e4..8e28a935 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -1,6 +1,6 @@ 'use strict'; -const util = require('util'); +const { inspect } = require('util'); const lodash = require('lodash'); const espree = require('espree'); const eslintScope = require('eslint-scope'); @@ -118,7 +118,7 @@ describe('utils', () => { const ruleInfo = utils.getRuleInfo({ ast }); assert( lodash.isMatch(ruleInfo, CASES[ruleSource]), - `Expected \n${util.inspect(ruleInfo)}\nto match\n${util.inspect(CASES[ruleSource])}` + `Expected \n${inspect(ruleInfo)}\nto match\n${inspect(CASES[ruleSource])}` ); }); }); @@ -143,7 +143,7 @@ describe('utils', () => { const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( lodash.isMatch(ruleInfo, expected), - `Expected \n${util.inspect(ruleInfo)}\nto match\n${util.inspect(expected)}` + `Expected \n${inspect(ruleInfo)}\nto match\n${inspect(expected)}` ); }); } @@ -178,7 +178,7 @@ describe('utils', () => { const identifiers = utils.getContextIdentifiers(scope, ast); assert(identifiers instanceof Set, 'getContextIdentifiers should return a Set'); - Array.from(identifiers).forEach((identifier, index) => { + [...identifiers].forEach((identifier, index) => { assert.strictEqual(identifier, CASES[ruleSource](ast)[index]); }); }); @@ -352,7 +352,8 @@ describe('utils', () => { `context.report(${args.join(', ')})`, { ecmaVersion: 6, loc: false, range: false } ).body[0].expression.arguments; - const reportInfo = utils.getReportInfo(parsedArgs); + const context = { getScope () {} }; // mock object + const reportInfo = utils.getReportInfo(parsedArgs, context); assert.deepEqual(reportInfo, CASES.get(args)(parsedArgs)); });