From 2b22ac313028d2ed5b175a73b5d32c2fc0eea5fc Mon Sep 17 00:00:00 2001 From: 43081j <43081j@users.noreply.github.com> Date: Sun, 10 Mar 2024 15:41:41 +0000 Subject: [PATCH] feat: add ban-dependencies rule Drops the other rules and merges into one jumbo rule: `ban-dependencies`. This can be configured with `presets`. For example, `presets: ['native']` would ban dependencies which have equivalent native alternatives in your node version. --- README.md | 4 +- docs/rules/avoid-micro-utilities.md | 28 ----- docs/rules/ban-dependencies.md | 76 +++++++++++ docs/rules/prefer-light-dependencies.md | 32 ----- docs/rules/redundant-polyfills.md | 32 ----- src/main.ts | 8 +- src/replacements.ts | 9 +- src/rules/avoid-micro-utilities.ts | 33 ----- src/rules/ban-dependencies.ts | 93 ++++++++++++++ src/rules/prefer-light-dependencies.ts | 33 ----- src/rules/redundant-polyfills.ts | 35 ------ ...ities_test.ts => ban-dependencies_test.ts} | 60 ++++++++- .../rules/prefer-light-dependencies_test.ts | 100 --------------- src/test/rules/redundant-polyfills_test.ts | 119 ------------------ src/util/imports.ts | 8 ++ 15 files changed, 245 insertions(+), 425 deletions(-) delete mode 100644 docs/rules/avoid-micro-utilities.md create mode 100644 docs/rules/ban-dependencies.md delete mode 100644 docs/rules/prefer-light-dependencies.md delete mode 100644 docs/rules/redundant-polyfills.md delete mode 100644 src/rules/avoid-micro-utilities.ts create mode 100644 src/rules/ban-dependencies.ts delete mode 100644 src/rules/prefer-light-dependencies.ts delete mode 100644 src/rules/redundant-polyfills.ts rename src/test/rules/{avoid-micro-utilities_test.ts => ban-dependencies_test.ts} (60%) delete mode 100644 src/test/rules/prefer-light-dependencies_test.ts delete mode 100644 src/test/rules/redundant-polyfills_test.ts diff --git a/README.md b/README.md index bb2a253..0f23201 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,7 @@ export default [ ## Rules -- [`depend/redundant-polyfills`](./docs/rules/redundant-polyfills.md) -- [`depend/avoid-micro-utilities`](./docs/rules/avoid-micro-utilities.md) -- [`depend/prefer-light-dependencies`](./docs/rules/prefer-light-dependencies.md) +- [`depend/ban-dependencies`](./docs/rules/ban-dependencies.md) ## License diff --git a/docs/rules/avoid-micro-utilities.md b/docs/rules/avoid-micro-utilities.md deleted file mode 100644 index b76a06a..0000000 --- a/docs/rules/avoid-micro-utilities.md +++ /dev/null @@ -1,28 +0,0 @@ -# Detects possibly redundant micro-utilities, preferring local/inline functionality instead - -This rule detects imports of "micro utilities" - very small packages which -provide utilities you could replace with native functionality or your own -code. - -## Rule Details - -This rule detects possibly redundant micro-utilities. - -The following patterns are considered warnings: - -```ts -const isNaN = require('is-nan'); -isNaN(v); -``` - -The following patterns are not warnings: - -```ts -Number.isNaN(v); -``` - -## When Not To Use It - -If you prefer the cost of pulling in many utility dependencies over -the cost of writing the equivalent snippet yourself, it may be worth disabling -this rule. diff --git a/docs/rules/ban-dependencies.md b/docs/rules/ban-dependencies.md new file mode 100644 index 0000000..da06b3c --- /dev/null +++ b/docs/rules/ban-dependencies.md @@ -0,0 +1,76 @@ +# Bans a list of dependencies from being used + +This rule bans dependencies based on a preset list or a user defined list. + +## Options + +### `presets` + +You may choose a preset list of dependencies (or none). The following are +available: + +- `microutilities` - micro utilities (e.g. one liners) +- `native` - redundant packages with native equivalents + - Note that this preset will take into account `engines` in your +`package.json` if it is set. In that only native functionality available in your +defined `engines.node` version range will be considered (or all if it isn't +set) +- `preferred` - an opinionated list of packages with better maintained and +lighter alternatives + - Note the list for this is sourced from +[`module-replacements`](https://github.com/es-tooling/module-replacements) + +Example config: + +```json +{ + "rules": { + "depend/ban-dependencies": ["error", { + "presets": ["native"] + } + } +} +``` + +The **default** is `['native', 'microutilities', 'preferred']`. + +### `modules` + +You may also specify your own list of packages which will be disallowed +in code. + +For example: + +```json +{ + "rules": { + "depend/ban-dependencies": ["error", { + "modules": ["im-a-banned-package"] + } + } +} +``` + +## Rule Details + +This rule bans certain dependencies from being used. + +The following patterns are considered warnings: + +```ts +// with `presets: ['native']` +const isNaN = require('is-nan'); +isNaN(v); +``` + +The following patterns are not warnings: + +```ts +// with `presets: ['native']` +Number.isNaN(v); +``` + +## When Not To Use It + +If you prefer not to restrict which dependencies are used, this rule should +be disabled. diff --git a/docs/rules/prefer-light-dependencies.md b/docs/rules/prefer-light-dependencies.md deleted file mode 100644 index e1240e2..0000000 --- a/docs/rules/prefer-light-dependencies.md +++ /dev/null @@ -1,32 +0,0 @@ -# Prefers lighter alternatives over certain dependencies - -This rule prefers lighter alternatives over possibly problematic dependencies. - -For example, dependencies known to be bloated, no longer maintained or -have security issues may be detected by this rule. - -Note this is an _opinionated_ rule, in that the dependencies detected are -driven by those in the -[module-replacements](https://github.com/es-tooling/module-replacements) -project. - -## Rule Details - -This rule detects possibly problematic dependencies. - -The following patterns are considered warnings: - -```ts -const runAll = require('npm-run-all'); -``` - -The following patterns are not warnings: - -```ts -const runAll = require('npm-run-all2'); -``` - -## When Not To Use It - -If you disagree with the opinionated list of dependencies this rule detects, -you should not use this rule. diff --git a/docs/rules/redundant-polyfills.md b/docs/rules/redundant-polyfills.md deleted file mode 100644 index 8f7f721..0000000 --- a/docs/rules/redundant-polyfills.md +++ /dev/null @@ -1,32 +0,0 @@ -# Detects possibly redundant polyfills of natively available functionality - -This rule detects imports of dependencies which provide functionality now -available natively. - -If your package has an `engines` constraint for `node`, it will be taken -into account (i.e. polyfills of functionality your version doesn't yet have -will be allowed). - -## Rule Details - -This rule detects possibly redundant polyfills. - -The following patterns are considered warnings: - -```ts -// With no `engines` or `engines.node` is `>=7.0.0` -const entries = require('object.entries'); -entries({foo: 'bar'}); -``` - -The following patterns are not warnings: - -```ts -// With no `engines` or `engines.node` is `>=7.0.0` -Object.entries({foo: 'bar'}); -``` - -## When Not To Use It - -If you need to support much older JS runtimes (node, browsers, etc), this -rule may not be of much use as the polyfills are probably still useful there. diff --git a/src/main.ts b/src/main.ts index 06867db..2aa2ac1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,10 @@ import {recommended} from './configs/recommended.js'; -import {rule as redundantPolyfills} from './rules/redundant-polyfills.js'; -import {rule as avoidMicroUtils} from './rules/avoid-micro-utilities.js'; -import {rule as preferLightDependencies} from './rules/prefer-light-dependencies.js'; +import {rule as banDependencies} from './rules/ban-dependencies.js'; export const configs = { recommended }; export const rules = { - 'redundant-polyfills': redundantPolyfills, - 'avoid-micro-utilities': avoidMicroUtils, - 'prefer-light-dependencies': preferLightDependencies + 'ban-dependencies': banDependencies }; diff --git a/src/replacements.ts b/src/replacements.ts index 197b398..1c4db17 100644 --- a/src/replacements.ts +++ b/src/replacements.ts @@ -20,12 +20,17 @@ export interface DocumentedReplacement extends ReplacementLike { moduleName: string; } +export interface NoReplacement extends ReplacementLike { + type: 'none'; +} + export type Replacement = | NativeReplacement | DocumentedReplacement - | SimpleReplacement; + | SimpleReplacement + | NoReplacement; -export const lighterReplacements: Replacement[] = [ +export const preferredReplacements: Replacement[] = [ { type: 'documented', moduleName: 'npm-run-all', diff --git a/src/rules/avoid-micro-utilities.ts b/src/rules/avoid-micro-utilities.ts deleted file mode 100644 index 988d3d1..0000000 --- a/src/rules/avoid-micro-utilities.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {Rule} from 'eslint'; -import {getDocsUrl} from '../util/rule-meta.js'; -import {microUtilities} from '../replacements.js'; -import {createReplacementListener} from '../util/imports.js'; - -export const rule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: - 'Detects possibly redundant micro-utilities, preferring' + - 'local/inline functionality instead.', - url: getDocsUrl('avoid-micro-utilities') - }, - schema: [], - messages: { - nativeReplacement: - '"{{name}}" is a micro-utility which should be replaced with ' + - '{{replacement}} (native functionality), Read more here: {{url}}', - documentedReplacement: - '"{{name}}" is a micro-utility which should be replaced with a ' + - 'lighter alternative. Read more here: {{url}}', - simpleReplacement: - '"{{name}}" is a micro-utility which should be replaced with ' + - 'equivalent inline/local logic. {{replacement}}' - } - }, - create: (context) => { - return { - ...createReplacementListener(context, microUtilities) - }; - } -}; diff --git a/src/rules/ban-dependencies.ts b/src/rules/ban-dependencies.ts new file mode 100644 index 0000000..6a9fcbf --- /dev/null +++ b/src/rules/ban-dependencies.ts @@ -0,0 +1,93 @@ +import {Rule} from 'eslint'; +import {getDocsUrl} from '../util/rule-meta.js'; +import { + microUtilities, + preferredReplacements, + nativeReplacements, + Replacement +} from '../replacements.js'; +import {createReplacementListener} from '../util/imports.js'; + +interface BanDependenciesOptions { + presets?: string[]; + modules?: string[]; +} + +const availablePresets: Record = { + microutilities: microUtilities, + native: nativeReplacements, + preferred: preferredReplacements +}; + +const defaultPresets = ['microutilities', 'native', 'preferred']; + +export const rule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'Bans a list of dependencies from being used', + url: getDocsUrl('ban-dependencies') + }, + schema: [ + { + type: 'object', + properties: { + presets: { + type: 'array', + items: { + type: 'string' + } + }, + modules: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + } + ], + messages: { + nativeReplacement: + '"{{name}}" should be replaced with native functionality. ' + + 'You can instead use {{replacement}}. Read more here: {{url}}', + documentedReplacement: + '"{{name}}" should be replaced with an alternative package. ' + + 'Read more here: {{url}}', + simpleReplacement: + '"{{name}}" should be replaced with inline/local logic.' + + '{{replacement}}', + noneReplacement: + '"{{name}}" is a banned dependency. An alternative should be used.' + } + }, + create: (context) => { + const options = context.options[0] as BanDependenciesOptions | undefined; + const replacements: Replacement[] = []; + const presets = options?.presets ?? defaultPresets; + const modules = options?.modules; + + for (const preset of presets) { + const presetReplacements = availablePresets[preset]; + if (presetReplacements) { + for (const rep of presetReplacements) { + replacements.push(rep); + } + } + } + + if (modules) { + for (const mod of modules) { + replacements.push({ + type: 'none', + moduleName: mod + }); + } + } + + return { + ...createReplacementListener(context, replacements) + }; + } +}; diff --git a/src/rules/prefer-light-dependencies.ts b/src/rules/prefer-light-dependencies.ts deleted file mode 100644 index 9e11973..0000000 --- a/src/rules/prefer-light-dependencies.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {Rule} from 'eslint'; -import {getDocsUrl} from '../util/rule-meta.js'; -import {lighterReplacements} from '../replacements.js'; -import {createReplacementListener} from '../util/imports.js'; - -export const rule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: 'Prefers lighter alternatives over certain dependencies', - url: getDocsUrl('prefer-light-dependencies') - }, - schema: [], - messages: { - simpleReplacement: - '"{{name}}" is redundant within your supported versions of node.' + - 'It should be replaced by the natively available ' + - '"{{replacement}}"', - nativeReplacement: - '"{{name}}" is redundant within your supported versions of node.' + - 'It should be replaced by the natively available ' + - '"{{replacement}}" ({{url}})', - documentedReplacement: - '"{{name}}" should be replaced with a lighter alternative.' + - 'For possible replacements, see {{url}}' - } - }, - create: (context) => { - return { - ...createReplacementListener(context, lighterReplacements) - }; - } -}; diff --git a/src/rules/redundant-polyfills.ts b/src/rules/redundant-polyfills.ts deleted file mode 100644 index 50c495f..0000000 --- a/src/rules/redundant-polyfills.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {Rule} from 'eslint'; -import {getDocsUrl} from '../util/rule-meta.js'; -import {nativeReplacements} from '../replacements.js'; -import {createReplacementListener} from '../util/imports.js'; - -export const rule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: - 'Detects possibly redundant polyfills of natively available ' + - 'functionality', - url: getDocsUrl('redundant-polyfills') - }, - schema: [], - messages: { - simpleReplacement: - '"{{name}}" is redundant within your supported versions of node.' + - 'It should be replaced by the natively available ' + - '"{{replacement}}"', - nativeReplacement: - '"{{name}}" is redundant within your supported versions of node.' + - 'It should be replaced by the natively available ' + - '"{{replacement}}" ({{url}})', - documentedReplacement: - '"{{name}}" is redundant within your supported versions of node.' + - 'For possible replacements, see {{url}}' - } - }, - create: (context) => { - return { - ...createReplacementListener(context, nativeReplacements) - }; - } -}; diff --git a/src/test/rules/avoid-micro-utilities_test.ts b/src/test/rules/ban-dependencies_test.ts similarity index 60% rename from src/test/rules/avoid-micro-utilities_test.ts rename to src/test/rules/ban-dependencies_test.ts index 9b7a95e..94d130a 100644 --- a/src/test/rules/avoid-micro-utilities_test.ts +++ b/src/test/rules/ban-dependencies_test.ts @@ -1,5 +1,6 @@ -import {rule} from '../../rules/avoid-micro-utilities.js'; +import {rule} from '../../rules/ban-dependencies.js'; import {RuleTester} from 'eslint'; +import {getMdnUrl, getReplacementsDocUrl} from '../../util/rule-meta.js'; const ruleTester = new RuleTester({ parserOptions: { @@ -10,7 +11,7 @@ const ruleTester = new RuleTester({ const tseslintParser = require.resolve('@typescript-eslint/parser'); -ruleTester.run('avoid-micro-utilities', rule, { +ruleTester.run('ban-dependencies', rule, { valid: [ 'const foo = 303;', { @@ -34,6 +35,14 @@ ruleTester.run('avoid-micro-utilities', rule, { const moduleName = 'is-' + 'number'; await import(moduleName); ` + }, + { + code: `const foo = require('is-number');`, + options: [ + { + presets: [] + } + ] } ], @@ -94,6 +103,53 @@ ruleTester.run('avoid-micro-utilities', rule, { } } ] + }, + { + code: `import foo from 'object.entries';`, + errors: [ + { + line: 1, + column: 1, + messageId: 'nativeReplacement', + data: { + name: 'object.entries', + replacement: 'Object.entries', + url: getMdnUrl('Global_Objects/Object/entries') + } + } + ] + }, + { + code: `import foo from 'npm-run-all';`, + errors: [ + { + line: 1, + column: 1, + messageId: 'documentedReplacement', + data: { + name: 'npm-run-all', + url: getReplacementsDocUrl('npm-run-all') + } + } + ] + }, + { + code: `import foo from 'oogabooga';`, + options: [ + { + modules: ['oogabooga'] + } + ], + errors: [ + { + line: 1, + column: 1, + messageId: 'noneReplacement', + data: { + name: 'oogabooga' + } + } + ] } ] }); diff --git a/src/test/rules/prefer-light-dependencies_test.ts b/src/test/rules/prefer-light-dependencies_test.ts deleted file mode 100644 index f22a536..0000000 --- a/src/test/rules/prefer-light-dependencies_test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import {rule} from '../../rules/prefer-light-dependencies.js'; -import {RuleTester} from 'eslint'; -import {getReplacementsDocUrl} from '../../util/rule-meta.js'; - -const ruleTester = new RuleTester({ - parserOptions: { - sourceType: 'module', - ecmaVersion: 2022 - } -}); - -const tseslintParser = require.resolve('@typescript-eslint/parser'); - -ruleTester.run('prefer-light-dependencies', rule, { - valid: [ - 'const foo = 303;', - { - code: `import foo = require('unknown-module');`, - parser: tseslintParser - }, - { - code: `import foo from 'unknown-module';` - }, - { - code: `const foo = require('unknown-module');` - }, - { - code: ` - const moduleName = 'npm-run-' + 'all'; - require(moduleName); - ` - }, - { - code: ` - const moduleName = 'npm-run-' + 'all'; - await import(moduleName); - ` - } - ], - - invalid: [ - { - code: `const foo = require('npm-run-all');`, - errors: [ - { - line: 1, - column: 13, - messageId: 'documentedReplacement', - data: { - name: 'npm-run-all', - url: getReplacementsDocUrl('npm-run-all') - } - } - ] - }, - { - code: `import foo from 'npm-run-all';`, - errors: [ - { - line: 1, - column: 1, - messageId: 'documentedReplacement', - data: { - name: 'npm-run-all', - url: getReplacementsDocUrl('npm-run-all') - } - } - ] - }, - { - code: `const foo = await import('npm-run-all');`, - errors: [ - { - line: 1, - column: 19, - messageId: 'documentedReplacement', - data: { - name: 'npm-run-all', - url: getReplacementsDocUrl('npm-run-all') - } - } - ] - }, - { - code: `import foo = require('npm-run-all');`, - parser: tseslintParser, - errors: [ - { - line: 1, - column: 1, - messageId: 'documentedReplacement', - data: { - name: 'npm-run-all', - url: getReplacementsDocUrl('npm-run-all') - } - } - ] - } - ] -}); diff --git a/src/test/rules/redundant-polyfills_test.ts b/src/test/rules/redundant-polyfills_test.ts deleted file mode 100644 index 3ca3fd8..0000000 --- a/src/test/rules/redundant-polyfills_test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import {rule} from '../../rules/redundant-polyfills.js'; -import {RuleTester} from 'eslint'; -import {getMdnUrl} from '../../util/rule-meta.js'; - -const ruleTester = new RuleTester({ - parserOptions: { - sourceType: 'module', - ecmaVersion: 2022 - } -}); - -const tseslintParser = require.resolve('@typescript-eslint/parser'); - -ruleTester.run('redundant-polyfills', rule, { - valid: [ - 'const foo = 303;', - { - code: `import foo = require('unknown-module');`, - parser: tseslintParser - }, - { - code: `import foo from 'unknown-module';` - }, - { - code: `const foo = require('unknown-module');` - }, - { - code: ` - const moduleName = 'object' + '.entries'; - require(moduleName); - ` - }, - { - code: ` - const moduleName = 'object' + '.entries'; - await import(moduleName); - ` - } - ], - - invalid: [ - { - code: `const entries = require('object.entries');`, - errors: [ - { - line: 1, - column: 17, - messageId: 'nativeReplacement', - data: { - name: 'object.entries', - replacement: 'Object.entries', - url: getMdnUrl('Global_Objects/Object/entries') - } - } - ] - }, - { - code: `const entries = require('object.entries/whatever');`, - errors: [ - { - line: 1, - column: 17, - messageId: 'nativeReplacement', - data: { - name: 'object.entries', - replacement: 'Object.entries', - url: getMdnUrl('Global_Objects/Object/entries') - } - } - ] - }, - { - code: `import entries from 'object.entries';`, - errors: [ - { - line: 1, - column: 1, - messageId: 'nativeReplacement', - data: { - name: 'object.entries', - replacement: 'Object.entries', - url: getMdnUrl('Global_Objects/Object/entries') - } - } - ] - }, - { - code: `const entries = await import('object.entries');`, - errors: [ - { - line: 1, - column: 23, - messageId: 'nativeReplacement', - data: { - name: 'object.entries', - replacement: 'Object.entries', - url: getMdnUrl('Global_Objects/Object/entries') - } - } - ] - }, - { - code: `import entries = require('object.entries');`, - parser: tseslintParser, - errors: [ - { - line: 1, - column: 1, - messageId: 'nativeReplacement', - data: { - name: 'object.entries', - replacement: 'Object.entries', - url: getMdnUrl('Global_Objects/Object/entries') - } - } - ] - } - ] -}); diff --git a/src/util/imports.ts b/src/util/imports.ts index 54a43ad..4120fc1 100644 --- a/src/util/imports.ts +++ b/src/util/imports.ts @@ -128,6 +128,14 @@ function replacementListenerCallback( replacement: replacement.replacement } }); + } else if (replacement.type === 'none') { + context.report({ + node, + messageId: 'noneReplacement', + data: { + name: replacement.moduleName + } + }); } }