diff --git a/.cspell.json b/.cspell.json index 2dc68256f79c..93e100a70adf 100644 --- a/.cspell.json +++ b/.cspell.json @@ -49,9 +49,10 @@ "words": [ "Airbnb", "Airbnb's", - "ambiently", "allowdefaultproject", "allowdefaultprojectforfiles", + "allowforknownsafecalls", + "allowforknownsafepromises", "ambiently", "Arka", "Armano", @@ -102,6 +103,7 @@ "estree", "extrafileextensions", "falsiness", + "filespecifier", "fisker", "García", "globby", @@ -115,6 +117,7 @@ "joshuakgoldberg", "Katt", "kirkwaiblinger", + "libspecifier", "linebreaks", "Lundberg", "lzstring", @@ -140,6 +143,7 @@ "OOM", "OOMs", "oxlint", + "packagespecifier", "parameterised", "performant", "pluggable", diff --git a/docs/packages/type-utils/TypeOrValueSpecifier.mdx b/docs/packages/type-utils/TypeOrValueSpecifier.mdx new file mode 100644 index 000000000000..b9d76442406a --- /dev/null +++ b/docs/packages/type-utils/TypeOrValueSpecifier.mdx @@ -0,0 +1,135 @@ +--- +id: type-or-value-specifier +title: TypeOrValueSpecifier +--- + +Some lint rules include options to describe specific _types_ and/or _values_. +These options use a standardized format exported from the `type-utils` package, **`TypeOrValueSpecifier`**. + +`TypeOrValueSpecifier` allows three object forms of specifiers: + +- [`FileSpecifier`](#filespecifier): for types or values declared in local files +- [`LibSpecifier`](#libspecifier): for types or values declared in TypeScript's built-in lib definitions +- [`PackageSpecifier`](#packagespecifier): for types or values imported from packages + +For example, the following configuration of [`@typescript-eslint/no-floating-promises` > `allowForKnownSafeCalls`](/rules/no-floating-promises#allowforknownsafecalls) marks `node:test`'s `it` as safe using a package specifier: + +```json +{ + "@typescript-eslint/no-floating-promises": [ + "error", + { + "allowForKnownSafeCalls": [ + { "from": "package", "name": "it", "package": "node:test" } + ] + } + ] +} +``` + +Each object format requires at least: + +- `from`: which specifier to use, as `'file' | 'lib' | 'package'` +- `name`: a `string` or `string[]` for type or value name(s) to match on + +## FileSpecifier + +```ts +interface FileSpecifier { + from: 'file'; + name: string[] | string; + path?: string; +} +``` + +Describes specific types or values declared in local files. + +`path` may be used to specify a file the types or values must be declared in. +If omitted, all files will be matched. + +### FileSpecifier Examples + +Matching all types and values named `Props`: + +```json +{ "from": "file", "name": "Props" } +``` + +Matching all types and values named `Props` in `file.tsx`: + +```json +{ "from": "file", "name": "Props", "path": "file.tsx" } +``` + +## LibSpecifier + +```ts +interface LibSpecifier { + from: 'lib'; + name: string[] | string; +} +``` + +Describes specific types or values declared in TypeScript's built-in `lib.*.d.ts` ("lib") types. + +Lib types include `lib.dom.d.ts` globals such as `Window` and `lib.es*.ts` globals such as `Array`. + +### LibSpecifier Examples + +Matching all array-typed values: + +```json +{ "from": "lib", "name": "Array" } +``` + +Matching all `Promise` and `PromiseLike`-typed values: + +```json +{ "from": "lib", "name": ["Promise", "PromiseLike"] } +``` + +## PackageSpecifier + +```ts +interface PackageSpecifier { + from: 'package'; + name: string[] | string; + package: string; +} +``` + +Describes specific types or values imported from packages. + +`package` must be used to specify the package name. + +### PackageSpecifier Examples + +Matching the `SafePromise` type from `@reduxjs/toolkit`: + +```json +{ "from": "package", "name": "SafePromise", "package": "@reduxjs/toolkit" } +``` + +Matching the `describe`, `it`, and `test` values from `vitest`: + +```json +{ "from": "package", "name": ["describe", "it", "test"], "package": "vitest" } +``` + +## Universal String Specifiers + +`TypeOrValueSpecifier` also allows providing a plain string specifier to match all names regardless of declaration source. +For example, providing `"RegExp"` matches _all_ types and values named `RegExp`. + +:::danger +We strongly recommend not using universal string specifiers. +Matching _all_ names without specifying a source file, library, or package can accidentally match other types or values with a coincidentally similar name. + +Universal string specifiers will be removed in a future major version of typescript-eslint. +::: + +## Rule Options Using This Format + +- [`@typescript-eslint/no-floating-promises` > `allowForKnownSafeCalls`](/rules/no-floating-promises#allowforknownsafecalls) +- [`@typescript-eslint/no-floating-promises` > `allowForKnownSafePromises`](/rules/no-floating-promises#allowforknownsafepromises) +- [`@typescript-eslint/prefer-readonly-parameter-types` > `allow`](/rules/prefer-readonly-parameter-types/#allow) diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index bd814ac63dcd..18a84c2e0240 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -166,14 +166,9 @@ await (async function () { ### `allowForKnownSafePromises` -This option allows marking specific types as "safe" to be floating. For example, you may need to do this in the case of libraries whose APIs return Promises whose rejections are safely handled by the library. +Specific types to be marked as "safe" to be floating. For example, you may need to do this in the case of libraries whose APIs return Promises whose rejections are safely handled by the library. -This option takes an array of type specifiers to consider safe. -Each item in the array must have one of the following forms: - -- A type defined in a file (`{ from: "file", name: "Foo", path: "src/foo-file.ts" }` with `path` being an optional path relative to the project root directory) -- A type from the default library (`{ from: "lib", name: "PromiseLike" }`) -- A type from a package (`{ from: "package", name: "Foo", package: "foo-lib" }`, this also works for types defined in a typings package). +This option takes the shared [`TypeOrValueSpecifier` format](/packages/type-utils/type-or-value-specifier). Examples of code for this rule with: @@ -223,10 +218,10 @@ returnsSafePromise(); ### `allowForKnownSafeCalls` -This option allows marking specific functions as "safe" to be called to create floating Promises. +Specific functions to be marked as "safe" to be called to create floating Promises. For example, you may need to do this in the case of libraries whose APIs may be called without handling the resultant Promises. -This option takes the same array format as [`allowForKnownSafePromises`](#allowForKnownSafePromises). +This option takes the shared [`TypeOrValueSpecifier` format](/packages/type-utils/type-or-value-specifier). Examples of code for this rule with: diff --git a/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.mdx b/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.mdx index e700b429f4f6..f89d653bd63e 100644 --- a/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.mdx @@ -143,20 +143,13 @@ interface Foo { An array of type specifiers to ignore. Some complex types cannot easily be made readonly, for example the `HTMLElement` type or the `JQueryStatic` type from `@types/jquery`. This option allows you to globally disable reporting of such types. -Each item in the array must have one of the following forms: - -- A type defined in a file (`{ from: "file", name: "Foo", path: "src/foo-file.ts" }` with `path` being an optional path relative to the project root directory) -- A type from the default library (`{ from: "lib", name: "Foo" }`) -- A type from a package (`{ from: "package", name: "Foo", package: "foo-lib" }`, this also works for types defined in a typings package). - -Additionally, a type may be defined just as a simple string, which then matches the type independently of its origin. +This option takes the shared [`TypeOrValueSpecifier` format](/packages/type-utils/type-or-value-specifier). Examples of code for this rule with: ```json { "allow": [ - "$", { "from": "file", "name": "Foo" }, { "from": "lib", "name": "HTMLElement" }, { "from": "package", "name": "Bar", "package": "bar-lib" } @@ -167,7 +160,7 @@ Examples of code for this rule with: -```ts option='{"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +```ts option='{"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' interface ThisIsMutable { prop: string; } @@ -191,7 +184,7 @@ function fn2(arg: Wrapper) {} function fn3(arg: WrapperWithOther) {} ``` -```ts option='{"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +```ts option='{"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' import { Foo } from 'some-lib'; import { Bar } from 'incorrect-lib'; @@ -212,7 +205,7 @@ function fn3(arg: Bar) {} -```ts option='{"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +```ts option='{"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' interface Foo { prop: string; } @@ -229,7 +222,7 @@ function fn1(arg: Foo) {} function fn2(arg: Wrapper) {} ``` -```ts option='{"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +```ts option='{"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' import { Bar } from 'bar-lib'; interface Foo { @@ -246,7 +239,7 @@ function fn2(arg: HTMLElement) {} function fn3(arg: Bar) {} ``` -```ts option='{"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +```ts option='{"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]}' import { Foo } from './foo'; // Works because Foo is still a local type - it has to be in the same package diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-readonly-parameter-types.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-readonly-parameter-types.shot index 606039e2e781..c09a4fad60f3 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-readonly-parameter-types.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/prefer-readonly-parameter-types.shot @@ -127,7 +127,7 @@ interface Foo { exports[`Validating rule docs prefer-readonly-parameter-types.mdx code examples ESLint output 3`] = ` "Incorrect -Options: {"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} +Options: {"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} interface ThisIsMutable { prop: string; @@ -158,7 +158,7 @@ function fn3(arg: WrapperWithOther) {} exports[`Validating rule docs prefer-readonly-parameter-types.mdx code examples ESLint output 4`] = ` "Incorrect -Options: {"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} +Options: {"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} import { Foo } from 'some-lib'; import { Bar } from 'incorrect-lib'; @@ -181,7 +181,7 @@ function fn3(arg: Bar) {} exports[`Validating rule docs prefer-readonly-parameter-types.mdx code examples ESLint output 5`] = ` "Correct -Options: {"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} +Options: {"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} interface Foo { prop: string; @@ -202,7 +202,7 @@ function fn2(arg: Wrapper) {} exports[`Validating rule docs prefer-readonly-parameter-types.mdx code examples ESLint output 6`] = ` "Correct -Options: {"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} +Options: {"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} import { Bar } from 'bar-lib'; @@ -223,7 +223,7 @@ function fn3(arg: Bar) {} exports[`Validating rule docs prefer-readonly-parameter-types.mdx code examples ESLint output 7`] = ` "Correct -Options: {"allow":["$",{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} +Options: {"allow":[{"from":"file","name":"Foo"},{"from":"lib","name":"HTMLElement"},{"from":"package","name":"Bar","package":"bar-lib"}]} import { Foo } from './foo'; diff --git a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts index 097a16928ed0..090146c0c90b 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -819,6 +819,20 @@ promise().then(() => {}); }, ], }, + { + code: ` + import { it } from 'node:test'; + + it('...', () => {}); + `, + options: [ + { + allowForKnownSafeCalls: [ + { from: 'package', name: 'it', package: 'node:test' }, + ], + }, + ], + }, { code: ` interface SafePromise extends Promise { diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 2d7c5a3e6555..422e83769208 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -7,23 +7,59 @@ import { typeDeclaredInFile } from './typeOrValueSpecifiers/typeDeclaredInFile'; import { typeDeclaredInLib } from './typeOrValueSpecifiers/typeDeclaredInLib'; import { typeDeclaredInPackageDeclarationFile } from './typeOrValueSpecifiers/typeDeclaredInPackageDeclarationFile'; +/** + * Describes specific types or values declared in local files. + * See [TypeOrValueSpecifier > FileSpecifier](/packages/type-utils/type-or-value-specifier#filespecifier). + */ export interface FileSpecifier { from: 'file'; + + /** + * Type or value name(s) to match on. + */ name: string[] | string; + + /** + * A specific file the types or values must be declared in. + */ path?: string; } +/** + * Describes specific types or values declared in TypeScript's built-in lib definitions. + * See [TypeOrValueSpecifier > LibSpecifier](/packages/type-utils/type-or-value-specifier#libspecifier). + */ export interface LibSpecifier { from: 'lib'; + + /** + * Type or value name(s) to match on. + */ name: string[] | string; } +/** + * Describes specific types or values imported from packages. + * See [TypeOrValueSpecifier > PackageSpecifier](/packages/type-utils/type-or-value-specifier#packagespecifier). + */ export interface PackageSpecifier { from: 'package'; + + /** + * Type or value name(s) to match on. + */ name: string[] | string; + + /** + * Package name the type or value must be declared in. + */ package: string; } +/** + * A centralized format for rule options to describe specific _types_ and/or _values_. + * See [TypeOrValueSpecifier](/packages/type-utils/type-or-value-specifier). + */ export type TypeOrValueSpecifier = | FileSpecifier | LibSpecifier diff --git a/packages/website/sidebars/sidebar.base.js b/packages/website/sidebars/sidebar.base.js index ea42ac995d7c..b0d55ed91ffb 100644 --- a/packages/website/sidebars/sidebar.base.js +++ b/packages/website/sidebars/sidebar.base.js @@ -105,7 +105,16 @@ module.exports = { 'packages/parser', 'packages/rule-tester', 'packages/scope-manager', - 'packages/type-utils', + { + collapsible: false, + items: ['packages/type-utils/type-or-value-specifier'], + label: 'type-utils', + link: { + id: 'packages/type-utils', + type: 'doc', + }, + type: 'category', + }, { collapsible: false, items: ['packages/typescript-estree/ast-spec'], diff --git a/packages/website/src/css/custom.css b/packages/website/src/css/custom.css index 1fe7a2879640..5ef592f5f21c 100644 --- a/packages/website/src/css/custom.css +++ b/packages/website/src/css/custom.css @@ -204,6 +204,10 @@ h6 { text-decoration: underline; } +.markdown a:has(code) { + text-underline-offset: 0.25em; +} + .markdown a * { vertical-align: baseline; }