From 3d41bb5779b4e05acc18d5ce508a47869ac62452 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 8 Aug 2024 09:14:01 -0700 Subject: [PATCH] feat(eslint): making a couple of our internal custom rules open source (#871) * feat: pulling across our `no-decorators-on-private-properties` rule * feat: pulling across `prefer-unicode-ellipsis` * feat: new `prefer-typescript` eslint rule * fix: bringing back the bespoke plugin configs * feat: introducing automatic doc generation * fix: resolving some prettier issues * chore: changing test case filenames * Update packages/eslint-plugin/.eslint-doc-generatorrc.js Co-authored-by: Kanad Gupta <8854718+kanadgupta@users.noreply.github.com> * fix: updating the docs --------- Co-authored-by: Kanad Gupta <8854718+kanadgupta@users.noreply.github.com> --- .vscode/settings.json | 7 + package-lock.json | 277 +++++++++++++++++- packages/eslint-config/react.js | 8 +- packages/eslint-config/typescript.js | 9 +- .../eslint-plugin/.eslint-doc-generatorrc.js | 15 + .../eslint-plugin/{LICENSE.md => LICENSE} | 2 +- packages/eslint-plugin/README.md | 32 +- .../no-decorators-on-private-properties.md | 73 +++++ .../docs/{ => rules}/no-dual-exports.md | 6 +- .../docs/rules/prefer-typescript.md | 5 + .../docs/rules/prefer-unicode-ellipsis.md | 35 +++ packages/eslint-plugin/index.js | 15 + packages/eslint-plugin/lib/utils.js | 2 +- packages/eslint-plugin/package.json | 5 +- .../no-decorators-on-private-properties.js | 59 ++++ .../eslint-plugin/rules/no-dual-exports.js | 4 +- .../eslint-plugin/rules/prefer-typescript.js | 26 ++ .../rules/prefer-unicode-ellipsis.js | 42 +++ ...o-decorators-on-private-properties.test.js | 88 ++++++ .../test/prefer-typescript.test.js | 62 ++++ .../test/prefer-unicode-ellipsis.test.js | 57 ++++ .../{vitest.config.ts => vitest.config.mts} | 0 22 files changed, 802 insertions(+), 27 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 packages/eslint-plugin/.eslint-doc-generatorrc.js rename packages/eslint-plugin/{LICENSE.md => LICENSE} (96%) create mode 100644 packages/eslint-plugin/docs/rules/no-decorators-on-private-properties.md rename packages/eslint-plugin/docs/{ => rules}/no-dual-exports.md (87%) create mode 100644 packages/eslint-plugin/docs/rules/prefer-typescript.md create mode 100644 packages/eslint-plugin/docs/rules/prefer-unicode-ellipsis.md create mode 100644 packages/eslint-plugin/rules/no-decorators-on-private-properties.js create mode 100644 packages/eslint-plugin/rules/prefer-typescript.js create mode 100644 packages/eslint-plugin/rules/prefer-unicode-ellipsis.js create mode 100644 packages/eslint-plugin/test/no-decorators-on-private-properties.test.js create mode 100644 packages/eslint-plugin/test/prefer-typescript.test.js create mode 100644 packages/eslint-plugin/test/prefer-unicode-ellipsis.test.js rename packages/eslint-plugin/{vitest.config.ts => vitest.config.mts} (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..98e77de7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "editor.formatOnSave": true +} diff --git a/package-lock.json b/package-lock.json index b45f005b..1f93e7c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5556,6 +5556,12 @@ "readable-stream": "^3.4.0" } }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true + }, "node_modules/boxen": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", @@ -6955,6 +6961,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "peer": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -7617,6 +7632,105 @@ "pnpm": ">=8.6.0" } }, + "node_modules/eslint-doc-generator": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/eslint-doc-generator/-/eslint-doc-generator-1.7.1.tgz", + "integrity": "sha512-i1Zjl+Xcy712SZhbceCeMVaIdhbFqY27i8d7f9gyb9P/6AQNnPA0VCWynAFVGYa0hpeR5kwUI09+GBELgC2nnA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.38.1", + "ajv": "^8.11.2", + "boolean": "^3.2.0", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "deepmerge": "^4.2.2", + "dot-prop": "^7.2.0", + "jest-diff": "^29.2.1", + "json-schema-traverse": "^1.0.0", + "markdown-table": "^3.0.3", + "no-case": "^3.0.4", + "type-fest": "^3.0.0" + }, + "bin": { + "eslint-doc-generator": "dist/bin/eslint-doc-generator.js" + }, + "engines": { + "node": "^14.18.0 || ^16.0.0 || >=18.0.0" + }, + "peerDependencies": { + "eslint": ">= 7" + } + }, + "node_modules/eslint-doc-generator/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-doc-generator/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/eslint-doc-generator/node_modules/dot-prop": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", + "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", + "dev": true, + "dependencies": { + "type-fest": "^2.11.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-doc-generator/node_modules/dot-prop/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-doc-generator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/eslint-doc-generator/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -8836,8 +8950,7 @@ "node_modules/fast-uri": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", - "peer": true + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -11734,6 +11847,21 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lower-case/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "node_modules/lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -13491,6 +13619,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/no-case/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -19719,6 +19863,8 @@ "devDependencies": { "@babel/eslint-parser": "^7.24.8", "@readme/eslint-config": "file:../eslint-config", + "@typescript-eslint/parser": "^8.0.1", + "eslint-doc-generator": "^1.7.1", "vitest": "^2.0.3" }, "engines": { @@ -19728,6 +19874,133 @@ "eslint": "^8.0.0" } }, + "packages/eslint-plugin/node_modules/@typescript-eslint/parser": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", + "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.0.1", + "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/typescript-estree": "8.0.1", + "@typescript-eslint/visitor-keys": "8.0.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", + "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/visitor-keys": "8.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", + "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", + "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/visitor-keys": "8.0.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", + "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/eslint-plugin/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/eslint-plugin/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/spectral-config": { "name": "@readme/spectral-config", "version": "5.0.6", diff --git a/packages/eslint-config/react.js b/packages/eslint-config/react.js index 9de288cc..7fd51e22 100644 --- a/packages/eslint-config/react.js +++ b/packages/eslint-config/react.js @@ -1,6 +1,12 @@ /** @type {import("eslint-define-config").ESLintConfig} */ const config = { - extends: ['plugin:jsx-a11y/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended'], + extends: [ + 'plugin:jsx-a11y/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:readme/react', + ], + plugins: ['readme'], env: { browser: true, }, diff --git a/packages/eslint-config/typescript.js b/packages/eslint-config/typescript.js index 57a9d371..3d9689ef 100644 --- a/packages/eslint-config/typescript.js +++ b/packages/eslint-config/typescript.js @@ -1,8 +1,13 @@ /** @type {import("eslint-define-config").ESLintConfig} */ const config = { - parser: '@typescript-eslint/parser', + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:typescript-sort-keys/recommended', + 'plugin:readme/typescript', + ], plugins: ['@typescript-eslint'], - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:typescript-sort-keys/recommended'], + parser: '@typescript-eslint/parser', settings: { 'import/resolver': 'typescript', }, diff --git a/packages/eslint-plugin/.eslint-doc-generatorrc.js b/packages/eslint-plugin/.eslint-doc-generatorrc.js new file mode 100644 index 00000000..0e7165c5 --- /dev/null +++ b/packages/eslint-plugin/.eslint-doc-generatorrc.js @@ -0,0 +1,15 @@ +/** @type {import('eslint-doc-generator').GenerateOptions} */ +const config = { + configEmoji: [ + ['esm', '📁'], + ['typescript', '🧠'], + ['react', '⚛️'], + ], + urlRuleDoc(name, page) { + if (page === 'README.md') { + return `https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/rules/${name}.md`; + } + }, +}; + +module.exports = config; diff --git a/packages/eslint-plugin/LICENSE.md b/packages/eslint-plugin/LICENSE similarity index 96% rename from packages/eslint-plugin/LICENSE.md rename to packages/eslint-plugin/LICENSE index 9cc818cf..4c6cc02d 100644 --- a/packages/eslint-plugin/LICENSE.md +++ b/packages/eslint-plugin/LICENSE @@ -1,4 +1,4 @@ -Copyright 2023 ReadMe +Copyright 2024 ReadMe Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index eab3df7e..1b886ece 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -1,6 +1,6 @@ # eslint-plugin-readme -Custom ESLint plugin for some ReadMe engineering guidelines and gotchas. +An ESLint plugin providing custom rules for ReadMe's coding standards. [![](https://raw.githubusercontent.com/readmeio/.github/main/oss-header.png)](https://readme.io) @@ -15,22 +15,22 @@ extends: ['plugin:readme/'], plugins: ['readme'], ``` -## 🔖 Available Configs - - - -| Config | Description | -| :--- | :--- | -| `esm` | Rules specific to ESM libraries. | - - - ## 📖 Rules - + + +💼 Configurations enabled in.\ +⚠️ Configurations set to warn in.\ +📁 Set in the `esm` configuration.\ +⚛️ Set in the `react` configuration.\ +🧠 Set in the `typescript` configuration.\ +🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| Rule | Description | Config | -| :--- | :--- | :--- | -| [no-dual-exports](https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/no-dual-exports.md) | Prevent cases of having a file with dual `default` and named exports. | `esm` | +| Name                                | Description | 💼 | ⚠️ | 🔧 | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------- | :-- | :-- | :-- | +| [no-decorators-on-private-properties](https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/rules/no-decorators-on-private-properties.md) | Prevent the use of decorators on private properties as they cannot be introspected. | 🧠 | | | +| [no-dual-exports](https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/rules/no-dual-exports.md) | Prevent cases of having a file with dual `default` and named exports. | 📁 | | | +| [prefer-typescript](https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/rules/prefer-typescript.md) | Prefer using TypeScript within a codebase. | | | | +| [prefer-unicode-ellipsis](https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/rules/prefer-unicode-ellipsis.md) | Prefer using a unicode ellipsis (`…`) instead of three periods (`...`). | | ⚛️ | 🔧 | - + diff --git a/packages/eslint-plugin/docs/rules/no-decorators-on-private-properties.md b/packages/eslint-plugin/docs/rules/no-decorators-on-private-properties.md new file mode 100644 index 00000000..599ab058 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-decorators-on-private-properties.md @@ -0,0 +1,73 @@ +# Prevent the use of decorators on private properties as they cannot be introspected (`readme/no-decorators-on-private-properties`) + +💼 This rule is enabled in the 🧠 `typescript` config. + + + +This rule aims to prevent you from introducing edge cases when using [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) on [private properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties) as they're fully private and can't be introspected upon -- which is a general use-case for decorators. + +## Fail + +```ts +class ExampleClass { + @exampleDecorator() + #methodName() { + return true; + } +} +``` + +```ts +class ExampleClass { + @exampleDecorator() + #buster = true; + + methodName() { + return true; + } +} +``` + +```ts +@exampleDecorator() +class ExampleClass { + #buster = true; + + methodName() { + return true; + } +} +``` + +## Pass + +```ts +class ExampleClass { + @exampleDecorator() + methodName() { + return true; + } +} +``` + +```ts +class ExampleClass { + @exampleDecorator() + buster = true; + + #methodName() { + return true; + } +} +``` + +```ts +@exampleDecorator() +class ExampleClass { + buster = true; + + methodName() { + return true; + } +} +``` diff --git a/packages/eslint-plugin/docs/no-dual-exports.md b/packages/eslint-plugin/docs/rules/no-dual-exports.md similarity index 87% rename from packages/eslint-plugin/docs/no-dual-exports.md rename to packages/eslint-plugin/docs/rules/no-dual-exports.md index 44086eb7..2377ccb3 100644 --- a/packages/eslint-plugin/docs/no-dual-exports.md +++ b/packages/eslint-plugin/docs/rules/no-dual-exports.md @@ -1,4 +1,8 @@ -# Prevent cases of having a file with dual `default` and named exports +# Prevent cases of having a file with dual `default` and named exports (`readme/no-dual-exports`) + +💼 This rule is enabled in the 📁 `esm` config. + + In libraries that support CJS and ESM environments, having a file that has a `default` **and** a named export makes that file incompatible with CJS environments due to the way that CJS resolution works in that case. To resolve this, a file should either have a single `default` export, or it should only be comprised of named exports. You cannot have both. diff --git a/packages/eslint-plugin/docs/rules/prefer-typescript.md b/packages/eslint-plugin/docs/rules/prefer-typescript.md new file mode 100644 index 00000000..5b5c4174 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-typescript.md @@ -0,0 +1,5 @@ +# Prefer using TypeScript within a codebase (`readme/prefer-typescript`) + + + +This rule allows you to optionally enforce that all code is written in TypeScript; it does this by ensuring that files have a TypeScript file extension. It unfortunately does not check if the file _actually_ contains TS. diff --git a/packages/eslint-plugin/docs/rules/prefer-unicode-ellipsis.md b/packages/eslint-plugin/docs/rules/prefer-unicode-ellipsis.md new file mode 100644 index 00000000..01cd18de --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-unicode-ellipsis.md @@ -0,0 +1,35 @@ +# Prefer using a unicode ellipsis (`…`) instead of three periods (`...`) (`readme/prefer-unicode-ellipsis`) + +⚠️ This rule _warns_ in the ⚛️ `react` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +We've standardized on representing an ellipsis in our frontend as a Unicode ellipsis (`…`) instead of three periods (`...`). + +## Fail + +```js + + +  Saving... + +``` + +```js +console.log('buster...'); +``` + +## Pass + +```js + + +  Saving… + +``` + +```js +console.log('buster…'); +``` diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js index b18fccc7..d91c34d9 100644 --- a/packages/eslint-plugin/index.js +++ b/packages/eslint-plugin/index.js @@ -13,8 +13,23 @@ module.exports = { 'readme/no-dual-exports': 'error', }, }, + react: { + plugins: ['readme'], + rules: { + 'readme/prefer-unicode-ellipsis': 'warn', + }, + }, + typescript: { + plugins: ['readme'], + rules: { + 'readme/no-decorators-on-private-properties': 'error', + }, + }, }, rules: { + 'no-decorators-on-private-properties': require('./rules/no-decorators-on-private-properties'), 'no-dual-exports': require('./rules/no-dual-exports'), + 'prefer-typescript': require('./rules/prefer-typescript'), + 'prefer-unicode-ellipsis': require('./rules/prefer-unicode-ellipsis'), }, }; diff --git a/packages/eslint-plugin/lib/utils.js b/packages/eslint-plugin/lib/utils.js index f12fa3d4..75472f7a 100644 --- a/packages/eslint-plugin/lib/utils.js +++ b/packages/eslint-plugin/lib/utils.js @@ -1,5 +1,5 @@ module.exports = { getDocURL: rule => { - return `https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/${rule}.md`; + return `https://github.com/readmeio/standards/tree/main/packages/eslint-plugin/docs/rules/${rule}.md`; }, }; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 678f3055..9bddeae1 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -22,7 +22,8 @@ }, "scripts": { "lint": "eslint .", - "test": "vitest run" + "test": "vitest run", + "update:docs": "eslint-doc-generator && npx prettier --write docs/rules/*.md *.md" }, "peerDependencies": { "eslint": "^8.0.0" @@ -30,6 +31,8 @@ "devDependencies": { "@babel/eslint-parser": "^7.24.8", "@readme/eslint-config": "file:../eslint-config", + "@typescript-eslint/parser": "^8.0.1", + "eslint-doc-generator": "^1.7.1", "vitest": "^2.0.3" } } diff --git a/packages/eslint-plugin/rules/no-decorators-on-private-properties.js b/packages/eslint-plugin/rules/no-decorators-on-private-properties.js new file mode 100644 index 00000000..8b3f912e --- /dev/null +++ b/packages/eslint-plugin/rules/no-decorators-on-private-properties.js @@ -0,0 +1,59 @@ +const { getDocURL } = require('../lib/utils'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Prevent the use of decorators on private properties as they cannot be introspected.', + url: getDocURL(__filename), + }, + }, + create: context => { + return { + /** + * Checking for class-level decorators that exist on a class that contains private properties + * + * @example + * class ExampleClass { + * @exampleDecorator() + * #methodName() { + * return true; + * } + * } + */ + 'ClassDeclaration[decorators.length>0] PrivateIdentifier': node => { + const decorators = node.parent.parent.parent.decorators; + + decorators.forEach(dnode => { + const decorator = dnode.expression.callee.name; + + context.report({ + node: dnode.expression.callee, + message: `You should not use decorators on ES6 classes that contain private properties as they cannot be introspected. If \`${decorator}\` does not do any method or property-level introspection then you can safely ignore this rule.`, + }); + }); + }, + + /** + * Checking for decorators that are attached to a private property. + * + * @example + * class ExampleClass { + * @exampleDecorator() + * #methodName() { + * return true; + * } + * } + */ + 'Decorator[parent.key.type=PrivateIdentifier]': node => { + const decorator = node.expression.callee.name; + + context.report({ + node, + message: `You should not use decorators on ES6 class private properties as they cannot be introspected. If \`${decorator}\` does not do any method or property-level introspection then you can safely ignore this rule.`, + }); + }, + }; + }, +}; diff --git a/packages/eslint-plugin/rules/no-dual-exports.js b/packages/eslint-plugin/rules/no-dual-exports.js index 0b7998dc..9d0515ca 100644 --- a/packages/eslint-plugin/rules/no-dual-exports.js +++ b/packages/eslint-plugin/rules/no-dual-exports.js @@ -1,12 +1,12 @@ const { getDocURL } = require('../lib/utils'); +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'problem', - category: 'ESM', docs: { description: 'Prevent cases of having a file with dual `default` and named exports.', - url: getDocURL('no-dual-exports'), + url: getDocURL(__filename), }, }, create: context => { diff --git a/packages/eslint-plugin/rules/prefer-typescript.js b/packages/eslint-plugin/rules/prefer-typescript.js new file mode 100644 index 00000000..9eaa78ed --- /dev/null +++ b/packages/eslint-plugin/rules/prefer-typescript.js @@ -0,0 +1,26 @@ +const path = require('node:path'); + +const { getDocURL } = require('../lib/utils'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Prefer using TypeScript within a codebase.', + url: getDocURL(__filename), + }, + }, + create: context => { + return { + Program(node) { + const filename = context.physicalFilename; + const extension = path.extname(filename); + + if (extension.match(/\.(c|m)?jsx?$/)) { + context.report(node, 'TypeScript is preferred within this codebase.'); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/rules/prefer-unicode-ellipsis.js b/packages/eslint-plugin/rules/prefer-unicode-ellipsis.js new file mode 100644 index 00000000..265a6849 --- /dev/null +++ b/packages/eslint-plugin/rules/prefer-unicode-ellipsis.js @@ -0,0 +1,42 @@ +const { getDocURL } = require('../lib/utils'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Prefer using a unicode ellipsis (`…`) instead of three periods (`...`).', + url: getDocURL(__filename), + }, + fixable: 'code', + }, + create: context => { + return { + /** + * @example console.log('buster...'); + */ + 'Literal[value=/^(.*)[.]{3}$/im]': node => { + context.report({ + node, + message: 'Ellipsis should be written as `…`.', + fix(fixer) { + return fixer.replaceText(node, node.raw.replace('...', '…')); + }, + }); + }, + + /** + * @example  Saving... + */ + 'JSXText[value=/^(.*)[.]{3}$/im]': node => { + context.report({ + node, + message: 'Ellipsis should be written as `…`.', + fix(fixer) { + return fixer.replaceText(node, node.raw.replace('...', '…')); + }, + }); + }, + }; + }, +}; diff --git a/packages/eslint-plugin/test/no-decorators-on-private-properties.test.js b/packages/eslint-plugin/test/no-decorators-on-private-properties.test.js new file mode 100644 index 00000000..bb970ddd --- /dev/null +++ b/packages/eslint-plugin/test/no-decorators-on-private-properties.test.js @@ -0,0 +1,88 @@ +const { RuleTester } = require('eslint'); + +const { rules } = require('..'); + +const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 2022, + requireConfigFile: false, + }, +}); + +const propertyErrorMessage = + 'You should not use decorators on ES6 class private properties as they cannot be introspected. If `exampleDecorator` does not do any method or property-level introspection then you can safely ignore this rule.'; + +const classErrorMessage = + 'You should not use decorators on ES6 classes that contain private properties as they cannot be introspected. If `exampleDecorator` does not do any method or property-level introspection then you can safely ignore this rule.'; + +ruleTester.run('no-decorators-on-private-properties', rules['no-decorators-on-private-properties'], { + valid: [ + { + code: ` +class ExampleClass { + #methodName() { + return true; + } +}`, + }, + { + code: ` +class ExampleClass { + @exampleDecorator() + buster = true; + + #methodName() { + return true; + } +}`, + }, + { + code: ` +@exampleDecorator() +class ExampleClass { + buster = true; + + methodName() { + return true; + } +}`, + }, + ], + invalid: [ + { + code: ` +class ExampleClass { + @exampleDecorator() + #methodName() { + return true; + } +}`, + errors: [{ message: propertyErrorMessage }], + }, + { + code: ` +class ExampleClass { + @exampleDecorator() + #buster = true; + + methodName() { + return true; + } +}`, + errors: [{ message: propertyErrorMessage }], + }, + { + code: ` +@exampleDecorator() +class ExampleClass { + #buster = true; + + methodName() { + return true; + } +}`, + errors: [{ message: classErrorMessage }], + }, + ], +}); diff --git a/packages/eslint-plugin/test/prefer-typescript.test.js b/packages/eslint-plugin/test/prefer-typescript.test.js new file mode 100644 index 00000000..5040491d --- /dev/null +++ b/packages/eslint-plugin/test/prefer-typescript.test.js @@ -0,0 +1,62 @@ +const { RuleTester } = require('eslint'); + +const { rules } = require('..'); + +const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 2022, + requireConfigFile: false, + }, +}); + +const errorMessage = 'TypeScript is preferred within this codebase.'; + +ruleTester.run('prefer-typescript', rules['prefer-typescript'], { + valid: [ + { filename: 'file.ts', code: '/** this is a TS file */' }, + { filename: 'file.tsx', code: '/** this is a TSX file */' }, + { filename: 'file.cts', code: '/** this is a CTS file */' }, + { filename: 'file.ctsx', code: '/** this is a CTSX file */' }, + { filename: 'file.mts', code: '/** this is a MTS file */' }, + { filename: 'file.mtsx', code: '/** this is a MTSX file */' }, + ], + invalid: [ + { + filename: 'file.js', + code: '/** this is a JS file */', + errors: [{ message: errorMessage }], + output: '/** this is a JS file */', + }, + { + filename: 'file.jsx', + code: '/** this is a JSX file */', + errors: [{ message: errorMessage }], + output: '/** this is a JSX file */', + }, + { + filename: 'file.cjs', + code: '/** this is a CJS file */', + errors: [{ message: errorMessage }], + output: '/** this is a CJS file */', + }, + { + filename: 'file.cjsx', + code: '/** this is a CJSX file */', + errors: [{ message: errorMessage }], + output: '/** this is a CJSX file */', + }, + { + filename: 'file.mjs', + code: '/** this is a MJS file */', + errors: [{ message: errorMessage }], + output: '/** this is a MJS file */', + }, + { + filename: 'file.mjsx', + code: '/** this is a MJSX file */', + errors: [{ message: errorMessage }], + output: '/** this is a MJSX file */', + }, + ], +}); diff --git a/packages/eslint-plugin/test/prefer-unicode-ellipsis.test.js b/packages/eslint-plugin/test/prefer-unicode-ellipsis.test.js new file mode 100644 index 00000000..2eabe54f --- /dev/null +++ b/packages/eslint-plugin/test/prefer-unicode-ellipsis.test.js @@ -0,0 +1,57 @@ +const { RuleTester } = require('eslint'); + +const { rules } = require('..'); + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run('prefer-unicode-ellipsis', rules['prefer-unicode-ellipsis'], { + valid: [ + { + code: 'console.log("buster…");', + }, + { + code: ' Saving…', + }, + { + code: ` + +  Saving… + `, + }, + ], + invalid: [ + { + code: 'console.log("buster...");', + errors: [ + { + message: 'Ellipsis should be written as `…`.', + }, + ], + output: 'console.log("buster…");', + }, + { + code: ' Saving...', + errors: [ + { + message: 'Ellipsis should be written as `…`.', + }, + ], + output: ' Saving…', + }, + { + code: ' Saving...', + errors: [ + { + message: 'Ellipsis should be written as `…`.', + }, + ], + output: ' Saving…', + }, + ], +}); diff --git a/packages/eslint-plugin/vitest.config.ts b/packages/eslint-plugin/vitest.config.mts similarity index 100% rename from packages/eslint-plugin/vitest.config.ts rename to packages/eslint-plugin/vitest.config.mts