From cfded6374e1b4420215f59ab17eb9515b132cc01 Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Tue, 28 Jun 2022 01:44:38 +0300 Subject: [PATCH 01/11] feat/issue5245-negated-or-optional-chaining --- packages/eslint-plugin/README.md | 178 +++++++++--------- packages/eslint-plugin/docs/rules/README.md | 178 +++++++++--------- .../docs/rules/prefer-optional-chain.md | 12 +- .../src/rules/prefer-optional-chain.ts | 139 +++++++++++++- .../tests/rules/prefer-optional-chain.test.ts | 160 ++++++++++++++++ 5 files changed, 487 insertions(+), 180 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 60a769945183..dbf33d637b1e 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -93,95 +93,95 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int **Key**: :white_check_mark: = recommended, :lock: = strict, :wrench: = fixable, :thought_balloon: = requires type information -| Name | Description | :white_check_mark::lock: | :wrench: | :thought_balloon: | -| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------ | -------- | ----------------- | -| [`@typescript-eslint/adjacent-overload-signatures`](./docs/rules/adjacent-overload-signatures.md) | Require that member overloads be consecutive | :white_check_mark: | | | -| [`@typescript-eslint/array-type`](./docs/rules/array-type.md) | Require using either `T[]` or `Array` for arrays | :lock: | :wrench: | | -| [`@typescript-eslint/await-thenable`](./docs/rules/await-thenable.md) | Disallow awaiting a value that is not a Thenable | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/ban-ts-comment`](./docs/rules/ban-ts-comment.md) | Disallow `@ts-` comments or require descriptions after directive | :white_check_mark: | | | -| [`@typescript-eslint/ban-tslint-comment`](./docs/rules/ban-tslint-comment.md) | Disallow `// tslint:` comments | :lock: | :wrench: | | -| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Disallow certain types | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/class-literal-property-style`](./docs/rules/class-literal-property-style.md) | Enforce that literals on classes are exposed in a consistent style | :lock: | :wrench: | | -| [`@typescript-eslint/consistent-generic-constructors`](./docs/rules/consistent-generic-constructors.md) | Enforce specifying generic type arguments on type annotation or constructor name of a constructor call | :lock: | :wrench: | | -| [`@typescript-eslint/consistent-indexed-object-style`](./docs/rules/consistent-indexed-object-style.md) | Require or disallow the `Record` type | :lock: | :wrench: | | -| [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforce consistent usage of type assertions | :lock: | | | -| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Enforce type definitions to consistently use either `interface` or `type` | :lock: | :wrench: | | -| [`@typescript-eslint/consistent-type-exports`](./docs/rules/consistent-type-exports.md) | Enforce consistent usage of type exports | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/consistent-type-imports`](./docs/rules/consistent-type-imports.md) | Enforce consistent usage of type imports | | :wrench: | | -| [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | | | | -| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | :wrench: | | -| [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | -| [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | | :wrench: | | -| [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | -| [`@typescript-eslint/method-signature-style`](./docs/rules/method-signature-style.md) | Enforce using a particular method signature syntax | | :wrench: | | -| [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforce naming conventions for everything across a codebase | | | :thought_balloon: | -| [`@typescript-eslint/no-base-to-string`](./docs/rules/no-base-to-string.md) | Require `.toString()` to only be called on objects which provide useful information when stringified | :lock: | | :thought_balloon: | -| [`@typescript-eslint/no-confusing-non-null-assertion`](./docs/rules/no-confusing-non-null-assertion.md) | Disallow non-null assertion in locations that may be confusing | :lock: | :wrench: | | -| [`@typescript-eslint/no-confusing-void-expression`](./docs/rules/no-confusing-void-expression.md) | Require expressions of type void to appear in statement position | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-duplicate-enum-values`](./docs/rules/no-duplicate-enum-values.md) | Disallow duplicate enum member values | :lock: | | | -| [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow using the `delete` operator on computed key expressions | :lock: | :wrench: | | -| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow the `any` type | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Disallow classes used as namespaces | :lock: | | | -| [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Require Promise-like statements to be handled appropriately | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallow explicit type declarations for variables or parameters initialized to a number, string, or boolean | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/no-invalid-void-type`](./docs/rules/no-invalid-void-type.md) | Disallow `void` type outside of generic or return types | :lock: | | | -| [`@typescript-eslint/no-meaningless-void-operator`](./docs/rules/no-meaningless-void-operator.md) | Disallow the `void` operator except when used to discard a value | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :white_check_mark: | | | -| [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Disallow Promises in places not designed to handle them | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow custom TypeScript modules and namespaces | :white_check_mark: | | | -| [`@typescript-eslint/no-non-null-asserted-nullish-coalescing`](./docs/rules/no-non-null-asserted-nullish-coalescing.md) | Disallow non-null assertions in the left operand of a nullish coalescing operator | :lock: | | | -| [`@typescript-eslint/no-non-null-asserted-optional-chain`](./docs/rules/no-non-null-asserted-optional-chain.md) | Disallow non-null assertions after an optional chain expression | :white_check_mark: | | | -| [`@typescript-eslint/no-non-null-assertion`](./docs/rules/no-non-null-assertion.md) | Disallow non-null assertions using the `!` postfix operator | :white_check_mark: | | | -| [`@typescript-eslint/no-redundant-type-constituents`](./docs/rules/no-redundant-type-constituents.md) | Disallow members of unions and intersections that do nothing or override type information | | | :thought_balloon: | -| [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallow invocation of `require()` | | | | -| [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | :white_check_mark: | | | -| [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow type aliases | | | | -| [`@typescript-eslint/no-unnecessary-boolean-literal-compare`](./docs/rules/no-unnecessary-boolean-literal-compare.md) | Disallow unnecessary equality comparisons against boolean literals | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-condition`](./docs/rules/no-unnecessary-condition.md) | Disallow conditionals where the type is always truthy or always falsy | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Disallow unnecessary namespace qualifiers | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Disallow type arguments that are equal to the default | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Disallow type assertions that do not change the type of an expression | :white_check_mark: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-type-constraint`](./docs/rules/no-unnecessary-type-constraint.md) | Disallow unnecessary constraints on generic types | :white_check_mark: | | | -| [`@typescript-eslint/no-unsafe-argument`](./docs/rules/no-unsafe-argument.md) | Disallow calling a function with a value with type `any` | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-unsafe-assignment`](./docs/rules/no-unsafe-assignment.md) | Disallow assigning a value with type `any` to variables and properties | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallow calling a value with type `any` | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallow member access on a value with type `any` | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallow returning a value with type `any` from a function | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-useless-empty-export`](./docs/rules/no-useless-empty-export.md) | Disallow empty exports that don't change anything in a module file | | :wrench: | | -| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallow `require` statements except in import statements | :white_check_mark: | | | -| [`@typescript-eslint/non-nullable-type-assertion-style`](./docs/rules/non-nullable-type-assertion-style.md) | Enforce non-null assertions over explicit type casts | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/parameter-properties`](./docs/rules/parameter-properties.md) | Require or disallow parameter properties in class constructors | | | | -| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Enforce the use of `as const` over literal type | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/prefer-enum-initializers`](./docs/rules/prefer-enum-initializers.md) | Require each enum member value to be explicitly initialized | | | | -| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Enforce the use of `for-of` loop over the standard `for` loop where possible | :lock: | | | -| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Enforce using function types instead of interfaces with call signatures | :lock: | :wrench: | | -| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-literal-enum-member`](./docs/rules/prefer-literal-enum-member.md) | Require all enum members to be literal values | :lock: | | | -| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require using `namespace` keyword over `module` keyword to declare custom TypeScript modules | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce using the nullish coalescing operator instead of logical chaining | :lock: | | :thought_balloon: | -| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Enforce using concise optional chain expressions instead of chained logical ands | :lock: | | | -| [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Require private members to be marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-readonly-parameter-types`](./docs/rules/prefer-readonly-parameter-types.md) | Require function parameters to be typed as `readonly` to prevent accidental mutation of inputs | | | :thought_balloon: | -| [`@typescript-eslint/prefer-reduce-type-parameter`](./docs/rules/prefer-reduce-type-parameter.md) | Enforce using type parameter when calling `Array#reduce` instead of casting | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce `RegExp#exec` over `String#match` if no global flag is provided | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-return-this-type`](./docs/rules/prefer-return-this-type.md) | Enforce that `this` is used when only `this` type is returned | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce using `String#startsWith` and `String#endsWith` over other equivalent methods of checking substrings | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-ts-expect-error`](./docs/rules/prefer-ts-expect-error.md) | Enforce using `@ts-expect-error` over `@ts-ignore` | :lock: | :wrench: | | -| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Require any function or method that returns a Promise to be marked async | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Require `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | -| [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | Require both operands of addition to have type `number` or `string` | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of `string` type | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/sort-type-union-intersection-members`](./docs/rules/sort-type-union-intersection-members.md) | Enforce members of a type union/intersection to be sorted alphabetically | | :wrench: | | -| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Disallow certain types in boolean expressions | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/switch-exhaustiveness-check`](./docs/rules/switch-exhaustiveness-check.md) | Require switch-case statements to be exhaustive with union type | | | :thought_balloon: | -| [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Disallow certain triple slash directives in favor of ES6-style import declarations | :white_check_mark: | | | -| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | | :wrench: | | -| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Require type annotations in certain places | | | | -| [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforce unbound methods are called with their expected scope | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Disallow two overloads that could be unified into one with a union or an optional/rest parameter | :lock: | | | +| Name | Description | :white_check_mark::lock: | :wrench: | :thought_balloon: | +| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------ | -------- | ----------------- | +| [`@typescript-eslint/adjacent-overload-signatures`](./docs/rules/adjacent-overload-signatures.md) | Require that member overloads be consecutive | :white_check_mark: | | | +| [`@typescript-eslint/array-type`](./docs/rules/array-type.md) | Require using either `T[]` or `Array` for arrays | :lock: | :wrench: | | +| [`@typescript-eslint/await-thenable`](./docs/rules/await-thenable.md) | Disallow awaiting a value that is not a Thenable | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/ban-ts-comment`](./docs/rules/ban-ts-comment.md) | Disallow `@ts-` comments or require descriptions after directive | :white_check_mark: | | | +| [`@typescript-eslint/ban-tslint-comment`](./docs/rules/ban-tslint-comment.md) | Disallow `// tslint:` comments | :lock: | :wrench: | | +| [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Disallow certain types | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/class-literal-property-style`](./docs/rules/class-literal-property-style.md) | Enforce that literals on classes are exposed in a consistent style | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-generic-constructors`](./docs/rules/consistent-generic-constructors.md) | Enforce specifying generic type arguments on type annotation or constructor name of a constructor call | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-indexed-object-style`](./docs/rules/consistent-indexed-object-style.md) | Require or disallow the `Record` type | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforce consistent usage of type assertions | :lock: | | | +| [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Enforce type definitions to consistently use either `interface` or `type` | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-type-exports`](./docs/rules/consistent-type-exports.md) | Enforce consistent usage of type exports | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/consistent-type-imports`](./docs/rules/consistent-type-imports.md) | Enforce consistent usage of type imports | | :wrench: | | +| [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | | | | +| [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | :wrench: | | +| [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | +| [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | | :wrench: | | +| [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | +| [`@typescript-eslint/method-signature-style`](./docs/rules/method-signature-style.md) | Enforce using a particular method signature syntax | | :wrench: | | +| [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforce naming conventions for everything across a codebase | | | :thought_balloon: | +| [`@typescript-eslint/no-base-to-string`](./docs/rules/no-base-to-string.md) | Require `.toString()` to only be called on objects which provide useful information when stringified | :lock: | | :thought_balloon: | +| [`@typescript-eslint/no-confusing-non-null-assertion`](./docs/rules/no-confusing-non-null-assertion.md) | Disallow non-null assertion in locations that may be confusing | :lock: | :wrench: | | +| [`@typescript-eslint/no-confusing-void-expression`](./docs/rules/no-confusing-void-expression.md) | Require expressions of type void to appear in statement position | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-duplicate-enum-values`](./docs/rules/no-duplicate-enum-values.md) | Disallow duplicate enum member values | :lock: | | | +| [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow using the `delete` operator on computed key expressions | :lock: | :wrench: | | +| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow the `any` type | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Disallow classes used as namespaces | :lock: | | | +| [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Require Promise-like statements to be handled appropriately | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallow explicit type declarations for variables or parameters initialized to a number, string, or boolean | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/no-invalid-void-type`](./docs/rules/no-invalid-void-type.md) | Disallow `void` type outside of generic or return types | :lock: | | | +| [`@typescript-eslint/no-meaningless-void-operator`](./docs/rules/no-meaningless-void-operator.md) | Disallow the `void` operator except when used to discard a value | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :white_check_mark: | | | +| [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Disallow Promises in places not designed to handle them | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow custom TypeScript modules and namespaces | :white_check_mark: | | | +| [`@typescript-eslint/no-non-null-asserted-nullish-coalescing`](./docs/rules/no-non-null-asserted-nullish-coalescing.md) | Disallow non-null assertions in the left operand of a nullish coalescing operator | :lock: | | | +| [`@typescript-eslint/no-non-null-asserted-optional-chain`](./docs/rules/no-non-null-asserted-optional-chain.md) | Disallow non-null assertions after an optional chain expression | :white_check_mark: | | | +| [`@typescript-eslint/no-non-null-assertion`](./docs/rules/no-non-null-assertion.md) | Disallow non-null assertions using the `!` postfix operator | :white_check_mark: | | | +| [`@typescript-eslint/no-redundant-type-constituents`](./docs/rules/no-redundant-type-constituents.md) | Disallow members of unions and intersections that do nothing or override type information | | | :thought_balloon: | +| [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallow invocation of `require()` | | | | +| [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | :white_check_mark: | | | +| [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow type aliases | | | | +| [`@typescript-eslint/no-unnecessary-boolean-literal-compare`](./docs/rules/no-unnecessary-boolean-literal-compare.md) | Disallow unnecessary equality comparisons against boolean literals | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-condition`](./docs/rules/no-unnecessary-condition.md) | Disallow conditionals where the type is always truthy or always falsy | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Disallow unnecessary namespace qualifiers | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Disallow type arguments that are equal to the default | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Disallow type assertions that do not change the type of an expression | :white_check_mark: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-constraint`](./docs/rules/no-unnecessary-type-constraint.md) | Disallow unnecessary constraints on generic types | :white_check_mark: | | | +| [`@typescript-eslint/no-unsafe-argument`](./docs/rules/no-unsafe-argument.md) | Disallow calling a function with a value with type `any` | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-assignment`](./docs/rules/no-unsafe-assignment.md) | Disallow assigning a value with type `any` to variables and properties | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallow calling a value with type `any` | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallow member access on a value with type `any` | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallow returning a value with type `any` from a function | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-useless-empty-export`](./docs/rules/no-useless-empty-export.md) | Disallow empty exports that don't change anything in a module file | | :wrench: | | +| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallow `require` statements except in import statements | :white_check_mark: | | | +| [`@typescript-eslint/non-nullable-type-assertion-style`](./docs/rules/non-nullable-type-assertion-style.md) | Enforce non-null assertions over explicit type casts | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/parameter-properties`](./docs/rules/parameter-properties.md) | Require or disallow parameter properties in class constructors | | | | +| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Enforce the use of `as const` over literal type | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/prefer-enum-initializers`](./docs/rules/prefer-enum-initializers.md) | Require each enum member value to be explicitly initialized | | | | +| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Enforce the use of `for-of` loop over the standard `for` loop where possible | :lock: | | | +| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Enforce using function types instead of interfaces with call signatures | :lock: | :wrench: | | +| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-literal-enum-member`](./docs/rules/prefer-literal-enum-member.md) | Require all enum members to be literal values | :lock: | | | +| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require using `namespace` keyword over `module` keyword to declare custom TypeScript modules | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce using the nullish coalescing operator instead of logical chaining | :lock: | | :thought_balloon: | +| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects | :lock: | | | +| [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Require private members to be marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-readonly-parameter-types`](./docs/rules/prefer-readonly-parameter-types.md) | Require function parameters to be typed as `readonly` to prevent accidental mutation of inputs | | | :thought_balloon: | +| [`@typescript-eslint/prefer-reduce-type-parameter`](./docs/rules/prefer-reduce-type-parameter.md) | Enforce using type parameter when calling `Array#reduce` instead of casting | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce `RegExp#exec` over `String#match` if no global flag is provided | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-return-this-type`](./docs/rules/prefer-return-this-type.md) | Enforce that `this` is used when only `this` type is returned | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce using `String#startsWith` and `String#endsWith` over other equivalent methods of checking substrings | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-ts-expect-error`](./docs/rules/prefer-ts-expect-error.md) | Enforce using `@ts-expect-error` over `@ts-ignore` | :lock: | :wrench: | | +| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Require any function or method that returns a Promise to be marked async | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Require `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | +| [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | Require both operands of addition to have type `number` or `string` | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of `string` type | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/sort-type-union-intersection-members`](./docs/rules/sort-type-union-intersection-members.md) | Enforce members of a type union/intersection to be sorted alphabetically | | :wrench: | | +| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Disallow certain types in boolean expressions | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/switch-exhaustiveness-check`](./docs/rules/switch-exhaustiveness-check.md) | Require switch-case statements to be exhaustive with union type | | | :thought_balloon: | +| [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Disallow certain triple slash directives in favor of ES6-style import declarations | :white_check_mark: | | | +| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | | :wrench: | | +| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Require type annotations in certain places | | | | +| [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforce unbound methods are called with their expected scope | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Disallow two overloads that could be unified into one with a union or an optional/rest parameter | :lock: | | | diff --git a/packages/eslint-plugin/docs/rules/README.md b/packages/eslint-plugin/docs/rules/README.md index 805f126f8228..10d7e7db5690 100644 --- a/packages/eslint-plugin/docs/rules/README.md +++ b/packages/eslint-plugin/docs/rules/README.md @@ -15,95 +15,95 @@ See [Configs](/docs/linting/configs) for how to enable recommended rules using c **Key**: :white_check_mark: = recommended, :lock: = strict, :wrench: = fixable, :thought_balloon: = requires type information -| Name | Description | :white_check_mark::lock: | :wrench: | :thought_balloon: | -| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------ | -------- | ----------------- | -| [`@typescript-eslint/adjacent-overload-signatures`](./adjacent-overload-signatures.md) | Require that member overloads be consecutive | :white_check_mark: | | | -| [`@typescript-eslint/array-type`](./array-type.md) | Require using either `T[]` or `Array` for arrays | :lock: | :wrench: | | -| [`@typescript-eslint/await-thenable`](./await-thenable.md) | Disallow awaiting a value that is not a Thenable | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/ban-ts-comment`](./ban-ts-comment.md) | Disallow `@ts-` comments or require descriptions after directive | :white_check_mark: | | | -| [`@typescript-eslint/ban-tslint-comment`](./ban-tslint-comment.md) | Disallow `// tslint:` comments | :lock: | :wrench: | | -| [`@typescript-eslint/ban-types`](./ban-types.md) | Disallow certain types | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/class-literal-property-style`](./class-literal-property-style.md) | Enforce that literals on classes are exposed in a consistent style | :lock: | :wrench: | | -| [`@typescript-eslint/consistent-generic-constructors`](./consistent-generic-constructors.md) | Enforce specifying generic type arguments on type annotation or constructor name of a constructor call | :lock: | :wrench: | | -| [`@typescript-eslint/consistent-indexed-object-style`](./consistent-indexed-object-style.md) | Require or disallow the `Record` type | :lock: | :wrench: | | -| [`@typescript-eslint/consistent-type-assertions`](./consistent-type-assertions.md) | Enforce consistent usage of type assertions | :lock: | | | -| [`@typescript-eslint/consistent-type-definitions`](./consistent-type-definitions.md) | Enforce type definitions to consistently use either `interface` or `type` | :lock: | :wrench: | | -| [`@typescript-eslint/consistent-type-exports`](./consistent-type-exports.md) | Enforce consistent usage of type exports | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/consistent-type-imports`](./consistent-type-imports.md) | Enforce consistent usage of type imports | | :wrench: | | -| [`@typescript-eslint/explicit-function-return-type`](./explicit-function-return-type.md) | Require explicit return types on functions and class methods | | | | -| [`@typescript-eslint/explicit-member-accessibility`](./explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | :wrench: | | -| [`@typescript-eslint/explicit-module-boundary-types`](./explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | -| [`@typescript-eslint/member-delimiter-style`](./member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | | :wrench: | | -| [`@typescript-eslint/member-ordering`](./member-ordering.md) | Require a consistent member declaration order | | | | -| [`@typescript-eslint/method-signature-style`](./method-signature-style.md) | Enforce using a particular method signature syntax | | :wrench: | | -| [`@typescript-eslint/naming-convention`](./naming-convention.md) | Enforce naming conventions for everything across a codebase | | | :thought_balloon: | -| [`@typescript-eslint/no-base-to-string`](./no-base-to-string.md) | Require `.toString()` to only be called on objects which provide useful information when stringified | :lock: | | :thought_balloon: | -| [`@typescript-eslint/no-confusing-non-null-assertion`](./no-confusing-non-null-assertion.md) | Disallow non-null assertion in locations that may be confusing | :lock: | :wrench: | | -| [`@typescript-eslint/no-confusing-void-expression`](./no-confusing-void-expression.md) | Require expressions of type void to appear in statement position | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-duplicate-enum-values`](./no-duplicate-enum-values.md) | Disallow duplicate enum member values | :lock: | | | -| [`@typescript-eslint/no-dynamic-delete`](./no-dynamic-delete.md) | Disallow using the `delete` operator on computed key expressions | :lock: | :wrench: | | -| [`@typescript-eslint/no-empty-interface`](./no-empty-interface.md) | Disallow the declaration of empty interfaces | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/no-explicit-any`](./no-explicit-any.md) | Disallow the `any` type | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/no-extra-non-null-assertion`](./no-extra-non-null-assertion.md) | Disallow extra non-null assertion | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/no-extraneous-class`](./no-extraneous-class.md) | Disallow classes used as namespaces | :lock: | | | -| [`@typescript-eslint/no-floating-promises`](./no-floating-promises.md) | Require Promise-like statements to be handled appropriately | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-for-in-array`](./no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-inferrable-types`](./no-inferrable-types.md) | Disallow explicit type declarations for variables or parameters initialized to a number, string, or boolean | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/no-invalid-void-type`](./no-invalid-void-type.md) | Disallow `void` type outside of generic or return types | :lock: | | | -| [`@typescript-eslint/no-meaningless-void-operator`](./no-meaningless-void-operator.md) | Disallow the `void` operator except when used to discard a value | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-misused-new`](./no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :white_check_mark: | | | -| [`@typescript-eslint/no-misused-promises`](./no-misused-promises.md) | Disallow Promises in places not designed to handle them | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-namespace`](./no-namespace.md) | Disallow custom TypeScript modules and namespaces | :white_check_mark: | | | -| [`@typescript-eslint/no-non-null-asserted-nullish-coalescing`](./no-non-null-asserted-nullish-coalescing.md) | Disallow non-null assertions in the left operand of a nullish coalescing operator | :lock: | | | -| [`@typescript-eslint/no-non-null-asserted-optional-chain`](./no-non-null-asserted-optional-chain.md) | Disallow non-null assertions after an optional chain expression | :white_check_mark: | | | -| [`@typescript-eslint/no-non-null-assertion`](./no-non-null-assertion.md) | Disallow non-null assertions using the `!` postfix operator | :white_check_mark: | | | -| [`@typescript-eslint/no-redundant-type-constituents`](./no-redundant-type-constituents.md) | Disallow members of unions and intersections that do nothing or override type information | | | :thought_balloon: | -| [`@typescript-eslint/no-require-imports`](./no-require-imports.md) | Disallow invocation of `require()` | | | | -| [`@typescript-eslint/no-this-alias`](./no-this-alias.md) | Disallow aliasing `this` | :white_check_mark: | | | -| [`@typescript-eslint/no-type-alias`](./no-type-alias.md) | Disallow type aliases | | | | -| [`@typescript-eslint/no-unnecessary-boolean-literal-compare`](./no-unnecessary-boolean-literal-compare.md) | Disallow unnecessary equality comparisons against boolean literals | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-condition`](./no-unnecessary-condition.md) | Disallow conditionals where the type is always truthy or always falsy | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-qualifier`](./no-unnecessary-qualifier.md) | Disallow unnecessary namespace qualifiers | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-type-arguments`](./no-unnecessary-type-arguments.md) | Disallow type arguments that are equal to the default | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-type-assertion`](./no-unnecessary-type-assertion.md) | Disallow type assertions that do not change the type of an expression | :white_check_mark: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-unnecessary-type-constraint`](./no-unnecessary-type-constraint.md) | Disallow unnecessary constraints on generic types | :white_check_mark: | | | -| [`@typescript-eslint/no-unsafe-argument`](./no-unsafe-argument.md) | Disallow calling a function with a value with type `any` | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-unsafe-assignment`](./no-unsafe-assignment.md) | Disallow assigning a value with type `any` to variables and properties | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-unsafe-call`](./no-unsafe-call.md) | Disallow calling a value with type `any` | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-unsafe-member-access`](./no-unsafe-member-access.md) | Disallow member access on a value with type `any` | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-unsafe-return`](./no-unsafe-return.md) | Disallow returning a value with type `any` from a function | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/no-useless-empty-export`](./no-useless-empty-export.md) | Disallow empty exports that don't change anything in a module file | | :wrench: | | -| [`@typescript-eslint/no-var-requires`](./no-var-requires.md) | Disallow `require` statements except in import statements | :white_check_mark: | | | -| [`@typescript-eslint/non-nullable-type-assertion-style`](./non-nullable-type-assertion-style.md) | Enforce non-null assertions over explicit type casts | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/parameter-properties`](./parameter-properties.md) | Require or disallow parameter properties in class constructors | | | | -| [`@typescript-eslint/prefer-as-const`](./prefer-as-const.md) | Enforce the use of `as const` over literal type | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/prefer-enum-initializers`](./prefer-enum-initializers.md) | Require each enum member value to be explicitly initialized | | | | -| [`@typescript-eslint/prefer-for-of`](./prefer-for-of.md) | Enforce the use of `for-of` loop over the standard `for` loop where possible | :lock: | | | -| [`@typescript-eslint/prefer-function-type`](./prefer-function-type.md) | Enforce using function types instead of interfaces with call signatures | :lock: | :wrench: | | -| [`@typescript-eslint/prefer-includes`](./prefer-includes.md) | Enforce `includes` method over `indexOf` method | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-literal-enum-member`](./prefer-literal-enum-member.md) | Require all enum members to be literal values | :lock: | | | -| [`@typescript-eslint/prefer-namespace-keyword`](./prefer-namespace-keyword.md) | Require using `namespace` keyword over `module` keyword to declare custom TypeScript modules | :white_check_mark: | :wrench: | | -| [`@typescript-eslint/prefer-nullish-coalescing`](./prefer-nullish-coalescing.md) | Enforce using the nullish coalescing operator instead of logical chaining | :lock: | | :thought_balloon: | -| [`@typescript-eslint/prefer-optional-chain`](./prefer-optional-chain.md) | Enforce using concise optional chain expressions instead of chained logical ands | :lock: | | | -| [`@typescript-eslint/prefer-readonly`](./prefer-readonly.md) | Require private members to be marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-readonly-parameter-types`](./prefer-readonly-parameter-types.md) | Require function parameters to be typed as `readonly` to prevent accidental mutation of inputs | | | :thought_balloon: | -| [`@typescript-eslint/prefer-reduce-type-parameter`](./prefer-reduce-type-parameter.md) | Enforce using type parameter when calling `Array#reduce` instead of casting | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-regexp-exec`](./prefer-regexp-exec.md) | Enforce `RegExp#exec` over `String#match` if no global flag is provided | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-return-this-type`](./prefer-return-this-type.md) | Enforce that `this` is used when only `this` type is returned | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-string-starts-ends-with`](./prefer-string-starts-ends-with.md) | Enforce using `String#startsWith` and `String#endsWith` over other equivalent methods of checking substrings | :lock: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/prefer-ts-expect-error`](./prefer-ts-expect-error.md) | Enforce using `@ts-expect-error` over `@ts-ignore` | :lock: | :wrench: | | -| [`@typescript-eslint/promise-function-async`](./promise-function-async.md) | Require any function or method that returns a Promise to be marked async | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/require-array-sort-compare`](./require-array-sort-compare.md) | Require `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | -| [`@typescript-eslint/restrict-plus-operands`](./restrict-plus-operands.md) | Require both operands of addition to have type `number` or `string` | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/restrict-template-expressions`](./restrict-template-expressions.md) | Enforce template literal expressions to be of `string` type | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/sort-type-union-intersection-members`](./sort-type-union-intersection-members.md) | Enforce members of a type union/intersection to be sorted alphabetically | | :wrench: | | -| [`@typescript-eslint/strict-boolean-expressions`](./strict-boolean-expressions.md) | Disallow certain types in boolean expressions | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/switch-exhaustiveness-check`](./switch-exhaustiveness-check.md) | Require switch-case statements to be exhaustive with union type | | | :thought_balloon: | -| [`@typescript-eslint/triple-slash-reference`](./triple-slash-reference.md) | Disallow certain triple slash directives in favor of ES6-style import declarations | :white_check_mark: | | | -| [`@typescript-eslint/type-annotation-spacing`](./type-annotation-spacing.md) | Require consistent spacing around type annotations | | :wrench: | | -| [`@typescript-eslint/typedef`](./typedef.md) | Require type annotations in certain places | | | | -| [`@typescript-eslint/unbound-method`](./unbound-method.md) | Enforce unbound methods are called with their expected scope | :white_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/unified-signatures`](./unified-signatures.md) | Disallow two overloads that could be unified into one with a union or an optional/rest parameter | :lock: | | | +| Name | Description | :white_check_mark::lock: | :wrench: | :thought_balloon: | +| ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------ | -------- | ----------------- | +| [`@typescript-eslint/adjacent-overload-signatures`](./adjacent-overload-signatures.md) | Require that member overloads be consecutive | :white_check_mark: | | | +| [`@typescript-eslint/array-type`](./array-type.md) | Require using either `T[]` or `Array` for arrays | :lock: | :wrench: | | +| [`@typescript-eslint/await-thenable`](./await-thenable.md) | Disallow awaiting a value that is not a Thenable | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/ban-ts-comment`](./ban-ts-comment.md) | Disallow `@ts-` comments or require descriptions after directive | :white_check_mark: | | | +| [`@typescript-eslint/ban-tslint-comment`](./ban-tslint-comment.md) | Disallow `// tslint:` comments | :lock: | :wrench: | | +| [`@typescript-eslint/ban-types`](./ban-types.md) | Disallow certain types | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/class-literal-property-style`](./class-literal-property-style.md) | Enforce that literals on classes are exposed in a consistent style | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-generic-constructors`](./consistent-generic-constructors.md) | Enforce specifying generic type arguments on type annotation or constructor name of a constructor call | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-indexed-object-style`](./consistent-indexed-object-style.md) | Require or disallow the `Record` type | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-type-assertions`](./consistent-type-assertions.md) | Enforce consistent usage of type assertions | :lock: | | | +| [`@typescript-eslint/consistent-type-definitions`](./consistent-type-definitions.md) | Enforce type definitions to consistently use either `interface` or `type` | :lock: | :wrench: | | +| [`@typescript-eslint/consistent-type-exports`](./consistent-type-exports.md) | Enforce consistent usage of type exports | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/consistent-type-imports`](./consistent-type-imports.md) | Enforce consistent usage of type imports | | :wrench: | | +| [`@typescript-eslint/explicit-function-return-type`](./explicit-function-return-type.md) | Require explicit return types on functions and class methods | | | | +| [`@typescript-eslint/explicit-member-accessibility`](./explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | :wrench: | | +| [`@typescript-eslint/explicit-module-boundary-types`](./explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | +| [`@typescript-eslint/member-delimiter-style`](./member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | | :wrench: | | +| [`@typescript-eslint/member-ordering`](./member-ordering.md) | Require a consistent member declaration order | | | | +| [`@typescript-eslint/method-signature-style`](./method-signature-style.md) | Enforce using a particular method signature syntax | | :wrench: | | +| [`@typescript-eslint/naming-convention`](./naming-convention.md) | Enforce naming conventions for everything across a codebase | | | :thought_balloon: | +| [`@typescript-eslint/no-base-to-string`](./no-base-to-string.md) | Require `.toString()` to only be called on objects which provide useful information when stringified | :lock: | | :thought_balloon: | +| [`@typescript-eslint/no-confusing-non-null-assertion`](./no-confusing-non-null-assertion.md) | Disallow non-null assertion in locations that may be confusing | :lock: | :wrench: | | +| [`@typescript-eslint/no-confusing-void-expression`](./no-confusing-void-expression.md) | Require expressions of type void to appear in statement position | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-duplicate-enum-values`](./no-duplicate-enum-values.md) | Disallow duplicate enum member values | :lock: | | | +| [`@typescript-eslint/no-dynamic-delete`](./no-dynamic-delete.md) | Disallow using the `delete` operator on computed key expressions | :lock: | :wrench: | | +| [`@typescript-eslint/no-empty-interface`](./no-empty-interface.md) | Disallow the declaration of empty interfaces | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/no-explicit-any`](./no-explicit-any.md) | Disallow the `any` type | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/no-extra-non-null-assertion`](./no-extra-non-null-assertion.md) | Disallow extra non-null assertion | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/no-extraneous-class`](./no-extraneous-class.md) | Disallow classes used as namespaces | :lock: | | | +| [`@typescript-eslint/no-floating-promises`](./no-floating-promises.md) | Require Promise-like statements to be handled appropriately | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-for-in-array`](./no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-inferrable-types`](./no-inferrable-types.md) | Disallow explicit type declarations for variables or parameters initialized to a number, string, or boolean | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/no-invalid-void-type`](./no-invalid-void-type.md) | Disallow `void` type outside of generic or return types | :lock: | | | +| [`@typescript-eslint/no-meaningless-void-operator`](./no-meaningless-void-operator.md) | Disallow the `void` operator except when used to discard a value | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-misused-new`](./no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :white_check_mark: | | | +| [`@typescript-eslint/no-misused-promises`](./no-misused-promises.md) | Disallow Promises in places not designed to handle them | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-namespace`](./no-namespace.md) | Disallow custom TypeScript modules and namespaces | :white_check_mark: | | | +| [`@typescript-eslint/no-non-null-asserted-nullish-coalescing`](./no-non-null-asserted-nullish-coalescing.md) | Disallow non-null assertions in the left operand of a nullish coalescing operator | :lock: | | | +| [`@typescript-eslint/no-non-null-asserted-optional-chain`](./no-non-null-asserted-optional-chain.md) | Disallow non-null assertions after an optional chain expression | :white_check_mark: | | | +| [`@typescript-eslint/no-non-null-assertion`](./no-non-null-assertion.md) | Disallow non-null assertions using the `!` postfix operator | :white_check_mark: | | | +| [`@typescript-eslint/no-redundant-type-constituents`](./no-redundant-type-constituents.md) | Disallow members of unions and intersections that do nothing or override type information | | | :thought_balloon: | +| [`@typescript-eslint/no-require-imports`](./no-require-imports.md) | Disallow invocation of `require()` | | | | +| [`@typescript-eslint/no-this-alias`](./no-this-alias.md) | Disallow aliasing `this` | :white_check_mark: | | | +| [`@typescript-eslint/no-type-alias`](./no-type-alias.md) | Disallow type aliases | | | | +| [`@typescript-eslint/no-unnecessary-boolean-literal-compare`](./no-unnecessary-boolean-literal-compare.md) | Disallow unnecessary equality comparisons against boolean literals | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-condition`](./no-unnecessary-condition.md) | Disallow conditionals where the type is always truthy or always falsy | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-qualifier`](./no-unnecessary-qualifier.md) | Disallow unnecessary namespace qualifiers | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-arguments`](./no-unnecessary-type-arguments.md) | Disallow type arguments that are equal to the default | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-assertion`](./no-unnecessary-type-assertion.md) | Disallow type assertions that do not change the type of an expression | :white_check_mark: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-type-constraint`](./no-unnecessary-type-constraint.md) | Disallow unnecessary constraints on generic types | :white_check_mark: | | | +| [`@typescript-eslint/no-unsafe-argument`](./no-unsafe-argument.md) | Disallow calling a function with a value with type `any` | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-assignment`](./no-unsafe-assignment.md) | Disallow assigning a value with type `any` to variables and properties | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-call`](./no-unsafe-call.md) | Disallow calling a value with type `any` | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-member-access`](./no-unsafe-member-access.md) | Disallow member access on a value with type `any` | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-unsafe-return`](./no-unsafe-return.md) | Disallow returning a value with type `any` from a function | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/no-useless-empty-export`](./no-useless-empty-export.md) | Disallow empty exports that don't change anything in a module file | | :wrench: | | +| [`@typescript-eslint/no-var-requires`](./no-var-requires.md) | Disallow `require` statements except in import statements | :white_check_mark: | | | +| [`@typescript-eslint/non-nullable-type-assertion-style`](./non-nullable-type-assertion-style.md) | Enforce non-null assertions over explicit type casts | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/parameter-properties`](./parameter-properties.md) | Require or disallow parameter properties in class constructors | | | | +| [`@typescript-eslint/prefer-as-const`](./prefer-as-const.md) | Enforce the use of `as const` over literal type | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/prefer-enum-initializers`](./prefer-enum-initializers.md) | Require each enum member value to be explicitly initialized | | | | +| [`@typescript-eslint/prefer-for-of`](./prefer-for-of.md) | Enforce the use of `for-of` loop over the standard `for` loop where possible | :lock: | | | +| [`@typescript-eslint/prefer-function-type`](./prefer-function-type.md) | Enforce using function types instead of interfaces with call signatures | :lock: | :wrench: | | +| [`@typescript-eslint/prefer-includes`](./prefer-includes.md) | Enforce `includes` method over `indexOf` method | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-literal-enum-member`](./prefer-literal-enum-member.md) | Require all enum members to be literal values | :lock: | | | +| [`@typescript-eslint/prefer-namespace-keyword`](./prefer-namespace-keyword.md) | Require using `namespace` keyword over `module` keyword to declare custom TypeScript modules | :white_check_mark: | :wrench: | | +| [`@typescript-eslint/prefer-nullish-coalescing`](./prefer-nullish-coalescing.md) | Enforce using the nullish coalescing operator instead of logical chaining | :lock: | | :thought_balloon: | +| [`@typescript-eslint/prefer-optional-chain`](./prefer-optional-chain.md) | Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects | :lock: | | | +| [`@typescript-eslint/prefer-readonly`](./prefer-readonly.md) | Require private members to be marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-readonly-parameter-types`](./prefer-readonly-parameter-types.md) | Require function parameters to be typed as `readonly` to prevent accidental mutation of inputs | | | :thought_balloon: | +| [`@typescript-eslint/prefer-reduce-type-parameter`](./prefer-reduce-type-parameter.md) | Enforce using type parameter when calling `Array#reduce` instead of casting | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-regexp-exec`](./prefer-regexp-exec.md) | Enforce `RegExp#exec` over `String#match` if no global flag is provided | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-return-this-type`](./prefer-return-this-type.md) | Enforce that `this` is used when only `this` type is returned | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-string-starts-ends-with`](./prefer-string-starts-ends-with.md) | Enforce using `String#startsWith` and `String#endsWith` over other equivalent methods of checking substrings | :lock: | :wrench: | :thought_balloon: | +| [`@typescript-eslint/prefer-ts-expect-error`](./prefer-ts-expect-error.md) | Enforce using `@ts-expect-error` over `@ts-ignore` | :lock: | :wrench: | | +| [`@typescript-eslint/promise-function-async`](./promise-function-async.md) | Require any function or method that returns a Promise to be marked async | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/require-array-sort-compare`](./require-array-sort-compare.md) | Require `Array#sort` calls to always provide a `compareFunction` | | | :thought_balloon: | +| [`@typescript-eslint/restrict-plus-operands`](./restrict-plus-operands.md) | Require both operands of addition to have type `number` or `string` | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/restrict-template-expressions`](./restrict-template-expressions.md) | Enforce template literal expressions to be of `string` type | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/sort-type-union-intersection-members`](./sort-type-union-intersection-members.md) | Enforce members of a type union/intersection to be sorted alphabetically | | :wrench: | | +| [`@typescript-eslint/strict-boolean-expressions`](./strict-boolean-expressions.md) | Disallow certain types in boolean expressions | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/switch-exhaustiveness-check`](./switch-exhaustiveness-check.md) | Require switch-case statements to be exhaustive with union type | | | :thought_balloon: | +| [`@typescript-eslint/triple-slash-reference`](./triple-slash-reference.md) | Disallow certain triple slash directives in favor of ES6-style import declarations | :white_check_mark: | | | +| [`@typescript-eslint/type-annotation-spacing`](./type-annotation-spacing.md) | Require consistent spacing around type annotations | | :wrench: | | +| [`@typescript-eslint/typedef`](./typedef.md) | Require type annotations in certain places | | | | +| [`@typescript-eslint/unbound-method`](./unbound-method.md) | Enforce unbound methods are called with their expected scope | :white_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/unified-signatures`](./unified-signatures.md) | Disallow two overloads that could be unified into one with a union or an optional/rest parameter | :lock: | | | diff --git a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md index c2b196a04a47..31b6ea659a38 100644 --- a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md +++ b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md @@ -1,6 +1,6 @@ # `prefer-optional-chain` -Enforces using concise optional chain expressions instead of chained logical ands. +Enforces using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects. TypeScript 3.7 added support for the optional chain operator. This operator allows you to safely access properties and methods on objects when they are potentially `null` or `undefined`. @@ -61,9 +61,15 @@ foo && foo.a && foo.a.b && foo.a.b.c; foo && foo['a'] && foo['a'].b && foo['a'].b.c; foo && foo.a && foo.a.b && foo.a.b.method && foo.a.b.method(); +// With empty objects (((foo || {}).a || {}).b || {}).c; (((foo || {})['a'] || {}).b || {}).c; +// With negated `or`s +!foo || !foo.bar; +!foo || !foo[bar]; +!foo || !foo.bar || !foo.bar.baz || !foo.bar.baz(); + // this rule also supports converting chained strict nullish checks: foo && foo.a != null && @@ -81,6 +87,10 @@ foo?.['a']?.b?.c; foo?.a?.b?.method?.(); foo?.a?.b?.c?.d?.e; + +!foo?.bar; +!foo?.[bar]; +!foo?.bar?.baz?.(); ``` **Note:** there are a few edge cases where this rule will false positive. Use your best judgement when evaluating reported errors. diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 04cd7e649933..ede814e9f18c 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -34,7 +34,7 @@ export default util.createRule({ type: 'suggestion', docs: { description: - 'Enforce using concise optional chain expressions instead of chained logical ands', + 'Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects', recommended: 'strict', suggestion: true, }, @@ -111,6 +111,143 @@ export default util.createRule({ ], }); }, + [[ + 'LogicalExpression[operator="||"] > UnaryExpression[operator="!"] > Identifier', + 'LogicalExpression[operator="||"] > UnaryExpression[operator="!"] > MemberExpression', + 'LogicalExpression[operator="||"] > UnaryExpression[operator="!"] > ChainExpression > MemberExpression', + ].join(',')]( + initialIdentifierOrNotEqualsExpr: + | TSESTree.BinaryExpression + | TSESTree.Identifier + | TSESTree.MemberExpression, + ): void { + // selector guarantees this cast + const initialExpression = ( + initialIdentifierOrNotEqualsExpr.parent?.type === + AST_NODE_TYPES.ChainExpression + ? initialIdentifierOrNotEqualsExpr.parent.parent + : initialIdentifierOrNotEqualsExpr.parent + )?.parent as TSESTree.LogicalExpression; + + if ( + initialExpression.left.type !== AST_NODE_TYPES.UnaryExpression || + initialExpression.left.argument !== initialIdentifierOrNotEqualsExpr + ) { + // the node(identifier or member expression) is not the deepest left node + return; + } + if (!isValidChainTarget(initialIdentifierOrNotEqualsExpr, true)) { + return; + } + + // walk up the tree to figure out how many logical expressions we can include + let previous: TSESTree.LogicalExpression = initialExpression; + let current: TSESTree.Node = initialExpression; + let previousLeftText = getText(initialIdentifierOrNotEqualsExpr); + let optionallyChainedCode = previousLeftText; + let expressionCount = 1; + while (current.type === AST_NODE_TYPES.LogicalExpression) { + if ( + current.right.type !== AST_NODE_TYPES.UnaryExpression || + !isValidChainTarget( + current.right.argument, + // only allow identifiers for the first chain - foo && foo() + expressionCount === 1, + ) + ) { + break; + } + + const leftText = previousLeftText; + const rightText = getText(current.right.argument); + // can't just use startsWith because of cases like foo && fooBar.baz; + const matchRegex = new RegExp( + `^${ + // escape regex characters + leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + }[^a-zA-Z0-9_$]`, + ); + if ( + !matchRegex.test(rightText) && + // handle redundant cases like foo.bar && foo.bar + leftText !== rightText + ) { + break; + } + + // omit weird doubled up expression that make no sense like foo.bar && foo.bar + if (rightText !== leftText) { + expressionCount += 1; + previousLeftText = rightText; + + /* + Diff the left and right text to construct the fix string + There are the following cases: + + 1) + rightText === 'foo.bar.baz.buzz' + leftText === 'foo.bar.baz' + diff === '.buzz' + + 2) + rightText === 'foo.bar.baz.buzz()' + leftText === 'foo.bar.baz' + diff === '.buzz()' + + 3) + rightText === 'foo.bar.baz.buzz()' + leftText === 'foo.bar.baz.buzz' + diff === '()' + + 4) + rightText === 'foo.bar.baz[buzz]' + leftText === 'foo.bar.baz' + diff === '[buzz]' + + 5) + rightText === 'foo.bar.baz?.buzz' + leftText === 'foo.bar.baz' + diff === '?.buzz' + */ + const diff = rightText.replace(leftText, ''); + if (diff.startsWith('?')) { + // item was "pre optional chained" + optionallyChainedCode += diff; + } else { + const needsDot = diff.startsWith('(') || diff.startsWith('['); + optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`; + } + } + + previous = current; + current = util.nullThrows( + current.parent, + util.NullThrowsReasons.MissingParent, + ); + } + + if (expressionCount > 1) { + if (previous.right.type === AST_NODE_TYPES.BinaryExpression) { + // case like foo && foo.bar !== someValue + optionallyChainedCode += ` ${ + previous.right.operator + } ${sourceCode.getText(previous.right.right)}`; + } + + context.report({ + node: previous, + messageId: 'preferOptionalChain', + suggest: [ + { + messageId: 'optionalChainSuggest', + fix: (fixer): TSESLint.RuleFix[] => [ + fixer.replaceText(previous, `!${optionallyChainedCode}`), + ], + }, + ], + }); + } + }, [[ 'LogicalExpression[operator="&&"] > Identifier', 'LogicalExpression[operator="&&"] > MemberExpression', diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts index 2b004e9d7a9c..a2003443ea66 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts @@ -122,6 +122,11 @@ const baseCases = [ code: 'foo.bar && foo.bar?.() && foo.bar?.().baz', output: 'foo.bar?.()?.baz', }, + // TODO: deepest left node already pre-optional chained + // { + // code: 'foo?.bar && foo.bar?.() && foo.bar?.().baz', + // output: 'foo?.bar?.()?.baz', + // }, ].map( c => ({ @@ -146,6 +151,13 @@ const baseCases = [ ruleTester.run('prefer-optional-chain', rule, { valid: [ + '!a || !b;', + '!a || a.b;', + '!a && a.b;', + '!a && !a.b;', + '!a.b || a.b?.();', + '!a.b || a.b();', + 'foo || {};', 'foo || ({} as any);', '(foo || {})?.bar;', @@ -1143,5 +1155,153 @@ foo?.bar(/* comment */a, }, ], }, + ...baseCases.map(c => ({ + ...c, + code: c.code.replace(/foo/g, '!foo').replace(/&&/g, '||'), + errors: [ + { + ...c.errors[0], + suggestions: [ + { + ...c.errors[0].suggestions![0], + output: `!${c.errors[0].suggestions![0].output}`, + }, + ], + }, + ], + })), + { + code: '!a.b || !a.b();', + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!a.b?.();', + }, + ], + }, + ], + }, + { + code: '!foo.bar || !foo.bar.baz;', + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!foo.bar?.baz;', + }, + ], + }, + ], + }, + { + code: '!foo[bar] || !foo[bar]?.[baz];', + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!foo[bar]?.[baz];', + }, + ], + }, + ], + }, + { + code: '!foo || !foo?.bar.baz;', + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!foo?.bar.baz;', + }, + ], + }, + ], + }, + // two errors + { + code: noFormat`(!foo || !foo.bar || !foo.bar.baz) && (!baz || !baz.bar || !baz.bar.foo);`, + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`(!foo?.bar?.baz) && (!baz || !baz.bar || !baz.bar.foo);`, + }, + ], + }, + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`(!foo || !foo.bar || !foo.bar.baz) && (!baz?.bar?.foo);`, + }, + ], + }, + ], + }, + /* // Not sure if should support + { + code: '!(foo.bar != undefined) || !foo.bar.baz;', + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!foo.bar?.baz;', + }, + ], + }, + ], + }, */ + /* // Not sure if should support + { + code: 'foo.bar == undefined || !foo.bar.baz;', + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!foo.bar?.baz;', + }, + ], + }, + ], + }, */ + // TODO: deepest left node already pre-optional chained + // { + // code: '!foo?.bar || !foo?.bar.baz;', + // output: null, + // errors: [ + // { + // messageId: 'preferOptionalChain', + // suggestions: [ + // { + // messageId: 'optionalChainSuggest', + // output: '!foo?.bar?.baz;', + // }, + // ], + // }, + // ], + // }, ], }); From 931274c17cfeaf9442404795e2ac71dc5815f280 Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Wed, 29 Jun 2022 05:22:09 +0300 Subject: [PATCH 02/11] fix lint errors with the updated rule --- .../src/rules/explicit-function-return-type.ts | 2 +- packages/eslint-plugin/src/rules/no-useless-constructor.ts | 7 ++----- packages/eslint-plugin/src/rules/require-await.ts | 2 +- packages/eslint-plugin/src/rules/return-await.ts | 2 +- packages/typescript-estree/src/convert.ts | 4 ++-- packages/utils/src/eslint-utils/getParserServices.ts | 3 +-- packages/website/src/components/RulesTable/index.tsx | 2 +- 7 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts index 1f69834d4784..d09ae5402b89 100644 --- a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts +++ b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts @@ -78,7 +78,7 @@ export default util.createRule({ | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration, ): boolean { - if (!options.allowedNames || !options.allowedNames.length) { + if (!options.allowedNames?.length) { return false; } diff --git a/packages/eslint-plugin/src/rules/no-useless-constructor.ts b/packages/eslint-plugin/src/rules/no-useless-constructor.ts index 10e55cfbe0eb..9414a535fa1d 100644 --- a/packages/eslint-plugin/src/rules/no-useless-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-useless-constructor.ts @@ -34,11 +34,8 @@ function checkAccessibility(node: TSESTree.MethodDefinition): boolean { * Check if method is not unless due to typescript parameter properties */ function checkParams(node: TSESTree.MethodDefinition): boolean { - return ( - !node.value.params || - !node.value.params.some( - param => param.type === AST_NODE_TYPES.TSParameterProperty, - ) + return !node.value.params?.some( + param => param.type === AST_NODE_TYPES.TSParameterProperty, ); } diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index 56067fe554a4..f01ece89afe0 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -104,7 +104,7 @@ export default util.createRule({ * function and the delegate is `true` */ function markAsHasDelegateGen(node: TSESTree.YieldExpression): void { - if (!scopeInfo || !scopeInfo.isGen || !node.argument) { + if (!scopeInfo?.isGen || !node.argument) { return; } diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index fbb78708bd44..bd16a9e4f531 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -306,7 +306,7 @@ export default util.createRule({ }, ReturnStatement(node): void { const scopeInfo = scopeInfoStack[scopeInfoStack.length - 1]; - if (!scopeInfo || !scopeInfo.hasAsync || !node.argument) { + if (!scopeInfo?.hasAsync || !node.argument) { return; } findPossiblyReturnedNodes(node.argument).forEach(node => { diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index be09a2b387db..81c17185743f 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -402,7 +402,7 @@ export class Converter { private convertParameters( parameters: ts.NodeArray, ): TSESTree.Parameter[] { - if (!parameters || !parameters.length) { + if (!parameters?.length) { return []; } return parameters.map(param => { @@ -688,7 +688,7 @@ export class Converter { result: TSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration, modifiers?: ts.ModifiersArray, ): void { - if (!modifiers || !modifiers.length) { + if (!modifiers?.length) { return; } const remainingModifiers: TSESTree.Modifier[] = []; diff --git a/packages/utils/src/eslint-utils/getParserServices.ts b/packages/utils/src/eslint-utils/getParserServices.ts index 27ad579272b9..f385b64fecb1 100644 --- a/packages/utils/src/eslint-utils/getParserServices.ts +++ b/packages/utils/src/eslint-utils/getParserServices.ts @@ -17,8 +17,7 @@ function getParserServices< // backwards compatibility check // old versions of the parser would not return any parserServices unless parserOptions.project was set if ( - !context.parserServices || - !context.parserServices.program || + !context.parserServices?.program || !context.parserServices.esTreeNodeToTSNodeMap || !context.parserServices.tsNodeToESTreeNodeMap ) { diff --git a/packages/website/src/components/RulesTable/index.tsx b/packages/website/src/components/RulesTable/index.tsx index 6629637a44b4..de763cda30ef 100644 --- a/packages/website/src/components/RulesTable/index.tsx +++ b/packages/website/src/components/RulesTable/index.tsx @@ -15,7 +15,7 @@ function interpolateCode(text: string): (JSX.Element | string)[] | string { } function RuleRow({ rule }: { rule: RulesMeta[number] }): JSX.Element | null { - if (!rule.docs || !rule.docs.url) { + if (!rule.docs?.url) { return null; } return ( From fdd99e03d8994c66062f98506afae774f24f1a53 Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Sat, 2 Jul 2022 17:14:19 +0300 Subject: [PATCH 03/11] reduce duplication --- .../src/rules/prefer-optional-chain.ts | 312 ++++++++++-------- 1 file changed, 172 insertions(+), 140 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index ede814e9f18c..4052145cb423 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -175,78 +175,31 @@ export default util.createRule({ break; } - // omit weird doubled up expression that make no sense like foo.bar && foo.bar - if (rightText !== leftText) { - expressionCount += 1; - previousLeftText = rightText; - - /* - Diff the left and right text to construct the fix string - There are the following cases: - - 1) - rightText === 'foo.bar.baz.buzz' - leftText === 'foo.bar.baz' - diff === '.buzz' - - 2) - rightText === 'foo.bar.baz.buzz()' - leftText === 'foo.bar.baz' - diff === '.buzz()' - - 3) - rightText === 'foo.bar.baz.buzz()' - leftText === 'foo.bar.baz.buzz' - diff === '()' - - 4) - rightText === 'foo.bar.baz[buzz]' - leftText === 'foo.bar.baz' - diff === '[buzz]' - - 5) - rightText === 'foo.bar.baz?.buzz' - leftText === 'foo.bar.baz' - diff === '?.buzz' - */ - const diff = rightText.replace(leftText, ''); - if (diff.startsWith('?')) { - // item was "pre optional chained" - optionallyChainedCode += diff; - } else { - const needsDot = diff.startsWith('(') || diff.startsWith('['); - optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`; - } - } - - previous = current; - current = util.nullThrows( - current.parent, - util.NullThrowsReasons.MissingParent, - ); + ({ + expressionCount, + previousLeftText, + optionallyChainedCode, + previous, + current, + } = normalizeRepeatingPatterns( + rightText, + leftText, + expressionCount, + previousLeftText, + optionallyChainedCode, + previous, + current, + )); } - if (expressionCount > 1) { - if (previous.right.type === AST_NODE_TYPES.BinaryExpression) { - // case like foo && foo.bar !== someValue - optionallyChainedCode += ` ${ - previous.right.operator - } ${sourceCode.getText(previous.right.right)}`; - } - - context.report({ - node: previous, - messageId: 'preferOptionalChain', - suggest: [ - { - messageId: 'optionalChainSuggest', - fix: (fixer): TSESLint.RuleFix[] => [ - fixer.replaceText(previous, `!${optionallyChainedCode}`), - ], - }, - ], - }); - } + reportIfMoreThanOne({ + expressionCount, + previous, + optionallyChainedCode, + sourceCode, + context, + shouldHandleChainedAnds: false, + }); }, [[ 'LogicalExpression[operator="&&"] > Identifier', @@ -310,78 +263,31 @@ export default util.createRule({ break; } - // omit weird doubled up expression that make no sense like foo.bar && foo.bar - if (rightText !== leftText) { - expressionCount += 1; - previousLeftText = rightText; - - /* - Diff the left and right text to construct the fix string - There are the following cases: - - 1) - rightText === 'foo.bar.baz.buzz' - leftText === 'foo.bar.baz' - diff === '.buzz' - - 2) - rightText === 'foo.bar.baz.buzz()' - leftText === 'foo.bar.baz' - diff === '.buzz()' - - 3) - rightText === 'foo.bar.baz.buzz()' - leftText === 'foo.bar.baz.buzz' - diff === '()' - - 4) - rightText === 'foo.bar.baz[buzz]' - leftText === 'foo.bar.baz' - diff === '[buzz]' - - 5) - rightText === 'foo.bar.baz?.buzz' - leftText === 'foo.bar.baz' - diff === '?.buzz' - */ - const diff = rightText.replace(leftText, ''); - if (diff.startsWith('?')) { - // item was "pre optional chained" - optionallyChainedCode += diff; - } else { - const needsDot = diff.startsWith('(') || diff.startsWith('['); - optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`; - } - } - - previous = current; - current = util.nullThrows( - current.parent, - util.NullThrowsReasons.MissingParent, - ); + ({ + expressionCount, + previousLeftText, + optionallyChainedCode, + previous, + current, + } = normalizeRepeatingPatterns( + rightText, + leftText, + expressionCount, + previousLeftText, + optionallyChainedCode, + previous, + current, + )); } - if (expressionCount > 1) { - if (previous.right.type === AST_NODE_TYPES.BinaryExpression) { - // case like foo && foo.bar !== someValue - optionallyChainedCode += ` ${ - previous.right.operator - } ${sourceCode.getText(previous.right.right)}`; - } - - context.report({ - node: previous, - messageId: 'preferOptionalChain', - suggest: [ - { - messageId: 'optionalChainSuggest', - fix: (fixer): TSESLint.RuleFix[] => [ - fixer.replaceText(previous, optionallyChainedCode), - ], - }, - ], - }); - } + reportIfMoreThanOne({ + expressionCount, + previous, + optionallyChainedCode, + sourceCode, + context, + shouldHandleChainedAnds: true, + }); }, }; @@ -531,6 +437,132 @@ const ALLOWED_NON_COMPUTED_PROP_TYPES: ReadonlySet = new Set([ AST_NODE_TYPES.Identifier, ]); +interface ReportIfMoreThanOneOptions { + expressionCount: number; + previous: TSESTree.LogicalExpression; + optionallyChainedCode: string; + sourceCode: Readonly; + context: Readonly< + TSESLint.RuleContext< + 'preferOptionalChain' | 'optionalChainSuggest', + never[] + > + >; + shouldHandleChainedAnds: boolean; +} + +function reportIfMoreThanOne({ + expressionCount, + previous, + optionallyChainedCode, + sourceCode, + context, + shouldHandleChainedAnds, +}: ReportIfMoreThanOneOptions): void { + if (expressionCount > 1) { + if ( + shouldHandleChainedAnds && + previous.right.type === AST_NODE_TYPES.BinaryExpression + ) { + // case like foo && foo.bar !== someValue + optionallyChainedCode += ` ${ + previous.right.operator + } ${sourceCode.getText(previous.right.right)}`; + } + + context.report({ + node: previous, + messageId: 'preferOptionalChain', + suggest: [ + { + messageId: 'optionalChainSuggest', + fix: (fixer): TSESLint.RuleFix[] => [ + fixer.replaceText( + previous, + `${shouldHandleChainedAnds ? '' : '!'}${optionallyChainedCode}`, + ), + ], + }, + ], + }); + } +} + +interface NormalizedPattern { + expressionCount: number; + previousLeftText: string; + optionallyChainedCode: string; + previous: TSESTree.LogicalExpression; + current: TSESTree.Node; +} + +function normalizeRepeatingPatterns( + rightText: string, + leftText: string, + expressionCount: number, + previousLeftText: string, + optionallyChainedCode: string, + previous: TSESTree.Node, + current: TSESTree.Node, +): NormalizedPattern { + // omit weird doubled up expression that make no sense like foo.bar && foo.bar + if (rightText !== leftText) { + expressionCount += 1; + previousLeftText = rightText; + + /* + Diff the left and right text to construct the fix string + There are the following cases: + + 1) + rightText === 'foo.bar.baz.buzz' + leftText === 'foo.bar.baz' + diff === '.buzz' + + 2) + rightText === 'foo.bar.baz.buzz()' + leftText === 'foo.bar.baz' + diff === '.buzz()' + + 3) + rightText === 'foo.bar.baz.buzz()' + leftText === 'foo.bar.baz.buzz' + diff === '()' + + 4) + rightText === 'foo.bar.baz[buzz]' + leftText === 'foo.bar.baz' + diff === '[buzz]' + + 5) + rightText === 'foo.bar.baz?.buzz' + leftText === 'foo.bar.baz' + diff === '?.buzz' + */ + const diff = rightText.replace(leftText, ''); + if (diff.startsWith('?')) { + // item was "pre optional chained" + optionallyChainedCode += diff; + } else { + const needsDot = diff.startsWith('(') || diff.startsWith('['); + optionallyChainedCode += `?${needsDot ? '.' : ''}${diff}`; + } + } + + previous = current as TSESTree.LogicalExpression; + current = util.nullThrows( + current.parent, + util.NullThrowsReasons.MissingParent, + ); + return { + expressionCount, + previousLeftText, + optionallyChainedCode, + previous, + current, + }; +} + function isValidChainTarget( node: TSESTree.Node, allowIdentifier: boolean, From 92f4285f07b72b549d10d26ef19ad78eab0721d5 Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Sat, 2 Jul 2022 21:43:07 +0300 Subject: [PATCH 04/11] coverage --- .../src/rules/prefer-optional-chain.ts | 6 +- .../tests/rules/prefer-optional-chain.test.ts | 61 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 4052145cb423..d3f41200e2d0 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -136,9 +136,9 @@ export default util.createRule({ // the node(identifier or member expression) is not the deepest left node return; } - if (!isValidChainTarget(initialIdentifierOrNotEqualsExpr, true)) { - return; - } + // if (!isValidChainTarget(initialIdentifierOrNotEqualsExpr, true)) { + // return; + // } // walk up the tree to figure out how many logical expressions we can include let previous: TSESTree.LogicalExpression = initialExpression; diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts index a2003443ea66..263b593ac1ce 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts @@ -64,6 +64,16 @@ const baseCases = [ code: 'foo && foo[bar].baz && foo[bar].baz.buzz', output: 'foo?.[bar].baz?.buzz', }, + // case with a property access in computed property + { + code: 'foo && foo[bar.baz] && foo[bar.baz].buzz', + output: 'foo?.[bar.baz]?.buzz', + }, + // case with this keyword + { + code: 'foo[this.bar] && foo[this.bar].baz', + output: 'foo[this.bar]?.baz', + }, // chained calls { code: 'foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz()', @@ -157,6 +167,7 @@ ruleTester.run('prefer-optional-chain', rule, { '!a && !a.b;', '!a.b || a.b?.();', '!a.b || a.b();', + '!foo() || !foo().bar;', 'foo || {};', 'foo || ({} as any);', @@ -174,6 +185,7 @@ ruleTester.run('prefer-optional-chain', rule, { 'foo ?? {};', '(foo ?? {})?.bar;', 'foo ||= bar ?? {};', + 'foo && bar;', 'foo && foo;', 'foo || bar;', @@ -191,6 +203,9 @@ ruleTester.run('prefer-optional-chain', rule, { 'foo && foo[bar as string] && foo[bar as string].baz;', 'foo && foo[1 + 2] && foo[1 + 2].baz;', 'foo && foo[typeof bar] && foo[typeof bar].baz;', + // currently do not handle 'this' as the first part of a chain + 'this && this.foo;', + '!this || !this.foo;', ], invalid: [ ...baseCases, @@ -458,6 +473,22 @@ foo?.bar(/* comment */a, }, ], }, + // case with this keyword at the start of expression + { + code: 'this.bar && this.bar.baz;', + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'this.bar?.baz;', + }, + ], + }, + ], + }, // other weird cases { code: 'foo && foo?.();', @@ -1155,6 +1186,20 @@ foo?.bar(/* comment */a, }, ], }, + { + code: '(this || {}).foo;', + errors: [ + { + messageId: 'optionalChainSuggest', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: 'this?.foo;', + }, + ], + }, + ], + }, ...baseCases.map(c => ({ ...c, code: c.code.replace(/foo/g, '!foo').replace(/&&/g, '||'), @@ -1170,6 +1215,22 @@ foo?.bar(/* comment */a, }, ], })), + // case with this keyword at the start of expression + { + code: '!this.bar || !this.bar.baz;', + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: '!this.bar?.baz;', + }, + ], + }, + ], + }, { code: '!a.b || !a.b();', output: null, From e63bd2fa5027e1a9276387231ee9de5e16afa92d Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Sat, 2 Jul 2022 23:48:16 +0300 Subject: [PATCH 05/11] coverage --- packages/eslint-plugin/src/rules/require-await.ts | 1 + packages/eslint-plugin/tests/rules/return-await.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index f01ece89afe0..257843d89548 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -104,6 +104,7 @@ export default util.createRule({ * function and the delegate is `true` */ function markAsHasDelegateGen(node: TSESTree.YieldExpression): void { + /* istanbul ignore next */ if (!scopeInfo?.isGen || !node.argument) { return; } diff --git a/packages/eslint-plugin/tests/rules/return-await.test.ts b/packages/eslint-plugin/tests/rules/return-await.test.ts index 43db87ef8195..a1aecbeb90f4 100644 --- a/packages/eslint-plugin/tests/rules/return-await.test.ts +++ b/packages/eslint-plugin/tests/rules/return-await.test.ts @@ -15,6 +15,7 @@ const ruleTester = new RuleTester({ // default rule is in-try-catch ruleTester.run('return-await', rule, { valid: [ + 'return;', // No function in scope, so behave like return in a commonjs module ` function test() { return; From 393d182e4ce7345c2a459ad57957840af69ba001 Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Sat, 2 Jul 2022 23:55:14 +0300 Subject: [PATCH 06/11] coverage --- packages/eslint-plugin/src/rules/no-useless-constructor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-constructor.ts b/packages/eslint-plugin/src/rules/no-useless-constructor.ts index 9414a535fa1d..760b468dc47c 100644 --- a/packages/eslint-plugin/src/rules/no-useless-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-useless-constructor.ts @@ -34,7 +34,7 @@ function checkAccessibility(node: TSESTree.MethodDefinition): boolean { * Check if method is not unless due to typescript parameter properties */ function checkParams(node: TSESTree.MethodDefinition): boolean { - return !node.value.params?.some( + return !node.value.params.some( param => param.type === AST_NODE_TYPES.TSParameterProperty, ); } From aa0063fd8a9eda05495975610d45cd942b48f550 Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Fri, 29 Jul 2022 14:35:24 +0300 Subject: [PATCH 07/11] CR fixes --- packages/eslint-plugin/src/rules/prefer-optional-chain.ts | 8 ++------ packages/eslint-plugin/src/rules/require-await.ts | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index e1a721f4b44d..55a3be23ebda 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -117,17 +117,16 @@ export default util.createRule({ 'LogicalExpression[operator="||"] > UnaryExpression[operator="!"] > ChainExpression > MemberExpression', ].join(',')]( initialIdentifierOrNotEqualsExpr: - | TSESTree.BinaryExpression | TSESTree.Identifier | TSESTree.MemberExpression, ): void { // selector guarantees this cast const initialExpression = ( - initialIdentifierOrNotEqualsExpr.parent?.type === + initialIdentifierOrNotEqualsExpr.parent!.type === AST_NODE_TYPES.ChainExpression ? initialIdentifierOrNotEqualsExpr.parent.parent : initialIdentifierOrNotEqualsExpr.parent - )?.parent as TSESTree.LogicalExpression; + )!.parent as TSESTree.LogicalExpression; if ( initialExpression.left.type !== AST_NODE_TYPES.UnaryExpression || @@ -136,9 +135,6 @@ export default util.createRule({ // the node(identifier or member expression) is not the deepest left node return; } - // if (!isValidChainTarget(initialIdentifierOrNotEqualsExpr, true)) { - // return; - // } // walk up the tree to figure out how many logical expressions we can include let previous: TSESTree.LogicalExpression = initialExpression; diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index 257843d89548..f01ece89afe0 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -104,7 +104,6 @@ export default util.createRule({ * function and the delegate is `true` */ function markAsHasDelegateGen(node: TSESTree.YieldExpression): void { - /* istanbul ignore next */ if (!scopeInfo?.isGen || !node.argument) { return; } From 265a1e8a72045168e1f76e068cfb6493fa72caa0 Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Fri, 29 Jul 2022 16:10:25 +0300 Subject: [PATCH 08/11] refactor more duplications --- .../src/rules/prefer-optional-chain.ts | 119 ++++++++++-------- 1 file changed, 70 insertions(+), 49 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 55a3be23ebda..e614b5047dd4 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -143,31 +143,17 @@ export default util.createRule({ let optionallyChainedCode = previousLeftText; let expressionCount = 1; while (current.type === AST_NODE_TYPES.LogicalExpression) { - if ( - current.right.type !== AST_NODE_TYPES.UnaryExpression || - !isValidChainTarget( - current.right.argument, - // only allow identifiers for the first chain - foo && foo() - expressionCount === 1, - ) - ) { - break; - } - - const leftText = previousLeftText; - const rightText = getText(current.right.argument); - // can't just use startsWith because of cases like foo && fooBar.baz; - const matchRegex = new RegExp( - `^${ - // escape regex characters - leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - }[^a-zA-Z0-9_$]`, - ); - if ( - !matchRegex.test(rightText) && - // handle redundant cases like foo.bar && foo.bar - leftText !== rightText - ) { + const rightNode = (current.right as TSESTree.UnaryExpression) + .argument; + const isUnary = current.right.type !== AST_NODE_TYPES.UnaryExpression; + const { leftText, rightText, shouldBreak } = + breakIfRedundantOrInvalid({ + expressionCount, + previousLeftText, + rightNode, + isUnary, + }); + if (shouldBreak) { break; } @@ -232,30 +218,16 @@ export default util.createRule({ let optionallyChainedCode = previousLeftText; let expressionCount = 1; while (current.type === AST_NODE_TYPES.LogicalExpression) { - if ( - !isValidChainTarget( - current.right, - // only allow identifiers for the first chain - foo && foo() - expressionCount === 1, - ) - ) { - break; - } - - const leftText = previousLeftText; - const rightText = getText(current.right); - // can't just use startsWith because of cases like foo && fooBar.baz; - const matchRegex = new RegExp( - `^${ - // escape regex characters - leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - }[^a-zA-Z0-9_$]`, - ); - if ( - !matchRegex.test(rightText) && - // handle redundant cases like foo.bar && foo.bar - leftText !== rightText - ) { + const rightNode = current.right; + const isUnary = false; + const { leftText, rightText, shouldBreak } = + breakIfRedundantOrInvalid({ + expressionCount, + previousLeftText, + rightNode, + isUnary, + }); + if (shouldBreak) { break; } @@ -287,6 +259,55 @@ export default util.createRule({ }, }; + interface BreakIfRedundantOrInvalidResult { + leftText: string; + rightText: string; + shouldBreak: boolean; + } + + function breakIfRedundantOrInvalid({ + expressionCount, + previousLeftText, + rightNode, + isUnary, + }: { + expressionCount: number; + previousLeftText: string; + rightNode: TSESTree.Expression; + isUnary: boolean; + }): BreakIfRedundantOrInvalidResult { + let shouldBreak = false; + if ( + isUnary || + !isValidChainTarget( + rightNode, + // only allow identifiers for the first chain - foo && foo() + expressionCount === 1, + ) + ) { + shouldBreak = true; + return { shouldBreak, leftText: '', rightText: '' }; + } + + const leftText = previousLeftText; + const rightText = getText(rightNode); + // can't just use startsWith because of cases like foo && fooBar.baz; + const matchRegex = new RegExp( + `^${ + // escape regex characters + leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + }[^a-zA-Z0-9_$]`, + ); + if ( + !matchRegex.test(rightText) && + // handle redundant cases like foo.bar && foo.bar + leftText !== rightText + ) { + shouldBreak = true; + } + return { shouldBreak, leftText, rightText }; + } + function getText(node: ValidChainTarget): string { if (node.type === AST_NODE_TYPES.BinaryExpression) { return getText( From f946a99e56f3a539971993b103eeb2ef9dcba7dc Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Fri, 29 Jul 2022 16:44:43 +0300 Subject: [PATCH 09/11] better --- .../src/rules/prefer-optional-chain.ts | 88 +++++++++---------- 1 file changed, 40 insertions(+), 48 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index e614b5047dd4..ed9ffbea60df 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -143,16 +143,20 @@ export default util.createRule({ let optionallyChainedCode = previousLeftText; let expressionCount = 1; while (current.type === AST_NODE_TYPES.LogicalExpression) { - const rightNode = (current.right as TSESTree.UnaryExpression) - .argument; - const isUnary = current.right.type !== AST_NODE_TYPES.UnaryExpression; - const { leftText, rightText, shouldBreak } = - breakIfRedundantOrInvalid({ - expressionCount, - previousLeftText, - rightNode, - isUnary, - }); + if ( + current.right.type !== AST_NODE_TYPES.UnaryExpression || + !isValidChainTarget( + current.right.argument, + // only allow unary '!' with identifiers for the first chain - !foo || !foo() + expressionCount === 1, + ) + ) { + break; + } + const { rightText, shouldBreak } = breakIfInvalid({ + rightNode: current.right.argument, + previousLeftText, + }); if (shouldBreak) { break; } @@ -165,7 +169,6 @@ export default util.createRule({ current, } = normalizeRepeatingPatterns( rightText, - leftText, expressionCount, previousLeftText, optionallyChainedCode, @@ -218,15 +221,19 @@ export default util.createRule({ let optionallyChainedCode = previousLeftText; let expressionCount = 1; while (current.type === AST_NODE_TYPES.LogicalExpression) { - const rightNode = current.right; - const isUnary = false; - const { leftText, rightText, shouldBreak } = - breakIfRedundantOrInvalid({ - expressionCount, - previousLeftText, - rightNode, - isUnary, - }); + if ( + !isValidChainTarget( + current.right, + // only allow identifiers for the first chain - foo && foo() + expressionCount === 1, + ) + ) { + break; + } + const { rightText, shouldBreak } = breakIfInvalid({ + rightNode: current.right as ValidChainTarget, + previousLeftText, + }); if (shouldBreak) { break; } @@ -239,7 +246,6 @@ export default util.createRule({ current, } = normalizeRepeatingPatterns( rightText, - leftText, expressionCount, previousLeftText, optionallyChainedCode, @@ -259,53 +265,39 @@ export default util.createRule({ }, }; - interface BreakIfRedundantOrInvalidResult { + interface BreakIfInvalidResult { leftText: string; rightText: string; shouldBreak: boolean; } - function breakIfRedundantOrInvalid({ - expressionCount, + interface BreakIfInvalidOptions { + previousLeftText: string; + rightNode: ValidChainTarget; + } + + function breakIfInvalid({ previousLeftText, rightNode, - isUnary, - }: { - expressionCount: number; - previousLeftText: string; - rightNode: TSESTree.Expression; - isUnary: boolean; - }): BreakIfRedundantOrInvalidResult { + }: BreakIfInvalidOptions): BreakIfInvalidResult { let shouldBreak = false; - if ( - isUnary || - !isValidChainTarget( - rightNode, - // only allow identifiers for the first chain - foo && foo() - expressionCount === 1, - ) - ) { - shouldBreak = true; - return { shouldBreak, leftText: '', rightText: '' }; - } - const leftText = previousLeftText; const rightText = getText(rightNode); // can't just use startsWith because of cases like foo && fooBar.baz; const matchRegex = new RegExp( `^${ // escape regex characters - leftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + previousLeftText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') }[^a-zA-Z0-9_$]`, ); if ( !matchRegex.test(rightText) && // handle redundant cases like foo.bar && foo.bar - leftText !== rightText + previousLeftText !== rightText ) { shouldBreak = true; } - return { shouldBreak, leftText, rightText }; + return { shouldBreak, leftText: previousLeftText, rightText }; } function getText(node: ValidChainTarget): string { @@ -515,15 +507,15 @@ interface NormalizedPattern { function normalizeRepeatingPatterns( rightText: string, - leftText: string, expressionCount: number, previousLeftText: string, optionallyChainedCode: string, previous: TSESTree.Node, current: TSESTree.Node, ): NormalizedPattern { + const leftText = previousLeftText; // omit weird doubled up expression that make no sense like foo.bar && foo.bar - if (rightText !== leftText) { + if (rightText !== previousLeftText) { expressionCount += 1; previousLeftText = rightText; From 078bd0d86020f0fdb0e516472cf3cbf14afe00db Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Fri, 29 Jul 2022 17:03:33 +0300 Subject: [PATCH 10/11] Remove commented " == undefined" tests in the "negated or" rule --- .../tests/rules/prefer-optional-chain.test.ts | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts index 263b593ac1ce..596235375f6f 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts @@ -1316,38 +1316,6 @@ foo?.bar(/* comment */a, }, ], }, - /* // Not sure if should support - { - code: '!(foo.bar != undefined) || !foo.bar.baz;', - output: null, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: [ - { - messageId: 'optionalChainSuggest', - output: '!foo.bar?.baz;', - }, - ], - }, - ], - }, */ - /* // Not sure if should support - { - code: 'foo.bar == undefined || !foo.bar.baz;', - output: null, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: [ - { - messageId: 'optionalChainSuggest', - output: '!foo.bar?.baz;', - }, - ], - }, - ], - }, */ // TODO: deepest left node already pre-optional chained // { // code: '!foo?.bar || !foo?.bar.baz;', From cf8c41ba79372a0e6513e847e09ae95a3861acb4 Mon Sep 17 00:00:00 2001 From: Omri Luzon Date: Fri, 19 Aug 2022 17:08:24 +0300 Subject: [PATCH 11/11] fix description conflict in md --- packages/eslint-plugin/docs/rules/prefer-optional-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md index 638ef41e3cd4..16b6d31883fa 100644 --- a/packages/eslint-plugin/docs/rules/prefer-optional-chain.md +++ b/packages/eslint-plugin/docs/rules/prefer-optional-chain.md @@ -1,5 +1,5 @@ --- -description: 'Enforce using concise optional chain expressions instead of chained logical ands.' +description: 'Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects.' --- > 🛑 This file is source code, not the primary documentation location! 🛑