diff --git a/docs/maintenance/Issues.mdx b/docs/maintenance/Issues.mdx index 525147f88c54..8b39f5e5506d 100644 --- a/docs/maintenance/Issues.mdx +++ b/docs/maintenance/Issues.mdx @@ -163,7 +163,7 @@ For enhancements meant to limit which kinds of nodes the rule targets, mark the #### 🚀 New Rules We're generally accepting of new rules that meet the above feature request criteria. -The biggest exception is rules that can roughly be implemented with [`@typescript-eslint/ban-types`](/rules/ban-types) and/or [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax). +The biggest exception is rules that can roughly be implemented with [`@typescript-eslint/no-restricted-types`](/rules/no-restricted-types) and/or [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax). ## Pruning Old Issues diff --git a/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md b/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md index b462a35c0511..b0fc34b3fc5f 100644 --- a/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md +++ b/packages/eslint-plugin/TSLINT_RULE_ALTERNATIVES.md @@ -15,32 +15,32 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th ### TypeScript-specific -| TSLint rule | | ESLint rule | -| --------------------------------- | :-: | ---------------------------------------------------- | -| [`adjacent-overload-signatures`] | ✅ | [`@typescript-eslint/adjacent-overload-signatures`] | -| [`ban-ts-ignore`] | ✅ | [`@typescript-eslint/ban-ts-comment`] | -| [`ban-types`] | 🌓 | [`@typescript-eslint/ban-types`][1] | -| [`invalid-void`] | ✅ | [`@typescript-eslint/no-invalid-void-type`] | -| [`member-access`] | ✅ | [`@typescript-eslint/explicit-member-accessibility`] | -| [`member-ordering`] | ✅ | [`@typescript-eslint/member-ordering`] | -| [`no-any`] | ✅ | [`@typescript-eslint/no-explicit-any`] | -| [`no-empty-interface`] | ✅ | [`@typescript-eslint/no-empty-object-type`] | -| [`no-import-side-effect`] | 🔌 | [`import/no-unassigned-import`] | -| [`no-inferrable-types`] | ✅ | [`@typescript-eslint/no-inferrable-types`] | -| [`no-internal-module`] | ✅ | [`@typescript-eslint/prefer-namespace-keyword`] | -| [`no-magic-numbers`] | ✅ | [`@typescript-eslint/no-magic-numbers`] | -| [`no-namespace`] | ✅ | [`@typescript-eslint/no-namespace`] | -| [`no-non-null-assertion`] | ✅ | [`@typescript-eslint/no-non-null-assertion`] | -| [`no-parameter-reassignment`] | ✅ | [`no-param-reassign`][no-param-reassign] | -| [`no-reference`] | ✅ | [`@typescript-eslint/triple-slash-reference`] | -| [`no-unnecessary-type-assertion`] | ✅ | [`@typescript-eslint/no-unnecessary-type-assertion`] | -| [`no-var-requires`] | ✅ | [`@typescript-eslint/no-var-requires`] | -| [`only-arrow-functions`] | 🔌 | [`prefer-arrow/prefer-arrow-functions`] | -| [`prefer-for-of`] | ✅ | [`@typescript-eslint/prefer-for-of`] | -| [`promise-function-async`] | ✅ | [`@typescript-eslint/promise-function-async`] | -| [`typedef-whitespace`] | ✅ | [`@typescript-eslint/type-annotation-spacing`] | -| [`typedef`] | ✅ | [`@typescript-eslint/typedef`] | -| [`unified-signatures`] | ✅ | [`@typescript-eslint/unified-signatures`] | +| TSLint rule | | ESLint rule | +| --------------------------------- | :-: | -------------------------------------------------------- | +| [`adjacent-overload-signatures`] | ✅ | [`@typescript-eslint/adjacent-overload-signatures`] | +| [`ban-ts-ignore`] | ✅ | [`@typescript-eslint/ban-ts-comment`] | +| [`ban-types`] | 🌓 | [`@typescript-eslint/no-restricted-types`][1] | +| [`invalid-void`] | ✅ | [`@typescript-eslint/no-invalid-void-type`] | +| [`member-access`] | ✅ | [`@typescript-eslint/explicit-member-accessibility`] | +| [`member-ordering`] | ✅ | [`@typescript-eslint/member-ordering`] | +| [`no-any`] | ✅ | [`@typescript-eslint/no-explicit-any`] | +| [`no-empty-interface`] | ✅ | [`@typescript-eslint/no-empty-object-type`] | +| [`no-import-side-effect`] | 🔌 | [`import/no-unassigned-import`] | +| [`no-inferrable-types`] | ✅ | [`@typescript-eslint/no-inferrable-types`] | +| [`no-internal-module`] | ✅ | [`@typescript-eslint/prefer-namespace-keyword`] | +| [`no-magic-numbers`] | ✅ | [`@typescript-eslint/no-magic-numbers`] | +| [`no-namespace`] | ✅ | [`@typescript-eslint/no-namespace`] | +| [`no-non-null-assertion`] | ✅ | [`@typescript-eslint/no-non-null-assertion`] | +| [`no-parameter-reassignment`] | ✅ | [`no-param-reassign`][no-param-reassign] | +| [`no-reference`] | ✅ | [`@typescript-eslint/triple-slash-reference`] | +| [`no-unnecessary-type-assertion`] | ✅ | [`@typescript-eslint/no-unnecessary-type-assertion`] | +| [`no-var-requires`] | ✅ | [`@typescript-eslint/no-var-requires`] | +| [`only-arrow-functions`] | 🔌 | [`prefer-arrow/prefer-arrow-functions`] | +| [`prefer-for-of`] | ✅ | [`@typescript-eslint/prefer-for-of`] | +| [`promise-function-async`] | ✅ | [`@typescript-eslint/promise-function-async`] | +| [`typedef-whitespace`] | ✅ | [`@typescript-eslint/type-annotation-spacing`] | +| [`typedef`] | ✅ | [`@typescript-eslint/typedef`] | +| [`unified-signatures`] | ✅ | [`@typescript-eslint/unified-signatures`] | [1] The ESLint rule only supports exact string matching, rather than regular expressions
@@ -595,7 +595,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/adjacent-overload-signatures`]: https://typescript-eslint.io/rules/adjacent-overload-signatures [`@typescript-eslint/await-thenable`]: https://typescript-eslint.io/rules/await-thenable -[`@typescript-eslint/ban-types`]: https://typescript-eslint.io/rules/ban-types +[`@typescript-eslint/no-restricted-types`]: https://typescript-eslint.io/rules/no-restricted-types [`@typescript-eslint/ban-ts-comment`]: https://typescript-eslint.io/rules/ban-ts-comment [`@typescript-eslint/class-methods-use-this`]: https://typescript-eslint.io/rules/class-methods-use-this [`@typescript-eslint/consistent-type-assertions`]: https://typescript-eslint.io/rules/consistent-type-assertions diff --git a/packages/eslint-plugin/docs/rules/ban-types.md b/packages/eslint-plugin/docs/rules/ban-types.md new file mode 100644 index 000000000000..f2aa717a07b2 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/ban-types.md @@ -0,0 +1,22 @@ +:::danger Deprecated + +The old `ban-types` rule encompassed multiple areas of functionality, and so has been split into several rules. + +**[`no-restricted-types`](./no-restricted-types.mdx)** is the new rule for banning a configurable list of type names. +It has no options enabled by default and is akin to rules like [`no-restricted-globals`](https://eslint.org/docs/latest/rules/no-restricted-globals), [`no-restricted-properties`](https://eslint.org/docs/latest/rules/no-restricted-properties), and [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax). + +The default options from `ban-types` are now covered by: + +- **[`no-empty-object-type`](./no-empty-object-type.mdx)**: banning the built-in `{}` type in confusing locations +- **[`no-unsafe-function-type`](./no-unsafe-function-type.mdx)**: banning the built-in `Function` +- **[`no-wrapper-object-types`](./no-wrapper-object-types.mdx)**: banning `Object` and built-in class wrappers such as `Number` + +`ban-types` itself is removed in typescript-eslint v8. +See [Announcing typescript-eslint v8 Beta](/blog/announcing-typescript-eslint-v8-beta) for more details. +::: + + diff --git a/packages/eslint-plugin/docs/rules/ban-types.mdx b/packages/eslint-plugin/docs/rules/ban-types.mdx deleted file mode 100644 index b52ae22df7d3..000000000000 --- a/packages/eslint-plugin/docs/rules/ban-types.mdx +++ /dev/null @@ -1,133 +0,0 @@ ---- -description: 'Disallow certain types.' ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -> 🛑 This file is source code, not the primary documentation location! 🛑 -> -> See **https://typescript-eslint.io/rules/ban-types** for documentation. - -Some built-in types have aliases, while some types are considered dangerous or harmful. -It's often a good idea to ban certain types to help with consistency and safety. - -This rule bans specific types and can suggest alternatives. -Note that it does not ban the corresponding runtime objects from being used. - -## Examples - -Examples of code with the default options: - - - - -```ts -// use lower-case primitives for consistency -const str: String = 'foo'; -const bool: Boolean = true; -const num: Number = 1; -const symb: Symbol = Symbol('foo'); -const bigInt: BigInt = 1n; - -// use a proper function type -const func: Function = () => 1; - -// use safer object types -const lowerObj: Object = {}; -const capitalObj: Object = { a: 'string' }; -``` - - - - -```ts -// use lower-case primitives for consistency -const str: string = 'foo'; -const bool: boolean = true; -const num: number = 1; -const symb: symbol = Symbol('foo'); -const bigInt: bigint = 1n; - -// use a proper function type -const func: () => number = () => 1; - -// use safer object types -const lowerObj: object = {}; -const capitalObj: { a: string } = { a: 'string' }; -``` - - - - -## Options - -The default options provide a set of "best practices", intended to provide safety and standardization in your codebase: - -- Don't use the upper-case primitive types or `Object`, you should use the lower-case types for consistency. -- Avoid the `Function` type, as it provides little safety for the following reasons: - - It provides no type safety when calling the value, which means it's easy to provide the wrong arguments. - - It accepts class declarations, which will fail when called, as they are called without the `new` keyword. - -
-Default Options - -{/* Inject default options */} - -
- -### `types` - -An object whose keys are the types you want to ban, and the values are error messages. - -The type can either be a type name literal (`Foo`), a type name with generic parameter instantiation(s) (`Foo`), the empty object literal (`{}`), or the empty tuple type (`[]`). - -The values can be: - -- A string, which is the error message to be reported; or -- `false` to specifically un-ban this type (useful when you are using `extendDefaults`); or -- An object with the following properties: - - `message: string` - the message to display when the type is matched. - - `fixWith?: string` - a string to replace the banned type with when the fixer is run. If this is omitted, no fix will be done. - - `suggest?: string[]` - a list of suggested replacements for the banned type. - -### `extendDefaults` - -If you're specifying custom `types`, you can set this to `true` to extend the default `types` configuration. This is a convenience option to save you copying across the defaults when adding another type. - -If this is `false`, the rule will _only_ use the types defined in your configuration. - -Example configuration: - -```jsonc -{ - "@typescript-eslint/ban-types": [ - "error", - { - "types": { - // add a custom message to help explain why not to use it - "Foo": "Don't use Foo because it is unsafe", - - // add a custom message, AND tell the plugin how to fix it - "OldAPI": { - "message": "Use NewAPI instead", - "fixWith": "NewAPI", - }, - - // un-ban a type that's banned by default - "{}": false, - }, - "extendDefaults": true, - }, - ], -} -``` - -## When Not To Use It - -If your project is a rare one that intentionally deals with the class equivalents of primitives, it might not be worthwhile to enable the default `ban-types` options. -You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule. - -## Related To - -- [`no-empty-object-type`](./no-empty-object-type.mdx) diff --git a/packages/eslint-plugin/docs/rules/no-restricted-types.mdx b/packages/eslint-plugin/docs/rules/no-restricted-types.mdx new file mode 100644 index 000000000000..20732174929e --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-restricted-types.mdx @@ -0,0 +1,71 @@ +--- +description: 'Disallow certain types.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-restricted-types** for documentation. + +It can sometimes be useful to ban specific types from being used in type annotations. +For example, a project might be migrating from using one type to another, and want to ban references to the old type. + +This rule can be configured to ban a list of specific types and can suggest alternatives. +Note that it does not ban the corresponding runtime objects from being used. + +## Options + +### `types` + +An object whose keys are the types you want to ban, and the values are error messages. + +The type can either be a type name literal (`OldType`) or a a type name with generic parameter instantiation(s) (`OldType`). + +The values can be: + +- A string, which is the error message to be reported; or +- `false` to specifically un-ban this type (useful when you are using `extendDefaults`); or +- An object with the following properties: + - `message: string`: the message to display when the type is matched. + - `fixWith?: string`: a string to replace the banned type with when the fixer is run. If this is omitted, no fix will be done. + - `suggest?: string[]`: a list of suggested replacements for the banned type. + +Example configuration: + +```jsonc +{ + "@typescript-eslint/no-restricted-types": [ + "error", + { + "types": { + // add a custom message to help explain why not to use it + "OldType": "Don't use OldType because it is unsafe", + + // add a custom message, and tell the plugin how to fix it + "OldAPI": { + "message": "Use NewAPI instead", + "fixWith": "NewAPI", + }, + + // add a custom message, and tell the plugin how to suggest a fix + "SoonToBeOldAPI": { + "message": "Use NewAPI instead", + "suggest": ["NewAPIOne", "NewAPITwo"], + }, + }, + }, + ], +} +``` + +## When Not To Use It + +If you have no need to ban specific types from being used in type annotations, you don't need this rule. + +## Related To + +- [`no-empty-object-type`](./no-empty-object-type.mdx) +- [`no-unsafe-function-type`](./no-unsafe-function-type.mdx) +- [`no-wrapper-object-types`](./no-wrapper-object-types.mdx) diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx b/packages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx new file mode 100644 index 000000000000..ea7b60794e4e --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-function-type.mdx @@ -0,0 +1,63 @@ +--- +description: 'Disallow using the unsafe built-in Function type.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-unsafe-function-type** for documentation. + +TypeScript's built-in `Function` type allows being called with any number of arguments and returns type `any`. +`Function` also allows classes or plain objects that happen to possess all properties of the `Function` class. +It's generally better to specify function parameters and return types with the function type syntax. + +"Catch-all" function types include: + +- `() => void`: a function that has no parameters and whose return is ignored +- `(...args: never) => unknown`: a "top type" for functions that can be assigned any function type, but can't be called + +Examples of code for this rule: + + + + +```ts +let noParametersOrReturn: Function; +noParametersOrReturn = () => {}; + +let stringToNumber: Function; +stringToNumber = (text: string) => text.length; + +let identity: Function; +identity = value => value; +``` + + + + +```ts +let noParametersOrReturn: () => void; +noParametersOrReturn = () => {}; + +let stringToNumber: (text: string) => number; +stringToNumber = text => text.length; + +let identity: (value: T) => T; +identity = value => value; +``` + + + + +## When Not To Use It + +If your project is still onboarding to TypeScript, it might be difficult to fully replace all unsafe `Function` types with more precise function types. +You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule. + +## Related To + +- [`no-empty-object-type`](./no-empty-object-type.mdx) +- [`no-restricted-types`](./no-restricted-types.mdx) +- [`no-wrapper-object-types`](./no-wrapper-object-types.mdx) diff --git a/packages/eslint-plugin/docs/rules/no-wrapper-object-types.mdx b/packages/eslint-plugin/docs/rules/no-wrapper-object-types.mdx new file mode 100644 index 000000000000..858802470cbb --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-wrapper-object-types.mdx @@ -0,0 +1,67 @@ +--- +description: 'Disallow using confusing built-in primitive class wrappers.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-wrapper-object-types** for documentation. + +The JavaScript language has a set of language types, but some of them correspond to two TypeScript types, which look similar: `boolean`/`Boolean`, `number`/`Number`, `string`/`String`, `bigint`/`BigInt`, `symbol`/`Symbol`, `object`/`Object`. +The difference is that the lowercase variants are compiler intrinsics and specify the actual _runtime types_ (that is, the return value when you use the `typeof` operator), while the uppercase variants are _structural types_ defined in the library that can be satisfied by any user-defined object with the right properties, not just the real primitives. +JavaScript also has a "wrapper" class object for each of those primitives, such as [`Boolean`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean) and [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number). +These wrapper objects are assignable to the uppercase types, but not to the lowercase types. + +Using the primitives like `0` instead of object wrappers like `new Number(0)` is generally considered a JavaScript best practice. +JavaScript programs typically work with the real number primitives, rather than objects that "look like" numbers. +Primitives are simpler to conceptualize and work with `==` and `===` equality checks -- which their object equivalents do notDeepEqual. +As a result, using the lowercase type names like `number` instead of the uppercase names like `Number` helps make your code behave more reliably. + +Examples of code for this rule: + + + + +```ts +let myBigInt: BigInt; +let myBoolean: Boolean; +let myNumber: Number; +let myString: String; +let mySymbol: Symbol; + +let myObject: Object = 'allowed by TypeScript'; +``` + + + + +```ts +let myBigint: bigint; +let myBoolean: boolean; +let myNumber: number; +let myString: string; +let mySymbol: symbol; + +let myObject: object = "Type 'string' is not assignable to type 'object'."; +``` + + + + +## When Not To Use It + +If your project is a rare one that intentionally deals with the class equivalents of primitives, it might not be worthwhile to use this rule. +You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule. + +## Further Reading + +- [MDN documentation on primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive) +- [MDN documentation on `string` primitives and `String` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_primitives_and_string_objects) + +## Related To + +- [`no-empty-object-type`](./no-empty-object-type.mdx) +- [`no-restricted-types`](./no-restricted-types.mdx) +- [`no-unsafe-function-type`](./no-unsafe-function-type.mdx) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 5c1e1d7725ac..ab24d6943de0 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -15,7 +15,6 @@ export = { '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/ban-ts-comment': 'error', '@typescript-eslint/ban-tslint-comment': 'error', - '@typescript-eslint/ban-types': 'error', '@typescript-eslint/class-literal-property-style': 'error', 'class-methods-use-this': 'off', '@typescript-eslint/class-methods-use-this': 'error', @@ -85,6 +84,7 @@ export = { '@typescript-eslint/no-require-imports': 'error', 'no-restricted-imports': 'off', '@typescript-eslint/no-restricted-imports': 'error', + '@typescript-eslint/no-restricted-types': 'error', 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-this-alias': 'error', @@ -100,6 +100,7 @@ export = { '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', @@ -112,6 +113,7 @@ export = { 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-useless-empty-export': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', diff --git a/packages/eslint-plugin/src/configs/recommended-type-checked.ts b/packages/eslint-plugin/src/configs/recommended-type-checked.ts index d0975fddfe8b..e8e10a7071a5 100644 --- a/packages/eslint-plugin/src/configs/recommended-type-checked.ts +++ b/packages/eslint-plugin/src/configs/recommended-type-checked.ts @@ -12,7 +12,6 @@ export = { rules: { '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/ban-ts-comment': 'error', - '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-array-delete': 'error', @@ -40,6 +39,7 @@ export = { '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', @@ -47,6 +47,7 @@ export = { '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', '@typescript-eslint/prefer-as-const': 'error', diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts index 6657edaaf5b9..487935ee1f23 100644 --- a/packages/eslint-plugin/src/configs/recommended.ts +++ b/packages/eslint-plugin/src/configs/recommended.ts @@ -11,7 +11,6 @@ export = { extends: ['./configs/base', './configs/eslint-recommended'], rules: { '@typescript-eslint/ban-ts-comment': 'error', - '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', @@ -25,10 +24,12 @@ export = { '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-unnecessary-type-constraint': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-namespace-keyword': 'error', '@typescript-eslint/triple-slash-reference': 'error', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index 7987868204db..11d65130de62 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -15,7 +15,6 @@ export = { 'error', { minimumDescriptionLength: 10 }, ], - '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-array-delete': 'error', @@ -55,6 +54,7 @@ export = { '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', @@ -64,6 +64,7 @@ export = { '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', '@typescript-eslint/prefer-as-const': 'error', diff --git a/packages/eslint-plugin/src/configs/strict.ts b/packages/eslint-plugin/src/configs/strict.ts index fc7cb1febebc..0e655d1464ca 100644 --- a/packages/eslint-plugin/src/configs/strict.ts +++ b/packages/eslint-plugin/src/configs/strict.ts @@ -14,7 +14,6 @@ export = { 'error', { minimumDescriptionLength: 10 }, ], - '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', @@ -33,12 +32,14 @@ export = { '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-unnecessary-type-constraint': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-literal-enum-member': 'error', '@typescript-eslint/prefer-namespace-keyword': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index c167013211c0..d11366a54341 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -5,7 +5,6 @@ import arrayType from './array-type'; import awaitThenable from './await-thenable'; import banTsComment from './ban-ts-comment'; import banTslintComment from './ban-tslint-comment'; -import banTypes from './ban-types'; import classLiteralPropertyStyle from './class-literal-property-style'; import classMethodsUseThis from './class-methods-use-this'; import consistentGenericConstructors from './consistent-generic-constructors'; @@ -62,6 +61,7 @@ import noRedeclare from './no-redeclare'; import noRedundantTypeConstituents from './no-redundant-type-constituents'; import noRequireImports from './no-require-imports'; import noRestrictedImports from './no-restricted-imports'; +import noRestrictedTypes from './no-restricted-types'; import noShadow from './no-shadow'; import noThisAlias from './no-this-alias'; import noTypeAlias from './no-type-alias'; @@ -77,6 +77,7 @@ import noUnsafeAssignment from './no-unsafe-assignment'; import noUnsafeCall from './no-unsafe-call'; import noUnsafeDeclarationMerging from './no-unsafe-declaration-merging'; import noUnsafeEnumComparison from './no-unsafe-enum-comparison'; +import noUnsafeFunctionType from './no-unsafe-function-type'; import noUnsafeMemberAccess from './no-unsafe-member-access'; import noUnsafeReturn from './no-unsafe-return'; import noUnsafeUnaryMinus from './no-unsafe-unary-minus'; @@ -86,6 +87,7 @@ import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noUselessEmptyExport from './no-useless-empty-export'; import noVarRequires from './no-var-requires'; +import noWrapperObjectTypes from './no-wrapper-object-types'; import nonNullableTypeAssertionStyle from './non-nullable-type-assertion-style'; import onlyThrowError from './only-throw-error'; import parameterProperties from './parameter-properties'; @@ -129,7 +131,6 @@ export default { 'await-thenable': awaitThenable, 'ban-ts-comment': banTsComment, 'ban-tslint-comment': banTslintComment, - 'ban-types': banTypes, 'class-literal-property-style': classLiteralPropertyStyle, 'class-methods-use-this': classMethodsUseThis, 'consistent-generic-constructors': consistentGenericConstructors, @@ -186,6 +187,7 @@ export default { 'no-redundant-type-constituents': noRedundantTypeConstituents, 'no-require-imports': noRequireImports, 'no-restricted-imports': noRestrictedImports, + 'no-restricted-types': noRestrictedTypes, 'no-shadow': noShadow, 'no-this-alias': noThisAlias, 'no-type-alias': noTypeAlias, @@ -201,6 +203,7 @@ export default { 'no-unsafe-call': noUnsafeCall, 'no-unsafe-declaration-merging': noUnsafeDeclarationMerging, 'no-unsafe-enum-comparison': noUnsafeEnumComparison, + 'no-unsafe-function-type': noUnsafeFunctionType, 'no-unsafe-member-access': noUnsafeMemberAccess, 'no-unsafe-return': noUnsafeReturn, 'no-unsafe-unary-minus': noUnsafeUnaryMinus, @@ -210,6 +213,7 @@ export default { 'no-useless-constructor': noUselessConstructor, 'no-useless-empty-export': noUselessEmptyExport, 'no-var-requires': noVarRequires, + 'no-wrapper-object-types': noWrapperObjectTypes, 'non-nullable-type-assertion-style': nonNullableTypeAssertionStyle, 'only-throw-error': onlyThrowError, 'parameter-properties': parameterProperties, diff --git a/packages/eslint-plugin/src/rules/no-implied-eval.ts b/packages/eslint-plugin/src/rules/no-implied-eval.ts index dee23d62d275..b2bfac55203a 100644 --- a/packages/eslint-plugin/src/rules/no-implied-eval.ts +++ b/packages/eslint-plugin/src/rules/no-implied-eval.ts @@ -3,7 +3,11 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; -import { createRule, getParserServices } from '../util'; +import { + createRule, + getParserServices, + isReferenceToGlobalFunction, +} from '../util'; const FUNCTION_CONSTRUCTOR = 'Function'; const GLOBAL_CANDIDATES = new Set(['global', 'window', 'globalThis']); @@ -119,18 +123,6 @@ export default createRule({ } } - function isReferenceToGlobalFunction( - calleeName: string, - node: TSESTree.Node, - ): boolean { - const ref = context.sourceCode - .getScope(node) - .references.find(ref => ref.identifier.name === calleeName); - - // ensure it's the "global" version - return !ref?.resolved || ref.resolved.defs.length === 0; - } - function checkImpliedEval( node: TSESTree.CallExpression | TSESTree.NewExpression, ): void { @@ -165,7 +157,7 @@ export default createRule({ if ( EVAL_LIKE_METHODS.has(calleeName) && !isFunction(handler) && - isReferenceToGlobalFunction(calleeName, node) + isReferenceToGlobalFunction(calleeName, node, context.sourceCode) ) { context.report({ node: handler, messageId: 'noImpliedEvalError' }); } diff --git a/packages/eslint-plugin/src/rules/ban-types.ts b/packages/eslint-plugin/src/rules/no-restricted-types.ts similarity index 74% rename from packages/eslint-plugin/src/rules/ban-types.ts rename to packages/eslint-plugin/src/rules/no-restricted-types.ts index 06c9daa62d4a..f3d2e3b3f197 100644 --- a/packages/eslint-plugin/src/rules/ban-types.ts +++ b/packages/eslint-plugin/src/rules/no-restricted-types.ts @@ -21,6 +21,7 @@ export type Options = [ extendDefaults?: boolean; }, ]; + export type MessageIds = 'bannedTypeMessage' | 'bannedTypeReplacement'; function removeSpaces(str: string): string { @@ -37,7 +38,7 @@ function stringifyNode( function getCustomMessage( bannedType: string | true | { message?: string; fixWith?: string } | null, ): string { - if (bannedType == null || bannedType === true) { + if (!bannedType || bannedType === true) { return ''; } @@ -52,42 +53,7 @@ function getCustomMessage( return ''; } -const defaultTypes: Types = { - String: { - message: 'Use string instead', - fixWith: 'string', - }, - Boolean: { - message: 'Use boolean instead', - fixWith: 'boolean', - }, - Number: { - message: 'Use number instead', - fixWith: 'number', - }, - Symbol: { - message: 'Use symbol instead', - fixWith: 'symbol', - }, - BigInt: { - message: 'Use bigint instead', - fixWith: 'bigint', - }, - Object: { - message: 'Use object instead', - fixWith: 'object', - }, - Function: { - message: [ - 'The `Function` type accepts any function-like value.', - 'It provides no type safety when calling the function, which can be a common source of bugs.', - 'It also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.', - 'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.', - ].join('\n'), - }, -}; - -export const TYPE_KEYWORDS = { +const TYPE_KEYWORDS = { bigint: AST_NODE_TYPES.TSBigIntKeyword, boolean: AST_NODE_TYPES.TSBooleanKeyword, never: AST_NODE_TYPES.TSNeverKeyword, @@ -102,12 +68,11 @@ export const TYPE_KEYWORDS = { }; export default createRule({ - name: 'ban-types', + name: 'no-restricted-types', meta: { type: 'suggestion', docs: { description: 'Disallow certain types', - recommended: 'recommended', }, fixable: 'code', hasSuggestions: true, @@ -120,16 +85,6 @@ export default createRule({ $defs: { banConfig: { oneOf: [ - { - type: 'null', - description: 'Bans the type with the default message', - }, - { - type: 'boolean', - enum: [false], - description: - 'Un-bans the type (useful when paired with `extendDefaults`)', - }, { type: 'boolean', enum: [true], @@ -156,7 +111,6 @@ export default createRule({ type: 'array', items: { type: 'string' }, description: 'Types to suggest replacing with.', - additionalItems: false, }, }, additionalProperties: false, @@ -172,23 +126,13 @@ export default createRule({ $ref: '#/items/0/$defs/banConfig', }, }, - extendDefaults: { - type: 'boolean', - }, }, additionalProperties: false, }, ], }, defaultOptions: [{}], - create(context, [options]) { - const extendDefaults = options.extendDefaults ?? true; - const customTypes = options.types ?? {}; - const types = Object.assign( - {}, - extendDefaults ? defaultTypes : {}, - customTypes, - ); + create(context, [{ types = {} }]) { const bannedTypes = new Map( Object.entries(types).map(([type, data]) => [removeSpaces(type), data]), ); @@ -249,15 +193,19 @@ export default createRule({ return { ...keywordSelectors, - TSTypeLiteral(node): void { - if (node.members.length) { - return; - } - + TSClassImplements(node): void { + checkBannedTypes(node); + }, + TSInterfaceHeritage(node): void { checkBannedTypes(node); }, TSTupleType(node): void { - if (node.elementTypes.length === 0) { + if (!node.elementTypes.length) { + checkBannedTypes(node); + } + }, + TSTypeLiteral(node): void { + if (!node.members.length) { checkBannedTypes(node); } }, @@ -268,12 +216,6 @@ export default createRule({ checkBannedTypes(node); } }, - TSInterfaceHeritage(node): void { - checkBannedTypes(node); - }, - TSClassImplements(node): void { - checkBannedTypes(node); - }, }; }, }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-function-type.ts b/packages/eslint-plugin/src/rules/no-unsafe-function-type.ts new file mode 100644 index 000000000000..624c038f8ff7 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-function-type.ts @@ -0,0 +1,50 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { createRule, isReferenceToGlobalFunction } from '../util'; + +export default createRule({ + name: 'no-unsafe-function-type', + meta: { + type: 'problem', + docs: { + description: 'Disallow using the unsafe built-in Function type', + recommended: 'recommended', + }, + fixable: 'code', + messages: { + bannedFunctionType: [ + 'The `Function` type accepts any function-like value.', + 'Prefer explicitly defining any function parameters and return type.', + ].join('\n'), + }, + schema: [], + }, + defaultOptions: [], + create(context) { + function checkBannedTypes(node: TSESTree.Node): void { + if ( + node.type === AST_NODE_TYPES.Identifier && + node.name === 'Function' && + isReferenceToGlobalFunction('Function', node, context.sourceCode) + ) { + context.report({ + node, + messageId: 'bannedFunctionType', + }); + } + } + + return { + TSClassImplements(node): void { + checkBannedTypes(node.expression); + }, + TSInterfaceHeritage(node): void { + checkBannedTypes(node.expression); + }, + TSTypeReference(node): void { + checkBannedTypes(node.typeName); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/no-wrapper-object-types.ts b/packages/eslint-plugin/src/rules/no-wrapper-object-types.ts new file mode 100644 index 000000000000..f51b6c8564b5 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-wrapper-object-types.ts @@ -0,0 +1,71 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { createRule, isReferenceToGlobalFunction } from '../util'; + +const classNames = new Set([ + 'BigInt', + // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum + 'Boolean', + 'Number', + 'Object', + // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum + 'String', + 'Symbol', +]); + +export default createRule({ + name: 'no-wrapper-object-types', + meta: { + type: 'problem', + docs: { + description: 'Disallow using confusing built-in primitive class wrappers', + recommended: 'recommended', + }, + fixable: 'code', + messages: { + bannedClassType: + 'Prefer using the primitive `{{preferred}}` as a type name, rather than the upper-cased `{{typeName}}`.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + function checkBannedTypes( + node: TSESTree.EntityName | TSESTree.Expression, + includeFix: boolean, + ): void { + const typeName = node.type === AST_NODE_TYPES.Identifier && node.name; + if ( + !typeName || + !classNames.has(typeName) || + !isReferenceToGlobalFunction(typeName, node, context.sourceCode) + ) { + return; + } + + const preferred = typeName.toLowerCase(); + + context.report({ + data: { typeName, preferred }, + fix: includeFix + ? (fixer): TSESLint.RuleFix => fixer.replaceText(node, preferred) + : undefined, + messageId: 'bannedClassType', + node, + }); + } + + return { + TSClassImplements(node): void { + checkBannedTypes(node.expression, false); + }, + TSInterfaceHeritage(node): void { + checkBannedTypes(node.expression, false); + }, + TSTypeReference(node): void { + checkBannedTypes(node.typeName, true); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 54c44f4edda9..53beee5d2826 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -13,7 +13,7 @@ import { } from 'ts-api-utils'; import * as ts from 'typescript'; -import { isTypeFlagSet } from '../../util'; +import { isReferenceToGlobalFunction, isTypeFlagSet } from '../../util'; import type { PreferOptionalChainOptions } from './PreferOptionalChainOptions'; const enum ComparisonValueType { @@ -153,16 +153,13 @@ export function gatherLogicalOperands( comparedExpression.operator === 'typeof' ) { const argument = comparedExpression.argument; - if (argument.type === AST_NODE_TYPES.Identifier) { - const reference = sourceCode - .getScope(argument) - .references.find(ref => ref.identifier.name === argument.name); - - if (!reference?.resolved?.defs.length) { - // typeof window === 'undefined' - result.push({ type: OperandValidity.Invalid }); - continue; - } + if ( + argument.type === AST_NODE_TYPES.Identifier && + // typeof window === 'undefined' + isReferenceToGlobalFunction(argument.name, argument, sourceCode) + ) { + result.push({ type: OperandValidity.Invalid }); + continue; } // typeof x.y === 'undefined' diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 1f8d657cd542..083353a69233 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -14,6 +14,7 @@ export * from './isNullLiteral'; export * from './isUndefinedIdentifier'; export * from './misc'; export * from './objectIterators'; +export * from './scopeUtils'; export * from './types'; export * from './isAssignee'; diff --git a/packages/eslint-plugin/src/util/scopeUtils.ts b/packages/eslint-plugin/src/util/scopeUtils.ts new file mode 100644 index 000000000000..b350a7412106 --- /dev/null +++ b/packages/eslint-plugin/src/util/scopeUtils.ts @@ -0,0 +1,15 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; + +export function isReferenceToGlobalFunction( + calleeName: string, + node: TSESTree.Node, + sourceCode: SourceCode, +): boolean { + const ref = sourceCode + .getScope(node) + .references.find(ref => ref.identifier.name === calleeName); + + // ensure it's the "global" version + return !ref?.resolved?.defs.length; +} diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/ban-types.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/ban-types.shot deleted file mode 100644 index 6644fd985d9b..000000000000 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/ban-types.shot +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Validating rule docs ban-types.mdx code examples ESLint output 1`] = ` -"Incorrect - -// use lower-case primitives for consistency -const str: String = 'foo'; - ~~~~~~ Don't use \`String\` as a type. Use string instead -const bool: Boolean = true; - ~~~~~~~ Don't use \`Boolean\` as a type. Use boolean instead -const num: Number = 1; - ~~~~~~ Don't use \`Number\` as a type. Use number instead -const symb: Symbol = Symbol('foo'); - ~~~~~~ Don't use \`Symbol\` as a type. Use symbol instead -const bigInt: BigInt = 1n; - ~~~~~~ Don't use \`BigInt\` as a type. Use bigint instead - -// use a proper function type -const func: Function = () => 1; - ~~~~~~~~ Don't use \`Function\` as a type. The \`Function\` type accepts any function-like value. - It provides no type safety when calling the function, which can be a common source of bugs. - It also accepts things like class declarations, which will throw at runtime as they will not be called with \`new\`. - If you are expecting the function to accept certain arguments, you should explicitly define the function shape. - -// use safer object types -const lowerObj: Object = {}; - ~~~~~~ Don't use \`Object\` as a type. Use object instead -const capitalObj: Object = { a: 'string' }; - ~~~~~~ Don't use \`Object\` as a type. Use object instead -" -`; - -exports[`Validating rule docs ban-types.mdx code examples ESLint output 2`] = ` -"Correct - -// use lower-case primitives for consistency -const str: string = 'foo'; -const bool: boolean = true; -const num: number = 1; -const symb: symbol = Symbol('foo'); -const bigInt: bigint = 1n; - -// use a proper function type -const func: () => number = () => 1; - -// use safer object types -const lowerObj: object = {}; -const capitalObj: { a: string } = { a: 'string' }; -" -`; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-function-type.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-function-type.shot new file mode 100644 index 000000000000..c77f15d6feb3 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-function-type.shot @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs no-unsafe-function-type.mdx code examples ESLint output 1`] = ` +"Incorrect + +let noParametersOrReturn: Function; + ~~~~~~~~ The \`Function\` type accepts any function-like value. + Prefer explicitly defining any function parameters and return type. +noParametersOrReturn = () => {}; + +let stringToNumber: Function; + ~~~~~~~~ The \`Function\` type accepts any function-like value. + Prefer explicitly defining any function parameters and return type. +stringToNumber = (text: string) => text.length; + +let identity: Function; + ~~~~~~~~ The \`Function\` type accepts any function-like value. + Prefer explicitly defining any function parameters and return type. +identity = value => value; +" +`; + +exports[`Validating rule docs no-unsafe-function-type.mdx code examples ESLint output 2`] = ` +"Correct + +let noParametersOrReturn: () => void; +noParametersOrReturn = () => {}; + +let stringToNumber: (text: string) => number; +stringToNumber = text => text.length; + +let identity: (value: T) => T; +identity = value => value; +" +`; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-wrapper-object-types.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-wrapper-object-types.shot new file mode 100644 index 000000000000..4303b79a77a7 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-wrapper-object-types.shot @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs no-wrapper-object-types.mdx code examples ESLint output 1`] = ` +"Incorrect + +let myBigInt: BigInt; + ~~~~~~ Prefer using the primitive \`bigint\` as a type name, rather than the upper-cased \`BigInt\`. +let myBoolean: Boolean; + ~~~~~~~ Prefer using the primitive \`boolean\` as a type name, rather than the upper-cased \`Boolean\`. +let myNumber: Number; + ~~~~~~ Prefer using the primitive \`number\` as a type name, rather than the upper-cased \`Number\`. +let myString: String; + ~~~~~~ Prefer using the primitive \`string\` as a type name, rather than the upper-cased \`String\`. +let mySymbol: Symbol; + ~~~~~~ Prefer using the primitive \`symbol\` as a type name, rather than the upper-cased \`Symbol\`. + +let myObject: Object = 'allowed by TypeScript'; + ~~~~~~ Prefer using the primitive \`object\` as a type name, rather than the upper-cased \`Object\`. +" +`; + +exports[`Validating rule docs no-wrapper-object-types.mdx code examples ESLint output 2`] = ` +"Correct + +let myBigint: bigint; +let myBoolean: boolean; +let myNumber: number; +let myString: string; +let mySymbol: symbol; + +let myObject: object = "Type 'string' is not assignable to type 'object'."; +" +`; diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index 96401ecb2fa5..a54f57a7bee4 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -155,6 +155,7 @@ describe('Validating rule docs', () => { 'TEMPLATE.md', // These rule docs were left behind on purpose for legacy reasons. See the // comments in the files for more information. + 'ban-types.md', 'no-duplicate-imports.mdx', 'no-parameter-properties.mdx', 'no-useless-template-literals.mdx', @@ -162,7 +163,11 @@ describe('Validating rule docs', () => { ...oldStylisticRules, ]); - const rulesWithComplexOptions = new Set(['array-type', 'member-ordering']); + const rulesWithComplexOptions = new Set([ + 'array-type', + 'member-ordering', + 'no-restricted-types', + ]); // TODO: whittle this list down to as few as possible const rulesWithComplexOptionHeadings = new Set([ diff --git a/packages/eslint-plugin/tests/rules/ban-types.test.ts b/packages/eslint-plugin/tests/rules/ban-types.test.ts deleted file mode 100644 index 046ac290a635..000000000000 --- a/packages/eslint-plugin/tests/rules/ban-types.test.ts +++ /dev/null @@ -1,741 +0,0 @@ -/* eslint-disable @typescript-eslint/internal/prefer-ast-types-enum */ -import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; -import type { TSESLint } from '@typescript-eslint/utils'; - -import type { MessageIds, Options } from '../../src/rules/ban-types'; -import rule, { TYPE_KEYWORDS } from '../../src/rules/ban-types'; -import { objectReduceKey } from '../../src/util'; - -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', -}); - -const options: Options = [ - { - types: { - String: { - message: 'Use string instead.', - fixWith: 'string', - }, - Object: "Use '{}' instead.", - Array: null, - F: null, - 'NS.Bad': { - message: 'Use NS.Good instead.', - fixWith: 'NS.Good', - }, - }, - extendDefaults: false, - }, -]; - -ruleTester.run('ban-types', rule, { - valid: [ - 'let f = Object();', // Should not fail if there is no options set - 'let f: { x: number; y: number } = { x: 1, y: 1 };', - { - code: 'let f = Object();', - options, - }, - { - code: 'let g = Object.create(null);', - options, - }, - { - code: 'let h = String(false);', - options, - }, - { - code: 'let e: foo.String;', - options, - }, - { - code: 'let a: _.NS.Bad;', - options, - }, - { - code: 'let a: NS.Bad._;', - options, - }, - // Replace default options instead of merging with extendDefaults: false - { - code: 'let a: String;', - options: [ - { - types: { - Number: { - message: 'Use number instead.', - fixWith: 'number', - }, - }, - extendDefaults: false, - }, - ], - }, - { - code: 'let a: undefined;', - options: [ - { - types: { - null: { - message: 'Use undefined instead.', - fixWith: 'undefined', - }, - }, - }, - ], - }, - { - code: 'let a: null;', - options: [ - { - types: { - undefined: null, - }, - extendDefaults: false, - }, - ], - }, - { - code: 'type Props = {};', - options: [ - { - types: { - '{}': false, - }, - extendDefaults: true, - }, - ], - }, - 'let a: [];', - ], - invalid: [ - { - code: 'let a: String;', - output: 'let a: string;', - errors: [ - { - messageId: 'bannedTypeMessage', - line: 1, - column: 8, - }, - ], - }, - { - code: 'let a: Object;', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'Object', - customMessage: " Use '{}' instead.", - }, - line: 1, - column: 8, - }, - ], - options, - }, - { - code: 'let aa: Foo;', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'Foo', - customMessage: '', - }, - }, - ], - options: [ - { - types: { - Foo: { message: '' }, - }, - }, - ], - }, - { - code: 'let b: { c: String };', - output: 'let b: { c: string };', - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 1, - column: 13, - }, - ], - options, - }, - { - code: 'function foo(a: String) {}', - output: 'function foo(a: string) {}', - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 1, - column: 17, - }, - ], - options, - }, - { - code: "'a' as String;", - output: "'a' as string;", - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 1, - column: 8, - }, - ], - options, - }, - { - code: 'let c: F;', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { name: 'F', customMessage: '' }, - line: 1, - column: 8, - }, - ], - options, - }, - { - code: ` -class Foo extends Bar implements Baz { - constructor(foo: String | Object) {} - - exit(): Array { - const foo: String = 1 as String; - } -} - `, - output: ` -class Foo extends Bar implements Baz { - constructor(foo: string | Object) {} - - exit(): Array { - const foo: string = 1 as string; - } -} - `, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 2, - column: 15, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 2, - column: 35, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: 'Object', - customMessage: " Use '{}' instead.", - }, - line: 2, - column: 58, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 3, - column: 20, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: 'Object', - customMessage: " Use '{}' instead.", - }, - line: 3, - column: 29, - }, - { - messageId: 'bannedTypeMessage', - data: { name: 'Array', customMessage: '' }, - line: 5, - column: 11, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 5, - column: 17, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 6, - column: 16, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: 'String', - customMessage: ' Use string instead.', - }, - line: 6, - column: 30, - }, - ], - options, - }, - { - code: 'let a: NS.Bad;', - output: 'let a: NS.Good;', - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'NS.Bad', - customMessage: ' Use NS.Good instead.', - }, - line: 1, - column: 8, - }, - ], - options, - }, - { - code: ` -let a: NS.Bad; -let b: Foo; - `, - output: ` -let a: NS.Good; -let b: Foo; - `, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'NS.Bad', - customMessage: ' Use NS.Good instead.', - }, - line: 2, - column: 8, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: 'NS.Bad', - customMessage: ' Use NS.Good instead.', - }, - line: 3, - column: 12, - }, - ], - options, - }, - { - code: 'let foo: {} = {};', - output: 'let foo: object = {};', - options: [ - { - types: { - '{}': { - message: 'Use object instead.', - fixWith: 'object', - }, - }, - }, - ], - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: '{}', - customMessage: ' Use object instead.', - }, - line: 1, - column: 10, - }, - ], - }, - { - code: noFormat` -let foo: {} = {}; -let bar: { } = {}; -let baz: { -} = {}; - `, - output: ` -let foo: object = {}; -let bar: object = {}; -let baz: object = {}; - `, - options: [ - { - types: { - '{ }': { - message: 'Use object instead.', - fixWith: 'object', - }, - }, - }, - ], - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: '{}', - customMessage: ' Use object instead.', - }, - line: 2, - column: 10, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: '{}', - customMessage: ' Use object instead.', - }, - line: 3, - column: 10, - }, - { - messageId: 'bannedTypeMessage', - data: { - name: '{}', - customMessage: ' Use object instead.', - }, - line: 4, - column: 10, - }, - ], - }, - { - code: 'let a: NS.Bad;', - output: 'let a: NS.Good;', - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'NS.Bad', - customMessage: ' Use NS.Good instead.', - }, - line: 1, - column: 8, - }, - ], - options: [ - { - types: { - ' NS.Bad ': { - message: 'Use NS.Good instead.', - fixWith: 'NS.Good', - }, - }, - }, - ], - }, - { - code: noFormat`let a: Foo< F >;`, - output: `let a: Foo< T >;`, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'F', - customMessage: ' Use T instead.', - }, - line: 1, - column: 15, - }, - ], - options: [ - { - types: { - ' F ': { - message: 'Use T instead.', - fixWith: 'T', - }, - }, - }, - ], - }, - { - code: 'type Foo = Bar;', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'Bar', - customMessage: " Don't use `any` as a type parameter to `Bar`", - }, - line: 1, - column: 12, - }, - ], - options: [ - { - types: { - 'Bar': "Don't use `any` as a type parameter to `Bar`", - }, - }, - ], - }, - { - code: noFormat`type Foo = Bar;`, - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: 'Bar', - customMessage: " Don't pass `A, B` as parameters to `Bar`", - }, - line: 1, - column: 12, - }, - ], - options: [ - { - types: { - 'Bar': "Don't pass `A, B` as parameters to `Bar`", - }, - }, - ], - }, - { - code: 'let a: [];', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: '[]', - customMessage: ' `[]` does only allow empty arrays.', - }, - line: 1, - column: 8, - }, - ], - options: [ - { - types: { - '[]': '`[]` does only allow empty arrays.', - }, - }, - ], - }, - { - code: noFormat`let a: [ ] ;`, - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: '[]', - customMessage: ' `[]` does only allow empty arrays.', - }, - line: 1, - column: 9, - }, - ], - options: [ - { - types: { - '[]': '`[]` does only allow empty arrays.', - }, - }, - ], - }, - { - code: 'let a: [];', - output: 'let a: any[];', - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: '[]', - customMessage: ' `[]` does only allow empty arrays.', - }, - line: 1, - column: 8, - }, - ], - options: [ - { - types: { - '[]': { - message: '`[]` does only allow empty arrays.', - fixWith: 'any[]', - }, - }, - }, - ], - }, - { - code: 'let a: [[]];', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: '[]', - customMessage: ' `[]` does only allow empty arrays.', - }, - line: 1, - column: 9, - }, - ], - options: [ - { - types: { - '[]': '`[]` does only allow empty arrays.', - }, - }, - ], - }, - { - code: 'type Baz = 1 & Foo;', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - }, - ], - options: [ - { - types: { - Foo: { message: '' }, - }, - }, - ], - }, - { - code: 'interface Foo extends Bar {}', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - }, - ], - options: [ - { - types: { - Bar: { message: '' }, - }, - }, - ], - }, - { - code: 'interface Foo extends Bar, Baz {}', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - }, - ], - options: [ - { - types: { - Bar: { message: '' }, - }, - }, - ], - }, - { - code: 'class Foo implements Bar {}', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - }, - ], - options: [ - { - types: { - Bar: { message: '' }, - }, - }, - ], - }, - { - code: 'class Foo implements Bar, Baz {}', - output: null, - errors: [ - { - messageId: 'bannedTypeMessage', - }, - ], - options: [ - { - types: { - Bar: { message: 'Bla' }, - }, - }, - ], - }, - ...objectReduceKey( - TYPE_KEYWORDS, - (acc: TSESLint.InvalidTestCase[], key) => { - acc.push({ - code: `function foo(x: ${key}) {}`, - errors: [ - { - messageId: 'bannedTypeMessage', - data: { - name: key, - customMessage: '', - }, - line: 1, - column: 17, - }, - ], - options: [ - { - extendDefaults: false, - types: { - [key]: null, - }, - }, - ], - }); - return acc; - }, - [], - ), - ], -}); diff --git a/packages/eslint-plugin/tests/rules/no-restricted-types.test.ts b/packages/eslint-plugin/tests/rules/no-restricted-types.test.ts new file mode 100644 index 000000000000..28bf5435828a --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-restricted-types.test.ts @@ -0,0 +1,628 @@ +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-restricted-types'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-restricted-types', rule, { + valid: [ + 'let f = Object();', + 'let f: { x: number; y: number } = { x: 1, y: 1 };', + { + code: 'let f = Object();', + options: [{ types: { Object: true } }], + }, + { + code: 'let f = Object(false);', + options: [{ types: { Object: true } }], + }, + { + code: 'let g = Object.create(null);', + options: [{ types: { Object: true } }], + }, + { + code: 'let e: namespace.Object;', + options: [{ types: { Object: true } }], + }, + { + code: 'let value: _.NS.Banned;', + options: [{ types: { 'NS.Banned': true } }], + }, + { + code: 'let value: NS.Banned._;', + options: [{ types: { 'NS.Banned': true } }], + }, + ], + invalid: [ + { + code: 'let value: bigint;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'bigint', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { bigint: 'Use Ok instead.' } }], + }, + { + code: 'let value: boolean;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'boolean', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { boolean: 'Use Ok instead.' } }], + }, + { + code: 'let value: never;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'never', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { never: 'Use Ok instead.' } }], + }, + { + code: 'let value: null;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'null', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { null: 'Use Ok instead.' } }], + }, + { + code: 'let value: number;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'number', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { number: 'Use Ok instead.' } }], + }, + { + code: 'let value: object;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'object', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { object: 'Use Ok instead.' } }], + }, + { + code: 'let value: string;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'string', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { string: 'Use Ok instead.' } }], + }, + { + code: 'let value: symbol;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'symbol', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { symbol: 'Use Ok instead.' } }], + }, + { + code: 'let value: undefined;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'undefined', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { undefined: 'Use Ok instead.' } }], + }, + { + code: 'let value: unknown;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'unknown', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { unknown: 'Use Ok instead.' } }], + }, + { + code: 'let value: void;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use Ok instead.', + name: 'void', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { void: 'Use Ok instead.' } }], + }, + { + code: 'let value: [];', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use unknown[] instead.', + name: '[]', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { '[]': 'Use unknown[] instead.' } }], + }, + { + code: noFormat`let value: [ ];`, + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use unknown[] instead.', + name: '[]', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { '[]': 'Use unknown[] instead.' } }], + }, + { + code: 'let value: [[]];', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: ' Use unknown[] instead.', + name: '[]', + }, + line: 1, + column: 13, + }, + ], + options: [{ types: { '[]': 'Use unknown[] instead.' } }], + }, + { + code: 'let value: Banned;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: '', + name: 'Banned', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { Banned: true } }], + }, + { + code: 'let value: Banned;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: " Use '{}' instead.", + name: 'Banned', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { Banned: "Use '{}' instead." } }], + }, + { + code: 'let value: Banned[];', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: " Use '{}' instead.", + name: 'Banned', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { Banned: "Use '{}' instead." } }], + }, + { + code: 'let value: [Banned];', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: " Use '{}' instead.", + name: 'Banned', + }, + line: 1, + column: 13, + }, + ], + options: [{ types: { Banned: "Use '{}' instead." } }], + }, + { + code: 'let value: Banned;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + customMessage: '', + name: 'Banned', + }, + line: 1, + column: 12, + }, + ], + options: [{ types: { Banned: '' } }], + }, + { + code: 'let b: { c: Banned };', + output: 'let b: { c: Ok };', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: ' Use Ok instead.', + }, + line: 1, + column: 13, + }, + ], + options: [ + { types: { Banned: { message: 'Use Ok instead.', fixWith: 'Ok' } } }, + ], + }, + { + code: '1 as Banned;', + output: '1 as Ok;', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: ' Use Ok instead.', + }, + line: 1, + column: 6, + }, + ], + options: [ + { types: { Banned: { message: 'Use Ok instead.', fixWith: 'Ok' } } }, + ], + }, + { + code: 'class Derived implements Banned {}', + output: 'class Derived implements Ok {}', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: ' Use Ok instead.', + }, + line: 1, + column: 26, + }, + ], + options: [ + { types: { Banned: { message: 'Use Ok instead.', fixWith: 'Ok' } } }, + ], + }, + { + code: 'class Derived implements Banned1, Banned2 {}', + output: 'class Derived implements Ok1, Ok2 {}', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned1', + customMessage: ' Use Ok1 instead.', + }, + line: 1, + column: 26, + }, + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned2', + customMessage: ' Use Ok2 instead.', + }, + line: 1, + column: 35, + }, + ], + options: [ + { + types: { + Banned1: { message: 'Use Ok1 instead.', fixWith: 'Ok1' }, + Banned2: { message: 'Use Ok2 instead.', fixWith: 'Ok2' }, + }, + }, + ], + }, + { + code: 'interface Derived extends Banned {}', + output: 'interface Derived extends Ok {}', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: ' Use Ok instead.', + }, + line: 1, + column: 27, + }, + ], + options: [ + { types: { Banned: { message: 'Use Ok instead.', fixWith: 'Ok' } } }, + ], + }, + { + code: 'type Intersection = Banned & {};', + output: 'type Intersection = Ok & {};', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: ' Use Ok instead.', + }, + line: 1, + column: 21, + }, + ], + options: [ + { types: { Banned: { message: 'Use Ok instead.', fixWith: 'Ok' } } }, + ], + }, + { + code: 'type Union = Banned | {};', + output: 'type Union = Ok | {};', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: ' Use Ok instead.', + }, + line: 1, + column: 14, + }, + ], + options: [ + { types: { Banned: { message: 'Use Ok instead.', fixWith: 'Ok' } } }, + ], + }, + { + code: 'let value: NS.Banned;', + output: 'let value: NS.Ok;', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'NS.Banned', + customMessage: ' Use NS.Ok instead.', + }, + line: 1, + column: 12, + }, + ], + options: [ + { + types: { + 'NS.Banned': { + message: 'Use NS.Ok instead.', + fixWith: 'NS.Ok', + }, + }, + }, + ], + }, + { + code: 'let value: {} = {};', + output: 'let value: object = {};', + options: [ + { + types: { + '{}': { + message: 'Use object instead.', + fixWith: 'object', + }, + }, + }, + ], + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: '{}', + customMessage: ' Use object instead.', + }, + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: NS.Banned;', + output: 'let value: NS.Ok;', + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'NS.Banned', + customMessage: ' Use NS.Ok instead.', + }, + line: 1, + column: 12, + }, + ], + options: [ + { + types: { + ' NS.Banned ': { + message: 'Use NS.Ok instead.', + fixWith: 'NS.Ok', + }, + }, + }, + ], + }, + { + code: noFormat`let value: Type< Banned >;`, + output: `let value: Type< Ok >;`, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: ' Use Ok instead.', + }, + line: 1, + column: 20, + }, + ], + options: [ + { + types: { + ' Banned ': { + message: 'Use Ok instead.', + fixWith: 'Ok', + }, + }, + }, + ], + }, + { + code: 'type Intersection = Banned;', + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: " Don't use `any` as a type parameter to `Banned`", + }, + line: 1, + column: 21, + }, + ], + options: [ + { + types: { + 'Banned': "Don't use `any` as a type parameter to `Banned`", + }, + }, + ], + }, + { + code: noFormat`type Intersection = Banned;`, + output: null, + errors: [ + { + messageId: 'bannedTypeMessage', + data: { + name: 'Banned', + customMessage: " Don't pass `A, B` as parameters to `Banned`", + }, + line: 1, + column: 21, + }, + ], + options: [ + { + types: { + 'Banned': "Don't pass `A, B` as parameters to `Banned`", + }, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-function-type.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-function-type.test.ts new file mode 100644 index 000000000000..88e2d932a07b --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-function-type.test.ts @@ -0,0 +1,83 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-unsafe-function-type'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-unsafe-function-type', rule, { + valid: [ + 'let value: () => void;', + 'let value: (t: T) => T;', + ` + type Function = () => void; + let value: Function; + `, + ], + invalid: [ + { + code: 'let value: Function;', + output: null, + errors: [ + { + messageId: 'bannedFunctionType', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: Function[];', + output: null, + errors: [ + { + messageId: 'bannedFunctionType', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: Function | number;', + output: null, + errors: [ + { + messageId: 'bannedFunctionType', + line: 1, + column: 12, + }, + ], + }, + { + code: ` + class Weird implements Function { + // ... + } + `, + output: null, + errors: [ + { + messageId: 'bannedFunctionType', + line: 2, + column: 32, + }, + ], + }, + { + code: ` + interface Weird extends Function { + // ... + } + `, + output: null, + errors: [ + { + messageId: 'bannedFunctionType', + line: 2, + column: 33, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-wrapper-object-types.test.ts b/packages/eslint-plugin/tests/rules/no-wrapper-object-types.test.ts new file mode 100644 index 000000000000..73613259cc22 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-wrapper-object-types.test.ts @@ -0,0 +1,242 @@ +/* eslint-disable @typescript-eslint/internal/prefer-ast-types-enum */ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-wrapper-object-types'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-wrapper-object-types', rule, { + valid: [ + 'let value: NumberLike;', + 'let value: Other;', + 'let value: bigint;', + 'let value: boolean;', + 'let value: never;', + 'let value: null;', + 'let value: number;', + 'let value: symbol;', + 'let value: undefined;', + 'let value: unknown;', + 'let value: void;', + 'let value: () => void;', + 'let value: () => () => void;', + 'let Bigint;', + 'let Boolean;', + 'let Never;', + 'let Null;', + 'let Number;', + 'let Symbol;', + 'let Undefined;', + 'let Unknown;', + 'let Void;', + 'interface Bigint {}', + 'interface Boolean {}', + 'interface Never {}', + 'interface Null {}', + 'interface Number {}', + 'interface Symbol {}', + 'interface Undefined {}', + 'interface Unknown {}', + 'interface Void {}', + 'type Bigint = {};', + 'type Boolean = {};', + 'type Never = {};', + 'type Null = {};', + 'type Number = {};', + 'type Symbol = {};', + 'type Undefined = {};', + 'type Unknown = {};', + 'type Void = {};', + 'class MyClass extends Number {}', + ` + type Number = 0 | 1; + let value: Number; + `, + ` + type Bigint = 0 | 1; + let value: Bigint; + `, + ` + type T = Symbol; + type U = UU extends T ? Function : never; + `, + ], + invalid: [ + { + code: 'let value: BigInt;', + output: 'let value: bigint;', + errors: [ + { + data: { typeName: 'BigInt', preferred: 'bigint' }, + messageId: 'bannedClassType', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: Boolean;', + output: 'let value: boolean;', + errors: [ + { + data: { typeName: 'Boolean', preferred: 'boolean' }, + messageId: 'bannedClassType', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: Number;', + output: 'let value: number;', + errors: [ + { + data: { typeName: 'Number', preferred: 'number' }, + messageId: 'bannedClassType', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: Object;', + output: 'let value: object;', + errors: [ + { + data: { typeName: 'Object', preferred: 'object' }, + messageId: 'bannedClassType', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: String;', + output: 'let value: string;', + errors: [ + { + data: { typeName: 'String', preferred: 'string' }, + messageId: 'bannedClassType', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: Symbol;', + output: 'let value: symbol;', + errors: [ + { + data: { typeName: 'Symbol', preferred: 'symbol' }, + messageId: 'bannedClassType', + line: 1, + column: 12, + }, + ], + }, + { + code: 'let value: Number | Symbol;', + output: 'let value: number | symbol;', + errors: [ + { + data: { typeName: 'Number', preferred: 'number' }, + messageId: 'bannedClassType', + line: 1, + column: 12, + }, + { + data: { typeName: 'Symbol', preferred: 'symbol' }, + messageId: 'bannedClassType', + line: 1, + column: 21, + }, + ], + }, + { + code: 'let value: { property: Number };', + output: 'let value: { property: number };', + errors: [ + { + messageId: 'bannedClassType', + line: 1, + column: 24, + }, + ], + }, + { + code: '0 as Number;', + output: '0 as number;', + errors: [ + { + messageId: 'bannedClassType', + line: 1, + column: 6, + }, + ], + }, + { + code: 'type MyType = Number;', + output: 'type MyType = number;', + errors: [ + { + messageId: 'bannedClassType', + line: 1, + column: 15, + }, + ], + }, + { + code: 'type MyType = [Number];', + output: 'type MyType = [number];', + errors: [ + { + messageId: 'bannedClassType', + line: 1, + column: 16, + }, + ], + }, + { + code: 'class MyClass implements Number {}', + output: null, + errors: [ + { + messageId: 'bannedClassType', + line: 1, + column: 26, + }, + ], + }, + { + code: 'interface MyInterface extends Number {}', + output: null, + errors: [ + { + messageId: 'bannedClassType', + line: 1, + column: 31, + }, + ], + }, + { + code: 'type MyType = Number & String;', + output: 'type MyType = number & string;', + errors: [ + { + data: { preferred: 'number', typeName: 'Number' }, + messageId: 'bannedClassType', + line: 1, + column: 15, + }, + { + data: { preferred: 'string', typeName: 'String' }, + messageId: 'bannedClassType', + line: 1, + column: 24, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/schema-snapshots/ban-types.shot b/packages/eslint-plugin/tests/schema-snapshots/no-restricted-types.shot similarity index 76% rename from packages/eslint-plugin/tests/schema-snapshots/ban-types.shot rename to packages/eslint-plugin/tests/schema-snapshots/no-restricted-types.shot index a7fe2d555cda..845262dabe82 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/ban-types.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-restricted-types.shot @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Rule schemas should be convertible to TS types for documentation purposes ban-types 1`] = ` +exports[`Rule schemas should be convertible to TS types for documentation purposes no-restricted-types 1`] = ` " # SCHEMA: @@ -9,15 +9,6 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "$defs": { "banConfig": { "oneOf": [ - { - "description": "Bans the type with the default message", - "type": "null" - }, - { - "description": "Un-bans the type (useful when paired with \`extendDefaults\`)", - "enum": [false], - "type": "boolean" - }, { "description": "Bans the type with the default message", "enum": [true], @@ -40,7 +31,6 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos "type": "string" }, "suggest": { - "additionalItems": false, "description": "Types to suggest replacing with.", "items": { "type": "string" @@ -55,9 +45,6 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos }, "additionalProperties": false, "properties": { - "extendDefaults": { - "type": "boolean" - }, "types": { "additionalProperties": { "$ref": "#/items/0/$defs/banConfig" @@ -85,15 +72,10 @@ type BanConfig = /** Bans the type with a custom message */ | string /** Bans the type with the default message */ - | null - /** Bans the type with the default message */ - | true - /** Un-bans the type (useful when paired with \`extendDefaults\`) */ - | false; + | true; type Options = [ { - extendDefaults?: boolean; types?: { [k: string]: BanConfig; }; diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-function-type.shot b/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-function-type.shot new file mode 100644 index 000000000000..0317a17c3ad5 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-unsafe-function-type.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes no-unsafe-function-type 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-wrapper-object-types.shot b/packages/eslint-plugin/tests/schema-snapshots/no-wrapper-object-types.shot new file mode 100644 index 000000000000..247f1dd2e183 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-wrapper-object-types.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes no-wrapper-object-types 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; diff --git a/packages/scope-manager/tests/test-utils/serializers/baseSerializer.ts b/packages/scope-manager/tests/test-utils/serializers/baseSerializer.ts index cb619d683a5b..509608a30fb4 100644 --- a/packages/scope-manager/tests/test-utils/serializers/baseSerializer.ts +++ b/packages/scope-manager/tests/test-utils/serializers/baseSerializer.ts @@ -33,7 +33,7 @@ function createSerializer( ): string { const id = thing.$id != null ? `$${thing.$id}` : ''; // If `type` is a base class, we should print out the name of the subclass - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types const constructorName = (Object.getPrototypeOf(thing) as Object) .constructor.name; diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index f01ac17c8ddb..dc899379b164 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -24,7 +24,6 @@ export default ( '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/ban-ts-comment': 'error', '@typescript-eslint/ban-tslint-comment': 'error', - '@typescript-eslint/ban-types': 'error', '@typescript-eslint/class-literal-property-style': 'error', 'class-methods-use-this': 'off', '@typescript-eslint/class-methods-use-this': 'error', @@ -94,6 +93,7 @@ export default ( '@typescript-eslint/no-require-imports': 'error', 'no-restricted-imports': 'off', '@typescript-eslint/no-restricted-imports': 'error', + '@typescript-eslint/no-restricted-types': 'error', 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'error', '@typescript-eslint/no-this-alias': 'error', @@ -109,6 +109,7 @@ export default ( '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', @@ -121,6 +122,7 @@ export default ( 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-useless-empty-export': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', diff --git a/packages/typescript-eslint/src/configs/recommended-type-checked.ts b/packages/typescript-eslint/src/configs/recommended-type-checked.ts index 47b8c9b08ef6..993f7baa90ed 100644 --- a/packages/typescript-eslint/src/configs/recommended-type-checked.ts +++ b/packages/typescript-eslint/src/configs/recommended-type-checked.ts @@ -21,7 +21,6 @@ export default ( rules: { '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/ban-ts-comment': 'error', - '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-array-delete': 'error', @@ -49,6 +48,7 @@ export default ( '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', @@ -56,6 +56,7 @@ export default ( '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', '@typescript-eslint/prefer-as-const': 'error', diff --git a/packages/typescript-eslint/src/configs/recommended.ts b/packages/typescript-eslint/src/configs/recommended.ts index 0c9d5bb3c91e..1e6be3251a08 100644 --- a/packages/typescript-eslint/src/configs/recommended.ts +++ b/packages/typescript-eslint/src/configs/recommended.ts @@ -20,7 +20,6 @@ export default ( name: 'typescript-eslint/recommended', rules: { '@typescript-eslint/ban-ts-comment': 'error', - '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', @@ -34,10 +33,12 @@ export default ( '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-unnecessary-type-constraint': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-namespace-keyword': 'error', '@typescript-eslint/triple-slash-reference': 'error', diff --git a/packages/typescript-eslint/src/configs/strict-type-checked.ts b/packages/typescript-eslint/src/configs/strict-type-checked.ts index 61d0a4d579a2..fb53665756e3 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts @@ -24,7 +24,6 @@ export default ( 'error', { minimumDescriptionLength: 10 }, ], - '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-array-delete': 'error', @@ -64,6 +63,7 @@ export default ( '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', '@typescript-eslint/no-unsafe-unary-minus': 'error', @@ -73,6 +73,7 @@ export default ( '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', '@typescript-eslint/prefer-as-const': 'error', diff --git a/packages/typescript-eslint/src/configs/strict.ts b/packages/typescript-eslint/src/configs/strict.ts index 4a581ff95335..d6c5a37e9c54 100644 --- a/packages/typescript-eslint/src/configs/strict.ts +++ b/packages/typescript-eslint/src/configs/strict.ts @@ -23,7 +23,6 @@ export default ( 'error', { minimumDescriptionLength: 10 }, ], - '@typescript-eslint/ban-types': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', @@ -42,12 +41,14 @@ export default ( '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-unnecessary-type-constraint': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-literal-enum-member': 'error', '@typescript-eslint/prefer-namespace-keyword': 'error', diff --git a/packages/utils/src/ts-eslint/Rule.ts b/packages/utils/src/ts-eslint/Rule.ts index 899b8a98346b..fe3fa0412d07 100644 --- a/packages/utils/src/ts-eslint/Rule.ts +++ b/packages/utils/src/ts-eslint/Rule.ts @@ -690,7 +690,7 @@ never only allow unidirectional) export type LooseRuleCreateFunction = (context: any) => Record< string, /* - eslint-disable-next-line @typescript-eslint/ban-types -- + eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- intentionally use Function here to give us the basic "is a function" validation without enforcing specific argument types so that different AST types can still be passed to configs diff --git a/packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx b/packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx index 6bda8fcbf66e..b30789289077 100644 --- a/packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx +++ b/packages/website/blog/2024-05-27-announcing-typescript-eslint-v8-beta.mdx @@ -187,11 +187,45 @@ Several rules are changed in significant enough ways to be considered breaking c - If you want to have the rule check conditional tests, set its [`ignoreConditionalTests` option](/rules/prefer-nullish-coalescing/#ignoreconditionaltests) to `false` in your ESLint config - [feat(eslint-plugin): [no-unused-vars] align catch behavior to ESLint 9](https://github.com/typescript-eslint/typescript-eslint/pull/8971) - If you want [`@typescript-eslint/no-unused-vars`](/rules/no-unused-vars) to ignore caught errors, enable its `caughtErrors` option to `'none'` in your ESLint config + +#### Replacement of `ban-types` + +[`@typescript-eslint/ban-types`](https://typescript-eslint.io/rules/ban-types) has long been one of the more controversial rules in typescript-eslint. +It served two purposes: + +- Allowing users to ban a configurable list of types from being used in type annotations +- Banning confusing or dangerous built-in types such as `Function` and `Number` + +Notably, `ban-types` banned the built-in `{}` ("empty object") type in TypeScript. +The `{}` type is a common source of confusion for TypeScript developers because it matches _any non-nullable_ value, including primitives like `""`. + +Banning `{}` in `ban-types` was helpful to prevent developers from accidentally using it instead of a more safe type such as `object`. +On the other hand, there are legitimate uses for `{}`, and banning it by default was harmful in those cases. + +typescript-eslint v8 deletes the `ban-types` rule and replaces it with several more targeted rules: + +- [`@typescript-eslint/no-restricted-types`](/rules/no-restricted-types) is the new rule for banning a configurable list of type names. + It has no options enabled by default. +- [`@typescript-eslint/no-empty-object-type`](/rules/no-empty-object-type) bans the built-in `{}` type in confusing locations. +- [`@typescript-eslint/no-unsafe-function-type`](/rules/no-unsafe-function-type) bans the built-in `Function` type +- [`@typescript-eslint/no-wrapper-object-types`](/rules/no-wrapper-object-types) bans `Object` and built-in class wrappers such as `Number`. + +To migrate to the new rules: + +- If you were disabling the ban on `{}`, consider enabling [`@typescript-eslint/no-empty-object-type`](https://v8--typescript-eslint.netlify.app/rules/no-empty-object-type), as it allows some cases of `{}` that were previously banned. +- If you were banning any configurable types lists, provide a similar configuration to [`no-restricted-types`](/rules/no-restricted-types). +- If you have [`@typescript-eslint/ban-types`](/rules/ban-types) manually enabled, it will no longer ban: + - `{}` or `object`: use a [recommended config](/users/configs) or manually enable [`@typescript-eslint/no-empty-object-type`](https://v8--typescript-eslint.netlify.app/rules/no-empty-object-type) + - `Function`: use a [recommended config](/users/configs) or manually enable [`@typescript-eslint/no-unsafe-function-type`](https://v8--typescript-eslint.netlify.app/rules/no-unsafe-function-type) + - `Number` or other built-in uppercase types: use a [recommended config](/users/configs) or manually enable [`@typescript-eslint/no-wrapper-object-types`](https://v8--typescript-eslint.netlify.app/rules/no-wrapper-object-types) +- If you have [`@typescript-eslint/no-empty-interface`](/rules/no-empty-interface) manually enabled, remove that, and instead either use a [recommended config](/users/configs) or manually enable [`@typescript-eslint/no-empty-object-type`](https://v8--typescript-eslint.netlify.app/rules/no-empty-object-type) + +For more details, see the issues and pull requests that split apart the `ban-types` rule: + +- [Enhancement: [ban-types] Split the {} ban into a separate, better-phrased rule](https://github.com/typescript-eslint/typescript-eslint/issues/8700) - [feat(eslint-plugin): split no-empty-object-type out from ban-types and no-empty-interface](https://github.com/typescript-eslint/typescript-eslint/pull/8977) - - If you have [`@typescript-eslint/ban-types`](/rules/ban-types) manually enabled, it will no longer ban the `{}` or `object` types; use a [recommended config](/users/configs) or manually enable [`@typescript-eslint/no-empty-object-type`](https://v8--typescript-eslint.netlify.app/rules/no-empty-object-type) - - If you have [`@typescript-eslint/no-empty-interface`](/rules/no-empty-interface) manually enabled, remove that, and instead either use a [recommended config](/users/configs) or manually enable [`@typescript-eslint/no-empty-object-type`](https://v8--typescript-eslint.netlify.app/rules/no-empty-object-type) -- ⏳ [Enhancement: [ban-types] Split into default-less no-restricted-types and more targeted type ban rule(s)](https://github.com/typescript-eslint/typescript-eslint/issues/8978) - - [#9102](https://github.com/typescript-eslint/typescript-eslint/pull/9102) is still in review; we'll update this post when the migration path is settled +- [Enhancement: [ban-types] Split into default-less no-restricted-types and more targeted type ban rule(s)](https://github.com/typescript-eslint/typescript-eslint/issues/8978) +- [feat(eslint-plugin): replace ban-types with no-restricted-types, no-unsafe-function-type, no-wrapper-object-types](https://github.com/typescript-eslint/typescript-eslint/pull/9102) ### Tooling Breaking Changes diff --git a/packages/website/plugins/generated-rule-docs/index.ts b/packages/website/plugins/generated-rule-docs/index.ts index fd4d8d50efc8..c0a7265d28b4 100644 --- a/packages/website/plugins/generated-rule-docs/index.ts +++ b/packages/website/plugins/generated-rule-docs/index.ts @@ -10,7 +10,6 @@ import { insertFormattingNotice } from './insertions/insertFormattingNotice'; import { insertNewRuleReferences } from './insertions/insertNewRuleReferences'; import { insertResources } from './insertions/insertResources'; import { insertRuleDescription } from './insertions/insertRuleDescription'; -import { insertSpecialCaseOptions } from './insertions/insertSpecialCaseOptions'; import { insertWhenNotToUseIt } from './insertions/insertWhenNotToUseIt'; import { removeSourceCodeNotice } from './removeSourceCodeNotice'; @@ -35,7 +34,6 @@ export const generatedRuleDocs: Plugin = () => { ? insertBaseRuleReferences(page) : await insertNewRuleReferences(page); - insertSpecialCaseOptions(page); insertWhenNotToUseIt(page); insertResources(page); addESLintHashToCodeBlocksMeta(page, eslintrc); diff --git a/packages/website/plugins/generated-rule-docs/insertions/insertNewRuleReferences.ts b/packages/website/plugins/generated-rule-docs/insertions/insertNewRuleReferences.ts index 9d6ccdfa7be2..fcd6196feda8 100644 --- a/packages/website/plugins/generated-rule-docs/insertions/insertNewRuleReferences.ts +++ b/packages/website/plugins/generated-rule-docs/insertions/insertNewRuleReferences.ts @@ -20,14 +20,6 @@ const COMPLICATED_RULE_OPTIONS = new Set([ 'naming-convention', ]); -/** - * Rules that do funky things with their defaults and require special code - * rather than just JSON.stringify-ing their defaults blob - */ -const SPECIAL_CASE_DEFAULTS = new Map([ - ['ban-types', '[{ /* See below for default options */ }]'], -]); - const PRETTIER_CONFIG_PATH = path.resolve( __dirname, '..', @@ -190,10 +182,7 @@ function linkToConfigs(configs: string[]): mdast.Node[] { } function getRuleDefaultOptions(page: RuleDocsPage): string { - const defaults = - SPECIAL_CASE_DEFAULTS.get(page.file.stem) ?? - JSON.stringify(page.rule.defaultOptions); - + const defaults = JSON.stringify(page.rule.defaultOptions); const recommended = page.rule.meta.docs.recommended; return typeof recommended === 'object' diff --git a/packages/website/plugins/generated-rule-docs/insertions/insertSpecialCaseOptions.ts b/packages/website/plugins/generated-rule-docs/insertions/insertSpecialCaseOptions.ts deleted file mode 100644 index cacce340d634..000000000000 --- a/packages/website/plugins/generated-rule-docs/insertions/insertSpecialCaseOptions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as fs from 'fs'; -import type * as mdast from 'mdast'; -import * as path from 'path'; - -import { eslintPluginDirectory } from '../../utils/rules'; -import type { RuleDocsPage } from '../RuleDocsPage'; - -export function insertSpecialCaseOptions(page: RuleDocsPage): void { - if (page.file.stem !== 'ban-types') { - return; - } - - const detailsElement = page.children.find( - (node): node is mdast.Parent => - (node as mdast.Node & { name: string }).name === 'details' && - (node as mdast.Parent).children.length > 0 && - ((node as mdast.Parent).children[0] as { name: string }).name === - 'summary', - ); - - if (!detailsElement) { - throw new Error('Could not find default injection site in ban-types'); - } - - const defaultOptions = /^const defaultTypes.+?^\};$/msu.exec( - fs.readFileSync( - path.join(eslintPluginDirectory, 'src/rules/ban-types.ts'), - 'utf8', - ), - )?.[0]; - - if (!defaultOptions) { - throw new Error('Could not find default options for ban-types'); - } - - detailsElement.children.push({ - lang: 'ts', - type: 'code', - value: defaultOptions, - } as mdast.Code); -}