From 674bd98c52541a969a62c84e4a47f9f453d6aa75 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 20 Aug 2024 14:41:16 -0400 Subject: [PATCH 01/13] chore: enable eslint-plugin-perfectionist on eslint-plugin package --- eslint.config.mjs | 15 +- .../src/rules/adjacent-overload-signatures.ts | 47 +- .../eslint-plugin/src/rules/array-type.ts | 125 +- .../eslint-plugin/src/rules/await-thenable.ts | 37 +- .../eslint-plugin/src/rules/ban-ts-comment.ts | 179 +- .../src/rules/ban-tslint-comment.ts | 32 +- .../src/rules/class-literal-property-style.ts | 61 +- .../src/rules/class-methods-use-this.ts | 147 +- .../rules/consistent-generic-constructors.ts | 57 +- .../rules/consistent-indexed-object-style.ts | 57 +- .../src/rules/consistent-return.ts | 44 +- .../src/rules/consistent-type-assertions.ts | 137 +- .../src/rules/consistent-type-definitions.ts | 49 +- .../src/rules/consistent-type-exports.ts | 93 +- .../src/rules/consistent-type-imports.ts | 153 +- .../src/rules/default-param-last.ts | 29 +- .../eslint-plugin/src/rules/dot-notation.ts | 102 +- .../rules/explicit-function-return-type.ts | 168 +- .../rules/explicit-member-accessibility.ts | 187 +- .../rules/explicit-module-boundary-types.ts | 176 +- .../src/rules/init-declarations.ts | 30 +- .../eslint-plugin/src/rules/max-params.ts | 68 +- .../src/rules/member-ordering.ts | 253 +- .../src/rules/method-signature-style.ts | 61 +- .../rules/naming-convention-utils/enums.ts | 48 +- .../rules/naming-convention-utils/format.ts | 6 +- .../rules/naming-convention-utils/index.ts | 4 +- .../naming-convention-utils/parse-options.ts | 45 +- .../rules/naming-convention-utils/schema.ts | 111 +- .../rules/naming-convention-utils/shared.ts | 3 +- .../rules/naming-convention-utils/types.ts | 28 +- .../naming-convention-utils/validator.ts | 72 +- .../src/rules/naming-convention.ts | 486 +- .../src/rules/no-array-constructor.ts | 35 +- .../src/rules/no-array-delete.ts | 41 +- .../src/rules/no-base-to-string.ts | 67 +- .../rules/no-confusing-non-null-assertion.ts | 59 +- .../src/rules/no-confusing-void-expression.ts | 128 +- .../src/rules/no-dupe-class-members.ts | 26 +- .../src/rules/no-duplicate-enum-values.ts | 33 +- .../rules/no-duplicate-type-constituents.ts | 85 +- .../src/rules/no-dynamic-delete.ts | 33 +- .../src/rules/no-empty-function.ts | 40 +- .../src/rules/no-empty-interface.ts | 73 +- .../src/rules/no-empty-object-type.ts | 99 +- .../src/rules/no-explicit-any.ts | 97 +- .../src/rules/no-extra-non-null-assertion.ts | 38 +- .../src/rules/no-extraneous-class.ts | 105 +- .../src/rules/no-floating-promises.ts | 134 +- .../src/rules/no-for-in-array.ts | 30 +- .../src/rules/no-implied-eval.ts | 43 +- .../src/rules/no-import-type-side-effects.ts | 35 +- .../src/rules/no-inferrable-types.ts | 81 +- .../src/rules/no-invalid-this.ts | 40 +- .../src/rules/no-invalid-void-type.ts | 97 +- .../eslint-plugin/src/rules/no-loop-func.ts | 34 +- .../src/rules/no-loss-of-precision.ts | 15 +- .../src/rules/no-magic-numbers.ts | 56 +- .../src/rules/no-meaningless-void-operator.ts | 73 +- .../eslint-plugin/src/rules/no-misused-new.ts | 59 +- .../src/rules/no-misused-promises.ts | 137 +- .../eslint-plugin/src/rules/no-mixed-enums.ts | 31 +- .../eslint-plugin/src/rules/no-namespace.ts | 75 +- ...no-non-null-asserted-nullish-coalescing.ts | 41 +- .../no-non-null-asserted-optional-chain.ts | 42 +- .../src/rules/no-non-null-assertion.ts | 49 +- .../eslint-plugin/src/rules/no-redeclare.ts | 113 +- .../rules/no-redundant-type-constituents.ts | 40 +- .../src/rules/no-require-imports.ts | 63 +- .../src/rules/no-restricted-imports.ts | 114 +- .../src/rules/no-restricted-types.ts | 147 +- packages/eslint-plugin/src/rules/no-shadow.ts | 133 +- .../eslint-plugin/src/rules/no-this-alias.ts | 83 +- .../eslint-plugin/src/rules/no-type-alias.ts | 185 +- .../no-unnecessary-boolean-literal-compare.ts | 115 +- .../src/rules/no-unnecessary-condition.ts | 141 +- ...necessary-parameter-property-assignment.ts | 99 +- .../src/rules/no-unnecessary-qualifier.ts | 67 +- .../no-unnecessary-template-expression.ts | 47 +- .../rules/no-unnecessary-type-arguments.ts | 37 +- .../rules/no-unnecessary-type-assertion.ts | 231 +- .../rules/no-unnecessary-type-constraint.ts | 45 +- .../rules/no-unnecessary-type-parameters.ts | 38 +- .../src/rules/no-unsafe-argument.ts | 103 +- .../src/rules/no-unsafe-assignment.ts | 123 +- .../eslint-plugin/src/rules/no-unsafe-call.ts | 45 +- .../rules/no-unsafe-declaration-merging.ts | 33 +- .../src/rules/no-unsafe-enum-comparison.ts | 41 +- .../src/rules/no-unsafe-function-type.ts | 37 +- .../src/rules/no-unsafe-member-access.ts | 57 +- .../src/rules/no-unsafe-return.ts | 55 +- .../src/rules/no-unsafe-unary-minus.ts | 32 +- .../src/rules/no-unused-expressions.ts | 39 +- .../eslint-plugin/src/rules/no-unused-vars.ts | 193 +- .../src/rules/no-use-before-define.ts | 115 +- .../src/rules/no-useless-constructor.ts | 28 +- .../src/rules/no-useless-empty-export.ts | 31 +- .../src/rules/no-var-requires.ts | 55 +- .../src/rules/no-wrapper-object-types.ts | 33 +- .../non-nullable-type-assertion-style.ts | 35 +- .../src/rules/only-throw-error.ts | 73 +- .../src/rules/parameter-properties.ts | 117 +- .../src/rules/prefer-as-const.ts | 57 +- .../src/rules/prefer-destructuring.ts | 98 +- .../src/rules/prefer-enum-initializers.ts | 44 +- .../eslint-plugin/src/rules/prefer-find.ts | 55 +- .../eslint-plugin/src/rules/prefer-for-of.ts | 33 +- .../src/rules/prefer-function-type.ts | 47 +- .../src/rules/prefer-includes.ts | 59 +- .../src/rules/prefer-literal-enum-member.ts | 65 +- .../src/rules/prefer-namespace-keyword.ts | 36 +- .../src/rules/prefer-nullish-coalescing.ts | 155 +- .../PreferOptionalChainOptions.ts | 14 +- .../analyzeChain.ts | 26 +- .../checkNullishAndReport.ts | 3 +- .../compareNodes.ts | 1 + .../gatherLogicalOperands.ts | 34 +- .../src/rules/prefer-optional-chain.ts | 264 +- .../src/rules/prefer-promise-reject-errors.ts | 61 +- .../rules/prefer-readonly-parameter-types.ts | 82 +- .../src/rules/prefer-readonly.ts | 209 +- .../src/rules/prefer-reduce-type-parameter.ts | 45 +- .../src/rules/prefer-regexp-exec.ts | 67 +- .../src/rules/prefer-return-this-type.ts | 41 +- .../rules/prefer-string-starts-ends-with.ts | 95 +- .../src/rules/prefer-ts-expect-error.ts | 39 +- .../src/rules/promise-function-async.ts | 121 +- .../src/rules/require-array-sort-compare.ts | 66 +- .../eslint-plugin/src/rules/require-await.ts | 73 +- .../src/rules/restrict-plus-operands.ts | 149 +- .../rules/restrict-template-expressions.ts | 107 +- .../eslint-plugin/src/rules/return-await.ts | 139 +- .../src/rules/sort-type-constituents.ts | 153 +- .../src/rules/strict-boolean-expressions.ts | 433 +- .../src/rules/switch-exhaustiveness-check.ts | 111 +- .../src/rules/triple-slash-reference.ts | 107 +- packages/eslint-plugin/src/rules/typedef.ts | 83 +- .../eslint-plugin/src/rules/unbound-method.ts | 73 +- .../src/rules/unified-signatures.ts | 120 +- .../use-unknown-in-catch-callback-variable.ts | 101 +- packages/eslint-plugin/src/util/astUtils.ts | 5 +- .../src/util/collectUnusedVariables.ts | 249 +- .../src/util/explicitReturnTypeUtils.ts | 7 +- .../src/util/getESLintCoreRule.ts | 2 +- .../eslint-plugin/src/util/getFixOrSuggest.ts | 4 +- .../src/util/getForStatementHeadLoc.ts | 3 +- .../src/util/getFunctionHeadLoc.ts | 3 +- .../src/util/getMemberHeadLoc.ts | 7 +- .../src/util/getOperatorPrecedence.ts | 131 +- .../src/util/getStaticStringValue.ts | 1 + .../src/util/getThisExpression.ts | 1 + .../src/util/getWrappingFixer.ts | 17 +- packages/eslint-plugin/src/util/index.ts | 10 +- packages/eslint-plugin/src/util/isAssignee.ts | 1 + .../eslint-plugin/src/util/isNodeEqual.ts | 1 + .../eslint-plugin/src/util/isNullLiteral.ts | 1 + .../src/util/isStartOfExpressionStatement.ts | 1 + .../eslint-plugin/src/util/isTypeImport.ts | 1 + .../src/util/isUndefinedIdentifier.ts | 1 + packages/eslint-plugin/src/util/misc.ts | 29 +- .../src/util/needsPrecedingSemiColon.ts | 3 +- .../src/util/referenceContainsTypeQuery.ts | 1 + packages/eslint-plugin/src/util/types.ts | 4 +- packages/eslint-plugin/tests/RuleTester.ts | 8 +- .../tests/areOptionsValid.test.ts | 14 +- .../eslint-plugin/tests/areOptionsValid.ts | 13 +- packages/eslint-plugin/tests/configs.test.ts | 30 +- packages/eslint-plugin/tests/docs.test.ts | 31 +- .../tests/eslint-rules/arrow-parens.test.ts | 2 +- .../tests/eslint-rules/no-dupe-args.test.ts | 2 +- .../eslint-rules/no-implicit-globals.test.ts | 2 +- .../no-restricted-globals.test.ts | 92 +- .../tests/eslint-rules/no-undef.test.ts | 354 +- .../tests/eslint-rules/prefer-const.test.ts | 2 +- .../tests/eslint-rules/strict.test.ts | 34 +- .../adjacent-overload-signatures.test.ts | 1234 +-- .../tests/rules/array-type.test.ts | 2629 +++---- .../tests/rules/await-thenable.test.ts | 362 +- .../tests/rules/ban-ts-comment.test.ts | 1054 +-- .../tests/rules/ban-tslint-comment.test.ts | 34 +- .../class-literal-property-style.test.ts | 936 +-- .../class-methods-use-this-core.test.ts | 400 +- .../class-methods-use-this.test.ts | 716 +- .../consistent-generic-constructors.test.ts | 288 +- .../consistent-indexed-object-style.test.ts | 330 +- .../tests/rules/consistent-return.test.ts | 490 +- .../rules/consistent-type-assertions.test.ts | 371 +- .../rules/consistent-type-definitions.test.ts | 298 +- .../rules/consistent-type-exports.test.ts | 302 +- .../rules/consistent-type-imports.test.ts | 2618 +++---- .../tests/rules/default-param-last.test.ts | 428 +- .../tests/rules/dot-notation.test.ts | 344 +- .../explicit-function-return-type.test.ts | 2340 +++--- .../explicit-member-accessibility.test.ts | 2786 +++---- .../explicit-module-boundary-types.test.ts | 2680 +++---- .../tests/rules/init-declarations.test.ts | 1128 +-- .../tests/rules/max-params.test.ts | 84 +- .../tests/rules/member-ordering.test.ts | 6630 ++++++++-------- ...habetically-case-insensitive-order.test.ts | 564 +- ...mber-ordering-alphabetically-order.test.ts | 1738 ++--- ...ing-natural-case-insensitive-order.test.ts | 98 +- .../member-ordering-natural-order.test.ts | 98 +- .../member-ordering-optionalMembers.test.ts | 202 +- .../rules/method-signature-style.test.ts | 458 +- .../cases/createTestCases.ts | 84 +- .../naming-convention/cases/default.test.ts | 2 +- .../cases/parameterProperty.test.ts | 2 +- .../naming-convention.test.ts | 2348 +++--- .../tests/rules/no-array-constructor.test.ts | 76 +- .../tests/rules/no-array-delete.test.ts | 154 +- .../tests/rules/no-base-to-string.test.ts | 162 +- .../no-confusing-non-null-assertion.test.ts | 50 +- .../no-confusing-void-expression.test.ts | 312 +- .../tests/rules/no-dupe-class-members.test.ts | 172 +- .../rules/no-duplicate-enum-values.test.ts | 116 +- .../no-duplicate-type-constituents.test.ts | 588 +- .../tests/rules/no-dynamic-delete.test.ts | 102 +- .../tests/rules/no-empty-function.test.ts | 180 +- .../tests/rules/no-empty-interface.test.ts | 204 +- .../tests/rules/no-empty-object-type.test.ts | 212 +- .../tests/rules/no-explicit-any.test.ts | 1990 ++--- .../rules/no-extra-non-null-assertion.test.ts | 182 +- .../tests/rules/no-extraneous-class.test.ts | 172 +- .../tests/rules/no-floating-promises.test.ts | 6830 ++++++++--------- .../tests/rules/no-for-in-array.test.ts | 70 +- .../tests/rules/no-implied-eval.test.ts | 1106 +-- .../rules/no-import-type-side-effects.test.ts | 36 +- .../tests/rules/no-inferrable-types.test.ts | 280 +- .../tests/rules/no-invalid-this.test.ts | 1186 +-- .../tests/rules/no-invalid-void-type.test.ts | 352 +- .../tests/rules/no-loop-func.test.ts | 524 +- .../tests/rules/no-loss-of-precision.test.ts | 16 +- .../tests/rules/no-magic-numbers.test.ts | 758 +- .../no-meaningless-void-operator.test.ts | 66 +- .../tests/rules/no-misused-new.test.ts | 164 +- .../tests/rules/no-misused-promises.test.ts | 2904 +++---- .../tests/rules/no-mixed-enums.test.ts | 526 +- .../tests/rules/no-namespace.test.ts | 322 +- ...n-null-asserted-nullish-coalescing.test.ts | 110 +- ...o-non-null-asserted-optional-chain.test.ts | 30 +- .../tests/rules/no-non-null-assertion.test.ts | 118 +- .../tests/rules/no-redeclare.test.ts | 406 +- .../no-redundant-type-constituents.test.ts | 308 +- .../tests/rules/no-require-imports.test.ts | 322 +- .../tests/rules/no-restricted-imports.test.ts | 626 +- .../tests/rules/no-restricted-types.test.ts | 320 +- .../rules/no-shadow/no-shadow-eslint.test.ts | 1488 ++-- .../tests/rules/no-shadow/no-shadow.test.ts | 1216 +-- .../tests/rules/no-this-alias.test.ts | 78 +- .../tests/rules/no-type-alias.test.ts | 3926 +++++----- ...nnecessary-boolean-literal-compare.test.ts | 138 +- .../rules/no-unnecessary-condition.test.ts | 3637 ++++----- ...sary-parameter-property-assignment.test.ts | 362 +- .../rules/no-unnecessary-qualifier.test.ts | 136 +- ...no-unnecessary-template-expression.test.ts | 559 +- .../no-unnecessary-type-arguments.test.ts | 288 +- .../no-unnecessary-type-assertion.test.ts | 1377 ++-- .../no-unnecessary-type-constraint.test.ts | 172 +- .../no-unnecessary-type-parameters.test.ts | 1040 +-- .../tests/rules/no-unsafe-argument.test.ts | 376 +- .../tests/rules/no-unsafe-assignment.test.ts | 282 +- .../tests/rules/no-unsafe-call.test.ts | 118 +- .../no-unsafe-declaration-merging.test.ts | 118 +- .../rules/no-unsafe-enum-comparison.test.ts | 644 +- .../rules/no-unsafe-function-type.test.ts | 46 +- .../rules/no-unsafe-member-access.test.ts | 218 +- .../tests/rules/no-unsafe-return.test.ts | 430 +- .../tests/rules/no-unsafe-unary-minus.test.ts | 30 +- .../tests/rules/no-unused-expressions.test.ts | 197 +- .../no-unused-vars-eslint.test.ts | 4532 +++++------ .../no-unused-vars/no-unused-vars.test.ts | 3820 ++++----- .../tests/rules/no-use-before-define.test.ts | 1926 ++--- .../rules/no-useless-constructor.test.ts | 176 +- .../rules/no-useless-empty-export.test.ts | 96 +- .../tests/rules/no-var-requires.test.ts | 138 +- .../rules/no-wrapper-object-types.test.ts | 218 +- .../non-nullable-type-assertion-style.test.ts | 144 +- .../tests/rules/only-throw-error.test.ts | 262 +- .../tests/rules/parameter-properties.test.ts | 1204 +-- .../tests/rules/prefer-as-const.test.ts | 280 +- .../tests/rules/prefer-destructuring.test.ts | 1342 ++-- .../rules/prefer-enum-initializers.test.ts | 10 +- .../tests/rules/prefer-find.test.ts | 130 +- .../tests/rules/prefer-for-of.test.ts | 370 +- .../tests/rules/prefer-function-type.test.ts | 148 +- .../tests/rules/prefer-includes.test.ts | 226 +- .../rules/prefer-literal-enum-member.test.ts | 350 +- .../rules/prefer-namespace-keyword.test.ts | 44 +- .../rules/prefer-nullish-coalescing.test.ts | 2087 ++--- .../rules/prefer-optional-chain/base-cases.ts | 116 +- .../prefer-optional-chain.test.ts | 954 +-- .../prefer-promise-reject-errors.test.ts | 1298 ++-- .../prefer-readonly-parameter-types.test.ts | 1296 ++-- .../tests/rules/prefer-readonly.test.ts | 3352 ++++---- .../prefer-reduce-type-parameter.test.ts | 198 +- .../tests/rules/prefer-regexp-exec.test.ts | 228 +- .../rules/prefer-return-this-type.test.ts | 172 +- .../prefer-string-starts-ends-with.test.ts | 764 +- .../rules/prefer-ts-expect-error.test.ts | 128 +- .../rules/promise-function-async.test.ts | 466 +- .../rules/require-array-sort-compare.test.ts | 242 +- .../tests/rules/require-await.test.ts | 750 +- .../rules/restrict-plus-operands.test.ts | 1768 ++--- .../restrict-template-expressions.test.ts | 714 +- .../tests/rules/return-await.test.ts | 2163 +++--- .../rules/sort-type-constituents.test.ts | 222 +- .../rules/strict-boolean-expressions.test.ts | 4086 +++++----- .../rules/switch-exhaustiveness-check.test.ts | 2965 ++++--- .../rules/triple-slash-reference.test.ts | 140 +- .../eslint-plugin/tests/rules/typedef.test.ts | 1112 +-- .../tests/rules/unbound-method.test.ts | 2020 ++--- .../tests/rules/unified-signatures.test.ts | 532 +- ...unknown-in-catch-callback-variable.test.ts | 302 +- packages/eslint-plugin/tests/schemas.test.ts | 6 +- .../tests/util/getWrappedCode.test.ts | 41 +- .../tests/util/getWrappingFixer.test.ts | 87 +- .../tests/util/isNodeEqual.test.ts | 49 +- .../eslint-plugin/typings/eslint-rules.d.ts | 80 +- .../eslint-plugin/typings/typescript.d.ts | 16 +- 319 files changed, 64801 insertions(+), 64590 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 60b302d8cb50..052489694f75 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -578,8 +578,13 @@ export default tseslint.config( }, { extends: [perfectionistPlugin.configs['recommended-alphabetical']], - files: ['packages/utils/src/**/*.ts'], + ignores: ['packages/eslint-plugin/src/configs/*'], + files: [ + 'packages/eslint-plugin/{src,tests,tools,typings}/**/*.ts', + 'packages/utils/src/**/*.ts', + ], rules: { + '@typescript-eslint/sort-type-constituents': 'off', 'perfectionist/sort-classes': [ 'error', { @@ -588,6 +593,14 @@ export default tseslint.config( type: 'natural', }, ], + 'perfectionist/sort-enums': [ + 'error', + { + order: 'asc', + partitionByComment: true, + type: 'natural', + }, + ], 'perfectionist/sort-objects': [ 'error', { diff --git a/packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts b/packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts index f28472e73a7b..5d8e094fe761 100644 --- a/packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts +++ b/packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, getNameFromMember, MemberNameType } from '../util'; @@ -21,24 +22,11 @@ type MemberDeclaration = | TSESTree.NamedExportDeclarations; export default createRule({ - name: 'adjacent-overload-signatures', - meta: { - type: 'suggestion', - docs: { - description: 'Require that function overload signatures be consecutive', - recommended: 'stylistic', - }, - schema: [], - messages: { - adjacentSignature: 'All {{name}} signatures should be adjacent.', - }, - }, - defaultOptions: [], create(context) { interface Method { + callSignature: boolean; name: string; static?: boolean; - callSignature: boolean; type: MemberNameType; } @@ -68,34 +56,34 @@ export default createRule({ return null; } return { - name, callSignature: false, + name, type: MemberNameType.Normal, }; } case AST_NODE_TYPES.TSMethodSignature: return { ...getNameFromMember(member, context.sourceCode), - static: !!member.static, callSignature: false, + static: !!member.static, }; case AST_NODE_TYPES.TSCallSignatureDeclaration: return { - name: 'call', callSignature: true, + name: 'call', type: MemberNameType.Normal, }; case AST_NODE_TYPES.TSConstructSignatureDeclaration: return { - name: 'new', callSignature: false, + name: 'new', type: MemberNameType.Normal, }; case AST_NODE_TYPES.MethodDefinition: return { ...getNameFromMember(member, context.sourceCode), - static: !!member.static, callSignature: false, + static: !!member.static, }; } @@ -144,11 +132,11 @@ export default createRule({ ); if (index > -1 && !isSameMethod(method, lastMethod)) { context.report({ - node: member, - messageId: 'adjacentSignature', data: { name: `${method.static ? 'static ' : ''}${method.name}`, }, + messageId: 'adjacentSignature', + node: member, }); } else if (index === -1) { seenMethods.push(method); @@ -159,12 +147,25 @@ export default createRule({ } return { + BlockStatement: checkBodyForOverloadMethods, ClassBody: checkBodyForOverloadMethods, Program: checkBodyForOverloadMethods, + TSInterfaceBody: checkBodyForOverloadMethods, TSModuleBlock: checkBodyForOverloadMethods, TSTypeLiteral: checkBodyForOverloadMethods, - TSInterfaceBody: checkBodyForOverloadMethods, - BlockStatement: checkBodyForOverloadMethods, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Require that function overload signatures be consecutive', + recommended: 'stylistic', + }, + messages: { + adjacentSignature: 'All {{name}} signatures should be adjacent.', + }, + schema: [], + type: 'suggestion', + }, + name: 'adjacent-overload-signatures', }); diff --git a/packages/eslint-plugin/src/rules/array-type.ts b/packages/eslint-plugin/src/rules/array-type.ts index 742507ada730..92b53a9c7898 100644 --- a/packages/eslint-plugin/src/rules/array-type.ts +++ b/packages/eslint-plugin/src/rules/array-type.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isParenthesized } from '../util'; @@ -71,7 +72,7 @@ function typeNeedsParentheses(node: TSESTree.Node): boolean { } } -export type OptionString = 'array-simple' | 'array' | 'generic'; +export type OptionString = 'array' | 'array-simple' | 'generic'; type Options = [ { default: OptionString; @@ -80,65 +81,13 @@ type Options = [ ]; type MessageIds = | 'errorStringArray' + | 'errorStringArrayReadonly' | 'errorStringArraySimple' + | 'errorStringArraySimpleReadonly' | 'errorStringGeneric' - | 'errorStringGenericSimple' - | 'errorStringArrayReadonly' - | 'errorStringArraySimpleReadonly'; + | 'errorStringGenericSimple'; export default createRule({ - name: 'array-type', - meta: { - type: 'suggestion', - docs: { - description: - 'Require consistently using either `T[]` or `Array` for arrays', - recommended: 'stylistic', - }, - fixable: 'code', - messages: { - errorStringGeneric: - "Array type using '{{readonlyPrefix}}{{type}}[]' is forbidden. Use '{{className}}<{{type}}>' instead.", - errorStringArray: - "Array type using '{{className}}<{{type}}>' is forbidden. Use '{{readonlyPrefix}}{{type}}[]' instead.", - errorStringArrayReadonly: - "Array type using '{{className}}<{{type}}>' is forbidden. Use '{{readonlyPrefix}}{{type}}' instead.", - errorStringArraySimple: - "Array type using '{{className}}<{{type}}>' is forbidden for simple types. Use '{{readonlyPrefix}}{{type}}[]' instead.", - errorStringGenericSimple: - "Array type using '{{readonlyPrefix}}{{type}}[]' is forbidden for non-simple types. Use '{{className}}<{{type}}>' instead.", - errorStringArraySimpleReadonly: - "Array type using '{{className}}<{{type}}>' is forbidden for simple types. Use '{{readonlyPrefix}}{{type}}' instead.", - }, - schema: [ - { - $defs: { - arrayOption: { - type: 'string', - enum: ['array', 'generic', 'array-simple'], - }, - }, - additionalProperties: false, - properties: { - default: { - $ref: '#/items/0/$defs/arrayOption', - description: 'The array type expected for mutable cases.', - }, - readonly: { - $ref: '#/items/0/$defs/arrayOption', - description: - 'The array type expected for readonly cases. If omitted, the value for `default` will be used.', - }, - }, - type: 'object', - }, - ], - }, - defaultOptions: [ - { - default: 'array', - }, - ], create(context, [options]) { const defaultOption = options.default; const readonlyOption = options.readonly ?? defaultOption; @@ -175,8 +124,6 @@ export default createRule({ const errorNode = isReadonly ? node.parent : node; context.report({ - node: errorNode, - messageId, data: { className: isReadonly ? 'ReadonlyArray' : 'Array', readonlyPrefix: isReadonly ? 'readonly ' : '', @@ -197,6 +144,8 @@ export default createRule({ ), ]; }, + messageId, + node: errorNode, }); }, @@ -243,8 +192,6 @@ export default createRule({ if (!typeParams || typeParams.length === 0) { // Create an 'any' array context.report({ - node, - messageId, data: { className: isReadonlyArrayType ? 'ReadonlyArray' : 'Array', readonlyPrefix, @@ -253,6 +200,8 @@ export default createRule({ fix(fixer) { return fixer.replaceText(node, `${readonlyPrefix}any[]`); }, + messageId, + node, }); return; @@ -277,8 +226,6 @@ export default createRule({ }`; const end = `${typeParens ? ')' : ''}${isReadonlyWithGenericArrayType ? '' : `[]`}${parentParens ? ')' : ''}`; context.report({ - node, - messageId, data: { className: isReadonlyArrayType ? node.typeName.name : 'Array', readonlyPrefix, @@ -290,8 +237,62 @@ export default createRule({ fixer.replaceTextRange([type.range[1], node.range[1]], end), ]; }, + messageId, + node, }); }, }; }, + defaultOptions: [ + { + default: 'array', + }, + ], + meta: { + docs: { + description: + 'Require consistently using either `T[]` or `Array` for arrays', + recommended: 'stylistic', + }, + fixable: 'code', + messages: { + errorStringArray: + "Array type using '{{className}}<{{type}}>' is forbidden. Use '{{readonlyPrefix}}{{type}}[]' instead.", + errorStringArrayReadonly: + "Array type using '{{className}}<{{type}}>' is forbidden. Use '{{readonlyPrefix}}{{type}}' instead.", + errorStringArraySimple: + "Array type using '{{className}}<{{type}}>' is forbidden for simple types. Use '{{readonlyPrefix}}{{type}}[]' instead.", + errorStringArraySimpleReadonly: + "Array type using '{{className}}<{{type}}>' is forbidden for simple types. Use '{{readonlyPrefix}}{{type}}' instead.", + errorStringGeneric: + "Array type using '{{readonlyPrefix}}{{type}}[]' is forbidden. Use '{{className}}<{{type}}>' instead.", + errorStringGenericSimple: + "Array type using '{{readonlyPrefix}}{{type}}[]' is forbidden for non-simple types. Use '{{className}}<{{type}}>' instead.", + }, + schema: [ + { + $defs: { + arrayOption: { + enum: ['array', 'generic', 'array-simple'], + type: 'string', + }, + }, + additionalProperties: false, + properties: { + default: { + $ref: '#/items/0/$defs/arrayOption', + description: 'The array type expected for mutable cases.', + }, + readonly: { + $ref: '#/items/0/$defs/arrayOption', + description: + 'The array type expected for readonly cases. If omitted, the value for `default` will be used.', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'array-type', }); diff --git a/packages/eslint-plugin/src/rules/await-thenable.ts b/packages/eslint-plugin/src/rules/await-thenable.ts index 084ea2447e89..71c13ee2e2cc 100644 --- a/packages/eslint-plugin/src/rules/await-thenable.ts +++ b/packages/eslint-plugin/src/rules/await-thenable.ts @@ -1,4 +1,5 @@ import type { TSESLint } from '@typescript-eslint/utils'; + import * as tsutils from 'ts-api-utils'; import { @@ -12,23 +13,6 @@ import { } from '../util'; export default createRule({ - name: 'await-thenable', - meta: { - docs: { - description: 'Disallow awaiting a value that is not a Thenable', - recommended: 'recommended', - requiresTypeChecking: true, - }, - hasSuggestions: true, - messages: { - await: 'Unexpected `await` of a non-Promise (non-"Thenable") value.', - removeAwait: 'Remove unnecessary `await`.', - }, - schema: [], - type: 'problem', - }, - defaultOptions: [], - create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -48,7 +32,6 @@ export default createRule({ node, suggest: [ { - messageId: 'removeAwait', fix(fixer): TSESLint.RuleFix { const awaitKeyword = nullThrows( context.sourceCode.getFirstToken(node, isAwaitKeyword), @@ -57,6 +40,7 @@ export default createRule({ return fixer.remove(awaitKeyword); }, + messageId: 'removeAwait', }, ], }); @@ -64,4 +48,21 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow awaiting a value that is not a Thenable', + recommended: 'recommended', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + await: 'Unexpected `await` of a non-Promise (non-"Thenable") value.', + removeAwait: 'Remove unnecessary `await`.', + }, + schema: [], + type: 'problem', + }, + + name: 'await-thenable', }); diff --git a/packages/eslint-plugin/src/rules/ban-ts-comment.ts b/packages/eslint-plugin/src/rules/ban-ts-comment.ts index b554510f57d3..f00c6846f85c 100644 --- a/packages/eslint-plugin/src/rules/ban-ts-comment.ts +++ b/packages/eslint-plugin/src/rules/ban-ts-comment.ts @@ -1,107 +1,37 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule, getStringLength, nullThrows } from '../util'; type DirectiveConfig = - | boolean | 'allow-with-description' - | { descriptionFormat: string }; + | { descriptionFormat: string } + | boolean; interface Options { + minimumDescriptionLength?: number; + 'ts-check'?: DirectiveConfig; 'ts-expect-error'?: DirectiveConfig; 'ts-ignore'?: DirectiveConfig; 'ts-nocheck'?: DirectiveConfig; - 'ts-check'?: DirectiveConfig; - minimumDescriptionLength?: number; } const defaultMinimumDescriptionLength = 3; type MessageIds = + | 'replaceTsIgnoreWithTsExpectError' | 'tsDirectiveComment' - | 'tsIgnoreInsteadOfExpectError' | 'tsDirectiveCommentDescriptionNotMatchPattern' | 'tsDirectiveCommentRequiresDescription' - | 'replaceTsIgnoreWithTsExpectError'; + | 'tsIgnoreInsteadOfExpectError'; interface MatchedTSDirective { - directive: string; description: string; + directive: string; } export default createRule<[Options], MessageIds>({ - name: 'ban-ts-comment', - meta: { - type: 'problem', - docs: { - description: - 'Disallow `@ts-` comments or require descriptions after directives', - recommended: { - recommended: true, - strict: [{ minimumDescriptionLength: 10 }], - }, - }, - messages: { - tsDirectiveComment: - 'Do not use "@ts-{{directive}}" because it alters compilation errors.', - tsIgnoreInsteadOfExpectError: - 'Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.', - tsDirectiveCommentRequiresDescription: - 'Include a description after the "@ts-{{directive}}" directive to explain why the @ts-{{directive}} is necessary. The description must be {{minimumDescriptionLength}} characters or longer.', - tsDirectiveCommentDescriptionNotMatchPattern: - 'The description for the "@ts-{{directive}}" directive must match the {{format}} format.', - replaceTsIgnoreWithTsExpectError: - 'Replace "@ts-ignore" with "@ts-expect-error".', - }, - hasSuggestions: true, - schema: [ - { - $defs: { - directiveConfigSchema: { - oneOf: [ - { - type: 'boolean', - default: true, - }, - { - type: 'string', - enum: ['allow-with-description'], - }, - { - type: 'object', - additionalProperties: false, - properties: { - descriptionFormat: { type: 'string' }, - }, - }, - ], - }, - }, - properties: { - 'ts-expect-error': { $ref: '#/items/0/$defs/directiveConfigSchema' }, - 'ts-ignore': { $ref: '#/items/0/$defs/directiveConfigSchema' }, - 'ts-nocheck': { $ref: '#/items/0/$defs/directiveConfigSchema' }, - 'ts-check': { $ref: '#/items/0/$defs/directiveConfigSchema' }, - minimumDescriptionLength: { - type: 'number', - default: defaultMinimumDescriptionLength, - }, - }, - type: 'object', - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - 'ts-expect-error': 'allow-with-description', - 'ts-ignore': true, - 'ts-nocheck': true, - 'ts-check': false, - minimumDescriptionLength: defaultMinimumDescriptionLength, - }, - ], create(context, [options]) { // https://github.com/microsoft/TypeScript/blob/6f1ad5ad8bec5671f7e951a3524b62d82ec4be68/src/compiler/parser.ts#L10591 const singleLinePragmaRegEx = @@ -138,19 +68,19 @@ export default createRule<[Options], MessageIds>({ return null; } - const { directive, description } = nullThrows( + const { description, directive } = nullThrows( match.groups, 'RegExp should contain groups', ); return { - directive: nullThrows( - directive, - 'RegExp should contain "directive" group', - ), description: nullThrows( description, 'RegExp should contain "description" group', ), + directive: nullThrows( + directive, + 'RegExp should contain "directive" group', + ), }; } @@ -188,7 +118,7 @@ export default createRule<[Options], MessageIds>({ if (!match) { return; } - const { directive, description } = match; + const { description, directive } = match; const fullDirective = `ts-${directive}` as keyof Options; @@ -197,11 +127,10 @@ export default createRule<[Options], MessageIds>({ if (directive === 'ignore') { // Special case to suggest @ts-expect-error instead of @ts-ignore context.report({ - node: comment, messageId: 'tsIgnoreInsteadOfExpectError', + node: comment, suggest: [ { - messageId: 'replaceTsIgnoreWithTsExpectError', fix(fixer): TSESLint.RuleFix { const commentText = comment.value.replace( /@ts-ignore/, @@ -214,14 +143,15 @@ export default createRule<[Options], MessageIds>({ : `/*${commentText}*/`, ); }, + messageId: 'replaceTsIgnoreWithTsExpectError', }, ], }); } else { context.report({ data: { directive }, - node: comment, messageId: 'tsDirectiveComment', + node: comment, }); } } @@ -241,14 +171,14 @@ export default createRule<[Options], MessageIds>({ ) { context.report({ data: { directive, minimumDescriptionLength }, - node: comment, messageId: 'tsDirectiveCommentRequiresDescription', + node: comment, }); } else if (format && !format.test(description)) { context.report({ data: { directive, format: format.source }, - node: comment, messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', + node: comment, }); } } @@ -256,4 +186,75 @@ export default createRule<[Options], MessageIds>({ }, }; }, + defaultOptions: [ + { + minimumDescriptionLength: defaultMinimumDescriptionLength, + 'ts-check': false, + 'ts-expect-error': 'allow-with-description', + 'ts-ignore': true, + 'ts-nocheck': true, + }, + ], + meta: { + docs: { + description: + 'Disallow `@ts-` comments or require descriptions after directives', + recommended: { + recommended: true, + strict: [{ minimumDescriptionLength: 10 }], + }, + }, + hasSuggestions: true, + messages: { + replaceTsIgnoreWithTsExpectError: + 'Replace "@ts-ignore" with "@ts-expect-error".', + tsDirectiveComment: + 'Do not use "@ts-{{directive}}" because it alters compilation errors.', + tsDirectiveCommentDescriptionNotMatchPattern: + 'The description for the "@ts-{{directive}}" directive must match the {{format}} format.', + tsDirectiveCommentRequiresDescription: + 'Include a description after the "@ts-{{directive}}" directive to explain why the @ts-{{directive}} is necessary. The description must be {{minimumDescriptionLength}} characters or longer.', + tsIgnoreInsteadOfExpectError: + 'Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free.', + }, + schema: [ + { + $defs: { + directiveConfigSchema: { + oneOf: [ + { + default: true, + type: 'boolean', + }, + { + enum: ['allow-with-description'], + type: 'string', + }, + { + additionalProperties: false, + properties: { + descriptionFormat: { type: 'string' }, + }, + type: 'object', + }, + ], + }, + }, + additionalProperties: false, + properties: { + minimumDescriptionLength: { + default: defaultMinimumDescriptionLength, + type: 'number', + }, + 'ts-check': { $ref: '#/items/0/$defs/directiveConfigSchema' }, + 'ts-expect-error': { $ref: '#/items/0/$defs/directiveConfigSchema' }, + 'ts-ignore': { $ref: '#/items/0/$defs/directiveConfigSchema' }, + 'ts-nocheck': { $ref: '#/items/0/$defs/directiveConfigSchema' }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'ban-ts-comment', }); diff --git a/packages/eslint-plugin/src/rules/ban-tslint-comment.ts b/packages/eslint-plugin/src/rules/ban-tslint-comment.ts index 4c84981a9fa3..95ed5d959788 100644 --- a/packages/eslint-plugin/src/rules/ban-tslint-comment.ts +++ b/packages/eslint-plugin/src/rules/ban-tslint-comment.ts @@ -16,20 +16,6 @@ const toText = ( : ['/*', text.trim(), '*/'].join(' '); export default createRule({ - name: 'ban-tslint-comment', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow `// tslint:` comments', - recommended: 'stylistic', - }, - messages: { - commentDetected: 'tslint comment detected: "{{ text }}"', - }, - schema: [], - fixable: 'code', - }, - defaultOptions: [], create: context => { return { Program(): void { @@ -38,8 +24,6 @@ export default createRule({ if (ENABLE_DISABLE_REGEX.test(c.value)) { context.report({ data: { text: toText(c.value, c.type) }, - node: c, - messageId: 'commentDetected', fix(fixer) { const rangeStart = context.sourceCode.getIndexFromLoc({ column: c.loc.start.column > 0 ? c.loc.start.column - 1 : 0, @@ -51,10 +35,26 @@ export default createRule({ }); return fixer.removeRange([rangeStart, rangeEnd + 1]); }, + messageId: 'commentDetected', + node: c, }); } }); }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow `// tslint:` comments', + recommended: 'stylistic', + }, + fixable: 'code', + messages: { + commentDetected: 'tslint comment detected: "{{ text }}"', + }, + schema: [], + type: 'suggestion', + }, + name: 'ban-tslint-comment', }); diff --git a/packages/eslint-plugin/src/rules/class-literal-property-style.ts b/packages/eslint-plugin/src/rules/class-literal-property-style.ts index 816c7803604a..13109cb32e28 100644 --- a/packages/eslint-plugin/src/rules/class-literal-property-style.ts +++ b/packages/eslint-plugin/src/rules/class-literal-property-style.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { @@ -22,8 +23,8 @@ interface NodeWithModifiers { } interface PropertiesInfo { - properties: TSESTree.PropertyDefinition[]; excludeSet: Set; + properties: TSESTree.PropertyDefinition[]; } const printNodeModifiers = ( @@ -53,29 +54,6 @@ const isSupportedLiteral = ( }; export default createRule({ - name: 'class-literal-property-style', - meta: { - type: 'problem', - docs: { - description: - 'Enforce that literals on classes are exposed in a consistent style', - recommended: 'stylistic', - }, - hasSuggestions: true, - messages: { - preferFieldStyle: 'Literals should be exposed using readonly fields.', - preferFieldStyleSuggestion: 'Replace the literals with readonly fields.', - preferGetterStyle: 'Literals should be exposed using getters.', - preferGetterStyleSuggestion: 'Replace the literals with getters.', - }, - schema: [ - { - type: 'string', - enum: ['fields', 'getters'], - }, - ], - }, - defaultOptions: ['fields'], create(context, [style]) { const propertiesInfoStack: PropertiesInfo[] = []; @@ -85,13 +63,13 @@ export default createRule({ function enterClassBody(): void { propertiesInfoStack.push({ - properties: [], excludeSet: new Set(), + properties: [], }); } function exitClassBody(): void { - const { properties, excludeSet } = nullThrows( + const { excludeSet, properties } = nullThrows( propertiesInfoStack.pop(), 'Stack should exist on class exit', ); @@ -108,11 +86,10 @@ export default createRule({ } context.report({ - node: node.key, messageId: 'preferGetterStyle', + node: node.key, suggest: [ { - messageId: 'preferGetterStyleSuggestion', fix(fixer): TSESLint.RuleFix { const name = context.sourceCode.getText(node.key); @@ -123,6 +100,7 @@ export default createRule({ return fixer.replaceText(node, text); }, + messageId: 'preferGetterStyleSuggestion', }, ], }); @@ -181,11 +159,10 @@ export default createRule({ } context.report({ - node: node.key, messageId: 'preferFieldStyle', + node: node.key, suggest: [ { - messageId: 'preferFieldStyleSuggestion', fix(fixer): TSESLint.RuleFix { const name = context.sourceCode.getText(node.key); @@ -197,6 +174,7 @@ export default createRule({ return fixer.replaceText(node, text); }, + messageId: 'preferFieldStyleSuggestion', }, ], }); @@ -234,4 +212,27 @@ export default createRule({ }), }; }, + defaultOptions: ['fields'], + meta: { + docs: { + description: + 'Enforce that literals on classes are exposed in a consistent style', + recommended: 'stylistic', + }, + hasSuggestions: true, + messages: { + preferFieldStyle: 'Literals should be exposed using readonly fields.', + preferFieldStyleSuggestion: 'Replace the literals with readonly fields.', + preferGetterStyle: 'Literals should be exposed using getters.', + preferGetterStyleSuggestion: 'Replace the literals with getters.', + }, + schema: [ + { + enum: ['fields', 'getters'], + type: 'string', + }, + ], + type: 'problem', + }, + name: 'class-literal-property-style', }); diff --git a/packages/eslint-plugin/src/rules/class-methods-use-this.ts b/packages/eslint-plugin/src/rules/class-methods-use-this.ts index 4f574471e03b..f0142a26010c 100644 --- a/packages/eslint-plugin/src/rules/class-methods-use-this.ts +++ b/packages/eslint-plugin/src/rules/class-methods-use-this.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { @@ -10,77 +11,15 @@ import { type Options = [ { - exceptMethods?: string[]; enforceForClassFields?: boolean; + exceptMethods?: string[]; + ignoreClassesThatImplementAnInterface?: 'public-fields' | boolean; ignoreOverrideMethods?: boolean; - ignoreClassesThatImplementAnInterface?: boolean | 'public-fields'; }, ]; type MessageIds = 'missingThis'; export default createRule({ - name: 'class-methods-use-this', - meta: { - type: 'suggestion', - docs: { - description: 'Enforce that class methods utilize `this`', - extendsBaseRule: true, - requiresTypeChecking: false, - }, - schema: [ - { - type: 'object', - properties: { - exceptMethods: { - type: 'array', - description: - 'Allows specified method names to be ignored with this rule', - items: { - type: 'string', - }, - }, - enforceForClassFields: { - type: 'boolean', - description: - 'Enforces that functions used as instance field initializers utilize `this`', - default: true, - }, - ignoreOverrideMethods: { - type: 'boolean', - description: 'Ignore members marked with the `override` modifier', - }, - ignoreClassesThatImplementAnInterface: { - oneOf: [ - { - type: 'boolean', - description: 'Ignore all classes that implement an interface', - }, - { - type: 'string', - enum: ['public-fields'], - description: - 'Ignore only the public fields of classes that implement an interface', - }, - ], - description: - 'Ignore classes that specifically implement some interface', - }, - }, - additionalProperties: false, - }, - ], - messages: { - missingThis: "Expected 'this' to be used by class {{name}}.", - }, - }, - defaultOptions: [ - { - enforceForClassFields: true, - exceptMethods: [], - ignoreClassesThatImplementAnInterface: false, - ignoreOverrideMethods: false, - }, - ], create( context, [ @@ -95,14 +34,14 @@ export default createRule({ const exceptMethods = new Set(exceptMethodsRaw); type Stack = | { - member: null; class: null; + member: null; parent: Stack | undefined; usesThis: boolean; } | { - member: TSESTree.MethodDefinition | TSESTree.PropertyDefinition; class: TSESTree.ClassDeclaration | TSESTree.ClassExpression; + member: TSESTree.MethodDefinition | TSESTree.PropertyDefinition; parent: Stack | undefined; usesThis: boolean; }; @@ -113,17 +52,17 @@ export default createRule({ ): void { if (member?.parent.type === AST_NODE_TYPES.ClassBody) { stack = { - member, class: member.parent.parent, - usesThis: false, + member, parent: stack, + usesThis: false, }; } else { stack = { - member: null, class: null, - usesThis: false, + member: null, parent: stack, + usesThis: false, }; } } @@ -214,12 +153,12 @@ export default createRule({ if (isIncludedInstanceMethod(stackContext.member)) { context.report({ - node, - loc: getFunctionHeadLoc(node, context.sourceCode), - messageId: 'missingThis', data: { name: getFunctionNameWithKind(node), }, + loc: getFunctionHeadLoc(node, context.sourceCode), + messageId: 'missingThis', + node, }); } } @@ -284,4 +223,66 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + enforceForClassFields: true, + exceptMethods: [], + ignoreClassesThatImplementAnInterface: false, + ignoreOverrideMethods: false, + }, + ], + meta: { + docs: { + description: 'Enforce that class methods utilize `this`', + extendsBaseRule: true, + requiresTypeChecking: false, + }, + messages: { + missingThis: "Expected 'this' to be used by class {{name}}.", + }, + schema: [ + { + additionalProperties: false, + properties: { + enforceForClassFields: { + default: true, + description: + 'Enforces that functions used as instance field initializers utilize `this`', + type: 'boolean', + }, + exceptMethods: { + description: + 'Allows specified method names to be ignored with this rule', + items: { + type: 'string', + }, + type: 'array', + }, + ignoreClassesThatImplementAnInterface: { + description: + 'Ignore classes that specifically implement some interface', + oneOf: [ + { + description: 'Ignore all classes that implement an interface', + type: 'boolean', + }, + { + description: + 'Ignore only the public fields of classes that implement an interface', + enum: ['public-fields'], + type: 'string', + }, + ], + }, + ignoreOverrideMethods: { + description: 'Ignore members marked with the `override` modifier', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'class-methods-use-this', }); diff --git a/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts index c4a110c7aa86..06bddbfc6b52 100644 --- a/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts +++ b/packages/eslint-plugin/src/rules/consistent-generic-constructors.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, nullThrows, NullThrowsReasons } from '../util'; @@ -7,29 +8,6 @@ type MessageIds = 'preferConstructor' | 'preferTypeAnnotation'; type Options = ['constructor' | 'type-annotation']; export default createRule({ - name: 'consistent-generic-constructors', - meta: { - type: 'suggestion', - docs: { - description: - 'Enforce specifying generic type arguments on type annotation or constructor name of a constructor call', - recommended: 'stylistic', - }, - messages: { - preferTypeAnnotation: - 'The generic type arguments should be specified as part of the type annotation.', - preferConstructor: - 'The generic type arguments should be specified as part of the constructor type arguments.', - }, - fixable: 'code', - schema: [ - { - type: 'string', - enum: ['type-annotation', 'constructor'], - }, - ], - }, - defaultOptions: ['constructor'], create(context, [mode]) { return { 'VariableDeclarator,PropertyDefinition,:matches(FunctionDeclaration,FunctionExpression) > AssignmentPattern'( @@ -75,13 +53,11 @@ export default createRule({ } if (mode === 'type-annotation') { if (!lhs && rhs.typeArguments) { - const { typeArguments, callee } = rhs; + const { callee, typeArguments } = rhs; const typeAnnotation = context.sourceCode.getText(callee) + context.sourceCode.getText(typeArguments); context.report({ - node, - messageId: 'preferTypeAnnotation', fix(fixer) { function getIDToAttachAnnotation(): | TSESTree.Node @@ -107,6 +83,8 @@ export default createRule({ ), ]; }, + messageId: 'preferTypeAnnotation', + node, }); } return; @@ -122,8 +100,6 @@ export default createRule({ .getCommentsInside(lhs.typeArguments) .forEach(c => extraComments.delete(c)); context.report({ - node, - messageId: 'preferConstructor', *fix(fixer) { yield fixer.remove(lhs.parent); for (const comment of extraComments) { @@ -140,9 +116,34 @@ export default createRule({ yield fixer.insertTextAfter(rhs.callee, '()'); } }, + messageId: 'preferConstructor', + node, }); } }, }; }, + defaultOptions: ['constructor'], + meta: { + docs: { + description: + 'Enforce specifying generic type arguments on type annotation or constructor name of a constructor call', + recommended: 'stylistic', + }, + fixable: 'code', + messages: { + preferConstructor: + 'The generic type arguments should be specified as part of the constructor type arguments.', + preferTypeAnnotation: + 'The generic type arguments should be specified as part of the type annotation.', + }, + schema: [ + { + enum: ['type-annotation', 'constructor'], + type: 'string', + }, + ], + type: 'suggestion', + }, + name: 'consistent-generic-constructors', }); diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index f0f91cc32b8e..88c55a7eec75 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -7,26 +8,6 @@ type MessageIds = 'preferIndexSignature' | 'preferRecord'; type Options = ['index-signature' | 'record']; export default createRule({ - name: 'consistent-indexed-object-style', - meta: { - type: 'suggestion', - docs: { - description: 'Require or disallow the `Record` type', - recommended: 'stylistic', - }, - messages: { - preferRecord: 'A record is preferred over an index signature.', - preferIndexSignature: 'An index signature is preferred over a record.', - }, - fixable: 'code', - schema: [ - { - type: 'string', - enum: ['record', 'index-signature'], - }, - ], - }, - defaultOptions: ['record'], create(context, [mode]) { function checkMembers( members: TSESTree.TypeElement[], @@ -77,8 +58,6 @@ export default createRule({ } context.report({ - node, - messageId: 'preferRecord', fix: safeFix ? (fixer): TSESLint.RuleFix => { const key = context.sourceCode.getText(keyType.typeAnnotation); @@ -91,6 +70,8 @@ export default createRule({ return fixer.replaceText(node, `${prefix}${record}${postfix}`); } : null, + messageId: 'preferRecord', + node, }); } @@ -111,21 +92,17 @@ export default createRule({ } context.report({ - node, - messageId: 'preferIndexSignature', fix(fixer) { const key = context.sourceCode.getText(params[0]); const type = context.sourceCode.getText(params[1]); return fixer.replaceText(node, `{ [key: ${key}]: ${type} }`); }, + messageId: 'preferIndexSignature', + node, }); }, }), ...(mode === 'record' && { - TSTypeLiteral(node): void { - const parent = findParentDeclaration(node); - checkMembers(node.members, node, parent?.id, '', ''); - }, TSInterfaceDeclaration(node): void { let genericTypes = ''; @@ -144,9 +121,33 @@ export default createRule({ !node.extends.length, ); }, + TSTypeLiteral(node): void { + const parent = findParentDeclaration(node); + checkMembers(node.members, node, parent?.id, '', ''); + }, }), }; }, + defaultOptions: ['record'], + meta: { + docs: { + description: 'Require or disallow the `Record` type', + recommended: 'stylistic', + }, + fixable: 'code', + messages: { + preferIndexSignature: 'An index signature is preferred over a record.', + preferRecord: 'A record is preferred over an index signature.', + }, + schema: [ + { + enum: ['record', 'index-signature'], + type: 'string', + }, + ], + type: 'suggestion', + }, + name: 'consistent-indexed-object-style', }); function findParentDeclaration( diff --git a/packages/eslint-plugin/src/rules/consistent-return.ts b/packages/eslint-plugin/src/rules/consistent-return.ts index 5d4cc3fb9256..2b62e7372e5a 100644 --- a/packages/eslint-plugin/src/rules/consistent-return.ts +++ b/packages/eslint-plugin/src/rules/consistent-return.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -6,6 +7,7 @@ import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule, getParserServices, isTypeFlagSet } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -15,25 +17,11 @@ type Options = InferOptionsTypeFromRule; type MessageIds = InferMessageIdsTypeFromRule; type FunctionNode = + | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.ArrowFunctionExpression; + | TSESTree.FunctionExpression; export default createRule({ - name: 'consistent-return', - meta: { - type: 'suggestion', - docs: { - description: - 'Require `return` statements to either always or never specify values', - extendsBaseRule: true, - requiresTypeChecking: true, - }, - hasSuggestions: baseRule.meta.hasSuggestions, - schema: baseRule.meta.schema, - messages: baseRule.meta.messages, - }, - defaultOptions: [{ treatUndefinedAsUnspecified: false }], create(context, [options]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -86,6 +74,11 @@ export default createRule({ return { ...rules, + ArrowFunctionExpression: enterFunction, + 'ArrowFunctionExpression:exit'(node): void { + exitFunction(); + rules['ArrowFunctionExpression:exit'](node); + }, FunctionDeclaration: enterFunction, 'FunctionDeclaration:exit'(node): void { exitFunction(); @@ -96,11 +89,6 @@ export default createRule({ exitFunction(); rules['FunctionExpression:exit'](node); }, - ArrowFunctionExpression: enterFunction, - 'ArrowFunctionExpression:exit'(node): void { - exitFunction(); - rules['ArrowFunctionExpression:exit'](node); - }, ReturnStatement(node): void { const functionNode = getCurrentFunction(); if ( @@ -125,4 +113,18 @@ export default createRule({ }, }; }, + defaultOptions: [{ treatUndefinedAsUnspecified: false }], + meta: { + docs: { + description: + 'Require `return` statements to either always or never specify values', + extendsBaseRule: true, + requiresTypeChecking: true, + }, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: baseRule.meta.schema, + type: 'suggestion', + }, + name: 'consistent-return', }); diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index 4c45d12e5a74..ed7e641381e5 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -23,7 +24,7 @@ export type MessageIds = type OptUnion = | { assertionStyle: 'angle-bracket' | 'as'; - objectLiteralTypeAssertions?: 'allow-as-parameter' | 'allow' | 'never'; + objectLiteralTypeAssertions?: 'allow' | 'allow-as-parameter' | 'never'; } | { assertionStyle: 'never'; @@ -31,64 +32,6 @@ type OptUnion = export type Options = readonly [OptUnion]; export default createRule({ - name: 'consistent-type-assertions', - meta: { - type: 'suggestion', - fixable: 'code', - hasSuggestions: true, - docs: { - description: 'Enforce consistent usage of type assertions', - recommended: 'stylistic', - }, - messages: { - as: "Use 'as {{cast}}' instead of '<{{cast}}>'.", - 'angle-bracket': "Use '<{{cast}}>' instead of 'as {{cast}}'.", - never: 'Do not use any type assertions.', - unexpectedObjectTypeAssertion: 'Always prefer const x: T = { ... }.', - replaceObjectTypeAssertionWithAnnotation: - 'Use const x: {{cast}} = { ... } instead.', - replaceObjectTypeAssertionWithSatisfies: - 'Use const x = { ... } satisfies {{cast}} instead.', - }, - schema: [ - { - oneOf: [ - { - type: 'object', - properties: { - assertionStyle: { - type: 'string', - enum: ['never'], - }, - }, - additionalProperties: false, - required: ['assertionStyle'], - }, - { - type: 'object', - properties: { - assertionStyle: { - type: 'string', - enum: ['as', 'angle-bracket'], - }, - objectLiteralTypeAssertions: { - type: 'string', - enum: ['allow', 'allow-as-parameter', 'never'], - }, - }, - additionalProperties: false, - required: ['assertionStyle'], - }, - ], - }, - ], - }, - defaultOptions: [ - { - assertionStyle: 'as', - objectLiteralTypeAssertions: 'allow', - }, - ], create(context, [options]) { const parserServices = getParserServices(context, true); @@ -113,8 +56,6 @@ export default createRule({ return; } context.report({ - node, - messageId, data: messageId !== 'never' ? { cast: context.sourceCode.getText(node.typeAnnotation) } @@ -167,6 +108,8 @@ export default createRule({ ); } : undefined, + messageId, + node, }); } @@ -221,7 +164,6 @@ export default createRule({ ) { const { parent } = node; suggest.push({ - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: context.sourceCode.getText(node.typeAnnotation) }, fix: fixer => [ fixer.insertTextAfter( @@ -233,10 +175,10 @@ export default createRule({ getTextWithParentheses(context.sourceCode, node.expression), ), ], + messageId: 'replaceObjectTypeAssertionWithAnnotation', }); } suggest.push({ - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: context.sourceCode.getText(node.typeAnnotation) }, fix: fixer => [ fixer.replaceText( @@ -248,27 +190,28 @@ export default createRule({ ` satisfies ${context.sourceCode.getText(node.typeAnnotation)}`, ), ], + messageId: 'replaceObjectTypeAssertionWithSatisfies', }); context.report({ - node, messageId: 'unexpectedObjectTypeAssertion', + node, suggest, }); } } return { - TSTypeAssertion(node): void { - if (options.assertionStyle !== 'angle-bracket') { + TSAsExpression(node): void { + if (options.assertionStyle !== 'as') { reportIncorrectAssertionType(node); return; } checkExpression(node); }, - TSAsExpression(node): void { - if (options.assertionStyle !== 'as') { + TSTypeAssertion(node): void { + if (options.assertionStyle !== 'angle-bracket') { reportIncorrectAssertionType(node); return; } @@ -277,4 +220,62 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow', + }, + ], + meta: { + docs: { + description: 'Enforce consistent usage of type assertions', + recommended: 'stylistic', + }, + fixable: 'code', + hasSuggestions: true, + messages: { + 'angle-bracket': "Use '<{{cast}}>' instead of 'as {{cast}}'.", + as: "Use 'as {{cast}}' instead of '<{{cast}}>'.", + never: 'Do not use any type assertions.', + replaceObjectTypeAssertionWithAnnotation: + 'Use const x: {{cast}} = { ... } instead.', + replaceObjectTypeAssertionWithSatisfies: + 'Use const x = { ... } satisfies {{cast}} instead.', + unexpectedObjectTypeAssertion: 'Always prefer const x: T = { ... }.', + }, + schema: [ + { + oneOf: [ + { + additionalProperties: false, + properties: { + assertionStyle: { + enum: ['never'], + type: 'string', + }, + }, + required: ['assertionStyle'], + type: 'object', + }, + { + additionalProperties: false, + properties: { + assertionStyle: { + enum: ['as', 'angle-bracket'], + type: 'string', + }, + objectLiteralTypeAssertions: { + enum: ['allow', 'allow-as-parameter', 'never'], + type: 'string', + }, + }, + required: ['assertionStyle'], + type: 'object', + }, + ], + }, + ], + type: 'suggestion', + }, + name: 'consistent-type-assertions', }); diff --git a/packages/eslint-plugin/src/rules/consistent-type-definitions.ts b/packages/eslint-plugin/src/rules/consistent-type-definitions.ts index 45b156fdf0ee..3cc36be45170 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-definitions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-definitions.ts @@ -1,30 +1,10 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'consistent-type-definitions', - meta: { - type: 'suggestion', - docs: { - description: - 'Enforce type definitions to consistently use either `interface` or `type`', - recommended: 'stylistic', - }, - messages: { - interfaceOverType: 'Use an `interface` instead of a `type`.', - typeOverInterface: 'Use a `type` instead of an `interface`.', - }, - schema: [ - { - type: 'string', - enum: ['interface', 'type'], - }, - ], - fixable: 'code', - }, - defaultOptions: ['interface'], create(context, [option]) { /** * Iterates from the highest parent to the currently traversed node @@ -49,8 +29,6 @@ export default createRule({ node: TSESTree.TSTypeAliasDeclaration, ): void { context.report({ - node: node.id, - messageId: 'interfaceOverType', fix(fixer) { const typeNode = node.typeParameters ?? node.id; const fixes: TSESLint.RuleFix[] = []; @@ -79,6 +57,8 @@ export default createRule({ return fixes; }, + messageId: 'interfaceOverType', + node: node.id, }); }, }), @@ -123,8 +103,8 @@ export default createRule({ return fixes; }; context.report({ - node: node.id, messageId: 'typeOverInterface', + node: node.id, /** * remove automatically fix when the interface is within a declare global * @see {@link https://github.com/typescript-eslint/typescript-eslint/issues/2707} @@ -135,4 +115,25 @@ export default createRule({ }), }; }, + defaultOptions: ['interface'], + meta: { + docs: { + description: + 'Enforce type definitions to consistently use either `interface` or `type`', + recommended: 'stylistic', + }, + fixable: 'code', + messages: { + interfaceOverType: 'Use an `interface` instead of a `type`.', + typeOverInterface: 'Use a `type` instead of an `interface`.', + }, + schema: [ + { + enum: ['interface', 'type'], + type: 'string', + }, + ], + type: 'suggestion', + }, + name: 'consistent-type-definitions', }); diff --git a/packages/eslint-plugin/src/rules/consistent-type-exports.ts b/packages/eslint-plugin/src/rules/consistent-type-exports.ts index 236659d13adb..f64d82053648 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-exports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-exports.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { SymbolFlags } from 'typescript'; @@ -19,17 +20,17 @@ type Options = [ ]; interface SourceExports { - source: string; reportValueExports: ReportValueExport[]; + source: string; typeOnlyNamedExport: TSESTree.ExportNamedDeclaration | null; valueOnlyNamedExport: TSESTree.ExportNamedDeclaration | null; } interface ReportValueExport { + inlineTypeSpecifiers: TSESTree.ExportSpecifier[]; node: TSESTree.ExportNamedDeclaration; typeBasedSpecifiers: TSESTree.ExportSpecifier[]; valueSpecifiers: TSESTree.ExportSpecifier[]; - inlineTypeSpecifiers: TSESTree.ExportSpecifier[]; } type MessageIds = @@ -38,41 +39,6 @@ type MessageIds = | 'typeOverValue'; export default createRule({ - name: 'consistent-type-exports', - meta: { - type: 'suggestion', - docs: { - description: 'Enforce consistent usage of type exports', - requiresTypeChecking: true, - }, - messages: { - typeOverValue: - 'All exports in the declaration are only used as types. Use `export type`.', - - singleExportIsType: - 'Type export {{exportNames}} is not a value and should be exported using `export type`.', - multipleExportsAreTypes: - 'Type exports {{exportNames}} are not values and should be exported using `export type`.', - }, - schema: [ - { - type: 'object', - properties: { - fixMixedExportsWithInlineTypeSpecifier: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - fixable: 'code', - }, - defaultOptions: [ - { - fixMixedExportsWithInlineTypeSpecifier: false, - }, - ], - create(context, [{ fixMixedExportsWithInlineTypeSpecifier }]) { const sourceExportsMap: Record = {}; const services = getParserServices(context); @@ -109,8 +75,8 @@ export default createRule({ const source = getSourceFromExport(node) ?? 'undefined'; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const sourceExports = (sourceExportsMap[source] ||= { - source, reportValueExports: [], + source, typeOnlyNamedExport: null, valueOnlyNamedExport: null, }); @@ -157,10 +123,10 @@ export default createRule({ (node.exportKind === 'type' && valueSpecifiers.length) ) { sourceExports.reportValueExports.push({ + inlineTypeSpecifiers, node, typeBasedSpecifiers, valueSpecifiers, - inlineTypeSpecifiers, }); } }, @@ -176,8 +142,6 @@ export default createRule({ if (report.valueSpecifiers.length === 0) { // Export is all type-only with no type specifiers; convert the entire export to `export type`. context.report({ - node: report.node, - messageId: 'typeOverValue', *fix(fixer) { yield* fixExportInsertType( fixer, @@ -185,6 +149,8 @@ export default createRule({ report.node, ); }, + messageId: 'typeOverValue', + node: report.node, }); continue; } @@ -198,8 +164,6 @@ export default createRule({ const exportNames = allExportNames[0]; context.report({ - node: report.node, - messageId: 'singleExportIsType', data: { exportNames }, *fix(fixer) { if (fixMixedExportsWithInlineTypeSpecifier) { @@ -212,13 +176,13 @@ export default createRule({ ); } }, + messageId: 'singleExportIsType', + node: report.node, }); } else { const exportNames = formatWordList(allExportNames); context.report({ - node: report.node, - messageId: 'multipleExportsAreTypes', data: { exportNames }, *fix(fixer) { if (fixMixedExportsWithInlineTypeSpecifier) { @@ -231,6 +195,8 @@ export default createRule({ ); } }, + messageId: 'multipleExportsAreTypes', + node: report.node, }); } } @@ -238,6 +204,41 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + fixMixedExportsWithInlineTypeSpecifier: false, + }, + ], + meta: { + docs: { + description: 'Enforce consistent usage of type exports', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + multipleExportsAreTypes: + 'Type exports {{exportNames}} are not values and should be exported using `export type`.', + + singleExportIsType: + 'Type export {{exportNames}} is not a value and should be exported using `export type`.', + typeOverValue: + 'All exports in the declaration are only used as types. Use `export type`.', + }, + schema: [ + { + additionalProperties: false, + properties: { + fixMixedExportsWithInlineTypeSpecifier: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + + name: 'consistent-type-exports', }); /** @@ -288,7 +289,7 @@ function* fixSeparateNamedExports( sourceCode: Readonly, report: ReportValueExport, ): IterableIterator { - const { node, typeBasedSpecifiers, inlineTypeSpecifiers, valueSpecifiers } = + const { inlineTypeSpecifiers, node, typeBasedSpecifiers, valueSpecifiers } = report; const typeSpecifiers = typeBasedSpecifiers.concat(inlineTypeSpecifiers); const source = getSourceFromExport(node); diff --git a/packages/eslint-plugin/src/rules/consistent-type-imports.ts b/packages/eslint-plugin/src/rules/consistent-type-imports.ts index b0428aa71645..a46548d36adf 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-imports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-imports.ts @@ -1,7 +1,8 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { RuleListener } from '@typescript-eslint/utils/eslint-utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + import { createRule, formatWordList, @@ -20,80 +21,36 @@ type FixStyle = 'inline-type-imports' | 'separate-type-imports'; type Options = [ { - prefer?: Prefer; disallowTypeAnnotations?: boolean; fixStyle?: FixStyle; + prefer?: Prefer; }, ]; interface SourceImports { - source: string; reportValueImports: ReportValueImport[]; + source: string; // ImportDeclaration for type-only import only with named imports. typeOnlyNamedImport: TSESTree.ImportDeclaration | null; - // ImportDeclaration for value-only import only with named imports. - valueOnlyNamedImport: TSESTree.ImportDeclaration | null; // ImportDeclaration for value-only import only with default imports and/or named imports. valueImport: TSESTree.ImportDeclaration | null; + // ImportDeclaration for value-only import only with named imports. + valueOnlyNamedImport: TSESTree.ImportDeclaration | null; } interface ReportValueImport { + inlineTypeSpecifiers: TSESTree.ImportSpecifier[]; node: TSESTree.ImportDeclaration; typeSpecifiers: TSESTree.ImportClause[]; // It has at least one element. - valueSpecifiers: TSESTree.ImportClause[]; unusedSpecifiers: TSESTree.ImportClause[]; - inlineTypeSpecifiers: TSESTree.ImportSpecifier[]; + valueSpecifiers: TSESTree.ImportClause[]; } type MessageIds = - | 'typeOverValue' - | 'someImportsAreOnlyTypes' | 'avoidImportType' - | 'noImportTypeAnnotations'; + | 'noImportTypeAnnotations' + | 'someImportsAreOnlyTypes' + | 'typeOverValue'; export default createRule({ - name: 'consistent-type-imports', - meta: { - type: 'suggestion', - docs: { - description: 'Enforce consistent usage of type imports', - }, - messages: { - typeOverValue: - 'All imports in the declaration are only used as types. Use `import type`.', - someImportsAreOnlyTypes: 'Imports {{typeImports}} are only used as type.', - - avoidImportType: 'Use an `import` instead of an `import type`.', - noImportTypeAnnotations: '`import()` type annotations are forbidden.', - }, - schema: [ - { - type: 'object', - properties: { - disallowTypeAnnotations: { - type: 'boolean', - }, - fixStyle: { - type: 'string', - enum: ['separate-type-imports', 'inline-type-imports'], - }, - prefer: { - type: 'string', - enum: ['type-imports', 'no-type-imports'], - }, - }, - additionalProperties: false, - }, - ], - fixable: 'code', - }, - - defaultOptions: [ - { - prefer: 'type-imports', - disallowTypeAnnotations: true, - fixStyle: 'separate-type-imports', - }, - ], - create(context, [option]) { const prefer = option.prefer ?? 'type-imports'; const disallowTypeAnnotations = option.disallowTypeAnnotations !== false; @@ -103,8 +60,8 @@ export default createRule({ if (disallowTypeAnnotations) { selectors.TSImportType = (node): void => { context.report({ - node, messageId: 'noImportTypeAnnotations', + node, }); }; } @@ -116,22 +73,22 @@ export default createRule({ node: TSESTree.ImportDeclaration, ): void { context.report({ - node, - messageId: 'avoidImportType', fix(fixer) { return fixRemoveTypeSpecifierFromImportDeclaration(fixer, node); }, + messageId: 'avoidImportType', + node, }); }, 'ImportSpecifier[importKind = "type"]'( node: TSESTree.ImportSpecifier, ): void { context.report({ - node, - messageId: 'avoidImportType', fix(fixer) { return fixRemoveTypeSpecifierFromImportSpecifier(fixer, node); }, + messageId: 'avoidImportType', + node, }); }, }; @@ -160,11 +117,11 @@ export default createRule({ const source = node.source.value; // sourceImports is the object containing all the specifics for a particular import source, type or value sourceImportsMap[source] ??= { - source, reportValueImports: [], // if there is a mismatch where type importKind but value specifiers + source, typeOnlyNamedImport: null, // if only type imports - valueOnlyNamedImport: null, // if only value imports with named specifiers valueImport: null, // if only value imports + valueOnlyNamedImport: null, // if only value imports with named specifiers }; const sourceImports = sourceImportsMap[source]; if (node.importKind === 'type') { @@ -287,11 +244,11 @@ export default createRule({ if (node.importKind === 'value' && typeSpecifiers.length) { sourceImports.reportValueImports.push({ + inlineTypeSpecifiers, node, typeSpecifiers, - valueSpecifiers, unusedSpecifiers, - inlineTypeSpecifiers, + valueSpecifiers, }); } }, @@ -355,8 +312,6 @@ export default createRule({ */ if (report.node.attributes.length === 0) { context.report({ - node: report.node, - messageId: 'typeOverValue', *fix(fixer) { yield* fixToTypeImportDeclaration( fixer, @@ -364,6 +319,8 @@ export default createRule({ sourceImports, ); }, + messageId: 'typeOverValue', + node: report.node, }); } } else { @@ -373,24 +330,24 @@ export default createRule({ ); const message = ((): { - messageId: MessageIds; data: Record; + messageId: MessageIds; } => { const typeImports = formatWordList(importNames); if (importNames.length === 1) { return { - messageId: 'someImportsAreOnlyTypes', data: { typeImports, }, + messageId: 'someImportsAreOnlyTypes', }; } return { - messageId: 'someImportsAreOnlyTypes', data: { typeImports, }, + messageId: 'someImportsAreOnlyTypes', }; })(); @@ -414,8 +371,8 @@ export default createRule({ function classifySpecifier(node: TSESTree.ImportDeclaration): { defaultSpecifier: TSESTree.ImportDefaultSpecifier | null; - namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null; namedSpecifiers: TSESTree.ImportSpecifier[]; + namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null; } { const defaultSpecifier = node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier @@ -432,8 +389,8 @@ export default createRule({ ); return { defaultSpecifier, - namespaceSpecifier, namedSpecifiers, + namespaceSpecifier, }; } @@ -446,13 +403,13 @@ export default createRule({ subsetNamedSpecifiers: TSESTree.ImportSpecifier[], allNamedSpecifiers: TSESTree.ImportSpecifier[], ): { - typeNamedSpecifiersText: string; removeTypeNamedSpecifiers: TSESLint.RuleFix[]; + typeNamedSpecifiersText: string; } { if (allNamedSpecifiers.length === 0) { return { - typeNamedSpecifiersText: '', removeTypeNamedSpecifiers: [], + typeNamedSpecifiersText: '', }; } const typeNamedSpecifiersTexts: string[] = []; @@ -519,8 +476,8 @@ export default createRule({ } } return { - typeNamedSpecifiersText: typeNamedSpecifiersTexts.join(','), removeTypeNamedSpecifiers, + typeNamedSpecifiersText: typeNamedSpecifiersTexts.join(','), }; } @@ -531,8 +488,8 @@ export default createRule({ namedSpecifierGroup: TSESTree.ImportSpecifier[], allNamedSpecifiers: TSESTree.ImportSpecifier[], ): { - textRange: TSESTree.Range; removeRange: TSESTree.Range; + textRange: TSESTree.Range; } { const first = namedSpecifierGroup[0]; const last = namedSpecifierGroup[namedSpecifierGroup.length - 1]; @@ -563,8 +520,8 @@ export default createRule({ } return { - textRange, removeRange, + textRange, }; } @@ -653,7 +610,7 @@ export default createRule({ ): IterableIterator { const { node } = report; - const { defaultSpecifier, namespaceSpecifier, namedSpecifiers } = + const { defaultSpecifier, namedSpecifiers, namespaceSpecifier } = classifySpecifier(node); if (namespaceSpecifier && !defaultSpecifier) { @@ -943,4 +900,48 @@ export default createRule({ yield fixer.removeRange([typeToken.range[0], afterToken.range[0]]); } }, + defaultOptions: [ + { + disallowTypeAnnotations: true, + fixStyle: 'separate-type-imports', + prefer: 'type-imports', + }, + ], + + meta: { + docs: { + description: 'Enforce consistent usage of type imports', + }, + fixable: 'code', + messages: { + avoidImportType: 'Use an `import` instead of an `import type`.', + noImportTypeAnnotations: '`import()` type annotations are forbidden.', + + someImportsAreOnlyTypes: 'Imports {{typeImports}} are only used as type.', + typeOverValue: + 'All imports in the declaration are only used as types. Use `import type`.', + }, + schema: [ + { + additionalProperties: false, + properties: { + disallowTypeAnnotations: { + type: 'boolean', + }, + fixStyle: { + enum: ['separate-type-imports', 'inline-type-imports'], + type: 'string', + }, + prefer: { + enum: ['type-imports', 'no-type-imports'], + type: 'string', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + + name: 'consistent-type-imports', }); diff --git a/packages/eslint-plugin/src/rules/default-param-last.ts b/packages/eslint-plugin/src/rules/default-param-last.ts index 1340ecfa17b1..acccc135874d 100644 --- a/packages/eslint-plugin/src/rules/default-param-last.ts +++ b/packages/eslint-plugin/src/rules/default-param-last.ts @@ -1,22 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'default-param-last', - meta: { - type: 'suggestion', - docs: { - description: 'Enforce default parameters to be last', - extendsBaseRule: true, - }, - schema: [], - messages: { - shouldBeLast: 'Default parameters should be last.', - }, - }, - defaultOptions: [], create(context) { /** * checks if node is optional parameter @@ -71,7 +59,7 @@ export default createRule({ (isOptionalParam(param) || param.type === AST_NODE_TYPES.AssignmentPattern) ) { - context.report({ node: current, messageId: 'shouldBeLast' }); + context.report({ messageId: 'shouldBeLast', node: current }); } } } @@ -82,4 +70,17 @@ export default createRule({ FunctionExpression: checkDefaultParamLast, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Enforce default parameters to be last', + extendsBaseRule: true, + }, + messages: { + shouldBeLast: 'Default parameters should be last.', + }, + schema: [], + type: 'suggestion', + }, + name: 'default-param-last', }); diff --git a/packages/eslint-plugin/src/rules/dot-notation.ts b/packages/eslint-plugin/src/rules/dot-notation.ts index 06b1c78c29d9..d9efb21a0336 100644 --- a/packages/eslint-plugin/src/rules/dot-notation.ts +++ b/packages/eslint-plugin/src/rules/dot-notation.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -7,6 +8,7 @@ import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule, getModifiers, getParserServices } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -16,56 +18,6 @@ export type Options = InferOptionsTypeFromRule; export type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ - name: 'dot-notation', - meta: { - type: 'suggestion', - docs: { - description: 'Enforce dot notation whenever possible', - recommended: 'stylistic', - extendsBaseRule: true, - requiresTypeChecking: true, - }, - schema: [ - { - type: 'object', - properties: { - allowKeywords: { - type: 'boolean', - default: true, - }, - allowPattern: { - type: 'string', - default: '', - }, - allowPrivateClassPropertyAccess: { - type: 'boolean', - default: false, - }, - allowProtectedClassPropertyAccess: { - type: 'boolean', - default: false, - }, - allowIndexSignaturePropertyAccess: { - type: 'boolean', - default: false, - }, - }, - additionalProperties: false, - }, - ], - fixable: baseRule.meta.fixable, - hasSuggestions: baseRule.meta.hasSuggestions, - messages: baseRule.meta.messages, - }, - defaultOptions: [ - { - allowPrivateClassPropertyAccess: false, - allowProtectedClassPropertyAccess: false, - allowIndexSignaturePropertyAccess: false, - allowKeywords: true, - allowPattern: '', - }, - ], create(context, [options]) { const rules = baseRule.create(context); const services = getParserServices(context); @@ -129,4 +81,54 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowIndexSignaturePropertyAccess: false, + allowKeywords: true, + allowPattern: '', + allowPrivateClassPropertyAccess: false, + allowProtectedClassPropertyAccess: false, + }, + ], + meta: { + docs: { + description: 'Enforce dot notation whenever possible', + extendsBaseRule: true, + recommended: 'stylistic', + requiresTypeChecking: true, + }, + fixable: baseRule.meta.fixable, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: [ + { + additionalProperties: false, + properties: { + allowIndexSignaturePropertyAccess: { + default: false, + type: 'boolean', + }, + allowKeywords: { + default: true, + type: 'boolean', + }, + allowPattern: { + default: '', + type: 'string', + }, + allowPrivateClassPropertyAccess: { + default: false, + type: 'boolean', + }, + allowProtectedClassPropertyAccess: { + default: false, + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'dot-notation', }); 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 9904dc2e5336..b8264cdec926 100644 --- a/packages/eslint-plugin/src/rules/explicit-function-return-type.ts +++ b/packages/eslint-plugin/src/rules/explicit-function-return-type.ts @@ -1,8 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule, nullThrows } from '../util'; import type { FunctionInfo } from '../util/explicitReturnTypeUtils'; + +import { createRule, nullThrows } from '../util'; import { ancestorHasReturnType, checkFunctionReturnType, @@ -11,14 +13,14 @@ import { type Options = [ { - allowExpressions?: boolean; - allowTypedFunctionExpressions?: boolean; - allowHigherOrderFunctions?: boolean; - allowDirectConstAssertionInArrowFunctions?: boolean; allowConciseArrowFunctionExpressionsStartingWithVoid?: boolean; - allowFunctionsWithoutTypeParameters?: boolean; + allowDirectConstAssertionInArrowFunctions?: boolean; allowedNames?: string[]; + allowExpressions?: boolean; + allowFunctionsWithoutTypeParameters?: boolean; + allowHigherOrderFunctions?: boolean; allowIIFEs?: boolean; + allowTypedFunctionExpressions?: boolean; }, ]; type MessageIds = 'missingReturnType'; @@ -29,80 +31,6 @@ type FunctionNode = | TSESTree.FunctionExpression; export default createRule({ - name: 'explicit-function-return-type', - meta: { - type: 'problem', - docs: { - description: - 'Require explicit return types on functions and class methods', - }, - messages: { - missingReturnType: 'Missing return type on function.', - }, - schema: [ - { - type: 'object', - properties: { - allowConciseArrowFunctionExpressionsStartingWithVoid: { - description: - 'Whether to allow arrow functions that start with the `void` keyword.', - type: 'boolean', - }, - allowExpressions: { - description: - 'Whether to ignore function expressions (functions which are not part of a declaration).', - type: 'boolean', - }, - allowHigherOrderFunctions: { - description: - 'Whether to ignore functions immediately returning another function expression.', - type: 'boolean', - }, - allowTypedFunctionExpressions: { - description: - 'Whether to ignore type annotations on the variable of function expressions.', - type: 'boolean', - }, - allowDirectConstAssertionInArrowFunctions: { - description: - 'Whether to ignore arrow functions immediately returning a `as const` value.', - type: 'boolean', - }, - allowFunctionsWithoutTypeParameters: { - description: - "Whether to ignore functions that don't have generic type parameters.", - type: 'boolean', - }, - allowedNames: { - description: - 'An array of function/method names that will not have their arguments or return values checked.', - items: { - type: 'string', - }, - type: 'array', - }, - allowIIFEs: { - description: - 'Whether to ignore immediately invoked function expressions (IIFEs).', - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - allowExpressions: false, - allowTypedFunctionExpressions: true, - allowHigherOrderFunctions: true, - allowDirectConstAssertionInArrowFunctions: true, - allowConciseArrowFunctionExpressionsStartingWithVoid: false, - allowFunctionsWithoutTypeParameters: false, - allowedNames: [], - allowIIFEs: false, - }, - ], create(context, [options]) { const functionInfoStack: FunctionInfo[] = []; @@ -219,9 +147,9 @@ export default createRule({ checkFunctionReturnType(info, options, context.sourceCode, loc => context.report({ - node, loc, messageId: 'missingReturnType', + node, }), ); } @@ -230,7 +158,6 @@ export default createRule({ 'ArrowFunctionExpression, FunctionExpression, FunctionDeclaration': enterFunction, 'ArrowFunctionExpression:exit': exitFunctionExpression, - 'FunctionExpression:exit': exitFunctionExpression, 'FunctionDeclaration:exit'(node): void { const info = popFunctionInfo('function declaration'); if (isAllowedFunction(node)) { @@ -242,15 +169,90 @@ export default createRule({ checkFunctionReturnType(info, options, context.sourceCode, loc => context.report({ - node, loc, messageId: 'missingReturnType', + node, }), ); }, + 'FunctionExpression:exit': exitFunctionExpression, ReturnStatement(node): void { functionInfoStack.at(-1)?.returns.push(node); }, }; }, + defaultOptions: [ + { + allowConciseArrowFunctionExpressionsStartingWithVoid: false, + allowDirectConstAssertionInArrowFunctions: true, + allowedNames: [], + allowExpressions: false, + allowFunctionsWithoutTypeParameters: false, + allowHigherOrderFunctions: true, + allowIIFEs: false, + allowTypedFunctionExpressions: true, + }, + ], + meta: { + docs: { + description: + 'Require explicit return types on functions and class methods', + }, + messages: { + missingReturnType: 'Missing return type on function.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowConciseArrowFunctionExpressionsStartingWithVoid: { + description: + 'Whether to allow arrow functions that start with the `void` keyword.', + type: 'boolean', + }, + allowDirectConstAssertionInArrowFunctions: { + description: + 'Whether to ignore arrow functions immediately returning a `as const` value.', + type: 'boolean', + }, + allowedNames: { + description: + 'An array of function/method names that will not have their arguments or return values checked.', + items: { + type: 'string', + }, + type: 'array', + }, + allowExpressions: { + description: + 'Whether to ignore function expressions (functions which are not part of a declaration).', + type: 'boolean', + }, + allowFunctionsWithoutTypeParameters: { + description: + "Whether to ignore functions that don't have generic type parameters.", + type: 'boolean', + }, + allowHigherOrderFunctions: { + description: + 'Whether to ignore functions immediately returning another function expression.', + type: 'boolean', + }, + allowIIFEs: { + description: + 'Whether to ignore immediately invoked function expressions (IIFEs).', + type: 'boolean', + }, + allowTypedFunctionExpressions: { + description: + 'Whether to ignore type annotations on the variable of function expressions.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'explicit-function-return-type', }); diff --git a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts index 5947d292f8bd..1f180ff4c5d8 100644 --- a/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts +++ b/packages/eslint-plugin/src/rules/explicit-member-accessibility.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { @@ -24,8 +25,8 @@ interface Config { accessors?: AccessibilityLevel; constructors?: AccessibilityLevel; methods?: AccessibilityLevel; - properties?: AccessibilityLevel; parameterProperties?: AccessibilityLevel; + properties?: AccessibilityLevel; }; } @@ -37,75 +38,6 @@ type MessageIds = | 'unwantedPublicAccessibility'; export default createRule({ - name: 'explicit-member-accessibility', - meta: { - hasSuggestions: true, - type: 'problem', - docs: { - description: - 'Require explicit accessibility modifiers on class properties and methods', - // too opinionated to be recommended - }, - fixable: 'code', - messages: { - missingAccessibility: - 'Missing accessibility modifier on {{type}} {{name}}.', - unwantedPublicAccessibility: - 'Public accessibility modifier on {{type}} {{name}}.', - addExplicitAccessibility: "Add '{{ type }}' accessibility modifier", - }, - schema: [ - { - $defs: { - accessibilityLevel: { - oneOf: [ - { - type: 'string', - enum: ['explicit'], - description: 'Always require an accessor.', - }, - { - type: 'string', - enum: ['no-public'], - description: 'Require an accessor except when public.', - }, - { - type: 'string', - enum: ['off'], - description: 'Never check whether there is an accessor.', - }, - ], - }, - }, - type: 'object', - properties: { - accessibility: { $ref: '#/items/0/$defs/accessibilityLevel' }, - overrides: { - type: 'object', - properties: { - accessors: { $ref: '#/items/0/$defs/accessibilityLevel' }, - constructors: { $ref: '#/items/0/$defs/accessibilityLevel' }, - methods: { $ref: '#/items/0/$defs/accessibilityLevel' }, - properties: { $ref: '#/items/0/$defs/accessibilityLevel' }, - parameterProperties: { - $ref: '#/items/0/$defs/accessibilityLevel', - }, - }, - - additionalProperties: false, - }, - ignoredMethodNames: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [{ accessibility: 'explicit' }], create(context, [option]) { const baseCheck: AccessibilityLevel = option.accessibility ?? 'explicit'; const overrides = option.overrides ?? {}; @@ -160,22 +92,22 @@ export default createRule({ ) { const publicKeyword = findPublicKeyword(methodDefinition); context.report({ - loc: rangeToLoc(context.sourceCode, publicKeyword.range), - messageId: 'unwantedPublicAccessibility', data: { - type: nodeType, name: methodName, + type: nodeType, }, fix: fixer => fixer.removeRange(publicKeyword.rangeToRemove), + loc: rangeToLoc(context.sourceCode, publicKeyword.range), + messageId: 'unwantedPublicAccessibility', }); } else if (check === 'explicit' && !methodDefinition.accessibility) { context.report({ - loc: getMemberHeadLoc(context.sourceCode, methodDefinition), - messageId: 'missingAccessibility', data: { - type: nodeType, name: methodName, + type: nodeType, }, + loc: getMemberHeadLoc(context.sourceCode, methodDefinition), + messageId: 'missingAccessibility', suggest: getMissingAccessibilitySuggestions(methodDefinition), }); } @@ -253,19 +185,19 @@ export default createRule({ return [ { - messageId: 'addExplicitAccessibility', data: { type: 'public' }, fix: fixer => fix('public', fixer), + messageId: 'addExplicitAccessibility', }, { - messageId: 'addExplicitAccessibility', data: { type: 'private' }, fix: fixer => fix('private', fixer), + messageId: 'addExplicitAccessibility', }, { - messageId: 'addExplicitAccessibility', data: { type: 'protected' }, fix: fixer => fix('protected', fixer), + messageId: 'addExplicitAccessibility', }, ]; } @@ -295,25 +227,25 @@ export default createRule({ ) { const publicKeywordRange = findPublicKeyword(propertyDefinition); context.report({ - loc: rangeToLoc(context.sourceCode, publicKeywordRange.range), - messageId: 'unwantedPublicAccessibility', data: { - type: nodeType, name: propertyName, + type: nodeType, }, fix: fixer => fixer.removeRange(publicKeywordRange.rangeToRemove), + loc: rangeToLoc(context.sourceCode, publicKeywordRange.range), + messageId: 'unwantedPublicAccessibility', }); } else if ( propCheck === 'explicit' && !propertyDefinition.accessibility ) { context.report({ - loc: getMemberHeadLoc(context.sourceCode, propertyDefinition), - messageId: 'missingAccessibility', data: { - type: nodeType, name: propertyName, + type: nodeType, }, + loc: getMemberHeadLoc(context.sourceCode, propertyDefinition), + messageId: 'missingAccessibility', suggest: getMissingAccessibilitySuggestions(propertyDefinition), }); } @@ -345,16 +277,16 @@ export default createRule({ case 'explicit': { if (!node.accessibility) { context.report({ + data: { + name: nodeName, + type: nodeType, + }, loc: getParameterPropertyHeadLoc( context.sourceCode, node, nodeName, ), messageId: 'missingAccessibility', - data: { - type: nodeType, - name: nodeName, - }, suggest: getMissingAccessibilitySuggestions(node), }); } @@ -364,13 +296,13 @@ export default createRule({ if (node.accessibility === 'public' && node.readonly) { const publicKeyword = findPublicKeyword(node); context.report({ - loc: rangeToLoc(context.sourceCode, publicKeyword.range), - messageId: 'unwantedPublicAccessibility', data: { - type: nodeType, name: nodeName, + type: nodeType, }, fix: fixer => fixer.removeRange(publicKeyword.rangeToRemove), + loc: rangeToLoc(context.sourceCode, publicKeyword.range), + messageId: 'unwantedPublicAccessibility', }); } break; @@ -386,6 +318,75 @@ export default createRule({ TSParameterProperty: checkParameterPropertyAccessibilityModifier, }; }, + defaultOptions: [{ accessibility: 'explicit' }], + meta: { + docs: { + description: + 'Require explicit accessibility modifiers on class properties and methods', + // too opinionated to be recommended + }, + fixable: 'code', + hasSuggestions: true, + messages: { + addExplicitAccessibility: "Add '{{ type }}' accessibility modifier", + missingAccessibility: + 'Missing accessibility modifier on {{type}} {{name}}.', + unwantedPublicAccessibility: + 'Public accessibility modifier on {{type}} {{name}}.', + }, + schema: [ + { + $defs: { + accessibilityLevel: { + oneOf: [ + { + description: 'Always require an accessor.', + enum: ['explicit'], + type: 'string', + }, + { + description: 'Require an accessor except when public.', + enum: ['no-public'], + type: 'string', + }, + { + description: 'Never check whether there is an accessor.', + enum: ['off'], + type: 'string', + }, + ], + }, + }, + additionalProperties: false, + properties: { + accessibility: { $ref: '#/items/0/$defs/accessibilityLevel' }, + ignoredMethodNames: { + items: { + type: 'string', + }, + type: 'array', + }, + overrides: { + additionalProperties: false, + properties: { + accessors: { $ref: '#/items/0/$defs/accessibilityLevel' }, + constructors: { $ref: '#/items/0/$defs/accessibilityLevel' }, + methods: { $ref: '#/items/0/$defs/accessibilityLevel' }, + parameterProperties: { + $ref: '#/items/0/$defs/accessibilityLevel', + }, + properties: { $ref: '#/items/0/$defs/accessibilityLevel' }, + }, + + type: 'object', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'explicit-member-accessibility', }); function rangeToLoc( @@ -393,7 +394,7 @@ function rangeToLoc( range: TSESLint.AST.Range, ): TSESTree.SourceLocation { return { - start: sourceCode.getLocFromIndex(range[0]), end: sourceCode.getLocFromIndex(range[1]), + start: sourceCode.getLocFromIndex(range[0]), }; } diff --git a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts index c8981e403941..8673c647b6de 100644 --- a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts +++ b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts @@ -1,13 +1,15 @@ -import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; + +import { DefinitionType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule, isFunction } from '../util'; import type { FunctionExpression, FunctionInfo, FunctionNode, } from '../util/explicitReturnTypeUtils'; + +import { createRule, isFunction } from '../util'; import { ancestorHasReturnType, checkFunctionExpressionReturnType, @@ -33,71 +35,6 @@ type MessageIds = | 'missingReturnType'; export default createRule({ - name: 'explicit-module-boundary-types', - meta: { - type: 'problem', - docs: { - description: - "Require explicit return and argument types on exported functions' and classes' public class methods", - }, - messages: { - missingReturnType: 'Missing return type on function.', - missingArgType: "Argument '{{name}}' should be typed.", - missingArgTypeUnnamed: '{{type}} argument should be typed.', - anyTypedArg: "Argument '{{name}}' should be typed with a non-any type.", - anyTypedArgUnnamed: - '{{type}} argument should be typed with a non-any type.', - }, - schema: [ - { - type: 'object', - properties: { - allowArgumentsExplicitlyTypedAsAny: { - description: - 'Whether to ignore arguments that are explicitly typed as `any`.', - type: 'boolean', - }, - allowDirectConstAssertionInArrowFunctions: { - description: [ - 'Whether to ignore return type annotations on body-less arrow functions that return an `as const` type assertion.', - 'You must still type the parameters of the function.', - ].join('\n'), - type: 'boolean', - }, - allowedNames: { - description: - 'An array of function/method names that will not have their arguments or return values checked.', - items: { - type: 'string', - }, - type: 'array', - }, - allowHigherOrderFunctions: { - description: [ - 'Whether to ignore return type annotations on functions immediately returning another function expression.', - 'You must still type the parameters of the function.', - ].join('\n'), - type: 'boolean', - }, - allowTypedFunctionExpressions: { - description: - 'Whether to ignore type annotations on the variable of a function expression.', - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - allowArgumentsExplicitlyTypedAsAny: false, - allowDirectConstAssertionInArrowFunctions: true, - allowedNames: [], - allowHigherOrderFunctions: true, - allowTypedFunctionExpressions: true, - }, - ], create(context, [options]) { // tracks all of the functions we've already checked const checkedFunctions = new Set(); @@ -139,6 +76,9 @@ export default createRule({ */ return { + 'ArrowFunctionExpression, FunctionDeclaration, FunctionExpression': + enterFunction, + 'ArrowFunctionExpression:exit': exitFunction, 'ExportDefaultDeclaration:exit'(node): void { checkNode(node.declaration); }, @@ -153,12 +93,6 @@ export default createRule({ } } }, - 'TSExportAssignment:exit'(node): void { - checkNode(node.expression); - }, - 'ArrowFunctionExpression, FunctionDeclaration, FunctionExpression': - enterFunction, - 'ArrowFunctionExpression:exit': exitFunction, 'FunctionDeclaration:exit': exitFunction, 'FunctionExpression:exit': exitFunction, 'Program:exit'(): void { @@ -172,6 +106,9 @@ export default createRule({ const current = functionStack[functionStack.length - 1]; functionReturnsMap.get(current)?.push(node); }, + 'TSExportAssignment:exit'(node): void { + checkNode(node.expression); + }, }; function checkParameters( @@ -184,34 +121,34 @@ export default createRule({ ): void { if (param.type === AST_NODE_TYPES.Identifier) { context.report({ - node: param, - messageId: namedMessageId, data: { name: param.name }, + messageId: namedMessageId, + node: param, }); } else if (param.type === AST_NODE_TYPES.ArrayPattern) { context.report({ - node: param, - messageId: unnamedMessageId, data: { type: 'Array pattern' }, + messageId: unnamedMessageId, + node: param, }); } else if (param.type === AST_NODE_TYPES.ObjectPattern) { context.report({ - node: param, - messageId: unnamedMessageId, data: { type: 'Object pattern' }, + messageId: unnamedMessageId, + node: param, }); } else if (param.type === AST_NODE_TYPES.RestElement) { if (param.argument.type === AST_NODE_TYPES.Identifier) { context.report({ - node: param, - messageId: namedMessageId, data: { name: param.argument.name }, + messageId: namedMessageId, + node: param, }); } else { context.report({ - node: param, - messageId: unnamedMessageId, data: { type: 'Rest' }, + messageId: unnamedMessageId, + node: param, }); } } @@ -331,9 +268,9 @@ export default createRule({ // cases we don't care about in this rule if ( [ + DefinitionType.CatchClause, DefinitionType.ImplicitGlobalVariable, DefinitionType.ImportBinding, - DefinitionType.CatchClause, DefinitionType.Parameter, ].includes(definition.type) ) { @@ -443,8 +380,8 @@ export default createRule({ node.parent.kind === 'set'; if (!isConstructor && !isSetAccessor && !node.returnType) { context.report({ - node, messageId: 'missingReturnType', + node, }); } @@ -474,9 +411,9 @@ export default createRule({ context.sourceCode, loc => { context.report({ - node, loc, messageId: 'missingReturnType', + node, }); }, ); @@ -503,9 +440,9 @@ export default createRule({ context.sourceCode, loc => { context.report({ - node, loc, messageId: 'missingReturnType', + node, }); }, ); @@ -513,4 +450,69 @@ export default createRule({ checkParameters(node); } }, + defaultOptions: [ + { + allowArgumentsExplicitlyTypedAsAny: false, + allowDirectConstAssertionInArrowFunctions: true, + allowedNames: [], + allowHigherOrderFunctions: true, + allowTypedFunctionExpressions: true, + }, + ], + meta: { + docs: { + description: + "Require explicit return and argument types on exported functions' and classes' public class methods", + }, + messages: { + anyTypedArg: "Argument '{{name}}' should be typed with a non-any type.", + anyTypedArgUnnamed: + '{{type}} argument should be typed with a non-any type.', + missingArgType: "Argument '{{name}}' should be typed.", + missingArgTypeUnnamed: '{{type}} argument should be typed.', + missingReturnType: 'Missing return type on function.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowArgumentsExplicitlyTypedAsAny: { + description: + 'Whether to ignore arguments that are explicitly typed as `any`.', + type: 'boolean', + }, + allowDirectConstAssertionInArrowFunctions: { + description: [ + 'Whether to ignore return type annotations on body-less arrow functions that return an `as const` type assertion.', + 'You must still type the parameters of the function.', + ].join('\n'), + type: 'boolean', + }, + allowedNames: { + description: + 'An array of function/method names that will not have their arguments or return values checked.', + items: { + type: 'string', + }, + type: 'array', + }, + allowHigherOrderFunctions: { + description: [ + 'Whether to ignore return type annotations on functions immediately returning another function expression.', + 'You must still type the parameters of the function.', + ].join('\n'), + type: 'boolean', + }, + allowTypedFunctionExpressions: { + description: + 'Whether to ignore type annotations on the variable of a function expression.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'explicit-module-boundary-types', }); diff --git a/packages/eslint-plugin/src/rules/init-declarations.ts b/packages/eslint-plugin/src/rules/init-declarations.ts index 53ad84350e30..106587e8c47e 100644 --- a/packages/eslint-plugin/src/rules/init-declarations.ts +++ b/packages/eslint-plugin/src/rules/init-declarations.ts @@ -1,10 +1,12 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -14,19 +16,6 @@ export type Options = InferOptionsTypeFromRule; export type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ - name: 'init-declarations', - meta: { - type: 'suggestion', - docs: { - description: - 'Require or disallow initialization in variable declarations', - extendsBaseRule: true, - }, - hasSuggestions: baseRule.meta.hasSuggestions, - schema: baseRule.meta.schema, - messages: baseRule.meta.messages, - }, - defaultOptions: ['always'], create(context, [mode]) { // Make a custom context to adjust the loc of reports where the base // rule's behavior is a bit too aggressive with TS-specific syntax (namely, @@ -108,6 +97,19 @@ export default createRule({ return false; } }, + defaultOptions: ['always'], + meta: { + docs: { + description: + 'Require or disallow initialization in variable declarations', + extendsBaseRule: true, + }, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: baseRule.meta.schema, + type: 'suggestion', + }, + name: 'init-declarations', }); /** @@ -128,7 +130,7 @@ function getReportLoc( }; return { - start, end, + start, }; } diff --git a/packages/eslint-plugin/src/rules/max-params.ts b/packages/eslint-plugin/src/rules/max-params.ts index 622848affce1..e72d88b75a31 100644 --- a/packages/eslint-plugin/src/rules/max-params.ts +++ b/packages/eslint-plugin/src/rules/max-params.ts @@ -1,17 +1,19 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; type FunctionLike = + | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.ArrowFunctionExpression; + | TSESTree.FunctionExpression; type FunctionRuleListener = (node: T) => void; @@ -21,37 +23,6 @@ export type Options = InferOptionsTypeFromRule; export type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ - name: 'max-params', - meta: { - type: 'suggestion', - docs: { - description: - 'Enforce a maximum number of parameters in function definitions', - extendsBaseRule: true, - }, - schema: [ - { - type: 'object', - properties: { - maximum: { - type: 'integer', - minimum: 0, - }, - max: { - type: 'integer', - minimum: 0, - }, - countVoidThis: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - messages: baseRule.meta.messages, - }, - defaultOptions: [{ max: 3, countVoidThis: false }], - create(context, [{ countVoidThis }]) { const baseRules = baseRule.create(context); @@ -90,4 +61,35 @@ export default createRule({ FunctionExpression: wrapListener(baseRules.FunctionExpression), }; }, + defaultOptions: [{ countVoidThis: false, max: 3 }], + meta: { + docs: { + description: + 'Enforce a maximum number of parameters in function definitions', + extendsBaseRule: true, + }, + messages: baseRule.meta.messages, + schema: [ + { + additionalProperties: false, + properties: { + countVoidThis: { + type: 'boolean', + }, + max: { + minimum: 0, + type: 'integer', + }, + maximum: { + minimum: 0, + type: 'integer', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + + name: 'max-params', }); diff --git a/packages/eslint-plugin/src/rules/member-ordering.ts b/packages/eslint-plugin/src/rules/member-ordering.ts index 93e5f45adcdc..64e2220c8eab 100644 --- a/packages/eslint-plugin/src/rules/member-ordering.ts +++ b/packages/eslint-plugin/src/rules/member-ordering.ts @@ -2,6 +2,7 @@ /* eslint-disable eslint-plugin/no-property-in-node */ import type { JSONSchema, TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import naturalCompare from 'natural-compare'; @@ -20,7 +21,6 @@ export type MessageIds = type ReadonlyType = 'readonly-field' | 'readonly-signature'; type MemberKind = - | ReadonlyType | 'accessor' | 'call-signature' | 'constructor' @@ -29,15 +29,16 @@ type MemberKind = | 'method' | 'set' | 'signature' - | 'static-initialization'; + | 'static-initialization' + | ReadonlyType; type DecoratedMemberKind = - | Exclude | 'accessor' | 'field' | 'get' | 'method' - | 'set'; + | 'set' + | Exclude; type NonCallableMemberKind = Exclude< MemberKind, @@ -46,10 +47,9 @@ type NonCallableMemberKind = Exclude< type MemberScope = 'abstract' | 'instance' | 'static'; -type Accessibility = TSESTree.Accessibility | '#private'; +type Accessibility = '#private' | TSESTree.Accessibility; type BaseMemberType = - | MemberKind | `${Accessibility}-${Exclude< MemberKind, 'readonly-signature' | 'signature' | 'static-initialization' @@ -57,75 +57,76 @@ type BaseMemberType = | `${Accessibility}-${MemberScope}-${NonCallableMemberKind}` | `${Accessibility}-decorated-${DecoratedMemberKind}` | `${MemberScope}-${NonCallableMemberKind}` - | `decorated-${DecoratedMemberKind}`; + | `decorated-${DecoratedMemberKind}` + | MemberKind; type MemberType = BaseMemberType | BaseMemberType[]; type AlphabeticalOrder = - | 'alphabetically-case-insensitive' | 'alphabetically' - | 'natural-case-insensitive' - | 'natural'; + | 'alphabetically-case-insensitive' + | 'natural' + | 'natural-case-insensitive'; -type Order = AlphabeticalOrder | 'as-written'; +type Order = 'as-written' | AlphabeticalOrder; interface SortedOrderConfig { - memberTypes?: MemberType[] | 'never'; + memberTypes?: 'never' | MemberType[]; optionalityOrder?: OptionalityOrder; order?: Order; } -type OrderConfig = MemberType[] | SortedOrderConfig | 'never'; +type OrderConfig = 'never' | MemberType[] | SortedOrderConfig; type Member = TSESTree.ClassElement | TSESTree.TypeElement; type OptionalityOrder = 'optional-first' | 'required-first'; export type Options = [ { - default?: OrderConfig; classes?: OrderConfig; classExpressions?: OrderConfig; + default?: OrderConfig; interfaces?: OrderConfig; typeLiterals?: OrderConfig; }, ]; const neverConfig: JSONSchema.JSONSchema4 = { - type: 'string', enum: ['never'], + type: 'string', }; const arrayConfig = (memberTypes: string): JSONSchema.JSONSchema4 => ({ - type: 'array', items: { oneOf: [ { $ref: memberTypes, }, { - type: 'array', items: { $ref: memberTypes, }, + type: 'array', }, ], }, + type: 'array', }); const objectConfig = (memberTypes: string): JSONSchema.JSONSchema4 => ({ - type: 'object', + additionalProperties: false, properties: { memberTypes: { oneOf: [arrayConfig(memberTypes), neverConfig], }, - order: { - $ref: '#/items/0/$defs/orderOptions', - }, optionalityOrder: { $ref: '#/items/0/$defs/optionalityOrderOptions', }, + order: { + $ref: '#/items/0/$defs/orderOptions', + }, }, - additionalProperties: false, + type: 'object', }); export const defaultOrder: MemberType[] = [ @@ -415,12 +416,12 @@ function getNodeType(node: Member): MemberKind | null { */ function getMemberRawName( member: - | TSESTree.MethodDefinition | TSESTree.AccessorProperty + | TSESTree.MethodDefinition | TSESTree.Property | TSESTree.PropertyDefinition - | TSESTree.TSAbstractMethodDefinition | TSESTree.TSAbstractAccessorProperty + | TSESTree.TSAbstractMethodDefinition | TSESTree.TSAbstractPropertyDefinition | TSESTree.TSMethodSignature | TSESTree.TSPropertySignature, @@ -717,96 +718,6 @@ function getLowestRank( } export default createRule({ - name: 'member-ordering', - meta: { - type: 'suggestion', - docs: { - description: 'Require a consistent member declaration order', - }, - messages: { - incorrectOrder: - 'Member {{member}} should be declared before member {{beforeMember}}.', - incorrectGroupOrder: - 'Member {{name}} should be declared before all {{rank}} definitions.', - incorrectRequiredMembersOrder: `Member {{member}} should be declared after all {{optionalOrRequired}} members.`, - }, - schema: [ - { - $defs: { - orderOptions: { - type: 'string', - enum: [ - 'alphabetically', - 'alphabetically-case-insensitive', - 'as-written', - 'natural', - 'natural-case-insensitive', - ], - }, - optionalityOrderOptions: { - type: 'string', - enum: ['optional-first', 'required-first'], - }, - allItems: { - type: 'string', - enum: allMemberTypes as string[], - }, - typeItems: { - type: 'string', - enum: [ - 'readonly-signature', - 'signature', - 'readonly-field', - 'field', - 'method', - 'constructor', - ], - }, - - baseConfig: { - oneOf: [ - neverConfig, - arrayConfig('#/items/0/$defs/allItems'), - objectConfig('#/items/0/$defs/allItems'), - ], - }, - typesConfig: { - oneOf: [ - neverConfig, - arrayConfig('#/items/0/$defs/typeItems'), - objectConfig('#/items/0/$defs/typeItems'), - ], - }, - }, - type: 'object', - properties: { - default: { - $ref: '#/items/0/$defs/baseConfig', - }, - classes: { - $ref: '#/items/0/$defs/baseConfig', - }, - classExpressions: { - $ref: '#/items/0/$defs/baseConfig', - }, - interfaces: { - $ref: '#/items/0/$defs/typesConfig', - }, - typeLiterals: { - $ref: '#/items/0/$defs/typesConfig', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - default: { - memberTypes: defaultOrder, - }, - }, - ], create(context, [options]) { /** * Checks if the member groups are correctly sorted. @@ -839,12 +750,12 @@ export default createRule({ // Works for 1st item because x < undefined === false for any x (typeof string) if (rank < rankLastMember) { context.report({ - node: member, - messageId: 'incorrectGroupOrder', data: { name, rank: getLowestRank(previousRanks, rank, groupOrder), }, + messageId: 'incorrectGroupOrder', + node: member, }); isCorrectlySorted = false; @@ -884,12 +795,12 @@ export default createRule({ if (name) { if (naturalOutOfOrder(name, previousName, order)) { context.report({ - node: member, - messageId: 'incorrectOrder', data: { - member: name, beforeMember: previousName, + member: name, }, + messageId: 'incorrectOrder', + node: member, }); isCorrectlySorted = false; @@ -945,13 +856,13 @@ export default createRule({ const report = (member: Member): void => context.report({ - messageId: 'incorrectRequiredMembersOrder', - loc: member.loc, data: { member: getMemberName(member, context.sourceCode), optionalOrRequired: optionalityOrder === 'required-first' ? 'required' : 'optional', }, + loc: member.loc, + messageId: 'incorrectRequiredMembersOrder', }); // if the optionality of the first item is correct (based on optionalityOrder) @@ -1075,11 +986,6 @@ export default createRule({ // https://github.com/typescript-eslint/typescript-eslint/issues/5439 /* eslint-disable @typescript-eslint/no-non-null-assertion */ return { - 'ClassDeclaration, FunctionDeclaration'(node): void { - if ('superClass' in node) { - // ... - } - }, ClassDeclaration(node): void { validateMembersOrder( node.body.body, @@ -1087,6 +993,11 @@ export default createRule({ true, ); }, + 'ClassDeclaration, FunctionDeclaration'(node): void { + if ('superClass' in node) { + // ... + } + }, ClassExpression(node): void { validateMembersOrder( node.body.body, @@ -1111,4 +1022,94 @@ export default createRule({ }; /* eslint-enable @typescript-eslint/no-non-null-assertion */ }, + defaultOptions: [ + { + default: { + memberTypes: defaultOrder, + }, + }, + ], + meta: { + docs: { + description: 'Require a consistent member declaration order', + }, + messages: { + incorrectGroupOrder: + 'Member {{name}} should be declared before all {{rank}} definitions.', + incorrectOrder: + 'Member {{member}} should be declared before member {{beforeMember}}.', + incorrectRequiredMembersOrder: `Member {{member}} should be declared after all {{optionalOrRequired}} members.`, + }, + schema: [ + { + $defs: { + allItems: { + enum: allMemberTypes as string[], + type: 'string', + }, + baseConfig: { + oneOf: [ + neverConfig, + arrayConfig('#/items/0/$defs/allItems'), + objectConfig('#/items/0/$defs/allItems'), + ], + }, + optionalityOrderOptions: { + enum: ['optional-first', 'required-first'], + type: 'string', + }, + orderOptions: { + enum: [ + 'alphabetically', + 'alphabetically-case-insensitive', + 'as-written', + 'natural', + 'natural-case-insensitive', + ], + type: 'string', + }, + + typeItems: { + enum: [ + 'readonly-signature', + 'signature', + 'readonly-field', + 'field', + 'method', + 'constructor', + ], + type: 'string', + }, + typesConfig: { + oneOf: [ + neverConfig, + arrayConfig('#/items/0/$defs/typeItems'), + objectConfig('#/items/0/$defs/typeItems'), + ], + }, + }, + additionalProperties: false, + properties: { + classes: { + $ref: '#/items/0/$defs/baseConfig', + }, + classExpressions: { + $ref: '#/items/0/$defs/baseConfig', + }, + default: { + $ref: '#/items/0/$defs/baseConfig', + }, + interfaces: { + $ref: '#/items/0/$defs/typesConfig', + }, + typeLiterals: { + $ref: '#/items/0/$defs/typesConfig', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'member-ordering', }); diff --git a/packages/eslint-plugin/src/rules/method-signature-style.ts b/packages/eslint-plugin/src/rules/method-signature-style.ts index 48207e5cc91a..2939dd242182 100644 --- a/packages/eslint-plugin/src/rules/method-signature-style.ts +++ b/packages/eslint-plugin/src/rules/method-signature-style.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { @@ -14,28 +15,6 @@ export type Options = [('method' | 'property')?]; export type MessageIds = 'errorMethod' | 'errorProperty'; export default createRule({ - name: 'method-signature-style', - meta: { - type: 'suggestion', - docs: { - description: 'Enforce using a particular method signature syntax', - }, - fixable: 'code', - messages: { - errorMethod: - 'Shorthand method signature is forbidden. Use a function property instead.', - errorProperty: - 'Function property signature is forbidden. Use a method shorthand instead.', - }, - schema: [ - { - type: 'string', - enum: ['property', 'method'], - }, - ], - }, - defaultOptions: ['property'], - create(context, [mode]) { function getMethodKey( node: TSESTree.TSMethodSignature | TSESTree.TSPropertySignature, @@ -147,13 +126,11 @@ export default createRule({ if (duplicatedKeyMethodNodes.length > 0) { if (isParentModule) { context.report({ - node: methodNode, messageId: 'errorMethod', + node: methodNode, }); } else { context.report({ - node: methodNode, - messageId: 'errorMethod', *fix(fixer) { const methodNodes = [ methodNode, @@ -187,6 +164,8 @@ export default createRule({ } } }, + messageId: 'errorMethod', + node: methodNode, }); } return; @@ -194,13 +173,11 @@ export default createRule({ if (isParentModule) { context.report({ - node: methodNode, messageId: 'errorMethod', + node: methodNode, }); } else { context.report({ - node: methodNode, - messageId: 'errorMethod', fix: fixer => { const key = getMethodKey(methodNode); const params = getMethodParams(methodNode); @@ -211,6 +188,8 @@ export default createRule({ `${key}: ${params} => ${returnType}${delimiter}`, ); }, + messageId: 'errorMethod', + node: methodNode, }); } }, @@ -223,8 +202,6 @@ export default createRule({ } context.report({ - node: propertyNode, - messageId: 'errorProperty', fix: fixer => { const key = getMethodKey(propertyNode); const params = getMethodParams(typeNode); @@ -235,9 +212,33 @@ export default createRule({ `${key}${params}: ${returnType}${delimiter}`, ); }, + messageId: 'errorProperty', + node: propertyNode, }); }, }), }; }, + defaultOptions: ['property'], + meta: { + docs: { + description: 'Enforce using a particular method signature syntax', + }, + fixable: 'code', + messages: { + errorMethod: + 'Shorthand method signature is forbidden. Use a function property instead.', + errorProperty: + 'Function property signature is forbidden. Use a method shorthand instead.', + }, + schema: [ + { + enum: ['property', 'method'], + type: 'string', + }, + ], + type: 'suggestion', + }, + + name: 'method-signature-style', }); diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts index c30029882e16..7e26a97897ca 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts @@ -22,27 +22,27 @@ type UnderscoreOptionsString = keyof typeof UnderscoreOptions; enum Selectors { // variableLike - variable = 1 << 0, function = 1 << 1, parameter = 1 << 2, + variable = 1 << 0, // memberLike - parameterProperty = 1 << 3, + autoAccessor = 1 << 12, classicAccessor = 1 << 4, - enumMember = 1 << 5, classMethod = 1 << 6, - objectLiteralMethod = 1 << 7, - typeMethod = 1 << 8, classProperty = 1 << 9, + enumMember = 1 << 5, + objectLiteralMethod = 1 << 7, objectLiteralProperty = 1 << 10, + parameterProperty = 1 << 3, + typeMethod = 1 << 8, typeProperty = 1 << 11, - autoAccessor = 1 << 12, // typeLike class = 1 << 13, + enum = 1 << 16, interface = 1 << 14, typeAlias = 1 << 15, - enum = 1 << 16, typeParameter = 1 << 17, // other @@ -52,11 +52,8 @@ type SelectorsString = keyof typeof Selectors; enum MetaSelectors { /* eslint-disable @typescript-eslint/prefer-literal-enum-member */ + accessor = 0 | Selectors.classicAccessor | Selectors.autoAccessor, default = -1, - variableLike = 0 | - Selectors.variable | - Selectors.function | - Selectors.parameter, memberLike = 0 | Selectors.classProperty | Selectors.objectLiteralProperty | @@ -68,12 +65,6 @@ enum MetaSelectors { Selectors.typeMethod | Selectors.classicAccessor | Selectors.autoAccessor, - typeLike = 0 | - Selectors.class | - Selectors.interface | - Selectors.typeAlias | - Selectors.enum | - Selectors.typeParameter, method = 0 | Selectors.classMethod | Selectors.objectLiteralMethod | @@ -82,7 +73,16 @@ enum MetaSelectors { Selectors.classProperty | Selectors.objectLiteralProperty | Selectors.typeProperty, - accessor = 0 | Selectors.classicAccessor | Selectors.autoAccessor, + typeLike = 0 | + Selectors.class | + Selectors.interface | + Selectors.typeAlias | + Selectors.enum | + Selectors.typeParameter, + variableLike = 0 | + Selectors.variable | + Selectors.function | + Selectors.parameter, /* eslint-enable @typescript-eslint/prefer-literal-enum-member */ } type MetaSelectorsString = keyof typeof MetaSelectors; @@ -96,11 +96,11 @@ enum Modifiers { // static members static = 1 << 2, // member accessibility - public = 1 << 3, - protected = 1 << 4, - private = 1 << 5, '#private' = 1 << 6, abstract = 1 << 7, + private = 1 << 5, + protected = 1 << 4, + public = 1 << 3, // destructured variable destructured = 1 << 8, // variables declared in the top-level scope @@ -125,11 +125,11 @@ enum Modifiers { type ModifiersString = keyof typeof Modifiers; enum TypeModifiers { + array = 1 << 21, boolean = 1 << 17, - string = 1 << 18, - number = 1 << 19, function = 1 << 20, - array = 1 << 21, + number = 1 << 19, + string = 1 << 18, } type TypeModifiersString = keyof typeof TypeModifiers; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/format.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/format.ts index d3db62399eaa..e8fdbdfd4f52 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/format.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/format.ts @@ -99,12 +99,12 @@ function validateUnderscores(name: string): boolean { const PredefinedFormatToCheckFunction: Readonly< Record boolean> > = { - [PredefinedFormats.PascalCase]: isPascalCase, - [PredefinedFormats.StrictPascalCase]: isStrictPascalCase, [PredefinedFormats.camelCase]: isCamelCase, + [PredefinedFormats.PascalCase]: isPascalCase, + [PredefinedFormats.snake_case]: isSnakeCase, [PredefinedFormats.strictCamelCase]: isStrictCamelCase, + [PredefinedFormats.StrictPascalCase]: isStrictPascalCase, [PredefinedFormats.UPPER_CASE]: isUpperCase, - [PredefinedFormats.snake_case]: isSnakeCase, }; export { PredefinedFormatToCheckFunction }; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/index.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/index.ts index 56297213b66c..11d3953c76fa 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/index.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/index.ts @@ -1,6 +1,6 @@ export { Modifiers } from './enums'; export type { PredefinedFormatsString } from './enums'; -export type { Context, Selector, ValidatorFunction } from './types'; +export { parseOptions } from './parse-options'; export { SCHEMA } from './schema'; export { selectorTypeToMessageString } from './shared'; -export { parseOptions } from './parse-options'; +export type { Context, Selector, ValidatorFunction } from './types'; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/parse-options.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/parse-options.ts index 08ac8d41c8b0..199be4ccf7e9 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/parse-options.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/parse-options.ts @@ -1,3 +1,10 @@ +import type { + Context, + NormalizedSelector, + ParsedOptions, + Selector, +} from './types'; + import { getEnumNames } from '../../util'; import { MetaSelectors, @@ -8,12 +15,6 @@ import { UnderscoreOptions, } from './enums'; import { isMetaSelector } from './shared'; -import type { - Context, - NormalizedSelector, - ParsedOptions, - Selector, -} from './types'; import { createValidator } from './validator'; function normalizeOption(option: Selector): NormalizedSelector[] { @@ -32,37 +33,37 @@ function normalizeOption(option: Selector): NormalizedSelector[] { const normalizedOption = { // format options - format: option.format ? option.format.map(f => PredefinedFormats[f]) : null, custom: option.custom ? { - regex: new RegExp(option.custom.regex, 'u'), match: option.custom.match, + regex: new RegExp(option.custom.regex, 'u'), } : null, - leadingUnderscore: - option.leadingUnderscore !== undefined - ? UnderscoreOptions[option.leadingUnderscore] - : null, - trailingUnderscore: - option.trailingUnderscore !== undefined - ? UnderscoreOptions[option.trailingUnderscore] - : null, - prefix: option.prefix && option.prefix.length > 0 ? option.prefix : null, - suffix: option.suffix && option.suffix.length > 0 ? option.suffix : null, - modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null, - types: option.types?.map(m => TypeModifiers[m]) ?? null, filter: option.filter !== undefined ? typeof option.filter === 'string' ? { - regex: new RegExp(option.filter, 'u'), match: true, + regex: new RegExp(option.filter, 'u'), } : { - regex: new RegExp(option.filter.regex, 'u'), match: option.filter.match, + regex: new RegExp(option.filter.regex, 'u'), } : null, + format: option.format ? option.format.map(f => PredefinedFormats[f]) : null, + leadingUnderscore: + option.leadingUnderscore !== undefined + ? UnderscoreOptions[option.leadingUnderscore] + : null, + modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null, + prefix: option.prefix && option.prefix.length > 0 ? option.prefix : null, + suffix: option.suffix && option.suffix.length > 0 ? option.suffix : null, + trailingUnderscore: + option.trailingUnderscore !== undefined + ? UnderscoreOptions[option.trailingUnderscore] + : null, + types: option.types?.map(m => TypeModifiers[m]) ?? null, // calculated ordering weight based on modifiers modifierWeight: weight, }; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts index d7016e217f2f..13e4f9a8b3cd 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts @@ -1,10 +1,11 @@ import type { JSONSchema } from '@typescript-eslint/utils'; -import { getEnumNames } from '../../util'; import type { IndividualAndMetaSelectorsString, ModifiersString, } from './enums'; + +import { getEnumNames } from '../../util'; import { MetaSelectors, Modifiers, @@ -16,51 +17,51 @@ import { const $DEFS: Record = { // enums - underscoreOptions: { - type: 'string', - enum: getEnumNames(UnderscoreOptions), - }, predefinedFormats: { - type: 'string', enum: getEnumNames(PredefinedFormats), + type: 'string', }, typeModifiers: { - type: 'string', enum: getEnumNames(TypeModifiers), + type: 'string', + }, + underscoreOptions: { + enum: getEnumNames(UnderscoreOptions), + type: 'string', }, // repeated types - prefixSuffixConfig: { - type: 'array', - items: { - type: 'string', - minLength: 1, - }, - additionalItems: false, - }, - matchRegexConfig: { - type: 'object', - additionalProperties: false, - properties: { - match: { type: 'boolean' }, - regex: { type: 'string' }, - }, - required: ['match', 'regex'], - }, formatOptionsConfig: { oneOf: [ { - type: 'array', + additionalItems: false, items: { $ref: '#/$defs/predefinedFormats', }, - additionalItems: false, + type: 'array', }, { type: 'null', }, ], }, + matchRegexConfig: { + additionalProperties: false, + properties: { + match: { type: 'boolean' }, + regex: { type: 'string' }, + }, + required: ['match', 'regex'], + type: 'object', + }, + prefixSuffixConfig: { + additionalItems: false, + items: { + minLength: 1, + type: 'string', + }, + type: 'array', + }, }; const UNDERSCORE_SCHEMA: JSONSchema.JSONSchema4 = { @@ -74,17 +75,17 @@ const MATCH_REGEX_SCHEMA: JSONSchema.JSONSchema4 = { }; type JSONSchemaProperties = Record; const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { + custom: MATCH_REGEX_SCHEMA, + failureMessage: { + type: 'string', + }, format: { $ref: '#/$defs/formatOptionsConfig', }, - custom: MATCH_REGEX_SCHEMA, leadingUnderscore: UNDERSCORE_SCHEMA, - trailingUnderscore: UNDERSCORE_SCHEMA, prefix: PREFIX_SUFFIX_SCHEMA, suffix: PREFIX_SUFFIX_SCHEMA, - failureMessage: { - type: 'string', - }, + trailingUnderscore: UNDERSCORE_SCHEMA, }; function selectorSchema( selectorString: IndividualAndMetaSelectorsString, @@ -95,98 +96,98 @@ function selectorSchema( filter: { oneOf: [ { - type: 'string', minLength: 1, + type: 'string', }, MATCH_REGEX_SCHEMA, ], }, selector: { - type: 'string', enum: [selectorString], + type: 'string', }, }; if (modifiers && modifiers.length > 0) { selector.modifiers = { - type: 'array', + additionalItems: false, items: { - type: 'string', enum: modifiers, + type: 'string', }, - additionalItems: false, + type: 'array', }; } if (allowType) { selector.types = { - type: 'array', + additionalItems: false, items: { $ref: '#/$defs/typeModifiers', }, - additionalItems: false, + type: 'array', }; } return [ { - type: 'object', + additionalProperties: false, description: `Selector '${selectorString}'`, properties: { ...FORMAT_OPTIONS_PROPERTIES, ...selector, }, required: ['selector', 'format'], - additionalProperties: false, + type: 'object', }, ]; } function selectorsSchema(): JSONSchema.JSONSchema4 { return { - type: 'object', + additionalProperties: false, description: 'Multiple selectors in one config', properties: { ...FORMAT_OPTIONS_PROPERTIES, filter: { oneOf: [ { - type: 'string', minLength: 1, + type: 'string', }, MATCH_REGEX_SCHEMA, ], }, - selector: { - type: 'array', + modifiers: { + additionalItems: false, items: { + enum: getEnumNames(Modifiers), type: 'string', - enum: [...getEnumNames(MetaSelectors), ...getEnumNames(Selectors)], }, - additionalItems: false, - }, - modifiers: { type: 'array', + }, + selector: { + additionalItems: false, items: { + enum: [...getEnumNames(MetaSelectors), ...getEnumNames(Selectors)], type: 'string', - enum: getEnumNames(Modifiers), }, - additionalItems: false, + type: 'array', }, types: { - type: 'array', + additionalItems: false, items: { $ref: '#/$defs/typeModifiers', }, - additionalItems: false, + type: 'array', }, }, required: ['selector', 'format'], - additionalProperties: false, + type: 'object', }; } const SCHEMA: JSONSchema.JSONSchema4 = { $defs: $DEFS, - type: 'array', + additionalItems: false, items: { oneOf: [ selectorsSchema(), @@ -326,7 +327,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { ...selectorSchema('import', false, ['default', 'namespace']), ], }, - additionalItems: false, + type: 'array', }; export { SCHEMA }; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/shared.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/shared.ts index a7f3e7f6093e..c1c31b78ebe8 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/shared.ts @@ -4,6 +4,7 @@ import type { Selectors, SelectorsString, } from './enums'; + import { MetaSelectors } from './enums'; function selectorTypeToMessageString(selectorType: SelectorsString): string { @@ -26,7 +27,7 @@ function isMethodOrPropertySelector( } export { - selectorTypeToMessageString, isMetaSelector, isMethodOrPropertySelector, + selectorTypeToMessageString, }; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/types.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/types.ts index fd0910050e00..41df37e8cb15 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/types.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/types.ts @@ -17,47 +17,47 @@ import type { } from './enums'; interface MatchRegex { - regex: string; match: boolean; + regex: string; } interface Selector { + custom?: MatchRegex; + filter?: MatchRegex | string; // format options format: PredefinedFormatsString[] | null; - custom?: MatchRegex; leadingUnderscore?: UnderscoreOptionsString; - trailingUnderscore?: UnderscoreOptionsString; + modifiers?: ModifiersString[]; prefix?: string[]; - suffix?: string[]; // selector options selector: | IndividualAndMetaSelectorsString | IndividualAndMetaSelectorsString[]; - modifiers?: ModifiersString[]; + suffix?: string[]; + trailingUnderscore?: UnderscoreOptionsString; types?: TypeModifiersString[]; - filter?: MatchRegex | string; } interface NormalizedMatchRegex { - regex: RegExp; match: boolean; + regex: RegExp; } interface NormalizedSelector { + custom: NormalizedMatchRegex | null; + filter: NormalizedMatchRegex | null; // format options format: PredefinedFormats[] | null; - custom: NormalizedMatchRegex | null; leadingUnderscore: UnderscoreOptions | null; - trailingUnderscore: UnderscoreOptions | null; + modifiers: Modifiers[] | null; + // calculated ordering weight based on modifiers + modifierWeight: number; prefix: string[] | null; - suffix: string[] | null; // selector options selector: MetaSelectors | Selectors; - modifiers: Modifiers[] | null; + suffix: string[] | null; + trailingUnderscore: UnderscoreOptions | null; types: TypeModifiers[] | null; - filter: NormalizedMatchRegex | null; - // calculated ordering weight based on modifiers - modifierWeight: number; } type ValidatorFunction = ( diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/validator.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/validator.ts index f656769b1b67..afae8bb029b9 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/validator.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/validator.ts @@ -1,9 +1,12 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; -import { getParserServices } from '../../util'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + import type { SelectorsString } from './enums'; +import type { Context, NormalizedSelector } from './types'; + +import { getParserServices } from '../../util'; import { MetaSelectors, Modifiers, @@ -18,7 +21,6 @@ import { isMethodOrPropertySelector, selectorTypeToMessageString, } from './shared'; -import type { Context, NormalizedSelector } from './types'; function createValidator( type: SelectorsString, @@ -144,29 +146,28 @@ function createValidator( // centralizes the logic for formatting the report data function formatReportData({ affixes, + count, + custom, formats, originalName, - processedName, position, - custom, - count, + processedName, }: { affixes?: string[]; + count?: 'one' | 'two'; + custom?: NonNullable; formats?: PredefinedFormats[]; originalName: string; - processedName?: string; position?: 'leading' | 'prefix' | 'suffix' | 'trailing'; - custom?: NonNullable; - count?: 'one' | 'two'; + processedName?: string; }): Record { return { - type: selectorTypeToMessageString(type), - name: originalName, - processedName, - position, - count, affixes: affixes?.join(', '), + count, formats: formats?.map(f => PredefinedFormats[f]).join(', '), + name: originalName, + position, + processedName, regex: custom?.regex.toString(), regexMatch: custom?.match === true @@ -174,6 +175,7 @@ function createValidator( : custom?.match === false ? 'not match' : null, + type: selectorTypeToMessageString(type), }; } @@ -247,13 +249,13 @@ function createValidator( case UnderscoreOptions.forbid: { if (hasSingleUnderscore()) { context.report({ - node, - messageId: 'unexpectedUnderscore', data: formatReportData({ + count: 'one', originalName, position, - count: 'one', }), + messageId: 'unexpectedUnderscore', + node, }); return null; } @@ -265,13 +267,13 @@ function createValidator( case UnderscoreOptions.require: { if (!hasSingleUnderscore()) { context.report({ - node, - messageId: 'missingUnderscore', data: formatReportData({ + count: 'one', originalName, position, - count: 'one', }), + messageId: 'missingUnderscore', + node, }); return null; } @@ -282,13 +284,13 @@ function createValidator( case UnderscoreOptions.requireDouble: { if (!hasDoubleUnderscore()) { context.report({ - node, - messageId: 'missingUnderscore', data: formatReportData({ + count: 'two', originalName, position, - count: 'two', }), + messageId: 'missingUnderscore', + node, }); return null; } @@ -328,13 +330,13 @@ function createValidator( } context.report({ - node, - messageId: 'missingAffix', data: formatReportData({ + affixes, originalName, position, - affixes, }), + messageId: 'missingAffix', + node, }); return null; } @@ -362,12 +364,12 @@ function createValidator( } context.report({ - node, - messageId: 'satisfyCustom', data: formatReportData({ - originalName, custom, + originalName, }), + messageId: 'satisfyCustom', + node, }); return false; } @@ -397,16 +399,16 @@ function createValidator( } context.report({ - node, - messageId: - originalName === name - ? 'doesNotMatchFormat' - : 'doesNotMatchFormatTrimmed', data: formatReportData({ + formats, originalName, processedName: name, - formats, }), + messageId: + originalName === name + ? 'doesNotMatchFormat' + : 'doesNotMatchFormatTrimmed', + node, }); return false; } diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 262f86668071..6a4e4bc53c74 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -1,22 +1,24 @@ // This rule was feature-frozen before we enabled no-property-in-node. /* eslint-disable eslint-plugin/no-property-in-node */ -import { PatternVisitor } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils'; import type { ScriptTarget } from 'typescript'; -import { - collectVariables, - createRule, - getParserServices, - requiresQuoting as _requiresQuoting, -} from '../util'; +import { PatternVisitor } from '@typescript-eslint/scope-manager'; +import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils'; + import type { Context, Selector, ValidatorFunction, } from './naming-convention-utils'; + +import { + requiresQuoting as _requiresQuoting, + collectVariables, + createRule, + getParserServices, +} from '../util'; import { Modifiers, parseOptions, SCHEMA } from './naming-convention-utils'; type MessageIds = @@ -36,57 +38,31 @@ type Options = Selector[]; // note that that rule ignores leading and trailing underscores and only checks those in the middle of a variable name const defaultCamelCaseAllTheThingsConfig: Options = [ { - selector: 'default', format: ['camelCase'], leadingUnderscore: 'allow', + selector: 'default', trailingUnderscore: 'allow', }, { - selector: 'import', format: ['camelCase', 'PascalCase'], + selector: 'import', }, { - selector: 'variable', format: ['camelCase', 'UPPER_CASE'], leadingUnderscore: 'allow', + selector: 'variable', trailingUnderscore: 'allow', }, { - selector: 'typeLike', format: ['PascalCase'], + selector: 'typeLike', }, ]; export default createRule({ - name: 'naming-convention', - meta: { - docs: { - description: - 'Enforce naming conventions for everything across a codebase', - // technically only requires type checking if the user uses "type" modifiers - requiresTypeChecking: true, - }, - type: 'suggestion', - messages: { - unexpectedUnderscore: - '{{type}} name `{{name}}` must not have a {{position}} underscore.', - missingUnderscore: - '{{type}} name `{{name}}` must have {{count}} {{position}} underscore(s).', - missingAffix: - '{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}', - satisfyCustom: - '{{type}} name `{{name}}` must {{regexMatch}} the RegExp: {{regex}}', - doesNotMatchFormat: - '{{type}} name `{{name}}` must match one of the following formats: {{formats}}', - doesNotMatchFormatTrimmed: - '{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}', - }, - schema: SCHEMA, - }, - defaultOptions: defaultCamelCaseAllTheThingsConfig, create(contextWithoutDefaults) { const context = contextWithoutDefaults.options.length > 0 @@ -106,14 +82,14 @@ export default createRule({ function handleMember( validator: ValidatorFunction, node: + | TSESTree.AccessorPropertyNonComputedName | TSESTree.MethodDefinitionNonComputedName | TSESTree.PropertyDefinitionNonComputedName | TSESTree.PropertyNonComputedName | TSESTree.TSAbstractMethodDefinitionNonComputedName | TSESTree.TSAbstractPropertyDefinitionNonComputedName | TSESTree.TSMethodSignatureNonComputedName - | TSESTree.TSPropertySignatureNonComputedName - | TSESTree.AccessorPropertyNonComputedName, + | TSESTree.TSPropertySignatureNonComputedName, modifiers: Set, ): void { const key = node.key; @@ -126,13 +102,13 @@ export default createRule({ function getMemberModifiers( node: + | TSESTree.AccessorProperty | TSESTree.MethodDefinition | TSESTree.PropertyDefinition + | TSESTree.TSAbstractAccessorProperty | TSESTree.TSAbstractMethodDefinition | TSESTree.TSAbstractPropertyDefinition - | TSESTree.TSParameterProperty - | TSESTree.AccessorProperty - | TSESTree.TSAbstractAccessorProperty, + | TSESTree.TSParameterProperty, ): Set { const modifiers = new Set(); if ('key' in node && node.key.type === AST_NODE_TYPES.PrivateIdentifier) { @@ -225,17 +201,57 @@ export default createRule({ const selectors: { readonly [k in keyof TSESLint.RuleListener]: Readonly<{ - validator: ValidatorFunction; handler: ( node: Parameters>[0], validator: ValidatorFunction, ) => void; + validator: ValidatorFunction; }>; } = { // #region import + 'FunctionDeclaration, TSDeclareFunction, FunctionExpression': { + handler: ( + node: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.TSDeclareFunction, + validator, + ): void => { + if (node.id == null) { + return; + } + + const modifiers = new Set(); + // functions create their own nested scope + const scope = context.sourceCode.getScope(node).upper; + + if (isGlobal(scope)) { + modifiers.add(Modifiers.global); + } + + if (isExported(node, node.id.name, scope)) { + modifiers.add(Modifiers.exported); + } + + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + + if (node.async) { + modifiers.add(Modifiers.async); + } + + validator(node.id, modifiers); + }, + validator: validators.function, + }, + + // #endregion + + // #region variable + 'ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier': { - validator: validators.import, handler: ( node: | TSESTree.ImportDefaultSpecifier @@ -263,14 +279,14 @@ export default createRule({ validator(node.local, modifiers); }, + validator: validators.import, }, // #endregion - // #region variable + // #region function VariableDeclarator: { - validator: validators.variable, handler: (node, validator): void => { const identifiers = getIdentifiersFromPattern(node.id); @@ -307,112 +323,32 @@ export default createRule({ validator(id, modifiers); }); }, - }, - - // #endregion - - // #region function - - 'FunctionDeclaration, TSDeclareFunction, FunctionExpression': { - validator: validators.function, - handler: ( - node: - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.TSDeclareFunction, - validator, - ): void => { - if (node.id == null) { - return; - } - - const modifiers = new Set(); - // functions create their own nested scope - const scope = context.sourceCode.getScope(node).upper; - - if (isGlobal(scope)) { - modifiers.add(Modifiers.global); - } - - if (isExported(node, node.id.name, scope)) { - modifiers.add(Modifiers.exported); - } - - if (isUnused(node.id.name, scope)) { - modifiers.add(Modifiers.unused); - } - - if (node.async) { - modifiers.add(Modifiers.async); - } - - validator(node.id, modifiers); - }, + validator: validators.variable, }, // #endregion function // #region parameter - 'FunctionDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, FunctionExpression, ArrowFunctionExpression': + ':matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]': { - validator: validators.parameter, handler: ( node: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.TSDeclareFunction - | TSESTree.TSEmptyBodyFunctionExpression, + | TSESTree.PropertyDefinitionNonComputedName + | TSESTree.TSAbstractPropertyDefinitionNonComputedName, validator, ): void => { - node.params.forEach(param => { - if (param.type === AST_NODE_TYPES.TSParameterProperty) { - return; - } - - const identifiers = getIdentifiersFromPattern(param); - - identifiers.forEach(i => { - const modifiers = new Set(); - - if (isDestructured(i)) { - modifiers.add(Modifiers.destructured); - } - - if (isUnused(i.name, context.sourceCode.getScope(i))) { - modifiers.add(Modifiers.unused); - } - - validator(i, modifiers); - }); - }); + const modifiers = getMemberModifiers(node); + handleMember(validator, node, modifiers); }, + validator: validators.classProperty, }, // #endregion parameter // #region parameterProperty - TSParameterProperty: { - validator: validators.parameterProperty, - handler: (node, validator): void => { - const modifiers = getMemberModifiers(node); - - const identifiers = getIdentifiersFromPattern(node.parameter); - - identifiers.forEach(i => { - validator(i, modifiers); - }); - }, - }, - - // #endregion parameterProperty - - // #region property - ':not(ObjectPattern) > Property[computed = false][kind = "init"][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]': { - validator: validators.objectLiteralProperty, handler: ( node: TSESTree.PropertyNonComputedName, validator, @@ -420,55 +356,28 @@ export default createRule({ const modifiers = new Set([Modifiers.public]); handleMember(validator, node, modifiers); }, + validator: validators.objectLiteralProperty, }, - ':matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]': - { - validator: validators.classProperty, - handler: ( - node: - | TSESTree.PropertyDefinitionNonComputedName - | TSESTree.TSAbstractPropertyDefinitionNonComputedName, - validator, - ): void => { - const modifiers = getMemberModifiers(node); - handleMember(validator, node, modifiers); - }, - }, - - 'TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type != "TSFunctionType"]': - { - validator: validators.typeProperty, - handler: ( - node: TSESTree.TSPropertySignatureNonComputedName, - validator, - ): void => { - const modifiers = new Set([Modifiers.public]); - if (node.readonly) { - modifiers.add(Modifiers.readonly); - } - - handleMember(validator, node, modifiers); - }, - }, - - // #endregion property + // #endregion parameterProperty - // #region method + // #region property [[ - 'Property[computed = false][kind = "init"][value.type = "ArrowFunctionExpression"]', - 'Property[computed = false][kind = "init"][value.type = "FunctionExpression"]', - 'Property[computed = false][kind = "init"][value.type = "TSEmptyBodyFunctionExpression"]', + ':matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = "ArrowFunctionExpression"]', + ':matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = "FunctionExpression"]', + ':matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', + ':matches(MethodDefinition, TSAbstractMethodDefinition)[computed = false][kind = "method"]', ].join(', ')]: { - validator: validators.objectLiteralMethod, handler: ( node: - | TSESTree.PropertyNonComputedName - | TSESTree.TSMethodSignatureNonComputedName, + | TSESTree.MethodDefinitionNonComputedName + | TSESTree.PropertyDefinitionNonComputedName + | TSESTree.TSAbstractMethodDefinitionNonComputedName + | TSESTree.TSAbstractPropertyDefinitionNonComputedName, validator, ): void => { - const modifiers = new Set([Modifiers.public]); + const modifiers = getMemberModifiers(node); if (isAsyncMemberOrProperty(node)) { modifiers.add(Modifiers.async); @@ -476,24 +385,35 @@ export default createRule({ handleMember(validator, node, modifiers); }, + validator: validators.classMethod, }, [[ - ':matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = "ArrowFunctionExpression"]', - ':matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = "FunctionExpression"]', - ':matches(PropertyDefinition, TSAbstractPropertyDefinition)[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', - ':matches(MethodDefinition, TSAbstractMethodDefinition)[computed = false][kind = "method"]', + 'MethodDefinition[computed = false]:matches([kind = "get"], [kind = "set"])', + 'TSAbstractMethodDefinition[computed = false]:matches([kind="get"], [kind="set"])', ].join(', ')]: { - validator: validators.classMethod, handler: ( - node: - | TSESTree.MethodDefinitionNonComputedName - | TSESTree.PropertyDefinitionNonComputedName - | TSESTree.TSAbstractMethodDefinitionNonComputedName - | TSESTree.TSAbstractPropertyDefinitionNonComputedName, + node: TSESTree.MethodDefinitionNonComputedName, validator, ): void => { const modifiers = getMemberModifiers(node); + handleMember(validator, node, modifiers); + }, + validator: validators.classicAccessor, + }, + + [[ + 'Property[computed = false][kind = "init"][value.type = "ArrowFunctionExpression"]', + 'Property[computed = false][kind = "init"][value.type = "FunctionExpression"]', + 'Property[computed = false][kind = "init"][value.type = "TSEmptyBodyFunctionExpression"]', + ].join(', ')]: { + handler: ( + node: + | TSESTree.PropertyNonComputedName + | TSESTree.TSMethodSignatureNonComputedName, + validator, + ): void => { + const modifiers = new Set([Modifiers.public]); if (isAsyncMemberOrProperty(node)) { modifiers.add(Modifiers.async); @@ -501,13 +421,17 @@ export default createRule({ handleMember(validator, node, modifiers); }, + validator: validators.objectLiteralMethod, }, + // #endregion property + + // #region method + [[ 'TSMethodSignature[computed = false]', 'TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type = "TSFunctionType"]', ].join(', ')]: { - validator: validators.typeMethod, handler: ( node: | TSESTree.TSMethodSignatureNonComputedName @@ -517,80 +441,110 @@ export default createRule({ const modifiers = new Set([Modifiers.public]); handleMember(validator, node, modifiers); }, + validator: validators.typeMethod, }, + [[ + AST_NODE_TYPES.AccessorProperty, + AST_NODE_TYPES.TSAbstractAccessorProperty, + ].join(', ')]: { + handler: ( + node: TSESTree.AccessorPropertyNonComputedName, + validator, + ): void => { + const modifiers = getMemberModifiers(node); + handleMember(validator, node, modifiers); + }, + validator: validators.autoAccessor, + }, + + 'FunctionDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, FunctionExpression, ArrowFunctionExpression': + { + handler: ( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.TSDeclareFunction + | TSESTree.TSEmptyBodyFunctionExpression, + validator, + ): void => { + node.params.forEach(param => { + if (param.type === AST_NODE_TYPES.TSParameterProperty) { + return; + } + + const identifiers = getIdentifiersFromPattern(param); + + identifiers.forEach(i => { + const modifiers = new Set(); + + if (isDestructured(i)) { + modifiers.add(Modifiers.destructured); + } + + if (isUnused(i.name, context.sourceCode.getScope(i))) { + modifiers.add(Modifiers.unused); + } + + validator(i, modifiers); + }); + }); + }, + validator: validators.parameter, + }, + // #endregion method // #region accessor 'Property[computed = false]:matches([kind = "get"], [kind = "set"])': { - validator: validators.classicAccessor, handler: (node: TSESTree.PropertyNonComputedName, validator): void => { const modifiers = new Set([Modifiers.public]); handleMember(validator, node, modifiers); }, + validator: validators.classicAccessor, }, - [[ - 'MethodDefinition[computed = false]:matches([kind = "get"], [kind = "set"])', - 'TSAbstractMethodDefinition[computed = false]:matches([kind="get"], [kind="set"])', - ].join(', ')]: { - validator: validators.classicAccessor, - handler: ( - node: TSESTree.MethodDefinitionNonComputedName, - validator, - ): void => { + TSParameterProperty: { + handler: (node, validator): void => { const modifiers = getMemberModifiers(node); - handleMember(validator, node, modifiers); + + const identifiers = getIdentifiersFromPattern(node.parameter); + + identifiers.forEach(i => { + validator(i, modifiers); + }); }, + validator: validators.parameterProperty, }, // #endregion accessor // #region autoAccessor - [[ - AST_NODE_TYPES.AccessorProperty, - AST_NODE_TYPES.TSAbstractAccessorProperty, - ].join(', ')]: { - validator: validators.autoAccessor, - handler: ( - node: TSESTree.AccessorPropertyNonComputedName, - validator, - ): void => { - const modifiers = getMemberModifiers(node); - handleMember(validator, node, modifiers); + 'TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type != "TSFunctionType"]': + { + handler: ( + node: TSESTree.TSPropertySignatureNonComputedName, + validator, + ): void => { + const modifiers = new Set([Modifiers.public]); + if (node.readonly) { + modifiers.add(Modifiers.readonly); + } + + handleMember(validator, node, modifiers); + }, + validator: validators.typeProperty, }, - }, // #endregion autoAccessor // #region enumMember // computed is optional, so can't do [computed = false] - 'TSEnumMember[computed != true]': { - validator: validators.enumMember, - handler: ( - node: TSESTree.TSEnumMemberNonComputedName, - validator, - ): void => { - const id = node.id; - const modifiers = new Set(); - - if (requiresQuoting(id, compilerOptions.target)) { - modifiers.add(Modifiers.requiresQuotes); - } - - validator(id, modifiers); - }, - }, - - // #endregion enumMember - - // #region class - 'ClassDeclaration, ClassExpression': { - validator: validators.class, handler: ( node: TSESTree.ClassDeclaration | TSESTree.ClassExpression, validator, @@ -618,17 +572,18 @@ export default createRule({ validator(id, modifiers); }, + validator: validators.class, }, - // #endregion class + // #endregion enumMember - // #region interface + // #region class - TSInterfaceDeclaration: { - validator: validators.interface, + TSEnumDeclaration: { handler: (node, validator): void => { const modifiers = new Set(); - const scope = context.sourceCode.getScope(node); + // enums create their own nested scope + const scope = context.sourceCode.getScope(node).upper; if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); @@ -640,14 +595,35 @@ export default createRule({ validator(node.id, modifiers); }, + validator: validators.enum, + }, + + // #endregion class + + // #region interface + + 'TSEnumMember[computed != true]': { + handler: ( + node: TSESTree.TSEnumMemberNonComputedName, + validator, + ): void => { + const id = node.id; + const modifiers = new Set(); + + if (requiresQuoting(id, compilerOptions.target)) { + modifiers.add(Modifiers.requiresQuotes); + } + + validator(id, modifiers); + }, + validator: validators.enumMember, }, // #endregion interface // #region typeAlias - TSTypeAliasDeclaration: { - validator: validators.typeAlias, + TSInterfaceDeclaration: { handler: (node, validator): void => { const modifiers = new Set(); const scope = context.sourceCode.getScope(node); @@ -662,18 +638,17 @@ export default createRule({ validator(node.id, modifiers); }, + validator: validators.interface, }, // #endregion typeAlias // #region enum - TSEnumDeclaration: { - validator: validators.enum, + TSTypeAliasDeclaration: { handler: (node, validator): void => { const modifiers = new Set(); - // enums create their own nested scope - const scope = context.sourceCode.getScope(node).upper; + const scope = context.sourceCode.getScope(node); if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); @@ -685,6 +660,7 @@ export default createRule({ validator(node.id, modifiers); }, + validator: validators.typeAlias, }, // #endregion enum @@ -692,7 +668,6 @@ export default createRule({ // #region typeParameter 'TSTypeParameterDeclaration > TSTypeParameter': { - validator: validators.typeParameter, handler: (node: TSESTree.TSTypeParameter, validator): void => { const modifiers = new Set(); const scope = context.sourceCode.getScope(node); @@ -703,13 +678,14 @@ export default createRule({ validator(node.name, modifiers); }, + validator: validators.typeParameter, }, // #endregion typeParameter }; return Object.fromEntries( - Object.entries(selectors).map(([selector, { validator, handler }]) => { + Object.entries(selectors).map(([selector, { handler, validator }]) => { return [ selector, (node: Parameters[0]): void => { @@ -719,6 +695,32 @@ export default createRule({ }), ); }, + defaultOptions: defaultCamelCaseAllTheThingsConfig, + meta: { + docs: { + description: + 'Enforce naming conventions for everything across a codebase', + // technically only requires type checking if the user uses "type" modifiers + requiresTypeChecking: true, + }, + messages: { + doesNotMatchFormat: + '{{type}} name `{{name}}` must match one of the following formats: {{formats}}', + doesNotMatchFormatTrimmed: + '{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}', + missingAffix: + '{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}', + missingUnderscore: + '{{type}} name `{{name}}` must have {{count}} {{position}} underscore(s).', + satisfyCustom: + '{{type}} name `{{name}}` must {{regexMatch}} the RegExp: {{regex}}', + unexpectedUnderscore: + '{{type}} name `{{name}}` must not have a {{position}} underscore.', + }, + schema: SCHEMA, + type: 'suggestion', + }, + name: 'naming-convention', }); function getIdentifiersFromPattern( diff --git a/packages/eslint-plugin/src/rules/no-array-constructor.ts b/packages/eslint-plugin/src/rules/no-array-constructor.ts index b164e5713e6d..361928021fee 100644 --- a/packages/eslint-plugin/src/rules/no-array-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-array-constructor.ts @@ -1,24 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isOptionalCallExpression } from '../util'; export default createRule({ - name: 'no-array-constructor', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow generic `Array` constructors', - recommended: 'recommended', - extendsBaseRule: true, - }, - fixable: 'code', - messages: { - useLiteral: 'The array literal notation [] is preferable.', - }, - schema: [], - }, - defaultOptions: [], create(context) { /** * Disallow construction of dense arrays using the Array constructor @@ -35,8 +21,6 @@ export default createRule({ !isOptionalCallExpression(node) ) { context.report({ - node, - messageId: 'useLiteral', fix(fixer) { if (node.arguments.length === 0) { return fixer.replaceText(node, '[]'); @@ -49,6 +33,8 @@ export default createRule({ `[${fullText.slice(preambleLength + 1, -1)}]`, ); }, + messageId: 'useLiteral', + node, }); } } @@ -58,4 +44,19 @@ export default createRule({ NewExpression: check, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow generic `Array` constructors', + extendsBaseRule: true, + recommended: 'recommended', + }, + fixable: 'code', + messages: { + useLiteral: 'The array literal notation [] is preferable.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-array-constructor', }); diff --git a/packages/eslint-plugin/src/rules/no-array-delete.ts b/packages/eslint-plugin/src/rules/no-array-delete.ts index 9b6230ebf821..161081c56933 100644 --- a/packages/eslint-plugin/src/rules/no-array-delete.ts +++ b/packages/eslint-plugin/src/rules/no-array-delete.ts @@ -1,7 +1,8 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; +import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; + import { createRule, getConstrainedTypeAtLocation, @@ -11,23 +12,6 @@ import { type MessageId = 'noArrayDelete' | 'useSplice'; export default createRule<[], MessageId>({ - name: 'no-array-delete', - meta: { - hasSuggestions: true, - type: 'problem', - docs: { - description: 'Disallow using the `delete` operator on array values', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - noArrayDelete: - 'Using the `delete` operator with an array expression is unsafe.', - useSplice: 'Use `array.splice()` instead.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -64,11 +48,10 @@ export default createRule<[], MessageId>({ } context.report({ - node, messageId: 'noArrayDelete', + node, suggest: [ { - messageId: 'useSplice', fix(fixer): TSESLint.RuleFix | null { const { object, property } = argument; @@ -101,10 +84,28 @@ export default createRule<[], MessageId>({ return fixer.replaceText(node, suggestion); }, + messageId: 'useSplice', }, ], }); }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow using the `delete` operator on array values', + recommended: 'recommended', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + noArrayDelete: + 'Using the `delete` operator with an array expression is unsafe.', + useSplice: 'Use `array.splice()` instead.', + }, + schema: [], + type: 'problem', + }, + name: 'no-array-delete', }); diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index 0369e66fe66f..ed2f85fd008f 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -18,39 +19,6 @@ type Options = [ type MessageIds = 'baseToString'; export default createRule({ - name: 'no-base-to-string', - meta: { - docs: { - description: - 'Require `.toString()` to only be called on objects which provide useful information when stringified', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - baseToString: - "'{{name}}' {{certainty}} use Object's default stringification format ('[object Object]') when stringified.", - }, - schema: [ - { - type: 'object', - properties: { - ignoredTypeNames: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - additionalProperties: false, - }, - ], - type: 'suggestion', - }, - defaultOptions: [ - { - ignoredTypeNames: ['Error', 'RegExp', 'URL', 'URLSearchParams'], - }, - ], create(context, [option]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -180,4 +148,37 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + ignoredTypeNames: ['Error', 'RegExp', 'URL', 'URLSearchParams'], + }, + ], + meta: { + docs: { + description: + 'Require `.toString()` to only be called on objects which provide useful information when stringified', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + baseToString: + "'{{name}}' {{certainty}} use Object's default stringification format ('[object Object]') when stringified.", + }, + schema: [ + { + additionalProperties: false, + properties: { + ignoredTypeNames: { + items: { + type: 'string', + }, + type: 'array', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-base-to-string', }); diff --git a/packages/eslint-plugin/src/rules/no-confusing-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-confusing-non-null-assertion.ts index 9caf777aabad..7e16276795c7 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-non-null-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-non-null-assertion.ts @@ -1,32 +1,10 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'no-confusing-non-null-assertion', - meta: { - type: 'problem', - docs: { - description: - 'Disallow non-null assertion in locations that may be confusing', - recommended: 'stylistic', - }, - hasSuggestions: true, - messages: { - confusingEqual: - 'Confusing combinations of non-null assertion and equal test like "a! == b", which looks very similar to not equal "a !== b".', - confusingAssign: - 'Confusing combinations of non-null assertion and equal test like "a! = b", which looks very similar to not equal "a != b".', - notNeedInEqualTest: 'Unnecessary non-null assertion (!) in equal test.', - notNeedInAssign: - 'Unnecessary non-null assertion (!) in assignment left hand.', - wrapUpLeft: - 'Wrap up left hand to avoid putting non-null assertion "!" and "=" together.', - }, - schema: [], - }, - defaultOptions: [], create(context) { return { 'BinaryExpression, AssignmentExpression'( @@ -53,30 +31,30 @@ export default createRule({ ) { if (isLeftHandPrimaryExpression(node.left)) { context.report({ - node, messageId: isAssign ? 'confusingAssign' : 'confusingEqual', + node, suggest: [ { - messageId: isAssign - ? 'notNeedInAssign' - : 'notNeedInEqualTest', fix: (fixer): TSESLint.RuleFix[] => [ fixer.remove(leftHandFinalToken), ], + messageId: isAssign + ? 'notNeedInAssign' + : 'notNeedInEqualTest', }, ], }); } else { context.report({ - node, messageId: isAssign ? 'confusingAssign' : 'confusingEqual', + node, suggest: [ { - messageId: 'wrapUpLeft', fix: (fixer): TSESLint.RuleFix[] => [ fixer.insertTextBefore(node.left, '('), fixer.insertTextAfter(node.left, ')'), ], + messageId: 'wrapUpLeft', }, ], }); @@ -86,4 +64,27 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow non-null assertion in locations that may be confusing', + recommended: 'stylistic', + }, + hasSuggestions: true, + messages: { + confusingAssign: + 'Confusing combinations of non-null assertion and equal test like "a! = b", which looks very similar to not equal "a != b".', + confusingEqual: + 'Confusing combinations of non-null assertion and equal test like "a! == b", which looks very similar to not equal "a !== b".', + notNeedInAssign: + 'Unnecessary non-null assertion (!) in assignment left hand.', + notNeedInEqualTest: 'Unnecessary non-null assertion (!) in equal test.', + wrapUpLeft: + 'Wrap up left hand to avoid putting non-null assertion "!" and "=" together.', + }, + schema: [], + type: 'problem', + }, + name: 'no-confusing-non-null-assertion', }); diff --git a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index 38df4fb0dc44..a4fa1f6af727 100644 --- a/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -1,9 +1,11 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; import type { MakeRequired } from '../util'; + import { createRule, getConstrainedTypeAtLocation, @@ -33,55 +35,6 @@ export type MessageId = | 'voidExprWrapVoid'; export default createRule({ - name: 'no-confusing-void-expression', - meta: { - docs: { - description: - 'Require expressions of type void to appear in statement position', - recommended: 'strict', - requiresTypeChecking: true, - }, - messages: { - invalidVoidExpr: - 'Placing a void expression inside another expression is forbidden. ' + - 'Move it to its own statement instead.', - invalidVoidExprWrapVoid: - 'Void expressions used inside another expression ' + - 'must be moved to its own statement ' + - 'or marked explicitly with the `void` operator.', - invalidVoidExprArrow: - 'Returning a void expression from an arrow function shorthand is forbidden. ' + - 'Please add braces to the arrow function.', - invalidVoidExprArrowWrapVoid: - 'Void expressions returned from an arrow function shorthand ' + - 'must be marked explicitly with the `void` operator.', - invalidVoidExprReturn: - 'Returning a void expression from a function is forbidden. ' + - 'Please move it before the `return` statement.', - invalidVoidExprReturnLast: - 'Returning a void expression from a function is forbidden. ' + - 'Please remove the `return` statement.', - invalidVoidExprReturnWrapVoid: - 'Void expressions returned from a function ' + - 'must be marked explicitly with the `void` operator.', - voidExprWrapVoid: 'Mark with an explicit `void` operator.', - }, - schema: [ - { - type: 'object', - properties: { - ignoreArrowShorthand: { type: 'boolean' }, - ignoreVoidOperator: { type: 'boolean' }, - }, - additionalProperties: false, - }, - ], - type: 'problem', - fixable: 'code', - hasSuggestions: true, - }, - defaultOptions: [{ ignoreArrowShorthand: false, ignoreVoidOperator: false }], - create(context, [options]) { return { 'AwaitExpression, CallExpression, TaggedTemplateExpression'( @@ -115,17 +68,15 @@ export default createRule({ if (options.ignoreVoidOperator) { // handle wrapping with `void` return context.report({ - node, - messageId: 'invalidVoidExprArrowWrapVoid', fix: wrapVoidFix, + messageId: 'invalidVoidExprArrowWrapVoid', + node, }); } // handle wrapping with braces const arrowFunction = invalidAncestor; return context.report({ - node, - messageId: 'invalidVoidExprArrow', fix(fixer) { if (!canFix(arrowFunction)) { return null; @@ -161,6 +112,8 @@ export default createRule({ } return fixer.replaceText(arrowBody, newArrowBodyText); }, + messageId: 'invalidVoidExprArrow', + node, }); } @@ -170,17 +123,15 @@ export default createRule({ if (options.ignoreVoidOperator) { // handle wrapping with `void` return context.report({ - node, - messageId: 'invalidVoidExprReturnWrapVoid', fix: wrapVoidFix, + messageId: 'invalidVoidExprReturnWrapVoid', + node, }); } if (isFinalReturn(invalidAncestor)) { // remove the `return` keyword return context.report({ - node, - messageId: 'invalidVoidExprReturnLast', fix(fixer) { if (!canFix(invalidAncestor)) { return null; @@ -194,13 +145,13 @@ export default createRule({ } return fixer.replaceText(invalidAncestor, newReturnStmtText); }, + messageId: 'invalidVoidExprReturnLast', + node, }); } // move before the `return` keyword return context.report({ - node, - messageId: 'invalidVoidExprReturn', fix(fixer) { const returnValue = invalidAncestor.argument; const returnValueText = context.sourceCode.getText(returnValue); @@ -218,6 +169,8 @@ export default createRule({ } return fixer.replaceText(invalidAncestor, newReturnStmtText); }, + messageId: 'invalidVoidExprReturn', + node, }); } @@ -225,15 +178,15 @@ export default createRule({ if (options.ignoreVoidOperator) { // this would be reported by this rule btw. such irony return context.report({ - node, messageId: 'invalidVoidExprWrapVoid', - suggest: [{ messageId: 'voidExprWrapVoid', fix: wrapVoidFix }], + node, + suggest: [{ fix: wrapVoidFix, messageId: 'voidExprWrapVoid' }], }); } context.report({ - node, messageId: 'invalidVoidExpr', + node, }); }, }; @@ -329,9 +282,9 @@ export default createRule({ ); if ( ![ + AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionDeclaration, AST_NODE_TYPES.FunctionExpression, - AST_NODE_TYPES.ArrowFunctionExpression, ].includes(blockParent.type) ) { // e.g. `if (cond) { return; }` @@ -377,4 +330,53 @@ export default createRule({ return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike); } }, + defaultOptions: [{ ignoreArrowShorthand: false, ignoreVoidOperator: false }], + meta: { + docs: { + description: + 'Require expressions of type void to appear in statement position', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + hasSuggestions: true, + messages: { + invalidVoidExpr: + 'Placing a void expression inside another expression is forbidden. ' + + 'Move it to its own statement instead.', + invalidVoidExprArrow: + 'Returning a void expression from an arrow function shorthand is forbidden. ' + + 'Please add braces to the arrow function.', + invalidVoidExprArrowWrapVoid: + 'Void expressions returned from an arrow function shorthand ' + + 'must be marked explicitly with the `void` operator.', + invalidVoidExprReturn: + 'Returning a void expression from a function is forbidden. ' + + 'Please move it before the `return` statement.', + invalidVoidExprReturnLast: + 'Returning a void expression from a function is forbidden. ' + + 'Please remove the `return` statement.', + invalidVoidExprReturnWrapVoid: + 'Void expressions returned from a function ' + + 'must be marked explicitly with the `void` operator.', + invalidVoidExprWrapVoid: + 'Void expressions used inside another expression ' + + 'must be moved to its own statement ' + + 'or marked explicitly with the `void` operator.', + voidExprWrapVoid: 'Mark with an explicit `void` operator.', + }, + schema: [ + { + additionalProperties: false, + properties: { + ignoreArrowShorthand: { type: 'boolean' }, + ignoreVoidOperator: { type: 'boolean' }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + + name: 'no-confusing-void-expression', }); diff --git a/packages/eslint-plugin/src/rules/no-dupe-class-members.ts b/packages/eslint-plugin/src/rules/no-dupe-class-members.ts index 08dd0b35d3b8..a0d9c8e29eab 100644 --- a/packages/eslint-plugin/src/rules/no-dupe-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-dupe-class-members.ts @@ -1,10 +1,12 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -14,18 +16,6 @@ type Options = InferOptionsTypeFromRule; type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ - name: 'no-dupe-class-members', - meta: { - type: 'problem', - docs: { - description: 'Disallow duplicate class members', - extendsBaseRule: true, - }, - hasSuggestions: baseRule.meta.hasSuggestions, - schema: baseRule.meta.schema, - messages: baseRule.meta.messages, - }, - defaultOptions: [], create(context) { const rules = baseRule.create(context); @@ -55,4 +45,16 @@ export default createRule({ ), }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow duplicate class members', + extendsBaseRule: true, + }, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: baseRule.meta.schema, + type: 'problem', + }, + name: 'no-dupe-class-members', }); diff --git a/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts b/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts index d8ac6586666d..e89f8d66256f 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts @@ -1,23 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'no-duplicate-enum-values', - meta: { - type: 'problem', - docs: { - description: 'Disallow duplicate enum member values', - recommended: 'recommended', - }, - hasSuggestions: false, - messages: { - duplicateValue: 'Duplicate enum member value {{value}}.', - }, - schema: [], - }, - defaultOptions: [], create(context) { function isStringLiteral( node: TSESTree.Expression, @@ -58,11 +45,11 @@ export default createRule({ if (seenValues.has(value)) { context.report({ - node: member, - messageId: 'duplicateValue', data: { value, }, + messageId: 'duplicateValue', + node: member, }); } else { seenValues.add(value); @@ -71,4 +58,18 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow duplicate enum member values', + recommended: 'recommended', + }, + hasSuggestions: false, + messages: { + duplicateValue: 'Duplicate enum member value {{value}}.', + }, + schema: [], + type: 'problem', + }, + name: 'no-duplicate-enum-values', }); diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index d92de657aa47..65eb54471664 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -1,7 +1,8 @@ import type { TSESTree } from '@typescript-eslint/utils'; +import type { Type } from 'typescript'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; -import type { Type } from 'typescript'; import { createRule, getParserServices } from '../util'; @@ -67,40 +68,6 @@ const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { }; export default createRule({ - name: 'no-duplicate-type-constituents', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow duplicate constituents of union or intersection types', - recommended: 'recommended', - requiresTypeChecking: true, - }, - fixable: 'code', - messages: { - duplicate: '{{type}} type constituent is duplicated with {{previous}}.', - }, - schema: [ - { - additionalProperties: false, - type: 'object', - properties: { - ignoreIntersections: { - type: 'boolean', - }, - ignoreUnions: { - type: 'boolean', - }, - }, - }, - ], - }, - defaultOptions: [ - { - ignoreIntersections: false, - ignoreUnions: false, - }, - ], create(context, [{ ignoreIntersections, ignoreUnions }]) { const parserServices = getParserServices(context); @@ -169,25 +136,22 @@ export default createRule({ { count: bracketBeforeTokens.length }, ); const reportLocation: TSESTree.SourceLocation = { - start: duplicateConstituent.duplicated.loc.start, end: bracketAfterTokens.length > 0 ? bracketAfterTokens[bracketAfterTokens.length - 1].loc.end : duplicateConstituent.duplicated.loc.end, + start: duplicateConstituent.duplicated.loc.start, }; context.report({ data: { + previous: context.sourceCode.getText( + duplicateConstituent.duplicatePrevious, + ), type: parentNode.type === AST_NODE_TYPES.TSIntersectionType ? 'Intersection' : 'Union', - previous: context.sourceCode.getText( - duplicateConstituent.duplicatePrevious, - ), }, - messageId: 'duplicate', - node: duplicateConstituent.duplicated, - loc: reportLocation, fix: fixer => { return [ beforeUnionOrIntersectionToken, @@ -196,6 +160,9 @@ export default createRule({ ...bracketAfterTokens, ].map(token => fixer.remove(token)); }, + loc: reportLocation, + messageId: 'duplicate', + node: duplicateConstituent.duplicated, }); } return { @@ -207,4 +174,38 @@ export default createRule({ }), }; }, + defaultOptions: [ + { + ignoreIntersections: false, + ignoreUnions: false, + }, + ], + meta: { + docs: { + description: + 'Disallow duplicate constituents of union or intersection types', + recommended: 'recommended', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + duplicate: '{{type}} type constituent is duplicated with {{previous}}.', + }, + schema: [ + { + additionalProperties: false, + properties: { + ignoreIntersections: { + type: 'boolean', + }, + ignoreUnions: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-duplicate-type-constituents', }); diff --git a/packages/eslint-plugin/src/rules/no-dynamic-delete.ts b/packages/eslint-plugin/src/rules/no-dynamic-delete.ts index fb487fa6b7fc..d0da569f7f7e 100644 --- a/packages/eslint-plugin/src/rules/no-dynamic-delete.ts +++ b/packages/eslint-plugin/src/rules/no-dynamic-delete.ts @@ -1,24 +1,10 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, nullThrows, NullThrowsReasons } from '../util'; export default createRule({ - name: 'no-dynamic-delete', - meta: { - docs: { - description: - 'Disallow using the `delete` operator on computed key expressions', - recommended: 'strict', - }, - fixable: 'code', - messages: { - dynamicDelete: 'Do not delete dynamically computed property keys.', - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { function createFixer( member: TSESTree.MemberExpression, @@ -75,12 +61,27 @@ export default createRule({ ]; } }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow using the `delete` operator on computed key expressions', + recommended: 'strict', + }, + fixable: 'code', + messages: { + dynamicDelete: 'Do not delete dynamically computed property keys.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-dynamic-delete', }); function isAcceptableIndexExpression(property: TSESTree.Expression): boolean { return ( (property.type === AST_NODE_TYPES.Literal && - ['string', 'number'].includes(typeof property.value)) || + ['number', 'string'].includes(typeof property.value)) || (property.type === AST_NODE_TYPES.UnaryExpression && property.operator === '-' && property.argument.type === AST_NODE_TYPES.Literal && diff --git a/packages/eslint-plugin/src/rules/no-empty-function.ts b/packages/eslint-plugin/src/rules/no-empty-function.ts index 6a8e90ebaf13..cb3ecae32597 100644 --- a/packages/eslint-plugin/src/rules/no-empty-function.ts +++ b/packages/eslint-plugin/src/rules/no-empty-function.ts @@ -1,11 +1,13 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule, deepMerge } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -23,7 +25,6 @@ const schema = deepMerge( properties: { allow: { items: { - type: 'string', enum: [ 'functions', 'arrowFunctions', @@ -40,6 +41,7 @@ const schema = deepMerge( 'decoratedFunctions', 'overrideMethods', ], + type: 'string', }, }, }, @@ -47,23 +49,6 @@ const schema = deepMerge( ) as unknown as JSONSchema4; export default createRule({ - name: 'no-empty-function', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow empty functions', - recommended: 'stylistic', - extendsBaseRule: true, - }, - hasSuggestions: baseRule.meta.hasSuggestions, - schema: [schema], - messages: baseRule.meta.messages, - }, - defaultOptions: [ - { - allow: [], - }, - ], create(context, [{ allow = [] }]) { const rules = baseRule.create(context); @@ -174,4 +159,21 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allow: [], + }, + ], + meta: { + docs: { + description: 'Disallow empty functions', + extendsBaseRule: true, + recommended: 'stylistic', + }, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: [schema], + type: 'suggestion', + }, + name: 'no-empty-function', }); diff --git a/packages/eslint-plugin/src/rules/no-empty-interface.ts b/packages/eslint-plugin/src/rules/no-empty-interface.ts index 03d00c1a9535..a31d2fb853bf 100644 --- a/packages/eslint-plugin/src/rules/no-empty-interface.ts +++ b/packages/eslint-plugin/src/rules/no-empty-interface.ts @@ -1,5 +1,6 @@ -import { ScopeType } from '@typescript-eslint/scope-manager'; import type { TSESLint } from '@typescript-eslint/utils'; + +import { ScopeType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isDefinitionFile } from '../util'; @@ -12,38 +13,6 @@ type Options = [ type MessageIds = 'noEmpty' | 'noEmptyWithSuper'; export default createRule({ - name: 'no-empty-interface', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow the declaration of empty interfaces', - }, - deprecated: true, - replacedBy: ['@typescript-eslint/no-empty-object-type'], - fixable: 'code', - hasSuggestions: true, - messages: { - noEmpty: 'An empty interface is equivalent to `{}`.', - noEmptyWithSuper: - 'An interface declaring no members is equivalent to its supertype.', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - allowSingleExtends: { - type: 'boolean', - }, - }, - }, - ], - }, - defaultOptions: [ - { - allowSingleExtends: false, - }, - ], create(context, [{ allowSingleExtends }]) { return { TSInterfaceDeclaration(node): void { @@ -55,8 +24,8 @@ export default createRule({ const extend = node.extends; if (extend.length === 0) { context.report({ - node: node.id, messageId: 'noEmpty', + node: node.id, }); } else if (extend.length === 1) { // interface extends exactly 1 interface --> Report depending on rule setting @@ -92,16 +61,16 @@ export default createRule({ ); context.report({ - node: node.id, messageId: 'noEmptyWithSuper', + node: node.id, ...(useAutoFix ? { fix } : !mergedWithClassDeclaration ? { suggest: [ { - messageId: 'noEmptyWithSuper', fix, + messageId: 'noEmptyWithSuper', }, ], } @@ -112,4 +81,36 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowSingleExtends: false, + }, + ], + meta: { + deprecated: true, + docs: { + description: 'Disallow the declaration of empty interfaces', + }, + fixable: 'code', + hasSuggestions: true, + messages: { + noEmpty: 'An empty interface is equivalent to `{}`.', + noEmptyWithSuper: + 'An interface declaring no members is equivalent to its supertype.', + }, + replacedBy: ['@typescript-eslint/no-empty-object-type'], + schema: [ + { + additionalProperties: false, + properties: { + allowSingleExtends: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-empty-interface', }); diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts index 2471726240b8..f89b603c0629 100644 --- a/packages/eslint-plugin/src/rules/no-empty-object-type.ts +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -1,4 +1,5 @@ import type { TSESLint } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -17,8 +18,8 @@ export type Options = [ export type MessageIds = | 'noEmptyInterface' - | 'noEmptyObject' | 'noEmptyInterfaceWithSuper' + | 'noEmptyObject' | 'replaceEmptyInterface' | 'replaceEmptyInterfaceWithSuper' | 'replaceEmptyObjectType'; @@ -32,51 +33,7 @@ const noEmptyMessage = (emptyType: string): string => ].join('\n'); export default createRule({ - name: 'no-empty-object-type', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow accidentally using the "empty object" type', - recommended: 'recommended', - }, - hasSuggestions: true, - messages: { - noEmptyInterface: noEmptyMessage('An empty interface declaration'), - noEmptyObject: noEmptyMessage('The `{}` ("empty object") type'), - noEmptyInterfaceWithSuper: - 'An interface declaring no members is equivalent to its supertype.', - replaceEmptyInterface: 'Replace empty interface with `{{replacement}}`.', - replaceEmptyInterfaceWithSuper: - 'Replace empty interface with a type alias.', - replaceEmptyObjectType: 'Replace `{}` with `{{replacement}}`.', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - allowInterfaces: { - enum: ['always', 'never', 'with-single-extends'], - type: 'string', - }, - allowObjectTypes: { - enum: ['always', 'never'], - type: 'string', - }, - allowWithName: { - type: 'string', - }, - }, - }, - ], - }, - defaultOptions: [ - { - allowInterfaces: 'never', - allowObjectTypes: 'never', - }, - ], - create(context, [{ allowInterfaces, allowWithName, allowObjectTypes }]) { + create(context, [{ allowInterfaces, allowObjectTypes, allowWithName }]) { const allowWithNameTester = allowWithName ? new RegExp(allowWithName, 'u') : undefined; @@ -109,8 +66,8 @@ export default createRule({ if (extend.length === 0) { context.report({ data: { option: 'allowInterfaces' }, - node: node.id, messageId: 'noEmptyInterface', + node: node.id, ...(!mergedWithClassDeclaration && { suggest: ['object', 'unknown'].map(replacement => ({ data: { replacement }, @@ -133,8 +90,8 @@ export default createRule({ } context.report({ - node: node.id, messageId: 'noEmptyInterfaceWithSuper', + node: node.id, ...(!mergedWithClassDeclaration && { suggest: [ { @@ -175,13 +132,57 @@ export default createRule({ node, suggest: ['object', 'unknown'].map(replacement => ({ data: { replacement }, - messageId: 'replaceEmptyObjectType', fix: (fixer): TSESLint.RuleFix => fixer.replaceText(node, replacement), + messageId: 'replaceEmptyObjectType', })), }); }, }), }; }, + defaultOptions: [ + { + allowInterfaces: 'never', + allowObjectTypes: 'never', + }, + ], + meta: { + docs: { + description: 'Disallow accidentally using the "empty object" type', + recommended: 'recommended', + }, + hasSuggestions: true, + messages: { + noEmptyInterface: noEmptyMessage('An empty interface declaration'), + noEmptyInterfaceWithSuper: + 'An interface declaring no members is equivalent to its supertype.', + noEmptyObject: noEmptyMessage('The `{}` ("empty object") type'), + replaceEmptyInterface: 'Replace empty interface with `{{replacement}}`.', + replaceEmptyInterfaceWithSuper: + 'Replace empty interface with a type alias.', + replaceEmptyObjectType: 'Replace `{}` with `{{replacement}}`.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowInterfaces: { + enum: ['always', 'never', 'with-single-extends'], + type: 'string', + }, + allowObjectTypes: { + enum: ['always', 'never'], + type: 'string', + }, + allowWithName: { + type: 'string', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-empty-object-type', }); diff --git a/packages/eslint-plugin/src/rules/no-explicit-any.ts b/packages/eslint-plugin/src/rules/no-explicit-any.ts index 923750847365..a945e69aefb3 100644 --- a/packages/eslint-plugin/src/rules/no-explicit-any.ts +++ b/packages/eslint-plugin/src/rules/no-explicit-any.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -12,47 +13,7 @@ export type Options = [ export type MessageIds = 'suggestNever' | 'suggestUnknown' | 'unexpectedAny'; export default createRule({ - name: 'no-explicit-any', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow the `any` type', - recommended: 'recommended', - }, - fixable: 'code', - hasSuggestions: true, - messages: { - unexpectedAny: 'Unexpected any. Specify a different type.', - suggestUnknown: - 'Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct.', - suggestNever: - "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.", - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - fixToUnknown: { - description: - 'Whether to enable auto-fixing in which the `any` type is converted to the `unknown` type.', - type: 'boolean', - }, - ignoreRestArgs: { - description: 'Whether to ignore rest parameter arrays.', - type: 'boolean', - }, - }, - }, - ], - }, - defaultOptions: [ - { - fixToUnknown: false, - ignoreRestArgs: false, - }, - ], - create(context, [{ ignoreRestArgs, fixToUnknown }]) { + create(context, [{ fixToUnknown, ignoreRestArgs }]) { /** * Checks if the node is an arrow function, function/constructor declaration or function expression * @param node the node to be validated. @@ -64,13 +25,13 @@ export default createRule({ AST_NODE_TYPES.ArrowFunctionExpression, // const x = (...args: any[]) => {}; AST_NODE_TYPES.FunctionDeclaration, // function f(...args: any[]) {} AST_NODE_TYPES.FunctionExpression, // const x = function(...args: any[]) {}; - AST_NODE_TYPES.TSEmptyBodyFunctionExpression, // declare class A { f(...args: any[]): unknown; } - AST_NODE_TYPES.TSFunctionType, // type T = (...args: any[]) => unknown; - AST_NODE_TYPES.TSConstructorType, // type T = new (...args: any[]) => unknown AST_NODE_TYPES.TSCallSignatureDeclaration, // type T = {(...args: any[]): unknown}; + AST_NODE_TYPES.TSConstructorType, // type T = new (...args: any[]) => unknown AST_NODE_TYPES.TSConstructSignatureDeclaration, // type T = {new (...args: any[]): unknown}; - AST_NODE_TYPES.TSMethodSignature, // type T = {f(...args: any[]): unknown}; AST_NODE_TYPES.TSDeclareFunction, // declare function _8(...args: any[]): unknown; + AST_NODE_TYPES.TSEmptyBodyFunctionExpression, // declare class A { f(...args: any[]): unknown; } + AST_NODE_TYPES.TSFunctionType, // type T = (...args: any[]) => unknown; + AST_NODE_TYPES.TSMethodSignature, // type T = {f(...args: any[]): unknown}; ].includes(node.type); } @@ -182,16 +143,16 @@ export default createRule({ fix: null, suggest: [ { - messageId: 'suggestUnknown', fix(fixer): TSESLint.RuleFix { return fixer.replaceText(node, 'unknown'); }, + messageId: 'suggestUnknown', }, { - messageId: 'suggestNever', fix(fixer): TSESLint.RuleFix { return fixer.replaceText(node, 'never'); }, + messageId: 'suggestNever', }, ], }; @@ -202,11 +163,51 @@ export default createRule({ } context.report({ - node, messageId: 'unexpectedAny', + node, ...fixOrSuggest, }); }, }; }, + defaultOptions: [ + { + fixToUnknown: false, + ignoreRestArgs: false, + }, + ], + meta: { + docs: { + description: 'Disallow the `any` type', + recommended: 'recommended', + }, + fixable: 'code', + hasSuggestions: true, + messages: { + suggestNever: + "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of.", + suggestUnknown: + 'Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct.', + unexpectedAny: 'Unexpected any. Specify a different type.', + }, + schema: [ + { + additionalProperties: false, + properties: { + fixToUnknown: { + description: + 'Whether to enable auto-fixing in which the `any` type is converted to the `unknown` type.', + type: 'boolean', + }, + ignoreRestArgs: { + description: 'Whether to ignore rest parameter arrays.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-explicit-any', }); diff --git a/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts index ee1858fe7d95..6b9edf3b67fa 100644 --- a/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-extra-non-null-assertion.ts @@ -3,39 +3,39 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'no-extra-non-null-assertion', - meta: { - type: 'problem', - docs: { - description: 'Disallow extra non-null assertions', - recommended: 'recommended', - }, - fixable: 'code', - schema: [], - messages: { - noExtraNonNullAssertion: 'Forbidden extra non-null assertion.', - }, - }, - defaultOptions: [], create(context) { function checkExtraNonNullAssertion( node: TSESTree.TSNonNullExpression, ): void { context.report({ - node, - messageId: 'noExtraNonNullAssertion', fix(fixer) { return fixer.removeRange([node.range[1] - 1, node.range[1]]); }, + messageId: 'noExtraNonNullAssertion', + node, }); } return { - 'TSNonNullExpression > TSNonNullExpression': checkExtraNonNullAssertion, - 'MemberExpression[optional = true] > TSNonNullExpression.object': - checkExtraNonNullAssertion, 'CallExpression[optional = true] > TSNonNullExpression.callee': checkExtraNonNullAssertion, + 'MemberExpression[optional = true] > TSNonNullExpression.object': + checkExtraNonNullAssertion, + 'TSNonNullExpression > TSNonNullExpression': checkExtraNonNullAssertion, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow extra non-null assertions', + recommended: 'recommended', + }, + fixable: 'code', + messages: { + noExtraNonNullAssertion: 'Forbidden extra non-null assertion.', + }, + schema: [], + type: 'problem', + }, + name: 'no-extra-non-null-assertion', }); diff --git a/packages/eslint-plugin/src/rules/no-extraneous-class.ts b/packages/eslint-plugin/src/rules/no-extraneous-class.ts index 854cec6b40eb..18043abb9833 100644 --- a/packages/eslint-plugin/src/rules/no-extraneous-class.ts +++ b/packages/eslint-plugin/src/rules/no-extraneous-class.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -14,55 +15,6 @@ type Options = [ type MessageIds = 'empty' | 'onlyConstructor' | 'onlyStatic'; export default createRule({ - name: 'no-extraneous-class', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow classes used as namespaces', - recommended: 'strict', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - allowConstructorOnly: { - description: - 'Whether to allow extraneous classes that contain only a constructor.', - type: 'boolean', - }, - allowEmpty: { - description: - 'Whether to allow extraneous classes that have no body (i.e. are empty).', - type: 'boolean', - }, - allowStaticOnly: { - description: - 'Whether to allow extraneous classes that only contain static members.', - type: 'boolean', - }, - allowWithDecorator: { - description: - 'Whether to allow extraneous classes that include a decorator.', - type: 'boolean', - }, - }, - }, - ], - messages: { - empty: 'Unexpected empty class.', - onlyStatic: 'Unexpected class with only static properties.', - onlyConstructor: 'Unexpected class with only a constructor.', - }, - }, - defaultOptions: [ - { - allowConstructorOnly: false, - allowEmpty: false, - allowStaticOnly: false, - allowWithDecorator: false, - }, - ], create( context, [{ allowConstructorOnly, allowEmpty, allowStaticOnly, allowWithDecorator }], @@ -95,8 +47,8 @@ export default createRule({ } context.report({ - node: reportNode, messageId: 'empty', + node: reportNode, }); return; @@ -138,19 +90,68 @@ export default createRule({ if (onlyConstructor) { if (!allowConstructorOnly) { context.report({ - node: reportNode, messageId: 'onlyConstructor', + node: reportNode, }); } return; } if (onlyStatic && !allowStaticOnly) { context.report({ - node: reportNode, messageId: 'onlyStatic', + node: reportNode, }); } }, }; }, + defaultOptions: [ + { + allowConstructorOnly: false, + allowEmpty: false, + allowStaticOnly: false, + allowWithDecorator: false, + }, + ], + meta: { + docs: { + description: 'Disallow classes used as namespaces', + recommended: 'strict', + }, + messages: { + empty: 'Unexpected empty class.', + onlyConstructor: 'Unexpected class with only a constructor.', + onlyStatic: 'Unexpected class with only static properties.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowConstructorOnly: { + description: + 'Whether to allow extraneous classes that contain only a constructor.', + type: 'boolean', + }, + allowEmpty: { + description: + 'Whether to allow extraneous classes that have no body (i.e. are empty).', + type: 'boolean', + }, + allowStaticOnly: { + description: + 'Whether to allow extraneous classes that only contain static members.', + type: 'boolean', + }, + allowWithDecorator: { + description: + 'Whether to allow extraneous classes that include a decorator.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-extraneous-class', }); diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index e59c32665427..746d6aa740ea 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -1,9 +1,11 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; import type { TypeOrValueSpecifier } from '../util'; + import { createRule, getOperatorPrecedence, @@ -17,8 +19,8 @@ import { type Options = [ { - allowForKnownSafePromises?: TypeOrValueSpecifier[]; allowForKnownSafeCalls?: TypeOrValueSpecifier[]; + allowForKnownSafePromises?: TypeOrValueSpecifier[]; checkThenables?: boolean; ignoreIIFE?: boolean; ignoreVoid?: boolean; @@ -27,13 +29,13 @@ type Options = [ type MessageId = | 'floating' - | 'floatingVoid' - | 'floatingUselessRejectionHandler' - | 'floatingUselessRejectionHandlerVoid' | 'floatingFixAwait' | 'floatingFixVoid' | 'floatingPromiseArray' - | 'floatingPromiseArrayVoid'; + | 'floatingPromiseArrayVoid' + | 'floatingUselessRejectionHandler' + | 'floatingUselessRejectionHandlerVoid' + | 'floatingVoid'; const messageBase = 'Promises must be awaited, end with a call to .catch, or end with a call to .then with a rejection handler.'; @@ -53,61 +55,6 @@ const messagePromiseArrayVoid = ' or explicitly marking the expression as ignored with the `void` operator.'; export default createRule({ - name: 'no-floating-promises', - meta: { - docs: { - description: - 'Require Promise-like statements to be handled appropriately', - recommended: 'recommended', - requiresTypeChecking: true, - }, - hasSuggestions: true, - messages: { - floating: messageBase, - floatingFixAwait: 'Add await operator.', - floatingVoid: messageBaseVoid, - floatingFixVoid: 'Add void operator to ignore.', - floatingUselessRejectionHandler: `${messageBase} ${messageRejectionHandler}`, - floatingUselessRejectionHandlerVoid: `${messageBaseVoid} ${messageRejectionHandler}`, - floatingPromiseArray: messagePromiseArray, - floatingPromiseArrayVoid: messagePromiseArrayVoid, - }, - schema: [ - { - type: 'object', - properties: { - allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow, - allowForKnownSafeCalls: readonlynessOptionsSchema.properties.allow, - checkThenables: { - description: - 'Whether to check all "Thenable"s, not just the built-in Promise type.', - type: 'boolean', - }, - ignoreVoid: { - description: 'Whether to ignore `void` expressions.', - type: 'boolean', - }, - ignoreIIFE: { - description: - 'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).', - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - type: 'problem', - }, - defaultOptions: [ - { - allowForKnownSafeCalls: readonlynessOptionsDefaults.allow, - allowForKnownSafePromises: readonlynessOptionsDefaults.allow, - checkThenables: false, - ignoreIIFE: false, - ignoreVoid: true, - }, - ], - create(context, [options]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -141,20 +88,19 @@ export default createRule({ if (isUnhandled) { if (promiseArray) { context.report({ - node, messageId: options.ignoreVoid ? 'floatingPromiseArrayVoid' : 'floatingPromiseArray', + node, }); } else if (options.ignoreVoid) { context.report({ - node, messageId: nonFunctionHandler ? 'floatingUselessRejectionHandlerVoid' : 'floatingVoid', + node, suggest: [ { - messageId: 'floatingFixVoid', fix(fixer): TSESLint.RuleFix | TSESLint.RuleFix[] { const tsNode = services.esTreeNodeToTSNodeMap.get( node.expression, @@ -170,25 +116,26 @@ export default createRule({ ), ]; }, + messageId: 'floatingFixVoid', }, { - messageId: 'floatingFixAwait', fix: (fixer): TSESLint.RuleFix | TSESLint.RuleFix[] => addAwait(fixer, expression, node), + messageId: 'floatingFixAwait', }, ], }); } else { context.report({ - node, messageId: nonFunctionHandler ? 'floatingUselessRejectionHandler' : 'floating', + node, suggest: [ { - messageId: 'floatingFixAwait', fix: (fixer): TSESLint.RuleFix | TSESLint.RuleFix[] => addAwait(fixer, expression, node), + messageId: 'floatingFixAwait', }, ], }); @@ -448,6 +395,61 @@ export default createRule({ return false; } }, + defaultOptions: [ + { + allowForKnownSafeCalls: readonlynessOptionsDefaults.allow, + allowForKnownSafePromises: readonlynessOptionsDefaults.allow, + checkThenables: false, + ignoreIIFE: false, + ignoreVoid: true, + }, + ], + meta: { + docs: { + description: + 'Require Promise-like statements to be handled appropriately', + recommended: 'recommended', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + floating: messageBase, + floatingFixAwait: 'Add await operator.', + floatingFixVoid: 'Add void operator to ignore.', + floatingPromiseArray: messagePromiseArray, + floatingPromiseArrayVoid: messagePromiseArrayVoid, + floatingUselessRejectionHandler: `${messageBase} ${messageRejectionHandler}`, + floatingUselessRejectionHandlerVoid: `${messageBaseVoid} ${messageRejectionHandler}`, + floatingVoid: messageBaseVoid, + }, + schema: [ + { + additionalProperties: false, + properties: { + allowForKnownSafeCalls: readonlynessOptionsSchema.properties.allow, + allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow, + checkThenables: { + description: + 'Whether to check all "Thenable"s, not just the built-in Promise type.', + type: 'boolean', + }, + ignoreIIFE: { + description: + 'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).', + type: 'boolean', + }, + ignoreVoid: { + description: 'Whether to ignore `void` expressions.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + + name: 'no-floating-promises', }); function hasMatchingSignature( diff --git a/packages/eslint-plugin/src/rules/no-for-in-array.ts b/packages/eslint-plugin/src/rules/no-for-in-array.ts index 6d104b639ebe..08fcb66e8413 100644 --- a/packages/eslint-plugin/src/rules/no-for-in-array.ts +++ b/packages/eslint-plugin/src/rules/no-for-in-array.ts @@ -9,21 +9,6 @@ import { import { getForStatementHeadLoc } from '../util/getForStatementHeadLoc'; export default createRule({ - name: 'no-for-in-array', - meta: { - docs: { - description: 'Disallow iterating over an array with a for-in loop', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - forInViolation: - 'For-in loops over arrays skips holes, returns indices as strings, and may visit the prototype chain or other enumerable properties. Use a more robust iteration method such as for-of or array.forEach instead.', - }, - schema: [], - type: 'problem', - }, - defaultOptions: [], create(context) { return { ForInStatement(node): void { @@ -44,4 +29,19 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow iterating over an array with a for-in loop', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + forInViolation: + 'For-in loops over arrays skips holes, returns indices as strings, and may visit the prototype chain or other enumerable properties. Use a more robust iteration method such as for-of or array.forEach instead.', + }, + schema: [], + type: 'problem', + }, + name: 'no-for-in-array', }); diff --git a/packages/eslint-plugin/src/rules/no-implied-eval.ts b/packages/eslint-plugin/src/rules/no-implied-eval.ts index 62a8d7cd102e..b2f29d50c735 100644 --- a/packages/eslint-plugin/src/rules/no-implied-eval.ts +++ b/packages/eslint-plugin/src/rules/no-implied-eval.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -20,23 +21,6 @@ const EVAL_LIKE_METHODS = new Set([ ]); export default createRule({ - name: 'no-implied-eval', - meta: { - docs: { - description: 'Disallow the use of `eval()`-like methods', - recommended: 'recommended', - extendsBaseRule: true, - requiresTypeChecking: true, - }, - messages: { - noImpliedEvalError: 'Implied eval. Consider passing a function.', - noFunctionConstructor: - 'Implied eval. Do not use the Function constructor to create functions.', - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -132,11 +116,11 @@ export default createRule({ if ( isBuiltinSymbolLike(services.program, type, 'FunctionConstructor') ) { - context.report({ node, messageId: 'noFunctionConstructor' }); + context.report({ messageId: 'noFunctionConstructor', node }); return; } } else { - context.report({ node, messageId: 'noFunctionConstructor' }); + context.report({ messageId: 'noFunctionConstructor', node }); return; } } @@ -151,13 +135,30 @@ export default createRule({ !isFunction(handler) && isReferenceToGlobalFunction(calleeName, node, context.sourceCode) ) { - context.report({ node: handler, messageId: 'noImpliedEvalError' }); + context.report({ messageId: 'noImpliedEvalError', node: handler }); } } return { - NewExpression: checkImpliedEval, CallExpression: checkImpliedEval, + NewExpression: checkImpliedEval, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow the use of `eval()`-like methods', + extendsBaseRule: true, + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + noFunctionConstructor: + 'Implied eval. Do not use the Function constructor to create functions.', + noImpliedEvalError: 'Implied eval. Consider passing a function.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-implied-eval', }); diff --git a/packages/eslint-plugin/src/rules/no-import-type-side-effects.ts b/packages/eslint-plugin/src/rules/no-import-type-side-effects.ts index 1658d471bc7b..727e55a6ded7 100644 --- a/packages/eslint-plugin/src/rules/no-import-type-side-effects.ts +++ b/packages/eslint-plugin/src/rules/no-import-type-side-effects.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { @@ -13,21 +14,6 @@ type Options = []; type MessageIds = 'useTopLevelQualifier'; export default createRule({ - name: 'no-import-type-side-effects', - meta: { - type: 'problem', - docs: { - description: - 'Enforce the use of top-level import type qualifier when an import only has specifiers with inline type qualifiers', - }, - fixable: 'code', - messages: { - useTopLevelQualifier: - 'TypeScript will only remove the inline type specifiers which will leave behind a side effect import at runtime. Convert this to a top-level type qualifier to properly remove the entire import.', - }, - schema: [], - }, - defaultOptions: [], create(context) { return { 'ImportDeclaration[importKind!="type"]'( @@ -49,8 +35,6 @@ export default createRule({ } context.report({ - node, - messageId: 'useTopLevelQualifier', fix(fixer) { const fixes: TSESLint.RuleFix[] = []; for (const specifier of specifiers) { @@ -77,8 +61,25 @@ export default createRule({ return fixes; }, + messageId: 'useTopLevelQualifier', + node, }); }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Enforce the use of top-level import type qualifier when an import only has specifiers with inline type qualifiers', + }, + fixable: 'code', + messages: { + useTopLevelQualifier: + 'TypeScript will only remove the inline type specifiers which will leave behind a side effect import at runtime. Convert this to a top-level type qualifier to properly remove the entire import.', + }, + schema: [], + type: 'problem', + }, + name: 'no-import-type-side-effects', }); diff --git a/packages/eslint-plugin/src/rules/no-inferrable-types.ts b/packages/eslint-plugin/src/rules/no-inferrable-types.ts index 51ead2ae476a..bd27cd86af07 100644 --- a/packages/eslint-plugin/src/rules/no-inferrable-types.ts +++ b/packages/eslint-plugin/src/rules/no-inferrable-types.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/internal/prefer-ast-types-enum */ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, nullThrows, NullThrowsReasons } from '../util'; @@ -13,40 +14,6 @@ type Options = [ type MessageIds = 'noInferrableType'; export default createRule({ - name: 'no-inferrable-types', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow explicit type declarations for variables or parameters initialized to a number, string, or boolean', - recommended: 'stylistic', - }, - fixable: 'code', - messages: { - noInferrableType: - 'Type {{type}} trivially inferred from a {{type}} literal, remove type annotation.', - }, - schema: [ - { - type: 'object', - properties: { - ignoreParameters: { - type: 'boolean', - }, - ignoreProperties: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - ignoreParameters: false, - ignoreProperties: false, - }, - ], create(context, [{ ignoreParameters, ignoreProperties }]) { function isFunctionCall( init: TSESTree.Expression, @@ -97,8 +64,8 @@ export default createRule({ const keywordMap = { [AST_NODE_TYPES.TSBigIntKeyword]: 'bigint', [AST_NODE_TYPES.TSBooleanKeyword]: 'boolean', - [AST_NODE_TYPES.TSNumberKeyword]: 'number', [AST_NODE_TYPES.TSNullKeyword]: 'null', + [AST_NODE_TYPES.TSNumberKeyword]: 'number', [AST_NODE_TYPES.TSStringKeyword]: 'string', [AST_NODE_TYPES.TSSymbolKeyword]: 'symbol', [AST_NODE_TYPES.TSUndefinedKeyword]: 'undefined', @@ -211,8 +178,6 @@ export default createRule({ : keywordMap[typeNode.typeAnnotation.type]; context.report({ - node, - messageId: 'noInferrableType', data: { type, }, @@ -231,6 +196,8 @@ export default createRule({ } yield fixer.remove(typeNode); }, + messageId: 'noInferrableType', + node, }); } @@ -275,11 +242,45 @@ export default createRule({ } return { - VariableDeclarator: inferrableVariableVisitor, - FunctionExpression: inferrableParameterVisitor, - FunctionDeclaration: inferrableParameterVisitor, ArrowFunctionExpression: inferrableParameterVisitor, + FunctionDeclaration: inferrableParameterVisitor, + FunctionExpression: inferrableParameterVisitor, PropertyDefinition: inferrablePropertyVisitor, + VariableDeclarator: inferrableVariableVisitor, }; }, + defaultOptions: [ + { + ignoreParameters: false, + ignoreProperties: false, + }, + ], + meta: { + docs: { + description: + 'Disallow explicit type declarations for variables or parameters initialized to a number, string, or boolean', + recommended: 'stylistic', + }, + fixable: 'code', + messages: { + noInferrableType: + 'Type {{type}} trivially inferred from a {{type}} literal, remove type annotation.', + }, + schema: [ + { + additionalProperties: false, + properties: { + ignoreParameters: { + type: 'boolean', + }, + ignoreProperties: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-inferrable-types', }); diff --git a/packages/eslint-plugin/src/rules/no-invalid-this.ts b/packages/eslint-plugin/src/rules/no-invalid-this.ts index bf8d2a5c904b..f27b1ccae90d 100644 --- a/packages/eslint-plugin/src/rules/no-invalid-this.ts +++ b/packages/eslint-plugin/src/rules/no-invalid-this.ts @@ -1,10 +1,12 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -14,19 +16,6 @@ export type Options = InferOptionsTypeFromRule; export type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ - name: 'no-invalid-this', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow `this` keywords outside of classes or class-like objects', - extendsBaseRule: true, - }, - messages: baseRule.meta.messages, - hasSuggestions: baseRule.meta.hasSuggestions, - schema: baseRule.meta.schema, - }, - defaultOptions: [{ capIsConstructor: true }], create(context) { const rules = baseRule.create(context); @@ -48,12 +37,6 @@ export default createRule({ return { ...rules, - PropertyDefinition(): void { - thisIsValidStack.push(true); - }, - 'PropertyDefinition:exit'(): void { - thisIsValidStack.pop(); - }, AccessorProperty(): void { thisIsValidStack.push(true); }, @@ -82,6 +65,12 @@ export default createRule({ 'FunctionExpression:exit'(): void { thisIsValidStack.pop(); }, + PropertyDefinition(): void { + thisIsValidStack.push(true); + }, + 'PropertyDefinition:exit'(): void { + thisIsValidStack.pop(); + }, ThisExpression(node: TSESTree.ThisExpression): void { const thisIsValidHere = thisIsValidStack[thisIsValidStack.length - 1]; @@ -94,4 +83,17 @@ export default createRule({ }, }; }, + defaultOptions: [{ capIsConstructor: true }], + meta: { + docs: { + description: + 'Disallow `this` keywords outside of classes or class-like objects', + extendsBaseRule: true, + }, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: baseRule.meta.schema, + type: 'suggestion', + }, + name: 'no-invalid-this', }); diff --git a/packages/eslint-plugin/src/rules/no-invalid-void-type.ts b/packages/eslint-plugin/src/rules/no-invalid-void-type.ts index f518e2dc861b..e9db3b139152 100644 --- a/packages/eslint-plugin/src/rules/no-invalid-void-type.ts +++ b/packages/eslint-plugin/src/rules/no-invalid-void-type.ts @@ -1,11 +1,12 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; interface Options { - allowInGenericTypeArguments?: [string, ...string[]] | boolean; allowAsThisParameter?: boolean; + allowInGenericTypeArguments?: [string, ...string[]] | boolean; } type MessageIds = @@ -17,52 +18,7 @@ type MessageIds = | 'invalidVoidUnionConstituent'; export default createRule<[Options], MessageIds>({ - name: 'no-invalid-void-type', - meta: { - type: 'problem', - docs: { - description: 'Disallow `void` type outside of generic or return types', - recommended: 'strict', - }, - messages: { - invalidVoidForGeneric: - '{{ generic }} may not have void as a type argument.', - invalidVoidNotReturnOrGeneric: - 'void is only valid as a return type or generic type argument.', - invalidVoidNotReturn: 'void is only valid as a return type.', - invalidVoidNotReturnOrThisParam: - 'void is only valid as return type or type of `this` parameter.', - invalidVoidNotReturnOrThisParamOrGeneric: - 'void is only valid as a return type or generic type argument or the type of a `this` parameter.', - invalidVoidUnionConstituent: - 'void is not valid as a constituent in a union type', - }, - schema: [ - { - type: 'object', - properties: { - allowInGenericTypeArguments: { - oneOf: [ - { type: 'boolean' }, - { - type: 'array', - items: { type: 'string' }, - minItems: 1, - }, - ], - }, - allowAsThisParameter: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { allowInGenericTypeArguments: true, allowAsThisParameter: false }, - ], - create(context, [{ allowInGenericTypeArguments, allowAsThisParameter }]) { + create(context, [{ allowAsThisParameter, allowInGenericTypeArguments }]) { const validParents: AST_NODE_TYPES[] = [ AST_NODE_TYPES.TSTypeAnnotation, // ]; @@ -111,8 +67,8 @@ export default createRule<[Options], MessageIds>({ .includes(fullyQualifiedName) ) { context.report({ - messageId: 'invalidVoidForGeneric', data: { generic: fullyQualifiedName }, + messageId: 'invalidVoidForGeneric', node, }); } @@ -226,6 +182,51 @@ export default createRule<[Options], MessageIds>({ }, }; }, + defaultOptions: [ + { allowAsThisParameter: false, allowInGenericTypeArguments: true }, + ], + meta: { + docs: { + description: 'Disallow `void` type outside of generic or return types', + recommended: 'strict', + }, + messages: { + invalidVoidForGeneric: + '{{ generic }} may not have void as a type argument.', + invalidVoidNotReturn: 'void is only valid as a return type.', + invalidVoidNotReturnOrGeneric: + 'void is only valid as a return type or generic type argument.', + invalidVoidNotReturnOrThisParam: + 'void is only valid as return type or type of `this` parameter.', + invalidVoidNotReturnOrThisParamOrGeneric: + 'void is only valid as a return type or generic type argument or the type of a `this` parameter.', + invalidVoidUnionConstituent: + 'void is not valid as a constituent in a union type', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowAsThisParameter: { + type: 'boolean', + }, + allowInGenericTypeArguments: { + oneOf: [ + { type: 'boolean' }, + { + items: { type: 'string' }, + minItems: 1, + type: 'array', + }, + ], + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'no-invalid-void-type', }); function getNotReturnOrGenericMessageId( diff --git a/packages/eslint-plugin/src/rules/no-loop-func.ts b/packages/eslint-plugin/src/rules/no-loop-func.ts index a34217453b0d..d53f1959b906 100644 --- a/packages/eslint-plugin/src/rules/no-loop-func.ts +++ b/packages/eslint-plugin/src/rules/no-loop-func.ts @@ -1,10 +1,12 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -14,19 +16,6 @@ type Options = InferOptionsTypeFromRule; type MessageIds = InferMessageIdsTypeFromRule; export default createRule({ - name: 'no-loop-func', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow function declarations that contain unsafe references inside loop statements', - extendsBaseRule: true, - }, - hasSuggestions: baseRule.meta.hasSuggestions, - schema: [], - messages: baseRule.meta.messages, - }, - defaultOptions: [], create(context) { /** * Reports functions which match the following condition: @@ -54,19 +43,32 @@ export default createRule({ if (unsafeRefs.length > 0) { context.report({ - node, - messageId: 'unsafeRefs', data: { varNames: `'${unsafeRefs.join("', '")}'` }, + messageId: 'unsafeRefs', + node, }); } } return { ArrowFunctionExpression: checkForLoops, - FunctionExpression: checkForLoops, FunctionDeclaration: checkForLoops, + FunctionExpression: checkForLoops, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow function declarations that contain unsafe references inside loop statements', + extendsBaseRule: true, + }, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: [], + type: 'suggestion', + }, + name: 'no-loop-func', }); /** diff --git a/packages/eslint-plugin/src/rules/no-loss-of-precision.ts b/packages/eslint-plugin/src/rules/no-loss-of-precision.ts index 000e5d9aa0bc..1631c33ac46e 100644 --- a/packages/eslint-plugin/src/rules/no-loss-of-precision.ts +++ b/packages/eslint-plugin/src/rules/no-loss-of-precision.ts @@ -2,6 +2,7 @@ import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -11,20 +12,20 @@ type Options = InferOptionsTypeFromRule>; type MessageIds = InferMessageIdsTypeFromRule>; export default createRule({ - name: 'no-loss-of-precision', + create(context) { + return baseRule.create(context); + }, + defaultOptions: [], meta: { - type: 'problem', deprecated: true, docs: { description: 'Disallow literal numbers that lose precision', extendsBaseRule: true, }, hasSuggestions: baseRule.meta.hasSuggestions, - schema: [], messages: baseRule.meta.messages, + schema: [], + type: 'problem', }, - defaultOptions: [], - create(context) { - return baseRule.create(context); - }, + name: 'no-loss-of-precision', }); diff --git a/packages/eslint-plugin/src/rules/no-magic-numbers.ts b/packages/eslint-plugin/src/rules/no-magic-numbers.ts index 5a86056cb90d..7a39d084d894 100644 --- a/packages/eslint-plugin/src/rules/no-magic-numbers.ts +++ b/packages/eslint-plugin/src/rules/no-magic-numbers.ts @@ -1,11 +1,13 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule, deepMerge } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -22,10 +24,10 @@ const schema = deepMerge( : baseRule.meta.schema, { properties: { - ignoreNumericLiteralTypes: { + ignoreEnums: { type: 'boolean', }, - ignoreEnums: { + ignoreNumericLiteralTypes: { type: 'boolean', }, ignoreReadonlyClassProperties: { @@ -39,28 +41,6 @@ const schema = deepMerge( ) as unknown as JSONSchema4; export default createRule({ - name: 'no-magic-numbers', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow magic numbers', - extendsBaseRule: true, - }, - schema: [schema], - messages: baseRule.meta.messages, - }, - defaultOptions: [ - { - ignore: [], - ignoreArrayIndexes: false, - enforceConst: false, - detectObjects: false, - ignoreNumericLiteralTypes: false, - ignoreEnums: false, - ignoreReadonlyClassProperties: false, - ignoreTypeIndexes: false, - }, - ], create(context, [options]) { const rules = baseRule.create(context); @@ -121,9 +101,9 @@ export default createRule({ } context.report({ + data: { raw }, messageId: 'noMagic', node: fullNumberNode, - data: { raw }, }); return; @@ -134,6 +114,28 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + detectObjects: false, + enforceConst: false, + ignore: [], + ignoreArrayIndexes: false, + ignoreEnums: false, + ignoreNumericLiteralTypes: false, + ignoreReadonlyClassProperties: false, + ignoreTypeIndexes: false, + }, + ], + meta: { + docs: { + description: 'Disallow magic numbers', + extendsBaseRule: true, + }, + messages: baseRule.meta.messages, + schema: [schema], + type: 'suggestion', + }, + name: 'no-magic-numbers', }); /** @@ -158,7 +160,7 @@ function normalizeIgnoreValue( */ function normalizeLiteralValue( node: TSESTree.BigIntLiteral | TSESTree.NumberLiteral, - value: number | bigint, + value: bigint | number, ): bigint | number { if ( node.parent.type === AST_NODE_TYPES.UnaryExpression && diff --git a/packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts b/packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts index e8fdde8d1f33..d792a9a9338a 100644 --- a/packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts +++ b/packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { ESLintUtils } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -12,37 +13,6 @@ type Options = [ ]; export default createRule({ - name: 'no-meaningless-void-operator', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow the `void` operator except when used to discard a value', - recommended: 'strict', - requiresTypeChecking: true, - }, - fixable: 'code', - hasSuggestions: true, - messages: { - meaninglessVoidOperator: - "void operator shouldn't be used on {{type}}; it should convey that a return value is being ignored", - removeVoid: "Remove 'void'", - }, - schema: [ - { - type: 'object', - properties: { - checkNever: { - type: 'boolean', - default: false, - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [{ checkNever: false }], - create(context, [{ checkNever }]) { const services = ESLintUtils.getParserServices(context); const checker = services.program.getTypeChecker(); @@ -64,10 +34,10 @@ export default createRule({ ) ) { context.report({ - node, - messageId: 'meaninglessVoidOperator', data: { type: checker.typeToString(argType) }, fix, + messageId: 'meaninglessVoidOperator', + node, }); } else if ( checkNever && @@ -78,13 +48,44 @@ export default createRule({ ) ) { context.report({ - node, - messageId: 'meaninglessVoidOperator', data: { type: checker.typeToString(argType) }, - suggest: [{ messageId: 'removeVoid', fix }], + messageId: 'meaninglessVoidOperator', + node, + suggest: [{ fix, messageId: 'removeVoid' }], }); } }, }; }, + defaultOptions: [{ checkNever: false }], + meta: { + docs: { + description: + 'Disallow the `void` operator except when used to discard a value', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + hasSuggestions: true, + messages: { + meaninglessVoidOperator: + "void operator shouldn't be used on {{type}}; it should convey that a return value is being ignored", + removeVoid: "Remove 'void'", + }, + schema: [ + { + additionalProperties: false, + properties: { + checkNever: { + default: false, + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + + name: 'no-meaningless-void-operator', }); diff --git a/packages/eslint-plugin/src/rules/no-misused-new.ts b/packages/eslint-plugin/src/rules/no-misused-new.ts index f877f9d7d9cb..53c3447357ba 100644 --- a/packages/eslint-plugin/src/rules/no-misused-new.ts +++ b/packages/eslint-plugin/src/rules/no-misused-new.ts @@ -1,23 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'no-misused-new', - meta: { - type: 'problem', - docs: { - description: 'Enforce valid definition of `new` and `constructor`', - recommended: 'recommended', - }, - schema: [], - messages: { - errorMessageInterface: 'Interfaces cannot be constructed, only classes.', - errorMessageClass: 'Class cannot have method named `new`.', - }, - }, - defaultOptions: [], create(context) { /** * @param node type to be inspected. @@ -51,10 +38,10 @@ export default createRule({ */ function isMatchingParentType( parent: - | TSESTree.TSInterfaceDeclaration | TSESTree.ClassDeclaration | TSESTree.ClassExpression | TSESTree.Identifier + | TSESTree.TSInterfaceDeclaration | undefined, returnType: TSESTree.TSTypeAnnotation | undefined, ): boolean { @@ -71,6 +58,18 @@ export default createRule({ } return { + "ClassBody > MethodDefinition[key.name='new']"( + node: TSESTree.MethodDefinition, + ): void { + if (node.value.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression) { + if (isMatchingParentType(node.parent.parent, node.value.returnType)) { + context.report({ + messageId: 'errorMessageClass', + node, + }); + } + } + }, 'TSInterfaceBody > TSConstructSignatureDeclaration'( node: TSESTree.TSConstructSignatureDeclaration, ): void { @@ -82,8 +81,8 @@ export default createRule({ ) { // constructor context.report({ - node, messageId: 'errorMessageInterface', + node, }); } }, @@ -91,22 +90,24 @@ export default createRule({ node: TSESTree.TSMethodSignature, ): void { context.report({ - node, messageId: 'errorMessageInterface', + node, }); }, - "ClassBody > MethodDefinition[key.name='new']"( - node: TSESTree.MethodDefinition, - ): void { - if (node.value.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression) { - if (isMatchingParentType(node.parent.parent, node.value.returnType)) { - context.report({ - node, - messageId: 'errorMessageClass', - }); - } - } - }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Enforce valid definition of `new` and `constructor`', + recommended: 'recommended', + }, + messages: { + errorMessageClass: 'Class cannot have method named `new`.', + errorMessageInterface: 'Interfaces cannot be constructed, only classes.', + }, + schema: [], + type: 'problem', + }, + name: 'no-misused-new', }); diff --git a/packages/eslint-plugin/src/rules/no-misused-promises.ts b/packages/eslint-plugin/src/rules/no-misused-promises.ts index a5433edf7d10..014578519b65 100644 --- a/packages/eslint-plugin/src/rules/no-misused-promises.ts +++ b/packages/eslint-plugin/src/rules/no-misused-promises.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -15,8 +16,8 @@ import { type Options = [ { checksConditionals?: boolean; - checksVoidReturn?: ChecksVoidReturnOptions | boolean; checksSpreads?: boolean; + checksVoidReturn?: ChecksVoidReturnOptions | boolean; }, ]; @@ -70,71 +71,7 @@ function parseChecksVoidReturn( } export default createRule({ - name: 'no-misused-promises', - meta: { - docs: { - description: 'Disallow Promises in places not designed to handle them', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - voidReturnArgument: - 'Promise returned in function argument where a void return was expected.', - voidReturnAttribute: - 'Promise-returning function provided to attribute where a void return was expected.', - voidReturnInheritedMethod: - "Promise-returning method provided where a void return was expected by extended/implemented type '{{ heritageTypeName }}'.", - voidReturnProperty: - 'Promise-returning function provided to property where a void return was expected.', - voidReturnReturnValue: - 'Promise-returning function provided to return value where a void return was expected.', - voidReturnVariable: - 'Promise-returning function provided to variable where a void return was expected.', - conditional: 'Expected non-Promise value in a boolean conditional.', - spread: 'Expected a non-Promise value to be spreaded in an object.', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - checksConditionals: { - type: 'boolean', - }, - checksVoidReturn: { - oneOf: [ - { type: 'boolean' }, - { - additionalProperties: false, - properties: { - arguments: { type: 'boolean' }, - attributes: { type: 'boolean' }, - inheritedMethods: { type: 'boolean' }, - properties: { type: 'boolean' }, - returns: { type: 'boolean' }, - variables: { type: 'boolean' }, - }, - type: 'object', - }, - ], - }, - checksSpreads: { - type: 'boolean', - }, - }, - }, - ], - type: 'problem', - }, - defaultOptions: [ - { - checksConditionals: true, - checksVoidReturn: true, - checksSpreads: true, - }, - ], - - create(context, [{ checksConditionals, checksVoidReturn, checksSpreads }]) { + create(context, [{ checksConditionals, checksSpreads, checksVoidReturn }]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -537,9 +474,9 @@ export default createRule({ return; } context.report({ - node: services.tsNodeToESTreeNodeMap.get(nodeMember), - messageId: 'voidReturnInheritedMethod', data: { heritageTypeName: checker.typeToString(heritageType) }, + messageId: 'voidReturnInheritedMethod', + node: services.tsNodeToESTreeNodeMap.get(nodeMember), }); } @@ -590,6 +527,70 @@ export default createRule({ ...(checksSpreads ? spreadChecks : {}), }; }, + defaultOptions: [ + { + checksConditionals: true, + checksSpreads: true, + checksVoidReturn: true, + }, + ], + meta: { + docs: { + description: 'Disallow Promises in places not designed to handle them', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + conditional: 'Expected non-Promise value in a boolean conditional.', + spread: 'Expected a non-Promise value to be spreaded in an object.', + voidReturnArgument: + 'Promise returned in function argument where a void return was expected.', + voidReturnAttribute: + 'Promise-returning function provided to attribute where a void return was expected.', + voidReturnInheritedMethod: + "Promise-returning method provided where a void return was expected by extended/implemented type '{{ heritageTypeName }}'.", + voidReturnProperty: + 'Promise-returning function provided to property where a void return was expected.', + voidReturnReturnValue: + 'Promise-returning function provided to return value where a void return was expected.', + voidReturnVariable: + 'Promise-returning function provided to variable where a void return was expected.', + }, + schema: [ + { + additionalProperties: false, + properties: { + checksConditionals: { + type: 'boolean', + }, + checksSpreads: { + type: 'boolean', + }, + checksVoidReturn: { + oneOf: [ + { type: 'boolean' }, + { + additionalProperties: false, + properties: { + arguments: { type: 'boolean' }, + attributes: { type: 'boolean' }, + inheritedMethods: { type: 'boolean' }, + properties: { type: 'boolean' }, + returns: { type: 'boolean' }, + variables: { type: 'boolean' }, + }, + type: 'object', + }, + ], + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + + name: 'no-misused-promises', }); function isSometimesThenable(checker: ts.TypeChecker, node: ts.Node): boolean { diff --git a/packages/eslint-plugin/src/rules/no-mixed-enums.ts b/packages/eslint-plugin/src/rules/no-mixed-enums.ts index ce6a706a10d0..68e3594b27bb 100644 --- a/packages/eslint-plugin/src/rules/no-mixed-enums.ts +++ b/packages/eslint-plugin/src/rules/no-mixed-enums.ts @@ -1,6 +1,7 @@ import type { Scope } from '@typescript-eslint/scope-manager'; -import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; + +import { DefinitionType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -14,20 +15,6 @@ enum AllowedType { } export default createRule({ - name: 'no-mixed-enums', - meta: { - docs: { - description: 'Disallow enums from having both number and string members', - recommended: 'strict', - requiresTypeChecking: true, - }, - messages: { - mixed: `Mixing number and string enums can be confusing.`, - }, - schema: [], - type: 'problem', - }, - defaultOptions: [], create(context) { const parserServices = getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); @@ -217,4 +204,18 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow enums from having both number and string members', + recommended: 'strict', + requiresTypeChecking: true, + }, + messages: { + mixed: `Mixing number and string enums can be confusing.`, + }, + schema: [], + type: 'problem', + }, + name: 'no-mixed-enums', }); diff --git a/packages/eslint-plugin/src/rules/no-namespace.ts b/packages/eslint-plugin/src/rules/no-namespace.ts index c6b9213259be..95ddae51a851 100644 --- a/packages/eslint-plugin/src/rules/no-namespace.ts +++ b/packages/eslint-plugin/src/rules/no-namespace.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isDefinitionFile } from '../util'; @@ -12,42 +13,6 @@ type Options = [ type MessageIds = 'moduleSyntaxIsPreferred'; export default createRule({ - name: 'no-namespace', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow TypeScript namespaces', - recommended: 'recommended', - }, - messages: { - moduleSyntaxIsPreferred: - 'ES2015 module syntax is preferred over namespaces.', - }, - schema: [ - { - type: 'object', - properties: { - allowDeclarations: { - description: - 'Whether to allow `declare` with custom TypeScript namespaces.', - type: 'boolean', - }, - allowDefinitionFiles: { - description: - 'Whether to allow `declare` with custom TypeScript namespaces inside definition files.', - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - allowDeclarations: false, - allowDefinitionFiles: true, - }, - ], create(context, [{ allowDeclarations, allowDefinitionFiles }]) { function isDeclaration(node: TSESTree.Node): boolean { if (node.type === AST_NODE_TYPES.TSModuleDeclaration && node.declare) { @@ -70,10 +35,46 @@ export default createRule({ } context.report({ - node, messageId: 'moduleSyntaxIsPreferred', + node, }); }, }; }, + defaultOptions: [ + { + allowDeclarations: false, + allowDefinitionFiles: true, + }, + ], + meta: { + docs: { + description: 'Disallow TypeScript namespaces', + recommended: 'recommended', + }, + messages: { + moduleSyntaxIsPreferred: + 'ES2015 module syntax is preferred over namespaces.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowDeclarations: { + description: + 'Whether to allow `declare` with custom TypeScript namespaces.', + type: 'boolean', + }, + allowDefinitionFiles: { + description: + 'Whether to allow `declare` with custom TypeScript namespaces inside definition files.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-namespace', }); diff --git a/packages/eslint-plugin/src/rules/no-non-null-asserted-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/no-non-null-asserted-nullish-coalescing.ts index 1369254b16ae..3315fe241f15 100644 --- a/packages/eslint-plugin/src/rules/no-non-null-asserted-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/no-non-null-asserted-nullish-coalescing.ts @@ -1,6 +1,7 @@ import type { Definition } from '@typescript-eslint/scope-manager'; -import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESLint } from '@typescript-eslint/utils'; + +import { DefinitionType } from '@typescript-eslint/scope-manager'; import { ASTUtils, TSESTree } from '@typescript-eslint/utils'; import { createRule, nullThrows, NullThrowsReasons } from '../util'; @@ -30,23 +31,6 @@ function isDefinitionWithAssignment(definition: Definition): boolean { } export default createRule({ - name: 'no-non-null-asserted-nullish-coalescing', - meta: { - type: 'problem', - docs: { - description: - 'Disallow non-null assertions in the left operand of a nullish coalescing operator', - recommended: 'strict', - }, - messages: { - noNonNullAssertedNullishCoalescing: - 'The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed.', - suggestRemovingNonNull: 'Remove the non-null assertion.', - }, - schema: [], - hasSuggestions: true, - }, - defaultOptions: [], create(context) { return { 'LogicalExpression[operator = "??"] > TSNonNullExpression.left'( @@ -62,8 +46,8 @@ export default createRule({ } context.report({ - node, messageId: 'noNonNullAssertedNullishCoalescing', + node, /* Use a suggestion instead of a fixer, because this can break type checks. The resulting type of the nullish coalesce is only influenced by the right operand if the left operand can be `null` or `undefined`. @@ -79,7 +63,6 @@ export default createRule({ */ suggest: [ { - messageId: 'suggestRemovingNonNull', fix(fixer): TSESLint.RuleFix { const exclamationMark = nullThrows( context.sourceCode.getLastToken( @@ -90,10 +73,28 @@ export default createRule({ ); return fixer.remove(exclamationMark); }, + messageId: 'suggestRemovingNonNull', }, ], }); }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow non-null assertions in the left operand of a nullish coalescing operator', + recommended: 'strict', + }, + hasSuggestions: true, + messages: { + noNonNullAssertedNullishCoalescing: + 'The nullish coalescing operator is designed to handle undefined and null - using a non-null assertion is not needed.', + suggestRemovingNonNull: 'Remove the non-null assertion.', + }, + schema: [], + type: 'problem', + }, + name: 'no-non-null-asserted-nullish-coalescing', }); diff --git a/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts b/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts index efc8fc26cf5a..2e474b0a5513 100644 --- a/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/no-non-null-asserted-optional-chain.ts @@ -3,23 +3,6 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'no-non-null-asserted-optional-chain', - meta: { - type: 'problem', - docs: { - description: - 'Disallow non-null assertions after an optional chain expression', - recommended: 'recommended', - }, - hasSuggestions: true, - messages: { - noNonNullOptionalChain: - 'Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong.', - suggestRemovingNonNull: 'You should remove the non-null assertion.', - }, - schema: [], - }, - defaultOptions: [], create(context) { return { // non-nulling a wrapped chain will scrub all nulls introduced by the chain @@ -31,18 +14,18 @@ export default createRule({ // selector guarantees this assertion const parent = node.parent as TSESTree.TSNonNullExpression; context.report({ - node, messageId: 'noNonNullOptionalChain', + node, // use a suggestion instead of a fixer, because this can obviously break type checks suggest: [ { - messageId: 'suggestRemovingNonNull', fix(fixer): TSESLint.RuleFix { return fixer.removeRange([ parent.range[1] - 1, parent.range[1], ]); }, + messageId: 'suggestRemovingNonNull', }, ], }); @@ -55,19 +38,36 @@ export default createRule({ node: TSESTree.TSNonNullExpression, ): void { context.report({ - node, messageId: 'noNonNullOptionalChain', + node, // use a suggestion instead of a fixer, because this can obviously break type checks suggest: [ { - messageId: 'suggestRemovingNonNull', fix(fixer): TSESLint.RuleFix { return fixer.removeRange([node.range[1] - 1, node.range[1]]); }, + messageId: 'suggestRemovingNonNull', }, ], }); }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow non-null assertions after an optional chain expression', + recommended: 'recommended', + }, + hasSuggestions: true, + messages: { + noNonNullOptionalChain: + 'Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong.', + suggestRemovingNonNull: 'You should remove the non-null assertion.', + }, + schema: [], + type: 'problem', + }, + name: 'no-non-null-asserted-optional-chain', }); diff --git a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts index 475262c2a4a0..422f860489c0 100644 --- a/packages/eslint-plugin/src/rules/no-non-null-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-non-null-assertion.ts @@ -1,4 +1,5 @@ import type { TSESLint } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { @@ -11,23 +12,6 @@ import { type MessageIds = 'noNonNull' | 'suggestOptionalChain'; export default createRule<[], MessageIds>({ - name: 'no-non-null-assertion', - meta: { - type: 'problem', - docs: { - description: - 'Disallow non-null assertions using the `!` postfix operator', - recommended: 'strict', - }, - hasSuggestions: true, - messages: { - noNonNull: 'Forbidden non-null assertion.', - suggestOptionalChain: - 'Consider using the optional chain operator `?.` instead. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator.', - }, - schema: [], - }, - defaultOptions: [], create(context) { return { TSNonNullExpression(node): void { @@ -58,13 +42,12 @@ export default createRule<[], MessageIds>({ if (node.parent.computed) { // it is x![y]?.z suggest.push({ - messageId: 'suggestOptionalChain', fix: replaceTokenWithOptional(), + messageId: 'suggestOptionalChain', }); } else { // it is x!.y?.z suggest.push({ - messageId: 'suggestOptionalChain', fix(fixer) { // x!.y?.z // ^ punctuator @@ -77,19 +60,20 @@ export default createRule<[], MessageIds>({ fixer.insertTextBefore(punctuator, '?'), ]; }, + messageId: 'suggestOptionalChain', }); } } else if (node.parent.computed) { // it is x!?.[y].z suggest.push({ - messageId: 'suggestOptionalChain', fix: removeToken(), + messageId: 'suggestOptionalChain', }); } else { // it is x!?.y.z suggest.push({ - messageId: 'suggestOptionalChain', fix: removeToken(), + messageId: 'suggestOptionalChain', }); } } else if ( @@ -99,24 +83,41 @@ export default createRule<[], MessageIds>({ if (!node.parent.optional) { // it is x.y?.z!() suggest.push({ - messageId: 'suggestOptionalChain', fix: replaceTokenWithOptional(), + messageId: 'suggestOptionalChain', }); } else { // it is x.y.z!?.() suggest.push({ - messageId: 'suggestOptionalChain', fix: removeToken(), + messageId: 'suggestOptionalChain', }); } } context.report({ - node, messageId: 'noNonNull', + node, suggest, }); }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow non-null assertions using the `!` postfix operator', + recommended: 'strict', + }, + hasSuggestions: true, + messages: { + noNonNull: 'Forbidden non-null assertion.', + suggestOptionalChain: + 'Consider using the optional chain operator `?.` instead. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator.', + }, + schema: [], + type: 'problem', + }, + name: 'no-non-null-assertion', }); diff --git a/packages/eslint-plugin/src/rules/no-redeclare.ts b/packages/eslint-plugin/src/rules/no-redeclare.ts index b084d90650b0..4e77b8adc561 100644 --- a/packages/eslint-plugin/src/rules/no-redeclare.ts +++ b/packages/eslint-plugin/src/rules/no-redeclare.ts @@ -1,5 +1,6 @@ -import { ScopeType } from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +import { ScopeType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, getNameLocationInGlobalDirectiveComment } from '../util'; @@ -13,41 +14,6 @@ type Options = [ ]; export default createRule({ - name: 'no-redeclare', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow variable redeclaration', - extendsBaseRule: true, - }, - schema: [ - { - type: 'object', - properties: { - builtinGlobals: { - type: 'boolean', - }, - ignoreDeclarationMerge: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - messages: { - redeclared: "'{{id}}' is already defined.", - redeclaredAsBuiltin: - "'{{id}}' is already defined as a built-in global variable.", - redeclaredBySyntax: - "'{{id}}' is already defined by a variable declaration.", - }, - }, - defaultOptions: [ - { - builtinGlobals: true, - ignoreDeclarationMerge: true, - }, - ], create(context, [options]) { const CLASS_DECLARATION_MERGE_NODES = new Set([ AST_NODE_TYPES.TSInterfaceDeclaration, @@ -65,9 +31,9 @@ export default createRule({ function* iterateDeclarations(variable: TSESLint.Scope.Variable): Generator< { - type: 'builtin' | 'comment' | 'syntax'; - node?: TSESTree.Comment | TSESTree.Identifier; loc?: TSESTree.SourceLocation; + node?: TSESTree.Comment | TSESTree.Identifier; + type: 'builtin' | 'comment' | 'syntax'; }, void > { @@ -86,13 +52,13 @@ export default createRule({ ) { for (const comment of variable.eslintExplicitGlobalComments) { yield { - type: 'comment', - node: comment, loc: getNameLocationInGlobalDirectiveComment( context.sourceCode, comment, variable.name, ), + node: comment, + type: 'comment', }; } } @@ -143,7 +109,7 @@ export default createRule({ // there's more than one class declaration, which needs to be reported for (const { identifier } of classDecls) { - yield { type: 'syntax', node: identifier, loc: identifier.loc }; + yield { loc: identifier.loc, node: identifier, type: 'syntax' }; } return; } @@ -164,7 +130,7 @@ export default createRule({ // there's more than one function declaration, which needs to be reported for (const { identifier } of functionDecls) { - yield { type: 'syntax', node: identifier, loc: identifier.loc }; + yield { loc: identifier.loc, node: identifier, type: 'syntax' }; } return; } @@ -185,14 +151,14 @@ export default createRule({ // there's more than one enum declaration, which needs to be reported for (const { identifier } of enumDecls) { - yield { type: 'syntax', node: identifier, loc: identifier.loc }; + yield { loc: identifier.loc, node: identifier, type: 'syntax' }; } return; } } for (const { identifier } of identifiers) { - yield { type: 'syntax', node: identifier, loc: identifier.loc }; + yield { loc: identifier.loc, node: identifier, type: 'syntax' }; } } @@ -217,14 +183,14 @@ export default createRule({ const data = { id: variable.name }; // Report extra declarations. - for (const { type, node, loc } of extraDeclarations) { + for (const { loc, node, type } of extraDeclarations) { const messageId = type === declaration.type ? 'redeclared' : detailMessageId; if (node) { - context.report({ node, loc, messageId, data }); + context.report({ data, loc, messageId, node }); } else if (loc) { - context.report({ loc, messageId, data }); + context.report({ data, loc, messageId }); } } } @@ -246,6 +212,15 @@ export default createRule({ } return { + ArrowFunctionExpression: checkForBlock, + + BlockStatement: checkForBlock, + ForInStatement: checkForBlock, + ForOfStatement: checkForBlock, + + ForStatement: checkForBlock, + FunctionDeclaration: checkForBlock, + FunctionExpression: checkForBlock, Program(node): void { const scope = context.sourceCode.getScope(node); @@ -261,16 +236,42 @@ export default createRule({ findVariablesInScope(scope.childScopes[0]); } }, - - FunctionDeclaration: checkForBlock, - FunctionExpression: checkForBlock, - ArrowFunctionExpression: checkForBlock, - - BlockStatement: checkForBlock, - ForStatement: checkForBlock, - ForInStatement: checkForBlock, - ForOfStatement: checkForBlock, SwitchStatement: checkForBlock, }; }, + defaultOptions: [ + { + builtinGlobals: true, + ignoreDeclarationMerge: true, + }, + ], + meta: { + docs: { + description: 'Disallow variable redeclaration', + extendsBaseRule: true, + }, + messages: { + redeclared: "'{{id}}' is already defined.", + redeclaredAsBuiltin: + "'{{id}}' is already defined as a built-in global variable.", + redeclaredBySyntax: + "'{{id}}' is already defined by a variable declaration.", + }, + schema: [ + { + additionalProperties: false, + properties: { + builtinGlobals: { + type: 'boolean', + }, + ignoreDeclarationMerge: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-redeclare', }); diff --git a/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts b/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts index 7a1475ad0f32..063f9f92f383 100644 --- a/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-redundant-type-constituents.ts @@ -57,9 +57,9 @@ const keywordNodeTypesToTsTypes = new Map([ [TSESTree.AST_NODE_TYPES.TSBigIntKeyword, ts.TypeFlags.BigInt], [TSESTree.AST_NODE_TYPES.TSBooleanKeyword, ts.TypeFlags.Boolean], [TSESTree.AST_NODE_TYPES.TSNeverKeyword, ts.TypeFlags.Never], - [TSESTree.AST_NODE_TYPES.TSUnknownKeyword, ts.TypeFlags.Unknown], [TSESTree.AST_NODE_TYPES.TSNumberKeyword, ts.TypeFlags.Number], [TSESTree.AST_NODE_TYPES.TSStringKeyword, ts.TypeFlags.String], + [TSESTree.AST_NODE_TYPES.TSUnknownKeyword, ts.TypeFlags.Unknown], ]); type PrimitiveTypeFlag = (typeof primitiveTypeFlags)[number]; @@ -192,25 +192,6 @@ function unionTypePartsUnlessBoolean(type: ts.Type): ts.Type[] { } export default createRule({ - name: 'no-redundant-type-constituents', - meta: { - docs: { - description: - 'Disallow members of unions and intersections that do nothing or override type information', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - literalOverridden: `{{literal}} is overridden by {{primitive}} in this union type.`, - primitiveOverridden: `{{primitive}} is overridden by the {{literal}} in this intersection type.`, - overridden: `'{{typeName}}' is overridden by other types in this {{container}} type.`, - overrides: `'{{typeName}}' overrides all other types in this {{container}} type.`, - errorTypeOverrides: `'{{typeName}}' is an 'error' type that acts as 'any' and overrides all other types in this {{container}} type.`, - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const typesCache = new Map(); @@ -532,4 +513,23 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow members of unions and intersections that do nothing or override type information', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + errorTypeOverrides: `'{{typeName}}' is an 'error' type that acts as 'any' and overrides all other types in this {{container}} type.`, + literalOverridden: `{{literal}} is overridden by {{primitive}} in this union type.`, + overridden: `'{{typeName}}' is overridden by other types in this {{container}} type.`, + overrides: `'{{typeName}}' overrides all other types in this {{container}} type.`, + primitiveOverridden: `{{primitive}} is overridden by the {{literal}} in this intersection type.`, + }, + schema: [], + type: 'suggestion', + }, + name: 'no-redundant-type-constituents', }); diff --git a/packages/eslint-plugin/src/rules/no-require-imports.ts b/packages/eslint-plugin/src/rules/no-require-imports.ts index 64f626076c32..69337e0d2cd4 100644 --- a/packages/eslint-plugin/src/rules/no-require-imports.ts +++ b/packages/eslint-plugin/src/rules/no-require-imports.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import * as util from '../util'; @@ -12,35 +13,6 @@ type Options = [ type MessageIds = 'noRequireImports'; export default util.createRule({ - name: 'no-require-imports', - meta: { - type: 'problem', - docs: { - description: 'Disallow invocation of `require()`', - recommended: 'recommended', - }, - schema: [ - { - type: 'object', - properties: { - allow: { - type: 'array', - items: { type: 'string' }, - description: 'Patterns of import paths to allow requiring from.', - }, - allowAsImport: { - type: 'boolean', - description: 'Allows `require` statements in import declarations.', - }, - }, - additionalProperties: false, - }, - ], - messages: { - noRequireImports: 'A `require()` style import is forbidden.', - }, - }, - defaultOptions: [{ allow: [], allowAsImport: false }], create(context, options) { const allowAsImport = options[0].allowAsImport; const allowPatterns = options[0].allow?.map( @@ -75,8 +47,8 @@ export default util.createRule({ // of the commonjs standard if (!variable?.identifiers.length) { context.report({ - node, messageId: 'noRequireImports', + node, }); } }, @@ -94,10 +66,39 @@ export default util.createRule({ return; } context.report({ - node, messageId: 'noRequireImports', + node, }); }, }; }, + defaultOptions: [{ allow: [], allowAsImport: false }], + meta: { + docs: { + description: 'Disallow invocation of `require()`', + recommended: 'recommended', + }, + messages: { + noRequireImports: 'A `require()` style import is forbidden.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allow: { + description: 'Patterns of import paths to allow requiring from.', + items: { type: 'string' }, + type: 'array', + }, + allowAsImport: { + description: 'Allows `require` statements in import declarations.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'no-require-imports', }); diff --git a/packages/eslint-plugin/src/rules/no-restricted-imports.ts b/packages/eslint-plugin/src/rules/no-restricted-imports.ts index f61baa7ed396..3245778679dc 100644 --- a/packages/eslint-plugin/src/rules/no-restricted-imports.ts +++ b/packages/eslint-plugin/src/rules/no-restricted-imports.ts @@ -1,5 +1,4 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { JSONSchema4AnyOfSchema, JSONSchema4ArraySchema, @@ -11,12 +10,15 @@ import type { RuleListener, } from 'eslint/lib/rules/no-restricted-imports'; import type { Ignore } from 'ignore'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import ignore from 'ignore'; import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -40,58 +42,56 @@ const baseSchema = baseRule.meta.schema as { anyOf: [ unknown, { - type: 'array'; items: [ { - type: 'object'; properties: { paths: { - type: 'array'; items: { anyOf: [ { type: 'string' }, { - type: 'object'; properties: JSONSchema4ObjectSchema['properties']; required: string[]; + type: 'object'; }, ]; }; + type: 'array'; }; patterns: { anyOf: [ - { type: 'array'; items: { type: 'string' } }, + { items: { type: 'string' }; type: 'array' }, { - type: 'array'; items: { - type: 'object'; properties: JSONSchema4ObjectSchema['properties']; required: string[]; + type: 'object'; }; + type: 'array'; }, ]; }; }; + type: 'object'; }, ]; + type: 'array'; }, ]; }; const allowTypeImportsOptionSchema: JSONSchema4ObjectSchema['properties'] = { allowTypeImports: { - type: 'boolean', description: 'Disallow value imports, but allow type-only imports.', + type: 'boolean', }, }; const arrayOfStringsOrObjects: JSONSchema4ArraySchema = { - type: 'array', items: { anyOf: [ { type: 'string' }, { - type: 'object', additionalProperties: false, properties: { ...tryAccess( @@ -108,25 +108,25 @@ const arrayOfStringsOrObjects: JSONSchema4ArraySchema = { .required, undefined, ), + type: 'object', }, ], }, + type: 'array', uniqueItems: true, }; const arrayOfStringsOrObjectPatterns: JSONSchema4AnyOfSchema = { anyOf: [ { - type: 'array', items: { type: 'string', }, + type: 'array', uniqueItems: true, }, { - type: 'array', items: { - type: 'object', additionalProperties: false, properties: { ...tryAccess( @@ -143,7 +143,9 @@ const arrayOfStringsOrObjectPatterns: JSONSchema4AnyOfSchema = { .required, [], ), + type: 'object', }, + type: 'array', uniqueItems: true, }, ], @@ -153,18 +155,18 @@ const schema: JSONSchema4AnyOfSchema = { anyOf: [ arrayOfStringsOrObjects, { - type: 'array', + additionalItems: false, items: [ { - type: 'object', + additionalProperties: false, properties: { paths: arrayOfStringsOrObjects, patterns: arrayOfStringsOrObjectPatterns, }, - additionalProperties: false, + type: 'object', }, ], - additionalItems: false, + type: 'array', }, ], }; @@ -228,18 +230,6 @@ function shouldCreateRule( } export default createRule({ - name: 'no-restricted-imports', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow specified modules when loaded by `import`', - extendsBaseRule: true, - }, - messages: baseRule.meta.messages, - fixable: baseRule.meta.fixable, - schema, - }, - defaultOptions: [], create(context) { const rules = baseRule.create(context); const { options } = context; @@ -308,6 +298,29 @@ export default createRule({ } return { + ExportAllDeclaration: rules.ExportAllDeclaration, + 'ExportNamedDeclaration[source]'( + node: { + source: NonNullable; + } & TSESTree.ExportNamedDeclaration, + ): void { + if ( + node.exportKind === 'type' || + (node.specifiers.length > 0 && + node.specifiers.every(specifier => specifier.exportKind === 'type')) + ) { + const importSource = node.source.value.trim(); + if ( + !isAllowedTypeImportPath(importSource) && + !isAllowedTypeImportPattern(importSource) + ) { + return rules.ExportNamedDeclaration(node); + } + } else { + return rules.ExportNamedDeclaration(node); + } + }, + ImportDeclaration: checkImportNode, TSImportEqualsDeclaration( node: TSESTree.TSImportEqualsDeclaration, ): void { @@ -316,46 +329,35 @@ export default createRule({ ) { const synthesizedImport: TSESTree.ImportDeclaration = { ...node, - type: AST_NODE_TYPES.ImportDeclaration, - source: node.moduleReference.expression, assertions: [], attributes: [], + source: node.moduleReference.expression, specifiers: [ { ...node.id, - type: AST_NODE_TYPES.ImportDefaultSpecifier, local: node.id, + type: AST_NODE_TYPES.ImportDefaultSpecifier, // @ts-expect-error -- parent types are incompatible but it's fine for the purposes of this extension parent: node.id.parent, }, ], + type: AST_NODE_TYPES.ImportDeclaration, }; return checkImportNode(synthesizedImport); } }, - ImportDeclaration: checkImportNode, - 'ExportNamedDeclaration[source]'( - node: TSESTree.ExportNamedDeclaration & { - source: NonNullable; - }, - ): void { - if ( - node.exportKind === 'type' || - (node.specifiers.length > 0 && - node.specifiers.every(specifier => specifier.exportKind === 'type')) - ) { - const importSource = node.source.value.trim(); - if ( - !isAllowedTypeImportPath(importSource) && - !isAllowedTypeImportPattern(importSource) - ) { - return rules.ExportNamedDeclaration(node); - } - } else { - return rules.ExportNamedDeclaration(node); - } - }, - ExportAllDeclaration: rules.ExportAllDeclaration, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow specified modules when loaded by `import`', + extendsBaseRule: true, + }, + fixable: baseRule.meta.fixable, + messages: baseRule.meta.messages, + schema, + type: 'suggestion', + }, + name: 'no-restricted-imports', }); diff --git a/packages/eslint-plugin/src/rules/no-restricted-types.ts b/packages/eslint-plugin/src/rules/no-restricted-types.ts index c13f2838d85d..87ee382ee28c 100644 --- a/packages/eslint-plugin/src/rules/no-restricted-types.ts +++ b/packages/eslint-plugin/src/rules/no-restricted-types.ts @@ -1,24 +1,25 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, objectReduceKey } from '../util'; type Types = Record< string, - | boolean - | string | { - message: string; fixWith?: string; + message: string; suggest?: readonly string[]; } + | boolean + | string | null >; export type Options = [ { - types?: Types; extendDefaults?: boolean; + types?: Types; }, ]; @@ -36,7 +37,7 @@ function stringifyNode( } function getCustomMessage( - bannedType: string | true | { message?: string; fixWith?: string } | null, + bannedType: { fixWith?: string; message?: string } | true | string | null, ): string { if (!bannedType || bannedType === true) { return ''; @@ -68,70 +69,6 @@ const TYPE_KEYWORDS = { }; export default createRule({ - name: 'no-restricted-types', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow certain types', - }, - fixable: 'code', - hasSuggestions: true, - messages: { - bannedTypeMessage: "Don't use `{{name}}` as a type.{{customMessage}}", - bannedTypeReplacement: 'Replace `{{name}}` with `{{replacement}}`.', - }, - schema: [ - { - $defs: { - banConfig: { - oneOf: [ - { - type: 'boolean', - enum: [true], - description: 'Bans the type with the default message', - }, - { - type: 'string', - description: 'Bans the type with a custom message', - }, - { - type: 'object', - description: 'Bans a type', - properties: { - message: { - type: 'string', - description: 'Custom error message', - }, - fixWith: { - type: 'string', - description: - 'Type to autofix replace with. Note that autofixers can be applied automatically - so you need to be careful with this option.', - }, - suggest: { - type: 'array', - items: { type: 'string' }, - description: 'Types to suggest replacing with.', - }, - }, - additionalProperties: false, - }, - ], - }, - }, - type: 'object', - properties: { - types: { - type: 'object', - additionalProperties: { - $ref: '#/items/0/$defs/banConfig', - }, - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [{}], create(context, [{ types = {} }]) { const bannedTypes = new Map( Object.entries(types).map(([type, data]) => [removeSpaces(type), data]), @@ -156,23 +93,23 @@ export default createRule({ : undefined; context.report({ - node: typeNode, - messageId: 'bannedTypeMessage', data: { - name, customMessage, + name, }, fix: fixWith ? (fixer): TSESLint.RuleFix => fixer.replaceText(typeNode, fixWith) : null, + messageId: 'bannedTypeMessage', + node: typeNode, suggest: suggest?.map(replacement => ({ - messageId: 'bannedTypeReplacement', data: { name, replacement, }, fix: (fixer): TSESLint.RuleFix => fixer.replaceText(typeNode, replacement), + messageId: 'bannedTypeReplacement', })), }); } @@ -218,4 +155,68 @@ export default createRule({ }, }; }, + defaultOptions: [{}], + meta: { + docs: { + description: 'Disallow certain types', + }, + fixable: 'code', + hasSuggestions: true, + messages: { + bannedTypeMessage: "Don't use `{{name}}` as a type.{{customMessage}}", + bannedTypeReplacement: 'Replace `{{name}}` with `{{replacement}}`.', + }, + schema: [ + { + $defs: { + banConfig: { + oneOf: [ + { + description: 'Bans the type with the default message', + enum: [true], + type: 'boolean', + }, + { + description: 'Bans the type with a custom message', + type: 'string', + }, + { + additionalProperties: false, + description: 'Bans a type', + properties: { + fixWith: { + description: + 'Type to autofix replace with. Note that autofixers can be applied automatically - so you need to be careful with this option.', + type: 'string', + }, + message: { + description: 'Custom error message', + type: 'string', + }, + suggest: { + description: 'Types to suggest replacing with.', + items: { type: 'string' }, + type: 'array', + }, + }, + type: 'object', + }, + ], + }, + }, + additionalProperties: false, + properties: { + types: { + additionalProperties: { + $ref: '#/items/0/$defs/banConfig', + }, + type: 'object', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-restricted-types', }); diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts index 152d536239e8..049b656f1c6f 100644 --- a/packages/eslint-plugin/src/rules/no-shadow.ts +++ b/packages/eslint-plugin/src/rules/no-shadow.ts @@ -1,5 +1,6 @@ -import { DefinitionType, ScopeType } from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +import { DefinitionType, ScopeType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -11,9 +12,9 @@ type Options = [ allow?: string[]; builtinGlobals?: boolean; hoist?: 'all' | 'functions' | 'never'; + ignoreFunctionTypeParameterNameValueShadow?: boolean; ignoreOnInitialization?: boolean; ignoreTypeValueShadow?: boolean; - ignoreFunctionTypeParameterNameValueShadow?: boolean; }, ]; @@ -24,60 +25,6 @@ const allowedFunctionVariableDefTypes = new Set([ ]); export default createRule({ - name: 'no-shadow', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow variable declarations from shadowing variables declared in the outer scope', - extendsBaseRule: true, - }, - schema: [ - { - type: 'object', - properties: { - builtinGlobals: { - type: 'boolean', - }, - hoist: { - type: 'string', - enum: ['all', 'functions', 'never'], - }, - allow: { - type: 'array', - items: { - type: 'string', - }, - }, - ignoreOnInitialization: { - type: 'boolean', - }, - ignoreTypeValueShadow: { - type: 'boolean', - }, - ignoreFunctionTypeParameterNameValueShadow: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - messages: { - noShadow: - "'{{name}}' is already declared in the upper scope on line {{shadowedLine}} column {{shadowedColumn}}.", - noShadowGlobal: "'{{name}}' is already a global variable.", - }, - }, - defaultOptions: [ - { - allow: [], - builtinGlobals: false, - hoist: 'functions', - ignoreOnInitialization: false, - ignoreTypeValueShadow: true, - ignoreFunctionTypeParameterNameValueShadow: true, - }, - ], create(context, [options]) { /** * Check if a scope is a TypeScript module augmenting the global namespace. @@ -420,14 +367,14 @@ export default createRule({ } } else if ( [ - AST_NODE_TYPES.FunctionDeclaration, - AST_NODE_TYPES.ClassDeclaration, - AST_NODE_TYPES.FunctionExpression, - AST_NODE_TYPES.ClassExpression, AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.CatchClause, - AST_NODE_TYPES.ImportDeclaration, + AST_NODE_TYPES.ClassDeclaration, + AST_NODE_TYPES.ClassExpression, AST_NODE_TYPES.ExportNamedDeclaration, + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.ImportDeclaration, ].includes(node.type) ) { break; @@ -515,13 +462,13 @@ export default createRule({ */ function getDeclaredLocation( variable: TSESLint.Scope.Variable, - ): { global: false; line: number; column: number } | { global: true } { + ): { column: number; global: false; line: number } | { global: true } { const identifier = variable.identifiers.at(0); if (identifier) { return { + column: identifier.loc.start.column + 1, global: false, line: identifier.loc.start.line, - column: identifier.loc.start.column + 1, }; } return { @@ -613,18 +560,18 @@ export default createRule({ node: variable.identifiers[0], ...(location.global ? { - messageId: 'noShadowGlobal', data: { name: variable.name, }, + messageId: 'noShadowGlobal', } : { - messageId: 'noShadow', data: { name: variable.name, - shadowedLine: location.line, shadowedColumn: location.column, + shadowedLine: location.line, }, + messageId: 'noShadow', }), }); } @@ -646,4 +593,58 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allow: [], + builtinGlobals: false, + hoist: 'functions', + ignoreFunctionTypeParameterNameValueShadow: true, + ignoreOnInitialization: false, + ignoreTypeValueShadow: true, + }, + ], + meta: { + docs: { + description: + 'Disallow variable declarations from shadowing variables declared in the outer scope', + extendsBaseRule: true, + }, + messages: { + noShadow: + "'{{name}}' is already declared in the upper scope on line {{shadowedLine}} column {{shadowedColumn}}.", + noShadowGlobal: "'{{name}}' is already a global variable.", + }, + schema: [ + { + additionalProperties: false, + properties: { + allow: { + items: { + type: 'string', + }, + type: 'array', + }, + builtinGlobals: { + type: 'boolean', + }, + hoist: { + enum: ['all', 'functions', 'never'], + type: 'string', + }, + ignoreFunctionTypeParameterNameValueShadow: { + type: 'boolean', + }, + ignoreOnInitialization: { + type: 'boolean', + }, + ignoreTypeValueShadow: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-shadow', }); diff --git a/packages/eslint-plugin/src/rules/no-this-alias.ts b/packages/eslint-plugin/src/rules/no-this-alias.ts index 98006e56328d..7bee02498dc0 100644 --- a/packages/eslint-plugin/src/rules/no-this-alias.ts +++ b/packages/eslint-plugin/src/rules/no-this-alias.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -12,46 +13,6 @@ type Options = [ type MessageIds = 'thisAssignment' | 'thisDestructure'; export default createRule({ - name: 'no-this-alias', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow aliasing `this`', - recommended: 'recommended', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - allowDestructuring: { - description: - 'Whether to ignore destructurings, such as `const { props, state } = this`.', - type: 'boolean', - }, - allowedNames: { - description: - 'Names to ignore, such as ["self"] for `const self = this;`.', - type: 'array', - items: { - type: 'string', - }, - }, - }, - }, - ], - messages: { - thisAssignment: "Unexpected aliasing of 'this' to local variable.", - thisDestructure: - "Unexpected aliasing of members of 'this' to local variables.", - }, - }, - defaultOptions: [ - { - allowDestructuring: true, - allowedNames: [], - }, - ], create(context, [{ allowDestructuring, allowedNames }]) { return { "VariableDeclarator[init.type='ThisExpression'], AssignmentExpression[right.type='ThisExpression']"( @@ -71,14 +32,54 @@ export default createRule({ : false; if (!hasAllowedName) { context.report({ - node: id, messageId: id.type === AST_NODE_TYPES.Identifier ? 'thisAssignment' : 'thisDestructure', + node: id, }); } }, }; }, + defaultOptions: [ + { + allowDestructuring: true, + allowedNames: [], + }, + ], + meta: { + docs: { + description: 'Disallow aliasing `this`', + recommended: 'recommended', + }, + messages: { + thisAssignment: "Unexpected aliasing of 'this' to local variable.", + thisDestructure: + "Unexpected aliasing of members of 'this' to local variables.", + }, + schema: [ + { + additionalProperties: false, + properties: { + allowDestructuring: { + description: + 'Whether to ignore destructurings, such as `const { props, state } = this`.', + type: 'boolean', + }, + allowedNames: { + description: + 'Names to ignore, such as ["self"] for `const self = this;`.', + items: { + type: 'string', + }, + type: 'array', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-this-alias', }); diff --git a/packages/eslint-plugin/src/rules/no-type-alias.ts b/packages/eslint-plugin/src/rules/no-type-alias.ts index 53e64a3b5c69..adf47dad6248 100644 --- a/packages/eslint-plugin/src/rules/no-type-alias.ts +++ b/packages/eslint-plugin/src/rules/no-type-alias.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -6,8 +7,8 @@ import { createRule } from '../util'; type Values = | 'always' | 'in-intersections' - | 'in-unions-and-intersections' | 'in-unions' + | 'in-unions-and-intersections' | 'never'; type Options = [ @@ -16,10 +17,10 @@ type Options = [ allowCallbacks?: 'always' | 'never'; allowConditionalTypes?: 'always' | 'never'; allowConstructors?: 'always' | 'never'; + allowGenerics?: 'always' | 'never'; allowLiterals?: Values; allowMappedTypes?: Values; allowTupleTypes?: Values; - allowGenerics?: 'always' | 'never'; }, ]; type MessageIds = 'noCompositionAlias' | 'noTypeAlias'; @@ -28,94 +29,11 @@ type CompositionType = | AST_NODE_TYPES.TSIntersectionType | AST_NODE_TYPES.TSUnionType; interface TypeWithLabel { - node: TSESTree.Node; compositionType: CompositionType | null; + node: TSESTree.Node; } export default createRule({ - name: 'no-type-alias', - meta: { - deprecated: true, - type: 'suggestion', - docs: { - description: 'Disallow type aliases', - // too opinionated to be recommended - }, - messages: { - noTypeAlias: 'Type {{alias}} are not allowed.', - noCompositionAlias: - '{{typeName}} in {{compositionType}} types are not allowed.', - }, - schema: [ - { - $defs: { - expandedOptions: { - type: 'string', - enum: [ - 'always', - 'never', - 'in-unions', - 'in-intersections', - 'in-unions-and-intersections', - ] satisfies Values[], - }, - simpleOptions: { - type: 'string', - enum: ['always', 'never'], - }, - }, - type: 'object', - properties: { - allowAliases: { - description: 'Whether to allow direct one-to-one type aliases.', - $ref: '#/items/0/$defs/expandedOptions', - }, - allowCallbacks: { - description: 'Whether to allow type aliases for callbacks.', - $ref: '#/items/0/$defs/simpleOptions', - }, - allowConditionalTypes: { - description: 'Whether to allow type aliases for conditional types.', - $ref: '#/items/0/$defs/simpleOptions', - }, - allowConstructors: { - description: 'Whether to allow type aliases with constructors.', - $ref: '#/items/0/$defs/simpleOptions', - }, - allowLiterals: { - description: - 'Whether to allow type aliases with object literal types.', - $ref: '#/items/0/$defs/expandedOptions', - }, - allowMappedTypes: { - description: 'Whether to allow type aliases with mapped types.', - $ref: '#/items/0/$defs/expandedOptions', - }, - allowTupleTypes: { - description: 'Whether to allow type aliases with tuple types.', - $ref: '#/items/0/$defs/expandedOptions', - }, - allowGenerics: { - description: 'Whether to allow type aliases with generic types.', - $ref: '#/items/0/$defs/simpleOptions', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - allowAliases: 'never', - allowCallbacks: 'never', - allowConditionalTypes: 'never', - allowConstructors: 'never', - allowLiterals: 'never', - allowMappedTypes: 'never', - allowTupleTypes: 'never', - allowGenerics: 'never', - }, - ], create( context, [ @@ -124,10 +42,10 @@ export default createRule({ allowCallbacks, allowConditionalTypes, allowConstructors, + allowGenerics, allowLiterals, allowMappedTypes, allowTupleTypes, - allowGenerics, }, ], ) { @@ -189,17 +107,15 @@ export default createRule({ ): void { if (isRoot) { return context.report({ - node, - messageId: 'noTypeAlias', data: { alias: type.toLowerCase(), }, + messageId: 'noTypeAlias', + node, }); } return context.report({ - node, - messageId: 'noCompositionAlias', data: { compositionType: compositionType === AST_NODE_TYPES.TSUnionType @@ -207,6 +123,8 @@ export default createRule({ : 'intersection', typeName: type, }, + messageId: 'noCompositionAlias', + node, }); } @@ -330,7 +248,7 @@ export default createRule({ return acc; }, []); } - return [{ node, compositionType }]; + return [{ compositionType, node }]; } return { @@ -348,4 +266,87 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowAliases: 'never', + allowCallbacks: 'never', + allowConditionalTypes: 'never', + allowConstructors: 'never', + allowGenerics: 'never', + allowLiterals: 'never', + allowMappedTypes: 'never', + allowTupleTypes: 'never', + }, + ], + meta: { + deprecated: true, + docs: { + description: 'Disallow type aliases', + // too opinionated to be recommended + }, + messages: { + noCompositionAlias: + '{{typeName}} in {{compositionType}} types are not allowed.', + noTypeAlias: 'Type {{alias}} are not allowed.', + }, + schema: [ + { + $defs: { + expandedOptions: { + enum: [ + 'always', + 'never', + 'in-unions', + 'in-intersections', + 'in-unions-and-intersections', + ] satisfies Values[], + type: 'string', + }, + simpleOptions: { + enum: ['always', 'never'], + type: 'string', + }, + }, + additionalProperties: false, + properties: { + allowAliases: { + $ref: '#/items/0/$defs/expandedOptions', + description: 'Whether to allow direct one-to-one type aliases.', + }, + allowCallbacks: { + $ref: '#/items/0/$defs/simpleOptions', + description: 'Whether to allow type aliases for callbacks.', + }, + allowConditionalTypes: { + $ref: '#/items/0/$defs/simpleOptions', + description: 'Whether to allow type aliases for conditional types.', + }, + allowConstructors: { + $ref: '#/items/0/$defs/simpleOptions', + description: 'Whether to allow type aliases with constructors.', + }, + allowGenerics: { + $ref: '#/items/0/$defs/simpleOptions', + description: 'Whether to allow type aliases with generic types.', + }, + allowLiterals: { + $ref: '#/items/0/$defs/expandedOptions', + description: + 'Whether to allow type aliases with object literal types.', + }, + allowMappedTypes: { + $ref: '#/items/0/$defs/expandedOptions', + description: 'Whether to allow type aliases with mapped types.', + }, + allowTupleTypes: { + $ref: '#/items/0/$defs/expandedOptions', + description: 'Whether to allow type aliases with tuple types.', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-type-alias', }); diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts index 17732e926868..4d9df9513e96 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -14,8 +15,8 @@ type MessageIds = type Options = [ { - allowComparingNullableBooleansToTrue?: boolean; allowComparingNullableBooleansToFalse?: boolean; + allowComparingNullableBooleansToTrue?: boolean; }, ]; @@ -30,53 +31,6 @@ interface BooleanComparisonWithTypeInformation extends BooleanComparison { } export default createRule({ - name: 'no-unnecessary-boolean-literal-compare', - meta: { - docs: { - description: - 'Disallow unnecessary equality comparisons against boolean literals', - recommended: 'strict', - requiresTypeChecking: true, - }, - fixable: 'code', - messages: { - direct: - 'This expression unnecessarily compares a boolean value to a boolean instead of using it directly.', - negated: - 'This expression unnecessarily compares a boolean value to a boolean instead of negating it.', - comparingNullableToTrueDirect: - 'This expression unnecessarily compares a nullable boolean value to true instead of using it directly.', - comparingNullableToTrueNegated: - 'This expression unnecessarily compares a nullable boolean value to true instead of negating it.', - comparingNullableToFalse: - 'This expression unnecessarily compares a nullable boolean value to false instead of using the ?? operator to provide a default.', - }, - schema: [ - { - type: 'object', - properties: { - allowComparingNullableBooleansToTrue: { - description: - 'Whether to allow comparisons between nullable boolean variables and `true`.', - type: 'boolean', - }, - allowComparingNullableBooleansToFalse: { - description: - 'Whether to allow comparisons between nullable boolean variables and `false`.', - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - type: 'suggestion', - }, - defaultOptions: [ - { - allowComparingNullableBooleansToTrue: true, - allowComparingNullableBooleansToFalse: true, - }, - ], create(context, [options]) { const services = getParserServices(context); @@ -176,8 +130,8 @@ export default createRule({ const negated = !comparisonType.isPositive; return { - literalBooleanInComparison, expression, + literalBooleanInComparison, negated, }; } @@ -268,6 +222,53 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowComparingNullableBooleansToFalse: true, + allowComparingNullableBooleansToTrue: true, + }, + ], + meta: { + docs: { + description: + 'Disallow unnecessary equality comparisons against boolean literals', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + comparingNullableToFalse: + 'This expression unnecessarily compares a nullable boolean value to false instead of using the ?? operator to provide a default.', + comparingNullableToTrueDirect: + 'This expression unnecessarily compares a nullable boolean value to true instead of using it directly.', + comparingNullableToTrueNegated: + 'This expression unnecessarily compares a nullable boolean value to true instead of negating it.', + direct: + 'This expression unnecessarily compares a boolean value to a boolean instead of using it directly.', + negated: + 'This expression unnecessarily compares a boolean value to a boolean instead of negating it.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowComparingNullableBooleansToFalse: { + description: + 'Whether to allow comparisons between nullable boolean variables and `false`.', + type: 'boolean', + }, + allowComparingNullableBooleansToTrue: { + description: + 'Whether to allow comparisons between nullable boolean variables and `true`.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-unnecessary-boolean-literal-compare', }); interface EqualsKind { @@ -277,27 +278,27 @@ interface EqualsKind { function getEqualsKind(operator: string): EqualsKind | undefined { switch (operator) { - case '==': + case '!=': return { - isPositive: true, + isPositive: false, isStrict: false, }; - case '===': + case '!==': return { - isPositive: true, + isPositive: false, isStrict: true, }; - case '!=': + case '==': return { - isPositive: false, + isPositive: true, isStrict: false, }; - case '!==': + case '===': return { - isPositive: false, + isPositive: true, isStrict: true, }; diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index e3bf94a30c78..4e8fea5ee6af 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -87,61 +88,6 @@ export type MessageId = | 'noStrictNullCheck'; export default createRule({ - name: 'no-unnecessary-condition', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow conditionals where the type is always truthy or always falsy', - recommended: 'strict', - requiresTypeChecking: true, - }, - schema: [ - { - type: 'object', - properties: { - allowConstantLoopConditions: { - description: - 'Whether to ignore constant loop conditions, such as `while (true)`.', - type: 'boolean', - }, - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { - description: - 'Whether to not error when running with a tsconfig that has strictNullChecks turned.', - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - fixable: 'code', - messages: { - alwaysTruthy: 'Unnecessary conditional, value is always truthy.', - alwaysFalsy: 'Unnecessary conditional, value is always falsy.', - alwaysTruthyFunc: - 'This callback should return a conditional, but return is always truthy.', - alwaysFalsyFunc: - 'This callback should return a conditional, but return is always falsy.', - neverNullish: - 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.', - alwaysNullish: - 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`.', - literalBooleanExpression: - 'Unnecessary conditional, both sides of the expression are literal values.', - noOverlapBooleanExpression: - 'Unnecessary conditional, the types have no overlap.', - never: 'Unnecessary conditional, value is `never`.', - neverOptionalChain: 'Unnecessary optional chain on a non-nullish value.', - noStrictNullCheck: - 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', - }, - }, - defaultOptions: [ - { - allowConstantLoopConditions: false, - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, - }, - ], create( context, [ @@ -166,8 +112,8 @@ export default createRule({ ) { context.report({ loc: { - start: { line: 0, column: 0 }, - end: { line: 0, column: 0 }, + end: { column: 0, line: 0 }, + start: { column: 0, line: 0 }, }, messageId: 'noStrictNullCheck', }); @@ -291,7 +237,7 @@ export default createRule({ } if (messageId) { - context.report({ node, messageId }); + context.report({ messageId, node }); } } @@ -339,7 +285,7 @@ export default createRule({ } if (messageId) { - context.report({ node, messageId }); + context.report({ messageId, node }); } } @@ -372,7 +318,7 @@ export default createRule({ const leftType = getConstrainedTypeAtLocation(services, node.left); const rightType = getConstrainedTypeAtLocation(services, node.right); if (isLiteral(leftType) && isLiteral(rightType)) { - context.report({ node, messageId: 'literalBooleanExpression' }); + context.report({ messageId: 'literalBooleanExpression', node }); return; } // Workaround for https://github.com/microsoft/TypeScript/issues/37160 @@ -404,7 +350,7 @@ export default createRule({ (leftType.flags === NULL && !isComparable(rightType, NULL)) || (rightType.flags === NULL && !isComparable(leftType, NULL)) ) { - context.report({ node, messageId: 'noOverlapBooleanExpression' }); + context.report({ messageId: 'noOverlapBooleanExpression', node }); return; } } @@ -517,14 +463,14 @@ export default createRule({ } if (!returnTypes.some(isPossiblyFalsy)) { return context.report({ - node: callback, messageId: 'alwaysTruthyFunc', + node: callback, }); } if (!returnTypes.some(isPossiblyTruthy)) { return context.report({ - node: callback, messageId: 'alwaysFalsyFunc', + node: callback, }); } } @@ -654,7 +600,7 @@ export default createRule({ function checkOptionalChain( node: TSESTree.CallExpression | TSESTree.MemberExpression, beforeOperator: TSESTree.Node, - fix: '.' | '', + fix: '' | '.', ): void { // We only care if this step in the chain is optional. If just descend // from an optional chain, then that's fine. @@ -686,12 +632,12 @@ export default createRule({ ); context.report({ - node, - loc: questionDotOperator.loc, - messageId: 'neverOptionalChain', fix(fixer) { return fixer.replaceText(questionDotOperator, fix); }, + loc: questionDotOperator.loc, + messageId: 'neverOptionalChain', + node, }); } @@ -710,7 +656,7 @@ export default createRule({ ): void { // Similar to checkLogicalExpressionForUnnecessaryConditionals, since // a ||= b is equivalent to a || (a = b) - if (['||=', '&&='].includes(node.operator)) { + if (['&&=', '||='].includes(node.operator)) { checkNode(node.left); } else if (node.operator === '??=') { checkNodeForNullish(node.left); @@ -721,14 +667,69 @@ export default createRule({ AssignmentExpression: checkAssignmentExpression, BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional, CallExpression: checkCallExpression, + 'CallExpression[optional = true]': checkOptionalCallExpression, ConditionalExpression: (node): void => checkNode(node.test), DoWhileStatement: checkIfLoopIsNecessaryConditional, ForStatement: checkIfLoopIsNecessaryConditional, IfStatement: (node): void => checkNode(node.test), LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals, - WhileStatement: checkIfLoopIsNecessaryConditional, 'MemberExpression[optional = true]': checkOptionalMemberExpression, - 'CallExpression[optional = true]': checkOptionalCallExpression, + WhileStatement: checkIfLoopIsNecessaryConditional, }; }, + defaultOptions: [ + { + allowConstantLoopConditions: false, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + }, + ], + meta: { + docs: { + description: + 'Disallow conditionals where the type is always truthy or always falsy', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + alwaysFalsy: 'Unnecessary conditional, value is always falsy.', + alwaysFalsyFunc: + 'This callback should return a conditional, but return is always falsy.', + alwaysNullish: + 'Unnecessary conditional, left-hand side of `??` operator is always `null` or `undefined`.', + alwaysTruthy: 'Unnecessary conditional, value is always truthy.', + alwaysTruthyFunc: + 'This callback should return a conditional, but return is always truthy.', + literalBooleanExpression: + 'Unnecessary conditional, both sides of the expression are literal values.', + never: 'Unnecessary conditional, value is `never`.', + neverNullish: + 'Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined.', + neverOptionalChain: 'Unnecessary optional chain on a non-nullish value.', + noOverlapBooleanExpression: + 'Unnecessary conditional, the types have no overlap.', + noStrictNullCheck: + 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowConstantLoopConditions: { + description: + 'Whether to ignore constant loop conditions, such as `while (true)`.', + type: 'boolean', + }, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { + description: + 'Whether to not error when running with a tsconfig that has strictNullChecks turned.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'no-unnecessary-condition', }); diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-parameter-property-assignment.ts b/packages/eslint-plugin/src/rules/no-unnecessary-parameter-property-assignment.ts index 4e589a93af5c..7a5751430309 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-parameter-property-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-parameter-property-assignment.ts @@ -1,5 +1,6 @@ -import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; + +import { DefinitionType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { createRule, getStaticStringValue, nullThrows } from '../util'; @@ -7,24 +8,10 @@ import { createRule, getStaticStringValue, nullThrows } from '../util'; const UNNECESSARY_OPERATORS = new Set(['=', '&&=', '||=', '??=']); export default createRule({ - name: 'no-unnecessary-parameter-property-assignment', - meta: { - docs: { - description: - 'Disallow unnecessary assignment of constructor property parameter', - }, - messages: { - unnecessaryAssign: - 'This assignment is unnecessary since it is already assigned by a parameter property.', - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { const reportInfoStack: { - assignedBeforeUnnecessary: Set; assignedBeforeConstructor: Set; + assignedBeforeUnnecessary: Set; unnecessaryAssignments: { name: string; node: TSESTree.AssignmentExpression; @@ -57,9 +44,9 @@ export default createRule({ function findParentFunction( node: TSESTree.Node | undefined, ): - | TSESTree.FunctionExpression - | TSESTree.FunctionDeclaration | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression | undefined { if ( !node || @@ -136,51 +123,24 @@ export default createRule({ return { ClassBody(): void { reportInfoStack.push({ - unnecessaryAssignments: [], - assignedBeforeUnnecessary: new Set(), assignedBeforeConstructor: new Set(), + assignedBeforeUnnecessary: new Set(), + unnecessaryAssignments: [], }); }, 'ClassBody:exit'(): void { - const { unnecessaryAssignments, assignedBeforeConstructor } = + const { assignedBeforeConstructor, unnecessaryAssignments } = nullThrows(reportInfoStack.pop(), 'The top stack should exist'); unnecessaryAssignments.forEach(({ name, node }) => { if (assignedBeforeConstructor.has(name)) { return; } context.report({ - node, messageId: 'unnecessaryAssign', + node, }); }); }, - 'PropertyDefinition AssignmentExpression'( - node: TSESTree.AssignmentExpression, - ): void { - const name = getPropertyName(node.left); - - if (!name) { - return; - } - - const functionNode = findParentFunction(node); - if (functionNode) { - if ( - !( - isArrowIIFE(functionNode) && - findParentPropertyDefinition(node)?.value === functionNode.parent - ) - ) { - return; - } - } - - const { assignedBeforeConstructor } = nullThrows( - reportInfoStack.at(-1), - 'The top stack should exist', - ); - assignedBeforeConstructor.add(name); - }, "MethodDefinition[kind='constructor'] > FunctionExpression AssignmentExpression"( node: TSESTree.AssignmentExpression, ): void { @@ -227,6 +187,47 @@ export default createRule({ }); } }, + 'PropertyDefinition AssignmentExpression'( + node: TSESTree.AssignmentExpression, + ): void { + const name = getPropertyName(node.left); + + if (!name) { + return; + } + + const functionNode = findParentFunction(node); + if (functionNode) { + if ( + !( + isArrowIIFE(functionNode) && + findParentPropertyDefinition(node)?.value === functionNode.parent + ) + ) { + return; + } + } + + const { assignedBeforeConstructor } = nullThrows( + reportInfoStack.at(-1), + 'The top stack should exist', + ); + assignedBeforeConstructor.add(name); + }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow unnecessary assignment of constructor property parameter', + }, + messages: { + unnecessaryAssign: + 'This assignment is unnecessary since it is already assigned by a parameter property.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-unnecessary-parameter-property-assignment', }); diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts b/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts index 434df4f56a32..aebd62131a9f 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -6,21 +7,6 @@ import * as ts from 'typescript'; import { createRule, getParserServices } from '../util'; export default createRule({ - name: 'no-unnecessary-qualifier', - meta: { - docs: { - description: 'Disallow unnecessary namespace qualifiers', - requiresTypeChecking: true, - }, - fixable: 'code', - messages: { - unnecessaryQualifier: - "Qualifier is unnecessary since '{{ name }}' is in scope.", - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { const namespacesInScope: ts.Node[] = []; let currentFailedNamespaceExpression: TSESTree.Node | null = null; @@ -109,14 +95,14 @@ export default createRule({ ) { currentFailedNamespaceExpression = node; context.report({ - node: qualifier, - messageId: 'unnecessaryQualifier', data: { name: context.sourceCode.getText(name), }, fix(fixer) { return fixer.removeRange([qualifier.range[0], name.range[0]]); }, + messageId: 'unnecessaryQualifier', + node: qualifier, }); } } @@ -157,25 +143,15 @@ export default createRule({ } return { - 'TSModuleDeclaration > TSModuleBlock'( - node: TSESTree.TSModuleBlock, - ): void { - enterDeclaration(node.parent); - }, - TSEnumDeclaration: enterDeclaration, - 'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"]': - enterDeclaration, 'ExportNamedDeclaration[declaration.type="TSEnumDeclaration"]': enterDeclaration, - 'TSModuleDeclaration:exit': exitDeclaration, - 'TSEnumDeclaration:exit': exitDeclaration, - 'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"]:exit': - exitDeclaration, 'ExportNamedDeclaration[declaration.type="TSEnumDeclaration"]:exit': exitDeclaration, - TSQualifiedName(node: TSESTree.TSQualifiedName): void { - visitNamespaceAccess(node, node.left, node.right); - }, + 'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"]': + enterDeclaration, + 'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"]:exit': + exitDeclaration, + 'MemberExpression:exit': resetCurrentNamespaceExpression, 'MemberExpression[computed=false]'( node: TSESTree.MemberExpression, ): void { @@ -184,8 +160,33 @@ export default createRule({ visitNamespaceAccess(node, node.object, property); } }, + TSEnumDeclaration: enterDeclaration, + 'TSEnumDeclaration:exit': exitDeclaration, + 'TSModuleDeclaration > TSModuleBlock'( + node: TSESTree.TSModuleBlock, + ): void { + enterDeclaration(node.parent); + }, + 'TSModuleDeclaration:exit': exitDeclaration, + TSQualifiedName(node: TSESTree.TSQualifiedName): void { + visitNamespaceAccess(node, node.left, node.right); + }, 'TSQualifiedName:exit': resetCurrentNamespaceExpression, - 'MemberExpression:exit': resetCurrentNamespaceExpression, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow unnecessary namespace qualifiers', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + unnecessaryQualifier: + "Qualifier is unnecessary since '{{ name }}' is in scope.", + }, + schema: [], + type: 'suggestion', + }, + name: 'no-unnecessary-qualifier', }); diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts index dc15a9bbcd06..0698a772a372 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -25,28 +26,12 @@ function endsWithUnescapedDollarSign(str: string): boolean { } export default createRule<[], MessageId>({ - name: 'no-unnecessary-template-expression', - meta: { - fixable: 'code', - type: 'suggestion', - docs: { - description: 'Disallow unnecessary template expressions', - recommended: 'strict', - requiresTypeChecking: true, - }, - messages: { - noUnnecessaryTemplateExpression: - 'Template literal expression is unnecessary and can be simplified.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const services = getParserServices(context); function isUnderlyingTypeString( expression: TSESTree.Expression, - ): expression is TSESTree.StringLiteral | TSESTree.Identifier { + ): expression is TSESTree.Identifier | TSESTree.StringLiteral { const type = getConstrainedTypeAtLocation(services, expression); const isString = (t: ts.Type): boolean => { @@ -105,17 +90,17 @@ export default createRule<[], MessageId>({ if (hasSingleStringVariable) { context.report({ - node: node.expressions[0], - messageId: 'noUnnecessaryTemplateExpression', fix(fixer): TSESLint.RuleFix | null { const wrappingCode = getMovedNodeCode({ - sourceCode: context.sourceCode, - nodeToMove: node.expressions[0], destinationNode: node, + nodeToMove: node.expressions[0], + sourceCode: context.sourceCode, }); return fixer.replaceText(node, wrappingCode); }, + messageId: 'noUnnecessaryTemplateExpression', + node: node.expressions[0], }); return; @@ -246,8 +231,6 @@ export default createRule<[], MessageId>({ } context.report({ - node: expression, - messageId: 'noUnnecessaryTemplateExpression', fix(fixer): TSESLint.RuleFix[] { return [ // Remove the quasis' parts that are related to the current expression. @@ -263,9 +246,27 @@ export default createRule<[], MessageId>({ ...fixers.flatMap(cb => cb(fixer)), ]; }, + messageId: 'noUnnecessaryTemplateExpression', + node: expression, }); } }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow unnecessary template expressions', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + noUnnecessaryTemplateExpression: + 'Template literal expression is unnecessary and can be simplified.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-unnecessary-template-expression', }); diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts index d482ef1b672a..cb50145babfb 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -23,22 +24,6 @@ type ParameterCapableTSNode = type MessageIds = 'unnecessaryTypeParameter'; export default createRule<[], MessageIds>({ - name: 'no-unnecessary-type-arguments', - meta: { - docs: { - description: 'Disallow type arguments that are equal to the default', - recommended: 'strict', - requiresTypeChecking: true, - }, - fixable: 'code', - messages: { - unnecessaryTypeParameter: - 'This is the default value for this type parameter, so it can be omitted.', - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -96,14 +81,14 @@ export default createRule<[], MessageIds>({ } context.report({ - node: arg, - messageId: 'unnecessaryTypeParameter', fix: fixer => fixer.removeRange( i === 0 ? esParameters.range : [esParameters.params[i - 1].range[1], arg.range[1]], ), + messageId: 'unnecessaryTypeParameter', + node: arg, }); } @@ -118,6 +103,22 @@ export default createRule<[], MessageIds>({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow type arguments that are equal to the default', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + unnecessaryTypeParameter: + 'This is the default value for this type parameter, so it can be omitted.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-unnecessary-type-arguments', }); function getTypeParametersFromNode( diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts index 6b69d9e381d4..6a91baf9336b 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts @@ -1,5 +1,6 @@ import type { Scope } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -25,39 +26,6 @@ type Options = [ type MessageIds = 'contextuallyUnnecessary' | 'unnecessaryAssertion'; export default createRule({ - name: 'no-unnecessary-type-assertion', - meta: { - docs: { - description: - 'Disallow type assertions that do not change the type of an expression', - recommended: 'recommended', - requiresTypeChecking: true, - }, - fixable: 'code', - messages: { - unnecessaryAssertion: - 'This assertion is unnecessary since it does not change the type of the expression.', - contextuallyUnnecessary: - 'This assertion is unnecessary since the receiver accepts the original type of the expression.', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - typesToIgnore: { - description: 'A list of type names to ignore.', - type: 'array', - items: { - type: 'string', - }, - }, - }, - }, - ], - type: 'suggestion', - }, - defaultOptions: [{}], create(context, [options]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -207,6 +175,83 @@ export default createRule({ } return { + 'TSAsExpression, TSTypeAssertion'( + node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, + ): void { + if ( + options.typesToIgnore?.includes( + context.sourceCode.getText(node.typeAnnotation), + ) + ) { + return; + } + + const castType = services.getTypeAtLocation(node); + const uncastType = services.getTypeAtLocation(node.expression); + const typeIsUnchanged = isTypeUnchanged(uncastType, castType); + + const wouldSameTypeBeInferred = castType.isLiteral() + ? isImplicitlyNarrowedConstDeclaration(node) + : !isConstAssertion(node.typeAnnotation); + + if (typeIsUnchanged && wouldSameTypeBeInferred) { + context.report({ + fix(fixer) { + if (node.type === AST_NODE_TYPES.TSTypeAssertion) { + const openingAngleBracket = nullThrows( + context.sourceCode.getTokenBefore( + node.typeAnnotation, + token => + token.type === AST_TOKEN_TYPES.Punctuator && + token.value === '<', + ), + NullThrowsReasons.MissingToken('<', 'type annotation'), + ); + const closingAngleBracket = nullThrows( + context.sourceCode.getTokenAfter( + node.typeAnnotation, + token => + token.type === AST_TOKEN_TYPES.Punctuator && + token.value === '>', + ), + NullThrowsReasons.MissingToken('>', 'type annotation'), + ); + + // < ( number ) > ( 3 + 5 ) + // ^---remove---^ + return fixer.removeRange([ + openingAngleBracket.range[0], + closingAngleBracket.range[1], + ]); + } + // `as` is always present in TSAsExpression + const asToken = nullThrows( + context.sourceCode.getTokenAfter( + node.expression, + token => + token.type === AST_TOKEN_TYPES.Identifier && + token.value === 'as', + ), + NullThrowsReasons.MissingToken('>', 'type annotation'), + ); + const tokenBeforeAs = nullThrows( + context.sourceCode.getTokenBefore(asToken, { + includeComments: true, + }), + NullThrowsReasons.MissingToken('comment', 'as'), + ); + + // ( 3 + 5 ) as number + // ^--remove--^ + return fixer.removeRange([tokenBeforeAs.range[1], node.range[1]]); + }, + messageId: 'unnecessaryAssertion', + node, + }); + } + + // TODO - add contextually unnecessary check for this + }, TSNonNullExpression(node): void { if ( node.parent.type === AST_NODE_TYPES.AssignmentExpression && @@ -214,14 +259,14 @@ export default createRule({ ) { if (node.parent.left === node) { context.report({ - node, - messageId: 'contextuallyUnnecessary', fix(fixer) { return fixer.removeRange([ node.expression.range[1], node.range[1], ]); }, + messageId: 'contextuallyUnnecessary', + node, }); } // for all other = assignments we ignore non-null checks @@ -244,11 +289,11 @@ export default createRule({ } context.report({ - node, - messageId: 'unnecessaryAssertion', fix(fixer) { return fixer.removeRange([node.range[1] - 1, node.range[1]]); }, + messageId: 'unnecessaryAssertion', + node, }); } else { // we know it's a nullable type @@ -292,96 +337,52 @@ export default createRule({ if (isValidUndefined && isValidNull && isValidVoid) { context.report({ - node, - messageId: 'contextuallyUnnecessary', fix(fixer) { return fixer.removeRange([ node.expression.range[1], node.range[1], ]); }, + messageId: 'contextuallyUnnecessary', + node, }); } } } }, - 'TSAsExpression, TSTypeAssertion'( - node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, - ): void { - if ( - options.typesToIgnore?.includes( - context.sourceCode.getText(node.typeAnnotation), - ) - ) { - return; - } - - const castType = services.getTypeAtLocation(node); - const uncastType = services.getTypeAtLocation(node.expression); - const typeIsUnchanged = isTypeUnchanged(uncastType, castType); - - const wouldSameTypeBeInferred = castType.isLiteral() - ? isImplicitlyNarrowedConstDeclaration(node) - : !isConstAssertion(node.typeAnnotation); - - if (typeIsUnchanged && wouldSameTypeBeInferred) { - context.report({ - node, - messageId: 'unnecessaryAssertion', - fix(fixer) { - if (node.type === AST_NODE_TYPES.TSTypeAssertion) { - const openingAngleBracket = nullThrows( - context.sourceCode.getTokenBefore( - node.typeAnnotation, - token => - token.type === AST_TOKEN_TYPES.Punctuator && - token.value === '<', - ), - NullThrowsReasons.MissingToken('<', 'type annotation'), - ); - const closingAngleBracket = nullThrows( - context.sourceCode.getTokenAfter( - node.typeAnnotation, - token => - token.type === AST_TOKEN_TYPES.Punctuator && - token.value === '>', - ), - NullThrowsReasons.MissingToken('>', 'type annotation'), - ); - - // < ( number ) > ( 3 + 5 ) - // ^---remove---^ - return fixer.removeRange([ - openingAngleBracket.range[0], - closingAngleBracket.range[1], - ]); - } - // `as` is always present in TSAsExpression - const asToken = nullThrows( - context.sourceCode.getTokenAfter( - node.expression, - token => - token.type === AST_TOKEN_TYPES.Identifier && - token.value === 'as', - ), - NullThrowsReasons.MissingToken('>', 'type annotation'), - ); - const tokenBeforeAs = nullThrows( - context.sourceCode.getTokenBefore(asToken, { - includeComments: true, - }), - NullThrowsReasons.MissingToken('comment', 'as'), - ); - - // ( 3 + 5 ) as number - // ^--remove--^ - return fixer.removeRange([tokenBeforeAs.range[1], node.range[1]]); + }; + }, + defaultOptions: [{}], + meta: { + docs: { + description: + 'Disallow type assertions that do not change the type of an expression', + recommended: 'recommended', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + contextuallyUnnecessary: + 'This assertion is unnecessary since the receiver accepts the original type of the expression.', + unnecessaryAssertion: + 'This assertion is unnecessary since it does not change the type of the expression.', + }, + schema: [ + { + additionalProperties: false, + properties: { + typesToIgnore: { + description: 'A list of type names to ignore.', + items: { + type: 'string', }, - }); - } - - // TODO - add contextually unnecessary check for this + type: 'array', + }, + }, + type: 'object', }, - }; + ], + type: 'suggestion', }, + name: 'no-unnecessary-type-assertion', }); diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-constraint.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-constraint.ts index d31044ce86d2..4664ddfc4a47 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-constraint.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-constraint.ts @@ -1,10 +1,11 @@ -import { extname } from 'node:path'; - import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { extname } from 'node:path'; import * as ts from 'typescript'; import type { MakeRequired } from '../util'; + import { createRule } from '../util'; type TypeParameterWithConstraint = MakeRequired< @@ -13,23 +14,6 @@ type TypeParameterWithConstraint = MakeRequired< >; export default createRule({ - name: 'no-unnecessary-type-constraint', - meta: { - docs: { - description: 'Disallow unnecessary constraints on generic types', - recommended: 'recommended', - }, - hasSuggestions: true, - messages: { - unnecessaryConstraint: - 'Constraining the generic type `{{name}}` to `{{constraint}}` does nothing and is unnecessary.', - removeUnnecessaryConstraint: - 'Remove the unnecessary `{{constraint}}` constraint.', - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { // In theory, we could use the type checker for more advanced constraint types... // ...but in practice, these types are rare, and likely not worth requiring type info. @@ -81,9 +65,10 @@ export default createRule({ constraint, name: node.name.name, }, + messageId: 'unnecessaryConstraint', + node, suggest: [ { - messageId: 'removeUnnecessaryConstraint', data: { constraint, }, @@ -93,10 +78,9 @@ export default createRule({ shouldAddTrailingComma() ? ',' : '', ); }, + messageId: 'removeUnnecessaryConstraint', }, ], - messageId: 'unnecessaryConstraint', - node, }); } }; @@ -114,4 +98,21 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow unnecessary constraints on generic types', + recommended: 'recommended', + }, + hasSuggestions: true, + messages: { + removeUnnecessaryConstraint: + 'Remove the unnecessary `{{constraint}}` constraint.', + unnecessaryConstraint: + 'Constraining the generic type `{{name}}` to `{{constraint}}` does nothing and is unnecessary.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-unnecessary-type-constraint', }); diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts index d1fa75410f1a..940c5fc05dbe 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts @@ -1,32 +1,20 @@ import type { Reference } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; import type { MakeRequired } from '../util'; + import { createRule, getParserServices } from '../util'; type NodeWithTypeParameters = MakeRequired< - ts.SignatureDeclaration | ts.ClassLikeDeclaration, + ts.ClassLikeDeclaration | ts.SignatureDeclaration, 'typeParameters' >; export default createRule({ - defaultOptions: [], - meta: { - docs: { - description: "Disallow type parameters that aren't used multiple times", - requiresTypeChecking: true, - recommended: 'strict', - }, - messages: { - sole: 'Type parameter {{name}} is {{uses}} in the {{descriptor}} signature.', - }, - schema: [], - type: 'problem', - }, - name: 'no-unnecessary-type-parameters', create(context) { const parserServices = getParserServices(context); @@ -67,11 +55,11 @@ export default createRule({ context.report({ data: { descriptor, - uses: identifierCounts === 1 ? 'never used' : 'used only once', name: typeParameter.name.text, + uses: identifierCounts === 1 ? 'never used' : 'used only once', }, - node: esTypeParameter, messageId: 'sole', + node: esTypeParameter, }); } } @@ -98,6 +86,20 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: "Disallow type parameters that aren't used multiple times", + recommended: 'strict', + requiresTypeChecking: true, + }, + messages: { + sole: 'Type parameter {{name}} is {{uses}} in the {{descriptor}} signature.', + }, + schema: [], + type: 'problem', + }, + name: 'no-unnecessary-type-parameters', }); function isTypeParameterRepeatedInAST( @@ -399,9 +401,9 @@ function collectTypeParameterUsageCounts( } interface MappedType extends ts.ObjectType { - typeParameter?: ts.Type; constraintType?: ts.Type; templateType?: ts.Type; + typeParameter?: ts.Type; } function isMappedType(type: ts.Type): type is MappedType { diff --git a/packages/eslint-plugin/src/rules/no-unsafe-argument.ts b/packages/eslint-plugin/src/rules/no-unsafe-argument.ts index 08950fa8d732..57bc5984f96e 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-argument.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-argument.ts @@ -1,7 +1,8 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type * as ts from 'typescript'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + import { createRule, getParserServices, @@ -25,24 +26,31 @@ const enum RestTypeKind { } type RestType = | { - type: ts.Type; - kind: RestTypeKind.Array; index: number; + kind: RestTypeKind.Array; + type: ts.Type; } | { - type: ts.Type; - kind: RestTypeKind.Other; index: number; + kind: RestTypeKind.Other; + type: ts.Type; } | { - typeArguments: readonly ts.Type[]; - kind: RestTypeKind.Tuple; index: number; + kind: RestTypeKind.Tuple; + typeArguments: readonly ts.Type[]; }; class FunctionSignature { + private hasConsumedArguments = false; + private parameterTypeIndex = 0; + private constructor( + private paramTypes: ts.Type[], + private restType: RestType | null, + ) {} + public static create( checker: ts.TypeChecker, tsNode: ts.CallLikeExpression, @@ -65,21 +73,21 @@ class FunctionSignature { // is a rest param if (checker.isArrayType(type)) { restType = { - type: checker.getTypeArguments(type)[0], - kind: RestTypeKind.Array, index: i, + kind: RestTypeKind.Array, + type: checker.getTypeArguments(type)[0], }; } else if (checker.isTupleType(type)) { restType = { - typeArguments: checker.getTypeArguments(type), - kind: RestTypeKind.Tuple, index: i, + kind: RestTypeKind.Tuple, + typeArguments: checker.getTypeArguments(type), }; } else { restType = { - type, - kind: RestTypeKind.Other, index: i, + kind: RestTypeKind.Other, + type, }; } break; @@ -91,12 +99,9 @@ class FunctionSignature { return new this(paramTypes, restType); } - private hasConsumedArguments = false; - - private constructor( - private paramTypes: ts.Type[], - private restType: RestType | null, - ) {} + public consumeRemainingArguments(): void { + this.hasConsumedArguments = true; + } public getNextParameterType(): ts.Type | null { const index = this.parameterTypeIndex; @@ -133,38 +138,15 @@ class FunctionSignature { } return this.paramTypes[index]; } - - public consumeRemainingArguments(): void { - this.hasConsumedArguments = true; - } } export default createRule<[], MessageIds>({ - name: 'no-unsafe-argument', - meta: { - type: 'problem', - docs: { - description: 'Disallow calling a function with a value with type `any`', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - unsafeArgument: - 'Unsafe argument of type `{{sender}}` assigned to a parameter of type `{{receiver}}`.', - unsafeTupleSpread: - 'Unsafe spread of a tuple type. The argument is of type `{{sender}}` and is assigned to a parameter of type `{{receiver}}`.', - unsafeArraySpread: 'Unsafe spread of an `any` array type.', - unsafeSpread: 'Unsafe spread of an `any` type.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); function checkUnsafeArguments( - args: TSESTree.Expression[] | TSESTree.CallExpressionArgument[], + args: TSESTree.CallExpressionArgument[] | TSESTree.Expression[], callee: TSESTree.Expression, node: | TSESTree.CallExpression @@ -200,16 +182,16 @@ export default createRule<[], MessageIds>({ if (isTypeAnyType(spreadArgType)) { // foo(...any) context.report({ - node: argument, messageId: 'unsafeSpread', + node: argument, }); } else if (isTypeAnyArrayType(spreadArgType, checker)) { // foo(...any[]) // TODO - we could break down the spread and compare the array type against each argument context.report({ - node: argument, messageId: 'unsafeArraySpread', + node: argument, }); } else if (checker.isTupleType(spreadArgType)) { // foo(...[tuple1, tuple2]) @@ -230,12 +212,12 @@ export default createRule<[], MessageIds>({ ); if (result) { context.report({ - node: argument, - messageId: 'unsafeTupleSpread', data: { - sender: checker.typeToString(tupleType), receiver: checker.typeToString(parameterType), + sender: checker.typeToString(tupleType), }, + messageId: 'unsafeTupleSpread', + node: argument, }); } } @@ -267,12 +249,12 @@ export default createRule<[], MessageIds>({ ); if (result) { context.report({ - node: argument, - messageId: 'unsafeArgument', data: { - sender: checker.typeToString(argumentType), receiver: checker.typeToString(parameterType), + sender: checker.typeToString(argumentType), }, + messageId: 'unsafeArgument', + node: argument, }); } } @@ -291,4 +273,23 @@ export default createRule<[], MessageIds>({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow calling a function with a value with type `any`', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + unsafeArgument: + 'Unsafe argument of type `{{sender}}` assigned to a parameter of type `{{receiver}}`.', + unsafeArraySpread: 'Unsafe spread of an `any` array type.', + unsafeSpread: 'Unsafe spread of an `any` type.', + unsafeTupleSpread: + 'Unsafe spread of a tuple type. The argument is of type `{{sender}}` and is assigned to a parameter of type `{{receiver}}`.', + }, + schema: [], + type: 'problem', + }, + name: 'no-unsafe-argument', }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts b/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts index 22b3bfd09bd9..3caa4c1cd4bf 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-assignment.ts @@ -1,7 +1,8 @@ import type { TSESTree } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; -import type * as ts from 'typescript'; import { createRule, @@ -27,32 +28,6 @@ const enum ComparisonType { } export default createRule({ - name: 'no-unsafe-assignment', - meta: { - type: 'problem', - docs: { - description: - 'Disallow assigning a value with type `any` to variables and properties', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - anyAssignment: 'Unsafe assignment of an {{sender}} value.', - anyAssignmentThis: [ - 'Unsafe assignment of an {{sender}} value. `this` is typed as `any`.', - 'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.', - ].join('\n'), - unsafeArrayPattern: - 'Unsafe array destructuring of an {{sender}} array value.', - unsafeArrayPatternFromTuple: - 'Unsafe array destructuring of a tuple element with an {{sender}} value.', - unsafeAssignment: - 'Unsafe assignment of type {{sender}} to a variable of type {{receiver}}.', - unsafeArraySpread: 'Unsafe spread of an {{sender}} value in an array.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -87,9 +62,9 @@ export default createRule({ // const [x] = ([] as any[]); if (isTypeAnyArrayType(senderType, checker)) { context.report({ - node: receiverNode, - messageId: 'unsafeArrayPattern', data: createData(senderType), + messageId: 'unsafeArrayPattern', + node: receiverNode, }); return false; } @@ -126,9 +101,9 @@ export default createRule({ // check for the any type first so we can handle [[[x]]] = [any] if (isTypeAnyType(senderType)) { context.report({ - node: receiverElement, - messageId: 'unsafeArrayPatternFromTuple', data: createData(senderType), + messageId: 'unsafeArrayPatternFromTuple', + node: receiverElement, }); // we want to report on every invalid element in the tuple didReport = true; @@ -213,9 +188,9 @@ export default createRule({ // check for the any type first so we can handle {x: {y: z}} = {x: any} if (isTypeAnyType(senderType)) { context.report({ - node: receiverProperty.value, - messageId: 'unsafeArrayPatternFromTuple', data: createData(senderType), + messageId: 'unsafeArrayPatternFromTuple', + node: receiverProperty.value, }); didReport = true; } else if ( @@ -277,9 +252,9 @@ export default createRule({ } context.report({ - node: reportingNode, - messageId, data: createData(senderType), + messageId, + node: reportingNode, }); return true; @@ -299,11 +274,11 @@ export default createRule({ return false; } - const { sender, receiver } = result; + const { receiver, sender } = result; context.report({ - node: reportingNode, - messageId: 'unsafeAssignment', data: createData(sender, receiver), + messageId: 'unsafeAssignment', + node: reportingNode, }); return true; } @@ -324,8 +299,8 @@ export default createRule({ ): Readonly> | undefined { if (receiverType) { return { - sender: `\`${checker.typeToString(senderType)}\``, receiver: `\`${checker.typeToString(receiverType)}\``, + sender: `\`${checker.typeToString(senderType)}\``, }; } return { @@ -336,29 +311,26 @@ export default createRule({ } return { - 'VariableDeclarator[init != null]'( - node: TSESTree.VariableDeclarator, + 'AssignmentExpression[operator = "="], AssignmentPattern'( + node: TSESTree.AssignmentExpression | TSESTree.AssignmentPattern, ): void { - const init = nullThrows( - node.init, - NullThrowsReasons.MissingToken(node.type, 'init'), - ); let didReport = checkAssignment( - node.id, - init, + node.left, + node.right, node, - getComparisonType(node.id.typeAnnotation), + // the variable already has some form of a type to compare against + ComparisonType.Basic, ); if (!didReport) { - didReport = checkArrayDestructureHelper(node.id, init); + didReport = checkArrayDestructureHelper(node.left, node.right); } if (!didReport) { - checkObjectDestructureHelper(node.id, init); + checkObjectDestructureHelper(node.left, node.right); } }, 'PropertyDefinition[value != null]'( - node: TSESTree.PropertyDefinition & { value: NonNullable }, + node: { value: NonNullable } & TSESTree.PropertyDefinition, ): void { checkAssignment( node.key, @@ -367,22 +339,25 @@ export default createRule({ getComparisonType(node.typeAnnotation), ); }, - 'AssignmentExpression[operator = "="], AssignmentPattern'( - node: TSESTree.AssignmentExpression | TSESTree.AssignmentPattern, + 'VariableDeclarator[init != null]'( + node: TSESTree.VariableDeclarator, ): void { + const init = nullThrows( + node.init, + NullThrowsReasons.MissingToken(node.type, 'init'), + ); let didReport = checkAssignment( - node.left, - node.right, + node.id, + init, node, - // the variable already has some form of a type to compare against - ComparisonType.Basic, + getComparisonType(node.id.typeAnnotation), ); if (!didReport) { - didReport = checkArrayDestructureHelper(node.left, node.right); + didReport = checkArrayDestructureHelper(node.id, init); } if (!didReport) { - checkObjectDestructureHelper(node.left, node.right); + checkObjectDestructureHelper(node.id, init); } }, // object pattern props are checked via assignments @@ -401,9 +376,9 @@ export default createRule({ const restType = services.getTypeAtLocation(node.argument); if (isTypeAnyType(restType) || isTypeAnyArrayType(restType, checker)) { context.report({ - node, - messageId: 'unsafeArraySpread', data: createData(restType), + messageId: 'unsafeArraySpread', + node, }); } }, @@ -428,4 +403,30 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow assigning a value with type `any` to variables and properties', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + anyAssignment: 'Unsafe assignment of an {{sender}} value.', + anyAssignmentThis: [ + 'Unsafe assignment of an {{sender}} value. `this` is typed as `any`.', + 'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.', + ].join('\n'), + unsafeArrayPattern: + 'Unsafe array destructuring of an {{sender}} array value.', + unsafeArrayPatternFromTuple: + 'Unsafe array destructuring of a tuple element with an {{sender}} value.', + unsafeArraySpread: 'Unsafe spread of an {{sender}} value in an array.', + unsafeAssignment: + 'Unsafe assignment of type {{sender}} to a variable of type {{receiver}}.', + }, + schema: [], + type: 'problem', + }, + name: 'no-unsafe-assignment', }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-call.ts b/packages/eslint-plugin/src/rules/no-unsafe-call.ts index 6bdec17a4427..bf661c03d7d3 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-call.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-call.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import * as tsutils from 'ts-api-utils'; import { @@ -16,26 +17,6 @@ type MessageIds = | 'unsafeTemplateTag'; export default createRule<[], MessageIds>({ - name: 'no-unsafe-call', - meta: { - type: 'problem', - docs: { - description: 'Disallow calling a value with type `any`', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - unsafeCall: 'Unsafe call of an {{type}} typed value.', - unsafeCallThis: [ - 'Unsafe call of an `any` typed value. `this` is typed as `any`.', - 'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.', - ].join('\n'), - unsafeNew: 'Unsafe construction of an any type value.', - unsafeTemplateTag: 'Unsafe any typed template tag.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const compilerOptions = services.program.getCompilerOptions(); @@ -68,11 +49,11 @@ export default createRule<[], MessageIds>({ const isErrorType = tsutils.isIntrinsicErrorType(type); context.report({ - node: reportingNode, - messageId, data: { type: isErrorType ? '`error` type' : '`any`', }, + messageId, + node: reportingNode, }); } } @@ -91,4 +72,24 @@ export default createRule<[], MessageIds>({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow calling a value with type `any`', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + unsafeCall: 'Unsafe call of an {{type}} typed value.', + unsafeCallThis: [ + 'Unsafe call of an `any` typed value. `this` is typed as `any`.', + 'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.', + ].join('\n'), + unsafeNew: 'Unsafe construction of an any type value.', + unsafeTemplateTag: 'Unsafe any typed template tag.', + }, + schema: [], + type: 'problem', + }, + name: 'no-unsafe-call', }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-declaration-merging.ts b/packages/eslint-plugin/src/rules/no-unsafe-declaration-merging.ts index 2d8a797b116f..9b698b2ace71 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-declaration-merging.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-declaration-merging.ts @@ -1,25 +1,11 @@ import type { Scope } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'no-unsafe-declaration-merging', - meta: { - type: 'problem', - docs: { - description: 'Disallow unsafe declaration merging', - recommended: 'recommended', - requiresTypeChecking: false, - }, - messages: { - unsafeMerging: - 'Unsafe declaration merging between classes and interfaces.', - }, - schema: [], - }, - defaultOptions: [], create(context) { function checkUnsafeDeclaration( scope: Scope, @@ -38,8 +24,8 @@ export default createRule({ if (defs.some(def => def.node.type === unsafeKind)) { context.report({ - node, messageId: 'unsafeMerging', + node, }); } } @@ -70,4 +56,19 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow unsafe declaration merging', + recommended: 'recommended', + requiresTypeChecking: false, + }, + messages: { + unsafeMerging: + 'Unsafe declaration merging between classes and interfaces.', + }, + schema: [], + type: 'problem', + }, + name: 'no-unsafe-declaration-merging', }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts index ae7b33bc0e3e..eb1045559fa0 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -55,25 +56,6 @@ function getEnumValueType(type: ts.Type): ts.TypeFlags | undefined { } export default createRule({ - name: 'no-unsafe-enum-comparison', - meta: { - hasSuggestions: true, - type: 'suggestion', - docs: { - description: 'Disallow comparing an enum value with a non-enum value', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - mismatchedCase: - 'The case statement does not have a shared enum type with the switch predicate.', - mismatchedCondition: - 'The two values in this comparison do not have a shared enum type.', - replaceValueWithEnum: 'Replace with an enum value comparison.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const parserServices = getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); @@ -145,7 +127,6 @@ export default createRule({ node, suggest: [ { - messageId: 'replaceValueWithEnum', fix(fixer): TSESLint.RuleFix | null { // Replace the right side with an enum key if possible: // @@ -178,6 +159,7 @@ export default createRule({ return null; }, + messageId: 'replaceValueWithEnum', }, ], }); @@ -204,4 +186,23 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow comparing an enum value with a non-enum value', + recommended: 'recommended', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + mismatchedCase: + 'The case statement does not have a shared enum type with the switch predicate.', + mismatchedCondition: + 'The two values in this comparison do not have a shared enum type.', + replaceValueWithEnum: 'Replace with an enum value comparison.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-unsafe-enum-comparison', }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-function-type.ts b/packages/eslint-plugin/src/rules/no-unsafe-function-type.ts index 624c038f8ff7..53ae131c1016 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-function-type.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-function-type.ts @@ -1,26 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isReferenceToGlobalFunction } from '../util'; export default createRule({ - name: 'no-unsafe-function-type', - meta: { - type: 'problem', - docs: { - description: 'Disallow using the unsafe built-in Function type', - recommended: 'recommended', - }, - fixable: 'code', - messages: { - bannedFunctionType: [ - 'The `Function` type accepts any function-like value.', - 'Prefer explicitly defining any function parameters and return type.', - ].join('\n'), - }, - schema: [], - }, - defaultOptions: [], create(context) { function checkBannedTypes(node: TSESTree.Node): void { if ( @@ -29,8 +13,8 @@ export default createRule({ isReferenceToGlobalFunction('Function', node, context.sourceCode) ) { context.report({ - node, messageId: 'bannedFunctionType', + node, }); } } @@ -47,4 +31,21 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow using the unsafe built-in Function type', + recommended: 'recommended', + }, + fixable: 'code', + messages: { + bannedFunctionType: [ + 'The `Function` type accepts any function-like value.', + 'Prefer explicitly defining any function parameters and return type.', + ].join('\n'), + }, + schema: [], + type: 'problem', + }, + name: 'no-unsafe-function-type', }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-member-access.ts b/packages/eslint-plugin/src/rules/no-unsafe-member-access.ts index 4fae2b560073..1d789d949925 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-member-access.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-member-access.ts @@ -1,7 +1,8 @@ import type { TSESTree } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; -import type * as ts from 'typescript'; import { createRule, @@ -12,37 +13,16 @@ import { } from '../util'; const enum State { - Unsafe = 1, Safe = 2, + Unsafe = 1, } -function createDataType(type: ts.Type): '`error` typed' | '`any`' { +function createDataType(type: ts.Type): '`any`' | '`error` typed' { const isErrorType = tsutils.isIntrinsicErrorType(type); return isErrorType ? '`error` typed' : '`any`'; } export default createRule({ - name: 'no-unsafe-member-access', - meta: { - type: 'problem', - docs: { - description: 'Disallow member access on a value with type `any`', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - unsafeMemberExpression: - 'Unsafe member access {{property}} on an {{type}} value.', - unsafeThisMemberExpression: [ - 'Unsafe member access {{property}} on an `any` value. `this` is typed as `any`.', - 'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.', - ].join('\n'), - unsafeComputedMemberAccess: - 'Computed name {{property}} resolves to an {{type}} value.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const compilerOptions = services.program.getCompilerOptions(); @@ -94,12 +74,12 @@ export default createRule({ } context.report({ - node: node.property, - messageId, data: { property: node.computed ? `[${propertyName}]` : `.${propertyName}`, type: createDataType(type), }, + messageId, + node: node.property, }); } @@ -130,15 +110,36 @@ export default createRule({ if (isTypeAnyType(type)) { const propertyName = context.sourceCode.getText(node); context.report({ - node, - messageId: 'unsafeComputedMemberAccess', data: { property: `[${propertyName}]`, type: createDataType(type), }, + messageId: 'unsafeComputedMemberAccess', + node, }); } }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow member access on a value with type `any`', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + unsafeComputedMemberAccess: + 'Computed name {{property}} resolves to an {{type}} value.', + unsafeMemberExpression: + 'Unsafe member access {{property}} on an {{type}} value.', + unsafeThisMemberExpression: [ + 'Unsafe member access {{property}} on an `any` value. `this` is typed as `any`.', + 'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.', + ].join('\n'), + }, + schema: [], + type: 'problem', + }, + name: 'no-unsafe-member-access', }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-return.ts b/packages/eslint-plugin/src/rules/no-unsafe-return.ts index 1b89ffbf8568..5bda8442c7c5 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-return.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-return.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -19,26 +20,6 @@ import { } from '../util'; export default createRule({ - name: 'no-unsafe-return', - meta: { - type: 'problem', - docs: { - description: 'Disallow returning a value with type `any` from a function', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - unsafeReturn: 'Unsafe return of a value of type {{type}}.', - unsafeReturnThis: [ - 'Unsafe return of a value of type `{{type}}`. `this` is typed as `any`.', - 'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.', - ].join('\n'), - unsafeReturnAssignment: - 'Unsafe return of type `{{sender}}` from function with return type `{{receiver}}`.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -192,8 +173,6 @@ export default createRule({ // If the function return type was not unknown/unknown[], mark usage as unsafeReturn. return context.report({ - node: reportingNode, - messageId, data: { type: isErrorType ? 'error' @@ -203,6 +182,8 @@ export default createRule({ ? '`Promise`' : '`any[]`', }, + messageId, + node: reportingNode, }); } @@ -219,19 +200,20 @@ export default createRule({ return; } - const { sender, receiver } = result; + const { receiver, sender } = result; return context.report({ - node: reportingNode, - messageId: 'unsafeReturnAssignment', data: { - sender: checker.typeToString(sender), receiver: checker.typeToString(receiver), + sender: checker.typeToString(sender), }, + messageId: 'unsafeReturnAssignment', + node: reportingNode, }); } } return { + 'ArrowFunctionExpression > :not(BlockStatement).body': checkReturn, ReturnStatement(node): void { const argument = node.argument; if (!argument) { @@ -240,7 +222,26 @@ export default createRule({ checkReturn(argument, node); }, - 'ArrowFunctionExpression > :not(BlockStatement).body': checkReturn, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow returning a value with type `any` from a function', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + unsafeReturn: 'Unsafe return of a value of type {{type}}.', + unsafeReturnAssignment: + 'Unsafe return of type `{{sender}}` from function with return type `{{receiver}}`.', + unsafeReturnThis: [ + 'Unsafe return of a value of type `{{type}}`. `this` is typed as `any`.', + 'You can try to fix this by turning on the `noImplicitThis` compiler option, or adding a `this` parameter to the function.', + ].join('\n'), + }, + schema: [], + type: 'problem', + }, + name: 'no-unsafe-return', }); diff --git a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts index a61572515437..2619ea4ec52b 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-unary-minus.ts @@ -7,21 +7,6 @@ type Options = []; type MessageIds = 'unaryMinus'; export default util.createRule({ - name: 'no-unsafe-unary-minus', - meta: { - type: 'problem', - docs: { - description: 'Require unary negation to take a number', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - unaryMinus: - 'Argument of unary negation should be assignable to number | bigint but is {{type}} instead.', - }, - schema: [], - }, - defaultOptions: [], create(context) { return { UnaryExpression(node): void { @@ -49,12 +34,27 @@ export default util.createRule({ ) ) { context.report({ + data: { type: checker.typeToString(argType) }, messageId: 'unaryMinus', node, - data: { type: checker.typeToString(argType) }, }); } }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Require unary negation to take a number', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + unaryMinus: + 'Argument of unary negation should be assignable to number | bigint but is {{type}} instead.', + }, + schema: [], + type: 'problem', + }, + name: 'no-unsafe-unary-minus', }); diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts index 3c386aa69a87..8f832ca5a780 100644 --- a/packages/eslint-plugin/src/rules/no-unused-expressions.ts +++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts @@ -4,6 +4,7 @@ import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -13,25 +14,6 @@ type MessageIds = InferMessageIdsTypeFromRule; type Options = InferOptionsTypeFromRule; export default createRule({ - name: 'no-unused-expressions', - meta: { - type: 'suggestion', - docs: { - description: 'Disallow unused expressions', - extendsBaseRule: true, - recommended: 'recommended', - }, - hasSuggestions: baseRule.meta.hasSuggestions, - schema: baseRule.meta.schema, - messages: baseRule.meta.messages, - }, - defaultOptions: [ - { - allowShortCircuit: false, - allowTernary: false, - allowTaggedTemplates: false, - }, - ], create(context, [{ allowShortCircuit = false, allowTernary = false }]) { const rules = baseRule.create(context); @@ -78,4 +60,23 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowShortCircuit: false, + allowTaggedTemplates: false, + allowTernary: false, + }, + ], + meta: { + docs: { + description: 'Disallow unused expressions', + extendsBaseRule: true, + recommended: 'recommended', + }, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: baseRule.meta.schema, + type: 'suggestion', + }, + name: 'no-unused-expressions', }); diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index d98c2f5bf5a5..28feecb5f887 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -2,11 +2,12 @@ import type { Definition, ScopeVariable, } from '@typescript-eslint/scope-manager'; +import type { TSESTree } from '@typescript-eslint/utils'; + import { DefinitionType, PatternVisitor, } from '@typescript-eslint/scope-manager'; -import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils'; import { @@ -25,30 +26,30 @@ export type Options = [ | 'all' | 'local' | { - vars?: 'all' | 'local'; - varsIgnorePattern?: string; args?: 'after-used' | 'all' | 'none'; - ignoreRestSiblings?: boolean; argsIgnorePattern?: string; caughtErrors?: 'all' | 'none'; caughtErrorsIgnorePattern?: string; destructuredArrayIgnorePattern?: string; ignoreClassWithStaticInitBlock?: boolean; + ignoreRestSiblings?: boolean; reportUsedIgnorePattern?: boolean; + vars?: 'all' | 'local'; + varsIgnorePattern?: string; }, ]; interface TranslatedOptions { - vars: 'all' | 'local'; - varsIgnorePattern?: RegExp; args: 'after-used' | 'all' | 'none'; - ignoreRestSiblings: boolean; argsIgnorePattern?: RegExp; caughtErrors: 'all' | 'none'; caughtErrorsIgnorePattern?: RegExp; destructuredArrayIgnorePattern?: RegExp; ignoreClassWithStaticInitBlock: boolean; + ignoreRestSiblings: boolean; reportUsedIgnorePattern: boolean; + vars: 'all' | 'local'; + varsIgnorePattern?: RegExp; } type VariableType = @@ -58,83 +59,17 @@ type VariableType = | 'variable'; export default createRule({ - name: 'no-unused-vars', - meta: { - type: 'problem', - docs: { - description: 'Disallow unused variables', - recommended: 'recommended', - extendsBaseRule: true, - }, - schema: [ - { - oneOf: [ - { - type: 'string', - enum: ['all', 'local'], - }, - { - type: 'object', - properties: { - vars: { - type: 'string', - enum: ['all', 'local'], - }, - varsIgnorePattern: { - type: 'string', - }, - args: { - type: 'string', - enum: ['all', 'after-used', 'none'], - }, - ignoreRestSiblings: { - type: 'boolean', - }, - argsIgnorePattern: { - type: 'string', - }, - caughtErrors: { - type: 'string', - enum: ['all', 'none'], - }, - caughtErrorsIgnorePattern: { - type: 'string', - }, - destructuredArrayIgnorePattern: { - type: 'string', - }, - ignoreClassWithStaticInitBlock: { - type: 'boolean', - }, - reportUsedIgnorePattern: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - ], - messages: { - unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.", - usedIgnoredVar: - "'{{varName}}' is marked as ignored but is used{{additional}}.", - usedOnlyAsType: - "'{{varName}}' is {{action}} but only used as a type{{additional}}.", - }, - }, - defaultOptions: [{}], create(context, [firstOption]) { const MODULE_DECL_CACHE = new Map(); const options = ((): TranslatedOptions => { const options: TranslatedOptions = { - vars: 'all', args: 'after-used', - ignoreRestSiblings: false, caughtErrors: 'all', ignoreClassWithStaticInitBlock: false, + ignoreRestSiblings: false, reportUsedIgnorePattern: false, + vars: 'all', }; if (typeof firstOption === 'string') { @@ -267,7 +202,7 @@ export default createRule({ let additionalMessageData = ''; if (def) { - const { variableDescription, pattern } = getVariableDescription( + const { pattern, variableDescription } = getVariableDescription( defToVariableType(def), ); @@ -277,9 +212,9 @@ export default createRule({ } return { - varName: unusedVar.name, action: 'defined', additional: additionalMessageData, + varName: unusedVar.name, }; } @@ -296,7 +231,7 @@ export default createRule({ let additionalMessageData = ''; if (def) { - const { variableDescription, pattern } = getVariableDescription( + const { pattern, variableDescription } = getVariableDescription( defToVariableType(def), ); @@ -306,9 +241,9 @@ export default createRule({ } return { - varName: unusedVar.name, action: 'assigned a value', additional: additionalMessageData, + varName: unusedVar.name, }; } @@ -323,7 +258,7 @@ export default createRule({ variable: ScopeVariable, variableType: VariableType, ): Record { - const { variableDescription, pattern } = + const { pattern, variableDescription } = getVariableDescription(variableType); let additionalMessageData = ''; @@ -333,8 +268,8 @@ export default createRule({ } return { - varName: variable.name, additional: additionalMessageData, + varName: variable.name, }; } @@ -433,9 +368,9 @@ export default createRule({ ) { if (options.reportUsedIgnorePattern && used) { context.report({ - node: def.name, - messageId: 'usedIgnoredVar', data: getUsedIgnoredMessageData(variable, 'array-destructure'), + messageId: 'usedIgnoredVar', + node: def.name, }); } continue; @@ -463,9 +398,9 @@ export default createRule({ ) { if (options.reportUsedIgnorePattern && used) { context.report({ - node: def.name, - messageId: 'usedIgnoredVar', data: getUsedIgnoredMessageData(variable, 'catch-clause'), + messageId: 'usedIgnoredVar', + node: def.name, }); } continue; @@ -482,9 +417,9 @@ export default createRule({ ) { if (options.reportUsedIgnorePattern && used) { context.report({ - node: def.name, - messageId: 'usedIgnoredVar', data: getUsedIgnoredMessageData(variable, 'parameter'), + messageId: 'usedIgnoredVar', + node: def.name, }); } continue; @@ -505,9 +440,9 @@ export default createRule({ ) { if (options.reportUsedIgnorePattern && used) { context.report({ - node: def.name, - messageId: 'usedIgnoredVar', data: getUsedIgnoredMessageData(variable, 'variable'), + messageId: 'usedIgnoredVar', + node: def.name, }); } continue; @@ -608,19 +543,19 @@ export default createRule({ const idLength = id.name.length; const loc = { - start, end: { - line: start.line, column: start.column + idLength, + line: start.line, }, + start, }; context.report({ - loc, - messageId, data: unusedVar.references.some(ref => ref.isWrite()) ? getAssignedMessageData(unusedVar) : getDefinedMessageData(unusedVar), + loc, + messageId, }); // If there are no regular declaration, report the first `/*globals*/` comment directive. @@ -631,14 +566,14 @@ export default createRule({ const directiveComment = unusedVar.eslintExplicitGlobalComments[0]; context.report({ - node: programNode, + data: getDefinedMessageData(unusedVar), loc: getNameLocationInGlobalDirectiveComment( context.sourceCode, directiveComment, unusedVar.name, ), messageId: 'unusedVar', - data: getDefinedMessageData(unusedVar), + node: programNode, }); } } @@ -721,8 +656,8 @@ export default createRule({ let scope = context.sourceCode.getScope(node); const shouldUseUpperScope = [ - AST_NODE_TYPES.TSModuleDeclaration, AST_NODE_TYPES.TSDeclareFunction, + AST_NODE_TYPES.TSModuleDeclaration, ].includes(node.type); if (scope.variableScope !== scope) { @@ -747,6 +682,72 @@ export default createRule({ visitor.visit(node); } }, + defaultOptions: [{}], + meta: { + docs: { + description: 'Disallow unused variables', + extendsBaseRule: true, + recommended: 'recommended', + }, + messages: { + unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.", + usedIgnoredVar: + "'{{varName}}' is marked as ignored but is used{{additional}}.", + usedOnlyAsType: + "'{{varName}}' is {{action}} but only used as a type{{additional}}.", + }, + schema: [ + { + oneOf: [ + { + enum: ['all', 'local'], + type: 'string', + }, + { + additionalProperties: false, + properties: { + args: { + enum: ['all', 'after-used', 'none'], + type: 'string', + }, + argsIgnorePattern: { + type: 'string', + }, + caughtErrors: { + enum: ['all', 'none'], + type: 'string', + }, + caughtErrorsIgnorePattern: { + type: 'string', + }, + destructuredArrayIgnorePattern: { + type: 'string', + }, + ignoreClassWithStaticInitBlock: { + type: 'boolean', + }, + ignoreRestSiblings: { + type: 'boolean', + }, + reportUsedIgnorePattern: { + type: 'boolean', + }, + vars: { + enum: ['all', 'local'], + type: 'string', + }, + varsIgnorePattern: { + type: 'string', + }, + }, + type: 'object', + }, + ], + }, + ], + type: 'problem', + }, + name: 'no-unused-vars', }); /* diff --git a/packages/eslint-plugin/src/rules/no-use-before-define.ts b/packages/eslint-plugin/src/rules/no-use-before-define.ts index f333683d2f42..cd60bb1e708f 100644 --- a/packages/eslint-plugin/src/rules/no-use-before-define.ts +++ b/packages/eslint-plugin/src/rules/no-use-before-define.ts @@ -1,5 +1,6 @@ -import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; + +import { DefinitionType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -33,13 +34,13 @@ function parseOptions(options: Config | string | null): Required { } return { - functions, + allowNamedExports, classes, enums, - variables, - typedefs, + functions, ignoreTypeReferences, - allowNamedExports, + typedefs, + variables, }; } @@ -203,63 +204,18 @@ function isInInitializer( } interface Config { - functions?: boolean; + allowNamedExports?: boolean; classes?: boolean; enums?: boolean; - variables?: boolean; - typedefs?: boolean; + functions?: boolean; ignoreTypeReferences?: boolean; - allowNamedExports?: boolean; + typedefs?: boolean; + variables?: boolean; } -type Options = [Config | 'nofunc']; +type Options = ['nofunc' | Config]; type MessageIds = 'noUseBeforeDefine'; export default createRule({ - name: 'no-use-before-define', - meta: { - type: 'problem', - docs: { - description: 'Disallow the use of variables before they are defined', - extendsBaseRule: true, - }, - messages: { - noUseBeforeDefine: "'{{name}}' was used before it was defined.", - }, - schema: [ - { - oneOf: [ - { - type: 'string', - enum: ['nofunc'], - }, - { - type: 'object', - properties: { - functions: { type: 'boolean' }, - classes: { type: 'boolean' }, - enums: { type: 'boolean' }, - variables: { type: 'boolean' }, - typedefs: { type: 'boolean' }, - ignoreTypeReferences: { type: 'boolean' }, - allowNamedExports: { type: 'boolean' }, - }, - additionalProperties: false, - }, - ], - }, - ], - }, - defaultOptions: [ - { - functions: true, - classes: true, - enums: true, - variables: true, - typedefs: true, - ignoreTypeReferences: true, - allowNamedExports: false, - }, - ], create(context, optionsWithDefault) { const options = parseOptions(optionsWithDefault[0]); @@ -313,11 +269,11 @@ export default createRule({ function report(): void { context.report({ - node: reference.identifier, - messageId: 'noUseBeforeDefine', data: { name: reference.identifier.name, }, + messageId: 'noUseBeforeDefine', + node: reference.identifier, }); } @@ -365,4 +321,49 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowNamedExports: false, + classes: true, + enums: true, + functions: true, + ignoreTypeReferences: true, + typedefs: true, + variables: true, + }, + ], + meta: { + docs: { + description: 'Disallow the use of variables before they are defined', + extendsBaseRule: true, + }, + messages: { + noUseBeforeDefine: "'{{name}}' was used before it was defined.", + }, + schema: [ + { + oneOf: [ + { + enum: ['nofunc'], + type: 'string', + }, + { + additionalProperties: false, + properties: { + allowNamedExports: { type: 'boolean' }, + classes: { type: 'boolean' }, + enums: { type: 'boolean' }, + functions: { type: 'boolean' }, + ignoreTypeReferences: { type: 'boolean' }, + typedefs: { type: 'boolean' }, + variables: { type: 'boolean' }, + }, + type: 'object', + }, + ], + }, + ], + type: 'problem', + }, + name: 'no-use-before-define', }); diff --git a/packages/eslint-plugin/src/rules/no-useless-constructor.ts b/packages/eslint-plugin/src/rules/no-useless-constructor.ts index cd91a71c2002..39a28c1e8fa0 100644 --- a/packages/eslint-plugin/src/rules/no-useless-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-useless-constructor.ts @@ -1,10 +1,12 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; @@ -42,19 +44,6 @@ function checkParams(node: TSESTree.MethodDefinition): boolean { } export default createRule({ - name: 'no-useless-constructor', - meta: { - type: 'problem', - docs: { - description: 'Disallow unnecessary constructors', - recommended: 'strict', - extendsBaseRule: true, - }, - hasSuggestions: baseRule.meta.hasSuggestions, - schema: baseRule.meta.schema, - messages: baseRule.meta.messages, - }, - defaultOptions: [], create(context) { const rules = baseRule.create(context); return { @@ -69,4 +58,17 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow unnecessary constructors', + extendsBaseRule: true, + recommended: 'strict', + }, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema: baseRule.meta.schema, + type: 'problem', + }, + name: 'no-useless-constructor', }); diff --git a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts index a21f1cfa6224..32befd651a19 100644 --- a/packages/eslint-plugin/src/rules/no-useless-empty-export.ts +++ b/packages/eslint-plugin/src/rules/no-useless-empty-export.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isDefinitionFile } from '../util'; @@ -24,21 +25,6 @@ const exportOrImportNodeTypes = new Set([ ]); export default createRule({ - name: 'no-useless-empty-export', - meta: { - docs: { - description: - "Disallow empty exports that don't change anything in a module file", - }, - fixable: 'code', - hasSuggestions: false, - messages: { - uselessExport: 'Empty export does nothing and can be removed.', - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { // In a definition file, export {} is necessary to make the module properly // encapsulated, even when there are other exports @@ -80,4 +66,19 @@ export default createRule({ TSModuleDeclaration: checkNode, }; }, + defaultOptions: [], + meta: { + docs: { + description: + "Disallow empty exports that don't change anything in a module file", + }, + fixable: 'code', + hasSuggestions: false, + messages: { + uselessExport: 'Empty export does nothing and can be removed.', + }, + schema: [], + type: 'suggestion', + }, + name: 'no-useless-empty-export', }); diff --git a/packages/eslint-plugin/src/rules/no-var-requires.ts b/packages/eslint-plugin/src/rules/no-var-requires.ts index b7c38620168a..c13c95350c1a 100644 --- a/packages/eslint-plugin/src/rules/no-var-requires.ts +++ b/packages/eslint-plugin/src/rules/no-var-requires.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import { createRule, getStaticStringValue } from '../util'; @@ -11,32 +12,6 @@ type Options = [ type MessageIds = 'noVarReqs'; export default createRule({ - name: 'no-var-requires', - meta: { - deprecated: true, - replacedBy: ['@typescript-eslint/no-require-imports'], - type: 'problem', - docs: { - description: 'Disallow `require` statements except in import statements', - }, - messages: { - noVarReqs: 'Require statement not part of import statement.', - }, - schema: [ - { - type: 'object', - properties: { - allow: { - type: 'array', - items: { type: 'string' }, - description: 'Patterns of import paths to allow requiring from.', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [{ allow: [] }], create(context, options) { const allowPatterns = options[0].allow.map( pattern => new RegExp(pattern, 'u'), @@ -85,12 +60,38 @@ export default createRule({ if (!variable?.identifiers.length) { context.report({ - node, messageId: 'noVarReqs', + node, }); } } }, }; }, + defaultOptions: [{ allow: [] }], + meta: { + deprecated: true, + docs: { + description: 'Disallow `require` statements except in import statements', + }, + messages: { + noVarReqs: 'Require statement not part of import statement.', + }, + replacedBy: ['@typescript-eslint/no-require-imports'], + schema: [ + { + additionalProperties: false, + properties: { + allow: { + description: 'Patterns of import paths to allow requiring from.', + items: { type: 'string' }, + type: 'array', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'no-var-requires', }); diff --git a/packages/eslint-plugin/src/rules/no-wrapper-object-types.ts b/packages/eslint-plugin/src/rules/no-wrapper-object-types.ts index f51b6c8564b5..433684819dfc 100644 --- a/packages/eslint-plugin/src/rules/no-wrapper-object-types.ts +++ b/packages/eslint-plugin/src/rules/no-wrapper-object-types.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isReferenceToGlobalFunction } from '../util'; @@ -15,21 +16,6 @@ const classNames = new Set([ ]); export default createRule({ - name: 'no-wrapper-object-types', - meta: { - type: 'problem', - docs: { - description: 'Disallow using confusing built-in primitive class wrappers', - recommended: 'recommended', - }, - fixable: 'code', - messages: { - bannedClassType: - 'Prefer using the primitive `{{preferred}}` as a type name, rather than the upper-cased `{{typeName}}`.', - }, - schema: [], - }, - defaultOptions: [], create(context) { function checkBannedTypes( node: TSESTree.EntityName | TSESTree.Expression, @@ -47,7 +33,7 @@ export default createRule({ const preferred = typeName.toLowerCase(); context.report({ - data: { typeName, preferred }, + data: { preferred, typeName }, fix: includeFix ? (fixer): TSESLint.RuleFix => fixer.replaceText(node, preferred) : undefined, @@ -68,4 +54,19 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Disallow using confusing built-in primitive class wrappers', + recommended: 'recommended', + }, + fixable: 'code', + messages: { + bannedClassType: + 'Prefer using the primitive `{{preferred}}` as a type name, rather than the upper-cased `{{typeName}}`.', + }, + schema: [], + type: 'problem', + }, + name: 'no-wrapper-object-types', }); diff --git a/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts index e4b237cccc85..364c10053c19 100644 --- a/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts +++ b/packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -11,23 +12,6 @@ import { } from '../util'; export default createRule({ - name: 'non-nullable-type-assertion-style', - meta: { - docs: { - description: 'Enforce non-null assertions over explicit type casts', - recommended: 'stylistic', - requiresTypeChecking: true, - }, - fixable: 'code', - messages: { - preferNonNullAssertion: - 'Use a ! assertion to more succinctly remove null and undefined from the type.', - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], - create(context) { const services = getParserServices(context); @@ -144,4 +128,21 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Enforce non-null assertions over explicit type casts', + recommended: 'stylistic', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + preferNonNullAssertion: + 'Use a ! assertion to more succinctly remove null and undefined from the type.', + }, + schema: [], + type: 'suggestion', + }, + + name: 'non-nullable-type-assertion-style', }); diff --git a/packages/eslint-plugin/src/rules/only-throw-error.ts b/packages/eslint-plugin/src/rules/only-throw-error.ts index 45a6dac0942d..2c48b9d2d3db 100644 --- a/packages/eslint-plugin/src/rules/only-throw-error.ts +++ b/packages/eslint-plugin/src/rules/only-throw-error.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -20,40 +21,6 @@ type Options = [ ]; export default createRule({ - name: 'only-throw-error', - meta: { - type: 'problem', - docs: { - description: 'Disallow throwing non-`Error` values as exceptions', - recommended: 'recommended', - extendsBaseRule: 'no-throw-literal', - requiresTypeChecking: true, - }, - schema: [ - { - type: 'object', - properties: { - allowThrowingAny: { - type: 'boolean', - }, - allowThrowingUnknown: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - messages: { - object: 'Expected an error object to be thrown.', - undef: 'Do not throw undefined.', - }, - }, - defaultOptions: [ - { - allowThrowingAny: true, - allowThrowingUnknown: true, - }, - ], create(context, [options]) { const services = getParserServices(context); @@ -68,7 +35,7 @@ export default createRule({ const type = services.getTypeAtLocation(node); if (type.flags & ts.TypeFlags.Undefined) { - context.report({ node, messageId: 'undef' }); + context.report({ messageId: 'undef', node }); return; } @@ -84,7 +51,7 @@ export default createRule({ return; } - context.report({ node, messageId: 'object' }); + context.report({ messageId: 'object', node }); } return { @@ -95,4 +62,38 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowThrowingAny: true, + allowThrowingUnknown: true, + }, + ], + meta: { + docs: { + description: 'Disallow throwing non-`Error` values as exceptions', + extendsBaseRule: 'no-throw-literal', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + object: 'Expected an error object to be thrown.', + undef: 'Do not throw undefined.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowThrowingAny: { + type: 'boolean', + }, + allowThrowingUnknown: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'only-throw-error', }); diff --git a/packages/eslint-plugin/src/rules/parameter-properties.ts b/packages/eslint-plugin/src/rules/parameter-properties.ts index 5246594e912e..4ebdd23264b6 100644 --- a/packages/eslint-plugin/src/rules/parameter-properties.ts +++ b/packages/eslint-plugin/src/rules/parameter-properties.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, nullThrows } from '../util'; @@ -24,58 +25,6 @@ type Options = [ type MessageIds = 'preferClassProperty' | 'preferParameterProperty'; export default createRule({ - name: 'parameter-properties', - meta: { - type: 'problem', - docs: { - description: - 'Require or disallow parameter properties in class constructors', - }, - messages: { - preferClassProperty: - 'Property {{parameter}} should be declared as a class property.', - preferParameterProperty: - 'Property {{parameter}} should be declared as a parameter property.', - }, - schema: [ - { - $defs: { - modifier: { - type: 'string', - enum: [ - 'readonly', - 'private', - 'protected', - 'public', - 'private readonly', - 'protected readonly', - 'public readonly', - ], - }, - }, - type: 'object', - properties: { - allow: { - type: 'array', - items: { - $ref: '#/items/0/$defs/modifier', - }, - }, - prefer: { - type: 'string', - enum: ['class-property', 'parameter-property'], - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - allow: [], - prefer: 'class-property', - }, - ], create(context, [{ allow = [], prefer = 'class-property' }]) { /** * Gets the modifiers of `node`. @@ -117,11 +66,11 @@ export default createRule({ (node.parameter.left as TSESTree.Identifier).name; context.report({ - node, - messageId: 'preferClassProperty', data: { parameter: name, }, + messageId: 'preferClassProperty', + node, }); } }, @@ -169,10 +118,6 @@ export default createRule({ } return { - 'ClassDeclaration, ClassExpression'(): void { - propertyNodesByNameStack.push(new Map()); - }, - ':matches(ClassDeclaration, ClassExpression):exit'(): void { const propertyNodesByName = nullThrows( propertyNodesByNameStack.pop(), @@ -213,6 +158,10 @@ export default createRule({ } }, + 'ClassDeclaration, ClassExpression'(): void { + propertyNodesByNameStack.push(new Map()); + }, + 'MethodDefinition[kind="constructor"]'( node: TSESTree.MethodDefinition, ): void { @@ -244,4 +193,56 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allow: [], + prefer: 'class-property', + }, + ], + meta: { + docs: { + description: + 'Require or disallow parameter properties in class constructors', + }, + messages: { + preferClassProperty: + 'Property {{parameter}} should be declared as a class property.', + preferParameterProperty: + 'Property {{parameter}} should be declared as a parameter property.', + }, + schema: [ + { + $defs: { + modifier: { + enum: [ + 'readonly', + 'private', + 'protected', + 'public', + 'private readonly', + 'protected readonly', + 'public readonly', + ], + type: 'string', + }, + }, + additionalProperties: false, + properties: { + allow: { + items: { + $ref: '#/items/0/$defs/modifier', + }, + type: 'array', + }, + prefer: { + enum: ['class-property', 'parameter-property'], + type: 'string', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'parameter-properties', }); diff --git a/packages/eslint-plugin/src/rules/prefer-as-const.ts b/packages/eslint-plugin/src/rules/prefer-as-const.ts index f3bcaf2963af..54841bf69c4f 100644 --- a/packages/eslint-plugin/src/rules/prefer-as-const.ts +++ b/packages/eslint-plugin/src/rules/prefer-as-const.ts @@ -1,28 +1,10 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'prefer-as-const', - meta: { - type: 'suggestion', - docs: { - description: 'Enforce the use of `as const` over literal type', - recommended: 'recommended', - }, - fixable: 'code', - hasSuggestions: true, - messages: { - preferConstAssertion: - 'Expected a `const` instead of a literal type assertion.', - variableConstAssertion: - 'Expected a `const` assertion instead of a literal type annotation.', - variableSuggest: 'You should use `as const` instead of type annotation.', - }, - schema: [], - }, - defaultOptions: [], create(context) { function compareTypes( valueNode: TSESTree.Expression, @@ -37,21 +19,21 @@ export default createRule({ ) { if (canFix) { context.report({ - node: typeNode, - messageId: 'preferConstAssertion', fix: fixer => fixer.replaceText(typeNode, 'const'), + messageId: 'preferConstAssertion', + node: typeNode, }); } else { context.report({ - node: typeNode, messageId: 'variableConstAssertion', + node: typeNode, suggest: [ { - messageId: 'variableSuggest', fix: (fixer): TSESLint.RuleFix[] => [ fixer.remove(typeNode.parent), fixer.insertTextAfter(valueNode, ' as const'), ], + messageId: 'variableSuggest', }, ], }); @@ -60,17 +42,17 @@ export default createRule({ } return { + PropertyDefinition(node): void { + if (node.value && node.typeAnnotation) { + compareTypes(node.value, node.typeAnnotation.typeAnnotation, false); + } + }, TSAsExpression(node): void { compareTypes(node.expression, node.typeAnnotation, true); }, TSTypeAssertion(node): void { compareTypes(node.expression, node.typeAnnotation, true); }, - PropertyDefinition(node): void { - if (node.value && node.typeAnnotation) { - compareTypes(node.value, node.typeAnnotation.typeAnnotation, false); - } - }, VariableDeclarator(node): void { if (node.init && node.id.typeAnnotation) { compareTypes(node.init, node.id.typeAnnotation.typeAnnotation, false); @@ -78,4 +60,23 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: 'Enforce the use of `as const` over literal type', + recommended: 'recommended', + }, + fixable: 'code', + hasSuggestions: true, + messages: { + preferConstAssertion: + 'Expected a `const` instead of a literal type assertion.', + variableConstAssertion: + 'Expected a `const` assertion instead of a literal type annotation.', + variableSuggest: 'You should use `as const` instead of type annotation.', + }, + schema: [], + type: 'suggestion', + }, + name: 'prefer-as-const', }); diff --git a/packages/eslint-plugin/src/rules/prefer-destructuring.ts b/packages/eslint-plugin/src/rules/prefer-destructuring.ts index 60e53dbb61e6..dcfa10eb8ab3 100644 --- a/packages/eslint-plugin/src/rules/prefer-destructuring.ts +++ b/packages/eslint-plugin/src/rules/prefer-destructuring.ts @@ -1,28 +1,30 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; -import * as tsutils from 'ts-api-utils'; import type * as ts from 'typescript'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; + import type { InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, } from '../util'; + import { createRule, getParserServices, isTypeAnyType } from '../util'; import { getESLintCoreRule } from '../util/getESLintCoreRule'; const baseRule = getESLintCoreRule('prefer-destructuring'); type BaseOptions = InferOptionsTypeFromRule; -type EnforcementOptions = BaseOptions[1] & { +type EnforcementOptions = { enforceForDeclarationWithTypeAnnotation?: boolean; -}; +} & BaseOptions[1]; type Options = [BaseOptions[0], EnforcementOptions]; type MessageIds = InferMessageIdsTypeFromRule; const destructuringTypeConfig: JSONSchema4 = { - type: 'object', + additionalProperties: false, properties: { array: { type: 'boolean', @@ -31,89 +33,63 @@ const destructuringTypeConfig: JSONSchema4 = { type: 'boolean', }, }, - additionalProperties: false, + type: 'object', }; const schema: readonly JSONSchema4[] = [ { oneOf: [ { - type: 'object', + additionalProperties: false, properties: { - VariableDeclarator: destructuringTypeConfig, AssignmentExpression: destructuringTypeConfig, + VariableDeclarator: destructuringTypeConfig, }, - additionalProperties: false, + type: 'object', }, destructuringTypeConfig, ], }, { - type: 'object', properties: { - enforceForRenamedProperties: { + enforceForDeclarationWithTypeAnnotation: { type: 'boolean', }, - enforceForDeclarationWithTypeAnnotation: { + enforceForRenamedProperties: { type: 'boolean', }, }, + type: 'object', }, ]; export default createRule({ - name: 'prefer-destructuring', - meta: { - type: 'suggestion', - docs: { - description: 'Require destructuring from arrays and/or objects', - extendsBaseRule: true, - requiresTypeChecking: true, - }, - schema, - fixable: baseRule.meta.fixable, - hasSuggestions: baseRule.meta.hasSuggestions, - messages: baseRule.meta.messages, - }, - defaultOptions: [ - { - VariableDeclarator: { - array: true, - object: true, - }, - AssignmentExpression: { - array: true, - object: true, - }, - }, - {}, - ], create(context, [enabledTypes, options]) { const { - enforceForRenamedProperties = false, enforceForDeclarationWithTypeAnnotation = false, + enforceForRenamedProperties = false, } = options; - const { program, esTreeNodeToTSNodeMap } = getParserServices(context); + const { esTreeNodeToTSNodeMap, program } = getParserServices(context); const typeChecker = program.getTypeChecker(); const baseRules = baseRule.create(context); let baseRulesWithoutFixCache: typeof baseRules | null = null; return { - VariableDeclarator(node): void { - performCheck(node.id, node.init, node); - }, AssignmentExpression(node): void { if (node.operator !== '=') { return; } performCheck(node.left, node.right, node); }, + VariableDeclarator(node): void { + performCheck(node.id, node.init, node); + }, }; function performCheck( leftNode: TSESTree.BindingName | TSESTree.Expression, rightNode: TSESTree.Expression | null, - reportNode: TSESTree.VariableDeclarator | TSESTree.AssignmentExpression, + reportNode: TSESTree.AssignmentExpression | TSESTree.VariableDeclarator, ): void { const rules = leftNode.type === AST_NODE_TYPES.Identifier && @@ -145,9 +121,9 @@ export default createRule({ return; } context.report({ - node: reportNode, - messageId: 'preferDestructuring', data: { type: 'object' }, + messageId: 'preferDestructuring', + node: reportNode, }); return; } @@ -162,8 +138,8 @@ export default createRule({ function getNormalizedEnabledType( nodeType: - | AST_NODE_TYPES.VariableDeclarator - | AST_NODE_TYPES.AssignmentExpression, + | AST_NODE_TYPES.AssignmentExpression + | AST_NODE_TYPES.VariableDeclarator, destructuringType: 'array' | 'object', ): boolean | undefined { if ('object' in enabledTypes || 'array' in enabledTypes) { @@ -179,6 +155,32 @@ export default createRule({ return baseRulesWithoutFixCache; } }, + defaultOptions: [ + { + AssignmentExpression: { + array: true, + object: true, + }, + VariableDeclarator: { + array: true, + object: true, + }, + }, + {}, + ], + meta: { + docs: { + description: 'Require destructuring from arrays and/or objects', + extendsBaseRule: true, + requiresTypeChecking: true, + }, + fixable: baseRule.meta.fixable, + hasSuggestions: baseRule.meta.hasSuggestions, + messages: baseRule.meta.messages, + schema, + type: 'suggestion', + }, + name: 'prefer-destructuring', }); type Context = TSESLint.RuleContext; diff --git a/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts b/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts index 00dae8cccb96..14fac17fa4a7 100644 --- a/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts +++ b/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts @@ -5,23 +5,6 @@ import { createRule } from '../util'; type MessageIds = 'defineInitializer' | 'defineInitializerSuggestion'; export default createRule<[], MessageIds>({ - name: 'prefer-enum-initializers', - meta: { - type: 'suggestion', - docs: { - description: - 'Require each enum member value to be explicitly initialized', - }, - hasSuggestions: true, - messages: { - defineInitializer: - "The value of the member '{{ name }}' should be explicitly defined.", - defineInitializerSuggestion: - 'Can be fixed to {{ name }} = {{ suggested }}', - }, - schema: [], - }, - defaultOptions: [], create(context) { function TSEnumDeclaration(node: TSESTree.TSEnumDeclaration): void { const { members } = node.body; @@ -30,32 +13,32 @@ export default createRule<[], MessageIds>({ if (member.initializer == null) { const name = context.sourceCode.getText(member); context.report({ - node: member, - messageId: 'defineInitializer', data: { name, }, + messageId: 'defineInitializer', + node: member, suggest: [ { - messageId: 'defineInitializerSuggestion', data: { name, suggested: index }, fix: (fixer): TSESLint.RuleFix => { return fixer.replaceText(member, `${name} = ${index}`); }, + messageId: 'defineInitializerSuggestion', }, { - messageId: 'defineInitializerSuggestion', data: { name, suggested: index + 1 }, fix: (fixer): TSESLint.RuleFix => { return fixer.replaceText(member, `${name} = ${index + 1}`); }, + messageId: 'defineInitializerSuggestion', }, { - messageId: 'defineInitializerSuggestion', data: { name, suggested: `'${name}'` }, fix: (fixer): TSESLint.RuleFix => { return fixer.replaceText(member, `${name} = '${name}'`); }, + messageId: 'defineInitializerSuggestion', }, ], }); @@ -67,4 +50,21 @@ export default createRule<[], MessageIds>({ TSEnumDeclaration, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Require each enum member value to be explicitly initialized', + }, + hasSuggestions: true, + messages: { + defineInitializer: + "The value of the member '{{ name }}' should be explicitly defined.", + defineInitializerSuggestion: + 'Can be fixed to {{ name }} = {{ suggested }}', + }, + schema: [], + type: 'suggestion', + }, + name: 'prefer-enum-initializers', }); diff --git a/packages/eslint-plugin/src/rules/prefer-find.ts b/packages/eslint-plugin/src/rules/prefer-find.ts index 587cd5d0f03d..7b61e01b5160 100644 --- a/packages/eslint-plugin/src/rules/prefer-find.ts +++ b/packages/eslint-plugin/src/rules/prefer-find.ts @@ -1,9 +1,10 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { RuleFix, Scope } from '@typescript-eslint/utils/ts-eslint'; -import * as tsutils from 'ts-api-utils'; import type { Type } from 'typescript'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; + import { createRule, getConstrainedTypeAtLocation, @@ -13,33 +14,14 @@ import { } from '../util'; export default createRule({ - name: 'prefer-find', - meta: { - docs: { - description: - 'Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result', - recommended: 'stylistic', - requiresTypeChecking: true, - }, - messages: { - preferFind: 'Prefer .find(...) instead of .filter(...)[0].', - preferFindSuggestion: 'Use .find(...) instead of .filter(...)[0].', - }, - schema: [], - type: 'suggestion', - hasSuggestions: true, - }, - - defaultOptions: [], - create(context) { const globalScope = context.sourceCode.getScope(context.sourceCode.ast); const services = getParserServices(context); const checker = services.program.getTypeChecker(); interface FilterExpressionData { - isBracketSyntaxForFilter: boolean; filterNode: TSESTree.Node; + isBracketSyntaxForFilter: boolean; } function parseArrayFilterExpressions( @@ -102,8 +84,8 @@ export default createRule({ if (isArrayish(filteredObjectType)) { return [ { - isBracketSyntaxForFilter, filterNode, + isBracketSyntaxForFilter, }, ]; } @@ -251,11 +233,10 @@ export default createRule({ const filterExpressions = parseArrayFilterExpressions(object); if (filterExpressions.length !== 0) { context.report({ - node, messageId: 'preferFind', + node, suggest: [ { - messageId: 'preferFindSuggestion', fix: (fixer): TSESLint.RuleFix[] => { return [ ...filterExpressions.map(filterExpression => @@ -272,6 +253,7 @@ export default createRule({ ), ]; }, + messageId: 'preferFindSuggestion', }, ], }); @@ -291,11 +273,10 @@ export default createRule({ const filterExpressions = parseArrayFilterExpressions(object); if (filterExpressions.length !== 0) { context.report({ - node, messageId: 'preferFind', + node, suggest: [ { - messageId: 'preferFindSuggestion', fix: (fixer): TSESLint.RuleFix[] => { return [ ...filterExpressions.map(filterExpression => @@ -312,6 +293,7 @@ export default createRule({ ), ]; }, + messageId: 'preferFindSuggestion', }, ], }); @@ -320,6 +302,25 @@ export default createRule({ }, }; }, + defaultOptions: [], + + meta: { + docs: { + description: + 'Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result', + recommended: 'stylistic', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + preferFind: 'Prefer .find(...) instead of .filter(...)[0].', + preferFindSuggestion: 'Use .find(...) instead of .filter(...)[0].', + }, + schema: [], + type: 'suggestion', + }, + + name: 'prefer-find', }); /** diff --git a/packages/eslint-plugin/src/rules/prefer-for-of.ts b/packages/eslint-plugin/src/rules/prefer-for-of.ts index 18622a04e2ec..2f76b4f09328 100644 --- a/packages/eslint-plugin/src/rules/prefer-for-of.ts +++ b/packages/eslint-plugin/src/rules/prefer-for-of.ts @@ -1,24 +1,10 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, isAssignee } from '../util'; export default createRule({ - name: 'prefer-for-of', - meta: { - type: 'suggestion', - docs: { - description: - 'Enforce the use of `for-of` loop over the standard `for` loop where possible', - recommended: 'stylistic', - }, - messages: { - preferForOf: - 'Expected a `for-of` loop instead of a `for` loop with this simple iteration.', - }, - schema: [], - }, - defaultOptions: [], create(context) { function isSingleVariableDeclaration( node: TSESTree.Node | null, @@ -154,11 +140,26 @@ export default createRule({ isIndexOnlyUsedWithArray(node.body, indexVar, arrayExpression) ) { context.report({ - node, messageId: 'preferForOf', + node, }); } }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Enforce the use of `for-of` loop over the standard `for` loop where possible', + recommended: 'stylistic', + }, + messages: { + preferForOf: + 'Expected a `for-of` loop instead of a `for` loop with this simple iteration.', + }, + schema: [], + type: 'suggestion', + }, + name: 'prefer-for-of', }); diff --git a/packages/eslint-plugin/src/rules/prefer-function-type.ts b/packages/eslint-plugin/src/rules/prefer-function-type.ts index c7372c067f0b..04e1bd174db1 100644 --- a/packages/eslint-plugin/src/rules/prefer-function-type.ts +++ b/packages/eslint-plugin/src/rules/prefer-function-type.ts @@ -1,32 +1,15 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export const phrases = { - [AST_NODE_TYPES.TSTypeLiteral]: 'Type literal', [AST_NODE_TYPES.TSInterfaceDeclaration]: 'Interface', + [AST_NODE_TYPES.TSTypeLiteral]: 'Type literal', } as const; export default createRule({ - name: 'prefer-function-type', - meta: { - docs: { - description: - 'Enforce using function types instead of interfaces with call signatures', - recommended: 'stylistic', - }, - fixable: 'code', - messages: { - functionTypeOverCallableType: - '{{ literalOrInterface }} only has a call signature, you should use a function type instead.', - unexpectedThisOnFunctionOnlyInterface: - "`this` refers to the function type '{{ interfaceName }}', did you intend to use a generic `this` parameter like `(this: Self, ...) => Self` instead?", - }, - schema: [], - type: 'suggestion', - }, - defaultOptions: [], create(context) { /** * Checks if there the interface has exactly one supertype that isn't named 'Function' @@ -85,11 +68,11 @@ export default createRule({ // the message can be confusing if we don't point directly to the `this` node instead of the whole member // and in favour of generating at most one error we'll only report the first occurrence of `this` if there are multiple context.report({ - node: tsThisTypes[0], - messageId: 'unexpectedThisOnFunctionOnlyInterface', data: { interfaceName: node.id.name, }, + messageId: 'unexpectedThisOnFunctionOnlyInterface', + node: tsThisTypes[0], }); return; } @@ -176,12 +159,12 @@ export default createRule({ }; context.report({ - node: member, - messageId: 'functionTypeOverCallableType', data: { literalOrInterface: phrases[node.type], }, fix, + messageId: 'functionTypeOverCallableType', + node: member, }); } } @@ -221,4 +204,22 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Enforce using function types instead of interfaces with call signatures', + recommended: 'stylistic', + }, + fixable: 'code', + messages: { + functionTypeOverCallableType: + '{{ literalOrInterface }} only has a call signature, you should use a function type instead.', + unexpectedThisOnFunctionOnlyInterface: + "`this` refers to the function type '{{ interfaceName }}', did you intend to use a generic `this` parameter like `(this: Self, ...) => Self` instead?", + }, + schema: [], + type: 'suggestion', + }, + name: 'prefer-function-type', }); diff --git a/packages/eslint-plugin/src/rules/prefer-includes.ts b/packages/eslint-plugin/src/rules/prefer-includes.ts index 1a8f705b312f..ddb58e74123e 100644 --- a/packages/eslint-plugin/src/rules/prefer-includes.ts +++ b/packages/eslint-plugin/src/rules/prefer-includes.ts @@ -1,5 +1,6 @@ -import { parseRegExpLiteral } from '@eslint-community/regexpp'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +import { parseRegExpLiteral } from '@eslint-community/regexpp'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -11,25 +12,6 @@ import { } from '../util'; export default createRule({ - name: 'prefer-includes', - defaultOptions: [], - - meta: { - type: 'suggestion', - docs: { - description: 'Enforce `includes` method over `indexOf` method', - recommended: 'stylistic', - requiresTypeChecking: true, - }, - fixable: 'code', - messages: { - preferIncludes: "Use 'includes()' method instead.", - preferStringIncludes: - 'Use `String#includes()` method with a string instead.', - }, - schema: [], - }, - create(context) { const globalScope = context.sourceCode.getScope(context.sourceCode.ast); const services = getParserServices(context); @@ -102,7 +84,7 @@ export default createRule({ return null; } - const { pattern, flags } = parseRegExpLiteral(evaluated.value); + const { flags, pattern } = parseRegExpLiteral(evaluated.value); if ( pattern.alternatives.length !== 1 || flags.ignoreCase || @@ -124,13 +106,13 @@ export default createRule({ function escapeString(str: string): string { const EscapeMap = { '\0': '\\0', - "'": "\\'", - '\\': '\\\\', + '\t': '\\t', '\n': '\\n', - '\r': '\\r', '\v': '\\v', - '\t': '\\t', '\f': '\\f', + '\r': '\\r', + "'": "\\'", + '\\': '\\\\', // "\b" cause unexpected replacements // '\b': '\\b', }; @@ -188,8 +170,8 @@ export default createRule({ // Report it. context.report({ - node: compareNode, messageId: 'preferIncludes', + node: compareNode, ...(allowFixing && { *fix(fixer): Generator { if (negative) { @@ -219,7 +201,7 @@ export default createRule({ // /bar/.test(foo) 'CallExpression[arguments.length=1] > MemberExpression.callee[property.name="test"][computed=false]'( - node: TSESTree.MemberExpression & { parent: TSESTree.CallExpression }, + node: { parent: TSESTree.CallExpression } & TSESTree.MemberExpression, ): void { const callNode = node.parent; const text = parseRegExp(node.object); @@ -239,8 +221,6 @@ export default createRule({ } context.report({ - node: callNode, - messageId: 'preferStringIncludes', *fix(fixer) { const argNode = callNode.arguments[0]; const needsParen = @@ -261,8 +241,29 @@ export default createRule({ `${node.optional ? '?.' : '.'}includes('${escapeString(text)}')`, ); }, + messageId: 'preferStringIncludes', + node: callNode, }); }, }; }, + defaultOptions: [], + + meta: { + docs: { + description: 'Enforce `includes` method over `indexOf` method', + recommended: 'stylistic', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + preferIncludes: "Use 'includes()' method instead.", + preferStringIncludes: + 'Use `String#includes()` method with a string instead.', + }, + schema: [], + type: 'suggestion', + }, + + name: 'prefer-includes', }); diff --git a/packages/eslint-plugin/src/rules/prefer-literal-enum-member.ts b/packages/eslint-plugin/src/rules/prefer-literal-enum-member.ts index efc3eacdd518..e2ff6be8757c 100644 --- a/packages/eslint-plugin/src/rules/prefer-literal-enum-member.ts +++ b/packages/eslint-plugin/src/rules/prefer-literal-enum-member.ts @@ -1,37 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, getStaticStringValue } from '../util'; export default createRule({ - name: 'prefer-literal-enum-member', - meta: { - type: 'suggestion', - docs: { - description: 'Require all enum members to be literal values', - recommended: 'strict', - requiresTypeChecking: false, - }, - messages: { - notLiteral: `Explicit enum value must only be a literal value (string, number, boolean, etc).`, - }, - schema: [ - { - type: 'object', - properties: { - allowBitwiseExpressions: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - allowBitwiseExpressions: false, - }, - ], create(context, [{ allowBitwiseExpressions }]) { function isIdentifierWithName(node: TSESTree.Node, name: string): boolean { return node.type === AST_NODE_TYPES.Identifier && node.name === name; @@ -107,7 +80,7 @@ export default createRule({ if (node.initializer.type === AST_NODE_TYPES.UnaryExpression) { if ( node.initializer.argument.type === AST_NODE_TYPES.Literal && - ['+', '-'].includes(node.initializer.operator) + ['-', '+'].includes(node.initializer.operator) ) { return; } @@ -123,7 +96,7 @@ export default createRule({ if ( node.initializer.type === AST_NODE_TYPES.UnaryExpression && node.initializer.argument.type === AST_NODE_TYPES.Literal && - (['+', '-'].includes(node.initializer.operator) || + (['-', '+'].includes(node.initializer.operator) || (allowBitwiseExpressions && node.initializer.operator === '~')) ) { return; @@ -132,7 +105,7 @@ export default createRule({ if ( allowBitwiseExpressions && node.initializer.type === AST_NODE_TYPES.BinaryExpression && - ['|', '&', '^', '<<', '>>', '>>>'].includes( + ['&', '^', '<<', '>>', '>>>', '|'].includes( node.initializer.operator, ) && isAllowedBitwiseOperand(declaration, node.initializer.left) && @@ -142,10 +115,38 @@ export default createRule({ } context.report({ - node: node.id, messageId: 'notLiteral', + node: node.id, }); }, }; }, + defaultOptions: [ + { + allowBitwiseExpressions: false, + }, + ], + meta: { + docs: { + description: 'Require all enum members to be literal values', + recommended: 'strict', + requiresTypeChecking: false, + }, + messages: { + notLiteral: `Explicit enum value must only be a literal value (string, number, boolean, etc).`, + }, + schema: [ + { + additionalProperties: false, + properties: { + allowBitwiseExpressions: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'prefer-literal-enum-member', }); diff --git a/packages/eslint-plugin/src/rules/prefer-namespace-keyword.ts b/packages/eslint-plugin/src/rules/prefer-namespace-keyword.ts index 774c65e53771..5d20247caf5b 100644 --- a/packages/eslint-plugin/src/rules/prefer-namespace-keyword.ts +++ b/packages/eslint-plugin/src/rules/prefer-namespace-keyword.ts @@ -3,22 +3,6 @@ import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; export default createRule({ - name: 'prefer-namespace-keyword', - meta: { - type: 'suggestion', - docs: { - description: - 'Require using `namespace` keyword over `module` keyword to declare custom TypeScript modules', - recommended: 'recommended', - }, - fixable: 'code', - messages: { - useNamespace: - "Use 'namespace' instead of 'module' to declare custom TypeScript modules.", - }, - schema: [], - }, - defaultOptions: [], create(context) { return { TSModuleDeclaration(node): void { @@ -35,14 +19,30 @@ export default createRule({ moduleType.value === 'module' ) { context.report({ - node, - messageId: 'useNamespace', fix(fixer) { return fixer.replaceText(moduleType, 'namespace'); }, + messageId: 'useNamespace', + node, }); } }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Require using `namespace` keyword over `module` keyword to declare custom TypeScript modules', + recommended: 'recommended', + }, + fixable: 'code', + messages: { + useNamespace: + "Use 'namespace' instead of 'module' to declare custom TypeScript modules.", + }, + schema: [], + type: 'suggestion', + }, + name: 'prefer-namespace-keyword', }); diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 87a23ab98808..360e09f3b7a7 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -41,77 +42,6 @@ export type MessageIds = | 'suggestNullish'; export default createRule({ - name: 'prefer-nullish-coalescing', - meta: { - type: 'suggestion', - docs: { - description: - 'Enforce using the nullish coalescing operator instead of logical assignments or chaining', - recommended: 'stylistic', - requiresTypeChecking: true, - }, - hasSuggestions: true, - messages: { - preferNullishOverOr: - 'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.', - preferNullishOverTernary: - 'Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read.', - suggestNullish: 'Fix to nullish coalescing operator (`??`).', - noStrictNullCheck: - 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', - }, - schema: [ - { - type: 'object', - properties: { - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { - type: 'boolean', - }, - ignoreConditionalTests: { - type: 'boolean', - }, - ignoreMixedLogicalExpressions: { - type: 'boolean', - }, - ignorePrimitives: { - oneOf: [ - { - type: 'object', - properties: { - bigint: { type: 'boolean' }, - boolean: { type: 'boolean' }, - number: { type: 'boolean' }, - string: { type: 'boolean' }, - }, - }, - { - type: 'boolean', - enum: [true], - }, - ], - }, - ignoreTernaryTests: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, - ignoreConditionalTests: true, - ignoreTernaryTests: false, - ignoreMixedLogicalExpressions: false, - ignorePrimitives: { - bigint: false, - boolean: false, - number: false, - string: false, - }, - }, - ], create( context, [ @@ -139,8 +69,8 @@ export default createRule({ ) { context.report({ loc: { - start: { line: 0, column: 0 }, - end: { line: 0, column: 0 }, + end: { column: 0, line: 0 }, + start: { column: 0, line: 0 }, }, messageId: 'noStrictNullCheck', }); @@ -277,11 +207,10 @@ export default createRule({ if (isFixable) { context.report({ - node, messageId: 'preferNullishOverTernary', + node, suggest: [ { - messageId: 'suggestNullish', fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { const [left, right] = operator === '===' || operator === '==' @@ -295,6 +224,7 @@ export default createRule({ )}`, ); }, + messageId: 'suggestNullish', }, ], }); @@ -375,18 +305,89 @@ export default createRule({ } context.report({ - node: barBarOperator, messageId: 'preferNullishOverOr', + node: barBarOperator, suggest: [ { - messageId: 'suggestNullish', fix, + messageId: 'suggestNullish', }, ], }); }, }; }, + defaultOptions: [ + { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + ignoreConditionalTests: true, + ignoreMixedLogicalExpressions: false, + ignorePrimitives: { + bigint: false, + boolean: false, + number: false, + string: false, + }, + ignoreTernaryTests: false, + }, + ], + meta: { + docs: { + description: + 'Enforce using the nullish coalescing operator instead of logical assignments or chaining', + recommended: 'stylistic', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + noStrictNullCheck: + 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', + preferNullishOverOr: + 'Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.', + preferNullishOverTernary: + 'Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read.', + suggestNullish: 'Fix to nullish coalescing operator (`??`).', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { + type: 'boolean', + }, + ignoreConditionalTests: { + type: 'boolean', + }, + ignoreMixedLogicalExpressions: { + type: 'boolean', + }, + ignorePrimitives: { + oneOf: [ + { + properties: { + bigint: { type: 'boolean' }, + boolean: { type: 'boolean' }, + number: { type: 'boolean' }, + string: { type: 'boolean' }, + }, + type: 'object', + }, + { + enum: [true], + type: 'boolean', + }, + ], + }, + ignoreTernaryTests: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'prefer-nullish-coalescing', }); function isConditionalTest(node: TSESTree.Node): boolean { diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions.ts index b755c9463954..de1147b54447 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/PreferOptionalChainOptions.ts @@ -1,14 +1,14 @@ export type PreferOptionalChainMessageIds = - | 'preferOptionalChain' - | 'optionalChainSuggest'; + | 'optionalChainSuggest' + | 'preferOptionalChain'; export interface PreferOptionalChainOptions { + allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing?: boolean; checkAny?: boolean; - checkUnknown?: boolean; - checkString?: boolean; - checkNumber?: boolean; - checkBoolean?: boolean; checkBigInt?: boolean; + checkBoolean?: boolean; + checkNumber?: boolean; + checkString?: boolean; + checkUnknown?: boolean; requireNullish?: boolean; - allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing?: boolean; } diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts index 8a3358ad7172..cd4894cc384c 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts @@ -2,16 +2,23 @@ import type { ParserServicesWithTypeInformation, TSESTree, } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { ReportDescriptor, ReportFixFunction, RuleContext, SourceCode, } from '@typescript-eslint/utils/ts-eslint'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { unionTypeParts } from 'ts-api-utils'; import * as ts from 'typescript'; +import type { ValidOperand } from './gatherLogicalOperands'; +import type { + PreferOptionalChainMessageIds, + PreferOptionalChainOptions, +} from './PreferOptionalChainOptions'; + import { getFixOrSuggest, getOperatorPrecedenceForNode, @@ -24,12 +31,7 @@ import { } from '../../util'; import { checkNullishAndReport } from './checkNullishAndReport'; import { compareNodes, NodeComparisonResult } from './compareNodes'; -import type { ValidOperand } from './gatherLogicalOperands'; import { NullishComparisonType } from './gatherLogicalOperands'; -import type { - PreferOptionalChainMessageIds, - PreferOptionalChainOptions, -} from './PreferOptionalChainOptions'; function includesType( parserServices: ParserServicesWithTypeInformation, @@ -55,7 +57,7 @@ type OperandAnalyzer = ( operand: ValidOperand, index: number, chain: readonly ValidOperand[], -) => readonly [ValidOperand] | readonly [ValidOperand, ValidOperand] | null; +) => readonly [ValidOperand, ValidOperand] | readonly [ValidOperand] | null; const analyzeAndChainOperand: OperandAnalyzer = ( parserServices, operand, @@ -405,17 +407,17 @@ function getReportDescriptor( fixer.replaceTextRange(reportRange, newCode); return { - messageId: 'preferOptionalChain', loc: { - start: sourceCode.getLocFromIndex(reportRange[0]), end: sourceCode.getLocFromIndex(reportRange[1]), + start: sourceCode.getLocFromIndex(reportRange[0]), }, + messageId: 'preferOptionalChain', ...getFixOrSuggest({ - useFix: !useSuggestionFixer, suggestion: { - messageId: 'optionalChainSuggest', fix, + messageId: 'optionalChainSuggest', }, + useFix: !useSuggestionFixer, }), }; @@ -541,7 +543,7 @@ export function analyzeChain( // Things like x !== null && x !== undefined have two nodes, but they are // one logical unit here, so we'll allow them to be grouped. - let subChain: (ValidOperand | readonly ValidOperand[])[] = []; + let subChain: (readonly ValidOperand[] | ValidOperand)[] = []; const maybeReportThenReset = ( newChainSeed?: readonly [ValidOperand, ...ValidOperand[]], ): void => { diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts index 404b3736040a..158376eb7755 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/checkNullishAndReport.ts @@ -1,4 +1,3 @@ -import { isTypeFlagSet } from '@typescript-eslint/type-utils'; import type { ParserServicesWithTypeInformation, TSESTree, @@ -7,6 +6,8 @@ import type { ReportDescriptor, RuleContext, } from '@typescript-eslint/utils/ts-eslint'; + +import { isTypeFlagSet } from '@typescript-eslint/type-utils'; import { unionTypeParts } from 'ts-api-utils'; import * as ts from 'typescript'; diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/compareNodes.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/compareNodes.ts index d9dce486ec91..8cbc5469f0d7 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/compareNodes.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/compareNodes.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { visitorKeys } from '@typescript-eslint/visitor-keys'; diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts index 53beee5d2826..82b519fcd08a 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts @@ -2,8 +2,9 @@ import type { ParserServicesWithTypeInformation, TSESTree, } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { isBigIntLiteralType, isBooleanLiteralType, @@ -13,17 +14,18 @@ import { } from 'ts-api-utils'; import * as ts from 'typescript'; -import { isReferenceToGlobalFunction, isTypeFlagSet } from '../../util'; import type { PreferOptionalChainOptions } from './PreferOptionalChainOptions'; +import { isReferenceToGlobalFunction, isTypeFlagSet } from '../../util'; + const enum ComparisonValueType { Null = 'Null', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum Undefined = 'Undefined', UndefinedStringLiteral = 'UndefinedStringLiteral', } export const enum OperandValidity { - Valid = 'Valid', Invalid = 'Invalid', + Valid = 'Valid', } export const enum NullishComparisonType { /** `x != null`, `x != undefined` */ @@ -47,16 +49,16 @@ export const enum NullishComparisonType { Boolean = 'Boolean', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum } export interface ValidOperand { - type: OperandValidity.Valid; comparedName: TSESTree.Node; comparisonType: NullishComparisonType; isYoda: boolean; node: TSESTree.Expression; + type: OperandValidity.Valid; } export interface InvalidOperand { type: OperandValidity.Invalid; } -type Operand = ValidOperand | InvalidOperand; +type Operand = InvalidOperand | ValidOperand; const NULLISH_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined; function isValidFalseBooleanCheckType( @@ -118,11 +120,11 @@ export function gatherLogicalOperands( sourceCode: Readonly, options: PreferOptionalChainOptions, ): { - operands: Operand[]; newlySeenLogicals: Set; + operands: Operand[]; } { const result: Operand[] = []; - const { operands, newlySeenLogicals } = flattenLogicalOperands(node); + const { newlySeenLogicals, operands } = flattenLogicalOperands(node); for (const operand of operands) { const areMoreOperands = operand !== operands.at(-1); @@ -164,13 +166,13 @@ export function gatherLogicalOperands( // typeof x.y === 'undefined' result.push({ - type: OperandValidity.Valid, comparedName: comparedExpression.argument, comparisonType: operand.operator.startsWith('!') ? NullishComparisonType.NotStrictEqualUndefined : NullishComparisonType.StrictEqualUndefined, isYoda, node: operand, + type: OperandValidity.Valid, }); continue; } @@ -189,13 +191,13 @@ export function gatherLogicalOperands( ) { // x == null, x == undefined result.push({ - type: OperandValidity.Valid, comparedName: comparedExpression, comparisonType: operand.operator.startsWith('!') ? NullishComparisonType.NotEqualNullOrUndefined : NullishComparisonType.EqualNullOrUndefined, isYoda, node: operand, + type: OperandValidity.Valid, }); continue; } @@ -209,25 +211,25 @@ export function gatherLogicalOperands( switch (comparedValue) { case ComparisonValueType.Null: result.push({ - type: OperandValidity.Valid, comparedName, comparisonType: operand.operator.startsWith('!') ? NullishComparisonType.NotStrictEqualNull : NullishComparisonType.StrictEqualNull, isYoda, node: operand, + type: OperandValidity.Valid, }); continue; case ComparisonValueType.Undefined: result.push({ - type: OperandValidity.Valid, comparedName, comparisonType: operand.operator.startsWith('!') ? NullishComparisonType.NotStrictEqualUndefined : NullishComparisonType.StrictEqualUndefined, isYoda, node: operand, + type: OperandValidity.Valid, }); continue; @@ -254,11 +256,11 @@ export function gatherLogicalOperands( ) ) { result.push({ - type: OperandValidity.Valid, comparedName: operand.argument, comparisonType: NullishComparisonType.NotBoolean, isYoda: false, node: operand, + type: OperandValidity.Valid, }); continue; } @@ -280,11 +282,11 @@ export function gatherLogicalOperands( ) ) { result.push({ - type: OperandValidity.Valid, comparedName: operand, comparisonType: NullishComparisonType.Boolean, isYoda: false, node: operand, + type: OperandValidity.Valid, }); } else { result.push({ type: OperandValidity.Invalid }); @@ -294,8 +296,8 @@ export function gatherLogicalOperands( } return { - operands: result, newlySeenLogicals, + operands: result, }; /* @@ -320,8 +322,8 @@ export function gatherLogicalOperands( like `foo || foo.bar && foo.bar.baz` - separate selector */ function flattenLogicalOperands(node: TSESTree.LogicalExpression): { - operands: TSESTree.Expression[]; newlySeenLogicals: Set; + operands: TSESTree.Expression[]; } { const operands: TSESTree.Expression[] = []; const newlySeenLogicals = new Set([node]); @@ -342,8 +344,8 @@ export function gatherLogicalOperands( } return { - operands, newlySeenLogicals, + operands, }; } diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 1e979e47a4c8..8bbae4fe808f 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -1,8 +1,15 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { RuleFix } from '@typescript-eslint/utils/ts-eslint'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; +import type { ValidOperand } from './prefer-optional-chain-utils/gatherLogicalOperands'; +import type { + PreferOptionalChainMessageIds, + PreferOptionalChainOptions, +} from './prefer-optional-chain-utils/PreferOptionalChainOptions'; + import { createRule, getOperatorPrecedence, @@ -11,97 +18,15 @@ import { } from '../util'; import { analyzeChain } from './prefer-optional-chain-utils/analyzeChain'; import { checkNullishAndReport } from './prefer-optional-chain-utils/checkNullishAndReport'; -import type { ValidOperand } from './prefer-optional-chain-utils/gatherLogicalOperands'; import { gatherLogicalOperands, OperandValidity, } from './prefer-optional-chain-utils/gatherLogicalOperands'; -import type { - PreferOptionalChainMessageIds, - PreferOptionalChainOptions, -} from './prefer-optional-chain-utils/PreferOptionalChainOptions'; export default createRule< [PreferOptionalChainOptions], PreferOptionalChainMessageIds >({ - name: 'prefer-optional-chain', - meta: { - type: 'suggestion', - docs: { - description: - 'Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects', - recommended: 'stylistic', - requiresTypeChecking: true, - }, - fixable: 'code', - hasSuggestions: true, - messages: { - preferOptionalChain: - "Prefer using an optional chain expression instead, as it's more concise and easier to read.", - optionalChainSuggest: 'Change to an optional chain.', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - checkAny: { - type: 'boolean', - description: - 'Check operands that are typed as `any` when inspecting "loose boolean" operands.', - }, - checkUnknown: { - type: 'boolean', - description: - 'Check operands that are typed as `unknown` when inspecting "loose boolean" operands.', - }, - checkString: { - type: 'boolean', - description: - 'Check operands that are typed as `string` when inspecting "loose boolean" operands.', - }, - checkNumber: { - type: 'boolean', - description: - 'Check operands that are typed as `number` when inspecting "loose boolean" operands.', - }, - checkBoolean: { - type: 'boolean', - description: - 'Check operands that are typed as `boolean` when inspecting "loose boolean" operands.', - }, - checkBigInt: { - type: 'boolean', - description: - 'Check operands that are typed as `bigint` when inspecting "loose boolean" operands.', - }, - requireNullish: { - type: 'boolean', - description: - 'Skip operands that are not typed with `null` and/or `undefined` when inspecting "loose boolean" operands.', - }, - allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: { - type: 'boolean', - description: - 'Allow autofixers that will change the return type of the expression. This option is considered unsafe as it may break the build.', - }, - }, - }, - ], - }, - defaultOptions: [ - { - checkAny: true, - checkUnknown: true, - checkString: true, - checkNumber: true, - checkBoolean: true, - checkBigInt: true, - requireNullish: false, - allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: false, - }, - ], create(context, [options]) { const parserServices = getParserServices(context); @@ -109,6 +34,53 @@ export default createRule< return { // specific handling for `(foo ?? {}).bar` / `(foo || {}).bar` + 'LogicalExpression[operator!="??"]'( + node: TSESTree.LogicalExpression, + ): void { + if (seenLogicals.has(node)) { + return; + } + + const { newlySeenLogicals, operands } = gatherLogicalOperands( + node, + parserServices, + context.sourceCode, + options, + ); + for (const logical of newlySeenLogicals) { + seenLogicals.add(logical); + } + + let currentChain: ValidOperand[] = []; + for (const operand of operands) { + if (operand.type === OperandValidity.Invalid) { + analyzeChain( + context, + parserServices, + options, + node, + node.operator, + currentChain, + ); + currentChain = []; + } else { + currentChain.push(operand); + } + } + + // make sure to check whatever's left + if (currentChain.length > 0) { + analyzeChain( + context, + parserServices, + options, + node, + node.operator, + currentChain, + ); + } + }, + 'LogicalExpression[operator="||"], LogicalExpression[operator="??"]'( node: TSESTree.LogicalExpression, ): void { @@ -147,7 +119,6 @@ export default createRule< node: parentNode, suggest: [ { - messageId: 'optionalChainSuggest', fix: (fixer): RuleFix => { const leftNodeText = context.sourceCode.getText(leftNode); // Any node that is made of an operator with higher or equal precedence, @@ -165,57 +136,88 @@ export default createRule< `${maybeWrappedLeftNode}?.${maybeWrappedProperty}`, ); }, + messageId: 'optionalChainSuggest', }, ], }); }, - - 'LogicalExpression[operator!="??"]'( - node: TSESTree.LogicalExpression, - ): void { - if (seenLogicals.has(node)) { - return; - } - - const { operands, newlySeenLogicals } = gatherLogicalOperands( - node, - parserServices, - context.sourceCode, - options, - ); - for (const logical of newlySeenLogicals) { - seenLogicals.add(logical); - } - - let currentChain: ValidOperand[] = []; - for (const operand of operands) { - if (operand.type === OperandValidity.Invalid) { - analyzeChain( - context, - parserServices, - options, - node, - node.operator, - currentChain, - ); - currentChain = []; - } else { - currentChain.push(operand); - } - } - - // make sure to check whatever's left - if (currentChain.length > 0) { - analyzeChain( - context, - parserServices, - options, - node, - node.operator, - currentChain, - ); - } - }, }; }, + defaultOptions: [ + { + allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: false, + checkAny: true, + checkBigInt: true, + checkBoolean: true, + checkNumber: true, + checkString: true, + checkUnknown: true, + requireNullish: false, + }, + ], + meta: { + docs: { + description: + 'Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects', + recommended: 'stylistic', + requiresTypeChecking: true, + }, + fixable: 'code', + hasSuggestions: true, + messages: { + optionalChainSuggest: 'Change to an optional chain.', + preferOptionalChain: + "Prefer using an optional chain expression instead, as it's more concise and easier to read.", + }, + schema: [ + { + additionalProperties: false, + properties: { + allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: { + description: + 'Allow autofixers that will change the return type of the expression. This option is considered unsafe as it may break the build.', + type: 'boolean', + }, + checkAny: { + description: + 'Check operands that are typed as `any` when inspecting "loose boolean" operands.', + type: 'boolean', + }, + checkBigInt: { + description: + 'Check operands that are typed as `bigint` when inspecting "loose boolean" operands.', + type: 'boolean', + }, + checkBoolean: { + description: + 'Check operands that are typed as `boolean` when inspecting "loose boolean" operands.', + type: 'boolean', + }, + checkNumber: { + description: + 'Check operands that are typed as `number` when inspecting "loose boolean" operands.', + type: 'boolean', + }, + checkString: { + description: + 'Check operands that are typed as `string` when inspecting "loose boolean" operands.', + type: 'boolean', + }, + checkUnknown: { + description: + 'Check operands that are typed as `unknown` when inspecting "loose boolean" operands.', + type: 'boolean', + }, + requireNullish: { + description: + 'Skip operands that are not typed with `null` and/or `undefined` when inspecting "loose boolean" operands.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'prefer-optional-chain', }); diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index ef15fe918c17..6feb3eaea2f8 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { @@ -21,35 +22,6 @@ export type Options = [ ]; export default createRule({ - name: 'prefer-promise-reject-errors', - meta: { - type: 'suggestion', - docs: { - description: 'Require using Error objects as Promise rejection reasons', - recommended: 'recommended', - extendsBaseRule: true, - requiresTypeChecking: true, - }, - schema: [ - { - type: 'object', - properties: { - allowEmptyReject: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - messages: { - rejectAnError: 'Expected the Promise rejection reason to be an Error.', - }, - }, - defaultOptions: [ - { - allowEmptyReject: false, - }, - ], create(context, [options]) { const services = getParserServices(context); @@ -68,8 +40,8 @@ export default createRule({ } context.report({ - node: callExpression, messageId: 'rejectAnError', + node: callExpression, }); } @@ -150,4 +122,33 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowEmptyReject: false, + }, + ], + meta: { + docs: { + description: 'Require using Error objects as Promise rejection reasons', + extendsBaseRule: true, + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + rejectAnError: 'Expected the Promise rejection reason to be an Error.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowEmptyReject: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'prefer-promise-reject-errors', }); diff --git a/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts b/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts index 6cb935c1db9a..8909a250d1b4 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly-parameter-types.ts @@ -1,7 +1,9 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { TypeOrValueSpecifier } from '../util'; + import { createRule, getParserServices, @@ -21,44 +23,6 @@ type Options = [ type MessageIds = 'shouldBeReadonly'; export default createRule({ - name: 'prefer-readonly-parameter-types', - meta: { - type: 'suggestion', - docs: { - description: - 'Require function parameters to be typed as `readonly` to prevent accidental mutation of inputs', - requiresTypeChecking: true, - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - allow: readonlynessOptionsSchema.properties.allow, - checkParameterProperties: { - type: 'boolean', - }, - ignoreInferredTypes: { - type: 'boolean', - }, - treatMethodsAsReadonly: - readonlynessOptionsSchema.properties.treatMethodsAsReadonly, - }, - }, - ], - messages: { - shouldBeReadonly: 'Parameter should be a read only type.', - }, - }, - defaultOptions: [ - { - allow: readonlynessOptionsDefaults.allow, - checkParameterProperties: true, - ignoreInferredTypes: false, - treatMethodsAsReadonly: - readonlynessOptionsDefaults.treatMethodsAsReadonly, - }, - ], create( context, [ @@ -114,18 +78,56 @@ export default createRule({ const type = services.getTypeAtLocation(actualParam); const isReadOnly = isTypeReadonly(services.program, type, { - treatMethodsAsReadonly: !!treatMethodsAsReadonly, allow, + treatMethodsAsReadonly: !!treatMethodsAsReadonly, }); if (!isReadOnly) { context.report({ - node: actualParam, messageId: 'shouldBeReadonly', + node: actualParam, }); } } }, }; }, + defaultOptions: [ + { + allow: readonlynessOptionsDefaults.allow, + checkParameterProperties: true, + ignoreInferredTypes: false, + treatMethodsAsReadonly: + readonlynessOptionsDefaults.treatMethodsAsReadonly, + }, + ], + meta: { + docs: { + description: + 'Require function parameters to be typed as `readonly` to prevent accidental mutation of inputs', + requiresTypeChecking: true, + }, + messages: { + shouldBeReadonly: 'Parameter should be a read only type.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allow: readonlynessOptionsSchema.properties.allow, + checkParameterProperties: { + type: 'boolean', + }, + ignoreInferredTypes: { + type: 'boolean', + }, + treatMethodsAsReadonly: + readonlynessOptionsSchema.properties.treatMethodsAsReadonly, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'prefer-readonly-parameter-types', }); diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index f163b1aaab34..b347e0563b4d 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -29,32 +30,6 @@ const functionScopeBoundaries = [ ].join(', '); export default createRule({ - name: 'prefer-readonly', - meta: { - docs: { - description: - "Require private members to be marked as `readonly` if they're never modified outside of the constructor", - requiresTypeChecking: true, - }, - fixable: 'code', - messages: { - preferReadonly: - "Member '{{name}}' is never reassigned; mark it as `readonly`.", - }, - schema: [ - { - additionalProperties: false, - properties: { - onlyInlineLambdas: { - type: 'boolean', - }, - }, - type: 'object', - }, - ], - type: 'suggestion', - }, - defaultOptions: [{ onlyInlineLambdas: false }], create(context, [{ onlyInlineLambdas }]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -171,6 +146,19 @@ export default createRule({ } return { + [`${functionScopeBoundaries}:exit`]( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.MethodDefinition, + ): void { + if (ASTUtils.isConstructor(node)) { + classScopeStack[classScopeStack.length - 1].exitConstructor(); + } else if (isFunctionScopeBoundaryInStack(node)) { + classScopeStack[classScopeStack.length - 1].exitNonConstructor(); + } + }, 'ClassDeclaration, ClassExpression'( node: TSESTree.ClassDeclaration | TSESTree.ClassExpression, ): void { @@ -193,8 +181,8 @@ export default createRule({ getEsNodesFromViolatingNode(violatingNode); const reportNodeOrLoc: - | { node: TSESTree.Node } - | { loc: TSESTree.SourceLocation } = (() => { + | { loc: TSESTree.SourceLocation } + | { node: TSESTree.Node } = (() => { switch (esNode.type) { case AST_NODE_TYPES.MethodDefinition: case AST_NODE_TYPES.PropertyDefinition: @@ -223,18 +211,6 @@ export default createRule({ }); } }, - MemberExpression(node): void { - if (classScopeStack.length !== 0 && !node.computed) { - const tsNode = services.esTreeNodeToTSNodeMap.get( - node, - ) as ts.PropertyAccessExpression; - handlePropertyAccessExpression( - tsNode, - tsNode.parent, - classScopeStack[classScopeStack.length - 1], - ); - } - }, [functionScopeBoundaries]( node: | TSESTree.ArrowFunctionExpression @@ -250,21 +226,46 @@ export default createRule({ classScopeStack[classScopeStack.length - 1].enterNonConstructor(); } }, - [`${functionScopeBoundaries}:exit`]( - node: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.MethodDefinition, - ): void { - if (ASTUtils.isConstructor(node)) { - classScopeStack[classScopeStack.length - 1].exitConstructor(); - } else if (isFunctionScopeBoundaryInStack(node)) { - classScopeStack[classScopeStack.length - 1].exitNonConstructor(); + MemberExpression(node): void { + if (classScopeStack.length !== 0 && !node.computed) { + const tsNode = services.esTreeNodeToTSNodeMap.get( + node, + ) as ts.PropertyAccessExpression; + handlePropertyAccessExpression( + tsNode, + tsNode.parent, + classScopeStack[classScopeStack.length - 1], + ); } }, }; }, + defaultOptions: [{ onlyInlineLambdas: false }], + meta: { + docs: { + description: + "Require private members to be marked as `readonly` if they're never modified outside of the constructor", + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + preferReadonly: + "Member '{{name}}' is never reassigned; mark it as `readonly`.", + }, + schema: [ + { + additionalProperties: false, + properties: { + onlyInlineLambdas: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'prefer-readonly', }); type ParameterOrPropertyDeclaration = @@ -282,20 +283,20 @@ enum TypeToClassRelation { } class ClassScope { + private readonly classType: ts.Type; + private constructorScopeDepth = OUTSIDE_CONSTRUCTOR; + private readonly memberVariableModifications = new Set(); private readonly privateModifiableMembers = new Map< string, ParameterOrPropertyDeclaration >(); + private readonly privateModifiableStatics = new Map< string, ParameterOrPropertyDeclaration >(); - private readonly memberVariableModifications = new Set(); - private readonly staticVariableModifications = new Set(); - - private readonly classType: ts.Type; - private constructorScopeDepth = OUTSIDE_CONSTRUCTOR; + private readonly staticVariableModifications = new Set(); public constructor( private readonly checker: ts.TypeChecker, @@ -345,50 +346,6 @@ class ClassScope { ).set(node.name.getText(), node); } - public getTypeToClassRelation(type: ts.Type): TypeToClassRelation { - if (type.isIntersection()) { - let result: TypeToClassRelation = TypeToClassRelation.None; - for (const subType of type.types) { - const subTypeResult = this.getTypeToClassRelation(subType); - switch (subTypeResult) { - case TypeToClassRelation.Class: - if (result === TypeToClassRelation.Instance) { - return TypeToClassRelation.ClassAndInstance; - } - result = TypeToClassRelation.Class; - break; - case TypeToClassRelation.Instance: - if (result === TypeToClassRelation.Class) { - return TypeToClassRelation.ClassAndInstance; - } - result = TypeToClassRelation.Instance; - break; - } - } - return result; - } - if (type.isUnion()) { - // any union of class/instance and something else will prevent access to - // private members, so we assume that union consists only of classes - // or class instances, because otherwise tsc will report an error - return this.getTypeToClassRelation(type.types[0]); - } - - if (!type.getSymbol() || !typeIsOrHasBaseType(type, this.classType)) { - return TypeToClassRelation.None; - } - - const typeIsClass = - tsutils.isObjectType(type) && - tsutils.isObjectFlagSet(type, ts.ObjectFlags.Anonymous); - - if (typeIsClass) { - return TypeToClassRelation.Class; - } - - return TypeToClassRelation.Instance; - } - public addVariableModification(node: ts.PropertyAccessExpression): void { const modifierType = this.checker.getTypeAtLocation(node.expression); @@ -432,16 +389,16 @@ class ClassScope { } } - public exitConstructor(): void { - this.constructorScopeDepth = OUTSIDE_CONSTRUCTOR; - } - public enterNonConstructor(): void { if (this.constructorScopeDepth !== OUTSIDE_CONSTRUCTOR) { this.constructorScopeDepth += 1; } } + public exitConstructor(): void { + this.constructorScopeDepth = OUTSIDE_CONSTRUCTOR; + } + public exitNonConstructor(): void { if (this.constructorScopeDepth !== OUTSIDE_CONSTRUCTOR) { this.constructorScopeDepth -= 1; @@ -462,4 +419,48 @@ class ClassScope { ...Array.from(this.privateModifiableStatics.values()), ]; } + + public getTypeToClassRelation(type: ts.Type): TypeToClassRelation { + if (type.isIntersection()) { + let result: TypeToClassRelation = TypeToClassRelation.None; + for (const subType of type.types) { + const subTypeResult = this.getTypeToClassRelation(subType); + switch (subTypeResult) { + case TypeToClassRelation.Class: + if (result === TypeToClassRelation.Instance) { + return TypeToClassRelation.ClassAndInstance; + } + result = TypeToClassRelation.Class; + break; + case TypeToClassRelation.Instance: + if (result === TypeToClassRelation.Class) { + return TypeToClassRelation.ClassAndInstance; + } + result = TypeToClassRelation.Instance; + break; + } + } + return result; + } + if (type.isUnion()) { + // any union of class/instance and something else will prevent access to + // private members, so we assume that union consists only of classes + // or class instances, because otherwise tsc will report an error + return this.getTypeToClassRelation(type.types[0]); + } + + if (!type.getSymbol() || !typeIsOrHasBaseType(type, this.classType)) { + return TypeToClassRelation.None; + } + + const typeIsClass = + tsutils.isObjectType(type) && + tsutils.isObjectFlagSet(type, ts.ObjectFlags.Anonymous); + + if (typeIsClass) { + return TypeToClassRelation.Class; + } + + return TypeToClassRelation.Instance; + } } diff --git a/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts b/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts index 7eb781ed0d18..58e2ec5e384c 100644 --- a/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts +++ b/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts @@ -1,7 +1,8 @@ import type { TSESTree } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; -import type * as ts from 'typescript'; import { createRule, @@ -10,9 +11,9 @@ import { isTypeAssertion, } from '../util'; -type MemberExpressionWithCallExpressionParent = TSESTree.MemberExpression & { +type MemberExpressionWithCallExpressionParent = { parent: TSESTree.CallExpression; -}; +} & TSESTree.MemberExpression; const getMemberExpressionName = ( member: TSESTree.MemberExpression, @@ -32,23 +33,6 @@ const getMemberExpressionName = ( }; export default createRule({ - name: 'prefer-reduce-type-parameter', - meta: { - type: 'problem', - docs: { - description: - 'Enforce using type parameter when calling `Array#reduce` instead of casting', - recommended: 'strict', - requiresTypeChecking: true, - }, - messages: { - preferTypeParameter: - 'Unnecessary cast: Array#reduce accepts a type parameter for the default value.', - }, - fixable: 'code', - schema: [], - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -86,8 +70,6 @@ export default createRule({ // Check the owner type of the `reduce` method. if (isArrayType(calleeObjType)) { context.report({ - messageId: 'preferTypeParameter', - node: secondArg, fix: fixer => { const fixes = [ fixer.removeRange([ @@ -111,6 +93,8 @@ export default createRule({ return fixes; }, + messageId: 'preferTypeParameter', + node: secondArg, }); return; @@ -118,4 +102,21 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Enforce using type parameter when calling `Array#reduce` instead of casting', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + preferTypeParameter: + 'Unnecessary cast: Array#reduce accepts a type parameter for the default value.', + }, + schema: [], + type: 'problem', + }, + name: 'prefer-reduce-type-parameter', }); diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index b19e0305c7bb..921308ab1c8e 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -1,7 +1,8 @@ import type { TSESTree } from '@typescript-eslint/utils'; +import type * as ts from 'typescript'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; -import type * as ts from 'typescript'; import { createRule, @@ -12,31 +13,13 @@ import { } from '../util'; enum ArgumentType { + Both = String | RegExp, Other = 0, - String = 1 << 0, RegExp = 1 << 1, - Both = String | RegExp, + String = 1 << 0, } export default createRule({ - name: 'prefer-regexp-exec', - defaultOptions: [], - - meta: { - type: 'suggestion', - fixable: 'code', - docs: { - description: - 'Enforce `RegExp#exec` over `String#match` if no global flag is provided', - recommended: 'stylistic', - requiresTypeChecking: true, - }, - messages: { - regExpExecOverStringMatch: 'Use the `RegExp#exec()` method instead.', - }, - schema: [], - }, - create(context) { const globalScope = context.sourceCode.getScope(context.sourceCode.ast); const services = getParserServices(context); @@ -132,14 +115,14 @@ export default createRule({ return; } return context.report({ - node: memberNode.property, - messageId: 'regExpExecOverStringMatch', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: callNode, innerNode: [objectNode], + node: callNode, + sourceCode: context.sourceCode, wrap: objectCode => `${regExp.toString()}.exec(${objectCode})`, }), + messageId: 'regExpExecOverStringMatch', + node: memberNode.property, }); } @@ -150,31 +133,49 @@ export default createRule({ switch (argumentTypes) { case ArgumentType.RegExp: return context.report({ - node: memberNode.property, - messageId: 'regExpExecOverStringMatch', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: callNode, innerNode: [objectNode, argumentNode], + node: callNode, + sourceCode: context.sourceCode, wrap: (objectCode, argumentCode) => `${argumentCode}.exec(${objectCode})`, }), + messageId: 'regExpExecOverStringMatch', + node: memberNode.property, }); case ArgumentType.String: return context.report({ - node: memberNode.property, - messageId: 'regExpExecOverStringMatch', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: callNode, innerNode: [objectNode, argumentNode], + node: callNode, + sourceCode: context.sourceCode, wrap: (objectCode, argumentCode) => `RegExp(${argumentCode}).exec(${objectCode})`, }), + messageId: 'regExpExecOverStringMatch', + node: memberNode.property, }); } }, }; }, + defaultOptions: [], + + meta: { + docs: { + description: + 'Enforce `RegExp#exec` over `String#match` if no global flag is provided', + recommended: 'stylistic', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + regExpExecOverStringMatch: 'Use the `RegExp#exec()` method instead.', + }, + schema: [], + type: 'suggestion', + }, + + name: 'prefer-regexp-exec', }); diff --git a/packages/eslint-plugin/src/rules/prefer-return-this-type.ts b/packages/eslint-plugin/src/rules/prefer-return-this-type.ts index 80e55427078d..4e1099a7fe42 100644 --- a/packages/eslint-plugin/src/rules/prefer-return-this-type.ts +++ b/packages/eslint-plugin/src/rules/prefer-return-this-type.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -13,24 +14,6 @@ type FunctionLike = | TSESTree.MethodDefinition['value']; export default createRule({ - name: 'prefer-return-this-type', - defaultOptions: [], - - meta: { - type: 'suggestion', - docs: { - description: - 'Enforce that `this` is used when only `this` type is returned', - recommended: 'strict', - requiresTypeChecking: true, - }, - messages: { - useThisType: 'Use `this` type instead.', - }, - schema: [], - fixable: 'code', - }, - create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -140,9 +123,9 @@ export default createRule({ if (isFunctionReturningThis(originalFunc, originalClass)) { context.report({ - node, - messageId: 'useThisType', fix: fixer => fixer.replaceText(node, 'this'), + messageId: 'useThisType', + node, }); } } @@ -167,4 +150,22 @@ export default createRule({ }, }; }, + defaultOptions: [], + + meta: { + docs: { + description: + 'Enforce that `this` is used when only `this` type is returned', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + useThisType: 'Use `this` type instead.', + }, + schema: [], + type: 'suggestion', + }, + + name: 'prefer-return-this-type', }); diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index f6a22152043c..72ed3ae365f5 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -1,5 +1,6 @@ -import { RegExpParser } from '@eslint-community/regexpp'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +import { RegExpParser } from '@eslint-community/regexpp'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { @@ -27,38 +28,6 @@ export type Options = [ type MessageIds = 'preferEndsWith' | 'preferStartsWith'; export default createRule({ - name: 'prefer-string-starts-ends-with', - defaultOptions: [{ allowSingleElementEquality: 'never' }], - - meta: { - type: 'suggestion', - docs: { - description: - 'Enforce using `String#startsWith` and `String#endsWith` over other equivalent methods of checking substrings', - recommended: 'stylistic', - requiresTypeChecking: true, - }, - messages: { - preferStartsWith: "Use 'String#startsWith' method instead.", - preferEndsWith: "Use the 'String#endsWith' method instead.", - }, - schema: [ - { - additionalProperties: false, - properties: { - allowSingleElementEquality: { - description: - 'Whether to allow equality checks against the first or last element of a string.', - enum: ['always', 'never'], - type: 'string', - }, - }, - type: 'object', - }, - ], - fixable: 'code', - }, - create(context, [{ allowSingleElementEquality }]) { const globalScope = context.sourceCode.getScope(context.sourceCode.ast); @@ -278,13 +247,13 @@ export default createRule({ */ function parseRegExp( node: TSESTree.Node, - ): { isStartsWith: boolean; isEndsWith: boolean; text: string } | null { + ): { isEndsWith: boolean; isStartsWith: boolean; text: string } | null { const evaluated = getStaticValue(node, globalScope); if (evaluated == null || !(evaluated.value instanceof RegExp)) { return null; } - const { source, flags } = evaluated.value; + const { flags, source } = evaluated.value; const isStartsWith = source.startsWith('^'); const isEndsWith = source.endsWith('$'); if ( @@ -435,8 +404,6 @@ export default createRule({ const eqNode = parentNode; context.report({ - node: parentNode, - messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', fix(fixer) { // Don't fix if it can change the behavior. if (!isCharacter(eqNode.right)) { @@ -450,6 +417,8 @@ export default createRule({ node.optional, ); }, + messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', + node: parentNode, }); }, @@ -471,8 +440,6 @@ export default createRule({ } context.report({ - node: parentNode, - messageId: 'preferStartsWith', fix(fixer) { return fixWithArgument( fixer, @@ -484,6 +451,8 @@ export default createRule({ node.optional, ); }, + messageId: 'preferStartsWith', + node: parentNode, }); }, @@ -509,8 +478,6 @@ export default createRule({ } context.report({ - node: parentNode, - messageId: 'preferEndsWith', fix(fixer) { return fixWithArgument( fixer, @@ -522,6 +489,8 @@ export default createRule({ node.optional, ); }, + messageId: 'preferEndsWith', + node: parentNode, }); }, @@ -552,8 +521,6 @@ export default createRule({ const { isStartsWith, text } = parsed; context.report({ - node: callNode, - messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', *fix(fixer) { if (!parentNode.operator.startsWith('!')) { yield fixer.insertTextBefore(parentNode, '!'); @@ -570,6 +537,8 @@ export default createRule({ ); yield fixer.removeRange([callNode.range[1], parentNode.range[1]]); }, + messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', + node: callNode, }); }, @@ -638,8 +607,6 @@ export default createRule({ const negativeIndexSupported = (node.property as TSESTree.Identifier).name === 'slice'; context.report({ - node: parentNode, - messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', fix(fixer) { // Don't fix if it can change the behavior. if ( @@ -680,6 +647,8 @@ export default createRule({ node.optional, ); }, + messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', + node: parentNode, }); }, @@ -699,8 +668,6 @@ export default createRule({ const messageId = isStartsWith ? 'preferStartsWith' : 'preferEndsWith'; const methodName = isStartsWith ? 'startsWith' : 'endsWith'; context.report({ - node: callNode, - messageId, *fix(fixer) { const argNode = callNode.arguments[0]; const needsParen = @@ -722,8 +689,42 @@ export default createRule({ )}`, ); }, + messageId, + node: callNode, }); }, }; }, + defaultOptions: [{ allowSingleElementEquality: 'never' }], + + meta: { + docs: { + description: + 'Enforce using `String#startsWith` and `String#endsWith` over other equivalent methods of checking substrings', + recommended: 'stylistic', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + preferEndsWith: "Use the 'String#endsWith' method instead.", + preferStartsWith: "Use 'String#startsWith' method instead.", + }, + schema: [ + { + additionalProperties: false, + properties: { + allowSingleElementEquality: { + description: + 'Whether to allow equality checks against the first or last element of a string.', + enum: ['always', 'never'], + type: 'string', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + + name: 'prefer-string-starts-ends-with', }); diff --git a/packages/eslint-plugin/src/rules/prefer-ts-expect-error.ts b/packages/eslint-plugin/src/rules/prefer-ts-expect-error.ts index 6ae1f11720e1..836719a2b725 100644 --- a/packages/eslint-plugin/src/rules/prefer-ts-expect-error.ts +++ b/packages/eslint-plugin/src/rules/prefer-ts-expect-error.ts @@ -1,28 +1,13 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import type { RuleFix, RuleFixer } from '@typescript-eslint/utils/ts-eslint'; +import { AST_TOKEN_TYPES } from '@typescript-eslint/utils'; + import { createRule } from '../util'; type MessageIds = 'preferExpectErrorComment'; export default createRule<[], MessageIds>({ - name: 'prefer-ts-expect-error', - meta: { - type: 'problem', - deprecated: true, - replacedBy: ['@typescript-eslint/ban-ts-comment'], - docs: { - description: 'Enforce using `@ts-expect-error` over `@ts-ignore`', - }, - fixable: 'code', - messages: { - preferExpectErrorComment: - 'Use "@ts-expect-error" to ensure an error is actually being suppressed.', - }, - schema: [], - }, - defaultOptions: [], create(context) { const tsIgnoreRegExpSingleLine = /^\s*\/?\s*@ts-ignore/; const tsIgnoreRegExpMultiLine = /^\s*(?:\/|\*)*\s*@ts-ignore/; @@ -69,15 +54,31 @@ export default createRule<[], MessageIds>({ ); context.report({ - node: comment, - messageId: 'preferExpectErrorComment', fix: isLineComment(comment) ? lineCommentRuleFixer : blockCommentRuleFixer, + messageId: 'preferExpectErrorComment', + node: comment, }); } }); }, }; }, + defaultOptions: [], + meta: { + deprecated: true, + docs: { + description: 'Enforce using `@ts-expect-error` over `@ts-ignore`', + }, + fixable: 'code', + messages: { + preferExpectErrorComment: + 'Use "@ts-expect-error" to ensure an error is actually being suppressed.', + }, + replacedBy: ['@typescript-eslint/ban-ts-comment'], + schema: [], + type: 'problem', + }, + name: 'prefer-ts-expect-error', }); diff --git a/packages/eslint-plugin/src/rules/promise-function-async.ts b/packages/eslint-plugin/src/rules/promise-function-async.ts index c191ce45b0db..8642d77063e7 100644 --- a/packages/eslint-plugin/src/rules/promise-function-async.ts +++ b/packages/eslint-plugin/src/rules/promise-function-async.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -25,62 +26,6 @@ type Options = [ type MessageIds = 'missingAsync'; export default createRule({ - name: 'promise-function-async', - meta: { - type: 'suggestion', - fixable: 'code', - docs: { - description: - 'Require any function or method that returns a Promise to be marked async', - requiresTypeChecking: true, - }, - messages: { - missingAsync: 'Functions that return promises must be async.', - }, - schema: [ - { - type: 'object', - properties: { - allowAny: { - description: - 'Whether to consider `any` and `unknown` to be Promises.', - type: 'boolean', - }, - allowedPromiseNames: { - description: - 'Any extra names of classes or interfaces to be considered Promises.', - type: 'array', - items: { - type: 'string', - }, - }, - checkArrowFunctions: { - type: 'boolean', - }, - checkFunctionDeclarations: { - type: 'boolean', - }, - checkFunctionExpressions: { - type: 'boolean', - }, - checkMethodDeclarations: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - allowAny: true, - allowedPromiseNames: [], - checkArrowFunctions: true, - checkFunctionDeclarations: true, - checkFunctionExpressions: true, - checkMethodDeclarations: true, - }, - ], create( context, [ @@ -146,16 +91,13 @@ export default createRule({ if (isTypeFlagSet(returnType, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { // Report without auto fixer because the return type is unknown return context.report({ + loc: getFunctionHeadLoc(node, context.sourceCode), messageId: 'missingAsync', node, - loc: getFunctionHeadLoc(node, context.sourceCode), }); } context.report({ - messageId: 'missingAsync', - node, - loc: getFunctionHeadLoc(node, context.sourceCode), fix: fixer => { if ( node.parent.type === AST_NODE_TYPES.MethodDefinition || @@ -212,6 +154,9 @@ export default createRule({ return fixer.insertTextBefore(node, 'async '); }, + loc: getFunctionHeadLoc(node, context.sourceCode), + messageId: 'missingAsync', + node, }); } @@ -248,4 +193,60 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowAny: true, + allowedPromiseNames: [], + checkArrowFunctions: true, + checkFunctionDeclarations: true, + checkFunctionExpressions: true, + checkMethodDeclarations: true, + }, + ], + meta: { + docs: { + description: + 'Require any function or method that returns a Promise to be marked async', + requiresTypeChecking: true, + }, + fixable: 'code', + messages: { + missingAsync: 'Functions that return promises must be async.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowAny: { + description: + 'Whether to consider `any` and `unknown` to be Promises.', + type: 'boolean', + }, + allowedPromiseNames: { + description: + 'Any extra names of classes or interfaces to be considered Promises.', + items: { + type: 'string', + }, + type: 'array', + }, + checkArrowFunctions: { + type: 'boolean', + }, + checkFunctionDeclarations: { + type: 'boolean', + }, + checkFunctionExpressions: { + type: 'boolean', + }, + checkMethodDeclarations: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'promise-function-async', }); diff --git a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts index e6f83e800bbb..dea7e1009985 100644 --- a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts +++ b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts @@ -16,38 +16,6 @@ export type Options = [ export type MessageIds = 'requireCompare'; export default createRule({ - name: 'require-array-sort-compare', - defaultOptions: [ - { - ignoreStringArrays: true, - }, - ], - - meta: { - type: 'problem', - docs: { - description: - 'Require `Array#sort` and `Array#toSorted` calls to always provide a `compareFunction`', - requiresTypeChecking: true, - }, - messages: { - requireCompare: "Require 'compare' argument.", - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - ignoreStringArrays: { - description: - 'Whether to ignore arrays in which all elements are strings.', - type: 'boolean', - }, - }, - }, - ], - }, - create(context, [options]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -76,7 +44,7 @@ export default createRule({ } if (isTypeArrayTypeOrUnionOfArrayTypes(calleeObjType, checker)) { - context.report({ node: callee.parent, messageId: 'requireCompare' }); + context.report({ messageId: 'requireCompare', node: callee.parent }); } } @@ -87,4 +55,36 @@ export default createRule({ checkSortArgument, }; }, + defaultOptions: [ + { + ignoreStringArrays: true, + }, + ], + + meta: { + docs: { + description: + 'Require `Array#sort` and `Array#toSorted` calls to always provide a `compareFunction`', + requiresTypeChecking: true, + }, + messages: { + requireCompare: "Require 'compare' argument.", + }, + schema: [ + { + additionalProperties: false, + properties: { + ignoreStringArrays: { + description: + 'Whether to ignore arrays in which all elements are strings.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + + name: 'require-array-sort-compare', }); diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index e08b4a895a43..33af3017a3a4 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -1,9 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import type { AST, RuleFix } from '@typescript-eslint/utils/ts-eslint'; -import * as tsutils from 'ts-api-utils'; import type * as ts from 'typescript'; +import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; + import { createRule, getFunctionHeadLoc, @@ -16,11 +17,11 @@ import { } from '../util'; interface ScopeInfo { - upper: ScopeInfo | null; - hasAwait: boolean; hasAsync: boolean; - isGen: boolean; + hasAwait: boolean; isAsyncYield: boolean; + isGen: boolean; + upper: ScopeInfo | null; } type FunctionNode = | TSESTree.ArrowFunctionExpression @@ -28,24 +29,6 @@ type FunctionNode = | TSESTree.FunctionExpression; export default createRule({ - name: 'require-await', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow async functions which do not return promises and have no `await` expression', - recommended: 'recommended', - requiresTypeChecking: true, - extendsBaseRule: true, - }, - schema: [], - messages: { - missingAwait: "{{name}} has no 'await' expression.", - removeAsync: "Remove 'async'.", - }, - hasSuggestions: true, - }, - defaultOptions: [], create(context) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -57,11 +40,11 @@ export default createRule({ */ function enterFunction(node: FunctionNode): void { scopeInfo = { - upper: scopeInfo, - hasAwait: false, hasAsync: node.async, - isGen: node.generator || false, + hasAwait: false, isAsyncYield: false, + isGen: node.generator || false, + upper: scopeInfo, }; } @@ -186,21 +169,21 @@ export default createRule({ } context.report({ - node, - loc: getFunctionHeadLoc(node, context.sourceCode), - messageId: 'missingAwait', data: { name: upperCaseFirst(getFunctionNameWithKind(node)), }, + loc: getFunctionHeadLoc(node, context.sourceCode), + messageId: 'missingAwait', + node, suggest: [ { - messageId: 'removeAsync', fix: (fixer): RuleFix[] => changes.map(change => change.replacement !== undefined ? fixer.replaceTextRange(change.range, change.replacement) : fixer.removeRange(change.range), ), + messageId: 'removeAsync', }, ], }); @@ -268,16 +251,16 @@ export default createRule({ } return { - FunctionDeclaration: enterFunction, - FunctionExpression: enterFunction, ArrowFunctionExpression: enterFunction, - 'FunctionDeclaration:exit': exitFunction, - 'FunctionExpression:exit': exitFunction, 'ArrowFunctionExpression:exit': exitFunction, - AwaitExpression: markAsHasAwait, - 'VariableDeclaration[kind = "await using"]': markAsHasAwait, 'ForOfStatement[await = true]': markAsHasAwait, + FunctionDeclaration: enterFunction, + 'FunctionDeclaration:exit': exitFunction, + + FunctionExpression: enterFunction, + 'FunctionExpression:exit': exitFunction, + 'VariableDeclaration[kind = "await using"]': markAsHasAwait, YieldExpression: visitYieldExpression, // check body-less async arrow function. @@ -306,6 +289,24 @@ export default createRule({ }, }; }, + defaultOptions: [], + meta: { + docs: { + description: + 'Disallow async functions which do not return promises and have no `await` expression', + extendsBaseRule: true, + recommended: 'recommended', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + missingAwait: "{{name}} has no 'await' expression.", + removeAsync: "Remove 'async'.", + }, + schema: [], + type: 'suggestion', + }, + name: 'require-await', }); function isEmptyFunction(node: FunctionNode): boolean { diff --git a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts index 1953c15c70cb..f60ebde2fce6 100644 --- a/packages/eslint-plugin/src/rules/restrict-plus-operands.ts +++ b/packages/eslint-plugin/src/rules/restrict-plus-operands.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -25,79 +26,6 @@ type Options = [ type MessageIds = 'bigintAndNumber' | 'invalid' | 'mismatched'; export default createRule({ - name: 'restrict-plus-operands', - meta: { - type: 'problem', - docs: { - description: - 'Require both operands of addition to be the same type and be `bigint`, `number`, or `string`', - recommended: { - recommended: true, - strict: [ - { - allowAny: false, - allowBoolean: false, - allowNullish: false, - allowNumberAndString: false, - allowRegExp: false, - }, - ], - }, - requiresTypeChecking: true, - }, - messages: { - bigintAndNumber: - "Numeric '+' operations must either be both bigints or both numbers. Got `{{left}}` + `{{right}}`.", - invalid: - "Invalid operand for a '+' operation. Operands must each be a number or {{stringLike}}. Got `{{type}}`.", - mismatched: - "Operands of '+' operations must be a number or {{stringLike}}. Got `{{left}}` + `{{right}}`.", - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - allowAny: { - description: 'Whether to allow `any` typed values.', - type: 'boolean', - }, - allowBoolean: { - description: 'Whether to allow `boolean` typed values.', - type: 'boolean', - }, - allowNullish: { - description: - 'Whether to allow potentially `null` or `undefined` typed values.', - type: 'boolean', - }, - allowNumberAndString: { - description: - 'Whether to allow `bigint`/`number` typed values and `string` typed values to be added together.', - type: 'boolean', - }, - allowRegExp: { - description: 'Whether to allow `regexp` typed values.', - type: 'boolean', - }, - skipCompoundAssignments: { - description: 'Whether to skip compound assignments such as `+=`.', - type: 'boolean', - }, - }, - }, - ], - }, - defaultOptions: [ - { - allowAny: true, - allowBoolean: true, - allowNullish: true, - allowNumberAndString: true, - allowRegExp: true, - skipCompoundAssignments: false, - }, - ], create( context, [ @@ -221,9 +149,9 @@ export default createRule({ ) { return context.report({ data: { - stringLike, left: typeChecker.typeToString(leftType), right: typeChecker.typeToString(rightType), + stringLike, }, messageId: 'mismatched', node, @@ -255,6 +183,79 @@ export default createRule({ }), }; }, + defaultOptions: [ + { + allowAny: true, + allowBoolean: true, + allowNullish: true, + allowNumberAndString: true, + allowRegExp: true, + skipCompoundAssignments: false, + }, + ], + meta: { + docs: { + description: + 'Require both operands of addition to be the same type and be `bigint`, `number`, or `string`', + recommended: { + recommended: true, + strict: [ + { + allowAny: false, + allowBoolean: false, + allowNullish: false, + allowNumberAndString: false, + allowRegExp: false, + }, + ], + }, + requiresTypeChecking: true, + }, + messages: { + bigintAndNumber: + "Numeric '+' operations must either be both bigints or both numbers. Got `{{left}}` + `{{right}}`.", + invalid: + "Invalid operand for a '+' operation. Operands must each be a number or {{stringLike}}. Got `{{type}}`.", + mismatched: + "Operands of '+' operations must be a number or {{stringLike}}. Got `{{left}}` + `{{right}}`.", + }, + schema: [ + { + additionalProperties: false, + properties: { + allowAny: { + description: 'Whether to allow `any` typed values.', + type: 'boolean', + }, + allowBoolean: { + description: 'Whether to allow `boolean` typed values.', + type: 'boolean', + }, + allowNullish: { + description: + 'Whether to allow potentially `null` or `undefined` typed values.', + type: 'boolean', + }, + allowNumberAndString: { + description: + 'Whether to allow `bigint`/`number` typed values and `string` typed values to be added together.', + type: 'boolean', + }, + allowRegExp: { + description: 'Whether to allow `regexp` typed values.', + type: 'boolean', + }, + skipCompoundAssignments: { + description: 'Whether to skip compound assignments such as `+=`.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'restrict-plus-operands', }); function isDeeplyObjectType(type: ts.Type): boolean { diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index 5aa90084ac63..c733d4f5b9fc 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -1,6 +1,7 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { Type, TypeChecker } from 'typescript'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { TypeFlags } from 'typescript'; import { @@ -45,9 +46,9 @@ const optionTesters = ( ['Never', isTypeNeverType], ] satisfies [string, OptionTester][] ).map(([type, tester]) => ({ - type, option: `allow${type}` as const, tester, + type, })); type Options = [ { [Type in (typeof optionTesters)[number]['option']]?: boolean }, @@ -56,55 +57,6 @@ type Options = [ type MessageId = 'invalidType'; export default createRule({ - name: 'restrict-template-expressions', - meta: { - type: 'problem', - docs: { - description: - 'Enforce template literal expressions to be of `string` type', - recommended: { - recommended: true, - strict: [ - { - allowAny: false, - allowBoolean: false, - allowNullish: false, - allowNumber: false, - allowRegExp: false, - allowNever: false, - }, - ], - }, - requiresTypeChecking: true, - }, - messages: { - invalidType: 'Invalid type "{{type}}" of template literal expression.', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: Object.fromEntries( - optionTesters.map(({ option, type }) => [ - option, - { - description: `Whether to allow \`${type.toLowerCase()}\` typed values in template expressions.`, - type: 'boolean', - }, - ]), - ), - }, - ], - }, - defaultOptions: [ - { - allowAny: true, - allowBoolean: true, - allowNullish: true, - allowNumber: true, - allowRegExp: true, - }, - ], create(context, [options]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -127,9 +79,9 @@ export default createRule({ if (!recursivelyCheckType(expressionType)) { context.report({ - node: expression, - messageId: 'invalidType', data: { type: checker.typeToString(expressionType) }, + messageId: 'invalidType', + node: expression, }); } } @@ -153,4 +105,53 @@ export default createRule({ ); } }, + defaultOptions: [ + { + allowAny: true, + allowBoolean: true, + allowNullish: true, + allowNumber: true, + allowRegExp: true, + }, + ], + meta: { + docs: { + description: + 'Enforce template literal expressions to be of `string` type', + recommended: { + recommended: true, + strict: [ + { + allowAny: false, + allowBoolean: false, + allowNever: false, + allowNullish: false, + allowNumber: false, + allowRegExp: false, + }, + ], + }, + requiresTypeChecking: true, + }, + messages: { + invalidType: 'Invalid type "{{type}}" of template literal expression.', + }, + schema: [ + { + additionalProperties: false, + properties: Object.fromEntries( + optionTesters.map(({ option, type }) => [ + option, + { + description: `Whether to allow \`${type.toLowerCase()}\` typed values in template expressions.`, + type: 'boolean', + }, + ]), + ), + type: 'object', + }, + ], + type: 'problem', + }, + name: 'restrict-template-expressions', }); diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index 716962a8a11b..1562a58a80d9 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -25,51 +26,12 @@ interface ScopeInfo { } type Option = - | 'in-try-catch' | 'always' - | 'never' - | 'error-handling-correctness-only'; + | 'error-handling-correctness-only' + | 'in-try-catch' + | 'never'; export default createRule({ - name: 'return-await', - meta: { - docs: { - description: 'Enforce consistent awaiting of returned promises', - requiresTypeChecking: true, - extendsBaseRule: 'no-return-await', - recommended: { - strict: ['error-handling-correctness-only'], - }, - }, - fixable: 'code', - hasSuggestions: true, - type: 'problem', - messages: { - nonPromiseAwait: - 'Returning an awaited value that is not a promise is not allowed.', - disallowedPromiseAwait: - 'Returning an awaited promise is not allowed in this context.', - requiredPromiseAwait: - 'Returning an awaited promise is required in this context.', - requiredPromiseAwaitSuggestion: - 'Add `await` before the expression. Use caution as this may impact control flow.', - disallowedPromiseAwaitSuggestion: - 'Remove `await` before the expression. Use caution as this may impact control flow.', - }, - schema: [ - { - type: 'string', - enum: [ - 'in-try-catch', - 'always', - 'never', - 'error-handling-correctness-only', - ] satisfies Option[], - }, - ], - }, - defaultOptions: ['in-try-catch'], - create(context, [option]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -108,7 +70,7 @@ export default createRule({ // if it's a using/await using declaration, and it comes _before_ the // node we're checking, it affects control flow for that node. if ( - ['using', 'await using'].includes(declarationNode.kind) && + ['await using', 'using'].includes(declarationNode.kind) && declaratorNode.range[1] < node.range[0] ) { return true; @@ -149,13 +111,9 @@ export default createRule({ return false; } - const { tryStatement, block } = tryAncestorResult; + const { block, tryStatement } = tryAncestorResult; switch (block) { - case 'try': - // Try blocks are always followed by either a catch or finally, - // so exceptions thrown here always affect control flow. - return true; case 'catch': // Exceptions thrown in catch blocks followed by a finally block affect // control flow. @@ -167,6 +125,10 @@ export default createRule({ return affectsExplicitErrorHandling(tryStatement); case 'finally': return affectsExplicitErrorHandling(tryStatement); + case 'try': + // Try blocks are always followed by either a catch or finally, + // so exceptions thrown here always affect control flow. + return true; default: { const __never: never = block; throw new Error(`Unexpected block type: ${String(__never)}`); @@ -175,8 +137,8 @@ export default createRule({ } interface FindContainingTryStatementResult { + block: 'catch' | 'finally' | 'try'; tryStatement: ts.TryStatement; - block: 'try' | 'catch' | 'finally'; } /** @@ -193,7 +155,7 @@ export default createRule({ while (ancestor && !ts.isFunctionLike(ancestor)) { if (ts.isTryStatement(ancestor)) { - let block: 'try' | 'catch' | 'finally' | undefined; + let block: 'catch' | 'finally' | 'try' | undefined; if (child === ancestor.tryBlock) { block = 'try'; } else if (child === ancestor.catchClause) { @@ -203,11 +165,11 @@ export default createRule({ } return { - tryStatement: ancestor, block: nullThrows( block, 'Child of a try statement must be a try block, catch clause, or finally block', ), + tryStatement: ancestor, }; } child = ancestor; @@ -296,8 +258,8 @@ export default createRule({ messageId: 'nonPromiseAwait', node, ...fixOrSuggest(useAutoFix, { - messageId: 'nonPromiseAwait', fix: fixer => removeAwait(fixer, node), + messageId: 'nonPromiseAwait', }), }); } @@ -318,33 +280,33 @@ export default createRule({ : ruleConfiguration.ordinaryContext; switch (shouldAwaitInCurrentContext) { - case "don't-care": - break; case 'await': if (!isAwait) { context.report({ messageId: 'requiredPromiseAwait', node, ...fixOrSuggest(useAutoFix, { - messageId: 'requiredPromiseAwaitSuggestion', fix: fixer => insertAwait( fixer, node, isHigherPrecedenceThanAwait(expression), ), + messageId: 'requiredPromiseAwaitSuggestion', }), }); } break; + case "don't-care": + break; case 'no-await': if (isAwait) { context.report({ messageId: 'disallowedPromiseAwait', node, ...fixOrSuggest(useAutoFix, { - messageId: 'disallowedPromiseAwaitSuggestion', fix: fixer => removeAwait(fixer, node), + messageId: 'disallowedPromiseAwaitSuggestion', }), }); } @@ -365,13 +327,13 @@ export default createRule({ } return { - FunctionDeclaration: enterFunction, - FunctionExpression: enterFunction, ArrowFunctionExpression: enterFunction, + 'ArrowFunctionExpression:exit': exitFunction, + FunctionDeclaration: enterFunction, 'FunctionDeclaration:exit': exitFunction, + FunctionExpression: enterFunction, 'FunctionExpression:exit': exitFunction, - 'ArrowFunctionExpression:exit': exitFunction, // executes after less specific handler, so exitFunction is called 'ArrowFunctionExpression[async = true]:exit'( @@ -396,36 +358,75 @@ export default createRule({ }, }; }, + defaultOptions: ['in-try-catch'], + meta: { + docs: { + description: 'Enforce consistent awaiting of returned promises', + extendsBaseRule: 'no-return-await', + recommended: { + strict: ['error-handling-correctness-only'], + }, + requiresTypeChecking: true, + }, + fixable: 'code', + hasSuggestions: true, + messages: { + disallowedPromiseAwait: + 'Returning an awaited promise is not allowed in this context.', + disallowedPromiseAwaitSuggestion: + 'Remove `await` before the expression. Use caution as this may impact control flow.', + nonPromiseAwait: + 'Returning an awaited value that is not a promise is not allowed.', + requiredPromiseAwait: + 'Returning an awaited promise is required in this context.', + requiredPromiseAwaitSuggestion: + 'Add `await` before the expression. Use caution as this may impact control flow.', + }, + schema: [ + { + enum: [ + 'in-try-catch', + 'always', + 'never', + 'error-handling-correctness-only', + ] satisfies Option[], + type: 'string', + }, + ], + type: 'problem', + }, + + name: 'return-await', }); -type WhetherToAwait = 'await' | 'no-await' | "don't-care"; +type WhetherToAwait = "don't-care" | 'await' | 'no-await'; interface RuleConfiguration { - ordinaryContext: WhetherToAwait; errorHandlingContext: WhetherToAwait; + ordinaryContext: WhetherToAwait; } function getConfiguration(option: Option): RuleConfiguration { switch (option) { case 'always': return { - ordinaryContext: 'await', errorHandlingContext: 'await', - }; - case 'never': - return { - ordinaryContext: 'no-await', - errorHandlingContext: 'no-await', + ordinaryContext: 'await', }; case 'error-handling-correctness-only': return { - ordinaryContext: "don't-care", errorHandlingContext: 'await', + ordinaryContext: "don't-care", }; case 'in-try-catch': return { - ordinaryContext: 'no-await', errorHandlingContext: 'await', + ordinaryContext: 'no-await', + }; + case 'never': + return { + errorHandlingContext: 'no-await', + ordinaryContext: 'no-await', }; } } diff --git a/packages/eslint-plugin/src/rules/sort-type-constituents.ts b/packages/eslint-plugin/src/rules/sort-type-constituents.ts index de87dc2aa962..d73887909d6d 100644 --- a/packages/eslint-plugin/src/rules/sort-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/sort-type-constituents.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, getEnumNames, typeNodeRequiresParentheses } from '../util'; @@ -9,9 +10,9 @@ enum Group { import = 'import', intersection = 'intersection', keyword = 'keyword', - nullish = 'nullish', literal = 'literal', named = 'named', + nullish = 'nullish', object = 'object', operator = 'operator', tuple = 'tuple', @@ -106,91 +107,22 @@ function caseSensitiveSort(a: string, b: string): number { export type Options = [ { + caseSensitive?: boolean; checkIntersections?: boolean; checkUnions?: boolean; - caseSensitive?: boolean; groupOrder?: string[]; }, ]; export type MessageIds = 'notSorted' | 'notSortedNamed' | 'suggestFix'; export default createRule({ - name: 'sort-type-constituents', - meta: { - deprecated: true, - replacedBy: [ - 'perfectionist/sort-intersection-types', - 'perfectionist/sort-union-types', - ], - type: 'suggestion', - docs: { - description: - 'Enforce constituents of a type union/intersection to be sorted alphabetically', - }, - fixable: 'code', - hasSuggestions: true, - messages: { - notSorted: '{{type}} type constituents must be sorted.', - notSortedNamed: '{{type}} type {{name}} constituents must be sorted.', - suggestFix: 'Sort constituents of type (removes all comments).', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - checkIntersections: { - description: 'Whether to check intersection types.', - type: 'boolean', - }, - checkUnions: { - description: 'Whether to check union types.', - type: 'boolean', - }, - caseSensitive: { - description: 'Whether to sort using case sensitive sorting.', - type: 'boolean', - }, - groupOrder: { - description: 'Ordering of the groups.', - type: 'array', - items: { - type: 'string', - enum: getEnumNames(Group), - }, - }, - }, - }, - ], - }, - defaultOptions: [ - { - checkIntersections: true, - checkUnions: true, - caseSensitive: false, - groupOrder: [ - Group.named, - Group.keyword, - Group.operator, - Group.literal, - Group.function, - Group.import, - Group.conditional, - Group.object, - Group.tuple, - Group.intersection, - Group.union, - Group.nullish, - ], - }, - ], create( context, - [{ checkIntersections, checkUnions, caseSensitive, groupOrder }], + [{ caseSensitive, checkIntersections, checkUnions, groupOrder }], ) { const collator = new Intl.Collator('en', { - sensitivity: 'base', numeric: true, + sensitivity: 'base', }); function checkSorting( @@ -257,17 +189,17 @@ export default createRule({ return fixer.replaceText(node, sorted); }; return context.report({ - node, - messageId, data, + messageId, + node, // don't autofix if any of the types have leading/trailing comments // the logic for preserving them correctly is a pain - we may implement this later ...(hasComments ? { suggest: [ { - messageId: 'suggestFix', fix, + messageId: 'suggestFix', }, ], } @@ -290,4 +222,73 @@ export default createRule({ }), }; }, + defaultOptions: [ + { + caseSensitive: false, + checkIntersections: true, + checkUnions: true, + groupOrder: [ + Group.named, + Group.keyword, + Group.operator, + Group.literal, + Group.function, + Group.import, + Group.conditional, + Group.object, + Group.tuple, + Group.intersection, + Group.union, + Group.nullish, + ], + }, + ], + meta: { + deprecated: true, + docs: { + description: + 'Enforce constituents of a type union/intersection to be sorted alphabetically', + }, + fixable: 'code', + hasSuggestions: true, + messages: { + notSorted: '{{type}} type constituents must be sorted.', + notSortedNamed: '{{type}} type {{name}} constituents must be sorted.', + suggestFix: 'Sort constituents of type (removes all comments).', + }, + replacedBy: [ + 'perfectionist/sort-intersection-types', + 'perfectionist/sort-union-types', + ], + schema: [ + { + additionalProperties: false, + properties: { + caseSensitive: { + description: 'Whether to sort using case sensitive sorting.', + type: 'boolean', + }, + checkIntersections: { + description: 'Whether to check intersection types.', + type: 'boolean', + }, + checkUnions: { + description: 'Whether to check union types.', + type: 'boolean', + }, + groupOrder: { + description: 'Ordering of the groups.', + items: { + enum: getEnumNames(Group), + type: 'string', + }, + type: 'array', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'sort-type-constituents', }); diff --git a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts index 3e01b5c15a1a..630925365333 100644 --- a/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts +++ b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts @@ -2,6 +2,7 @@ import type { ParserServicesWithTypeInformation, TSESTree, } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -16,15 +17,15 @@ import { export type Options = [ { - allowString?: boolean; - allowNumber?: boolean; - allowNullableObject?: boolean; + allowAny?: boolean; allowNullableBoolean?: boolean; - allowNullableString?: boolean; - allowNullableNumber?: boolean; allowNullableEnum?: boolean; - allowAny?: boolean; + allowNullableNumber?: boolean; + allowNullableObject?: boolean; + allowNullableString?: boolean; + allowNumber?: boolean; allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean; + allowString?: boolean; }, ]; @@ -54,108 +55,6 @@ export type MessageId = | 'noStrictNullCheck'; export default createRule({ - name: 'strict-boolean-expressions', - meta: { - type: 'suggestion', - fixable: 'code', - hasSuggestions: true, - docs: { - description: 'Disallow certain types in boolean expressions', - requiresTypeChecking: true, - }, - schema: [ - { - type: 'object', - properties: { - allowString: { type: 'boolean' }, - allowNumber: { type: 'boolean' }, - allowNullableObject: { type: 'boolean' }, - allowNullableBoolean: { type: 'boolean' }, - allowNullableString: { type: 'boolean' }, - allowNullableNumber: { type: 'boolean' }, - allowNullableEnum: { type: 'boolean' }, - allowAny: { type: 'boolean' }, - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - messages: { - conditionErrorOther: - 'Unexpected value in conditional. ' + - 'A boolean expression is required.', - conditionErrorAny: - 'Unexpected any value in conditional. ' + - 'An explicit comparison or type cast is required.', - conditionErrorNullish: - 'Unexpected nullish value in conditional. ' + - 'The condition is always false.', - conditionErrorNullableBoolean: - 'Unexpected nullable boolean value in conditional. ' + - 'Please handle the nullish case explicitly.', - conditionErrorString: - 'Unexpected string value in conditional. ' + - 'An explicit empty string check is required.', - conditionErrorNullableString: - 'Unexpected nullable string value in conditional. ' + - 'Please handle the nullish/empty cases explicitly.', - conditionErrorNumber: - 'Unexpected number value in conditional. ' + - 'An explicit zero/NaN check is required.', - conditionErrorNullableNumber: - 'Unexpected nullable number value in conditional. ' + - 'Please handle the nullish/zero/NaN cases explicitly.', - conditionErrorObject: - 'Unexpected object value in conditional. ' + - 'The condition is always true.', - conditionErrorNullableObject: - 'Unexpected nullable object value in conditional. ' + - 'An explicit null check is required.', - conditionErrorNullableEnum: - 'Unexpected nullable enum value in conditional. ' + - 'Please handle the nullish/zero/NaN cases explicitly.', - noStrictNullCheck: - 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', - - conditionFixDefaultFalse: - 'Explicitly treat nullish value the same as false (`value ?? false`)', - conditionFixDefaultEmptyString: - 'Explicitly treat nullish value the same as an empty string (`value ?? ""`)', - conditionFixDefaultZero: - 'Explicitly treat nullish value the same as 0 (`value ?? 0`)', - conditionFixCompareNullish: - 'Change condition to check for null/undefined (`value != null`)', - conditionFixCastBoolean: - 'Explicitly cast value to a boolean (`Boolean(value)`)', - conditionFixCompareTrue: - 'Change condition to check if true (`value === true`)', - conditionFixCompareFalse: - 'Change condition to check if false (`value === false`)', - conditionFixCompareStringLength: - "Change condition to check string's length (`value.length !== 0`)", - conditionFixCompareEmptyString: - 'Change condition to check for empty string (`value !== ""`)', - conditionFixCompareZero: - 'Change condition to check for 0 (`value !== 0`)', - conditionFixCompareNaN: - 'Change condition to check for NaN (`!Number.isNaN(value)`)', - }, - }, - defaultOptions: [ - { - allowString: true, - allowNumber: true, - allowNullableObject: true, - allowNullableBoolean: false, - allowNullableString: false, - allowNullableNumber: false, - allowNullableEnum: false, - allowAny: false, - allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, - }, - ], create(context, [options]) { const services = getParserServices(context); const checker = services.program.getTypeChecker(); @@ -172,8 +71,8 @@ export default createRule({ ) { context.report({ loc: { - start: { line: 0, column: 0 }, - end: { line: 0, column: 0 }, + end: { column: 0, line: 0 }, + start: { column: 0, line: 0 }, }, messageId: 'noStrictNullCheck', }); @@ -182,14 +81,14 @@ export default createRule({ const traversedNodes = new Set(); return { + CallExpression: traverseCallExpression, ConditionalExpression: traverseTestExpression, DoWhileStatement: traverseTestExpression, ForStatement: traverseTestExpression, IfStatement: traverseTestExpression, - WhileStatement: traverseTestExpression, 'LogicalExpression[operator!="??"]': traverseLogicalExpression, 'UnaryExpression[operator="!"]': traverseUnaryLogicalExpression, - CallExpression: traverseCallExpression, + WhileStatement: traverseTestExpression, }; type TestExpression = @@ -428,7 +327,7 @@ export default createRule({ // nullish if (is('nullish')) { // condition is always false - context.report({ node, messageId: 'conditionErrorNullish' }); + context.report({ messageId: 'conditionErrorNullish', node }); return; } @@ -443,49 +342,49 @@ export default createRule({ if (isLogicalNegationExpression(node.parent)) { // if (!nullableBoolean) context.report({ - node, messageId: 'conditionErrorNullableBoolean', + node, suggest: [ { - messageId: 'conditionFixDefaultFalse', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} ?? false`, }), + messageId: 'conditionFixDefaultFalse', }, { - messageId: 'conditionFixCompareFalse', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `${code} === false`, }), + messageId: 'conditionFixCompareFalse', }, ], }); } else { // if (nullableBoolean) context.report({ - node, messageId: 'conditionErrorNullableBoolean', + node, suggest: [ { - messageId: 'conditionFixDefaultFalse', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} ?? false`, }), + messageId: 'conditionFixDefaultFalse', }, { - messageId: 'conditionFixCompareTrue', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} === true`, }), + messageId: 'conditionFixCompareTrue', }, ], }); @@ -508,67 +407,67 @@ export default createRule({ if (isLogicalNegationExpression(node.parent)) { // if (!string) context.report({ - node, messageId: 'conditionErrorString', + node, suggest: [ { - messageId: 'conditionFixCompareStringLength', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `${code}.length === 0`, }), + messageId: 'conditionFixCompareStringLength', }, { - messageId: 'conditionFixCompareEmptyString', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `${code} === ""`, }), + messageId: 'conditionFixCompareEmptyString', }, { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `!Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); } else { // if (string) context.report({ - node, messageId: 'conditionErrorString', + node, suggest: [ { - messageId: 'conditionFixCompareStringLength', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code}.length > 0`, }), + messageId: 'conditionFixCompareStringLength', }, { - messageId: 'conditionFixCompareEmptyString', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} !== ""`, }), + messageId: 'conditionFixCompareEmptyString', }, { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); @@ -583,66 +482,66 @@ export default createRule({ if (isLogicalNegationExpression(node.parent)) { // if (!nullableString) context.report({ - node, messageId: 'conditionErrorNullableString', + node, suggest: [ { - messageId: 'conditionFixCompareNullish', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `${code} == null`, }), + messageId: 'conditionFixCompareNullish', }, { - messageId: 'conditionFixDefaultEmptyString', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} ?? ""`, }), + messageId: 'conditionFixDefaultEmptyString', }, { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `!Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); } else { // if (nullableString) context.report({ - node, messageId: 'conditionErrorNullableString', + node, suggest: [ { - messageId: 'conditionFixCompareNullish', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} != null`, }), + messageId: 'conditionFixCompareNullish', }, { - messageId: 'conditionFixDefaultEmptyString', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} ?? ""`, }), + messageId: 'conditionFixDefaultEmptyString', }, { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); @@ -658,93 +557,93 @@ export default createRule({ if (isLogicalNegationExpression(node.parent)) { // if (!array.length) context.report({ - node, - messageId: 'conditionErrorNumber', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `${code} === 0`, }), + messageId: 'conditionErrorNumber', + node, }); } else { // if (array.length) context.report({ - node, - messageId: 'conditionErrorNumber', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} > 0`, }), + messageId: 'conditionErrorNumber', + node, }); } } else if (isLogicalNegationExpression(node.parent)) { // if (!number) context.report({ - node, messageId: 'conditionErrorNumber', + node, suggest: [ { - messageId: 'conditionFixCompareZero', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, // TODO: we have to compare to 0n if the type is bigint wrap: code => `${code} === 0`, }), + messageId: 'conditionFixCompareZero', }, { // TODO: don't suggest this for bigint because it can't be NaN - messageId: 'conditionFixCompareNaN', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `Number.isNaN(${code})`, }), + messageId: 'conditionFixCompareNaN', }, { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `!Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); } else { // if (number) context.report({ - node, messageId: 'conditionErrorNumber', + node, suggest: [ { - messageId: 'conditionFixCompareZero', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} !== 0`, }), + messageId: 'conditionFixCompareZero', }, { - messageId: 'conditionFixCompareNaN', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `!Number.isNaN(${code})`, }), + messageId: 'conditionFixCompareNaN', }, { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); @@ -759,66 +658,66 @@ export default createRule({ if (isLogicalNegationExpression(node.parent)) { // if (!nullableNumber) context.report({ - node, messageId: 'conditionErrorNullableNumber', + node, suggest: [ { - messageId: 'conditionFixCompareNullish', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `${code} == null`, }), + messageId: 'conditionFixCompareNullish', }, { - messageId: 'conditionFixDefaultZero', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} ?? 0`, }), + messageId: 'conditionFixDefaultZero', }, { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `!Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); } else { // if (nullableNumber) context.report({ - node, messageId: 'conditionErrorNullableNumber', + node, suggest: [ { - messageId: 'conditionFixCompareNullish', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} != null`, }), + messageId: 'conditionFixCompareNullish', }, { - messageId: 'conditionFixDefaultZero', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} ?? 0`, }), + messageId: 'conditionFixDefaultZero', }, { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); @@ -830,7 +729,7 @@ export default createRule({ // object if (is('object')) { // condition is always true - context.report({ node, messageId: 'conditionErrorObject' }); + context.report({ messageId: 'conditionErrorObject', node }); return; } @@ -840,33 +739,33 @@ export default createRule({ if (isLogicalNegationExpression(node.parent)) { // if (!nullableObject) context.report({ - node, messageId: 'conditionErrorNullableObject', + node, suggest: [ { - messageId: 'conditionFixCompareNullish', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `${code} == null`, }), + messageId: 'conditionFixCompareNullish', }, ], }); } else { // if (nullableObject) context.report({ - node, messageId: 'conditionErrorNullableObject', + node, suggest: [ { - messageId: 'conditionFixCompareNullish', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} != null`, }), + messageId: 'conditionFixCompareNullish', }, ], }); @@ -890,24 +789,24 @@ export default createRule({ if (!options.allowNullableEnum) { if (isLogicalNegationExpression(node.parent)) { context.report({ - node, - messageId: 'conditionErrorNullableEnum', fix: getWrappingFixer({ - sourceCode: context.sourceCode, - node: node.parent, innerNode: node, + node: node.parent, + sourceCode: context.sourceCode, wrap: code => `${code} == null`, }), + messageId: 'conditionErrorNullableEnum', + node, }); } else { context.report({ - node, - messageId: 'conditionErrorNullableEnum', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `${code} != null`, }), + messageId: 'conditionErrorNullableEnum', + node, }); } } @@ -918,16 +817,16 @@ export default createRule({ if (is('any')) { if (!options.allowAny) { context.report({ - node, messageId: 'conditionErrorAny', + node, suggest: [ { - messageId: 'conditionFixCastBoolean', fix: getWrappingFixer({ - sourceCode: context.sourceCode, node, + sourceCode: context.sourceCode, wrap: code => `Boolean(${code})`, }), + messageId: 'conditionFixCastBoolean', }, ], }); @@ -936,7 +835,7 @@ export default createRule({ } // other - context.report({ node, messageId: 'conditionErrorOther' }); + context.report({ messageId: 'conditionErrorOther', node }); } /** The types we care about */ @@ -1062,6 +961,108 @@ export default createRule({ return variantTypes; } }, + defaultOptions: [ + { + allowAny: false, + allowNullableBoolean: false, + allowNullableEnum: false, + allowNullableNumber: false, + allowNullableObject: true, + allowNullableString: false, + allowNumber: true, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, + allowString: true, + }, + ], + meta: { + docs: { + description: 'Disallow certain types in boolean expressions', + requiresTypeChecking: true, + }, + fixable: 'code', + hasSuggestions: true, + messages: { + conditionErrorAny: + 'Unexpected any value in conditional. ' + + 'An explicit comparison or type cast is required.', + conditionErrorNullableBoolean: + 'Unexpected nullable boolean value in conditional. ' + + 'Please handle the nullish case explicitly.', + conditionErrorNullableEnum: + 'Unexpected nullable enum value in conditional. ' + + 'Please handle the nullish/zero/NaN cases explicitly.', + conditionErrorNullableNumber: + 'Unexpected nullable number value in conditional. ' + + 'Please handle the nullish/zero/NaN cases explicitly.', + conditionErrorNullableObject: + 'Unexpected nullable object value in conditional. ' + + 'An explicit null check is required.', + conditionErrorNullableString: + 'Unexpected nullable string value in conditional. ' + + 'Please handle the nullish/empty cases explicitly.', + conditionErrorNullish: + 'Unexpected nullish value in conditional. ' + + 'The condition is always false.', + conditionErrorNumber: + 'Unexpected number value in conditional. ' + + 'An explicit zero/NaN check is required.', + conditionErrorObject: + 'Unexpected object value in conditional. ' + + 'The condition is always true.', + conditionErrorOther: + 'Unexpected value in conditional. ' + + 'A boolean expression is required.', + conditionErrorString: + 'Unexpected string value in conditional. ' + + 'An explicit empty string check is required.', + conditionFixCastBoolean: + 'Explicitly cast value to a boolean (`Boolean(value)`)', + + conditionFixCompareEmptyString: + 'Change condition to check for empty string (`value !== ""`)', + conditionFixCompareFalse: + 'Change condition to check if false (`value === false`)', + conditionFixCompareNaN: + 'Change condition to check for NaN (`!Number.isNaN(value)`)', + conditionFixCompareNullish: + 'Change condition to check for null/undefined (`value != null`)', + conditionFixCompareStringLength: + "Change condition to check string's length (`value.length !== 0`)", + conditionFixCompareTrue: + 'Change condition to check if true (`value === true`)', + conditionFixCompareZero: + 'Change condition to check for 0 (`value !== 0`)', + conditionFixDefaultEmptyString: + 'Explicitly treat nullish value the same as an empty string (`value ?? ""`)', + conditionFixDefaultFalse: + 'Explicitly treat nullish value the same as false (`value ?? false`)', + conditionFixDefaultZero: + 'Explicitly treat nullish value the same as 0 (`value ?? 0`)', + noStrictNullCheck: + 'This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowAny: { type: 'boolean' }, + allowNullableBoolean: { type: 'boolean' }, + allowNullableEnum: { type: 'boolean' }, + allowNullableNumber: { type: 'boolean' }, + allowNullableObject: { type: 'boolean' }, + allowNullableString: { type: 'boolean' }, + allowNumber: { type: 'boolean' }, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: { + type: 'boolean', + }, + allowString: { type: 'boolean' }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'strict-boolean-expressions', }); function isLogicalNegationExpression( diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index 1452d7de6cdb..abce22923154 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -14,10 +15,10 @@ import { } from '../util'; interface SwitchMetadata { - readonly symbolName: string | undefined; + readonly containsNonLiteralType: boolean; readonly defaultCase: TSESTree.SwitchCase | undefined; readonly missingLiteralBranchTypes: ts.Type[]; - readonly containsNonLiteralType: boolean; + readonly symbolName: string | undefined; } type Options = [ @@ -40,49 +41,11 @@ type Options = [ ]; type MessageIds = - | 'switchIsNotExhaustive' + | 'addMissingCases' | 'dangerousDefaultCase' - | 'addMissingCases'; + | 'switchIsNotExhaustive'; export default createRule({ - name: 'switch-exhaustiveness-check', - meta: { - type: 'suggestion', - docs: { - description: 'Require switch-case statements to be exhaustive', - requiresTypeChecking: true, - }, - hasSuggestions: true, - schema: [ - { - type: 'object', - properties: { - allowDefaultCaseForExhaustiveSwitch: { - description: `If 'true', allow 'default' cases on switch statements with exhaustive cases.`, - type: 'boolean', - }, - requireDefaultForNonUnion: { - description: `If 'true', require a 'default' clause for switches on non-union types.`, - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - messages: { - switchIsNotExhaustive: - 'Switch is not exhaustive. Cases not matched: {{missingBranches}}', - dangerousDefaultCase: - 'The switch statement is exhaustive, so the default case is unnecessary.', - addMissingCases: 'Add branches for missing cases.', - }, - }, - defaultOptions: [ - { - allowDefaultCaseForExhaustiveSwitch: true, - requireDefaultForNonUnion: false, - }, - ], create( context, [{ allowDefaultCaseForExhaustiveSwitch, requireDefaultForNonUnion }], @@ -141,10 +104,10 @@ export default createRule({ } return { - symbolName, - missingLiteralBranchTypes, - defaultCase, containsNonLiteralType, + defaultCase, + missingLiteralBranchTypes, + symbolName, }; } @@ -152,7 +115,7 @@ export default createRule({ node: TSESTree.SwitchStatement, switchMetadata: SwitchMetadata, ): void { - const { missingLiteralBranchTypes, symbolName, defaultCase } = + const { defaultCase, missingLiteralBranchTypes, symbolName } = switchMetadata; // We only trigger the rule if a `default` case does not exist, since that @@ -160,8 +123,6 @@ export default createRule({ // match the members of a union. if (missingLiteralBranchTypes.length > 0 && defaultCase === undefined) { context.report({ - node: node.discriminant, - messageId: 'switchIsNotExhaustive', data: { missingBranches: missingLiteralBranchTypes .map(missingType => @@ -171,9 +132,10 @@ export default createRule({ ) .join(' | '), }, + messageId: 'switchIsNotExhaustive', + node: node.discriminant, suggest: [ { - messageId: 'addMissingCases', fix(fixer): TSESLint.RuleFix | null { return fixSwitch( fixer, @@ -182,6 +144,7 @@ export default createRule({ symbolName?.toString(), ); }, + messageId: 'addMissingCases', }, ], }); @@ -275,7 +238,7 @@ export default createRule({ return; } - const { missingLiteralBranchTypes, defaultCase, containsNonLiteralType } = + const { containsNonLiteralType, defaultCase, missingLiteralBranchTypes } = switchMetadata; if ( @@ -284,8 +247,8 @@ export default createRule({ !containsNonLiteralType ) { context.report({ - node: defaultCase, messageId: 'dangerousDefaultCase', + node: defaultCase, }); } } @@ -298,19 +261,19 @@ export default createRule({ return; } - const { defaultCase, containsNonLiteralType } = switchMetadata; + const { containsNonLiteralType, defaultCase } = switchMetadata; if (containsNonLiteralType && defaultCase === undefined) { context.report({ - node: node.discriminant, - messageId: 'switchIsNotExhaustive', data: { missingBranches: 'default' }, + messageId: 'switchIsNotExhaustive', + node: node.discriminant, suggest: [ { - messageId: 'addMissingCases', fix(fixer): TSESLint.RuleFix { return fixSwitch(fixer, node, [null]); }, + messageId: 'addMissingCases', }, ], }); @@ -327,6 +290,44 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + allowDefaultCaseForExhaustiveSwitch: true, + requireDefaultForNonUnion: false, + }, + ], + meta: { + docs: { + description: 'Require switch-case statements to be exhaustive', + requiresTypeChecking: true, + }, + hasSuggestions: true, + messages: { + addMissingCases: 'Add branches for missing cases.', + dangerousDefaultCase: + 'The switch statement is exhaustive, so the default case is unnecessary.', + switchIsNotExhaustive: + 'Switch is not exhaustive. Cases not matched: {{missingBranches}}', + }, + schema: [ + { + additionalProperties: false, + properties: { + allowDefaultCaseForExhaustiveSwitch: { + description: `If 'true', allow 'default' cases on switch statements with exhaustive cases.`, + type: 'boolean', + }, + requireDefaultForNonUnion: { + description: `If 'true', require a 'default' clause for switches on non-union types.`, + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'switch-exhaustiveness-check', }); function isTypeLiteralLikeType(type: ts.Type): boolean { diff --git a/packages/eslint-plugin/src/rules/triple-slash-reference.ts b/packages/eslint-plugin/src/rules/triple-slash-reference.ts index a45662c33d4c..371ce0f38545 100644 --- a/packages/eslint-plugin/src/rules/triple-slash-reference.ts +++ b/packages/eslint-plugin/src/rules/triple-slash-reference.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -13,46 +14,6 @@ type Options = [ type MessageIds = 'tripleSlashReference'; export default createRule({ - name: 'triple-slash-reference', - meta: { - type: 'suggestion', - docs: { - description: - 'Disallow certain triple slash directives in favor of ES6-style import declarations', - recommended: 'recommended', - }, - messages: { - tripleSlashReference: - 'Do not use a triple slash reference for {{module}}, use `import` style instead.', - }, - schema: [ - { - type: 'object', - properties: { - lib: { - type: 'string', - enum: ['always', 'never'], - }, - path: { - type: 'string', - enum: ['always', 'never'], - }, - types: { - type: 'string', - enum: ['always', 'never', 'prefer-import'], - }, - }, - additionalProperties: false, - }, - ], - }, - defaultOptions: [ - { - lib: 'always', - path: 'never', - types: 'prefer-import', - }, - ], create(context, [{ lib, path, types }]) { let programNode: TSESTree.Node | undefined; @@ -65,11 +26,11 @@ export default createRule({ references.forEach(reference => { if (reference.importName === source.value) { context.report({ - node: reference.comment, - messageId: 'tripleSlashReference', data: { module: reference.importName, }, + messageId: 'tripleSlashReference', + node: reference.comment, }); } }); @@ -80,15 +41,6 @@ export default createRule({ hasMatchingReference(node.source); } }, - TSImportEqualsDeclaration(node): void { - if (programNode) { - const reference = node.moduleReference; - - if (reference.type === AST_NODE_TYPES.TSExternalModuleReference) { - hasMatchingReference(reference.expression as TSESTree.Literal); - } - } - }, Program(node): void { if (lib === 'always' && path === 'always' && types === 'always') { return; @@ -112,11 +64,11 @@ export default createRule({ (referenceResult[1] === 'lib' && lib === 'never') ) { context.report({ - node: comment, - messageId: 'tripleSlashReference', data: { module: referenceResult[2], }, + messageId: 'tripleSlashReference', + node: comment, }); return; } @@ -126,6 +78,55 @@ export default createRule({ } }); }, + TSImportEqualsDeclaration(node): void { + if (programNode) { + const reference = node.moduleReference; + + if (reference.type === AST_NODE_TYPES.TSExternalModuleReference) { + hasMatchingReference(reference.expression as TSESTree.Literal); + } + } + }, }; }, + defaultOptions: [ + { + lib: 'always', + path: 'never', + types: 'prefer-import', + }, + ], + meta: { + docs: { + description: + 'Disallow certain triple slash directives in favor of ES6-style import declarations', + recommended: 'recommended', + }, + messages: { + tripleSlashReference: + 'Do not use a triple slash reference for {{module}}, use `import` style instead.', + }, + schema: [ + { + additionalProperties: false, + properties: { + lib: { + enum: ['always', 'never'], + type: 'string', + }, + path: { + enum: ['always', 'never'], + type: 'string', + }, + types: { + enum: ['always', 'never', 'prefer-import'], + type: 'string', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'triple-slash-reference', }); diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts index 187f4d620365..91e0259fb25c 100644 --- a/packages/eslint-plugin/src/rules/typedef.ts +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -19,45 +20,6 @@ type Options = { [k in OptionKeys]?: boolean }; type MessageIds = 'expectedTypedef' | 'expectedTypedefNamed'; export default createRule<[Options], MessageIds>({ - name: 'typedef', - meta: { - docs: { - description: 'Require type annotations in certain places', - }, - messages: { - expectedTypedef: 'Expected a type annotation.', - expectedTypedefNamed: 'Expected {{name}} to have a type annotation.', - }, - schema: [ - { - type: 'object', - additionalProperties: false, - properties: { - [OptionKeys.ArrayDestructuring]: { type: 'boolean' }, - [OptionKeys.ArrowParameter]: { type: 'boolean' }, - [OptionKeys.MemberVariableDeclaration]: { type: 'boolean' }, - [OptionKeys.ObjectDestructuring]: { type: 'boolean' }, - [OptionKeys.Parameter]: { type: 'boolean' }, - [OptionKeys.PropertyDeclaration]: { type: 'boolean' }, - [OptionKeys.VariableDeclaration]: { type: 'boolean' }, - [OptionKeys.VariableDeclarationIgnoreFunction]: { type: 'boolean' }, - }, - }, - ], - type: 'suggestion', - }, - defaultOptions: [ - { - [OptionKeys.ArrayDestructuring]: false, - [OptionKeys.ArrowParameter]: false, - [OptionKeys.MemberVariableDeclaration]: false, - [OptionKeys.ObjectDestructuring]: false, - [OptionKeys.Parameter]: false, - [OptionKeys.PropertyDeclaration]: false, - [OptionKeys.VariableDeclaration]: false, - [OptionKeys.VariableDeclarationIgnoreFunction]: false, - }, - ], create( context, [ @@ -75,9 +37,9 @@ export default createRule<[Options], MessageIds>({ ) { function report(location: TSESTree.Node, name?: string): void { context.report({ - node: location, - messageId: name ? 'expectedTypedefNamed' : 'expectedTypedef', data: { name }, + messageId: name ? 'expectedTypedefNamed' : 'expectedTypedef', + node: location, }); } @@ -275,4 +237,43 @@ export default createRule<[Options], MessageIds>({ }, }; }, + defaultOptions: [ + { + [OptionKeys.ArrayDestructuring]: false, + [OptionKeys.ArrowParameter]: false, + [OptionKeys.MemberVariableDeclaration]: false, + [OptionKeys.ObjectDestructuring]: false, + [OptionKeys.Parameter]: false, + [OptionKeys.PropertyDeclaration]: false, + [OptionKeys.VariableDeclaration]: false, + [OptionKeys.VariableDeclarationIgnoreFunction]: false, + }, + ], + meta: { + docs: { + description: 'Require type annotations in certain places', + }, + messages: { + expectedTypedef: 'Expected a type annotation.', + expectedTypedefNamed: 'Expected {{name}} to have a type annotation.', + }, + schema: [ + { + additionalProperties: false, + properties: { + [OptionKeys.ArrayDestructuring]: { type: 'boolean' }, + [OptionKeys.ArrowParameter]: { type: 'boolean' }, + [OptionKeys.MemberVariableDeclaration]: { type: 'boolean' }, + [OptionKeys.ObjectDestructuring]: { type: 'boolean' }, + [OptionKeys.Parameter]: { type: 'boolean' }, + [OptionKeys.PropertyDeclaration]: { type: 'boolean' }, + [OptionKeys.VariableDeclaration]: { type: 'boolean' }, + [OptionKeys.VariableDeclarationIgnoreFunction]: { type: 'boolean' }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'typedef', }); diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 6e2c34d94b72..33b984972b62 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -103,38 +104,6 @@ const BASE_MESSAGE = 'Avoid referencing unbound methods which may cause unintentional scoping of `this`.'; export default createRule({ - name: 'unbound-method', - meta: { - docs: { - description: - 'Enforce unbound methods are called with their expected scope', - recommended: 'recommended', - requiresTypeChecking: true, - }, - messages: { - unbound: BASE_MESSAGE, - unboundWithoutThisAnnotation: `${BASE_MESSAGE}\nIf your function does not access \`this\`, you can annotate it with \`this: void\`, or consider using an arrow function instead.`, - }, - schema: [ - { - type: 'object', - properties: { - ignoreStatic: { - description: - 'Whether to skip checking whether `static` methods are correctly bound.', - type: 'boolean', - }, - }, - additionalProperties: false, - }, - ], - type: 'problem', - }, - defaultOptions: [ - { - ignoreStatic: false, - }, - ], create(context, [{ ignoreStatic }]) { const services = getParserServices(context); const currentSourceFile = services.program.getSourceFile(context.filename); @@ -272,6 +241,38 @@ export default createRule({ }, }; }, + defaultOptions: [ + { + ignoreStatic: false, + }, + ], + meta: { + docs: { + description: + 'Enforce unbound methods are called with their expected scope', + recommended: 'recommended', + requiresTypeChecking: true, + }, + messages: { + unbound: BASE_MESSAGE, + unboundWithoutThisAnnotation: `${BASE_MESSAGE}\nIf your function does not access \`this\`, you can annotate it with \`this: void\`, or consider using an arrow function instead.`, + }, + schema: [ + { + additionalProperties: false, + properties: { + ignoreStatic: { + description: + 'Whether to skip checking whether `static` methods are correctly bound.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'problem', + }, + name: 'unbound-method', }); function isNodeInsideTypeDeclaration(node: TSESTree.Node): boolean { @@ -337,9 +338,9 @@ function checkIfMethod( function checkMethod( valueDeclaration: + | ts.FunctionExpression | ts.MethodDeclaration - | ts.MethodSignature - | ts.FunctionExpression, + | ts.MethodSignature, ignoreStatic: boolean, ): CheckMethodResult { const firstParam = valueDeclaration.parameters.at(0); @@ -389,10 +390,10 @@ function isSafeUse(node: TSESTree.Node): boolean { // the first case is safe for obvious // reasons. The second one is also fine // since we're returning something falsy - return ['typeof', '!', 'void', 'delete'].includes(parent.operator); + return ['!', 'delete', 'typeof', 'void'].includes(parent.operator); case AST_NODE_TYPES.BinaryExpression: - return ['instanceof', '==', '!=', '===', '!=='].includes(parent.operator); + return ['!=', '!==', '==', '===', 'instanceof'].includes(parent.operator); case AST_NODE_TYPES.AssignmentExpression: return ( diff --git a/packages/eslint-plugin/src/rules/unified-signatures.ts b/packages/eslint-plugin/src/rules/unified-signatures.ts index 9a6705576ed4..80f983a727d5 100644 --- a/packages/eslint-plugin/src/rules/unified-signatures.ts +++ b/packages/eslint-plugin/src/rules/unified-signatures.ts @@ -1,18 +1,20 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { Equal } from '../util'; + import { arraysAreEqual, createRule, nullThrows } from '../util'; interface Failure { - unify: Unify; only2: boolean; + unify: Unify; } type Unify = | { - kind: 'extra-parameter'; extraParameter: TSESTree.Parameter; + kind: 'extra-parameter'; otherSignature: SignatureDefinition; } | { @@ -63,41 +65,6 @@ type Options = [ ]; export default createRule({ - name: 'unified-signatures', - meta: { - docs: { - description: - 'Disallow two overloads that could be unified into one with a union or an optional/rest parameter', - // too opinionated to be recommended - recommended: 'strict', - }, - type: 'suggestion', - messages: { - omittingRestParameter: '{{failureStringStart}} with a rest parameter.', - omittingSingleParameter: - '{{failureStringStart}} with an optional parameter.', - singleParameterDifference: - '{{failureStringStart}} taking `{{type1}} | {{type2}}`.', - }, - schema: [ - { - additionalProperties: false, - properties: { - ignoreDifferentlyNamedParameters: { - description: - 'Whether two parameters with different names at the same index should be considered different even if their types are the same.', - type: 'boolean', - }, - }, - type: 'object', - }, - ], - }, - defaultOptions: [ - { - ignoreDifferentlyNamedParameters: false, - }, - ], create(context, [{ ignoreDifferentlyNamedParameters }]) { //---------------------------------------------------------------------- // Helpers @@ -114,7 +81,7 @@ export default createRule({ function addFailures(failures: Failure[]): void { for (const failure of failures) { - const { unify, only2 } = failure; + const { only2, unify } = failure; switch (unify.kind) { case 'single-parameter-difference': { const { p0, p1 } = unify; @@ -128,8 +95,6 @@ export default createRule({ : p1.typeAnnotation; context.report({ - loc: p1.loc, - messageId: 'singleParameterDifference', data: { failureStringStart: failureStringStart(lineOfOtherOverload), type1: context.sourceCode.getText( @@ -139,6 +104,8 @@ export default createRule({ typeAnnotation1?.typeAnnotation, ), }, + loc: p1.loc, + messageId: 'singleParameterDifference', node: p1, }); break; @@ -150,14 +117,14 @@ export default createRule({ : otherSignature.loc.start.line; context.report({ + data: { + failureStringStart: failureStringStart(lineOfOtherOverload), + }, loc: extraParameter.loc, messageId: extraParameter.type === AST_NODE_TYPES.RestElement ? 'omittingRestParameter' : 'omittingSingleParameter', - data: { - failureStringStart: failureStringStart(lineOfOtherOverload), - }, node: extraParameter, }); } @@ -182,7 +149,7 @@ export default createRule({ isTypeParameter, ); if (unify !== undefined) { - result.push({ unify, only2: overloads.length === 2 }); + result.push({ only2: overloads.length === 2, unify }); } }); } @@ -546,43 +513,78 @@ export default createRule({ //---------------------------------------------------------------------- return { - Program: createScope, - TSModuleBlock: createScope, - TSInterfaceDeclaration(node): void { + ClassDeclaration(node): void { createScope(node.body, node.typeParameters); }, - ClassDeclaration(node): void { + Program: createScope, + TSInterfaceDeclaration(node): void { createScope(node.body, node.typeParameters); }, + TSModuleBlock: createScope, TSTypeLiteral: createScope, // collect overloads - TSDeclareFunction(node): void { - const exportingNode = getExportingNode(node); - addOverload(node, node.id?.name ?? exportingNode?.type, exportingNode); - }, - TSCallSignatureDeclaration: addOverload, - TSConstructSignatureDeclaration: addOverload, - TSMethodSignature: addOverload, - TSAbstractMethodDefinition(node): void { + MethodDefinition(node): void { if (!node.value.body) { addOverload(node); } }, - MethodDefinition(node): void { + TSAbstractMethodDefinition(node): void { if (!node.value.body) { addOverload(node); } }, + TSCallSignatureDeclaration: addOverload, + TSConstructSignatureDeclaration: addOverload, + TSDeclareFunction(node): void { + const exportingNode = getExportingNode(node); + addOverload(node, node.id?.name ?? exportingNode?.type, exportingNode); + }, + TSMethodSignature: addOverload, // validate scopes + 'ClassDeclaration:exit': checkScope, 'Program:exit': checkScope, - 'TSModuleBlock:exit': checkScope, 'TSInterfaceDeclaration:exit': checkScope, - 'ClassDeclaration:exit': checkScope, + 'TSModuleBlock:exit': checkScope, 'TSTypeLiteral:exit': checkScope, }; }, + defaultOptions: [ + { + ignoreDifferentlyNamedParameters: false, + }, + ], + meta: { + docs: { + description: + 'Disallow two overloads that could be unified into one with a union or an optional/rest parameter', + // too opinionated to be recommended + recommended: 'strict', + }, + messages: { + omittingRestParameter: '{{failureStringStart}} with a rest parameter.', + omittingSingleParameter: + '{{failureStringStart}} with an optional parameter.', + singleParameterDifference: + '{{failureStringStart}} taking `{{type1}} | {{type2}}`.', + }, + schema: [ + { + additionalProperties: false, + properties: { + ignoreDifferentlyNamedParameters: { + description: + 'Whether two parameters with different names at the same index should be considered different even if their types are the same.', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + name: 'unified-signatures', }); function getExportingNode( diff --git a/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts b/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts index 0c9cf63adf2b..a593693d2210 100644 --- a/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts +++ b/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts @@ -1,10 +1,11 @@ import type { Scope } from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { ReportDescriptor } from '@typescript-eslint/utils/ts-eslint'; -import * as tsutils from 'ts-api-utils'; import type * as ts from 'typescript'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as tsutils from 'ts-api-utils'; + import { createRule, getParserServices, @@ -15,13 +16,13 @@ import { } from '../util'; type MessageIds = + | 'addUnknownRestTypeAnnotationSuggestion' + | 'addUnknownTypeAnnotationSuggestion' | 'useUnknown' | 'useUnknownArrayDestructuringPattern' | 'useUnknownObjectDestructuringPattern' - | 'addUnknownTypeAnnotationSuggestion' - | 'addUnknownRestTypeAnnotationSuggestion' - | 'wrongTypeAnnotationSuggestion' - | 'wrongRestTypeAnnotationSuggestion'; + | 'wrongRestTypeAnnotationSuggestion' + | 'wrongTypeAnnotationSuggestion'; const useUnknownMessageBase = 'Prefer the safe `: unknown` for a `{{method}}`{{append}} callback variable.'; @@ -40,39 +41,8 @@ const getStaticMemberAccessKey = ( computed ? getStaticValue(property, scope) : { value: property.name }; export default createRule<[], MessageIds>({ - name: 'use-unknown-in-catch-callback-variable', - meta: { - docs: { - description: - 'Enforce typing arguments in Promise rejection callbacks as `unknown`', - requiresTypeChecking: true, - recommended: 'strict', - }, - type: 'suggestion', - messages: { - useUnknown: useUnknownMessageBase, - useUnknownArrayDestructuringPattern: `${useUnknownMessageBase} The thrown error may not be iterable.`, - useUnknownObjectDestructuringPattern: `${ - useUnknownMessageBase - } The thrown error may be nullable, or may not have the expected shape.`, - addUnknownTypeAnnotationSuggestion: - 'Add an explicit `: unknown` type annotation to the rejection callback variable.', - addUnknownRestTypeAnnotationSuggestion: - 'Add an explicit `: [unknown]` type annotation to the rejection callback rest variable.', - wrongTypeAnnotationSuggestion: - 'Change existing type annotation to `: unknown`.', - wrongRestTypeAnnotationSuggestion: - 'Change existing type annotation to `: [unknown]`.', - }, - fixable: 'code', - schema: [], - hasSuggestions: true, - }, - - defaultOptions: [], - create(context) { - const { program, esTreeNodeToTSNodeMap } = getParserServices(context); + const { esTreeNodeToTSNodeMap, program } = getParserServices(context); const checker = program.getTypeChecker(); function isFlaggableHandlerType(type: ts.Type): boolean { @@ -128,7 +98,7 @@ export default createRule<[], MessageIds>({ */ function refineReportIfPossible( argument: TSESTree.Expression, - ): undefined | Partial> { + ): Partial> | undefined { // Only know how to be helpful if a function literal has been provided. if ( !( @@ -163,7 +133,6 @@ export default createRule<[], MessageIds>({ node: catchVariableOuter, suggest: [ { - messageId: 'addUnknownTypeAnnotationSuggestion', fix: (fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] => { if ( argument.type === @@ -180,6 +149,7 @@ export default createRule<[], MessageIds>({ fixer.insertTextAfter(catchVariableInner, ': unknown'), ]; }, + messageId: 'addUnknownTypeAnnotationSuggestion', }, ], }; @@ -189,23 +159,23 @@ export default createRule<[], MessageIds>({ node: catchVariableOuter, suggest: [ { - messageId: 'wrongTypeAnnotationSuggestion', fix: (fixer: TSESLint.RuleFixer): TSESLint.RuleFix => fixer.replaceText(catchVariableTypeAnnotation, ': unknown'), + messageId: 'wrongTypeAnnotationSuggestion', }, ], }; } case AST_NODE_TYPES.ArrayPattern: { return { - node: catchVariableOuter, messageId: 'useUnknownArrayDestructuringPattern', + node: catchVariableOuter, }; } case AST_NODE_TYPES.ObjectPattern: { return { - node: catchVariableOuter, messageId: 'useUnknownObjectDestructuringPattern', + node: catchVariableOuter, }; } case AST_NODE_TYPES.RestElement: { @@ -215,9 +185,9 @@ export default createRule<[], MessageIds>({ node: catchVariableOuter, suggest: [ { - messageId: 'addUnknownRestTypeAnnotationSuggestion', fix: (fixer): TSESLint.RuleFix => fixer.insertTextAfter(catchVariableInner, ': [unknown]'), + messageId: 'addUnknownRestTypeAnnotationSuggestion', }, ], }; @@ -226,9 +196,9 @@ export default createRule<[], MessageIds>({ node: catchVariableOuter, suggest: [ { - messageId: 'wrongRestTypeAnnotationSuggestion', fix: (fixer): TSESLint.RuleFix => fixer.replaceText(catchVariableTypeAnnotation, ': [unknown]'), + messageId: 'wrongRestTypeAnnotationSuggestion', }, ], }; @@ -252,12 +222,12 @@ export default createRule<[], MessageIds>({ const promiseMethodInfo = ( [ - { method: 'catch', append: '', argIndexToCheck: 0 }, - { method: 'then', append: ' rejection', argIndexToCheck: 1 }, + { append: '', argIndexToCheck: 0, method: 'catch' }, + { append: ' rejection', argIndexToCheck: 1, method: 'then' }, ] satisfies { - method: string; append: string; argIndexToCheck: number; + method: string; }[] ).find(({ method }) => staticMemberAccessKey.value === method); if (!promiseMethodInfo) { @@ -299,13 +269,44 @@ export default createRule<[], MessageIds>({ // to determine exactly where, and whether we can fix it. const overrides = refineReportIfPossible(node); context.report({ - node, - messageId: 'useUnknown', data, + messageId: 'useUnknown', + node, ...overrides, }); } }, }; }, + defaultOptions: [], + + meta: { + docs: { + description: + 'Enforce typing arguments in Promise rejection callbacks as `unknown`', + recommended: 'strict', + requiresTypeChecking: true, + }, + fixable: 'code', + hasSuggestions: true, + messages: { + addUnknownRestTypeAnnotationSuggestion: + 'Add an explicit `: [unknown]` type annotation to the rejection callback rest variable.', + addUnknownTypeAnnotationSuggestion: + 'Add an explicit `: unknown` type annotation to the rejection callback variable.', + useUnknown: useUnknownMessageBase, + useUnknownArrayDestructuringPattern: `${useUnknownMessageBase} The thrown error may not be iterable.`, + useUnknownObjectDestructuringPattern: `${ + useUnknownMessageBase + } The thrown error may be nullable, or may not have the expected shape.`, + wrongRestTypeAnnotationSuggestion: + 'Change existing type annotation to `: [unknown]`.', + wrongTypeAnnotationSuggestion: + 'Change existing type annotation to `: unknown`.', + }, + schema: [], + type: 'suggestion', + }, + + name: 'use-unknown-in-catch-callback-variable', }); diff --git a/packages/eslint-plugin/src/util/astUtils.ts b/packages/eslint-plugin/src/util/astUtils.ts index daa69bc85831..c2450ae30130 100644 --- a/packages/eslint-plugin/src/util/astUtils.ts +++ b/packages/eslint-plugin/src/util/astUtils.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import * as ts from 'typescript'; import { escapeRegExp } from './escapeRegExp'; @@ -37,11 +38,11 @@ export function getNameLocationInGlobalDirectiveComment( comment.range[0] + '/*'.length + (match ? match.index + 1 : 0), ); const end = { - line: start.line, column: start.column + (match ? name.length : 1), + line: start.line, }; - return { start, end }; + return { end, start }; } // Copied from typescript https://github.com/microsoft/TypeScript/blob/42b0e3c4630c129ca39ce0df9fff5f0d1b4dd348/src/compiler/utilities.ts#L1335 diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index f5602f52e2e7..074a986a00a3 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -2,12 +2,13 @@ import type { ScopeManager, ScopeVariable, } from '@typescript-eslint/scope-manager'; +import type { TSESTree } from '@typescript-eslint/utils'; + import { ImplicitLibVariable, ScopeType, Visitor, } from '@typescript-eslint/scope-manager'; -import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES, ASTUtils, @@ -40,6 +41,41 @@ class UnusedVarsVisitor extends Visitor { VariableAnalysis >(); + protected ClassDeclaration = this.visitClass; + + protected ClassExpression = this.visitClass; + + protected ForInStatement = this.visitForInForOf; + + protected ForOfStatement = this.visitForInForOf; + + //#region HELPERS + + protected FunctionDeclaration = this.visitFunction; + + protected FunctionExpression = this.visitFunction; + protected MethodDefinition = this.visitSetter; + protected Property = this.visitSetter; + + protected TSCallSignatureDeclaration = this.visitFunctionTypeSignature; + + protected TSConstructorType = this.visitFunctionTypeSignature; + + protected TSConstructSignatureDeclaration = this.visitFunctionTypeSignature; + + protected TSDeclareFunction = this.visitFunctionTypeSignature; + + protected TSEmptyBodyFunctionExpression = this.visitFunctionTypeSignature; + + //#endregion HELPERS + + //#region VISITORS + // NOTE - This is a simple visitor - meaning it does not support selectors + + protected TSFunctionType = this.visitFunctionTypeSignature; + + protected TSMethodSignature = this.visitFunctionTypeSignature; + readonly #scopeManager: TSESLint.Scope.ScopeManager; private constructor(scopeManager: ScopeManager) { @@ -69,6 +105,60 @@ class UnusedVarsVisitor extends Visitor { return unusedVars; } + protected Identifier(node: TSESTree.Identifier): void { + const scope = this.getScope(node); + if ( + scope.type === TSESLint.Scope.ScopeType.function && + node.name === 'this' + ) { + // this parameters should always be considered used as they're pseudo-parameters + if ('params' in scope.block && scope.block.params.includes(node)) { + this.markVariableAsUsed(node); + } + } + } + + protected TSEnumDeclaration(node: TSESTree.TSEnumDeclaration): void { + // enum members create variables because they can be referenced within the enum, + // but they obviously aren't unused variables for the purposes of this rule. + const scope = this.getScope(node); + for (const variable of scope.variables) { + this.markVariableAsUsed(variable); + } + } + + protected TSMappedType(node: TSESTree.TSMappedType): void { + // mapped types create a variable for their type name, but it's not necessary to reference it, + // so we shouldn't consider it as unused for the purpose of this rule. + this.markVariableAsUsed(node.key); + } + + protected TSModuleDeclaration(node: TSESTree.TSModuleDeclaration): void { + // -- global augmentation can be in any file, and they do not need exports + if (node.kind === 'global') { + this.markVariableAsUsed('global', node.parent); + } + } + + protected TSParameterProperty(node: TSESTree.TSParameterProperty): void { + let identifier: TSESTree.Identifier | null = null; + switch (node.parameter.type) { + case AST_NODE_TYPES.AssignmentPattern: + if (node.parameter.left.type === AST_NODE_TYPES.Identifier) { + identifier = node.parameter.left; + } + break; + + case AST_NODE_TYPES.Identifier: + identifier = node.parameter; + break; + } + + if (identifier) { + this.markVariableAsUsed(identifier); + } + } + private collectUnusedVariables( scope: TSESLint.Scope.Scope, variables: MutableVariableAnalysis = { @@ -116,8 +206,6 @@ class UnusedVarsVisitor extends Visitor { return variables; } - //#region HELPERS - private getScope(currentNode: TSESTree.Node): TSESLint.Scope.Scope { // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. const inner = currentNode.type !== AST_NODE_TYPES.Program; @@ -142,7 +230,9 @@ class UnusedVarsVisitor extends Visitor { private markVariableAsUsed( variableOrIdentifier: ScopeVariable | TSESTree.Identifier, ): void; + private markVariableAsUsed(name: string, parent: TSESTree.Node): void; + private markVariableAsUsed( variableOrIdentifierOrName: ScopeVariable | TSESTree.Identifier | string, parent?: TSESTree.Node, @@ -194,49 +284,6 @@ class UnusedVarsVisitor extends Visitor { } } - private visitFunction( - node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, - ): void { - const scope = this.getScope(node); - // skip implicit "arguments" variable - const variable = scope.set.get('arguments'); - if (variable?.defs.length === 0) { - this.markVariableAsUsed(variable); - } - } - - private visitFunctionTypeSignature( - node: - | TSESTree.TSCallSignatureDeclaration - | TSESTree.TSConstructorType - | TSESTree.TSConstructSignatureDeclaration - | TSESTree.TSDeclareFunction - | TSESTree.TSEmptyBodyFunctionExpression - | TSESTree.TSFunctionType - | TSESTree.TSMethodSignature, - ): void { - // function type signature params create variables because they can be referenced within the signature, - // but they obviously aren't unused variables for the purposes of this rule. - for (const param of node.params) { - this.visitPattern(param, name => { - this.markVariableAsUsed(name); - }); - } - } - - private visitSetter( - node: TSESTree.MethodDefinition | TSESTree.Property, - ): void { - if (node.kind === 'set') { - // ignore setter parameters because they're syntactically required to exist - for (const param of (node.value as TSESTree.FunctionLike).params) { - this.visitPattern(param, id => { - this.markVariableAsUsed(id); - }); - } - } - } - private visitForInForOf( node: TSESTree.ForInStatement | TSESTree.ForOfStatement, ): void { @@ -288,92 +335,46 @@ class UnusedVarsVisitor extends Visitor { this.markVariableAsUsed(idOrVariable); } - //#endregion HELPERS - - //#region VISITORS - // NOTE - This is a simple visitor - meaning it does not support selectors - - protected ClassDeclaration = this.visitClass; - - protected ClassExpression = this.visitClass; - - protected FunctionDeclaration = this.visitFunction; - - protected FunctionExpression = this.visitFunction; - - protected ForInStatement = this.visitForInForOf; - - protected ForOfStatement = this.visitForInForOf; - - protected Identifier(node: TSESTree.Identifier): void { - const scope = this.getScope(node); - if ( - scope.type === TSESLint.Scope.ScopeType.function && - node.name === 'this' - ) { - // this parameters should always be considered used as they're pseudo-parameters - if ('params' in scope.block && scope.block.params.includes(node)) { - this.markVariableAsUsed(node); - } - } - } - - protected MethodDefinition = this.visitSetter; - - protected Property = this.visitSetter; - - protected TSCallSignatureDeclaration = this.visitFunctionTypeSignature; - - protected TSConstructorType = this.visitFunctionTypeSignature; - - protected TSConstructSignatureDeclaration = this.visitFunctionTypeSignature; - - protected TSDeclareFunction = this.visitFunctionTypeSignature; - - protected TSEmptyBodyFunctionExpression = this.visitFunctionTypeSignature; - - protected TSEnumDeclaration(node: TSESTree.TSEnumDeclaration): void { - // enum members create variables because they can be referenced within the enum, - // but they obviously aren't unused variables for the purposes of this rule. + private visitFunction( + node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, + ): void { const scope = this.getScope(node); - for (const variable of scope.variables) { + // skip implicit "arguments" variable + const variable = scope.set.get('arguments'); + if (variable?.defs.length === 0) { this.markVariableAsUsed(variable); } } - protected TSFunctionType = this.visitFunctionTypeSignature; - - protected TSMappedType(node: TSESTree.TSMappedType): void { - // mapped types create a variable for their type name, but it's not necessary to reference it, - // so we shouldn't consider it as unused for the purpose of this rule. - this.markVariableAsUsed(node.key); - } - - protected TSMethodSignature = this.visitFunctionTypeSignature; - - protected TSModuleDeclaration(node: TSESTree.TSModuleDeclaration): void { - // -- global augmentation can be in any file, and they do not need exports - if (node.kind === 'global') { - this.markVariableAsUsed('global', node.parent); + private visitFunctionTypeSignature( + node: + | TSESTree.TSCallSignatureDeclaration + | TSESTree.TSConstructorType + | TSESTree.TSConstructSignatureDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.TSEmptyBodyFunctionExpression + | TSESTree.TSFunctionType + | TSESTree.TSMethodSignature, + ): void { + // function type signature params create variables because they can be referenced within the signature, + // but they obviously aren't unused variables for the purposes of this rule. + for (const param of node.params) { + this.visitPattern(param, name => { + this.markVariableAsUsed(name); + }); } } - protected TSParameterProperty(node: TSESTree.TSParameterProperty): void { - let identifier: TSESTree.Identifier | null = null; - switch (node.parameter.type) { - case AST_NODE_TYPES.AssignmentPattern: - if (node.parameter.left.type === AST_NODE_TYPES.Identifier) { - identifier = node.parameter.left; - } - break; - - case AST_NODE_TYPES.Identifier: - identifier = node.parameter; - break; - } - - if (identifier) { - this.markVariableAsUsed(identifier); + private visitSetter( + node: TSESTree.MethodDefinition | TSESTree.Property, + ): void { + if (node.kind === 'set') { + // ignore setter parameters because they're syntactically required to exist + for (const param of (node.value as TSESTree.FunctionLike).params) { + this.visitPattern(param, id => { + this.markVariableAsUsed(id); + }); + } } } diff --git a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts index f65273a502ef..4adbe38132c2 100644 --- a/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts +++ b/packages/eslint-plugin/src/util/explicitReturnTypeUtils.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, ASTUtils, @@ -199,10 +200,10 @@ function returnsConstAssertionDirectly( } interface Options { + allowDirectConstAssertionInArrowFunctions?: boolean; allowExpressions?: boolean; - allowTypedFunctionExpressions?: boolean; allowHigherOrderFunctions?: boolean; - allowDirectConstAssertionInArrowFunctions?: boolean; + allowTypedFunctionExpressions?: boolean; } /** @@ -362,6 +363,7 @@ function ancestorHasReturnType(node: FunctionNode): boolean { } export { + ancestorHasReturnType, checkFunctionExpressionReturnType, checkFunctionReturnType, doesImmediatelyReturnFunctionExpression, @@ -369,5 +371,4 @@ export { FunctionNode, isTypedFunctionExpression, isValidFunctionExpressionReturnType, - ancestorHasReturnType, }; diff --git a/packages/eslint-plugin/src/util/getESLintCoreRule.ts b/packages/eslint-plugin/src/util/getESLintCoreRule.ts index 59b22c7292fd..97bc8620b01d 100644 --- a/packages/eslint-plugin/src/util/getESLintCoreRule.ts +++ b/packages/eslint-plugin/src/util/getESLintCoreRule.ts @@ -16,11 +16,11 @@ interface RuleMap { 'no-loop-func': typeof import('eslint/lib/rules/no-loop-func'); 'no-loss-of-precision': typeof import('eslint/lib/rules/no-loss-of-precision'); 'no-magic-numbers': typeof import('eslint/lib/rules/no-magic-numbers'); + 'no-restricted-globals': typeof import('eslint/lib/rules/no-restricted-globals'); 'no-restricted-imports': typeof import('eslint/lib/rules/no-restricted-imports'); 'no-undef': typeof import('eslint/lib/rules/no-undef'); 'no-unused-expressions': typeof import('eslint/lib/rules/no-unused-expressions'); 'no-useless-constructor': typeof import('eslint/lib/rules/no-useless-constructor'); - 'no-restricted-globals': typeof import('eslint/lib/rules/no-restricted-globals'); 'prefer-const': typeof import('eslint/lib/rules/prefer-const'); 'prefer-destructuring': typeof import('eslint/lib/rules/prefer-destructuring'); strict: typeof import('eslint/lib/rules/strict'); diff --git a/packages/eslint-plugin/src/util/getFixOrSuggest.ts b/packages/eslint-plugin/src/util/getFixOrSuggest.ts index 9e67075aa126..50ad7d2779a1 100644 --- a/packages/eslint-plugin/src/util/getFixOrSuggest.ts +++ b/packages/eslint-plugin/src/util/getFixOrSuggest.ts @@ -1,11 +1,11 @@ import type { TSESLint } from '@typescript-eslint/utils'; export function getFixOrSuggest({ - useFix, suggestion, + useFix, }: { - useFix: boolean; suggestion: TSESLint.SuggestionReportDescriptor; + useFix: boolean; }): | { fix: TSESLint.ReportFixFunction } | { suggest: TSESLint.SuggestionReportDescriptor[] } { diff --git a/packages/eslint-plugin/src/util/getForStatementHeadLoc.ts b/packages/eslint-plugin/src/util/getForStatementHeadLoc.ts index 8a7711fbe0b6..5a0a8fbc53a9 100644 --- a/packages/eslint-plugin/src/util/getForStatementHeadLoc.ts +++ b/packages/eslint-plugin/src/util/getForStatementHeadLoc.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { nullThrows } from '@typescript-eslint/utils/eslint-utils'; /** @@ -28,7 +29,7 @@ export function getForStatementHeadLoc( 'for statement must have a closing parenthesis.', ); return { - start: structuredClone(node.loc.start), end: structuredClone(closingParens.loc.end), + start: structuredClone(node.loc.start), }; } diff --git a/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts b/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts index 56227d7ed11d..49ab2b742dd9 100644 --- a/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts +++ b/packages/eslint-plugin/src/util/getFunctionHeadLoc.ts @@ -1,6 +1,7 @@ // adapted from https://github.com/eslint/eslint/blob/5bdaae205c3a0089ea338b382df59e21d5b06436/lib/rules/utils/ast-utils.js#L1668-L1787 import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'; import { isArrowToken, isOpeningParenToken } from './astUtils'; @@ -197,7 +198,7 @@ export function getFunctionHeadLoc( } return { - start: { ...start }, end: { ...end }, + start: { ...start }, }; } diff --git a/packages/eslint-plugin/src/util/getMemberHeadLoc.ts b/packages/eslint-plugin/src/util/getMemberHeadLoc.ts index 19401230e235..d28058d45691 100644 --- a/packages/eslint-plugin/src/util/getMemberHeadLoc.ts +++ b/packages/eslint-plugin/src/util/getMemberHeadLoc.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { nullThrows, NullThrowsReasons, @@ -30,8 +31,8 @@ export function getMemberHeadLoc( sourceCode: Readonly, node: | TSESTree.MethodDefinition - | TSESTree.TSAbstractMethodDefinition | TSESTree.PropertyDefinition + | TSESTree.TSAbstractMethodDefinition | TSESTree.TSAbstractPropertyDefinition, ): TSESTree.SourceLocation { let start: TSESTree.Position; @@ -60,8 +61,8 @@ export function getMemberHeadLoc( } return { - start: structuredClone(start), end: structuredClone(end), + start: structuredClone(start), }; } @@ -102,7 +103,7 @@ export function getParameterPropertyHeadLoc( ); return { - start, end, + start, }; } diff --git a/packages/eslint-plugin/src/util/getOperatorPrecedence.ts b/packages/eslint-plugin/src/util/getOperatorPrecedence.ts index 8aac3d6fbd2d..950708015d89 100644 --- a/packages/eslint-plugin/src/util/getOperatorPrecedence.ts +++ b/packages/eslint-plugin/src/util/getOperatorPrecedence.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/prefer-literal-enum-member -- the enums come from TS so to make merging upstream easier we purposely avoid adding literal values. */ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { SyntaxKind } from 'typescript'; @@ -320,27 +321,27 @@ export function getOperatorPrecedence( case SyntaxKind.BinaryExpression: switch (operatorKind) { - case SyntaxKind.CommaToken: - return OperatorPrecedence.Comma; - - case SyntaxKind.EqualsToken: - case SyntaxKind.PlusEqualsToken: - case SyntaxKind.MinusEqualsToken: + case SyntaxKind.AmpersandAmpersandEqualsToken: + case SyntaxKind.AmpersandEqualsToken: case SyntaxKind.AsteriskAsteriskEqualsToken: case SyntaxKind.AsteriskEqualsToken: - case SyntaxKind.SlashEqualsToken: - case SyntaxKind.PercentEqualsToken: - case SyntaxKind.LessThanLessThanEqualsToken: + case SyntaxKind.BarBarEqualsToken: + case SyntaxKind.BarEqualsToken: + case SyntaxKind.CaretEqualsToken: + case SyntaxKind.EqualsToken: case SyntaxKind.GreaterThanGreaterThanEqualsToken: case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - case SyntaxKind.AmpersandEqualsToken: - case SyntaxKind.CaretEqualsToken: - case SyntaxKind.BarEqualsToken: - case SyntaxKind.BarBarEqualsToken: - case SyntaxKind.AmpersandAmpersandEqualsToken: + case SyntaxKind.LessThanLessThanEqualsToken: + case SyntaxKind.MinusEqualsToken: + case SyntaxKind.PercentEqualsToken: + case SyntaxKind.PlusEqualsToken: case SyntaxKind.QuestionQuestionEqualsToken: + case SyntaxKind.SlashEqualsToken: return OperatorPrecedence.Assignment; + case SyntaxKind.CommaToken: + return OperatorPrecedence.Comma; + default: return getBinaryOperatorPrecedence(operatorKind); } @@ -410,81 +411,81 @@ export function getBinaryOperatorPrecedence( kind: SyntaxKind | TSESTreeOperatorKind, ): OperatorPrecedence { switch (kind) { - case SyntaxKind.QuestionQuestionToken: + case '-': + case '+': + case SyntaxKind.MinusToken: + case SyntaxKind.PlusToken: + return OperatorPrecedence.Additive; + + case '!=': + case '!==': + case '==': + case '===': + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + return OperatorPrecedence.Equality; + case '??': + case SyntaxKind.QuestionQuestionToken: return OperatorPrecedence.Coalesce; - case SyntaxKind.BarBarToken: - case '||': - return OperatorPrecedence.LogicalOR; + case '*': + case '/': + case '%': + case SyntaxKind.AsteriskToken: + case SyntaxKind.PercentToken: + case SyntaxKind.SlashToken: + return OperatorPrecedence.Multiplicative; + + case '**': + case SyntaxKind.AsteriskAsteriskToken: + return OperatorPrecedence.Exponentiation; + + case '&': + case SyntaxKind.AmpersandToken: + return OperatorPrecedence.BitwiseAND; - case SyntaxKind.AmpersandAmpersandToken: case '&&': + case SyntaxKind.AmpersandAmpersandToken: return OperatorPrecedence.LogicalAND; - case SyntaxKind.BarToken: - case '|': - return OperatorPrecedence.BitwiseOR; - - case SyntaxKind.CaretToken: case '^': + case SyntaxKind.CaretToken: return OperatorPrecedence.BitwiseXOR; - case SyntaxKind.AmpersandToken: - case '&': - return OperatorPrecedence.BitwiseAND; - - case SyntaxKind.EqualsEqualsToken: - case '==': - case SyntaxKind.ExclamationEqualsToken: - case '!=': - case SyntaxKind.EqualsEqualsEqualsToken: - case '===': - case SyntaxKind.ExclamationEqualsEqualsToken: - case '!==': - return OperatorPrecedence.Equality; - - case SyntaxKind.LessThanToken: case '<': - case SyntaxKind.GreaterThanToken: - case '>': - case SyntaxKind.LessThanEqualsToken: case '<=': - case SyntaxKind.GreaterThanEqualsToken: + case '>': case '>=': - case SyntaxKind.InstanceOfKeyword: - case 'instanceof': - case SyntaxKind.InKeyword: case 'in': + case 'instanceof': case SyntaxKind.AsKeyword: + case SyntaxKind.GreaterThanEqualsToken: + case SyntaxKind.GreaterThanToken: + case SyntaxKind.InKeyword: + case SyntaxKind.InstanceOfKeyword: + case SyntaxKind.LessThanEqualsToken: + case SyntaxKind.LessThanToken: // case 'as': -- we don't have a token for this return OperatorPrecedence.Relational; - case SyntaxKind.LessThanLessThanToken: case '<<': - case SyntaxKind.GreaterThanGreaterThanToken: case '>>': - case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: case '>>>': + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanToken: + case SyntaxKind.LessThanLessThanToken: return OperatorPrecedence.Shift; - case SyntaxKind.PlusToken: - case '+': - case SyntaxKind.MinusToken: - case '-': - return OperatorPrecedence.Additive; - - case SyntaxKind.AsteriskToken: - case '*': - case SyntaxKind.SlashToken: - case '/': - case SyntaxKind.PercentToken: - case '%': - return OperatorPrecedence.Multiplicative; + case '|': + case SyntaxKind.BarToken: + return OperatorPrecedence.BitwiseOR; - case SyntaxKind.AsteriskAsteriskToken: - case '**': - return OperatorPrecedence.Exponentiation; + case '||': + case SyntaxKind.BarBarToken: + return OperatorPrecedence.LogicalOR; } // -1 is lower than all other precedences. Returning it will cause binary expression diff --git a/packages/eslint-plugin/src/util/getStaticStringValue.ts b/packages/eslint-plugin/src/util/getStaticStringValue.ts index 6eeaf9af8ce3..103e53154ba2 100644 --- a/packages/eslint-plugin/src/util/getStaticStringValue.ts +++ b/packages/eslint-plugin/src/util/getStaticStringValue.ts @@ -1,6 +1,7 @@ // adapted from https://github.com/eslint/eslint/blob/5bdaae205c3a0089ea338b382df59e21d5b06436/lib/rules/utils/ast-utils.js#L191-L230 import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { isNullLiteral } from './isNullLiteral'; diff --git a/packages/eslint-plugin/src/util/getThisExpression.ts b/packages/eslint-plugin/src/util/getThisExpression.ts index 196254c35f96..5bcd33bdf089 100644 --- a/packages/eslint-plugin/src/util/getThisExpression.ts +++ b/packages/eslint-plugin/src/util/getThisExpression.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; export function getThisExpression( diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index cb45f1cdfc3d..64c76337c7f9 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -1,4 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES, ASTUtils, @@ -6,10 +7,6 @@ import { } from '@typescript-eslint/utils'; interface WrappingFixerParams { - /** Source code. */ - sourceCode: Readonly; - /** The node we want to modify. */ - node: TSESTree.Node; /** * Descendant of `node` we want to preserve. * Use this to replace some code with another. @@ -17,6 +14,10 @@ interface WrappingFixerParams { * You can pass multiple nodes as an array. */ innerNode?: TSESTree.Node | TSESTree.Node[]; + /** The node we want to modify. */ + node: TSESTree.Node; + /** Source code. */ + sourceCode: Readonly; /** * The function which gets the code of the `innerNode` and returns some code around it. * Receives multiple arguments if there are multiple innerNodes. @@ -32,7 +33,7 @@ interface WrappingFixerParams { export function getWrappingFixer( params: WrappingFixerParams, ): TSESLint.ReportFixFunction { - const { sourceCode, node, innerNode = node, wrap } = params; + const { node, innerNode = node, sourceCode, wrap } = params; const innerNodes = Array.isArray(innerNode) ? innerNode : [innerNode]; return (fixer): TSESLint.RuleFix => { @@ -82,11 +83,11 @@ export function getWrappingFixer( * @returns If parentheses are required, code for the nodeToMove node is returned with parentheses at both ends of the code. */ export function getMovedNodeCode(params: { - sourceCode: Readonly; - nodeToMove: TSESTree.Node; destinationNode: TSESTree.Node; + nodeToMove: TSESTree.Node; + sourceCode: Readonly; }): string { - const { sourceCode, nodeToMove: existingNode, destinationNode } = params; + const { destinationNode, nodeToMove: existingNode, sourceCode } = params; const code = sourceCode.getText(existingNode); if (isStrongPrecedenceNode(existingNode)) { // Moved node never needs parens diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 4ceb0b42f2ce..4c7f791c4af7 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -3,6 +3,7 @@ import { ESLintUtils } from '@typescript-eslint/utils'; export * from './astUtils'; export * from './collectUnusedVariables'; export * from './createRule'; +export * from './getFixOrSuggest'; export * from './getFunctionHeadLoc'; export * from './getOperatorPrecedence'; export * from './getStaticStringValue'; @@ -10,6 +11,7 @@ export * from './getStringLength'; export * from './getTextWithParentheses'; export * from './getThisExpression'; export * from './getWrappingFixer'; +export * from './isAssignee'; export * from './isNodeEqual'; export * from './isNullLiteral'; export * from './isStartOfExpressionStatement'; @@ -19,16 +21,14 @@ export * from './needsPrecedingSemiColon'; export * from './objectIterators'; export * from './scopeUtils'; export * from './types'; -export * from './isAssignee'; -export * from './getFixOrSuggest'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; const { applyDefault, deepMerge, - isObjectNotArray, getParserServices, + isObjectNotArray, nullThrows, NullThrowsReasons, } = ESLintUtils; @@ -39,10 +39,10 @@ type InferOptionsTypeFromRule = ESLintUtils.InferOptionsTypeFromRule; export { applyDefault, deepMerge, - isObjectNotArray, getParserServices, - nullThrows, InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, + isObjectNotArray, + nullThrows, NullThrowsReasons, }; diff --git a/packages/eslint-plugin/src/util/isAssignee.ts b/packages/eslint-plugin/src/util/isAssignee.ts index 390243152503..9bf694827722 100644 --- a/packages/eslint-plugin/src/util/isAssignee.ts +++ b/packages/eslint-plugin/src/util/isAssignee.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; export function isAssignee(node: TSESTree.Node): boolean { diff --git a/packages/eslint-plugin/src/util/isNodeEqual.ts b/packages/eslint-plugin/src/util/isNodeEqual.ts index d783d8184285..729b38b58888 100644 --- a/packages/eslint-plugin/src/util/isNodeEqual.ts +++ b/packages/eslint-plugin/src/util/isNodeEqual.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; export function isNodeEqual(a: TSESTree.Node, b: TSESTree.Node): boolean { diff --git a/packages/eslint-plugin/src/util/isNullLiteral.ts b/packages/eslint-plugin/src/util/isNullLiteral.ts index d59a926c5aaa..a54aec1725a8 100644 --- a/packages/eslint-plugin/src/util/isNullLiteral.ts +++ b/packages/eslint-plugin/src/util/isNullLiteral.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; export function isNullLiteral(i: TSESTree.Node): i is TSESTree.NullLiteral { diff --git a/packages/eslint-plugin/src/util/isStartOfExpressionStatement.ts b/packages/eslint-plugin/src/util/isStartOfExpressionStatement.ts index 76f920dfbeb8..f5d39061c23c 100644 --- a/packages/eslint-plugin/src/util/isStartOfExpressionStatement.ts +++ b/packages/eslint-plugin/src/util/isStartOfExpressionStatement.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; // The following is copied from `eslint`'s source code. diff --git a/packages/eslint-plugin/src/util/isTypeImport.ts b/packages/eslint-plugin/src/util/isTypeImport.ts index 6e8af22d751c..b1c88d813a7d 100644 --- a/packages/eslint-plugin/src/util/isTypeImport.ts +++ b/packages/eslint-plugin/src/util/isTypeImport.ts @@ -2,6 +2,7 @@ import type { Definition, ImportBindingDefinition, } from '@typescript-eslint/scope-manager'; + import { DefinitionType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; diff --git a/packages/eslint-plugin/src/util/isUndefinedIdentifier.ts b/packages/eslint-plugin/src/util/isUndefinedIdentifier.ts index 75c301a1ea49..4436ead15d68 100644 --- a/packages/eslint-plugin/src/util/isUndefinedIdentifier.ts +++ b/packages/eslint-plugin/src/util/isUndefinedIdentifier.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; export function isUndefinedIdentifier(i: TSESTree.Node): boolean { diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 46ad05c3a0e1..34b5c1ae29e5 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -2,8 +2,9 @@ * @fileoverview Really small utility functions that didn't deserve their own files */ -import { requiresQuoting } from '@typescript-eslint/type-utils'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +import { requiresQuoting } from '@typescript-eslint/type-utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -97,10 +98,10 @@ function getNameFromIndexSignature(node: TSESTree.TSIndexSignature): string { } enum MemberNameType { + Expression = 4, + Normal = 3, Private = 1, Quoted = 2, - Normal = 3, - Expression = 4, } /** @@ -109,46 +110,46 @@ enum MemberNameType { */ function getNameFromMember( member: - | TSESTree.MethodDefinition | TSESTree.AccessorProperty + | TSESTree.MethodDefinition | TSESTree.Property | TSESTree.PropertyDefinition - | TSESTree.TSAbstractMethodDefinition | TSESTree.TSAbstractAccessorProperty + | TSESTree.TSAbstractMethodDefinition | TSESTree.TSAbstractPropertyDefinition | TSESTree.TSMethodSignature | TSESTree.TSPropertySignature, sourceCode: TSESLint.SourceCode, -): { type: MemberNameType; name: string } { +): { name: string; type: MemberNameType } { if (member.key.type === AST_NODE_TYPES.Identifier) { return { - type: MemberNameType.Normal, name: member.key.name, + type: MemberNameType.Normal, }; } if (member.key.type === AST_NODE_TYPES.PrivateIdentifier) { return { - type: MemberNameType.Private, name: `#${member.key.name}`, + type: MemberNameType.Private, }; } if (member.key.type === AST_NODE_TYPES.Literal) { const name = `${member.key.value}`; if (requiresQuoting(name)) { return { - type: MemberNameType.Quoted, name: `"${name}"`, + type: MemberNameType.Quoted, }; } return { - type: MemberNameType.Normal, name, + type: MemberNameType.Normal, }; } return { - type: MemberNameType.Expression, name: sourceCode.text.slice(...member.key.range), + type: MemberNameType.Expression, }; } @@ -159,7 +160,7 @@ type ExcludeKeys< type RequireKeys< Obj extends Record, Keys extends keyof Obj, -> = ExcludeKeys & { [k in Keys]-?: Exclude }; +> = { [k in Keys]-?: Exclude } & ExcludeKeys; function getEnumNames(myEnum: Record): T[] { return Object.keys(myEnum).filter(x => isNaN(Number(x))) as T[]; @@ -238,16 +239,16 @@ export { Equal, ExcludeKeys, findFirstResult, + findLastIndex, formatWordList, getEnumNames, getNameFromIndexSignature, getNameFromMember, isDefinitionFile, - isRestParameterDeclaration, isParenlessArrowFunction, + isRestParameterDeclaration, MemberNameType, RequireKeys, typeNodeRequiresParentheses, upperCaseFirst, - findLastIndex, }; diff --git a/packages/eslint-plugin/src/util/needsPrecedingSemiColon.ts b/packages/eslint-plugin/src/util/needsPrecedingSemiColon.ts index 5f9a94f551f4..84d0901a35c0 100644 --- a/packages/eslint-plugin/src/util/needsPrecedingSemiColon.ts +++ b/packages/eslint-plugin/src/util/needsPrecedingSemiColon.ts @@ -1,10 +1,11 @@ import type { TSESTree } from '@typescript-eslint/utils'; +import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; + import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import { isClosingBraceToken, isClosingParenToken, } from '@typescript-eslint/utils/ast-utils'; -import type { SourceCode } from '@typescript-eslint/utils/ts-eslint'; // The following is adapted from `eslint`'s source code. // https://github.com/eslint/eslint/blob/3a4eaf921543b1cd5d1df4ea9dec02fab396af2a/lib/rules/utils/ast-utils.js#L1043-L1132 diff --git a/packages/eslint-plugin/src/util/referenceContainsTypeQuery.ts b/packages/eslint-plugin/src/util/referenceContainsTypeQuery.ts index 60872beeddb5..2d5a42a102e2 100644 --- a/packages/eslint-plugin/src/util/referenceContainsTypeQuery.ts +++ b/packages/eslint-plugin/src/util/referenceContainsTypeQuery.ts @@ -1,4 +1,5 @@ import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; /** diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index 84991966a237..0765b2683d6e 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -1,5 +1,5 @@ -export type MakeRequired = Omit & { +export type MakeRequired = { [K in Key]-?: NonNullable; -}; +} & Omit; export type ValueOf = T[keyof T]; diff --git a/packages/eslint-plugin/tests/RuleTester.ts b/packages/eslint-plugin/tests/RuleTester.ts index 12511185f9ae..bc0317c8a466 100644 --- a/packages/eslint-plugin/tests/RuleTester.ts +++ b/packages/eslint-plugin/tests/RuleTester.ts @@ -1,10 +1,10 @@ -import * as path from 'node:path'; - import type { InvalidTestCase, ValidTestCase, } from '@typescript-eslint/rule-tester'; +import * as path from 'node:path'; + export function getFixturesRootDir(): string { return path.join(__dirname, 'fixtures'); } @@ -46,9 +46,9 @@ export function batchedSingleLineTests< Options extends readonly unknown[], >( options: - | (Omit, 'output'> & { + | ({ output?: string | null; - }) + } & Omit, 'output'>) | ValidTestCase, ): (InvalidTestCase | ValidTestCase)[] { // -- eslint counts lines from 1 diff --git a/packages/eslint-plugin/tests/areOptionsValid.test.ts b/packages/eslint-plugin/tests/areOptionsValid.test.ts index 1908acf1460f..dd5ddfabc12d 100644 --- a/packages/eslint-plugin/tests/areOptionsValid.test.ts +++ b/packages/eslint-plugin/tests/areOptionsValid.test.ts @@ -2,19 +2,19 @@ import { createRule } from '../src/util'; import { areOptionsValid } from './areOptionsValid'; const exampleRule = createRule<['value-a' | 'value-b'], never>({ - name: 'my-example-rule', + create() { + return {}; + }, + defaultOptions: ['value-a'], meta: { - type: 'suggestion', docs: { description: 'Detects something or other', }, - schema: [{ type: 'string', enum: ['value-a', 'value-b'] }], messages: {}, + schema: [{ enum: ['value-a', 'value-b'], type: 'string' }], + type: 'suggestion', }, - defaultOptions: ['value-a'], - create() { - return {}; - }, + name: 'my-example-rule', }); test('returns true for valid options', () => { diff --git a/packages/eslint-plugin/tests/areOptionsValid.ts b/packages/eslint-plugin/tests/areOptionsValid.ts index 807653deb2ff..177d000e0ac8 100644 --- a/packages/eslint-plugin/tests/areOptionsValid.ts +++ b/packages/eslint-plugin/tests/areOptionsValid.ts @@ -1,8 +1,9 @@ -import { TSUtils } from '@typescript-eslint/utils'; import type { RuleModule } from '@typescript-eslint/utils/ts-eslint'; -import Ajv from 'ajv'; import type { JSONSchema4 } from 'json-schema'; +import { TSUtils } from '@typescript-eslint/utils'; +import Ajv from 'ajv'; + const ajv = new Ajv({ async: false }); export function areOptionsValid( @@ -29,16 +30,16 @@ function normalizeSchema( if (schema.length === 0) { return { - type: 'array', - minItems: 0, maxItems: 0, + minItems: 0, + type: 'array', }; } return { - type: 'array', items: schema as JSONSchema4[], - minItems: 0, maxItems: schema.length, + minItems: 0, + type: 'array', }; } diff --git a/packages/eslint-plugin/tests/configs.test.ts b/packages/eslint-plugin/tests/configs.test.ts index 22da970a907d..ebb7f240193a 100644 --- a/packages/eslint-plugin/tests/configs.test.ts +++ b/packages/eslint-plugin/tests/configs.test.ts @@ -24,8 +24,8 @@ function entriesToObject(value: [string, T][]): Record { } function filterRules( - values: Record, -): [string, string | unknown[]][] { + values: Record, +): [string, unknown[] | string][] { return Object.entries(values).filter(([name]) => name.startsWith(RULE_NAME_PREFIX), ); @@ -33,14 +33,14 @@ function filterRules( interface FilterAndMapRuleConfigsSettings { excludeDeprecated?: boolean; - typeChecked?: 'exclude' | 'include-only'; recommendations?: (RuleRecommendation | undefined)[]; + typeChecked?: 'exclude' | 'include-only'; } function filterAndMapRuleConfigs({ excludeDeprecated, - typeChecked, recommendations, + typeChecked, }: FilterAndMapRuleConfigsSettings = {}): [string, unknown][] { let result = Object.entries(rules); @@ -91,7 +91,7 @@ function filterAndMapRuleConfigs({ } function itHasBaseRulesOverriden( - unfilteredConfigRules: Record, + unfilteredConfigRules: Record, ): void { it('has the base rules overriden by the appropriate extension rules', () => { const ruleNames = new Set(Object.keys(unfilteredConfigRules)); @@ -147,8 +147,8 @@ describe('recommended.ts', () => { const configRules = filterRules(unfilteredConfigRules); // note: include deprecated rules so that the config doesn't change between major bumps const ruleConfigs = filterAndMapRuleConfigs({ - typeChecked: 'exclude', recommendations: ['recommended'], + typeChecked: 'exclude', }); expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); @@ -182,8 +182,8 @@ describe('recommended-type-checked-only.ts', () => { const configRules = filterRules(unfilteredConfigRules); // note: include deprecated rules so that the config doesn't change between major bumps const ruleConfigs = filterAndMapRuleConfigs({ - typeChecked: 'include-only', recommendations: ['recommended'], + typeChecked: 'include-only', }).filter(([ruleName]) => ruleName); expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); @@ -193,7 +193,7 @@ describe('recommended-type-checked-only.ts', () => { }); describe('strict.ts', () => { - const unfilteredConfigRules: Record = + const unfilteredConfigRules: Record = plugin.configs.strict.rules; it('contains all strict rules, excluding type checked ones', () => { @@ -201,8 +201,8 @@ describe('strict.ts', () => { // note: exclude deprecated rules, this config is allowed to change between minor versions const ruleConfigs = filterAndMapRuleConfigs({ excludeDeprecated: true, - typeChecked: 'exclude', recommendations: ['recommended', 'strict'], + typeChecked: 'exclude', }); expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); @@ -212,7 +212,7 @@ describe('strict.ts', () => { }); describe('strict-type-checked.ts', () => { - const unfilteredConfigRules: Record = + const unfilteredConfigRules: Record = plugin.configs['strict-type-checked'].rules; it('contains all strict rules', () => { @@ -229,7 +229,7 @@ describe('strict-type-checked.ts', () => { }); describe('strict-type-checked-only.ts', () => { - const unfilteredConfigRules: Record = + const unfilteredConfigRules: Record = plugin.configs['strict-type-checked-only'].rules; it('contains only type-checked strict rules', () => { @@ -237,8 +237,8 @@ describe('strict-type-checked-only.ts', () => { // note: exclude deprecated rules, this config is allowed to change between minor versions const ruleConfigs = filterAndMapRuleConfigs({ excludeDeprecated: true, - typeChecked: 'include-only', recommendations: ['recommended', 'strict'], + typeChecked: 'include-only', }).filter(([ruleName]) => ruleName); expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); @@ -248,15 +248,15 @@ describe('strict-type-checked-only.ts', () => { }); describe('stylistic.ts', () => { - const unfilteredConfigRules: Record = + const unfilteredConfigRules: Record = plugin.configs.stylistic.rules; it('contains all stylistic rules, excluding deprecated or type checked ones', () => { const configRules = filterRules(unfilteredConfigRules); // note: include deprecated rules so that the config doesn't change between major bumps const ruleConfigs = filterAndMapRuleConfigs({ - typeChecked: 'exclude', recommendations: ['stylistic'], + typeChecked: 'exclude', }); expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); @@ -289,8 +289,8 @@ describe('stylistic-type-checked-only.ts', () => { const configRules = filterRules(unfilteredConfigRules); // note: include deprecated rules so that the config doesn't change between major bumps const ruleConfigs = filterAndMapRuleConfigs({ - typeChecked: 'include-only', recommendations: ['stylistic'], + typeChecked: 'include-only', }).filter(([ruleName]) => ruleName); expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index ee518640efed..7b4855cb8167 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -1,19 +1,18 @@ -import 'jest-specific-snapshot'; - -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import path from 'node:path'; +import type * as mdast from 'mdast'; +import type { fromMarkdown as FromMarkdown } from 'mdast-util-from-markdown' with { 'resolution-mode': 'import' }; +import type { mdxFromMarkdown as MdxFromMarkdown } from 'mdast-util-mdx' with { 'resolution-mode': 'import' }; +import type { mdxjs as Mdxjs } from 'micromark-extension-mdxjs' with { 'resolution-mode': 'import' }; +import type * as UnistUtilVisit from 'unist-util-visit' with { 'resolution-mode': 'import' }; import { parseForESLint } from '@typescript-eslint/parser'; import * as tseslintParser from '@typescript-eslint/parser'; import { Linter } from '@typescript-eslint/utils/ts-eslint'; +import 'jest-specific-snapshot'; import { marked } from 'marked'; -import type * as mdast from 'mdast'; -import type { fromMarkdown as FromMarkdown } from 'mdast-util-from-markdown' with { 'resolution-mode': 'import' }; -import type { mdxFromMarkdown as MdxFromMarkdown } from 'mdast-util-mdx' with { 'resolution-mode': 'import' }; -import type { mdxjs as Mdxjs } from 'micromark-extension-mdxjs' with { 'resolution-mode': 'import' }; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; import { titleCase } from 'title-case'; -import type * as UnistUtilVisit from 'unist-util-visit' with { 'resolution-mode': 'import' }; import rules from '../src/rules'; import { areOptionsValid } from './areOptionsValid'; @@ -43,7 +42,7 @@ type TokenType = marked.Token['type']; function tokenIs( token: marked.Token, type: Type, -): token is marked.Token & { type: Type } { +): token is { type: Type } & marked.Token { return token.type === type; } @@ -53,7 +52,7 @@ function tokenIsHeading(token: marked.Token): token is marked.Tokens.Heading { function tokenIsH2( token: marked.Token, -): token is marked.Tokens.Heading & { depth: 2 } { +): token is { depth: 2 } & marked.Tokens.Heading { return ( tokenIsHeading(token) && token.depth === 2 && !/[a-z]+: /.test(token.text) ); @@ -228,10 +227,10 @@ describe('Validating rule docs', () => { type: 'hr', }); expect(tokens[1]).toMatchObject({ + depth: 2, text: description.includes("'") ? `description: "${description}."` : `description: '${description}.'`, - depth: 2, type: 'heading', }); }); @@ -376,8 +375,8 @@ describe('Validating rule docs', () => { jsx: /^tsx\b/i.test(lang), }, ecmaVersion: 'latest', - sourceType: 'module', range: true, + sourceType: 'module', }); } catch { throw new Error(`Parsing error:\n\n${token.text}`); @@ -435,7 +434,7 @@ describe('Validating rule docs', () => { function lintCodeBlock( token: mdast.Code, - shouldContainLintErrors: boolean | 'skip-check', + shouldContainLintErrors: 'skip-check' | boolean, ): void { const lang = token.lang?.trim(); if (!lang || !/^tsx?\b/i.test(lang)) { @@ -467,8 +466,8 @@ describe('Validating rule docs', () => { parser: '@typescript-eslint/parser', parserOptions: { disallowAutomaticSingleRunInference: true, - tsconfigRootDir: rootPath, project: './tsconfig.json', + tsconfigRootDir: rootPath, }, rules: { [ruleName]: ruleConfig, diff --git a/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts b/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts index 9f27b18b35ec..2abf2dbb843b 100644 --- a/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/arrow-parens.test.ts @@ -7,6 +7,7 @@ const rule = getESLintCoreRule('arrow-parens'); const ruleTester = new RuleTester(); ruleTester.run('arrow-parens', rule, { + invalid: [], valid: [ // https://github.com/typescript-eslint/typescript-eslint/issues/14 noFormat`const foo = (t) => {};`, @@ -40,5 +41,4 @@ const foo = (bar: any): void => { options: ['as-needed', { requireForBlockBody: true }], }, ], - invalid: [], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/no-dupe-args.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-dupe-args.test.ts index 0050a131761b..6b51c17c048c 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-dupe-args.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-dupe-args.test.ts @@ -7,6 +7,7 @@ const rule = getESLintCoreRule('no-dupe-args'); const ruleTester = new RuleTester(); ruleTester.run('no-dupe-args', rule, { + invalid: [], valid: [ // https://github.com/eslint/typescript-eslint-parser/issues/535 ` @@ -15,5 +16,4 @@ function foo({ bar }: { bar: string }) { } `, ], - invalid: [], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/no-implicit-globals.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-implicit-globals.test.ts index ebac70b7d2f7..197c215230fb 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-implicit-globals.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-implicit-globals.test.ts @@ -6,6 +6,7 @@ const rule = getESLintCoreRule('no-implicit-globals'); const ruleTester = new RuleTester(); ruleTester.run('no-implicit-globals', rule, { + invalid: [], valid: [ // https://github.com/typescript-eslint/typescript-eslint/issues/23 ` @@ -16,5 +17,4 @@ function foo() { module.exports = foo; `, ], - invalid: [], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts index 2958d9d13cfd..ac03e65f49b9 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts @@ -7,46 +7,6 @@ const rule = getESLintCoreRule('no-restricted-globals'); const ruleTester = new RuleTester(); ruleTester.run('no-restricted-globals', rule, { - valid: [ - // https://github.com/eslint/typescript-eslint-parser/issues/487 - { - code: ` -export default class Test { - private status: string; - getStatus() { - return this.status; - } -} - `, - options: ['status'], - }, - { - code: ` -type Handler = (event: string) => any; - `, - options: ['event'], - }, - { - code: ` - const a = foo?.bar?.name; - `, - }, - { - code: ` - const a = foo?.bar?.name ?? 'foobar'; - `, - }, - { - code: ` - const a = foo()?.bar; - `, - }, - { - code: ` - const a = foo()?.bar ?? true; - `, - }, - ], invalid: [ { code: ` @@ -56,43 +16,83 @@ function onClick() { fdescribe('foo', function () {}); `, - options: ['event'], errors: [ { - messageId: 'defaultMessage', data: { name: 'event', }, + messageId: 'defaultMessage', }, ], + options: ['event'], }, { code: ` confirm('TEST'); `, - options: ['confirm'], errors: [ { - messageId: 'defaultMessage', data: { name: 'confirm', }, + messageId: 'defaultMessage', }, ], + options: ['confirm'], }, { code: ` var a = confirm('TEST')?.a; `, - options: ['confirm'], errors: [ { - messageId: 'defaultMessage', data: { name: 'confirm', }, + messageId: 'defaultMessage', }, ], + options: ['confirm'], + }, + ], + valid: [ + // https://github.com/eslint/typescript-eslint-parser/issues/487 + { + code: ` +export default class Test { + private status: string; + getStatus() { + return this.status; + } +} + `, + options: ['status'], + }, + { + code: ` +type Handler = (event: string) => any; + `, + options: ['event'], + }, + { + code: ` + const a = foo?.bar?.name; + `, + }, + { + code: ` + const a = foo?.bar?.name ?? 'foobar'; + `, + }, + { + code: ` + const a = foo()?.bar; + `, + }, + { + code: ` + const a = foo()?.bar ?? true; + `, }, ], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts index 2180223d1eab..3668c6c98135 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts @@ -7,6 +7,183 @@ const rule = getESLintCoreRule('no-undef'); const ruleTester = new RuleTester(); ruleTester.run('no-undef', rule, { + invalid: [ + { + code: 'a = 5;', + errors: [ + { + data: { + name: 'a', + }, + messageId: 'undef', + }, + ], + }, + { + code: 'a?.b = 5;', + errors: [ + { + data: { + name: 'a', + }, + messageId: 'undef', + }, + ], + }, + { + code: 'a()?.b = 5;', + errors: [ + { + data: { + name: 'a', + }, + messageId: 'undef', + }, + ], + }, + { + code: ';', + errors: [ + { + column: 2, + data: { + name: 'Foo', + }, + line: 1, + messageId: 'undef', + }, + ], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }, + { + code: ` +function Foo() {} +; + `, + errors: [ + { + column: 12, + data: { + name: 'x', + }, + line: 3, + messageId: 'undef', + }, + ], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }, + { + code: ` +function Foo() {} +; + `, + errors: [ + { + column: 10, + data: { + name: 'x', + }, + line: 3, + messageId: 'undef', + }, + ], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }, + { + code: ` +function Foo() {} + />; + `, + errors: [ + { + column: 6, + data: { + name: 'T', + }, + line: 3, + messageId: 'undef', + }, + ], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }, + { + code: ` +function Foo() {} +{x}; + `, + errors: [ + { + column: 7, + data: { + name: 'x', + }, + line: 3, + messageId: 'undef', + }, + ], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }, + { + code: ` +class Foo { + [x: Bar]: string; +} + `, + errors: [ + { + data: { + name: 'Bar', + }, + messageId: 'undef', + }, + ], + }, + { + code: ` +class Foo { + [x: string]: Bar; +} + `, + errors: [ + { + data: { + name: 'Bar', + }, + messageId: 'undef', + }, + ], + }, + ], valid: [ ` import Beemo from './Beemo'; @@ -274,181 +451,4 @@ class Foo { } `, ], - invalid: [ - { - code: 'a = 5;', - errors: [ - { - messageId: 'undef', - data: { - name: 'a', - }, - }, - ], - }, - { - code: 'a?.b = 5;', - errors: [ - { - messageId: 'undef', - data: { - name: 'a', - }, - }, - ], - }, - { - code: 'a()?.b = 5;', - errors: [ - { - messageId: 'undef', - data: { - name: 'a', - }, - }, - ], - }, - { - code: ';', - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - }, - errors: [ - { - messageId: 'undef', - data: { - name: 'Foo', - }, - line: 1, - column: 2, - }, - ], - }, - { - code: ` -function Foo() {} -; - `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - }, - errors: [ - { - messageId: 'undef', - data: { - name: 'x', - }, - line: 3, - column: 12, - }, - ], - }, - { - code: ` -function Foo() {} -; - `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - }, - errors: [ - { - messageId: 'undef', - data: { - name: 'x', - }, - line: 3, - column: 10, - }, - ], - }, - { - code: ` -function Foo() {} - />; - `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - }, - errors: [ - { - messageId: 'undef', - data: { - name: 'T', - }, - line: 3, - column: 6, - }, - ], - }, - { - code: ` -function Foo() {} -{x}; - `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - }, - errors: [ - { - messageId: 'undef', - data: { - name: 'x', - }, - line: 3, - column: 7, - }, - ], - }, - { - code: ` -class Foo { - [x: Bar]: string; -} - `, - errors: [ - { - messageId: 'undef', - data: { - name: 'Bar', - }, - }, - ], - }, - { - code: ` -class Foo { - [x: string]: Bar; -} - `, - errors: [ - { - messageId: 'undef', - data: { - name: 'Bar', - }, - }, - ], - }, - ], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts b/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts index 5223be374c4a..9bd707aedf20 100644 --- a/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts @@ -7,6 +7,7 @@ const rule = getESLintCoreRule('prefer-const'); const ruleTester = new RuleTester(); ruleTester.run('prefer-const', rule, { + invalid: [], valid: [ ` let x: number | undefined = 1; @@ -21,5 +22,4 @@ let x: number | undefined = 1; (x as number) += 1; `, ], - invalid: [], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/strict.test.ts b/packages/eslint-plugin/tests/eslint-rules/strict.test.ts index e25d6f7a44da..41bf41cb129f 100644 --- a/packages/eslint-plugin/tests/eslint-rules/strict.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/strict.test.ts @@ -7,16 +7,6 @@ const rule = getESLintCoreRule('strict'); const ruleTester = new RuleTester(); ruleTester.run('strict', rule, { - valid: [ - // https://github.com/typescript-eslint/typescript-eslint/issues/58 - ` -window.whatevs = { - myFunc() { - console.log('yep'); - }, -}; - `, - ], invalid: [ { // https://github.com/typescript-eslint/typescript-eslint/issues/58 @@ -27,20 +17,30 @@ window.whatevs = { }, }; `, - languageOptions: { - parserOptions: { - sourceType: 'script', - }, - }, errors: [ { - message: "Use the function form of 'use strict'.", - line: 3, column: 9, + line: 3, + message: "Use the function form of 'use strict'.", // the base rule doesn't use messageId // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, ], + languageOptions: { + parserOptions: { + sourceType: 'script', + }, + }, }, ], + valid: [ + // https://github.com/typescript-eslint/typescript-eslint/issues/58 + ` +window.whatevs = { + myFunc() { + console.log('yep'); + }, +}; + `, + ], }); diff --git a/packages/eslint-plugin/tests/rules/adjacent-overload-signatures.test.ts b/packages/eslint-plugin/tests/rules/adjacent-overload-signatures.test.ts index 3839469351ce..e238eaf75dad 100644 --- a/packages/eslint-plugin/tests/rules/adjacent-overload-signatures.test.ts +++ b/packages/eslint-plugin/tests/rules/adjacent-overload-signatures.test.ts @@ -5,924 +5,924 @@ import rule from '../../src/rules/adjacent-overload-signatures'; const ruleTester = new RuleTester(); ruleTester.run('adjacent-overload-signatures', rule, { - valid: [ + invalid: [ { code: ` -function error(a: string); -function error(b: number); -function error(ab: string | number) {} -export { error }; +function wrap() { + function foo(s: string); + function foo(n: number); + type bar = number; + function foo(sn: string | number) {} +} `, - languageOptions: { parserOptions: { sourceType: 'module' } }, + errors: [ + { + column: 3, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], }, { code: ` -import { connect } from 'react-redux'; -export interface ErrorMessageModel { - message: string; +if (true) { + function foo(s: string); + function foo(n: number); + let a = 1; + function foo(sn: string | number) {} + foo(a); } -function mapStateToProps() {} -function mapDispatchToProps() {} -export default connect(mapStateToProps, mapDispatchToProps)(ErrorMessage); `, - languageOptions: { parserOptions: { sourceType: 'module' } }, + errors: [ + { + column: 3, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], }, - ` -export const foo = 'a', - bar = 'b'; -export interface Foo {} -export class Foo {} - `, - ` -export interface Foo {} -export const foo = 'a', - bar = 'b'; -export class Foo {} - `, - ` -const foo = 'a', - bar = 'b'; -interface Foo {} -class Foo {} - `, - ` -interface Foo {} -const foo = 'a', - bar = 'b'; -class Foo {} - `, - ` -export class Foo {} -export class Bar {} -export type FooBar = Foo | Bar; - `, - ` -export interface Foo {} -export class Foo {} -export class Bar {} -export type FooBar = Foo | Bar; - `, - ` + { + code: ` export function foo(s: string); export function foo(n: number); -export function foo(sn: string | number) {} export function bar(): void {} export function baz(): void {} - `, - ` +export function foo(sn: string | number) {} + `, + errors: [ + { + column: 1, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` +export function foo(s: string); +export function foo(n: number); +export type bar = number; +export type baz = number | string; +export function foo(sn: string | number) {} + `, + errors: [ + { + column: 1, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` function foo(s: string); function foo(n: number); -function foo(sn: string | number) {} function bar(): void {} function baz(): void {} - `, - ` +function foo(sn: string | number) {} + `, + errors: [ + { + column: 1, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` +function foo(s: string); +function foo(n: number); +type bar = number; +type baz = number | string; +function foo(sn: string | number) {} + `, + errors: [ + { + column: 1, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` +function foo(s: string) {} +function foo(n: number) {} +const a = ''; +const b = ''; +function foo(sn: string | number) {} + `, + errors: [ + { + column: 1, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` +function foo(s: string) {} +function foo(n: number) {} +class Bar {} +function foo(sn: string | number) {} + `, + errors: [ + { + column: 1, + data: { name: 'foo' }, + line: 5, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` +function foo(s: string) {} +function foo(n: number) {} +function foo(sn: string | number) {} +class Bar { + foo(s: string); + foo(n: number); + name: string; + foo(sn: string | number) {} +} + `, + errors: [ + { + column: 3, + data: { name: 'foo' }, + line: 9, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` declare function foo(s: string); declare function foo(n: number); -declare function foo(sn: string | number); declare function bar(): void; declare function baz(): void; - `, - ` +declare function foo(sn: string | number); + `, + errors: [ + { + column: 1, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` +declare function foo(s: string); +declare function foo(n: number); +const a = ''; +const b = ''; +declare function foo(sn: string | number); + `, + errors: [ + { + column: 1, + data: { name: 'foo' }, + line: 6, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` declare module 'Foo' { export function foo(s: string): void; export function foo(n: number): void; - export function foo(sn: string | number): void; export function bar(): void; export function baz(): void; + export function foo(sn: string | number): void; } - `, - ` -declare namespace Foo { + `, + errors: [ + { + column: 3, + data: { name: 'foo' }, + line: 7, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` +declare module 'Foo' { export function foo(s: string): void; export function foo(n: number): void; export function foo(sn: string | number): void; + function baz(s: string): void; export function bar(): void; - export function baz(): void; -} - `, - ` -type Foo = { - foo(s: string): void; - foo(n: number): void; - foo(sn: string | number): void; - bar(): void; - baz(): void; -}; - `, - ` -type Foo = { - foo(s: string): void; - ['foo'](n: number): void; - foo(sn: string | number): void; - bar(): void; - baz(): void; -}; - `, - ` -interface Foo { - (s: string): void; - (n: number): void; - (sn: string | number): void; - foo(n: number): void; - bar(): void; - baz(): void; -} - `, - ` -interface Foo { - (s: string): void; - (n: number): void; - (sn: string | number): void; - foo(n: number): void; - bar(): void; - baz(): void; - call(): void; -} - `, - ` -interface Foo { - foo(s: string): void; - foo(n: number): void; - foo(sn: string | number): void; - bar(): void; - baz(): void; -} - `, - ` -interface Foo { - foo(s: string): void; - ['foo'](n: number): void; - foo(sn: string | number): void; - bar(): void; - baz(): void; -} - `, - ` -interface Foo { - foo(): void; - bar: { - baz(s: string): void; - baz(n: number): void; - baz(sn: string | number): void; - }; -} - `, - ` -interface Foo { - new (s: string); - new (n: number); - new (sn: string | number); - foo(): void; -} - `, - ` -class Foo { - constructor(s: string); - constructor(n: number); - constructor(sn: string | number) {} - bar(): void {} - baz(): void {} -} - `, - ` -class Foo { - foo(s: string): void; - foo(n: number): void; - foo(sn: string | number): void {} - bar(): void {} - baz(): void {} -} - `, - ` -class Foo { - foo(s: string): void; - ['foo'](n: number): void; - foo(sn: string | number): void {} - bar(): void {} - baz(): void {} -} - `, - ` -class Foo { - name: string; - foo(s: string): void; - foo(n: number): void; - foo(sn: string | number): void {} - bar(): void {} - baz(): void {} -} - `, - ` -class Foo { - name: string; - static foo(s: string): void; - static foo(n: number): void; - static foo(sn: string | number): void {} - bar(): void {} - baz(): void {} -} - `, - ` -class Test { - static test() {} - untest() {} - test() {} -} - `, - // examples from https://github.com/nzakas/eslint-plugin-typescript/issues/138 - 'export default function (foo: T) {}', - 'export default function named(foo: T) {}', - ` -interface Foo { - [Symbol.toStringTag](): void; - [Symbol.iterator](): void; -} - `, - // private members - ` -class Test { - #private(): void; - #private(arg: number): void {} - - bar() {} - - '#private'(): void; - '#private'(arg: number): void {} -} - `, - // block statement - ` -function wrap() { - function foo(s: string); - function foo(n: number); - function foo(sn: string | number) {} -} - `, - ` -if (true) { - function foo(s: string); - function foo(n: number); - function foo(sn: string | number) {} + function baz(n: number): void; + function baz(sn: string | number): void; } - `, - ], - invalid: [ + `, + errors: [ + { + column: 3, + data: { name: 'baz' }, + line: 8, + messageId: 'adjacentSignature', + }, + ], + }, { code: ` -function wrap() { - function foo(s: string); - function foo(n: number); - type bar = number; - function foo(sn: string | number) {} +declare namespace Foo { + export function foo(s: string): void; + export function foo(n: number): void; + export function bar(): void; + export function baz(): void; + export function foo(sn: string | number): void; } `, errors: [ { - messageId: 'adjacentSignature', + column: 3, data: { name: 'foo' }, - line: 6, + line: 7, + messageId: 'adjacentSignature', + }, + ], + }, + { + code: ` +declare namespace Foo { + export function foo(s: string): void; + export function foo(n: number): void; + export function foo(sn: string | number): void; + function baz(s: string): void; + export function bar(): void; + function baz(n: number): void; + function baz(sn: string | number): void; +} + `, + errors: [ + { column: 3, + data: { name: 'baz' }, + line: 8, + messageId: 'adjacentSignature', }, ], }, { code: ` -if (true) { - function foo(s: string); - function foo(n: number); - let a = 1; - function foo(sn: string | number) {} - foo(a); -} +type Foo = { + foo(s: string): void; + foo(n: number): void; + bar(): void; + baz(): void; + foo(sn: string | number): void; +}; `, errors: [ { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 6, column: 3, + data: { name: 'foo' }, + line: 7, + messageId: 'adjacentSignature', }, ], }, { code: ` -export function foo(s: string); -export function foo(n: number); -export function bar(): void {} -export function baz(): void {} -export function foo(sn: string | number) {} +type Foo = { + foo(s: string): void; + ['foo'](n: number): void; + bar(): void; + baz(): void; + foo(sn: string | number): void; +}; `, errors: [ { - messageId: 'adjacentSignature', + column: 3, data: { name: 'foo' }, - line: 6, - column: 1, + line: 7, + messageId: 'adjacentSignature', }, ], }, { code: ` -export function foo(s: string); -export function foo(n: number); -export type bar = number; -export type baz = number | string; -export function foo(sn: string | number) {} +type Foo = { + foo(s: string): void; + name: string; + foo(n: number): void; + foo(sn: string | number): void; + bar(): void; + baz(): void; +}; `, errors: [ { - messageId: 'adjacentSignature', + column: 3, data: { name: 'foo' }, - line: 6, - column: 1, + line: 5, + messageId: 'adjacentSignature', }, ], }, { code: ` -function foo(s: string); -function foo(n: number); -function bar(): void {} -function baz(): void {} -function foo(sn: string | number) {} +interface Foo { + (s: string): void; + foo(n: number): void; + (n: number): void; + (sn: string | number): void; + bar(): void; + baz(): void; + call(): void; +} `, errors: [ { + column: 3, + data: { name: 'call' }, + line: 5, messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 6, - column: 1, }, ], }, { code: ` -function foo(s: string); -function foo(n: number); -type bar = number; -type baz = number | string; -function foo(sn: string | number) {} +interface Foo { + foo(s: string): void; + foo(n: number): void; + bar(): void; + baz(): void; + foo(sn: string | number): void; +} `, errors: [ { - messageId: 'adjacentSignature', + column: 3, data: { name: 'foo' }, - line: 6, - column: 1, + line: 7, + messageId: 'adjacentSignature', }, ], }, { code: ` -function foo(s: string) {} -function foo(n: number) {} -const a = ''; -const b = ''; -function foo(sn: string | number) {} +interface Foo { + foo(s: string): void; + ['foo'](n: number): void; + bar(): void; + baz(): void; + foo(sn: string | number): void; +} `, errors: [ { - messageId: 'adjacentSignature', + column: 3, data: { name: 'foo' }, - line: 6, - column: 1, + line: 7, + messageId: 'adjacentSignature', }, ], }, { code: ` -function foo(s: string) {} -function foo(n: number) {} -class Bar {} -function foo(sn: string | number) {} +interface Foo { + foo(s: string): void; + 'foo'(n: number): void; + bar(): void; + baz(): void; + foo(sn: string | number): void; +} `, errors: [ { - messageId: 'adjacentSignature', + column: 3, data: { name: 'foo' }, - line: 5, - column: 1, + line: 7, + messageId: 'adjacentSignature', }, ], }, { code: ` -function foo(s: string) {} -function foo(n: number) {} -function foo(sn: string | number) {} -class Bar { - foo(s: string); - foo(n: number); +interface Foo { + foo(s: string): void; name: string; - foo(sn: string | number) {} + foo(n: number): void; + foo(sn: string | number): void; + bar(): void; + baz(): void; } `, errors: [ { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 9, column: 3, + data: { name: 'foo' }, + line: 5, + messageId: 'adjacentSignature', }, ], }, { code: ` -declare function foo(s: string); -declare function foo(n: number); -declare function bar(): void; -declare function baz(): void; -declare function foo(sn: string | number); +interface Foo { + foo(): void; + bar: { + baz(s: string): void; + baz(n: number): void; + foo(): void; + baz(sn: string | number): void; + }; +} `, errors: [ { + column: 5, + data: { name: 'baz' }, + line: 8, messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 6, - column: 1, }, ], }, { code: ` -declare function foo(s: string); -declare function foo(n: number); -const a = ''; -const b = ''; -declare function foo(sn: string | number); +interface Foo { + new (s: string); + new (n: number); + foo(): void; + bar(): void; + new (sn: string | number); +} `, errors: [ { + column: 3, + data: { name: 'new' }, + line: 7, messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 6, - column: 1, }, ], }, { code: ` -declare module 'Foo' { - export function foo(s: string): void; - export function foo(n: number): void; - export function bar(): void; - export function baz(): void; - export function foo(sn: string | number): void; +interface Foo { + new (s: string); + foo(): void; + new (n: number); + bar(): void; + new (sn: string | number); } `, errors: [ { + column: 3, + data: { name: 'new' }, + line: 5, messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 7, + }, + { column: 3, + data: { name: 'new' }, + line: 7, + messageId: 'adjacentSignature', }, ], }, { code: ` -declare module 'Foo' { - export function foo(s: string): void; - export function foo(n: number): void; - export function foo(sn: string | number): void; - function baz(s: string): void; - export function bar(): void; - function baz(n: number): void; - function baz(sn: string | number): void; +class Foo { + constructor(s: string); + constructor(n: number); + bar(): void {} + baz(): void {} + constructor(sn: string | number) {} } `, errors: [ { - messageId: 'adjacentSignature', - data: { name: 'baz' }, - line: 8, column: 3, + data: { name: 'constructor' }, + line: 7, + messageId: 'adjacentSignature', }, ], }, { code: ` -declare namespace Foo { - export function foo(s: string): void; - export function foo(n: number): void; - export function bar(): void; - export function baz(): void; - export function foo(sn: string | number): void; +class Foo { + foo(s: string): void; + foo(n: number): void; + bar(): void {} + baz(): void {} + foo(sn: string | number): void {} } `, errors: [ { - messageId: 'adjacentSignature', + column: 3, data: { name: 'foo' }, line: 7, - column: 3, + messageId: 'adjacentSignature', }, ], }, { code: ` -declare namespace Foo { - export function foo(s: string): void; - export function foo(n: number): void; - export function foo(sn: string | number): void; - function baz(s: string): void; - export function bar(): void; - function baz(n: number): void; - function baz(sn: string | number): void; +class Foo { + foo(s: string): void; + ['foo'](n: number): void; + bar(): void {} + baz(): void {} + foo(sn: string | number): void {} } `, errors: [ { - messageId: 'adjacentSignature', - data: { name: 'baz' }, - line: 8, column: 3, + data: { name: 'foo' }, + line: 7, + messageId: 'adjacentSignature', }, ], }, { code: ` -type Foo = { - foo(s: string): void; +class Foo { + // prettier-ignore + "foo"(s: string): void; foo(n: number): void; - bar(): void; - baz(): void; - foo(sn: string | number): void; -}; + bar(): void {} + baz(): void {} + foo(sn: string | number): void {} +} `, errors: [ { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 7, column: 3, + data: { name: 'foo' }, + line: 8, + messageId: 'adjacentSignature', }, ], }, { code: ` -type Foo = { - foo(s: string): void; - ['foo'](n: number): void; - bar(): void; - baz(): void; - foo(sn: string | number): void; -}; +class Foo { + constructor(s: string); + name: string; + constructor(n: number); + constructor(sn: string | number) {} + bar(): void {} + baz(): void {} +} `, errors: [ { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 7, column: 3, + data: { name: 'constructor' }, + line: 5, + messageId: 'adjacentSignature', }, ], }, { code: ` -type Foo = { +class Foo { foo(s: string): void; name: string; foo(n: number): void; - foo(sn: string | number): void; - bar(): void; - baz(): void; -}; + foo(sn: string | number): void {} + bar(): void {} + baz(): void {} +} `, errors: [ { - messageId: 'adjacentSignature', + column: 3, data: { name: 'foo' }, line: 5, - column: 3, + messageId: 'adjacentSignature', }, ], }, { code: ` -interface Foo { - (s: string): void; - foo(n: number): void; - (n: number): void; - (sn: string | number): void; - bar(): void; - baz(): void; - call(): void; +class Foo { + static foo(s: string): void; + name: string; + static foo(n: number): void; + static foo(sn: string | number): void {} + bar(): void {} + baz(): void {} } `, errors: [ { - messageId: 'adjacentSignature', - data: { name: 'call' }, - line: 5, column: 3, + data: { name: 'static foo' }, + line: 5, + messageId: 'adjacentSignature', }, ], }, + // private members { code: ` -interface Foo { - foo(s: string): void; - foo(n: number): void; - bar(): void; - baz(): void; - foo(sn: string | number): void; +class Test { + #private(): void; + '#private'(): void; + #private(arg: number): void {} + '#private'(arg: number): void {} } `, errors: [ { + column: 3, + data: { name: '#private' }, + line: 5, messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 7, + }, + { column: 3, + data: { name: '"#private"' }, + line: 6, + messageId: 'adjacentSignature', }, ], }, + ], + valid: [ { code: ` -interface Foo { +function error(a: string); +function error(b: number); +function error(ab: string | number) {} +export { error }; + `, + languageOptions: { parserOptions: { sourceType: 'module' } }, + }, + { + code: ` +import { connect } from 'react-redux'; +export interface ErrorMessageModel { + message: string; +} +function mapStateToProps() {} +function mapDispatchToProps() {} +export default connect(mapStateToProps, mapDispatchToProps)(ErrorMessage); + `, + languageOptions: { parserOptions: { sourceType: 'module' } }, + }, + ` +export const foo = 'a', + bar = 'b'; +export interface Foo {} +export class Foo {} + `, + ` +export interface Foo {} +export const foo = 'a', + bar = 'b'; +export class Foo {} + `, + ` +const foo = 'a', + bar = 'b'; +interface Foo {} +class Foo {} + `, + ` +interface Foo {} +const foo = 'a', + bar = 'b'; +class Foo {} + `, + ` +export class Foo {} +export class Bar {} +export type FooBar = Foo | Bar; + `, + ` +export interface Foo {} +export class Foo {} +export class Bar {} +export type FooBar = Foo | Bar; + `, + ` +export function foo(s: string); +export function foo(n: number); +export function foo(sn: string | number) {} +export function bar(): void {} +export function baz(): void {} + `, + ` +function foo(s: string); +function foo(n: number); +function foo(sn: string | number) {} +function bar(): void {} +function baz(): void {} + `, + ` +declare function foo(s: string); +declare function foo(n: number); +declare function foo(sn: string | number); +declare function bar(): void; +declare function baz(): void; + `, + ` +declare module 'Foo' { + export function foo(s: string): void; + export function foo(n: number): void; + export function foo(sn: string | number): void; + export function bar(): void; + export function baz(): void; +} + `, + ` +declare namespace Foo { + export function foo(s: string): void; + export function foo(n: number): void; + export function foo(sn: string | number): void; + export function bar(): void; + export function baz(): void; +} + `, + ` +type Foo = { foo(s: string): void; - ['foo'](n: number): void; + foo(n: number): void; + foo(sn: string | number): void; bar(): void; baz(): void; +}; + `, + ` +type Foo = { + foo(s: string): void; + ['foo'](n: number): void; foo(sn: string | number): void; + bar(): void; + baz(): void; +}; + `, + ` +interface Foo { + (s: string): void; + (n: number): void; + (sn: string | number): void; + foo(n: number): void; + bar(): void; + baz(): void; } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 7, - column: 3, - }, - ], - }, - { - code: ` + `, + ` +interface Foo { + (s: string): void; + (n: number): void; + (sn: string | number): void; + foo(n: number): void; + bar(): void; + baz(): void; + call(): void; +} + `, + ` interface Foo { foo(s: string): void; - 'foo'(n: number): void; + foo(n: number): void; + foo(sn: string | number): void; bar(): void; baz(): void; - foo(sn: string | number): void; } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 7, - column: 3, - }, - ], - }, - { - code: ` + `, + ` interface Foo { foo(s: string): void; - name: string; - foo(n: number): void; + ['foo'](n: number): void; foo(sn: string | number): void; bar(): void; baz(): void; } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 5, - column: 3, - }, - ], - }, - { - code: ` + `, + ` interface Foo { foo(): void; bar: { baz(s: string): void; baz(n: number): void; - foo(): void; baz(sn: string | number): void; }; } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'baz' }, - line: 8, - column: 5, - }, - ], - }, - { - code: ` + `, + ` interface Foo { new (s: string); new (n: number); - foo(): void; - bar(): void; new (sn: string | number); -} - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'new' }, - line: 7, - column: 3, - }, - ], - }, - { - code: ` -interface Foo { - new (s: string); foo(): void; - new (n: number); - bar(): void; - new (sn: string | number); } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'new' }, - line: 5, - column: 3, - }, - { - messageId: 'adjacentSignature', - data: { name: 'new' }, - line: 7, - column: 3, - }, - ], - }, - { - code: ` + `, + ` class Foo { constructor(s: string); constructor(n: number); + constructor(sn: string | number) {} bar(): void {} baz(): void {} - constructor(sn: string | number) {} } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'constructor' }, - line: 7, - column: 3, - }, - ], - }, - { - code: ` + `, + ` class Foo { foo(s: string): void; foo(n: number): void; + foo(sn: string | number): void {} bar(): void {} baz(): void {} - foo(sn: string | number): void {} } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 7, - column: 3, - }, - ], - }, - { - code: ` + `, + ` class Foo { foo(s: string): void; ['foo'](n: number): void; - bar(): void {} - baz(): void {} foo(sn: string | number): void {} -} - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 7, - column: 3, - }, - ], - }, - { - code: ` -class Foo { - // prettier-ignore - "foo"(s: string): void; - foo(n: number): void; bar(): void {} baz(): void {} - foo(sn: string | number): void {} } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 8, - column: 3, - }, - ], - }, - { - code: ` + `, + ` class Foo { - constructor(s: string); name: string; - constructor(n: number); - constructor(sn: string | number) {} - bar(): void {} - baz(): void {} -} - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'constructor' }, - line: 5, - column: 3, - }, - ], - }, - { - code: ` -class Foo { foo(s: string): void; - name: string; foo(n: number): void; foo(sn: string | number): void {} bar(): void {} baz(): void {} } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'foo' }, - line: 5, - column: 3, - }, - ], - }, - { - code: ` + `, + ` class Foo { - static foo(s: string): void; name: string; + static foo(s: string): void; static foo(n: number): void; static foo(sn: string | number): void {} bar(): void {} baz(): void {} } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: 'static foo' }, - line: 5, - column: 3, - }, - ], - }, + `, + ` +class Test { + static test() {} + untest() {} + test() {} +} + `, + // examples from https://github.com/nzakas/eslint-plugin-typescript/issues/138 + 'export default function (foo: T) {}', + 'export default function named(foo: T) {}', + ` +interface Foo { + [Symbol.toStringTag](): void; + [Symbol.iterator](): void; +} + `, // private members - { - code: ` + ` class Test { #private(): void; - '#private'(): void; #private(arg: number): void {} + + bar() {} + + '#private'(): void; '#private'(arg: number): void {} } - `, - errors: [ - { - messageId: 'adjacentSignature', - data: { name: '#private' }, - line: 5, - column: 3, - }, - { - messageId: 'adjacentSignature', - data: { name: '"#private"' }, - line: 6, - column: 3, - }, - ], - }, + `, + // block statement + ` +function wrap() { + function foo(s: string); + function foo(n: number); + function foo(sn: string | number) {} +} + `, + ` +if (true) { + function foo(s: string); + function foo(n: number); + function foo(sn: string | number) {} +} + `, ], }); diff --git a/packages/eslint-plugin/tests/rules/array-type.test.ts b/packages/eslint-plugin/tests/rules/array-type.test.ts index 52ce7be67578..0bed905c68a8 100644 --- a/packages/eslint-plugin/tests/rules/array-type.test.ts +++ b/packages/eslint-plugin/tests/rules/array-type.test.ts @@ -3,1770 +3,1857 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; import { TSESLint } from '@typescript-eslint/utils'; import type { OptionString } from '../../src/rules/array-type'; + import rule from '../../src/rules/array-type'; import { areOptionsValid } from '../areOptionsValid'; const ruleTester = new RuleTester(); ruleTester.run('array-type', rule, { - valid: [ + invalid: [ // Base cases from https://github.com/typescript-eslint/typescript-eslint/issues/2323#issuecomment-663977655 { - code: 'let a: number[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array' }], + output: 'let a: number[] = [];', }, { - code: 'let a: (string | number)[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array' }], + output: 'let a: (string | number)[] = [];', }, { - code: 'let a: readonly number[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array' }], + output: 'let a: readonly number[] = [];', }, { - code: 'let a: readonly (string | number)[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array' }], + output: 'let a: readonly (string | number)[] = [];', }, { - code: 'let a: number[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array', readonly: 'array' }], + output: 'let a: number[] = [];', }, { - code: 'let a: (string | number)[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array', readonly: 'array' }], + output: 'let a: (string | number)[] = [];', }, { - code: 'let a: readonly number[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array', readonly: 'array' }], + output: 'let a: readonly number[] = [];', }, { - code: 'let a: readonly (string | number)[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array', readonly: 'array' }], + output: 'let a: readonly (string | number)[] = [];', }, { - code: 'let a: number[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array', readonly: 'array-simple' }], + output: 'let a: number[] = [];', }, { - code: 'let a: (string | number)[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array', readonly: 'array-simple' }], + output: 'let a: (string | number)[] = [];', }, { - code: 'let a: readonly number[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'array', readonly: 'array-simple' }], + output: 'let a: readonly number[] = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly (string | number)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGenericSimple', + }, + ], options: [{ default: 'array', readonly: 'array-simple' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: number[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array', readonly: 'generic' }], + output: 'let a: number[] = [];', }, { - code: 'let a: (string | number)[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array', readonly: 'generic' }], + output: 'let a: (string | number)[] = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly number[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'array', readonly: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly (string | number)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'array', readonly: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: number[] = [];', - options: [{ default: 'array-simple' }], - }, - { - code: 'let a: Array = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'array-simple' }], + output: 'let a: number[] = [];', }, { - code: 'let a: readonly number[] = [];', + code: 'let a: (string | number)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGenericSimple', + }, + ], options: [{ default: 'array-simple' }], + output: 'let a: Array = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'array-simple' }], + output: 'let a: readonly number[] = [];', }, { - code: 'let a: number[] = [];', - options: [{ default: 'array-simple', readonly: 'array' }], - }, - { - code: 'let a: Array = [];', + code: 'let a: readonly (string | number)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGenericSimple', + }, + ], + options: [{ default: 'array-simple' }], + output: 'let a: ReadonlyArray = [];', + }, + { + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'array-simple', readonly: 'array' }], + output: 'let a: number[] = [];', }, { - code: 'let a: readonly number[] = [];', + code: 'let a: (string | number)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGenericSimple', + }, + ], options: [{ default: 'array-simple', readonly: 'array' }], + output: 'let a: Array = [];', }, { - code: 'let a: readonly (string | number)[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'array-simple', readonly: 'array' }], + output: 'let a: readonly number[] = [];', }, { - code: 'let a: number[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringArray', + }, + ], + options: [{ default: 'array-simple', readonly: 'array' }], + output: 'let a: readonly (string | number)[] = [];', + }, + { + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'array-simple', readonly: 'array-simple' }], + output: 'let a: number[] = [];', }, { - code: 'let a: Array = [];', + code: 'let a: (string | number)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGenericSimple', + }, + ], options: [{ default: 'array-simple', readonly: 'array-simple' }], + output: 'let a: Array = [];', }, { - code: 'let a: readonly number[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'array-simple', readonly: 'array-simple' }], + output: 'let a: readonly number[] = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly (string | number)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGenericSimple', + }, + ], options: [{ default: 'array-simple', readonly: 'array-simple' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: number[] = [];', + code: 'let a: Array = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'array-simple', readonly: 'generic' }], + output: 'let a: number[] = [];', }, { - code: 'let a: Array = [];', + code: 'let a: (string | number)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGenericSimple', + }, + ], options: [{ default: 'array-simple', readonly: 'generic' }], + output: 'let a: Array = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly number[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'array-simple', readonly: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly (string | number)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'array-simple', readonly: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: Array = [];', + code: 'let a: number[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'generic' }], + output: 'let a: Array = [];', }, { - code: 'let a: Array = [];', + code: 'let a: (string | number)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'generic' }], + output: 'let a: Array = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly number[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly (string | number)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'generic' }], + code: 'let a: number[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'array' }], + output: 'let a: Array = [];', }, { - code: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'generic' }], + code: 'let a: (string | number)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'array' }], + output: 'let a: Array = [];', }, { code: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic', readonly: 'generic' }], - }, - { - code: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic', readonly: 'generic' }], - }, - { - code: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array' }], - }, - { - code: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array' }], - }, - { - code: 'let a: readonly number[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'generic', readonly: 'array' }], + output: 'let a: readonly number[] = [];', }, { - code: 'let a: readonly (string | number)[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringArray', + }, + ], options: [{ default: 'generic', readonly: 'array' }], + output: 'let a: readonly (string | number)[] = [];', }, { - code: 'let a: Array = [];', + code: 'let a: number[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'generic', readonly: 'array-simple' }], + output: 'let a: Array = [];', }, { - code: 'let a: Array = [];', + code: 'let a: (string | number)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'generic', readonly: 'array-simple' }], + output: 'let a: Array = [];', }, { - code: 'let a: readonly number[] = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'generic', readonly: 'array-simple' }], + output: 'let a: readonly number[] = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: readonly (string | number)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGenericSimple', + }, + ], options: [{ default: 'generic', readonly: 'array-simple' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array' }], + code: 'let a: number[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'generic' }], + output: 'let a: Array = [];', }, { - code: 'let a: readonly bigint[] = [];', - options: [{ default: 'generic', readonly: 'array' }], + code: 'let a: (string | number)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'generic' }], + output: 'let a: Array = [];', }, { - code: 'let a: readonly (string | bigint)[] = [];', - options: [{ default: 'generic', readonly: 'array' }], + code: 'let a: readonly number[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'number', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array-simple' }], + code: 'let a: readonly (string | number)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'let a: Array = [];', + code: 'let a: bigint[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'bigint' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'generic', readonly: 'array-simple' }], + output: 'let a: Array = [];', }, { - code: 'let a: readonly bigint[] = [];', + code: 'let a: (string | bigint)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], options: [{ default: 'generic', readonly: 'array-simple' }], + output: 'let a: Array = [];', }, { - code: 'let a: ReadonlyArray = [];', + code: 'let a: ReadonlyArray = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'bigint', + }, + line: 1, + messageId: 'errorStringArraySimple', + }, + ], options: [{ default: 'generic', readonly: 'array-simple' }], + output: 'let a: readonly bigint[] = [];', }, - - // End of base cases - { - code: 'let a = new Array();', - options: [{ default: 'array' }], + code: 'let a: (string | bigint)[] = [];', + errors: [ + { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'generic' }], + output: 'let a: Array = [];', }, { - code: 'let a: { foo: Bar[] }[] = [];', - options: [{ default: 'array' }], + code: 'let a: readonly bigint[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'bigint', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, { - code: 'function foo(a: Array): Array {}', - options: [{ default: 'generic' }], + code: 'let a: readonly (string | bigint)[] = [];', + errors: [ + { + column: 8, + data: { + className: 'ReadonlyArray', + readonlyPrefix: 'readonly ', + type: 'T', + }, + line: 1, + messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic', readonly: 'generic' }], + output: 'let a: ReadonlyArray = [];', }, + + // End of base cases + { - code: 'let yy: number[][] = [[4, 5], [6]];', - options: [{ default: 'array-simple' }], + code: 'let a: { foo: Array }[] = [];', + errors: [ + { + column: 15, + data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, + line: 1, + messageId: 'errorStringArray', + }, + ], + options: [{ default: 'array' }], + output: 'let a: { foo: Bar[] }[] = [];', }, { - code: ` -function fooFunction(foo: Array>) { - return foo.map(e => e.foo); -} - `, - options: [{ default: 'array-simple' }], - }, - { - code: ` -function bazFunction(baz: Arr>) { - return baz.map(e => e.baz); -} - `, - options: [{ default: 'array-simple' }], - }, - { - code: 'let fooVar: Array<(c: number) => number>;', - options: [{ default: 'array-simple' }], - }, - { - code: 'type fooUnion = Array;', - options: [{ default: 'array-simple' }], - }, - { - code: 'type fooIntersection = Array;', - options: [{ default: 'array-simple' }], - }, - { - code: ` -namespace fooName { - type BarType = { bar: string }; - type BazType = Arr; -} - `, - options: [{ default: 'array-simple' }], - }, - { - code: ` -interface FooInterface { - '.bar': { baz: string[] }; -} - `, - options: [{ default: 'array-simple' }], - }, - { - code: 'let yy: number[][] = [[4, 5], [6]];', - options: [{ default: 'array' }], - }, - { - code: "let ya = [[1, '2']] as [number, string][];", - options: [{ default: 'array' }], - }, - { - code: ` -function barFunction(bar: ArrayClass[]) { - return bar.map(e => e.bar); -} - `, - options: [{ default: 'array' }], - }, - { - code: ` -function bazFunction(baz: Arr>) { - return baz.map(e => e.baz); -} - `, - options: [{ default: 'array' }], - }, - { - code: 'let barVar: ((c: number) => number)[];', - options: [{ default: 'array' }], - }, - { - code: 'type barUnion = (string | number | boolean)[];', - options: [{ default: 'array' }], - }, - { - code: 'type barIntersection = (string & number)[];', - options: [{ default: 'array' }], - }, - { - code: ` -interface FooInterface { - '.bar': { baz: string[] }; -} - `, - options: [{ default: 'array' }], - }, - { - // https://github.com/typescript-eslint/typescript-eslint/issues/172 - code: 'type Unwrap = T extends (infer E)[] ? E : T;', - options: [{ default: 'array' }], - }, - { - code: 'let xx: Array> = [[1, 2], [3]];', - options: [{ default: 'generic' }], - }, - { - code: 'type Arr = Array;', - options: [{ default: 'generic' }], - }, - { - code: ` -function fooFunction(foo: Array>) { - return foo.map(e => e.foo); -} - `, - options: [{ default: 'generic' }], - }, - { - code: ` -function bazFunction(baz: Arr>) { - return baz.map(e => e.baz); -} - `, - options: [{ default: 'generic' }], - }, - { - code: 'let fooVar: Array<(c: number) => number>;', - options: [{ default: 'generic' }], - }, - { - code: 'type fooUnion = Array;', - options: [{ default: 'generic' }], - }, - { - code: 'type fooIntersection = Array;', - options: [{ default: 'generic' }], - }, - { - // https://github.com/typescript-eslint/typescript-eslint/issues/172 - code: 'type Unwrap = T extends Array ? E : T;', - options: [{ default: 'generic' }], - }, - - // nested readonly - { - code: 'let a: ReadonlyArray = [[]];', - options: [{ default: 'array', readonly: 'generic' }], - }, - { - code: 'let a: readonly Array[] = [[]];', - options: [{ default: 'generic', readonly: 'array' }], - }, - { - code: 'let a: Readonly = [];', - options: [{ default: 'generic', readonly: 'array' }], - }, - { - code: "const x: Readonly = 'a';", - options: [{ default: 'array' }], - }, - ], - invalid: [ - // Base cases from https://github.com/typescript-eslint/typescript-eslint/issues/2323#issuecomment-663977655 - { - code: 'let a: Array = [];', - output: 'let a: number[] = [];', - options: [{ default: 'array' }], + code: 'let a: Array<{ foo: Bar[] }> = [];', errors: [ { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + column: 21, + data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, line: 1, - column: 8, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: 'let a: Array<{ foo: Array }> = [];', }, { - code: 'let a: Array = [];', - output: 'let a: (string | number)[] = [];', - options: [{ default: 'array' }], + code: 'let a: Array<{ foo: Foo | Bar[] }> = [];', errors: [ { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + column: 27, + data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, line: 1, - column: 8, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: 'let a: Array<{ foo: Foo | Array }> = [];', }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly number[] = [];', - options: [{ default: 'array' }], + code: 'function foo(a: Array): Array {}', errors: [ { + column: 17, + data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, + line: 1, messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, + }, + { + column: 30, + data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'function foo(a: Bar[]): Bar[] {}', }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly (string | number)[] = [];', - options: [{ default: 'array' }], + code: 'let x: Array = [undefined] as undefined[];', errors: [ { - messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'undefined' }, + line: 1, + messageId: 'errorStringArraySimple', }, ], + options: [{ default: 'array-simple' }], + output: 'let x: undefined[] = [undefined] as undefined[];', }, { - code: 'let a: Array = [];', - output: 'let a: number[] = [];', - options: [{ default: 'array', readonly: 'array' }], + code: "let y: string[] = >['2'];", errors: [ { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + column: 20, + data: { className: 'Array', readonlyPrefix: '', type: 'string' }, line: 1, - column: 8, + messageId: 'errorStringArraySimple', }, ], + options: [{ default: 'array-simple' }], + output: "let y: string[] = ['2'];", }, { - code: 'let a: Array = [];', - output: 'let a: (string | number)[] = [];', - options: [{ default: 'array', readonly: 'array' }], + code: "let z: Array = [3, '4'];", errors: [ { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'any' }, + line: 1, + messageId: 'errorStringArraySimple', }, ], + options: [{ default: 'array-simple' }], + output: "let z: any[] = [3, '4'];", }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly number[] = [];', - options: [{ default: 'array', readonly: 'array' }], + code: "let ya = [[1, '2']] as [number, string][];", errors: [ { - messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, + column: 24, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGenericSimple', }, ], + options: [{ default: 'array-simple' }], + output: "let ya = [[1, '2']] as Array<[number, string]>;", }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly (string | number)[] = [];', - options: [{ default: 'array', readonly: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, - line: 1, - column: 8, - }, - ], - }, - { - code: 'let a: Array = [];', - output: 'let a: number[] = [];', - options: [{ default: 'array', readonly: 'array-simple' }], + code: 'type Arr = Array;', errors: [ { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + column: 15, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringArraySimple', }, ], + options: [{ default: 'array-simple' }], + output: 'type Arr = T[];', }, { - code: 'let a: Array = [];', - output: 'let a: (string | number)[] = [];', - options: [{ default: 'array', readonly: 'array-simple' }], + code: ` +// Ignore user defined aliases +let yyyy: Arr>[]> = [[[['2']]]]; + `, errors: [ { - messageId: 'errorStringArray', + column: 15, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 8, + line: 3, + messageId: 'errorStringGenericSimple', }, ], + options: [{ default: 'array-simple' }], + output: ` +// Ignore user defined aliases +let yyyy: Arr>>> = [[[['2']]]]; + `, }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly number[] = [];', - options: [{ default: 'array', readonly: 'array-simple' }], + code: ` +interface ArrayClass { + foo: Array; + bar: T[]; + baz: Arr; + xyz: this[]; +} + `, errors: [ { - messageId: 'errorStringArraySimple', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 3, + messageId: 'errorStringArraySimple', }, ], + options: [{ default: 'array-simple' }], + output: ` +interface ArrayClass { + foo: T[]; + bar: T[]; + baz: Arr; + xyz: this[]; +} + `, }, { - code: 'let a: readonly (string | number)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'array', readonly: 'array-simple' }], + code: ` +function barFunction(bar: ArrayClass[]) { + return bar.map(e => e.bar); +} + `, errors: [ { + column: 27, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 2, messageId: 'errorStringGenericSimple', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, - line: 1, - column: 8, }, ], + options: [{ default: 'array-simple' }], + output: ` +function barFunction(bar: Array>) { + return bar.map(e => e.bar); +} + `, }, { - code: 'let a: Array = [];', - output: 'let a: number[] = [];', - options: [{ default: 'array', readonly: 'generic' }], + code: 'let barVar: ((c: number) => number)[];', errors: [ { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, + column: 13, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGenericSimple', }, ], + options: [{ default: 'array-simple' }], + output: 'let barVar: Array<(c: number) => number>;', }, { - code: 'let a: Array = [];', - output: 'let a: (string | number)[] = [];', - options: [{ default: 'array', readonly: 'generic' }], + code: 'type barUnion = (string | number | boolean)[];', errors: [ { - messageId: 'errorStringArray', + column: 17, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGenericSimple', }, ], + options: [{ default: 'array-simple' }], + output: 'type barUnion = Array;', }, { - code: 'let a: readonly number[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'array', readonly: 'generic' }], + code: 'type barIntersection = (string & number)[];', errors: [ { - messageId: 'errorStringGeneric', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, + column: 24, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGenericSimple', }, ], + options: [{ default: 'array-simple' }], + output: 'type barIntersection = Array;', }, { - code: 'let a: readonly (string | number)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'array', readonly: 'generic' }], + code: "let v: Array = [{ bar: 'bar' }];", errors: [ { - messageId: 'errorStringGeneric', + column: 8, data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', + className: 'Array', + readonlyPrefix: '', + type: 'fooName.BarType', }, line: 1, - column: 8, - }, - ], - }, - { - code: 'let a: Array = [];', - output: 'let a: number[] = [];', - options: [{ default: 'array-simple' }], - errors: [ - { messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, - line: 1, - column: 8, }, ], + options: [{ default: 'array-simple' }], + output: "let v: fooName.BarType[] = [{ bar: 'bar' }];", }, { - code: 'let a: (string | number)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'array-simple' }], + code: "let w: fooName.BazType[] = [['baz']];", errors: [ { - messageId: 'errorStringGenericSimple', + column: 8, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGenericSimple', }, ], + options: [{ default: 'array-simple' }], + output: "let w: Array> = [['baz']];", }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly number[] = [];', - options: [{ default: 'array-simple' }], + code: 'let x: Array = [undefined] as undefined[];', errors: [ { - messageId: 'errorStringArraySimple', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'undefined' }, + line: 1, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'let x: undefined[] = [undefined] as undefined[];', }, { - code: 'let a: readonly (string | number)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'array-simple' }], + code: "let y: string[] = >['2'];", errors: [ { - messageId: 'errorStringGenericSimple', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, + column: 20, + data: { className: 'Array', readonlyPrefix: '', type: 'string' }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: "let y: string[] = ['2'];", }, { - code: 'let a: Array = [];', - output: 'let a: number[] = [];', - options: [{ default: 'array-simple', readonly: 'array' }], + code: "let z: Array = [3, '4'];", errors: [ { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'any' }, + line: 1, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: "let z: any[] = [3, '4'];", }, { - code: 'let a: (string | number)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'array-simple', readonly: 'array' }], + code: 'type Arr = Array;', errors: [ { - messageId: 'errorStringGenericSimple', + column: 15, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'type Arr = T[];', }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly number[] = [];', - options: [{ default: 'array-simple', readonly: 'array' }], + code: ` +// Ignore user defined aliases +let yyyy: Arr>[]> = [[[['2']]]]; + `, errors: [ { + column: 15, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 3, messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, - line: 1, - column: 8, }, ], + options: [{ default: 'array' }], + output: ` +// Ignore user defined aliases +let yyyy: Arr[][]> = [[[['2']]]]; + `, }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly (string | number)[] = [];', - options: [{ default: 'array-simple', readonly: 'array' }], - errors: [ + code: ` +interface ArrayClass { + foo: Array; + bar: T[]; + baz: Arr; +} + `, + errors: [ { - messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 3, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: ` +interface ArrayClass { + foo: T[]; + bar: T[]; + baz: Arr; +} + `, }, { - code: 'let a: Array = [];', - output: 'let a: number[] = [];', - options: [{ default: 'array-simple', readonly: 'array-simple' }], + code: ` +function fooFunction(foo: Array>) { + return foo.map(e => e.foo); +} + `, errors: [ { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, - line: 1, - column: 8, + column: 27, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 2, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: ` +function fooFunction(foo: ArrayClass[]) { + return foo.map(e => e.foo); +} + `, }, { - code: 'let a: (string | number)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'array-simple', readonly: 'array-simple' }], + code: 'let fooVar: Array<(c: number) => number>;', errors: [ { - messageId: 'errorStringGenericSimple', + column: 13, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'let fooVar: ((c: number) => number)[];', }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly number[] = [];', - options: [{ default: 'array-simple', readonly: 'array-simple' }], + code: 'type fooUnion = Array;', errors: [ { - messageId: 'errorStringArraySimple', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, + column: 17, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'type fooUnion = (string | number | boolean)[];', }, { - code: 'let a: readonly (string | number)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'array-simple', readonly: 'array-simple' }], + code: 'type fooIntersection = Array;', errors: [ { - messageId: 'errorStringGenericSimple', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, + column: 24, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'type fooIntersection = (string & number)[];', }, { - code: 'let a: Array = [];', - output: 'let a: number[] = [];', - options: [{ default: 'array-simple', readonly: 'generic' }], + code: 'let x: Array;', errors: [ { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'any' }, + line: 1, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'let x: any[];', }, { - code: 'let a: (string | number)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'array-simple', readonly: 'generic' }], + code: 'let x: Array<>;', errors: [ { - messageId: 'errorStringGenericSimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'any' }, + line: 1, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'let x: any[];', }, { - code: 'let a: readonly number[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'array-simple', readonly: 'generic' }], + code: 'let x: Array;', errors: [ { - messageId: 'errorStringGeneric', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'any' }, + line: 1, + messageId: 'errorStringArraySimple', }, ], + options: [{ default: 'array-simple' }], + output: 'let x: any[];', }, { - code: 'let a: readonly (string | number)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'array-simple', readonly: 'generic' }], + code: 'let x: Array<>;', errors: [ { - messageId: 'errorStringGeneric', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, - line: 1, column: 8, + line: 1, + messageId: 'errorStringArraySimple', }, ], + options: [{ default: 'array-simple' }], + output: 'let x: any[];', }, { - code: 'let a: number[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic' }], + code: 'let x: Array = [1] as number[];', errors: [ { - messageId: 'errorStringGeneric', + column: 31, data: { className: 'Array', readonlyPrefix: '', type: 'number' }, line: 1, - column: 8, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: 'let x: Array = [1] as Array;', }, { - code: 'let a: (string | number)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic' }], + code: "let y: string[] = >['2'];", errors: [ { + column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'string' }, + line: 1, messageId: 'errorStringGeneric', + }, + ], + options: [{ default: 'generic' }], + output: "let y: Array = >['2'];", + }, + { + code: "let ya = [[1, '2']] as [number, string][];", + errors: [ + { + column: 24, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: "let ya = [[1, '2']] as Array<[number, string]>;", }, { - code: 'let a: readonly number[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic' }], + code: ` +// Ignore user defined aliases +let yyyy: Arr>[]> = [[[['2']]]]; + `, errors: [ { + column: 15, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 3, messageId: 'errorStringGeneric', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, - line: 1, - column: 8, }, ], + options: [{ default: 'generic' }], + output: ` +// Ignore user defined aliases +let yyyy: Arr>>> = [[[['2']]]]; + `, }, { - code: 'let a: readonly (string | number)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic' }], + code: ` +interface ArrayClass { + foo: Array; + bar: T[]; + baz: Arr; +} + `, errors: [ { - messageId: 'errorStringGeneric', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, - line: 1, column: 8, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 4, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: ` +interface ArrayClass { + foo: Array; + bar: Array; + baz: Arr; +} + `, }, { - code: 'let a: number[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array' }], + code: ` +function barFunction(bar: ArrayClass[]) { + return bar.map(e => e.bar); +} + `, errors: [ { + column: 27, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, + line: 2, messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, - line: 1, - column: 8, }, ], + options: [{ default: 'generic' }], + output: ` +function barFunction(bar: Array>) { + return bar.map(e => e.bar); +} + `, }, { - code: 'let a: (string | number)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array' }], + code: 'let barVar: ((c: number) => number)[];', errors: [ { - messageId: 'errorStringGeneric', + column: 13, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: 'let barVar: Array<(c: number) => number>;', }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly number[] = [];', - options: [{ default: 'generic', readonly: 'array' }], + code: 'type barUnion = (string | number | boolean)[];', errors: [ { - messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, + column: 17, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: 'type barUnion = Array;', }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly (string | number)[] = [];', - options: [{ default: 'generic', readonly: 'array' }], + code: 'type barIntersection = (string & number)[];', errors: [ { - messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, + column: 24, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: 'type barIntersection = Array;', }, { - code: 'let a: number[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array-simple' }], + code: ` +interface FooInterface { + '.bar': { baz: string[] }; +} + `, errors: [ { + column: 18, + data: { className: 'Array', readonlyPrefix: '', type: 'string' }, + line: 3, messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, - line: 1, - column: 8, }, ], + options: [{ default: 'generic' }], + output: ` +interface FooInterface { + '.bar': { baz: Array }; +} + `, }, { - code: 'let a: (string | number)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array-simple' }], + // https://github.com/typescript-eslint/typescript-eslint/issues/172 + code: 'type Unwrap = T extends Array ? E : T;', errors: [ { - messageId: 'errorStringGeneric', + column: 28, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'type Unwrap = T extends (infer E)[] ? E : T;', }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly number[] = [];', - options: [{ default: 'generic', readonly: 'array-simple' }], + // https://github.com/typescript-eslint/typescript-eslint/issues/172 + code: 'type Unwrap = T extends (infer E)[] ? E : T;', errors: [ { - messageId: 'errorStringArraySimple', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'number', - }, + column: 28, + data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringGeneric', }, ], + options: [{ default: 'generic' }], + output: 'type Unwrap = T extends Array ? E : T;', }, { - code: 'let a: readonly (string | number)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic', readonly: 'array-simple' }], + code: 'type Foo = ReadonlyArray[];', errors: [ { - messageId: 'errorStringGenericSimple', + column: 12, data: { className: 'ReadonlyArray', readonlyPrefix: 'readonly ', - type: 'T', + type: 'object', }, line: 1, - column: 8, - }, - ], - }, - { - code: 'let a: number[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, - line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'type Foo = (readonly object[])[];', }, { - code: 'let a: (string | number)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'generic' }], + code: 'const foo: Array void> = [];', errors: [ { - messageId: 'errorStringGeneric', + column: 12, data: { className: 'Array', readonlyPrefix: '', type: 'T' }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'const foo: (new (...args: any[]) => void)[] = [];', }, { - code: 'let a: readonly number[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic', readonly: 'generic' }], + code: 'const foo: ReadonlyArray void> = [];', errors: [ { - messageId: 'errorStringGeneric', + column: 12, data: { className: 'ReadonlyArray', readonlyPrefix: 'readonly ', - type: 'number', + type: 'T', }, line: 1, - column: 8, + messageId: 'errorStringArray', }, ], + options: [{ default: 'array' }], + output: 'const foo: readonly (new (...args: any[]) => void)[] = [];', }, { - code: 'let a: readonly (string | number)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic', readonly: 'generic' }], + code: "const x: Readonly = ['a', 'b'];", errors: [ { - messageId: 'errorStringGeneric', data: { - className: 'ReadonlyArray', + className: 'Readonly', readonlyPrefix: 'readonly ', - type: 'T', + type: 'string[]', }, - line: 1, - column: 8, - }, - ], - }, - { - code: 'let a: bigint[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'bigint' }, - line: 1, - column: 8, - }, - ], - }, - { - code: 'let a: (string | bigint)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 8, + messageId: 'errorStringArrayReadonly', }, ], + options: [{ default: 'array' }], + output: "const x: readonly string[] = ['a', 'b'];", }, { - code: 'let a: ReadonlyArray = [];', - output: 'let a: readonly bigint[] = [];', - options: [{ default: 'generic', readonly: 'array-simple' }], + code: 'declare function foo>(extra: E): E;', errors: [ { - messageId: 'errorStringArraySimple', data: { - className: 'ReadonlyArray', + className: 'Readonly', readonlyPrefix: 'readonly ', - type: 'bigint', + type: 'string[]', }, - line: 1, - column: 8, + messageId: 'errorStringArraySimpleReadonly', }, ], + options: [{ default: 'array-simple' }], + output: 'declare function foo(extra: E): E;', }, + ], + valid: [ + // Base cases from https://github.com/typescript-eslint/typescript-eslint/issues/2323#issuecomment-663977655 { - code: 'let a: (string | bigint)[] = [];', - output: 'let a: Array = [];', - options: [{ default: 'generic', readonly: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 8, - }, - ], + code: 'let a: number[] = [];', + options: [{ default: 'array' }], }, { - code: 'let a: readonly bigint[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic', readonly: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'bigint', - }, - line: 1, - column: 8, - }, - ], + code: 'let a: (string | number)[] = [];', + options: [{ default: 'array' }], }, { - code: 'let a: readonly (string | bigint)[] = [];', - output: 'let a: ReadonlyArray = [];', - options: [{ default: 'generic', readonly: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, - line: 1, - column: 8, - }, - ], + code: 'let a: readonly number[] = [];', + options: [{ default: 'array' }], }, - - // End of base cases - { - code: 'let a: { foo: Array }[] = [];', - output: 'let a: { foo: Bar[] }[] = [];', + code: 'let a: readonly (string | number)[] = [];', options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, - line: 1, - column: 15, - }, - ], }, { - code: 'let a: Array<{ foo: Bar[] }> = [];', - output: 'let a: Array<{ foo: Array }> = [];', - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, - line: 1, - column: 21, - }, - ], + code: 'let a: number[] = [];', + options: [{ default: 'array', readonly: 'array' }], }, { - code: 'let a: Array<{ foo: Foo | Bar[] }> = [];', - output: 'let a: Array<{ foo: Foo | Array }> = [];', - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, - line: 1, - column: 27, - }, - ], + code: 'let a: (string | number)[] = [];', + options: [{ default: 'array', readonly: 'array' }], }, { - code: 'function foo(a: Array): Array {}', - output: 'function foo(a: Bar[]): Bar[] {}', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, - line: 1, - column: 17, - }, - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'Bar' }, - line: 1, - column: 30, - }, - ], + code: 'let a: readonly number[] = [];', + options: [{ default: 'array', readonly: 'array' }], }, { - code: 'let x: Array = [undefined] as undefined[];', - output: 'let x: undefined[] = [undefined] as undefined[];', - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'undefined' }, - line: 1, - column: 8, - }, - ], + code: 'let a: readonly (string | number)[] = [];', + options: [{ default: 'array', readonly: 'array' }], }, { - code: "let y: string[] = >['2'];", - output: "let y: string[] = ['2'];", - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'string' }, - line: 1, - column: 20, - }, - ], + code: 'let a: number[] = [];', + options: [{ default: 'array', readonly: 'array-simple' }], }, { - code: "let z: Array = [3, '4'];", - output: "let z: any[] = [3, '4'];", - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'any' }, - line: 1, - column: 8, - }, - ], + code: 'let a: (string | number)[] = [];', + options: [{ default: 'array', readonly: 'array-simple' }], }, { - code: "let ya = [[1, '2']] as [number, string][];", - output: "let ya = [[1, '2']] as Array<[number, string]>;", - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGenericSimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 24, - }, - ], + code: 'let a: readonly number[] = [];', + options: [{ default: 'array', readonly: 'array-simple' }], }, { - code: 'type Arr = Array;', - output: 'type Arr = T[];', - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 15, - }, - ], + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'array', readonly: 'array-simple' }], }, { - code: ` -// Ignore user defined aliases -let yyyy: Arr>[]> = [[[['2']]]]; - `, - output: ` -// Ignore user defined aliases -let yyyy: Arr>>> = [[[['2']]]]; - `, - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGenericSimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 3, - column: 15, - }, - ], + code: 'let a: number[] = [];', + options: [{ default: 'array', readonly: 'generic' }], }, { - code: ` -interface ArrayClass { - foo: Array; - bar: T[]; - baz: Arr; - xyz: this[]; -} - `, - output: ` -interface ArrayClass { - foo: T[]; - bar: T[]; - baz: Arr; - xyz: this[]; -} - `, - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 3, - column: 8, - }, - ], + code: 'let a: (string | number)[] = [];', + options: [{ default: 'array', readonly: 'generic' }], }, { - code: ` -function barFunction(bar: ArrayClass[]) { - return bar.map(e => e.bar); -} - `, - output: ` -function barFunction(bar: Array>) { - return bar.map(e => e.bar); -} - `, - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGenericSimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 2, - column: 27, - }, - ], + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'array', readonly: 'generic' }], }, { - code: 'let barVar: ((c: number) => number)[];', - output: 'let barVar: Array<(c: number) => number>;', - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGenericSimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 13, - }, - ], + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'array', readonly: 'generic' }], }, { - code: 'type barUnion = (string | number | boolean)[];', - output: 'type barUnion = Array;', + code: 'let a: number[] = [];', options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGenericSimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 17, - }, - ], }, { - code: 'type barIntersection = (string & number)[];', - output: 'type barIntersection = Array;', + code: 'let a: Array = [];', options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGenericSimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 24, - }, - ], }, { - code: "let v: Array = [{ bar: 'bar' }];", - output: "let v: fooName.BarType[] = [{ bar: 'bar' }];", + code: 'let a: readonly number[] = [];', options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimple', - data: { - className: 'Array', - readonlyPrefix: '', - type: 'fooName.BarType', - }, - line: 1, - column: 8, - }, - ], }, { - code: "let w: fooName.BazType[] = [['baz']];", - output: "let w: Array> = [['baz']];", + code: 'let a: ReadonlyArray = [];', options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringGenericSimple', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 8, - }, - ], }, { - code: 'let x: Array = [undefined] as undefined[];', - output: 'let x: undefined[] = [undefined] as undefined[];', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'undefined' }, - line: 1, - column: 8, - }, - ], + code: 'let a: number[] = [];', + options: [{ default: 'array-simple', readonly: 'array' }], }, { - code: "let y: string[] = >['2'];", - output: "let y: string[] = ['2'];", - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'string' }, - line: 1, - column: 20, - }, - ], + code: 'let a: Array = [];', + options: [{ default: 'array-simple', readonly: 'array' }], }, { - code: "let z: Array = [3, '4'];", - output: "let z: any[] = [3, '4'];", - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'any' }, - line: 1, - column: 8, - }, - ], + code: 'let a: readonly number[] = [];', + options: [{ default: 'array-simple', readonly: 'array' }], }, { - code: 'type Arr = Array;', - output: 'type Arr = T[];', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 15, - }, - ], + code: 'let a: readonly (string | number)[] = [];', + options: [{ default: 'array-simple', readonly: 'array' }], }, { - code: ` -// Ignore user defined aliases -let yyyy: Arr>[]> = [[[['2']]]]; - `, - output: ` -// Ignore user defined aliases -let yyyy: Arr[][]> = [[[['2']]]]; - `, - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 3, - column: 15, - }, - ], + code: 'let a: number[] = [];', + options: [{ default: 'array-simple', readonly: 'array-simple' }], }, { - code: ` -interface ArrayClass { - foo: Array; - bar: T[]; - baz: Arr; -} - `, - output: ` -interface ArrayClass { - foo: T[]; - bar: T[]; - baz: Arr; -} - `, - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 3, - column: 8, - }, - ], + code: 'let a: Array = [];', + options: [{ default: 'array-simple', readonly: 'array-simple' }], }, { - code: ` -function fooFunction(foo: Array>) { - return foo.map(e => e.foo); -} - `, - output: ` -function fooFunction(foo: ArrayClass[]) { - return foo.map(e => e.foo); -} - `, - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 2, - column: 27, - }, - ], + code: 'let a: readonly number[] = [];', + options: [{ default: 'array-simple', readonly: 'array-simple' }], }, { - code: 'let fooVar: Array<(c: number) => number>;', - output: 'let fooVar: ((c: number) => number)[];', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 13, - }, - ], + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'array-simple', readonly: 'array-simple' }], }, { - code: 'type fooUnion = Array;', - output: 'type fooUnion = (string | number | boolean)[];', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 17, - }, - ], + code: 'let a: number[] = [];', + options: [{ default: 'array-simple', readonly: 'generic' }], }, { - code: 'type fooIntersection = Array;', - output: 'type fooIntersection = (string & number)[];', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 24, - }, - ], + code: 'let a: Array = [];', + options: [{ default: 'array-simple', readonly: 'generic' }], }, { - code: 'let x: Array;', - output: 'let x: any[];', + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'array-simple', readonly: 'generic' }], + }, + { + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'array-simple', readonly: 'generic' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic' }], + }, + { + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'generic' }], + }, + { + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'generic' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'generic' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'generic' }], + }, + { + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'generic', readonly: 'generic' }], + }, + { + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'generic', readonly: 'generic' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: 'let a: readonly number[] = [];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: 'let a: readonly (string | number)[] = [];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'array-simple' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'array-simple' }], + }, + { + code: 'let a: readonly number[] = [];', + options: [{ default: 'generic', readonly: 'array-simple' }], + }, + { + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'generic', readonly: 'array-simple' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: 'let a: readonly bigint[] = [];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: 'let a: readonly (string | bigint)[] = [];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'array-simple' }], + }, + { + code: 'let a: Array = [];', + options: [{ default: 'generic', readonly: 'array-simple' }], + }, + { + code: 'let a: readonly bigint[] = [];', + options: [{ default: 'generic', readonly: 'array-simple' }], + }, + { + code: 'let a: ReadonlyArray = [];', + options: [{ default: 'generic', readonly: 'array-simple' }], + }, + + // End of base cases + + { + code: 'let a = new Array();', options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'any' }, - line: 1, - column: 8, - }, - ], }, { - code: 'let x: Array<>;', - output: 'let x: any[];', + code: 'let a: { foo: Bar[] }[] = [];', options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'any' }, - line: 1, - column: 8, - }, - ], }, { - code: 'let x: Array;', - output: 'let x: any[];', + code: 'function foo(a: Array): Array {}', + options: [{ default: 'generic' }], + }, + { + code: 'let yy: number[][] = [[4, 5], [6]];', options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimple', - data: { className: 'Array', readonlyPrefix: '', type: 'any' }, - line: 1, - column: 8, - }, - ], }, { - code: 'let x: Array<>;', - output: 'let x: any[];', + code: ` +function fooFunction(foo: Array>) { + return foo.map(e => e.foo); +} + `, options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimple', - line: 1, - column: 8, - }, - ], }, { - code: 'let x: Array = [1] as number[];', - output: 'let x: Array = [1] as Array;', - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'number' }, - line: 1, - column: 31, - }, - ], + code: ` +function bazFunction(baz: Arr>) { + return baz.map(e => e.baz); +} + `, + options: [{ default: 'array-simple' }], }, { - code: "let y: string[] = >['2'];", - output: "let y: Array = >['2'];", - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'string' }, - line: 1, - column: 8, - }, - ], + code: 'let fooVar: Array<(c: number) => number>;', + options: [{ default: 'array-simple' }], }, { - code: "let ya = [[1, '2']] as [number, string][];", - output: "let ya = [[1, '2']] as Array<[number, string]>;", - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 24, - }, - ], + code: 'type fooUnion = Array;', + options: [{ default: 'array-simple' }], + }, + { + code: 'type fooIntersection = Array;', + options: [{ default: 'array-simple' }], }, { code: ` -// Ignore user defined aliases -let yyyy: Arr>[]> = [[[['2']]]]; - `, - output: ` -// Ignore user defined aliases -let yyyy: Arr>>> = [[[['2']]]]; +namespace fooName { + type BarType = { bar: string }; + type BazType = Arr; +} `, - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 3, - column: 15, - }, - ], + options: [{ default: 'array-simple' }], }, { code: ` -interface ArrayClass { - foo: Array; - bar: T[]; - baz: Arr; -} - `, - output: ` -interface ArrayClass { - foo: Array; - bar: Array; - baz: Arr; +interface FooInterface { + '.bar': { baz: string[] }; } `, - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 4, - column: 8, - }, - ], + options: [{ default: 'array-simple' }], + }, + { + code: 'let yy: number[][] = [[4, 5], [6]];', + options: [{ default: 'array' }], + }, + { + code: "let ya = [[1, '2']] as [number, string][];", + options: [{ default: 'array' }], }, { code: ` @@ -1774,59 +1861,27 @@ function barFunction(bar: ArrayClass[]) { return bar.map(e => e.bar); } `, - output: ` -function barFunction(bar: Array>) { - return bar.map(e => e.bar); + options: [{ default: 'array' }], + }, + { + code: ` +function bazFunction(baz: Arr>) { + return baz.map(e => e.baz); } `, - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 2, - column: 27, - }, - ], + options: [{ default: 'array' }], }, { code: 'let barVar: ((c: number) => number)[];', - output: 'let barVar: Array<(c: number) => number>;', - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 13, - }, - ], + options: [{ default: 'array' }], }, { code: 'type barUnion = (string | number | boolean)[];', - output: 'type barUnion = Array;', - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 17, - }, - ], + options: [{ default: 'array' }], }, { code: 'type barIntersection = (string & number)[];', - output: 'type barIntersection = Array;', - options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 24, - }, - ], + options: [{ default: 'array' }], }, { code: ` @@ -1834,125 +1889,71 @@ interface FooInterface { '.bar': { baz: string[] }; } `, - output: ` -interface FooInterface { - '.bar': { baz: Array }; + options: [{ default: 'array' }], + }, + { + // https://github.com/typescript-eslint/typescript-eslint/issues/172 + code: 'type Unwrap = T extends (infer E)[] ? E : T;', + options: [{ default: 'array' }], + }, + { + code: 'let xx: Array> = [[1, 2], [3]];', + options: [{ default: 'generic' }], + }, + { + code: 'type Arr = Array;', + options: [{ default: 'generic' }], + }, + { + code: ` +function fooFunction(foo: Array>) { + return foo.map(e => e.foo); } `, options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'string' }, - line: 3, - column: 18, - }, - ], }, { - // https://github.com/typescript-eslint/typescript-eslint/issues/172 - code: 'type Unwrap = T extends Array ? E : T;', - output: 'type Unwrap = T extends (infer E)[] ? E : T;', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 28, - }, - ], + code: ` +function bazFunction(baz: Arr>) { + return baz.map(e => e.baz); +} + `, + options: [{ default: 'generic' }], }, { - // https://github.com/typescript-eslint/typescript-eslint/issues/172 - code: 'type Unwrap = T extends (infer E)[] ? E : T;', - output: 'type Unwrap = T extends Array ? E : T;', + code: 'let fooVar: Array<(c: number) => number>;', options: [{ default: 'generic' }], - errors: [ - { - messageId: 'errorStringGeneric', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 28, - }, - ], }, { - code: 'type Foo = ReadonlyArray[];', - output: 'type Foo = (readonly object[])[];', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'object', - }, - line: 1, - column: 12, - }, - ], + code: 'type fooUnion = Array;', + options: [{ default: 'generic' }], }, { - code: 'const foo: Array void> = [];', - output: 'const foo: (new (...args: any[]) => void)[] = [];', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { className: 'Array', readonlyPrefix: '', type: 'T' }, - line: 1, - column: 12, - }, - ], + code: 'type fooIntersection = Array;', + options: [{ default: 'generic' }], }, { - code: 'const foo: ReadonlyArray void> = [];', - output: 'const foo: readonly (new (...args: any[]) => void)[] = [];', - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArray', - data: { - className: 'ReadonlyArray', - readonlyPrefix: 'readonly ', - type: 'T', - }, - line: 1, - column: 12, - }, - ], + // https://github.com/typescript-eslint/typescript-eslint/issues/172 + code: 'type Unwrap = T extends Array ? E : T;', + options: [{ default: 'generic' }], }, + + // nested readonly { - code: "const x: Readonly = ['a', 'b'];", - output: "const x: readonly string[] = ['a', 'b'];", - options: [{ default: 'array' }], - errors: [ - { - messageId: 'errorStringArrayReadonly', - data: { - className: 'Readonly', - readonlyPrefix: 'readonly ', - type: 'string[]', - }, - }, - ], + code: 'let a: ReadonlyArray = [[]];', + options: [{ default: 'array', readonly: 'generic' }], }, { - code: 'declare function foo>(extra: E): E;', - output: 'declare function foo(extra: E): E;', - options: [{ default: 'array-simple' }], - errors: [ - { - messageId: 'errorStringArraySimpleReadonly', - data: { - className: 'Readonly', - readonlyPrefix: 'readonly ', - type: 'string[]', - }, - }, - ], + code: 'let a: readonly Array[] = [[]];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: 'let a: Readonly = [];', + options: [{ default: 'generic', readonly: 'array' }], + }, + { + code: "const x: Readonly = 'a';", + options: [{ default: 'array' }], }, ], }); @@ -1975,13 +1976,13 @@ describe('array-type (nested)', () => { const result = linter.verifyAndFix( code, { + parser: '@typescript-eslint/parser', rules: { 'array-type': [ 2, { default: defaultOption, readonly: readonlyOption }, ], }, - parser: '@typescript-eslint/parser', }, { fix: true, diff --git a/packages/eslint-plugin/tests/rules/await-thenable.test.ts b/packages/eslint-plugin/tests/rules/await-thenable.test.ts index ebb8c6ef9537..3779ff6385fe 100644 --- a/packages/eslint-plugin/tests/rules/await-thenable.test.ts +++ b/packages/eslint-plugin/tests/rules/await-thenable.test.ts @@ -9,13 +9,193 @@ const messageId = 'await'; const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - tsconfigRootDir: rootDir, project: './tsconfig.json', + tsconfigRootDir: rootDir, }, }, }); ruleTester.run('await-thenable', rule, { + invalid: [ + { + code: 'await 0;', + errors: [ + { + line: 1, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: ' 0;', + }, + ], + }, + ], + }, + { + code: "await 'value';", + errors: [ + { + line: 1, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: " 'value';", + }, + ], + }, + ], + }, + { + code: "async () => await (Math.random() > 0.5 ? '' : 0);", + errors: [ + { + line: 1, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: "async () => (Math.random() > 0.5 ? '' : 0);", + }, + ], + }, + ], + }, + { + code: noFormat`async () => await(Math.random() > 0.5 ? '' : 0);`, + errors: [ + { + line: 1, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: "async () => (Math.random() > 0.5 ? '' : 0);", + }, + ], + }, + ], + }, + { + code: ` +class NonPromise extends Array {} +await new NonPromise(); + `, + errors: [ + { + line: 3, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: ` +class NonPromise extends Array {} + new NonPromise(); + `, + }, + ], + }, + ], + }, + { + code: ` +async function test() { + class IncorrectThenable { + then() {} + } + const thenable = new IncorrectThenable(); + + await thenable; +} + `, + errors: [ + { + line: 8, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: ` +async function test() { + class IncorrectThenable { + then() {} + } + const thenable = new IncorrectThenable(); + + thenable; +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const callback: (() => void) | undefined; +await callback?.(); + `, + errors: [ + { + line: 3, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: ` +declare const callback: (() => void) | undefined; + callback?.(); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const obj: { a?: { b?: () => void } }; +await obj.a?.b?.(); + `, + errors: [ + { + line: 3, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: ` +declare const obj: { a?: { b?: () => void } }; + obj.a?.b?.(); + `, + }, + ], + }, + ], + }, + { + code: ` +declare const obj: { a: { b: { c?: () => void } } } | undefined; +await obj?.a.b.c?.(); + `, + errors: [ + { + line: 3, + messageId, + suggestions: [ + { + messageId: 'removeAwait', + output: ` +declare const obj: { a: { b: { c?: () => void } } } | undefined; + obj?.a.b.c?.(); + `, + }, + ], + }, + ], + }, + ], + valid: [ ` async function test() { @@ -199,184 +379,4 @@ const doSomething = async ( }; `, ], - - invalid: [ - { - code: 'await 0;', - errors: [ - { - line: 1, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: ' 0;', - }, - ], - }, - ], - }, - { - code: "await 'value';", - errors: [ - { - line: 1, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: " 'value';", - }, - ], - }, - ], - }, - { - code: "async () => await (Math.random() > 0.5 ? '' : 0);", - errors: [ - { - line: 1, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: "async () => (Math.random() > 0.5 ? '' : 0);", - }, - ], - }, - ], - }, - { - code: noFormat`async () => await(Math.random() > 0.5 ? '' : 0);`, - errors: [ - { - line: 1, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: "async () => (Math.random() > 0.5 ? '' : 0);", - }, - ], - }, - ], - }, - { - code: ` -class NonPromise extends Array {} -await new NonPromise(); - `, - errors: [ - { - line: 3, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: ` -class NonPromise extends Array {} - new NonPromise(); - `, - }, - ], - }, - ], - }, - { - code: ` -async function test() { - class IncorrectThenable { - then() {} - } - const thenable = new IncorrectThenable(); - - await thenable; -} - `, - errors: [ - { - line: 8, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: ` -async function test() { - class IncorrectThenable { - then() {} - } - const thenable = new IncorrectThenable(); - - thenable; -} - `, - }, - ], - }, - ], - }, - { - code: ` -declare const callback: (() => void) | undefined; -await callback?.(); - `, - errors: [ - { - line: 3, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: ` -declare const callback: (() => void) | undefined; - callback?.(); - `, - }, - ], - }, - ], - }, - { - code: ` -declare const obj: { a?: { b?: () => void } }; -await obj.a?.b?.(); - `, - errors: [ - { - line: 3, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: ` -declare const obj: { a?: { b?: () => void } }; - obj.a?.b?.(); - `, - }, - ], - }, - ], - }, - { - code: ` -declare const obj: { a: { b: { c?: () => void } } } | undefined; -await obj?.a.b.c?.(); - `, - errors: [ - { - line: 3, - messageId, - suggestions: [ - { - messageId: 'removeAwait', - output: ` -declare const obj: { a: { b: { c?: () => void } } } | undefined; - obj?.a.b.c?.(); - `, - }, - ], - }, - ], - }, - ], }); diff --git a/packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts b/packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts index 8ac62dbb40cb..5cc5e3d5dfa3 100644 --- a/packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts +++ b/packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts @@ -5,196 +5,93 @@ import rule from '../../src/rules/ban-ts-comment'; const ruleTester = new RuleTester(); ruleTester.run('ts-expect-error', rule, { - valid: [ - '// just a comment containing @ts-expect-error somewhere', - ` -/* - @ts-expect-error running with long description in a block -*/ - `, - ` -/* @ts-expect-error not on the last line - */ - `, - ` -/** - * @ts-expect-error not on the last line - */ - `, - ` -/* not on the last line - * @ts-expect-error - */ - `, - ` -/* @ts-expect-error - * not on the last line */ - `, - { - code: '// @ts-expect-error', - options: [{ 'ts-expect-error': false }], - }, - { - code: '// @ts-expect-error here is why the error is expected', - options: [ - { - 'ts-expect-error': 'allow-with-description', - }, - ], - }, - { - code: ` -/* - * @ts-expect-error here is why the error is expected */ - `, - options: [ - { - 'ts-expect-error': 'allow-with-description', - }, - ], - }, - { - code: '// @ts-expect-error exactly 21 characters', - options: [ - { - 'ts-expect-error': 'allow-with-description', - minimumDescriptionLength: 21, - }, - ], - }, - { - code: ` -/* - * @ts-expect-error exactly 21 characters*/ - `, - options: [ - { - 'ts-expect-error': 'allow-with-description', - minimumDescriptionLength: 21, - }, - ], - }, - { - code: '// @ts-expect-error: TS1234 because xyz', - options: [ - { - 'ts-expect-error': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - minimumDescriptionLength: 10, - }, - ], - }, - { - code: ` -/* - * @ts-expect-error: TS1234 because xyz */ - `, - options: [ - { - 'ts-expect-error': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - minimumDescriptionLength: 10, - }, - ], - }, - { - code: noFormat`// @ts-expect-error 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦`, - options: [ - { - 'ts-expect-error': 'allow-with-description', - }, - ], - }, - ], invalid: [ { code: '// @ts-expect-error', - options: [{ 'ts-expect-error': true }], errors: [ { + column: 1, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 1, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: '/* @ts-expect-error */', - options: [{ 'ts-expect-error': true }], errors: [ { + column: 1, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 1, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: ` /* @ts-expect-error */ `, - options: [{ 'ts-expect-error': true }], errors: [ { + column: 1, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 2, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: ` /** on the last line @ts-expect-error */ `, - options: [{ 'ts-expect-error': true }], errors: [ { + column: 1, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 2, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: ` /** on the last line * @ts-expect-error */ `, - options: [{ 'ts-expect-error': true }], errors: [ { + column: 1, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 2, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: ` /** * @ts-expect-error: TODO */ `, - options: [ - { - 'ts-expect-error': 'allow-with-description', - minimumDescriptionLength: 10, - }, - ], errors: [ { + column: 1, data: { directive: 'expect-error', minimumDescriptionLength: 10 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 2, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', + }, + ], + options: [ + { + minimumDescriptionLength: 10, + 'ts-expect-error': 'allow-with-description', }, ], }, @@ -203,20 +100,20 @@ ruleTester.run('ts-expect-error', rule, { /** * @ts-expect-error: TS1234 because xyz */ `, + errors: [ + { + column: 1, + data: { directive: 'expect-error', minimumDescriptionLength: 25 }, + line: 2, + messageId: 'tsDirectiveCommentRequiresDescription', + }, + ], options: [ { + minimumDescriptionLength: 25, 'ts-expect-error': { descriptionFormat: '^: TS\\d+ because .+$', }, - minimumDescriptionLength: 25, - }, - ], - errors: [ - { - data: { directive: 'expect-error', minimumDescriptionLength: 25 }, - messageId: 'tsDirectiveCommentRequiresDescription', - line: 2, - column: 1, }, ], }, @@ -225,6 +122,14 @@ ruleTester.run('ts-expect-error', rule, { /** * @ts-expect-error: TS1234 */ `, + errors: [ + { + column: 1, + data: { directive: 'expect-error', format: '^: TS\\d+ because .+$' }, + line: 2, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', + }, + ], options: [ { 'ts-expect-error': { @@ -232,20 +137,20 @@ ruleTester.run('ts-expect-error', rule, { }, }, ], - errors: [ - { - data: { directive: 'expect-error', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', - line: 2, - column: 1, - }, - ], }, { code: ` /** * @ts-expect-error : TS1234 */ `, + errors: [ + { + column: 1, + data: { directive: 'expect-error', format: '^: TS\\d+ because .+$' }, + line: 2, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', + }, + ], options: [ { 'ts-expect-error': { @@ -253,69 +158,61 @@ ruleTester.run('ts-expect-error', rule, { }, }, ], - errors: [ - { - data: { directive: 'expect-error', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', - line: 2, - column: 1, - }, - ], }, { code: ` /** * @ts-expect-error 👨‍👩‍👧‍👦 */ `, - options: [ - { - 'ts-expect-error': 'allow-with-description', - }, - ], errors: [ { + column: 1, data: { directive: 'expect-error', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 2, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', + }, + ], + options: [ + { + 'ts-expect-error': 'allow-with-description', }, ], }, { code: '/** @ts-expect-error */', - options: [{ 'ts-expect-error': true }], errors: [ { + column: 1, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 1, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: '// @ts-expect-error: Suppress next line', - options: [{ 'ts-expect-error': true }], errors: [ { + column: 1, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 1, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: '/////@ts-expect-error: Suppress next line', - options: [{ 'ts-expect-error': true }], errors: [ { + column: 1, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 1, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: ` @@ -324,88 +221,78 @@ if (false) { console.log('hello'); } `, - options: [{ 'ts-expect-error': true }], errors: [ { + column: 3, data: { directive: 'expect-error' }, - messageId: 'tsDirectiveComment', line: 3, - column: 3, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-expect-error': true }], }, { code: '// @ts-expect-error', - options: [ - { - 'ts-expect-error': 'allow-with-description', - }, - ], errors: [ { + column: 1, data: { directive: 'expect-error', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 1, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], - }, - { - code: '// @ts-expect-error: TODO', options: [ { 'ts-expect-error': 'allow-with-description', - minimumDescriptionLength: 10, }, ], + }, + { + code: '// @ts-expect-error: TODO', errors: [ { + column: 1, data: { directive: 'expect-error', minimumDescriptionLength: 10 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 1, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], - }, - { - code: '// @ts-expect-error: TS1234 because xyz', options: [ { - 'ts-expect-error': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - minimumDescriptionLength: 25, + minimumDescriptionLength: 10, + 'ts-expect-error': 'allow-with-description', }, ], + }, + { + code: '// @ts-expect-error: TS1234 because xyz', errors: [ { + column: 1, data: { directive: 'expect-error', minimumDescriptionLength: 25 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 1, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], - }, - { - code: '// @ts-expect-error: TS1234', options: [ { + minimumDescriptionLength: 25, 'ts-expect-error': { descriptionFormat: '^: TS\\d+ because .+$', }, }, ], + }, + { + code: '// @ts-expect-error: TS1234', errors: [ { + column: 1, data: { directive: 'expect-error', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', line: 1, - column: 1, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', }, ], - }, - { - code: noFormat`// @ts-expect-error : TS1234 because xyz`, options: [ { 'ts-expect-error': { @@ -413,70 +300,56 @@ if (false) { }, }, ], + }, + { + code: noFormat`// @ts-expect-error : TS1234 because xyz`, errors: [ { + column: 1, data: { directive: 'expect-error', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', line: 1, - column: 1, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', }, ], - }, - { - code: noFormat`// @ts-expect-error 👨‍👩‍👧‍👦`, options: [ { - 'ts-expect-error': 'allow-with-description', + 'ts-expect-error': { + descriptionFormat: '^: TS\\d+ because .+$', + }, }, ], + }, + { + code: noFormat`// @ts-expect-error 👨‍👩‍👧‍👦`, errors: [ { + column: 1, data: { directive: 'expect-error', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 1, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], - }, - ], -}); - -ruleTester.run('ts-ignore', rule, { - valid: [ - '// just a comment containing @ts-ignore somewhere', - { - code: '// @ts-ignore', - options: [{ 'ts-ignore': false }], - }, - { - code: '// @ts-ignore I think that I am exempted from any need to follow the rules!', - options: [{ 'ts-ignore': 'allow-with-description' }], - }, - { - code: ` -/* - @ts-ignore running with long description in a block -*/ - `, options: [ { - 'ts-ignore': 'allow-with-description', - minimumDescriptionLength: 21, + 'ts-expect-error': 'allow-with-description', }, ], }, + ], + valid: [ + '// just a comment containing @ts-expect-error somewhere', ` /* - @ts-ignore + @ts-expect-error running with long description in a block */ `, ` -/* @ts-ignore not on the last line +/* @ts-expect-error not on the last line */ `, ` /** - * @ts-ignore not on the last line + * @ts-expect-error not on the last line */ `, ` @@ -485,84 +358,98 @@ ruleTester.run('ts-ignore', rule, { */ `, ` -/* @ts-ignore +/* @ts-expect-error * not on the last line */ `, { - code: '// @ts-ignore: TS1234 because xyz', - options: [ - { - 'ts-ignore': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - minimumDescriptionLength: 10, - }, - ], + code: '// @ts-expect-error', + options: [{ 'ts-expect-error': false }], }, { - code: noFormat`// @ts-ignore 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦`, + code: '// @ts-expect-error here is why the error is expected', options: [ { - 'ts-ignore': 'allow-with-description', + 'ts-expect-error': 'allow-with-description', }, ], }, { code: ` /* - * @ts-ignore here is why the error is expected */ + * @ts-expect-error here is why the error is expected */ `, options: [ { - 'ts-ignore': 'allow-with-description', + 'ts-expect-error': 'allow-with-description', }, ], }, { - code: '// @ts-ignore exactly 21 characters', + code: '// @ts-expect-error exactly 21 characters', options: [ { - 'ts-ignore': 'allow-with-description', minimumDescriptionLength: 21, + 'ts-expect-error': 'allow-with-description', }, ], }, { code: ` /* - * @ts-ignore exactly 21 characters*/ + * @ts-expect-error exactly 21 characters*/ `, options: [ { - 'ts-ignore': 'allow-with-description', minimumDescriptionLength: 21, + 'ts-expect-error': 'allow-with-description', + }, + ], + }, + { + code: '// @ts-expect-error: TS1234 because xyz', + options: [ + { + minimumDescriptionLength: 10, + 'ts-expect-error': { + descriptionFormat: '^: TS\\d+ because .+$', + }, }, ], }, { code: ` /* - * @ts-ignore: TS1234 because xyz */ + * @ts-expect-error: TS1234 because xyz */ `, options: [ { - 'ts-ignore': { + minimumDescriptionLength: 10, + 'ts-expect-error': { descriptionFormat: '^: TS\\d+ because .+$', }, - minimumDescriptionLength: 10, + }, + ], + }, + { + code: noFormat`// @ts-expect-error 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦`, + options: [ + { + 'ts-expect-error': 'allow-with-description', }, ], }, ], +}); + +ruleTester.run('ts-ignore', rule, { invalid: [ { code: '// @ts-ignore', - options: [{ 'ts-ignore': true, 'ts-expect-error': true }], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 1, column: 1, + line: 1, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -571,17 +458,15 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [{ 'ts-expect-error': true, 'ts-ignore': true }], }, { code: '// @ts-ignore', - options: [ - { 'ts-ignore': true, 'ts-expect-error': 'allow-with-description' }, - ], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 1, column: 1, + line: 1, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -590,14 +475,17 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [ + { 'ts-expect-error': 'allow-with-description', 'ts-ignore': true }, + ], }, { code: '// @ts-ignore', errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 1, column: 1, + line: 1, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -609,12 +497,11 @@ ruleTester.run('ts-ignore', rule, { }, { code: '/* @ts-ignore */', - options: [{ 'ts-ignore': true }], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 1, column: 1, + line: 1, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -623,18 +510,18 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [{ 'ts-ignore': true }], }, { code: ` /* @ts-ignore */ `, - options: [{ 'ts-ignore': true }], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 2, column: 1, + line: 2, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -646,18 +533,18 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [{ 'ts-ignore': true }], }, { code: ` /** on the last line @ts-ignore */ `, - options: [{ 'ts-ignore': true }], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 2, column: 1, + line: 2, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -669,18 +556,18 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [{ 'ts-ignore': true }], }, { code: ` /** on the last line * @ts-ignore */ `, - options: [{ 'ts-ignore': true }], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 2, column: 1, + line: 2, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -692,15 +579,15 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [{ 'ts-ignore': true }], }, { code: '/** @ts-ignore */', - options: [{ 'ts-ignore': true, 'ts-expect-error': false }], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 1, column: 1, + line: 1, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -709,23 +596,18 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [{ 'ts-expect-error': false, 'ts-ignore': true }], }, { code: ` /** * @ts-ignore: TODO */ `, - options: [ - { - 'ts-expect-error': 'allow-with-description', - minimumDescriptionLength: 10, - }, - ], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 2, column: 1, + line: 2, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -737,25 +619,23 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [ + { + minimumDescriptionLength: 10, + 'ts-expect-error': 'allow-with-description', + }, + ], }, { code: ` /** * @ts-ignore: TS1234 because xyz */ `, - options: [ - { - 'ts-expect-error': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - minimumDescriptionLength: 25, - }, - ], errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 2, column: 1, + line: 2, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -767,14 +647,22 @@ ruleTester.run('ts-ignore', rule, { ], }, ], + options: [ + { + minimumDescriptionLength: 25, + 'ts-expect-error': { + descriptionFormat: '^: TS\\d+ because .+$', + }, + }, + ], }, { code: '// @ts-ignore: Suppress next line', errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 1, column: 1, + line: 1, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -788,9 +676,9 @@ ruleTester.run('ts-ignore', rule, { code: '/////@ts-ignore: Suppress next line', errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 1, column: 1, + line: 1, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -809,9 +697,9 @@ if (false) { `, errors: [ { - messageId: 'tsIgnoreInsteadOfExpectError', - line: 3, column: 3, + line: 3, + messageId: 'tsIgnoreInsteadOfExpectError', suggestions: [ { messageId: 'replaceTsIgnoreWithTsExpectError', @@ -828,61 +716,69 @@ if (false) { }, { code: '// @ts-ignore', - options: [{ 'ts-ignore': 'allow-with-description' }], errors: [ { + column: 1, data: { directive: 'ignore', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 1, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], + options: [{ 'ts-ignore': 'allow-with-description' }], }, { code: noFormat`// @ts-ignore `, - options: [{ 'ts-ignore': 'allow-with-description' }], errors: [ { + column: 1, data: { directive: 'ignore', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 1, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], + options: [{ 'ts-ignore': 'allow-with-description' }], }, { code: '// @ts-ignore .', - options: [{ 'ts-ignore': 'allow-with-description' }], errors: [ { + column: 1, data: { directive: 'ignore', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', line: 1, - column: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], + options: [{ 'ts-ignore': 'allow-with-description' }], }, { code: '// @ts-ignore: TS1234 because xyz', - options: [ + errors: [ { - 'ts-ignore': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - minimumDescriptionLength: 25, + column: 1, + data: { directive: 'ignore', minimumDescriptionLength: 25 }, + line: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], - errors: [ + options: [ { - data: { directive: 'ignore', minimumDescriptionLength: 25 }, - messageId: 'tsDirectiveCommentRequiresDescription', - line: 1, - column: 1, + minimumDescriptionLength: 25, + 'ts-ignore': { + descriptionFormat: '^: TS\\d+ because .+$', + }, }, ], }, { code: '// @ts-ignore: TS1234', + errors: [ + { + column: 1, + data: { directive: 'ignore', format: '^: TS\\d+ because .+$' }, + line: 1, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', + }, + ], options: [ { 'ts-ignore': { @@ -890,53 +786,292 @@ if (false) { }, }, ], + }, + { + code: noFormat`// @ts-ignore : TS1234 because xyz`, errors: [ { + column: 1, data: { directive: 'ignore', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', line: 1, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', + }, + ], + options: [ + { + 'ts-ignore': { + descriptionFormat: '^: TS\\d+ because .+$', + }, + }, + ], + }, + { + code: noFormat`// @ts-ignore 👨‍👩‍👧‍👦`, + errors: [ + { column: 1, + data: { directive: 'ignore', minimumDescriptionLength: 3 }, + line: 1, + messageId: 'tsDirectiveCommentRequiresDescription', + }, + ], + options: [ + { + 'ts-ignore': 'allow-with-description', }, ], }, + ], + valid: [ + '// just a comment containing @ts-ignore somewhere', { - code: noFormat`// @ts-ignore : TS1234 because xyz`, + code: '// @ts-ignore', + options: [{ 'ts-ignore': false }], + }, + { + code: '// @ts-ignore I think that I am exempted from any need to follow the rules!', + options: [{ 'ts-ignore': 'allow-with-description' }], + }, + { + code: ` +/* + @ts-ignore running with long description in a block +*/ + `, + options: [ + { + minimumDescriptionLength: 21, + 'ts-ignore': 'allow-with-description', + }, + ], + }, + ` +/* + @ts-ignore +*/ + `, + ` +/* @ts-ignore not on the last line + */ + `, + ` +/** + * @ts-ignore not on the last line + */ + `, + ` +/* not on the last line + * @ts-expect-error + */ + `, + ` +/* @ts-ignore + * not on the last line */ + `, + { + code: '// @ts-ignore: TS1234 because xyz', + options: [ + { + minimumDescriptionLength: 10, + 'ts-ignore': { + descriptionFormat: '^: TS\\d+ because .+$', + }, + }, + ], + }, + { + code: noFormat`// @ts-ignore 👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦`, + options: [ + { + 'ts-ignore': 'allow-with-description', + }, + ], + }, + { + code: ` +/* + * @ts-ignore here is why the error is expected */ + `, + options: [ + { + 'ts-ignore': 'allow-with-description', + }, + ], + }, + { + code: '// @ts-ignore exactly 21 characters', + options: [ + { + minimumDescriptionLength: 21, + 'ts-ignore': 'allow-with-description', + }, + ], + }, + { + code: ` +/* + * @ts-ignore exactly 21 characters*/ + `, + options: [ + { + minimumDescriptionLength: 21, + 'ts-ignore': 'allow-with-description', + }, + ], + }, + { + code: ` +/* + * @ts-ignore: TS1234 because xyz */ + `, options: [ { + minimumDescriptionLength: 10, 'ts-ignore': { descriptionFormat: '^: TS\\d+ because .+$', }, }, ], + }, + ], +}); + +ruleTester.run('ts-nocheck', rule, { + invalid: [ + { + code: '// @ts-nocheck', errors: [ { - data: { directive: 'ignore', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', + column: 1, + data: { directive: 'nocheck' }, + line: 1, + messageId: 'tsDirectiveComment', + }, + ], + options: [{ 'ts-nocheck': true }], + }, + { + code: '// @ts-nocheck', + errors: [ + { + column: 1, + data: { directive: 'nocheck' }, line: 1, + messageId: 'tsDirectiveComment', + }, + ], + }, + { + code: '// @ts-nocheck: Suppress next line', + errors: [ + { column: 1, + data: { directive: 'nocheck' }, + line: 1, + messageId: 'tsDirectiveComment', }, ], }, { - code: noFormat`// @ts-ignore 👨‍👩‍👧‍👦`, - options: [ + code: ` +if (false) { + // @ts-nocheck: Unreachable code error + console.log('hello'); +} + `, + errors: [ { - 'ts-ignore': 'allow-with-description', + column: 3, + data: { directive: 'nocheck' }, + line: 3, + messageId: 'tsDirectiveComment', }, ], + }, + { + code: '// @ts-nocheck', errors: [ { - data: { directive: 'ignore', minimumDescriptionLength: 3 }, + column: 1, + data: { directive: 'nocheck', minimumDescriptionLength: 3 }, + line: 1, messageId: 'tsDirectiveCommentRequiresDescription', + }, + ], + options: [{ 'ts-nocheck': 'allow-with-description' }], + }, + { + code: '// @ts-nocheck: TS1234 because xyz', + errors: [ + { + column: 1, + data: { directive: 'nocheck', minimumDescriptionLength: 25 }, line: 1, + messageId: 'tsDirectiveCommentRequiresDescription', + }, + ], + options: [ + { + minimumDescriptionLength: 25, + 'ts-nocheck': { + descriptionFormat: '^: TS\\d+ because .+$', + }, + }, + ], + }, + { + code: '// @ts-nocheck: TS1234', + errors: [ + { column: 1, + data: { directive: 'nocheck', format: '^: TS\\d+ because .+$' }, + line: 1, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', + }, + ], + options: [ + { + 'ts-nocheck': { + descriptionFormat: '^: TS\\d+ because .+$', + }, + }, + ], + }, + { + code: noFormat`// @ts-nocheck : TS1234 because xyz`, + errors: [ + { + column: 1, + data: { directive: 'nocheck', format: '^: TS\\d+ because .+$' }, + line: 1, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', + }, + ], + options: [ + { + 'ts-nocheck': { + descriptionFormat: '^: TS\\d+ because .+$', + }, + }, + ], + }, + { + code: noFormat`// @ts-nocheck 👨‍👩‍👧‍👦`, + errors: [ + { + column: 1, + data: { directive: 'nocheck', minimumDescriptionLength: 3 }, + line: 1, + messageId: 'tsDirectiveCommentRequiresDescription', + }, + ], + options: [ + { + 'ts-nocheck': 'allow-with-description', }, ], }, ], -}); - -ruleTester.run('ts-nocheck', rule, { valid: [ '// just a comment containing @ts-nocheck somewhere', { @@ -955,8 +1090,8 @@ ruleTester.run('ts-nocheck', rule, { `, options: [ { - 'ts-nocheck': 'allow-with-description', minimumDescriptionLength: 21, + 'ts-nocheck': 'allow-with-description', }, ], }, @@ -964,10 +1099,10 @@ ruleTester.run('ts-nocheck', rule, { code: '// @ts-nocheck: TS1234 because xyz', options: [ { + minimumDescriptionLength: 10, 'ts-nocheck': { descriptionFormat: '^: TS\\d+ because .+$', }, - minimumDescriptionLength: 10, }, ], }, @@ -993,144 +1128,135 @@ ruleTester.run('ts-nocheck', rule, { '/** @ts-nocheck */', '/* @ts-nocheck */', ], +}); + +ruleTester.run('ts-check', rule, { invalid: [ { - code: '// @ts-nocheck', - options: [{ 'ts-nocheck': true }], + code: '// @ts-check', errors: [ { - data: { directive: 'nocheck' }, - messageId: 'tsDirectiveComment', - line: 1, column: 1, - }, - ], - }, - { - code: '// @ts-nocheck', - errors: [ - { - data: { directive: 'nocheck' }, - messageId: 'tsDirectiveComment', + data: { directive: 'check' }, line: 1, - column: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-check': true }], }, { - code: '// @ts-nocheck: Suppress next line', + code: '// @ts-check: Suppress next line', errors: [ { - data: { directive: 'nocheck' }, - messageId: 'tsDirectiveComment', - line: 1, column: 1, + data: { directive: 'check' }, + line: 1, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-check': true }], }, { code: ` if (false) { - // @ts-nocheck: Unreachable code error + // @ts-check: Unreachable code error console.log('hello'); } `, errors: [ { - data: { directive: 'nocheck' }, - messageId: 'tsDirectiveComment', - line: 3, column: 3, + data: { directive: 'check' }, + line: 3, + messageId: 'tsDirectiveComment', }, ], + options: [{ 'ts-check': true }], }, { - code: '// @ts-nocheck', - options: [{ 'ts-nocheck': 'allow-with-description' }], + code: '// @ts-check', errors: [ { - data: { directive: 'nocheck', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', - line: 1, column: 1, + data: { directive: 'check', minimumDescriptionLength: 3 }, + line: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], + options: [{ 'ts-check': 'allow-with-description' }], }, { - code: '// @ts-nocheck: TS1234 because xyz', + code: '// @ts-check: TS1234 because xyz', + errors: [ + { + column: 1, + data: { directive: 'check', minimumDescriptionLength: 25 }, + line: 1, + messageId: 'tsDirectiveCommentRequiresDescription', + }, + ], options: [ { - 'ts-nocheck': { + minimumDescriptionLength: 25, + 'ts-check': { descriptionFormat: '^: TS\\d+ because .+$', }, - minimumDescriptionLength: 25, }, ], + }, + { + code: '// @ts-check: TS1234', errors: [ { - data: { directive: 'nocheck', minimumDescriptionLength: 25 }, - messageId: 'tsDirectiveCommentRequiresDescription', - line: 1, column: 1, + data: { directive: 'check', format: '^: TS\\d+ because .+$' }, + line: 1, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', }, ], - }, - { - code: '// @ts-nocheck: TS1234', options: [ { - 'ts-nocheck': { + 'ts-check': { descriptionFormat: '^: TS\\d+ because .+$', }, }, ], + }, + { + code: noFormat`// @ts-check : TS1234 because xyz`, errors: [ { - data: { directive: 'nocheck', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', - line: 1, column: 1, + data: { directive: 'check', format: '^: TS\\d+ because .+$' }, + line: 1, + messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', }, ], - }, - { - code: noFormat`// @ts-nocheck : TS1234 because xyz`, options: [ { - 'ts-nocheck': { + 'ts-check': { descriptionFormat: '^: TS\\d+ because .+$', }, }, ], + }, + { + code: noFormat`// @ts-check 👨‍👩‍👧‍👦`, errors: [ { - data: { directive: 'nocheck', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', - line: 1, column: 1, + data: { directive: 'check', minimumDescriptionLength: 3 }, + line: 1, + messageId: 'tsDirectiveCommentRequiresDescription', }, ], - }, - { - code: noFormat`// @ts-nocheck 👨‍👩‍👧‍👦`, options: [ { - 'ts-nocheck': 'allow-with-description', - }, - ], - errors: [ - { - data: { directive: 'nocheck', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', - line: 1, - column: 1, + 'ts-check': 'allow-with-description', }, ], }, ], -}); - -ruleTester.run('ts-check', rule, { valid: [ '// just a comment containing @ts-check somewhere', ` @@ -1145,17 +1271,17 @@ ruleTester.run('ts-check', rule, { { code: '// @ts-check with a description and also with a no-op // @ts-ignore', options: [ - { 'ts-check': 'allow-with-description', minimumDescriptionLength: 3 }, + { minimumDescriptionLength: 3, 'ts-check': 'allow-with-description' }, ], }, { code: '// @ts-check: TS1234 because xyz', options: [ { + minimumDescriptionLength: 10, 'ts-check': { descriptionFormat: '^: TS\\d+ because .+$', }, - minimumDescriptionLength: 10, }, ], }, @@ -1196,130 +1322,4 @@ ruleTester.run('ts-check', rule, { options: [{ 'ts-check': true }], }, ], - invalid: [ - { - code: '// @ts-check', - options: [{ 'ts-check': true }], - errors: [ - { - data: { directive: 'check' }, - messageId: 'tsDirectiveComment', - line: 1, - column: 1, - }, - ], - }, - { - code: '// @ts-check: Suppress next line', - options: [{ 'ts-check': true }], - errors: [ - { - data: { directive: 'check' }, - messageId: 'tsDirectiveComment', - line: 1, - column: 1, - }, - ], - }, - { - code: ` -if (false) { - // @ts-check: Unreachable code error - console.log('hello'); -} - `, - options: [{ 'ts-check': true }], - errors: [ - { - data: { directive: 'check' }, - messageId: 'tsDirectiveComment', - line: 3, - column: 3, - }, - ], - }, - { - code: '// @ts-check', - options: [{ 'ts-check': 'allow-with-description' }], - errors: [ - { - data: { directive: 'check', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', - line: 1, - column: 1, - }, - ], - }, - { - code: '// @ts-check: TS1234 because xyz', - options: [ - { - 'ts-check': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - minimumDescriptionLength: 25, - }, - ], - errors: [ - { - data: { directive: 'check', minimumDescriptionLength: 25 }, - messageId: 'tsDirectiveCommentRequiresDescription', - line: 1, - column: 1, - }, - ], - }, - { - code: '// @ts-check: TS1234', - options: [ - { - 'ts-check': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - }, - ], - errors: [ - { - data: { directive: 'check', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', - line: 1, - column: 1, - }, - ], - }, - { - code: noFormat`// @ts-check : TS1234 because xyz`, - options: [ - { - 'ts-check': { - descriptionFormat: '^: TS\\d+ because .+$', - }, - }, - ], - errors: [ - { - data: { directive: 'check', format: '^: TS\\d+ because .+$' }, - messageId: 'tsDirectiveCommentDescriptionNotMatchPattern', - line: 1, - column: 1, - }, - ], - }, - { - code: noFormat`// @ts-check 👨‍👩‍👧‍👦`, - options: [ - { - 'ts-check': 'allow-with-description', - }, - ], - errors: [ - { - data: { directive: 'check', minimumDescriptionLength: 3 }, - messageId: 'tsDirectiveCommentRequiresDescription', - line: 1, - column: 1, - }, - ], - }, - ], }); diff --git a/packages/eslint-plugin/tests/rules/ban-tslint-comment.test.ts b/packages/eslint-plugin/tests/rules/ban-tslint-comment.test.ts index 062fcd531988..fdc00a705308 100644 --- a/packages/eslint-plugin/tests/rules/ban-tslint-comment.test.ts +++ b/packages/eslint-plugin/tests/rules/ban-tslint-comment.test.ts @@ -4,10 +4,10 @@ import rule from '../../src/rules/ban-tslint-comment'; interface Testable { code: string; - text?: string; column?: number; line?: number; output?: string; + text?: string; } const PALANTIR_EXAMPLES: Testable[] = [ @@ -22,9 +22,9 @@ const PALANTIR_EXAMPLES: Testable[] = [ { code: '// tslint:disable-next-line' }, // Disables all rules for the following line { code: 'someCode(); // tslint:disable-line', - text: '// tslint:disable-line', column: 13, output: 'someCode();', + text: '// tslint:disable-line', }, // Disables all rules for the current line { code: '// tslint:disable-next-line:rule1 rule2 rule3...', @@ -38,17 +38,31 @@ const MORE_EXAMPLES: Testable[] = [ // tslint:disable-line console.log(woah); `, + line: 2, output: `const woah = doSomeStuff(); console.log(woah); `, text: '// tslint:disable-line', - line: 2, }, ] const ruleTester = new RuleTester(); ruleTester.run('ban-tslint-comment', rule, { + invalid: [...PALANTIR_EXAMPLES, ...MORE_EXAMPLES].map( + ({ code, column, line, output, text }) => ({ + code, + errors: [ + { + column: column ?? 1, + data: { text: text ?? code }, + line: line ?? 1, + messageId: 'commentDetected' as const, + }, + ], + output: output ?? '', + }), + ), valid: [ { code: 'let a: readonly any[] = [];', @@ -66,18 +80,4 @@ ruleTester.run('ban-tslint-comment', rule, { code: '/* another comment that mentions tslint */', }, ], - invalid: [...PALANTIR_EXAMPLES, ...MORE_EXAMPLES].map( - ({ code, column, line, output, text }) => ({ - code, - output: output ?? '', - errors: [ - { - column: column ?? 1, - line: line ?? 1, - data: { text: text ?? code }, - messageId: 'commentDetected' as const, - }, - ], - }), - ), }); diff --git a/packages/eslint-plugin/tests/rules/class-literal-property-style.test.ts b/packages/eslint-plugin/tests/rules/class-literal-property-style.test.ts index 902274292572..5564a2ce7719 100644 --- a/packages/eslint-plugin/tests/rules/class-literal-property-style.test.ts +++ b/packages/eslint-plugin/tests/rules/class-literal-property-style.test.ts @@ -5,458 +5,358 @@ import rule from '../../src/rules/class-literal-property-style'; const ruleTester = new RuleTester(); ruleTester.run('class-literal-property-style', rule, { - valid: [ - ` + invalid: [ + { + code: ` class Mx { - declare readonly p1 = 1; + get p1() { + return 'hello world'; + } } - `, - ` + `, + errors: [ + { + column: 7, + line: 3, + messageId: 'preferFieldStyle', + suggestions: [ + { + messageId: 'preferFieldStyleSuggestion', + output: ` class Mx { readonly p1 = 'hello world'; } - `, - ` -class Mx { - p1 = 'hello world'; -} - `, - ` -class Mx { - static p1 = 'hello world'; -} - `, - ` -class Mx { - p1: string; -} - `, - ` -class Mx { - get p1(); -} - `, - ` -class Mx { - get p1() {} -} - `, - ` -abstract class Mx { - abstract get p1(): string; -} - `, - ` - class Mx { - get mySetting() { - if (this._aValue) { - return 'on'; - } - - return 'off'; - } - } - `, - ` - class Mx { - get mySetting() { - return \`build-\${process.env.build}\`; - } - } - `, - ` - class Mx { - getMySetting() { - if (this._aValue) { - return 'on'; - } - - return 'off'; - } - } - `, - ` - class Mx { - public readonly myButton = styled.button\` - color: \${props => (props.primary ? 'hotpink' : 'turquoise')}; - \`; - } - `, - ` - class Mx { - set p1(val) {} - get p1() { - return ''; - } - } - `, - ` - let p1 = 'p1'; - class Mx { - set [p1](val) {} - get [p1]() { - return ''; - } - } - `, - ` - let p1 = 'p1'; - class Mx { - set [/* before set */ p1 /* after set */](val) {} - get [/* before get */ p1 /* after get */]() { - return ''; - } - } - `, - ` - class Mx { - set ['foo'](val) {} - get foo() { - return ''; - } - set bar(val) {} - get ['bar']() { - return ''; - } - set ['baz'](val) {} - get baz() { - return ''; - } - } - `, - { - code: ` - class Mx { - public get myButton() { - return styled.button\` - color: \${props => (props.primary ? 'hotpink' : 'turquoise')}; - \`; - } - } `, - options: ['fields'], + }, + ], + }, + ], }, { code: ` class Mx { - public declare readonly foo = 1; + get p1() { + return \`hello world\`; + } } `, - options: ['getters'], - }, - { - code: ` + errors: [ + { + column: 7, + line: 3, + messageId: 'preferFieldStyle', + suggestions: [ + { + messageId: 'preferFieldStyleSuggestion', + output: ` class Mx { - get p1() { - return 'hello world'; - } + readonly p1 = \`hello world\`; } `, - options: ['getters'], + }, + ], + }, + ], }, { code: ` class Mx { - p1 = 'hello world'; + static get p1() { + return 'hello world'; + } } `, - options: ['getters'], - }, - { - code: ` + errors: [ + { + column: 14, + line: 3, + messageId: 'preferFieldStyle', + suggestions: [ + { + messageId: 'preferFieldStyleSuggestion', + output: ` class Mx { - p1: string; + static readonly p1 = 'hello world'; } `, - options: ['getters'], + }, + ], + }, + ], }, { code: ` class Mx { - readonly p1 = [1, 2, 3]; + public static get foo() { + return 1; + } } `, - options: ['getters'], - }, - { - code: ` + errors: [ + { + column: 21, + line: 3, + messageId: 'preferFieldStyle', + suggestions: [ + { + messageId: 'preferFieldStyleSuggestion', + output: ` class Mx { - static p1: string; + public static readonly foo = 1; } `, - options: ['getters'], + }, + ], + }, + ], }, { code: ` class Mx { - static get p1() { - return 'hello world'; + public get [myValue]() { + return 'a literal value'; } } `, - options: ['getters'], - }, - { - code: ` - class Mx { - public readonly myButton = styled.button\` - color: \${props => (props.primary ? 'hotpink' : 'turquoise')}; - \`; - } - `, - options: ['getters'], - }, - { - code: ` - class Mx { - public get myButton() { - return styled.button\` - color: \${props => (props.primary ? 'hotpink' : 'turquoise')}; - \`; - } - } - `, - options: ['getters'], - }, - { - code: ` - class A { - private readonly foo: string = 'bar'; - constructor(foo: string) { - this.foo = foo; - } - } + errors: [ + { + column: 15, + line: 3, + messageId: 'preferFieldStyle', + suggestions: [ + { + messageId: 'preferFieldStyleSuggestion', + output: ` +class Mx { + public readonly [myValue] = 'a literal value'; +} `, - options: ['getters'], + }, + ], + }, + ], }, { code: ` - class A { - private readonly foo: string = 'bar'; - constructor(foo: string) { - this['foo'] = foo; - } - } +class Mx { + public get [myValue]() { + return 12345n; + } +} `, - options: ['getters'], - }, - { - code: ` - class A { - private readonly foo: string = 'bar'; - constructor(foo: string) { - const bar = new (class { - private readonly foo: string = 'baz'; - constructor() { - this.foo = 'qux'; - } - })(); - this['foo'] = foo; - } - } + errors: [ + { + column: 15, + line: 3, + messageId: 'preferFieldStyle', + suggestions: [ + { + messageId: 'preferFieldStyleSuggestion', + output: ` +class Mx { + public readonly [myValue] = 12345n; +} `, - options: ['getters'], + }, + ], + }, + ], }, - ], - invalid: [ { code: ` class Mx { - get p1() { - return 'hello world'; - } + public readonly [myValue] = 'a literal value'; } `, errors: [ { - messageId: 'preferFieldStyle', - column: 7, + column: 20, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { - messageId: 'preferFieldStyleSuggestion', + messageId: 'preferGetterStyleSuggestion', output: ` class Mx { - readonly p1 = 'hello world'; + public get [myValue]() { return 'a literal value'; } } `, }, ], }, ], + options: ['getters'], }, { code: ` class Mx { - get p1() { - return \`hello world\`; - } + readonly p1 = 'hello world'; } `, errors: [ { - messageId: 'preferFieldStyle', - column: 7, + column: 12, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { - messageId: 'preferFieldStyleSuggestion', + messageId: 'preferGetterStyleSuggestion', output: ` class Mx { - readonly p1 = \`hello world\`; + get p1() { return 'hello world'; } } `, }, ], }, ], + options: ['getters'], }, { code: ` class Mx { - static get p1() { - return 'hello world'; - } + readonly p1 = \`hello world\`; } `, errors: [ { - messageId: 'preferFieldStyle', - column: 14, + column: 12, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { - messageId: 'preferFieldStyleSuggestion', + messageId: 'preferGetterStyleSuggestion', output: ` class Mx { - static readonly p1 = 'hello world'; + get p1() { return \`hello world\`; } } `, }, ], }, ], + options: ['getters'], }, { code: ` class Mx { - public static get foo() { - return 1; - } + static readonly p1 = 'hello world'; } `, errors: [ { - messageId: 'preferFieldStyle', - column: 21, + column: 19, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { - messageId: 'preferFieldStyleSuggestion', + messageId: 'preferGetterStyleSuggestion', output: ` class Mx { - public static readonly foo = 1; + static get p1() { return 'hello world'; } } `, }, ], }, ], + options: ['getters'], }, { code: ` class Mx { - public get [myValue]() { - return 'a literal value'; + protected get p1() { + return 'hello world'; } } `, errors: [ { - messageId: 'preferFieldStyle', - column: 15, + column: 17, line: 3, + messageId: 'preferFieldStyle', suggestions: [ { messageId: 'preferFieldStyleSuggestion', output: ` class Mx { - public readonly [myValue] = 'a literal value'; + protected readonly p1 = 'hello world'; } `, }, ], }, ], + options: ['fields'], }, { code: ` class Mx { - public get [myValue]() { - return 12345n; - } + protected readonly p1 = 'hello world'; } `, errors: [ { - messageId: 'preferFieldStyle', - column: 15, + column: 22, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { - messageId: 'preferFieldStyleSuggestion', + messageId: 'preferGetterStyleSuggestion', output: ` class Mx { - public readonly [myValue] = 12345n; + protected get p1() { return 'hello world'; } } `, }, ], }, ], + options: ['getters'], }, { code: ` class Mx { - public readonly [myValue] = 'a literal value'; + public static get p1() { + return 'hello world'; + } } `, errors: [ { - messageId: 'preferGetterStyle', - column: 20, + column: 21, line: 3, + messageId: 'preferFieldStyle', suggestions: [ { - messageId: 'preferGetterStyleSuggestion', + messageId: 'preferFieldStyleSuggestion', output: ` class Mx { - public get [myValue]() { return 'a literal value'; } + public static readonly p1 = 'hello world'; } `, }, ], }, ], - options: ['getters'], }, { code: ` class Mx { - readonly p1 = 'hello world'; + public static readonly p1 = 'hello world'; } `, errors: [ { - messageId: 'preferGetterStyle', - column: 12, + column: 26, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { messageId: 'preferGetterStyleSuggestion', output: ` class Mx { - get p1() { return 'hello world'; } + public static get p1() { return 'hello world'; } } `, }, @@ -468,45 +368,74 @@ class Mx { { code: ` class Mx { - readonly p1 = \`hello world\`; + public get myValue() { + return gql\` + { + user(id: 5) { + firstName + lastName + } + } + \`; + } } `, errors: [ { - messageId: 'preferGetterStyle', - column: 12, + column: 14, line: 3, + messageId: 'preferFieldStyle', suggestions: [ { - messageId: 'preferGetterStyleSuggestion', + messageId: 'preferFieldStyleSuggestion', output: ` class Mx { - get p1() { return \`hello world\`; } + public readonly myValue = gql\` + { + user(id: 5) { + firstName + lastName + } + } + \`; } `, }, ], }, ], - options: ['getters'], }, { code: ` class Mx { - static readonly p1 = 'hello world'; + public readonly myValue = gql\` + { + user(id: 5) { + firstName + lastName + } + } + \`; } `, errors: [ { - messageId: 'preferGetterStyle', column: 19, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { messageId: 'preferGetterStyleSuggestion', output: ` class Mx { - static get p1() { return 'hello world'; } + public get myValue() { return gql\` + { + user(id: 5) { + firstName + lastName + } + } + \`; } } `, }, @@ -517,48 +446,82 @@ class Mx { }, { code: ` -class Mx { - protected get p1() { - return 'hello world'; +class A { + private readonly foo: string = 'bar'; + constructor(foo: string) { + const bar = new (class { + private readonly foo: string = 'baz'; + constructor() { + this.foo = 'qux'; + } + })(); } } `, errors: [ { - messageId: 'preferFieldStyle', - column: 17, + column: 20, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { - messageId: 'preferFieldStyleSuggestion', + messageId: 'preferGetterStyleSuggestion', output: ` -class Mx { - protected readonly p1 = 'hello world'; +class A { + private get foo() { return 'bar'; } + constructor(foo: string) { + const bar = new (class { + private readonly foo: string = 'baz'; + constructor() { + this.foo = 'qux'; + } + })(); + } } `, }, ], }, ], - options: ['fields'], + options: ['getters'], }, { code: ` -class Mx { - protected readonly p1 = 'hello world'; +class A { + private readonly ['foo']: string = 'bar'; + constructor(foo: string) { + const bar = new (class { + private readonly foo: string = 'baz'; + constructor() {} + })(); + + if (bar) { + this.foo = 'baz'; + } + } } `, errors: [ { + column: 24, + line: 6, messageId: 'preferGetterStyle', - column: 22, - line: 3, suggestions: [ { messageId: 'preferGetterStyleSuggestion', output: ` -class Mx { - protected get p1() { return 'hello world'; } +class A { + private readonly ['foo']: string = 'bar'; + constructor(foo: string) { + const bar = new (class { + private get foo() { return 'baz'; } + constructor() {} + })(); + + if (bar) { + this.foo = 'baz'; + } + } } `, }, @@ -569,254 +532,291 @@ class Mx { }, { code: ` -class Mx { - public static get p1() { - return 'hello world'; +class A { + private readonly foo: string = 'bar'; + constructor(foo: string) { + function func() { + this.foo = 'aa'; + } } } `, errors: [ { - messageId: 'preferFieldStyle', - column: 21, + column: 20, line: 3, + messageId: 'preferGetterStyle', suggestions: [ { - messageId: 'preferFieldStyleSuggestion', + messageId: 'preferGetterStyleSuggestion', output: ` +class A { + private get foo() { return 'bar'; } + constructor(foo: string) { + function func() { + this.foo = 'aa'; + } + } +} + `, + }, + ], + }, + ], + options: ['getters'], + }, + ], + valid: [ + ` class Mx { - public static readonly p1 = 'hello world'; + declare readonly p1 = 1; +} + `, + ` +class Mx { + readonly p1 = 'hello world'; +} + `, + ` +class Mx { + p1 = 'hello world'; +} + `, + ` +class Mx { + static p1 = 'hello world'; +} + `, + ` +class Mx { + p1: string; +} + `, + ` +class Mx { + get p1(); +} + `, + ` +class Mx { + get p1() {} +} + `, + ` +abstract class Mx { + abstract get p1(): string; } + `, + ` + class Mx { + get mySetting() { + if (this._aValue) { + return 'on'; + } + + return 'off'; + } + } + `, + ` + class Mx { + get mySetting() { + return \`build-\${process.env.build}\`; + } + } + `, + ` + class Mx { + getMySetting() { + if (this._aValue) { + return 'on'; + } + + return 'off'; + } + } + `, + ` + class Mx { + public readonly myButton = styled.button\` + color: \${props => (props.primary ? 'hotpink' : 'turquoise')}; + \`; + } + `, + ` + class Mx { + set p1(val) {} + get p1() { + return ''; + } + } + `, + ` + let p1 = 'p1'; + class Mx { + set [p1](val) {} + get [p1]() { + return ''; + } + } + `, + ` + let p1 = 'p1'; + class Mx { + set [/* before set */ p1 /* after set */](val) {} + get [/* before get */ p1 /* after get */]() { + return ''; + } + } + `, + ` + class Mx { + set ['foo'](val) {} + get foo() { + return ''; + } + set bar(val) {} + get ['bar']() { + return ''; + } + set ['baz'](val) {} + get baz() { + return ''; + } + } + `, + { + code: ` + class Mx { + public get myButton() { + return styled.button\` + color: \${props => (props.primary ? 'hotpink' : 'turquoise')}; + \`; + } + } `, - }, - ], - }, - ], + options: ['fields'], }, { code: ` class Mx { - public static readonly p1 = 'hello world'; + public declare readonly foo = 1; } `, - errors: [ - { - messageId: 'preferGetterStyle', - column: 26, - line: 3, - suggestions: [ - { - messageId: 'preferGetterStyleSuggestion', - output: ` + options: ['getters'], + }, + { + code: ` class Mx { - public static get p1() { return 'hello world'; } + get p1() { + return 'hello world'; + } } `, - }, - ], - }, - ], options: ['getters'], }, { code: ` class Mx { - public get myValue() { - return gql\` - { - user(id: 5) { - firstName - lastName - } - } - \`; - } + p1 = 'hello world'; } `, - errors: [ - { - messageId: 'preferFieldStyle', - column: 14, - line: 3, - suggestions: [ - { - messageId: 'preferFieldStyleSuggestion', - output: ` + options: ['getters'], + }, + { + code: ` class Mx { - public readonly myValue = gql\` - { - user(id: 5) { - firstName - lastName - } - } - \`; + p1: string; } `, - }, - ], - }, - ], + options: ['getters'], }, { code: ` class Mx { - public readonly myValue = gql\` - { - user(id: 5) { - firstName - lastName - } - } - \`; + readonly p1 = [1, 2, 3]; } `, - errors: [ - { - messageId: 'preferGetterStyle', - column: 19, - line: 3, - suggestions: [ - { - messageId: 'preferGetterStyleSuggestion', - output: ` -class Mx { - public get myValue() { return gql\` + options: ['getters'], + }, { - user(id: 5) { - firstName - lastName - } - } - \`; } + code: ` +class Mx { + static p1: string; } `, - }, - ], - }, - ], options: ['getters'], }, { code: ` -class A { - private readonly foo: string = 'bar'; - constructor(foo: string) { - const bar = new (class { - private readonly foo: string = 'baz'; - constructor() { - this.foo = 'qux'; - } - })(); +class Mx { + static get p1() { + return 'hello world'; } } `, options: ['getters'], - errors: [ - { - messageId: 'preferGetterStyle', - column: 20, - line: 3, - suggestions: [ - { - messageId: 'preferGetterStyleSuggestion', - output: ` -class A { - private get foo() { return 'bar'; } - constructor(foo: string) { - const bar = new (class { - private readonly foo: string = 'baz'; - constructor() { - this.foo = 'qux'; - } - })(); - } -} + }, + { + code: ` + class Mx { + public readonly myButton = styled.button\` + color: \${props => (props.primary ? 'hotpink' : 'turquoise')}; + \`; + } `, - }, - ], - }, - ], + options: ['getters'], }, { code: ` -class A { - private readonly ['foo']: string = 'bar'; - constructor(foo: string) { - const bar = new (class { - private readonly foo: string = 'baz'; - constructor() {} - })(); - - if (bar) { - this.foo = 'baz'; - } - } -} + class Mx { + public get myButton() { + return styled.button\` + color: \${props => (props.primary ? 'hotpink' : 'turquoise')}; + \`; + } + } `, options: ['getters'], - errors: [ - { - messageId: 'preferGetterStyle', - column: 24, - line: 6, - suggestions: [ - { - messageId: 'preferGetterStyleSuggestion', - output: ` -class A { - private readonly ['foo']: string = 'bar'; - constructor(foo: string) { - const bar = new (class { - private get foo() { return 'baz'; } - constructor() {} - })(); - - if (bar) { - this.foo = 'baz'; - } - } -} + }, + { + code: ` + class A { + private readonly foo: string = 'bar'; + constructor(foo: string) { + this.foo = foo; + } + } `, - }, - ], - }, - ], + options: ['getters'], }, { code: ` -class A { - private readonly foo: string = 'bar'; - constructor(foo: string) { - function func() { - this.foo = 'aa'; - } - } -} + class A { + private readonly foo: string = 'bar'; + constructor(foo: string) { + this['foo'] = foo; + } + } `, options: ['getters'], - errors: [ - { - messageId: 'preferGetterStyle', - column: 20, - line: 3, - suggestions: [ - { - messageId: 'preferGetterStyleSuggestion', - output: ` -class A { - private get foo() { return 'bar'; } - constructor(foo: string) { - function func() { - this.foo = 'aa'; - } - } -} + }, + { + code: ` + class A { + private readonly foo: string = 'bar'; + constructor(foo: string) { + const bar = new (class { + private readonly foo: string = 'baz'; + constructor() { + this.foo = 'qux'; + } + })(); + this['foo'] = foo; + } + } `, - }, - ], - }, - ], + options: ['getters'], }, ], }); diff --git a/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this-core.test.ts b/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this-core.test.ts index 50f527082553..d0193efdc99d 100644 --- a/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this-core.test.ts +++ b/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this-core.test.ts @@ -8,440 +8,440 @@ import rule from '../../../src/rules/class-methods-use-this'; const ruleTester = new RuleTester(); ruleTester.run('class-methods-use-this', rule, { - valid: [ - { - code: 'class A { constructor() {} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { foo() {this} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: "class A { foo() {this.bar = 'bar';} }", - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { foo() {bar(this);} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A extends B { foo() {super.foo();} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { foo() { if(true) { return this; } } }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { static foo() {} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: '({ a(){} });', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { foo() { () => this; } }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: '({ a: function () {} });', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { foo() {this} bar() {} }', - options: [{ exceptMethods: ['bar'] }], - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { "foo"() { } }', - options: [{ exceptMethods: ['foo'] }], - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { 42() { } }', - options: [{ exceptMethods: ['42'] }], - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'class A { foo = function() {this} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { foo = () => {this} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { foo = () => {super.toString} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { static foo = function() {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { static foo = () => {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { #bar() {} }', - options: [{ exceptMethods: ['#bar'] }], - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { foo = function () {} }', - options: [{ enforceForClassFields: false }], - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { foo = () => {} }', - options: [{ enforceForClassFields: false }], - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { foo() { return class { [this.foo] = 1 }; } }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - { - code: 'class A { static {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, - }, - ], invalid: [ { code: 'class A { foo() {} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, }, { code: 'class A { foo() {/**this**/} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, }, { code: 'class A { foo() {var a = function () {this};} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, }, { code: 'class A { foo() {var a = function () {var b = function(){this}};} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, }, { code: 'class A { foo() {window.this} }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, }, { code: "class A { foo() {that.this = 'this';} }", - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, }, { code: 'class A { foo() { () => undefined; } }', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, }, { code: 'class A { foo() {} bar() {} }', - options: [{ exceptMethods: ['bar'] }], - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + options: [{ exceptMethods: ['bar'] }], }, { code: 'class A { foo() {} hasOwnProperty() {} }', - options: [{ exceptMethods: ['foo'] }], - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 20, - messageId: 'missingThis', data: { name: "method 'hasOwnProperty'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + options: [{ exceptMethods: ['foo'] }], }, { code: 'class A { [foo]() {} }', - options: [{ exceptMethods: ['foo'] }], - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 11, - messageId: 'missingThis', data: { name: 'method' }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + options: [{ exceptMethods: ['foo'] }], }, { code: 'class A { #foo() { } foo() {} #bar() {} }', - options: [{ exceptMethods: ['#foo'] }], - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 22, - messageId: 'missingThis', data: { name: "method 'foo'" }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, { - type: AST_NODE_TYPES.FunctionExpression, - line: 1, column: 31, - messageId: 'missingThis', data: { name: 'private method #bar' }, + line: 1, + messageId: 'missingThis', + type: AST_NODE_TYPES.FunctionExpression, }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + options: [{ exceptMethods: ['#foo'] }], }, { code: "class A { foo(){} 'bar'(){} 123(){} [`baz`](){} [a](){} [f(a)](){} get quux(){} set[a](b){} *quuux(){} }", - languageOptions: { parserOptions: { ecmaVersion: 6 } }, errors: [ { - messageId: 'missingThis', + column: 11, data: { name: "method 'foo'" }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 11, }, { - messageId: 'missingThis', + column: 19, data: { name: "method 'bar'" }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 19, }, { - messageId: 'missingThis', + column: 29, data: { name: "method '123'" }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 29, }, { - messageId: 'missingThis', + column: 37, data: { name: "method 'baz'" }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 37, }, { - messageId: 'missingThis', + column: 49, data: { name: 'method' }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 49, }, { - messageId: 'missingThis', + column: 57, data: { name: 'method' }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 57, }, { - messageId: 'missingThis', + column: 68, data: { name: "getter 'quux'" }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 68, }, { - messageId: 'missingThis', + column: 81, data: { name: 'setter' }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 81, }, { - messageId: 'missingThis', + column: 93, data: { name: "generator method 'quuux'" }, + messageId: 'missingThis', type: AST_NODE_TYPES.FunctionExpression, - column: 93, }, ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, }, { code: 'class A { foo = function() {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: "method 'foo'" }, column: 11, + data: { name: "method 'foo'" }, endColumn: 25, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { foo = () => {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: "method 'foo'" }, column: 11, + data: { name: "method 'foo'" }, endColumn: 17, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { #foo = function() {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: 'private method #foo' }, column: 11, + data: { name: 'private method #foo' }, endColumn: 26, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { #foo = () => {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: 'private method #foo' }, column: 11, + data: { name: 'private method #foo' }, endColumn: 18, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { #foo() {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: 'private method #foo' }, column: 11, + data: { name: 'private method #foo' }, endColumn: 15, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { get #foo() {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: 'private getter #foo' }, column: 11, + data: { name: 'private getter #foo' }, endColumn: 19, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { set #foo(x) {} }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: 'private setter #foo' }, column: 11, + data: { name: 'private setter #foo' }, endColumn: 19, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { foo () { return class { foo = this }; } }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: "method 'foo'" }, column: 11, + data: { name: "method 'foo'" }, endColumn: 15, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { foo () { return function () { foo = this }; } }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: "method 'foo'" }, column: 11, + data: { name: "method 'foo'" }, endColumn: 15, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, { code: 'class A { foo () { return class { static { this; } } } }', - languageOptions: { parserOptions: { ecmaVersion: 2022 } }, errors: [ { - messageId: 'missingThis', - data: { name: "method 'foo'" }, column: 11, + data: { name: "method 'foo'" }, endColumn: 15, + messageId: 'missingThis', }, ], + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + }, + ], + valid: [ + { + code: 'class A { constructor() {} }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: 'class A { foo() {this} }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: "class A { foo() {this.bar = 'bar';} }", + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: 'class A { foo() {bar(this);} }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: 'class A extends B { foo() {super.foo();} }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: 'class A { foo() { if(true) { return this; } } }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: 'class A { static foo() {} }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: '({ a(){} });', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: 'class A { foo() { () => this; } }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: '({ a: function () {} });', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: 'class A { foo() {this} bar() {} }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + options: [{ exceptMethods: ['bar'] }], + }, + { + code: 'class A { "foo"() { } }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + options: [{ exceptMethods: ['foo'] }], + }, + { + code: 'class A { 42() { } }', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + options: [{ exceptMethods: ['42'] }], + }, + { + code: 'class A { foo = function() {this} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + }, + { + code: 'class A { foo = () => {this} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + }, + { + code: 'class A { foo = () => {super.toString} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + }, + { + code: 'class A { static foo = function() {} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + }, + { + code: 'class A { static foo = () => {} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + }, + { + code: 'class A { #bar() {} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + options: [{ exceptMethods: ['#bar'] }], + }, + { + code: 'class A { foo = function () {} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + options: [{ enforceForClassFields: false }], + }, + { + code: 'class A { foo = () => {} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + options: [{ enforceForClassFields: false }], + }, + { + code: 'class A { foo() { return class { [this.foo] = 1 }; } }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, + }, + { + code: 'class A { static {} }', + languageOptions: { parserOptions: { ecmaVersion: 2022 } }, }, ], }); diff --git a/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this.test.ts b/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this.test.ts index 575daef11a8b..4238d4c05582 100644 --- a/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this.test.ts +++ b/packages/eslint-plugin/tests/rules/class-methods-use-this/class-methods-use-this.test.ts @@ -5,536 +5,523 @@ import rule from '../../../src/rules/class-methods-use-this'; const ruleTester = new RuleTester(); ruleTester.run('class-methods-use-this', rule, { - valid: [ + invalid: [ { code: ` -class Foo implements Bar { +class Foo { method() {} } `, - options: [{ ignoreClassesThatImplementAnInterface: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` -class Foo implements Bar { - get getter() {} +class Foo { + private method() {} } `, - options: [{ ignoreClassesThatImplementAnInterface: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` -class Foo implements Bar { - set setter() {} +class Foo { + protected method() {} } `, - options: [{ ignoreClassesThatImplementAnInterface: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo { - override method() {} + #method() {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo { - private override method() {} + get getter(): number {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo { - protected override method() {} + private get getter(): number {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo { - override get getter(): number {} + protected get getter(): number {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo { - private override get getter(): number {} + get #getter(): number {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo { - protected override get getter(): number {} + set setter(b: number) {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [ + { + ignoreClassesThatImplementAnInterface: false, + ignoreOverrideMethods: false, + }, + ], }, { code: ` class Foo { - override set setter(v: number) {} + private set setter(b: number) {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo { - private override set setter(v: number) {} + protected set setter(b: number) {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo { - protected override set setter(v: number) {} + set #setter(b: number) {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{}], }, { code: ` class Foo implements Bar { - override method() {} + method() {} } `, - options: [ + errors: [ { - ignoreClassesThatImplementAnInterface: true, - ignoreOverrideMethods: true, + messageId: 'missingThis', }, ], + options: [{ ignoreClassesThatImplementAnInterface: false }], }, { code: ` class Foo implements Bar { - private override method() {} + #method() {} } `, - options: [ + errors: [ { - // _interface_ cannot have `private`/`protected` modifier on members. - // We should ignore only public members. - ignoreClassesThatImplementAnInterface: 'public-fields', - // But overridden properties should be ignored. - ignoreOverrideMethods: true, + messageId: 'missingThis', }, ], + options: [{ ignoreClassesThatImplementAnInterface: false }], }, { code: ` class Foo implements Bar { - protected override method() {} + private method() {} } `, + errors: [ + { + messageId: 'missingThis', + }, + ], options: [ { // _interface_ cannot have `private`/`protected` modifier on members. // We should ignore only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - // But overridden properties should be ignored. - ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - override get getter(): number {} + protected method() {} } `, - options: [ + errors: [ { - ignoreClassesThatImplementAnInterface: true, - ignoreOverrideMethods: true, + messageId: 'missingThis', }, ], - }, - { - code: ` -class Foo implements Bar { - private override get getter(): number {} -} - `, options: [ { // _interface_ cannot have `private`/`protected` modifier on members. // We should ignore only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - // But overridden properties should be ignored. - ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - protected override get getter(): number {} + get getter(): number {} } `, - options: [ + errors: [ { - // _interface_ cannot have `private`/`protected` modifier on members. - // We should ignore only public members. - ignoreClassesThatImplementAnInterface: 'public-fields', - // But overridden properties should be ignored. - ignoreOverrideMethods: true, + messageId: 'missingThis', }, ], + options: [{ ignoreClassesThatImplementAnInterface: false }], }, { code: ` class Foo implements Bar { - override set setter(v: number) {} + get #getter(): number {} } `, - options: [ + errors: [ { - ignoreClassesThatImplementAnInterface: true, - ignoreOverrideMethods: true, + messageId: 'missingThis', }, ], + options: [{ ignoreClassesThatImplementAnInterface: false }], }, { code: ` class Foo implements Bar { - private override set setter(v: number) {} + private get getter(): number {} } `, + errors: [ + { + messageId: 'missingThis', + }, + ], options: [ { // _interface_ cannot have `private`/`protected` modifier on members. // We should ignore only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - // But overridden properties should be ignored. - ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - protected override set setter(v: number) {} + protected get getter(): number {} } `, + errors: [ + { + messageId: 'missingThis', + }, + ], options: [ { // _interface_ cannot have `private`/`protected` modifier on members. // We should ignore only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - // But overridden properties should be ignored. - ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - property = () => {}; -} - `, - options: [{ ignoreClassesThatImplementAnInterface: true }], - }, - { - code: ` -class Foo { - override property = () => {}; -} - `, - options: [{ ignoreOverrideMethods: true }], - }, - { - code: ` -class Foo { - private override property = () => {}; + set setter(v: number) {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{ ignoreClassesThatImplementAnInterface: false }], }, { code: ` -class Foo { - protected override property = () => {}; +class Foo implements Bar { + set #setter(v: number) {} } `, - options: [{ ignoreOverrideMethods: true }], + errors: [ + { + messageId: 'missingThis', + }, + ], + options: [{ ignoreClassesThatImplementAnInterface: false }], }, { code: ` class Foo implements Bar { - override property = () => {}; + private set setter(v: number) {} } `, + errors: [ + { + messageId: 'missingThis', + }, + ], options: [ { - ignoreClassesThatImplementAnInterface: true, - ignoreOverrideMethods: true, + // _interface_ cannot have `private`/`protected` modifier on members. + // We should ignore only public members. + ignoreClassesThatImplementAnInterface: 'public-fields', + ignoreOverrideMethods: false, }, ], }, { code: ` class Foo implements Bar { - property = () => {}; + protected set setter(v: number) {} } `, - options: [ + errors: [ { - ignoreClassesThatImplementAnInterface: false, - enforceForClassFields: false, + messageId: 'missingThis', }, ], - }, - { - code: ` -class Foo { - override property = () => {}; -} - `, - options: [ - { - ignoreOverrideMethods: false, - enforceForClassFields: false, - }, - ], - }, - { - code: ` -class Foo implements Bar { - private override property = () => {}; -} - `, - options: [ - { - // _interface_ cannot have `private`/`protected` modifier on members. - // We should check only public members. - ignoreClassesThatImplementAnInterface: 'public-fields', - // But overridden properties should be ignored. - ignoreOverrideMethods: true, - }, - ], - }, - { - code: ` -class Foo implements Bar { - protected override property = () => {}; -} - `, options: [ { // _interface_ cannot have `private`/`protected` modifier on members. - // We should check only public members. + // We should ignore only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - // But overridden properties should be ignored. - ignoreOverrideMethods: true, + ignoreOverrideMethods: false, }, ], }, - ], - invalid: [ { code: ` class Foo { - method() {} + override method() {} } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], + options: [{ ignoreOverrideMethods: false }], }, { code: ` class Foo { - private method() {} + override get getter(): number {} } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], + options: [{ ignoreOverrideMethods: false }], }, { code: ` class Foo { - protected method() {} + override set setter(v: number) {} } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], + options: [{ ignoreOverrideMethods: false }], }, { code: ` -class Foo { - #method() {} +class Foo implements Bar { + override method() {} } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], - }, - { - code: ` -class Foo { - get getter(): number {} -} - `, - options: [{}], - errors: [ + options: [ { - messageId: 'missingThis', + ignoreClassesThatImplementAnInterface: false, + ignoreOverrideMethods: false, }, ], }, { code: ` -class Foo { - private get getter(): number {} +class Foo implements Bar { + override get getter(): number {} } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], - }, - { - code: ` -class Foo { - protected get getter(): number {} -} - `, - options: [{}], - errors: [ + options: [ { - messageId: 'missingThis', + ignoreClassesThatImplementAnInterface: false, + ignoreOverrideMethods: false, }, ], }, { code: ` -class Foo { - get #getter(): number {} +class Foo implements Bar { + override set setter(v: number) {} } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], - }, - { - code: ` -class Foo { - set setter(b: number) {} -} - `, options: [ { ignoreClassesThatImplementAnInterface: false, ignoreOverrideMethods: false, }, ], - errors: [ - { - messageId: 'missingThis', - }, - ], }, { code: ` -class Foo { - private set setter(b: number) {} +class Foo implements Bar { + property = () => {}; } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], + options: [{ ignoreClassesThatImplementAnInterface: false }], }, { code: ` -class Foo { - protected set setter(b: number) {} +class Foo implements Bar { + #property = () => {}; } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], + options: [{ ignoreClassesThatImplementAnInterface: false }], }, { code: ` class Foo { - set #setter(b: number) {} + override property = () => {}; } `, - options: [{}], errors: [ { messageId: 'missingThis', }, ], + options: [{ ignoreOverrideMethods: false }], }, { code: ` class Foo implements Bar { - method() {} + override property = () => {}; } `, - options: [{ ignoreClassesThatImplementAnInterface: false }], errors: [ { messageId: 'missingThis', }, ], + options: [ + { + ignoreClassesThatImplementAnInterface: false, + ignoreOverrideMethods: false, + }, + ], }, { code: ` class Foo implements Bar { - #method() {} + private property = () => {}; } `, - options: [{ ignoreClassesThatImplementAnInterface: false }], errors: [ { messageId: 'missingThis', }, ], - }, - { - code: ` -class Foo implements Bar { - private method() {} -} - `, options: [ { // _interface_ cannot have `private`/`protected` modifier on members. @@ -542,18 +529,18 @@ class Foo implements Bar { ignoreClassesThatImplementAnInterface: 'public-fields', }, ], - errors: [ - { - messageId: 'missingThis', - }, - ], }, { code: ` class Foo implements Bar { - protected method() {} + protected property = () => {}; } `, + errors: [ + { + messageId: 'missingThis', + }, + ], options: [ { // _interface_ cannot have `private`/`protected` modifier on members. @@ -561,106 +548,122 @@ class Foo implements Bar { ignoreClassesThatImplementAnInterface: 'public-fields', }, ], - errors: [ - { - messageId: 'missingThis', - }, - ], }, + ], + valid: [ { code: ` class Foo implements Bar { - get getter(): number {} + method() {} } `, - options: [{ ignoreClassesThatImplementAnInterface: false }], - errors: [ - { - messageId: 'missingThis', - }, - ], + options: [{ ignoreClassesThatImplementAnInterface: true }], }, { code: ` class Foo implements Bar { - get #getter(): number {} + get getter() {} } `, - options: [{ ignoreClassesThatImplementAnInterface: false }], - errors: [ - { - messageId: 'missingThis', - }, - ], + options: [{ ignoreClassesThatImplementAnInterface: true }], }, { code: ` class Foo implements Bar { - private get getter(): number {} + set setter() {} } `, - options: [ - { - // _interface_ cannot have `private`/`protected` modifier on members. - // We should ignore only public members. - ignoreClassesThatImplementAnInterface: 'public-fields', - }, - ], - errors: [ - { - messageId: 'missingThis', - }, - ], + options: [{ ignoreClassesThatImplementAnInterface: true }], }, { code: ` -class Foo implements Bar { - protected get getter(): number {} +class Foo { + override method() {} } `, - options: [ - { - // _interface_ cannot have `private`/`protected` modifier on members. - // We should ignore only public members. - ignoreClassesThatImplementAnInterface: 'public-fields', - }, - ], - errors: [ - { - messageId: 'missingThis', - }, - ], + options: [{ ignoreOverrideMethods: true }], }, { code: ` -class Foo implements Bar { - set setter(v: number) {} +class Foo { + private override method() {} } `, - options: [{ ignoreClassesThatImplementAnInterface: false }], - errors: [ - { - messageId: 'missingThis', - }, - ], + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo { + protected override method() {} +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo { + override get getter(): number {} +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo { + private override get getter(): number {} +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo { + protected override get getter(): number {} +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo { + override set setter(v: number) {} +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo { + private override set setter(v: number) {} +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo { + protected override set setter(v: number) {} +} + `, + options: [{ ignoreOverrideMethods: true }], }, { code: ` class Foo implements Bar { - set #setter(v: number) {} + override method() {} } `, - options: [{ ignoreClassesThatImplementAnInterface: false }], - errors: [ + options: [ { - messageId: 'missingThis', + ignoreClassesThatImplementAnInterface: true, + ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - private set setter(v: number) {} + private override method() {} } `, options: [ @@ -668,19 +671,15 @@ class Foo implements Bar { // _interface_ cannot have `private`/`protected` modifier on members. // We should ignore only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - ignoreOverrideMethods: false, - }, - ], - errors: [ - { - messageId: 'missingThis', + // But overridden properties should be ignored. + ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - protected set setter(v: number) {} + protected override method() {} } `, options: [ @@ -688,105 +687,98 @@ class Foo implements Bar { // _interface_ cannot have `private`/`protected` modifier on members. // We should ignore only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - ignoreOverrideMethods: false, - }, - ], - errors: [ - { - messageId: 'missingThis', + // But overridden properties should be ignored. + ignoreOverrideMethods: true, }, ], }, { code: ` -class Foo { - override method() {} +class Foo implements Bar { + override get getter(): number {} } `, - options: [{ ignoreOverrideMethods: false }], - errors: [ + options: [ { - messageId: 'missingThis', + ignoreClassesThatImplementAnInterface: true, + ignoreOverrideMethods: true, }, ], }, { code: ` -class Foo { - override get getter(): number {} +class Foo implements Bar { + private override get getter(): number {} } `, - options: [{ ignoreOverrideMethods: false }], - errors: [ + options: [ { - messageId: 'missingThis', + // _interface_ cannot have `private`/`protected` modifier on members. + // We should ignore only public members. + ignoreClassesThatImplementAnInterface: 'public-fields', + // But overridden properties should be ignored. + ignoreOverrideMethods: true, }, ], }, { code: ` -class Foo { - override set setter(v: number) {} +class Foo implements Bar { + protected override get getter(): number {} } `, - options: [{ ignoreOverrideMethods: false }], - errors: [ + options: [ { - messageId: 'missingThis', + // _interface_ cannot have `private`/`protected` modifier on members. + // We should ignore only public members. + ignoreClassesThatImplementAnInterface: 'public-fields', + // But overridden properties should be ignored. + ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - override method() {} + override set setter(v: number) {} } `, options: [ { - ignoreClassesThatImplementAnInterface: false, - ignoreOverrideMethods: false, - }, - ], - errors: [ - { - messageId: 'missingThis', + ignoreClassesThatImplementAnInterface: true, + ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - override get getter(): number {} + private override set setter(v: number) {} } `, options: [ { - ignoreClassesThatImplementAnInterface: false, - ignoreOverrideMethods: false, - }, - ], - errors: [ - { - messageId: 'missingThis', + // _interface_ cannot have `private`/`protected` modifier on members. + // We should ignore only public members. + ignoreClassesThatImplementAnInterface: 'public-fields', + // But overridden properties should be ignored. + ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - override set setter(v: number) {} + protected override set setter(v: number) {} } `, options: [ { - ignoreClassesThatImplementAnInterface: false, - ignoreOverrideMethods: false, - }, - ], - errors: [ - { - messageId: 'missingThis', + // _interface_ cannot have `private`/`protected` modifier on members. + // We should ignore only public members. + ignoreClassesThatImplementAnInterface: 'public-fields', + // But overridden properties should be ignored. + ignoreOverrideMethods: true, }, ], }, @@ -796,92 +788,100 @@ class Foo implements Bar { property = () => {}; } `, - options: [{ ignoreClassesThatImplementAnInterface: false }], - errors: [ - { - messageId: 'missingThis', - }, - ], + options: [{ ignoreClassesThatImplementAnInterface: true }], }, { code: ` -class Foo implements Bar { - #property = () => {}; +class Foo { + override property = () => {}; } `, - options: [{ ignoreClassesThatImplementAnInterface: false }], - errors: [ - { - messageId: 'missingThis', - }, - ], + options: [{ ignoreOverrideMethods: true }], }, { code: ` class Foo { + private override property = () => {}; +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo { + protected override property = () => {}; +} + `, + options: [{ ignoreOverrideMethods: true }], + }, + { + code: ` +class Foo implements Bar { override property = () => {}; } `, - options: [{ ignoreOverrideMethods: false }], - errors: [ + options: [ { - messageId: 'missingThis', + ignoreClassesThatImplementAnInterface: true, + ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - override property = () => {}; + property = () => {}; } `, options: [ { + enforceForClassFields: false, ignoreClassesThatImplementAnInterface: false, - ignoreOverrideMethods: false, }, ], - errors: [ + }, + { + code: ` +class Foo { + override property = () => {}; +} + `, + options: [ { - messageId: 'missingThis', + enforceForClassFields: false, + ignoreOverrideMethods: false, }, ], }, { code: ` class Foo implements Bar { - private property = () => {}; + private override property = () => {}; } `, options: [ { // _interface_ cannot have `private`/`protected` modifier on members. - // We should ignore only public members. + // We should check only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - }, - ], - errors: [ - { - messageId: 'missingThis', + // But overridden properties should be ignored. + ignoreOverrideMethods: true, }, ], }, { code: ` class Foo implements Bar { - protected property = () => {}; + protected override property = () => {}; } `, options: [ { // _interface_ cannot have `private`/`protected` modifier on members. - // We should ignore only public members. + // We should check only public members. ignoreClassesThatImplementAnInterface: 'public-fields', - }, - ], - errors: [ - { - messageId: 'missingThis', + // But overridden properties should be ignored. + ignoreOverrideMethods: true, }, ], }, diff --git a/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts b/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts index f7f50ea44524..ec8d13719751 100644 --- a/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-generic-constructors.test.ts @@ -5,135 +5,6 @@ import rule from '../../src/rules/consistent-generic-constructors'; const ruleTester = new RuleTester(); ruleTester.run('consistent-generic-constructors', rule, { - valid: [ - // default: constructor - 'const a = new Foo();', - 'const a = new Foo();', - 'const a: Foo = new Foo();', - 'const a: Foo = new Foo();', - 'const a: Bar = new Foo();', - 'const a: Foo = new Foo();', - 'const a: Bar = new Foo();', - 'const a: Bar = new Foo();', - 'const a: Foo = Foo();', - 'const a: Foo = Foo();', - 'const a: Foo = Foo();', - ` -class Foo { - a = new Foo(); -} - `, - ` -function foo(a: Foo = new Foo()) {} - `, - ` -function foo({ a }: Foo = new Foo()) {} - `, - ` -function foo([a]: Foo = new Foo()) {} - `, - ` -class A { - constructor(a: Foo = new Foo()) {} -} - `, - ` -const a = function (a: Foo = new Foo()) {}; - `, - // type-annotation - { - code: 'const a = new Foo();', - options: ['type-annotation'], - }, - { - code: 'const a: Foo = new Foo();', - options: ['type-annotation'], - }, - { - code: 'const a: Foo = new Foo();', - options: ['type-annotation'], - }, - { - code: 'const a: Foo = new Foo();', - options: ['type-annotation'], - }, - { - code: 'const a: Bar = new Foo();', - options: ['type-annotation'], - }, - { - code: 'const a: Bar = new Foo();', - options: ['type-annotation'], - }, - { - code: 'const a: Foo = Foo();', - options: ['type-annotation'], - }, - { - code: 'const a: Foo = Foo();', - options: ['type-annotation'], - }, - { - code: 'const a: Foo = Foo();', - options: ['type-annotation'], - }, - { - code: 'const a = new (class C {})();', - options: ['type-annotation'], - }, - { - code: ` -class Foo { - a: Foo = new Foo(); -} - `, - options: ['type-annotation'], - }, - { - code: ` -function foo(a: Foo = new Foo()) {} - `, - options: ['type-annotation'], - }, - { - code: ` -function foo({ a }: Foo = new Foo()) {} - `, - options: ['type-annotation'], - }, - { - code: ` -function foo([a]: Foo = new Foo()) {} - `, - options: ['type-annotation'], - }, - { - code: ` -class A { - constructor(a: Foo = new Foo()) {} -} - `, - options: ['type-annotation'], - }, - { - code: ` -const a = function (a: Foo = new Foo()) {}; - `, - options: ['type-annotation'], - }, - { - code: ` -const [a = new Foo()] = []; - `, - options: ['type-annotation'], - }, - { - code: ` -function a([a = new Foo()]) {} - `, - options: ['type-annotation'], - }, - ], invalid: [ { code: 'const a: Foo = new Foo();', @@ -321,72 +192,72 @@ const a = function (a = new Foo()) {}; }, { code: 'const a = new Foo();', - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: 'const a: Foo = new Foo();', }, { code: 'const a = new Map();', - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: 'const a: Map = new Map();', }, { code: noFormat`const a = new Map ();`, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: `const a: Map = new Map ();`, }, { code: noFormat`const a = new Map< string, number >();`, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: `const a: Map< string, number > = new Map();`, }, { code: noFormat`const a = new \n Foo \n ();`, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: `const a: Foo = new \n Foo \n ();`, }, { code: 'const a = new Foo/* comment */ /* another */();', - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: `const a: Foo = new Foo/* comment */ /* another */();`, }, { code: 'const a = new Foo();', - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: `const a: Foo = new Foo();`, }, { @@ -395,12 +266,12 @@ class Foo { a = new Foo(); } `, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: ` class Foo { a: Foo = new Foo(); @@ -413,12 +284,12 @@ class Foo { [a] = new Foo(); } `, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: ` class Foo { [a]: Foo = new Foo(); @@ -431,12 +302,12 @@ class Foo { [a + b] = new Foo(); } `, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: ` class Foo { [a + b]: Foo = new Foo(); @@ -447,12 +318,12 @@ class Foo { code: ` function foo(a = new Foo()) {} `, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: ` function foo(a: Foo = new Foo()) {} `, @@ -461,12 +332,12 @@ function foo(a: Foo = new Foo()) {} code: ` function foo({ a } = new Foo()) {} `, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: ` function foo({ a }: Foo = new Foo()) {} `, @@ -475,12 +346,12 @@ function foo({ a }: Foo = new Foo()) {} code: ` function foo([a] = new Foo()) {} `, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: ` function foo([a]: Foo = new Foo()) {} `, @@ -491,12 +362,12 @@ class A { constructor(a = new Foo()) {} } `, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: ` class A { constructor(a: Foo = new Foo()) {} @@ -507,15 +378,144 @@ class A { code: ` const a = function (a = new Foo()) {}; `, - options: ['type-annotation'], errors: [ { messageId: 'preferTypeAnnotation', }, ], + options: ['type-annotation'], output: ` const a = function (a: Foo = new Foo()) {}; `, }, ], + valid: [ + // default: constructor + 'const a = new Foo();', + 'const a = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Bar = new Foo();', + 'const a: Foo = new Foo();', + 'const a: Bar = new Foo();', + 'const a: Bar = new Foo();', + 'const a: Foo = Foo();', + 'const a: Foo = Foo();', + 'const a: Foo = Foo();', + ` +class Foo { + a = new Foo(); +} + `, + ` +function foo(a: Foo = new Foo()) {} + `, + ` +function foo({ a }: Foo = new Foo()) {} + `, + ` +function foo([a]: Foo = new Foo()) {} + `, + ` +class A { + constructor(a: Foo = new Foo()) {} +} + `, + ` +const a = function (a: Foo = new Foo()) {}; + `, + // type-annotation + { + code: 'const a = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Bar = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Bar = new Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = Foo();', + options: ['type-annotation'], + }, + { + code: 'const a: Foo = Foo();', + options: ['type-annotation'], + }, + { + code: 'const a = new (class C {})();', + options: ['type-annotation'], + }, + { + code: ` +class Foo { + a: Foo = new Foo(); +} + `, + options: ['type-annotation'], + }, + { + code: ` +function foo(a: Foo = new Foo()) {} + `, + options: ['type-annotation'], + }, + { + code: ` +function foo({ a }: Foo = new Foo()) {} + `, + options: ['type-annotation'], + }, + { + code: ` +function foo([a]: Foo = new Foo()) {} + `, + options: ['type-annotation'], + }, + { + code: ` +class A { + constructor(a: Foo = new Foo()) {} +} + `, + options: ['type-annotation'], + }, + { + code: ` +const a = function (a: Foo = new Foo()) {}; + `, + options: ['type-annotation'], + }, + { + code: ` +const [a = new Foo()] = []; + `, + options: ['type-annotation'], + }, + { + code: ` +function a([a = new Foo()]) {} + `, + options: ['type-annotation'], + }, + ], }); diff --git a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts index 30f98f976e5c..cf0c5f8ba0fd 100644 --- a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts @@ -5,143 +5,6 @@ import rule from '../../src/rules/consistent-indexed-object-style'; const ruleTester = new RuleTester(); ruleTester.run('consistent-indexed-object-style', rule, { - valid: [ - // 'record' (default) - // Record - 'type Foo = Record;', - - // Interface - 'interface Foo {}', - ` -interface Foo { - bar: string; -} - `, - ` -interface Foo { - bar: string; - [key: string]: any; -} - `, - ` -interface Foo { - [key: string]: any; - bar: string; -} - `, - // circular - 'type Foo = { [key: string]: string | Foo };', - 'type Foo = { [key: string]: Foo };', - 'type Foo = { [key: string]: Foo } | Foo;', - ` -interface Foo { - [key: string]: Foo; -} - `, - ` -interface Foo { - [key: string]: Foo; -} - `, - ` -interface Foo { - [key: string]: Foo | string; -} - `, - // Type literal - 'type Foo = {};', - ` -type Foo = { - bar: string; - [key: string]: any; -}; - `, - ` -type Foo = { - bar: string; -}; - `, - ` -type Foo = { - [key: string]: any; - bar: string; -}; - `, - - // Generic - ` -type Foo = Generic<{ - [key: string]: any; - bar: string; -}>; - `, - - // Function types - 'function foo(arg: { [key: string]: any; bar: string }) {}', - 'function foo(): { [key: string]: any; bar: string } {}', - - // Invalid syntax allowed by the parser - 'type Foo = { [key: string] };', - 'type Foo = { [] };', - ` -interface Foo { - [key: string]; -} - `, - ` -interface Foo { - []; -} - `, - // 'index-signature' - // Unhandled type - { - code: 'type Foo = Misc;', - options: ['index-signature'], - }, - - // Invalid record - { - code: 'type Foo = Record;', - options: ['index-signature'], - }, - { - code: 'type Foo = Record;', - options: ['index-signature'], - }, - { - code: 'type Foo = Record;', - options: ['index-signature'], - }, - - // Type literal - { - code: 'type Foo = { [key: string]: any };', - options: ['index-signature'], - }, - - // Generic - { - code: 'type Foo = Generic<{ [key: string]: any }>;', - options: ['index-signature'], - }, - - // Function types - { - code: 'function foo(arg: { [key: string]: any }) {}', - options: ['index-signature'], - }, - { - code: 'function foo(): { [key: string]: any } {}', - options: ['index-signature'], - }, - - // Namespace - { - code: 'type T = A.B;', - options: ['index-signature'], - }, - ], invalid: [ // Interface { @@ -150,10 +13,10 @@ interface Foo { [key: string]: any; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Record; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, // Readonly interface @@ -163,10 +26,10 @@ interface Foo { readonly [key: string]: any; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Readonly>; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, // Interface with generic parameter @@ -176,10 +39,10 @@ interface Foo { [key: string]: A; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Record; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, // Interface with generic parameter and default value @@ -189,10 +52,10 @@ interface Foo { [key: string]: A; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Record; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, // Interface with extends @@ -202,8 +65,8 @@ interface B extends A { [index: number]: unknown; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: null, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, // Readonly interface with generic parameter { @@ -212,10 +75,10 @@ interface Foo { readonly [key: string]: A; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Readonly>; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, // Interface with multiple generic parameters @@ -225,10 +88,10 @@ interface Foo { [key: A]: B; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Record; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, // Readonly interface with multiple generic parameters @@ -238,101 +101,101 @@ interface Foo { readonly [key: A]: B; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Readonly>; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, // Type literal { code: 'type Foo = { [key: string]: any };', + errors: [{ column: 12, line: 1, messageId: 'preferRecord' }], output: 'type Foo = Record;', - errors: [{ messageId: 'preferRecord', line: 1, column: 12 }], }, // Readonly type literal { code: 'type Foo = { readonly [key: string]: any };', + errors: [{ column: 12, line: 1, messageId: 'preferRecord' }], output: 'type Foo = Readonly>;', - errors: [{ messageId: 'preferRecord', line: 1, column: 12 }], }, // Generic { code: 'type Foo = Generic<{ [key: string]: any }>;', + errors: [{ column: 20, line: 1, messageId: 'preferRecord' }], output: 'type Foo = Generic>;', - errors: [{ messageId: 'preferRecord', line: 1, column: 20 }], }, // Readonly Generic { code: 'type Foo = Generic<{ readonly [key: string]: any }>;', + errors: [{ column: 20, line: 1, messageId: 'preferRecord' }], output: 'type Foo = Generic>>;', - errors: [{ messageId: 'preferRecord', line: 1, column: 20 }], }, // Function types { code: 'function foo(arg: { [key: string]: any }) {}', + errors: [{ column: 19, line: 1, messageId: 'preferRecord' }], output: 'function foo(arg: Record) {}', - errors: [{ messageId: 'preferRecord', line: 1, column: 19 }], }, { code: 'function foo(): { [key: string]: any } {}', + errors: [{ column: 17, line: 1, messageId: 'preferRecord' }], output: 'function foo(): Record {}', - errors: [{ messageId: 'preferRecord', line: 1, column: 17 }], }, // Readonly function types { code: 'function foo(arg: { readonly [key: string]: any }) {}', + errors: [{ column: 19, line: 1, messageId: 'preferRecord' }], output: 'function foo(arg: Readonly>) {}', - errors: [{ messageId: 'preferRecord', line: 1, column: 19 }], }, { code: 'function foo(): { readonly [key: string]: any } {}', + errors: [{ column: 17, line: 1, messageId: 'preferRecord' }], output: 'function foo(): Readonly> {}', - errors: [{ messageId: 'preferRecord', line: 1, column: 17 }], }, // Never // Type literal { code: 'type Foo = Record;', + errors: [{ column: 12, line: 1, messageId: 'preferIndexSignature' }], options: ['index-signature'], output: 'type Foo = { [key: string]: any };', - errors: [{ messageId: 'preferIndexSignature', line: 1, column: 12 }], }, // Type literal with generic parameter { code: 'type Foo = Record;', + errors: [{ column: 15, line: 1, messageId: 'preferIndexSignature' }], options: ['index-signature'], output: 'type Foo = { [key: string]: T };', - errors: [{ messageId: 'preferIndexSignature', line: 1, column: 15 }], }, // Circular { code: 'type Foo = { [k: string]: A.Foo };', + errors: [{ column: 12, line: 1, messageId: 'preferRecord' }], output: 'type Foo = Record;', - errors: [{ messageId: 'preferRecord', line: 1, column: 12 }], }, { code: 'type Foo = { [key: string]: AnotherFoo };', + errors: [{ column: 12, line: 1, messageId: 'preferRecord' }], output: 'type Foo = Record;', - errors: [{ messageId: 'preferRecord', line: 1, column: 12 }], }, { code: 'type Foo = { [key: string]: { [key: string]: Foo } };', + errors: [{ column: 29, line: 1, messageId: 'preferRecord' }], output: 'type Foo = { [key: string]: Record };', - errors: [{ messageId: 'preferRecord', line: 1, column: 29 }], }, { code: 'type Foo = { [key: string]: string } | Foo;', + errors: [{ column: 12, line: 1, messageId: 'preferRecord' }], output: 'type Foo = Record | Foo;', - errors: [{ messageId: 'preferRecord', line: 1, column: 12 }], }, { code: ` @@ -340,10 +203,10 @@ interface Foo { [k: string]: T; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Record; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, { code: ` @@ -351,10 +214,10 @@ interface Foo { [k: string]: A.Foo; } `, + errors: [{ column: 1, line: 2, messageId: 'preferRecord' }], output: ` type Foo = Record; `, - errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, { code: ` @@ -362,34 +225,171 @@ interface Foo { [k: string]: { [key: string]: Foo }; } `, + errors: [{ column: 16, line: 3, messageId: 'preferRecord' }], output: ` interface Foo { [k: string]: Record; } `, - errors: [{ messageId: 'preferRecord', line: 3, column: 16 }], }, // Generic { code: 'type Foo = Generic>;', + errors: [{ column: 20, line: 1, messageId: 'preferIndexSignature' }], options: ['index-signature'], output: 'type Foo = Generic<{ [key: string]: any }>;', - errors: [{ messageId: 'preferIndexSignature', line: 1, column: 20 }], }, // Function types { code: 'function foo(arg: Record) {}', + errors: [{ column: 19, line: 1, messageId: 'preferIndexSignature' }], options: ['index-signature'], output: 'function foo(arg: { [key: string]: any }) {}', - errors: [{ messageId: 'preferIndexSignature', line: 1, column: 19 }], }, { code: 'function foo(): Record {}', + errors: [{ column: 17, line: 1, messageId: 'preferIndexSignature' }], options: ['index-signature'], output: 'function foo(): { [key: string]: any } {}', - errors: [{ messageId: 'preferIndexSignature', line: 1, column: 17 }], + }, + ], + valid: [ + // 'record' (default) + // Record + 'type Foo = Record;', + + // Interface + 'interface Foo {}', + ` +interface Foo { + bar: string; +} + `, + ` +interface Foo { + bar: string; + [key: string]: any; +} + `, + ` +interface Foo { + [key: string]: any; + bar: string; +} + `, + // circular + 'type Foo = { [key: string]: string | Foo };', + 'type Foo = { [key: string]: Foo };', + 'type Foo = { [key: string]: Foo } | Foo;', + ` +interface Foo { + [key: string]: Foo; +} + `, + ` +interface Foo { + [key: string]: Foo; +} + `, + ` +interface Foo { + [key: string]: Foo | string; +} + `, + // Type literal + 'type Foo = {};', + ` +type Foo = { + bar: string; + [key: string]: any; +}; + `, + ` +type Foo = { + bar: string; +}; + `, + ` +type Foo = { + [key: string]: any; + bar: string; +}; + `, + + // Generic + ` +type Foo = Generic<{ + [key: string]: any; + bar: string; +}>; + `, + + // Function types + 'function foo(arg: { [key: string]: any; bar: string }) {}', + 'function foo(): { [key: string]: any; bar: string } {}', + + // Invalid syntax allowed by the parser + 'type Foo = { [key: string] };', + 'type Foo = { [] };', + ` +interface Foo { + [key: string]; +} + `, + ` +interface Foo { + []; +} + `, + // 'index-signature' + // Unhandled type + { + code: 'type Foo = Misc;', + options: ['index-signature'], + }, + + // Invalid record + { + code: 'type Foo = Record;', + options: ['index-signature'], + }, + { + code: 'type Foo = Record;', + options: ['index-signature'], + }, + { + code: 'type Foo = Record;', + options: ['index-signature'], + }, + + // Type literal + { + code: 'type Foo = { [key: string]: any };', + options: ['index-signature'], + }, + + // Generic + { + code: 'type Foo = Generic<{ [key: string]: any }>;', + options: ['index-signature'], + }, + + // Function types + { + code: 'function foo(arg: { [key: string]: any }) {}', + options: ['index-signature'], + }, + { + code: 'function foo(): { [key: string]: any } {}', + options: ['index-signature'], + }, + + // Namespace + { + code: 'type T = A.B;', + options: ['index-signature'], }, ], }); diff --git a/packages/eslint-plugin/tests/rules/consistent-return.test.ts b/packages/eslint-plugin/tests/rules/consistent-return.test.ts index b26b70654dee..ce807a71175c 100644 --- a/packages/eslint-plugin/tests/rules/consistent-return.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-return.test.ts @@ -8,13 +8,257 @@ const rootDir = getFixturesRootDir(); const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - tsconfigRootDir: rootDir, project: './tsconfig.json', + tsconfigRootDir: rootDir, }, }, }); ruleTester.run('consistent-return', rule, { + invalid: [ + { + code: ` + function foo(flag: boolean): any { + if (flag) return true; + else return; + } + `, + errors: [ + { + column: 16, + data: { name: "Function 'foo'" }, + endColumn: 23, + endLine: 4, + line: 4, + messageId: 'missingReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + function bar(): undefined {} + function foo(flag: boolean): undefined { + if (flag) return bar(); + return; + } + `, + errors: [ + { + column: 11, + data: { name: "Function 'foo'" }, + endColumn: 18, + endLine: 5, + line: 5, + messageId: 'missingReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + declare function foo(): void; + function bar(flag: boolean): undefined { + function baz(): undefined { + if (flag) return; + return undefined; + } + if (flag) return baz(); + return; + } + `, + errors: [ + { + column: 13, + data: { name: "Function 'baz'" }, + endColumn: 30, + endLine: 6, + line: 6, + messageId: 'unexpectedReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + { + column: 11, + data: { name: "Function 'bar'" }, + endColumn: 18, + endLine: 9, + line: 9, + messageId: 'missingReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + function foo(flag: boolean): Promise { + if (flag) return Promise.resolve(void 0); + else return; + } + `, + errors: [ + { + column: 16, + data: { name: "Function 'foo'" }, + endColumn: 23, + endLine: 4, + line: 4, + messageId: 'missingReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + async function foo(flag: boolean): Promise { + if (flag) return; + else return 'value'; + } + `, + errors: [ + { + column: 16, + data: { name: "Async function 'foo'" }, + endColumn: 31, + endLine: 4, + line: 4, + messageId: 'unexpectedReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + async function foo(flag: boolean): Promise { + if (flag) return 'value'; + else return; + } + `, + errors: [ + { + column: 16, + data: { name: "Async function 'foo'" }, + endColumn: 23, + endLine: 4, + line: 4, + messageId: 'missingReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + async function foo(flag: boolean) { + if (flag) return; + return 1; + } + `, + errors: [ + { + column: 11, + data: { name: "Async function 'foo'" }, + endColumn: 20, + endLine: 4, + line: 4, + messageId: 'unexpectedReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + function foo(flag: boolean): Promise { + if (flag) return; + else return 'value'; + } + `, + errors: [ + { + column: 16, + data: { name: "Function 'foo'" }, + endColumn: 31, + endLine: 4, + line: 4, + messageId: 'unexpectedReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + declare function bar(): Promise; + function foo(flag?: boolean): Promise { + if (flag) { + return bar(); + } + return; + } + `, + errors: [ + { + column: 11, + data: { name: "Function 'foo'" }, + endColumn: 18, + endLine: 7, + line: 7, + messageId: 'missingReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + }, + { + code: ` + function foo(flag: boolean): undefined | boolean { + if (flag) { + return undefined; + } + return true; + } + `, + errors: [ + { + column: 11, + data: { name: "Function 'foo'" }, + endColumn: 23, + endLine: 6, + line: 6, + messageId: 'unexpectedReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + options: [ + { + treatUndefinedAsUnspecified: true, + }, + ], + }, + { + code: ` + declare const undefOrNum: undefined | number; + function foo(flag: boolean) { + if (flag) { + return; + } + return undefOrNum; + } + `, + errors: [ + { + column: 11, + data: { name: "Function 'foo'" }, + endColumn: 29, + endLine: 7, + line: 7, + messageId: 'unexpectedReturnValue', + type: AST_NODE_TYPES.ReturnStatement, + }, + ], + options: [ + { + treatUndefinedAsUnspecified: true, + }, + ], + }, + ], valid: [ // base rule ` @@ -210,248 +454,4 @@ ruleTester.run('consistent-return', rule, { ], }, ], - invalid: [ - { - code: ` - function foo(flag: boolean): any { - if (flag) return true; - else return; - } - `, - errors: [ - { - messageId: 'missingReturnValue', - data: { name: "Function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 4, - column: 16, - endLine: 4, - endColumn: 23, - }, - ], - }, - { - code: ` - function bar(): undefined {} - function foo(flag: boolean): undefined { - if (flag) return bar(); - return; - } - `, - errors: [ - { - messageId: 'missingReturnValue', - data: { name: "Function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 5, - column: 11, - endLine: 5, - endColumn: 18, - }, - ], - }, - { - code: ` - declare function foo(): void; - function bar(flag: boolean): undefined { - function baz(): undefined { - if (flag) return; - return undefined; - } - if (flag) return baz(); - return; - } - `, - errors: [ - { - messageId: 'unexpectedReturnValue', - data: { name: "Function 'baz'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 6, - column: 13, - endLine: 6, - endColumn: 30, - }, - { - messageId: 'missingReturnValue', - data: { name: "Function 'bar'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 9, - column: 11, - endLine: 9, - endColumn: 18, - }, - ], - }, - { - code: ` - function foo(flag: boolean): Promise { - if (flag) return Promise.resolve(void 0); - else return; - } - `, - errors: [ - { - messageId: 'missingReturnValue', - data: { name: "Function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 4, - column: 16, - endLine: 4, - endColumn: 23, - }, - ], - }, - { - code: ` - async function foo(flag: boolean): Promise { - if (flag) return; - else return 'value'; - } - `, - errors: [ - { - messageId: 'unexpectedReturnValue', - data: { name: "Async function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 4, - column: 16, - endLine: 4, - endColumn: 31, - }, - ], - }, - { - code: ` - async function foo(flag: boolean): Promise { - if (flag) return 'value'; - else return; - } - `, - errors: [ - { - messageId: 'missingReturnValue', - data: { name: "Async function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 4, - column: 16, - endLine: 4, - endColumn: 23, - }, - ], - }, - { - code: ` - async function foo(flag: boolean) { - if (flag) return; - return 1; - } - `, - errors: [ - { - messageId: 'unexpectedReturnValue', - data: { name: "Async function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 4, - column: 11, - endLine: 4, - endColumn: 20, - }, - ], - }, - { - code: ` - function foo(flag: boolean): Promise { - if (flag) return; - else return 'value'; - } - `, - errors: [ - { - messageId: 'unexpectedReturnValue', - data: { name: "Function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 4, - column: 16, - endLine: 4, - endColumn: 31, - }, - ], - }, - { - code: ` - declare function bar(): Promise; - function foo(flag?: boolean): Promise { - if (flag) { - return bar(); - } - return; - } - `, - errors: [ - { - messageId: 'missingReturnValue', - data: { name: "Function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 7, - column: 11, - endLine: 7, - endColumn: 18, - }, - ], - }, - { - code: ` - function foo(flag: boolean): undefined | boolean { - if (flag) { - return undefined; - } - return true; - } - `, - options: [ - { - treatUndefinedAsUnspecified: true, - }, - ], - errors: [ - { - messageId: 'unexpectedReturnValue', - data: { name: "Function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 6, - column: 11, - endLine: 6, - endColumn: 23, - }, - ], - }, - { - code: ` - declare const undefOrNum: undefined | number; - function foo(flag: boolean) { - if (flag) { - return; - } - return undefOrNum; - } - `, - options: [ - { - treatUndefinedAsUnspecified: true, - }, - ], - errors: [ - { - messageId: 'unexpectedReturnValue', - data: { name: "Function 'foo'" }, - type: AST_NODE_TYPES.ReturnStatement, - line: 7, - column: 11, - endLine: 7, - endColumn: 29, - }, - ], - }, - ], }); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts index 29eb3d5297a0..435030e82982 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -6,6 +6,7 @@ import type { MessageIds, Options, } from '../../src/rules/consistent-type-assertions'; + import rule from '../../src/rules/consistent-type-assertions'; import { dedupeTestCases } from '../dedupeTestCases'; import { batchedSingleLineTests } from '../RuleTester'; @@ -83,68 +84,6 @@ print\`\${{ bar: 5 }}\` `; ruleTester.run('consistent-type-assertions', rule, { - valid: [ - ...dedupeTestCases( - batchedSingleLineTests({ - code: AS_TESTS, - options: [ - { assertionStyle: 'as', objectLiteralTypeAssertions: 'allow' }, - ], - }), - ), - ...batchedSingleLineTests({ - code: ANGLE_BRACKET_TESTS, - options: [ - { - assertionStyle: 'angle-bracket', - objectLiteralTypeAssertions: 'allow', - }, - ], - }), - ...batchedSingleLineTests({ - code: `${OBJECT_LITERAL_AS_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_AS_CASTS}`, - options: [{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow' }], - }), - ...batchedSingleLineTests({ - code: `${OBJECT_LITERAL_ANGLE_BRACKET_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS}`, - options: [ - { - assertionStyle: 'angle-bracket', - objectLiteralTypeAssertions: 'allow', - }, - ], - }), - ...batchedSingleLineTests({ - code: OBJECT_LITERAL_ARGUMENT_AS_CASTS, - options: [ - { - assertionStyle: 'as', - objectLiteralTypeAssertions: 'allow-as-parameter', - }, - ], - }), - ...batchedSingleLineTests({ - code: OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS, - options: [ - { - assertionStyle: 'angle-bracket', - objectLiteralTypeAssertions: 'allow-as-parameter', - }, - ], - }), - { code: 'const x = [1];', options: [{ assertionStyle: 'never' }] }, - { code: 'const x = [1] as const;', options: [{ assertionStyle: 'never' }] }, - { - code: 'const bar = ;', - languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } }, - options: [ - { - assertionStyle: 'as', - objectLiteralTypeAssertions: 'allow-as-parameter', - }, - ], - }, - ], invalid: [ ...dedupeTestCases( ( @@ -157,445 +96,445 @@ ruleTester.run('consistent-type-assertions', rule, { ).flatMap(([assertionStyle, code, output]) => batchedSingleLineTests({ code, - options: [{ assertionStyle }], errors: code .split(`\n`) - .map((_, i) => ({ messageId: assertionStyle, line: i + 1 })), + .map((_, i) => ({ line: i + 1, messageId: assertionStyle })), + options: [{ assertionStyle }], output, }), ), ), ...batchedSingleLineTests({ code: OBJECT_LITERAL_AS_CASTS, - options: [ - { - assertionStyle: 'as', - objectLiteralTypeAssertions: 'allow-as-parameter', - }, - ], errors: [ { - messageId: 'unexpectedObjectTypeAssertion', line: 2, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithAnnotation', output: 'const x: Foo = {};', }, { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = {} satisfies Foo;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 3, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: 'a | b' }, + messageId: 'replaceObjectTypeAssertionWithAnnotation', output: 'const x: a | b = ({});', }, { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'a | b' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = ({}) satisfies a | b;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 4, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'A' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = {} satisfies A + b;', }, ], }, ], - }), - ...batchedSingleLineTests({ - code: OBJECT_LITERAL_ANGLE_BRACKET_CASTS, options: [ { - assertionStyle: 'angle-bracket', + assertionStyle: 'as', objectLiteralTypeAssertions: 'allow-as-parameter', }, ], + }), + ...batchedSingleLineTests({ + code: OBJECT_LITERAL_ANGLE_BRACKET_CASTS, errors: [ { - messageId: 'unexpectedObjectTypeAssertion', line: 2, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithAnnotation', output: 'const x: Foo = {};', }, { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = {} satisfies Foo;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 3, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: 'a | b' }, + messageId: 'replaceObjectTypeAssertionWithAnnotation', output: 'const x: a | b = ({});', }, { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'a | b' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = ({}) satisfies a | b;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 4, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'A' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = {} satisfies A + b;', }, ], }, ], + options: [ + { + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'allow-as-parameter', + }, + ], }), ...batchedSingleLineTests({ code: `${OBJECT_LITERAL_AS_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_AS_CASTS}`, - options: [{ assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }], errors: [ { - messageId: 'unexpectedObjectTypeAssertion', line: 2, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithAnnotation', output: 'const x: Foo = {};', }, { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = {} satisfies Foo;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 3, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: 'a | b' }, + messageId: 'replaceObjectTypeAssertionWithAnnotation', output: 'const x: a | b = ({});', }, { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'a | b' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = ({}) satisfies a | b;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 4, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'A' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = {} satisfies A + b;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 5, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'print({ bar: 5 } satisfies Foo)', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 6, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'new print({ bar: 5 } satisfies Foo)', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 7, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'function foo() { throw { bar: 5 } satisfies Foo }', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 8, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo.Bar' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'function b(x = {} satisfies Foo.Bar) {}', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 9, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'function c(x = {} satisfies Foo) {}', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 10, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'print?.({ bar: 5 } satisfies Foo)', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 11, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'print?.call({ bar: 5 } satisfies Foo)', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 12, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: `print\`\${{ bar: 5 } satisfies Foo}\``, }, ], }, ], + options: [{ assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }], }), ...batchedSingleLineTests({ code: `${OBJECT_LITERAL_ANGLE_BRACKET_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS}`, - options: [ - { - assertionStyle: 'angle-bracket', - objectLiteralTypeAssertions: 'never', - }, - ], errors: [ { - messageId: 'unexpectedObjectTypeAssertion', line: 2, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithAnnotation', output: 'const x: Foo = {};', }, { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = {} satisfies Foo;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 3, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithAnnotation', data: { cast: 'a | b' }, + messageId: 'replaceObjectTypeAssertionWithAnnotation', output: 'const x: a | b = ({});', }, { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'a | b' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = ({}) satisfies a | b;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 4, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'A' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'const x = {} satisfies A + b;', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 5, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'print({ bar: 5 } satisfies Foo)', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 6, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'new print({ bar: 5 } satisfies Foo)', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 7, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'function foo() { throw { bar: 5 } satisfies Foo }', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 8, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'print?.({ bar: 5 } satisfies Foo)', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 9, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: 'print?.call({ bar: 5 } satisfies Foo)', }, ], }, { - messageId: 'unexpectedObjectTypeAssertion', line: 10, + messageId: 'unexpectedObjectTypeAssertion', suggestions: [ { - messageId: 'replaceObjectTypeAssertionWithSatisfies', data: { cast: 'Foo' }, + messageId: 'replaceObjectTypeAssertionWithSatisfies', output: `print\`\${{ bar: 5 } satisfies Foo}\``, }, ], }, ], + options: [ + { + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'never', + }, + ], }), { code: 'const foo = ;', - output: null, + errors: [{ line: 1, messageId: 'never' }], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } }, options: [{ assertionStyle: 'never' }], - errors: [{ messageId: 'never', line: 1 }], + output: null, }, { code: 'const a = (b, c);', - output: `const a = (b, c) as any;`, - options: [ - { - assertionStyle: 'as', - }, - ], errors: [ { - messageId: 'as', line: 1, + messageId: 'as', }, ], - }, - { - code: 'const f = (() => {});', - output: 'const f = (() => {}) as any;', options: [ { assertionStyle: 'as', }, ], + output: `const a = (b, c) as any;`, + }, + { + code: 'const f = (() => {});', errors: [ { - messageId: 'as', line: 1, + messageId: 'as', }, ], - }, - { - code: 'const f = function () {};', - output: 'const f = function () {} as any;', options: [ { assertionStyle: 'as', }, ], + output: 'const f = (() => {}) as any;', + }, + { + code: 'const f = function () {};', errors: [ { - messageId: 'as', line: 1, + messageId: 'as', }, ], - }, - { - code: 'const f = (async () => {});', - output: 'const f = (async () => {}) as any;', options: [ { assertionStyle: 'as', }, ], + output: 'const f = function () {} as any;', + }, + { + code: 'const f = (async () => {});', errors: [ { - messageId: 'as', line: 1, + messageId: 'as', }, ], + options: [ + { + assertionStyle: 'as', + }, + ], + output: 'const f = (async () => {}) as any;', }, { // prettier wants to remove the parens around the yield expression, @@ -605,56 +544,118 @@ function* g() { const y = (yield a); } `, - output: ` -function* g() { - const y = (yield a) as any; -} - `, - options: [ + errors: [ { - assertionStyle: 'as', + line: 3, + messageId: 'as', }, ], - errors: [ + options: [ { - messageId: 'as', - line: 3, + assertionStyle: 'as', }, ], + output: ` +function* g() { + const y = (yield a) as any; +} + `, }, { code: ` declare let x: number, y: number; const bs = (x <<= y); `, + errors: [ + { + line: 3, + messageId: 'as', + }, + ], + options: [ + { + assertionStyle: 'as', + }, + ], output: ` declare let x: number, y: number; const bs = (x <<= y) as any; `, + }, + { + code: 'const ternary = (true ? x : y);', + errors: [ + { + line: 1, + messageId: 'as', + }, + ], options: [ { assertionStyle: 'as', }, ], - errors: [ + output: 'const ternary = (true ? x : y) as any;', + }, + ], + valid: [ + ...dedupeTestCases( + batchedSingleLineTests({ + code: AS_TESTS, + options: [ + { assertionStyle: 'as', objectLiteralTypeAssertions: 'allow' }, + ], + }), + ), + ...batchedSingleLineTests({ + code: ANGLE_BRACKET_TESTS, + options: [ { - messageId: 'as', - line: 3, + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'allow', }, ], - }, - { - code: 'const ternary = (true ? x : y);', - output: 'const ternary = (true ? x : y) as any;', + }), + ...batchedSingleLineTests({ + code: `${OBJECT_LITERAL_AS_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_AS_CASTS}`, + options: [{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow' }], + }), + ...batchedSingleLineTests({ + code: `${OBJECT_LITERAL_ANGLE_BRACKET_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS}`, + options: [ + { + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'allow', + }, + ], + }), + ...batchedSingleLineTests({ + code: OBJECT_LITERAL_ARGUMENT_AS_CASTS, options: [ { assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow-as-parameter', }, ], - errors: [ + }), + ...batchedSingleLineTests({ + code: OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS, + options: [ { - messageId: 'as', - line: 1, + assertionStyle: 'angle-bracket', + objectLiteralTypeAssertions: 'allow-as-parameter', + }, + ], + }), + { code: 'const x = [1];', options: [{ assertionStyle: 'never' }] }, + { code: 'const x = [1] as const;', options: [{ assertionStyle: 'never' }] }, + { + code: 'const bar = ;', + languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } }, + options: [ + { + assertionStyle: 'as', + objectLiteralTypeAssertions: 'allow-as-parameter', }, ], }, diff --git a/packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts index 4fa17b04e988..1fbbf9cb7ba6 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-definitions.test.ts @@ -5,96 +5,42 @@ import rule from '../../src/rules/consistent-type-definitions'; const ruleTester = new RuleTester(); ruleTester.run('consistent-type-definitions', rule, { - valid: [ - { - code: 'var foo = {};', - options: ['interface'], - }, - { - code: 'interface A {}', - options: ['interface'], - }, - { - code: ` -interface A extends B { - x: number; -} - `, - options: ['interface'], - }, - { - code: 'type U = string;', - options: ['interface'], - }, - { - code: 'type V = { x: number } | { y: string };', - options: ['interface'], - }, - { - code: ` -type Record = { - [K in T]: U; -}; - `, - options: ['interface'], - }, - { - code: 'type T = { x: number };', - options: ['type'], - }, - { - code: 'type A = { x: number } & B & C;', - options: ['type'], - }, - { - code: 'type A = { x: number } & B & C;', - options: ['type'], - }, - { - code: ` -export type W = { - x: T; -}; - `, - options: ['type'], - }, - ], invalid: [ { code: noFormat`type T = { x: number; };`, - output: `interface T { x: number; }`, - options: ['interface'], errors: [ { - messageId: 'interfaceOverType', - line: 1, column: 6, + line: 1, + messageId: 'interfaceOverType', }, ], + options: ['interface'], + output: `interface T { x: number; }`, }, { code: noFormat`type T={ x: number; };`, - output: `interface T { x: number; }`, - options: ['interface'], errors: [ { - messageId: 'interfaceOverType', - line: 1, column: 6, + line: 1, + messageId: 'interfaceOverType', }, ], + options: ['interface'], + output: `interface T { x: number; }`, }, { code: noFormat`type T= { x: number; };`, - output: `interface T { x: number; }`, - options: ['interface'], errors: [ { - messageId: 'interfaceOverType', - line: 1, column: 6, + line: 1, + messageId: 'interfaceOverType', }, ], + options: ['interface'], + output: `interface T { x: number; }`, }, { code: ` @@ -102,79 +48,79 @@ export type W = { x: T; }; `, - output: ` -export interface W { - x: T; -} - `, - options: ['interface'], errors: [ { - messageId: 'interfaceOverType', - line: 2, column: 13, + line: 2, + messageId: 'interfaceOverType', }, ], + options: ['interface'], + output: ` +export interface W { + x: T; +} + `, }, { code: noFormat`interface T { x: number; }`, - output: `type T = { x: number; }`, - options: ['type'], errors: [ { - messageId: 'typeOverInterface', - line: 1, column: 11, + line: 1, + messageId: 'typeOverInterface', }, ], + options: ['type'], + output: `type T = { x: number; }`, }, { code: noFormat`interface T{ x: number; }`, - output: `type T = { x: number; }`, - options: ['type'], errors: [ { - messageId: 'typeOverInterface', - line: 1, column: 11, + line: 1, + messageId: 'typeOverInterface', }, ], + options: ['type'], + output: `type T = { x: number; }`, }, { code: noFormat`interface T { x: number; }`, - output: `type T = { x: number; }`, - options: ['type'], errors: [ { - messageId: 'typeOverInterface', - line: 1, column: 11, + line: 1, + messageId: 'typeOverInterface', }, ], + options: ['type'], + output: `type T = { x: number; }`, }, { code: noFormat`interface A extends B, C { x: number; };`, - output: `type A = { x: number; } & B & C;`, - options: ['type'], errors: [ { - messageId: 'typeOverInterface', - line: 1, column: 11, + line: 1, + messageId: 'typeOverInterface', }, ], + options: ['type'], + output: `type A = { x: number; } & B & C;`, }, { code: noFormat`interface A extends B, C { x: number; };`, - output: `type A = { x: number; } & B & C;`, - options: ['type'], errors: [ { - messageId: 'typeOverInterface', - line: 1, column: 11, + line: 1, + messageId: 'typeOverInterface', }, ], + options: ['type'], + output: `type A = { x: number; } & B & C;`, }, { code: ` @@ -182,19 +128,19 @@ export interface W { x: T; } `, - output: ` -export type W = { - x: T; -} - `, - options: ['type'], errors: [ { - messageId: 'typeOverInterface', - line: 2, column: 18, + line: 2, + messageId: 'typeOverInterface', }, ], + options: ['type'], + output: ` +export type W = { + x: T; +} + `, }, { code: ` @@ -204,6 +150,14 @@ namespace JSX { } } `, + errors: [ + { + column: 13, + line: 3, + messageId: 'typeOverInterface', + }, + ], + options: ['type'], output: ` namespace JSX { type Array = { @@ -211,14 +165,6 @@ namespace JSX { } } `, - options: ['type'], - errors: [ - { - messageId: 'typeOverInterface', - line: 3, - column: 13, - }, - ], }, { code: ` @@ -228,6 +174,14 @@ global { } } `, + errors: [ + { + column: 13, + line: 3, + messageId: 'typeOverInterface', + }, + ], + options: ['type'], output: ` global { type Array = { @@ -235,14 +189,6 @@ global { } } `, - options: ['type'], - errors: [ - { - messageId: 'typeOverInterface', - line: 3, - column: 13, - }, - ], }, { code: ` @@ -252,15 +198,15 @@ declare global { } } `, - output: null, - options: ['type'], errors: [ { - messageId: 'typeOverInterface', - line: 3, column: 13, + line: 3, + messageId: 'typeOverInterface', }, ], + options: ['type'], + output: null, }, { code: ` @@ -270,15 +216,15 @@ declare global { } } `, - output: null, - options: ['type'], errors: [ { - messageId: 'typeOverInterface', - line: 4, column: 15, + line: 4, + messageId: 'typeOverInterface', }, ], + options: ['type'], + output: null, }, { // https://github.com/typescript-eslint/typescript-eslint/issues/3894 @@ -288,6 +234,14 @@ export default interface Test { foo(): number; } `, + errors: [ + { + column: 26, + line: 2, + messageId: 'typeOverInterface', + }, + ], + options: ['type'], output: ` type Test = { bar(): string; @@ -295,14 +249,6 @@ type Test = { } export default Test `, - options: ['type'], - errors: [ - { - messageId: 'typeOverInterface', - line: 2, - column: 26, - }, - ], }, { // https://github.com/typescript-eslint/typescript-eslint/issues/4333 @@ -312,20 +258,20 @@ export declare type Test = { bar: string; }; `, + errors: [ + { + column: 21, + line: 2, + messageId: 'interfaceOverType', + }, + ], + options: ['interface'], output: ` export declare interface Test { foo: string; bar: string; } `, - options: ['interface'], - errors: [ - { - messageId: 'interfaceOverType', - line: 2, - column: 21, - }, - ], }, { // https://github.com/typescript-eslint/typescript-eslint/issues/4333 @@ -335,20 +281,74 @@ export declare interface Test { bar: string; } `, + errors: [ + { + column: 26, + line: 2, + messageId: 'typeOverInterface', + }, + ], + options: ['type'], output: ` export declare type Test = { foo: string; bar: string; } `, + }, + ], + valid: [ + { + code: 'var foo = {};', + options: ['interface'], + }, + { + code: 'interface A {}', + options: ['interface'], + }, + { + code: ` +interface A extends B { + x: number; +} + `, + options: ['interface'], + }, + { + code: 'type U = string;', + options: ['interface'], + }, + { + code: 'type V = { x: number } | { y: string };', + options: ['interface'], + }, + { + code: ` +type Record = { + [K in T]: U; +}; + `, + options: ['interface'], + }, + { + code: 'type T = { x: number };', + options: ['type'], + }, + { + code: 'type A = { x: number } & B & C;', + options: ['type'], + }, + { + code: 'type A = { x: number } & B & C;', + options: ['type'], + }, + { + code: ` +export type W = { + x: T; +}; + `, options: ['type'], - errors: [ - { - messageId: 'typeOverInterface', - line: 2, - column: 26, - }, - ], }, ], }); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts index 209a285c7ee1..de0b57798c3f 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts @@ -8,135 +8,96 @@ const rootDir = getFixturesRootDir(); const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - tsconfigRootDir: rootDir, project: './tsconfig.json', + tsconfigRootDir: rootDir, }, }, }); ruleTester.run('consistent-type-exports', rule, { - valid: [ - // unknown module should be ignored - "export { Foo } from 'foo';", - - "export type { Type1 } from './consistent-type-exports';", - "export { value1 } from './consistent-type-exports';", - "export type { value1 } from './consistent-type-exports';", - ` -const variable = 1; -class Class {} -enum Enum {} -function Func() {} -namespace ValueNS { - export const x = 1; -} - -export { variable, Class, Enum, Func, ValueNS }; - `, - ` -type Alias = 1; -interface IFace {} -namespace TypeNS { - export type x = 1; -} - -export type { Alias, IFace, TypeNS }; - `, - ` -const foo = 1; -export type { foo }; - `, - ` -namespace NonTypeNS { - export const x = 1; -} - -export { NonTypeNS }; - `, - ], invalid: [ { code: "export { Type1 } from './consistent-type-exports';", - output: "export type { Type1 } from './consistent-type-exports';", errors: [ { - messageId: 'typeOverValue', - line: 1, column: 1, + line: 1, + messageId: 'typeOverValue', }, ], + output: "export type { Type1 } from './consistent-type-exports';", }, { code: "export { Type1, value1 } from './consistent-type-exports';", - output: - `export type { Type1 } from './consistent-type-exports';\n` + - `export { value1 } from './consistent-type-exports';`, errors: [ { - messageId: 'singleExportIsType', - line: 1, column: 1, + line: 1, + messageId: 'singleExportIsType', }, ], + output: + `export type { Type1 } from './consistent-type-exports';\n` + + `export { value1 } from './consistent-type-exports';`, }, { code: ` export { Type1, value1, value2 } from './consistent-type-exports'; - `, - output: ` -export type { Type1 } from './consistent-type-exports'; -export { value1, value2 } from './consistent-type-exports'; `, errors: [ { - messageId: 'singleExportIsType', - line: 2, column: 1, + line: 2, + messageId: 'singleExportIsType', }, ], + output: ` +export type { Type1 } from './consistent-type-exports'; +export { value1, value2 } from './consistent-type-exports'; + `, }, { code: ` export { Type1, value1, Type2, value2 } from './consistent-type-exports'; - `, - output: ` -export type { Type1, Type2 } from './consistent-type-exports'; -export { value1, value2 } from './consistent-type-exports'; `, errors: [ { - messageId: 'multipleExportsAreTypes', - line: 2, column: 1, + line: 2, + messageId: 'multipleExportsAreTypes', }, ], + output: ` +export type { Type1, Type2 } from './consistent-type-exports'; +export { value1, value2 } from './consistent-type-exports'; + `, }, { code: "export { Type2 as Foo } from './consistent-type-exports';", - output: "export type { Type2 as Foo } from './consistent-type-exports';", errors: [ { - messageId: 'typeOverValue', - line: 1, column: 1, + line: 1, + messageId: 'typeOverValue', }, ], + output: "export type { Type2 as Foo } from './consistent-type-exports';", }, { code: ` export { Type2 as Foo, value1 } from './consistent-type-exports'; - `, - output: ` -export type { Type2 as Foo } from './consistent-type-exports'; -export { value1 } from './consistent-type-exports'; `, errors: [ { - messageId: 'singleExportIsType', - line: 2, column: 1, + line: 2, + messageId: 'singleExportIsType', }, ], + output: ` +export type { Type2 as Foo } from './consistent-type-exports'; +export { value1 } from './consistent-type-exports'; + `, }, { code: ` @@ -145,53 +106,53 @@ export { value1 as BScope, value2 as CScope, } from './consistent-type-exports'; - `, - output: ` -export type { Type2 as Foo } from './consistent-type-exports'; -export { value1 as BScope, value2 as CScope } from './consistent-type-exports'; `, errors: [ { - messageId: 'singleExportIsType', - line: 2, column: 1, + line: 2, + messageId: 'singleExportIsType', }, ], + output: ` +export type { Type2 as Foo } from './consistent-type-exports'; +export { value1 as BScope, value2 as CScope } from './consistent-type-exports'; + `, }, { code: ` import { Type2 } from './consistent-type-exports'; export { Type2 }; - `, - output: ` -import { Type2 } from './consistent-type-exports'; -export type { Type2 }; `, errors: [ { - messageId: 'typeOverValue', - line: 3, column: 1, + line: 3, + messageId: 'typeOverValue', }, ], + output: ` +import { Type2 } from './consistent-type-exports'; +export type { Type2 }; + `, }, { code: ` import { value2, Type2 } from './consistent-type-exports'; export { value2, Type2 }; - `, - output: ` -import { value2, Type2 } from './consistent-type-exports'; -export type { Type2 }; -export { value2 }; `, errors: [ { - messageId: 'singleExportIsType', - line: 3, column: 1, + line: 3, + messageId: 'singleExportIsType', }, ], + output: ` +import { value2, Type2 } from './consistent-type-exports'; +export type { Type2 }; +export { value2 }; + `, }, { code: ` @@ -204,6 +165,13 @@ namespace TypeNS { export { Alias, IFace, TypeNS }; `, + errors: [ + { + column: 1, + line: 9, + messageId: 'multipleExportsAreTypes', + }, + ], output: ` type Alias = 1; interface IFace {} @@ -215,13 +183,6 @@ namespace TypeNS { export type { Alias, IFace }; export { TypeNS }; `, - errors: [ - { - messageId: 'multipleExportsAreTypes', - line: 9, - column: 1, - }, - ], }, { code: ` @@ -231,6 +192,13 @@ namespace TypeNS { export { TypeNS }; `, + errors: [ + { + column: 1, + line: 6, + messageId: 'typeOverValue', + }, + ], output: ` namespace TypeNS { export interface Foo {} @@ -238,47 +206,40 @@ namespace TypeNS { export type { TypeNS }; `, - errors: [ - { - messageId: 'typeOverValue', - line: 6, - column: 1, - }, - ], }, { code: ` type T = 1; export { type T, T }; - `, - output: ` -type T = 1; -export type { T, T }; `, errors: [ { - messageId: 'typeOverValue', - line: 3, column: 1, + line: 3, + messageId: 'typeOverValue', }, ], + output: ` +type T = 1; +export type { T, T }; + `, }, { code: noFormat` type T = 1; export { type/* */T, type /* */T, T }; - `, - output: ` -type T = 1; -export type { /* */T, /* */T, T }; `, errors: [ { - messageId: 'typeOverValue', - line: 3, column: 1, + line: 3, + messageId: 'typeOverValue', }, ], + output: ` +type T = 1; +export type { /* */T, /* */T, T }; + `, }, { code: ` @@ -286,19 +247,19 @@ type T = 1; const x = 1; export { type T, T, x }; `, + errors: [ + { + column: 1, + line: 4, + messageId: 'singleExportIsType', + }, + ], output: ` type T = 1; const x = 1; export type { T, T }; export { x }; `, - errors: [ - { - messageId: 'singleExportIsType', - line: 4, - column: 1, - }, - ], }, { code: ` @@ -306,37 +267,37 @@ type T = 1; const x = 1; export { T, x }; `, - output: ` -type T = 1; -const x = 1; -export { type T, x }; - `, - options: [{ fixMixedExportsWithInlineTypeSpecifier: true }], errors: [ { - messageId: 'singleExportIsType', - line: 4, column: 1, + line: 4, + messageId: 'singleExportIsType', }, ], + options: [{ fixMixedExportsWithInlineTypeSpecifier: true }], + output: ` +type T = 1; +const x = 1; +export { type T, x }; + `, }, { code: ` type T = 1; export { type T, T }; `, - output: ` -type T = 1; -export type { T, T }; - `, - options: [{ fixMixedExportsWithInlineTypeSpecifier: true }], errors: [ { - messageId: 'typeOverValue', - line: 3, column: 1, + line: 3, + messageId: 'typeOverValue', }, ], + options: [{ fixMixedExportsWithInlineTypeSpecifier: true }], + output: ` +type T = 1; +export type { T, T }; + `, }, { code: ` @@ -347,18 +308,18 @@ export { value2 as CScope, } from './consistent-type-exports'; `, - output: ` -export type { Type1, Type2 as Foo, value1 as BScope } from './consistent-type-exports'; -export { value2 as CScope } from './consistent-type-exports'; - `, - options: [{ fixMixedExportsWithInlineTypeSpecifier: false }], errors: [ { - messageId: 'multipleExportsAreTypes', - line: 2, column: 1, + line: 2, + messageId: 'multipleExportsAreTypes', }, ], + options: [{ fixMixedExportsWithInlineTypeSpecifier: false }], + output: ` +export type { Type1, Type2 as Foo, value1 as BScope } from './consistent-type-exports'; +export { value2 as CScope } from './consistent-type-exports'; + `, }, { code: ` @@ -369,6 +330,14 @@ export { value2 as CScope, } from './consistent-type-exports'; `, + errors: [ + { + column: 1, + line: 2, + messageId: 'multipleExportsAreTypes', + }, + ], + options: [{ fixMixedExportsWithInlineTypeSpecifier: true }], output: ` export { type Type1, @@ -377,14 +346,45 @@ export { value2 as CScope, } from './consistent-type-exports'; `, - options: [{ fixMixedExportsWithInlineTypeSpecifier: true }], - errors: [ - { - messageId: 'multipleExportsAreTypes', - line: 2, - column: 1, - }, - ], }, ], + valid: [ + // unknown module should be ignored + "export { Foo } from 'foo';", + + "export type { Type1 } from './consistent-type-exports';", + "export { value1 } from './consistent-type-exports';", + "export type { value1 } from './consistent-type-exports';", + ` +const variable = 1; +class Class {} +enum Enum {} +function Func() {} +namespace ValueNS { + export const x = 1; +} + +export { variable, Class, Enum, Func, ValueNS }; + `, + ` +type Alias = 1; +interface IFace {} +namespace TypeNS { + export type x = 1; +} + +export type { Alias, IFace, TypeNS }; + `, + ` +const foo = 1; +export type { foo }; + `, + ` +namespace NonTypeNS { + export const x = 1; +} + +export { NonTypeNS }; + `, + ], }); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts index 48d918a6436e..86c952435afe 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts @@ -4,16 +4,16 @@ import rule from '../../src/rules/consistent-type-imports'; const PARSER_OPTION_COMBOS = [ { - experimentalDecorators: false, emitDecoratorMetadata: false, + experimentalDecorators: false, }, { - experimentalDecorators: false, emitDecoratorMetadata: true, + experimentalDecorators: false, }, { - experimentalDecorators: true, emitDecoratorMetadata: false, + experimentalDecorators: true, }, ]; for (const parserOptions of PARSER_OPTION_COMBOS) { @@ -23,1926 +23,1926 @@ for (const parserOptions of PARSER_OPTION_COMBOS) { }); ruleTester.run('consistent-type-imports', rule, { - valid: [ - ` - import Foo from 'foo'; - const foo: Foo = new Foo(); - `, - ` - import foo from 'foo'; - const foo: foo.Foo = foo.fn(); - `, - ` - import { A, B } from 'foo'; - const foo: A = B(); - const bar = new A(); - `, - ` - import Foo from 'foo'; - `, - ` - import Foo from 'foo'; - type T = Foo; // shadowing - `, - ` - import Foo from 'foo'; - function fn() { - type Foo = {}; // shadowing - let foo: Foo; - } - `, - ` - import { A, B } from 'foo'; - const b = B; - `, - ` - import { A, B, C as c } from 'foo'; - const d = c; - `, - ` - import {} from 'foo'; // empty - `, + invalid: [ { code: ` -let foo: import('foo'); -let bar: import('foo').Bar; +import Foo from 'foo'; +let foo: Foo; +type Bar = Foo; +interface Baz { + foo: Foo; +} +function fn(a: Foo): Foo {} + `, + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + output: ` +import type Foo from 'foo'; +let foo: Foo; +type Bar = Foo; +interface Baz { + foo: Foo; +} +function fn(a: Foo): Foo {} `, - options: [{ disallowTypeAnnotations: false }], }, { code: ` import Foo from 'foo'; let foo: Foo; `, - options: [{ prefer: 'no-type-imports' }], - }, - // type queries - ` - import type Type from 'foo'; - - type T = typeof Type; - type T = typeof Type.foo; - `, - ` - import type { Type } from 'foo'; - - type T = typeof Type; - type T = typeof Type.foo; - `, - ` - import type * as Type from 'foo'; - - type T = typeof Type; - type T = typeof Type.foo; - `, - { - code: ` -import Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + options: [{ prefer: 'type-imports' }], + output: ` +import type Foo from 'foo'; +let foo: Foo; `, - options: [{ prefer: 'no-type-imports' }], }, { code: ` -import { Type } from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; +import Foo from 'foo'; +let foo: Foo; `, - options: [{ prefer: 'no-type-imports' }], - }, - { - code: ` -import * as Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import type Foo from 'foo'; +let foo: Foo; `, - options: [{ prefer: 'no-type-imports' }], }, { code: ` -import * as Type from 'foo' assert { type: 'json' }; -const a: typeof Type = Type; +import { A, B } from 'foo'; +let foo: A; +let bar: B; `, - options: [{ prefer: 'no-type-imports' }], - }, - ` - import { type A } from 'foo'; - type T = A; - `, - ` - import { type A, B } from 'foo'; - type T = A; - const b = B; - `, - ` - import { type A, type B } from 'foo'; - type T = A; - type Z = B; - `, - ` - import { B } from 'foo'; - import { type A } from 'foo'; - type T = A; - const b = B; - `, - { - code: ` -import { B, type A } from 'foo'; -type T = A; -const b = B; + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + output: ` +import type { A, B } from 'foo'; +let foo: A; +let bar: B; `, - options: [{ fixStyle: 'inline-type-imports' }], }, { code: ` -import { B } from 'foo'; -import type A from 'baz'; -type T = A; -const b = B; +import { A as a, B as b } from 'foo'; +let foo: a; +let bar: b; + `, + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + output: ` +import type { A as a, B as b } from 'foo'; +let foo: a; +let bar: b; `, - options: [{ fixStyle: 'inline-type-imports' }], }, { code: ` -import { type B } from 'foo'; -import type { A } from 'foo'; -type T = A; -const b = B; +import Foo from 'foo'; +type Bar = typeof Foo; // TSTypeQuery + `, + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + output: ` +import type Foo from 'foo'; +type Bar = typeof Foo; // TSTypeQuery `, - options: [{ fixStyle: 'inline-type-imports' }], }, { code: ` -import { B, type C } from 'foo'; -import type A from 'baz'; -type T = A; -type Z = C; -const b = B; +import foo from 'foo'; +type Bar = foo.Bar; // TSQualifiedName `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, ], + output: ` +import type foo from 'foo'; +type Bar = foo.Bar; // TSQualifiedName + `, }, { code: ` -import { B } from 'foo'; -import type { A } from 'foo'; -type T = A; -const b = B; +import foo from 'foo'; +type Baz = (typeof foo.bar)['Baz']; // TSQualifiedName & TSTypeQuery `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, ], + output: ` +import type foo from 'foo'; +type Baz = (typeof foo.bar)['Baz']; // TSQualifiedName & TSTypeQuery + `, }, { code: ` -import { B } from 'foo'; -import { A } from 'foo'; -type T = A; -const b = B; +import * as A from 'foo'; +let foo: A.Foo; `, - options: [ - { prefer: 'no-type-imports', fixStyle: 'inline-type-imports' }, - ], - }, - // exports - ` - import Type from 'foo'; - - export { Type }; // is a value export - export default Type; // is a value export - `, - ` - import type Type from 'foo'; - - export { Type }; // is a type-only export - export default Type; // is a type-only export - export type { Type }; // is a type-only export - `, - ` - import { Type } from 'foo'; - - export { Type }; // is a value export - export default Type; // is a value export - `, - ` - import type { Type } from 'foo'; - - export { Type }; // is a type-only export - export default Type; // is a type-only export - export type { Type }; // is a type-only export - `, - ` - import * as Type from 'foo'; - - export { Type }; // is a value export - export default Type; // is a value export - `, - ` - import type * as Type from 'foo'; - - export { Type }; // is a type-only export - export default Type; // is a type-only export - export type { Type }; // is a type-only export - `, - - { - code: ` -import Type from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + output: ` +import type * as A from 'foo'; +let foo: A.Foo; `, - options: [{ prefer: 'no-type-imports' }], }, { + // default and named code: ` -import { Type } from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export +import A, { B } from 'foo'; +let foo: A; +let bar: B; `, - options: [{ prefer: 'no-type-imports' }], - }, - { - code: ` -import * as Type from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + output: ` +import type { B } from 'foo'; +import type A from 'foo'; +let foo: A; +let bar: B; `, - options: [{ prefer: 'no-type-imports' }], }, - // https://github.com/typescript-eslint/typescript-eslint/issues/2455 { - code: ` -import React from 'react'; - -export const ComponentFoo: React.FC = () => { - return
Foo Foo
; -}; + code: noFormat` +import A, {} from 'foo'; +let foo: A; `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, + errors: [ + { + line: 2, + messageId: 'typeOverValue', }, - }, - }, - { - code: ` -import { h } from 'some-other-jsx-lib'; - -export const ComponentFoo: h.FC = () => { - return
Foo Foo
; -}; + ], + output: ` +import type A from 'foo'; +let foo: A; `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - jsxPragma: 'h', - }, - }, }, { code: ` -import { Fragment } from 'react'; - -export const ComponentFoo: Fragment = () => { - return <>Foo Foo; -}; +import { A, B } from 'foo'; +const foo: A = B(); `, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - jsxFragmentName: 'Fragment', + errors: [ + { + data: { typeImports: '"A"' }, + line: 2, + messageId: 'someImportsAreOnlyTypes', }, - }, + ], + output: ` +import type { A} from 'foo'; +import { B } from 'foo'; +const foo: A = B(); + `, }, - ` - import Default, * as Rest from 'module'; - const a: typeof Default = Default; - const b: typeof Rest = Rest; - `, - - // https://github.com/typescript-eslint/typescript-eslint/issues/2989 - ` - import type * as constants from './constants'; - - export type Y = { - [constants.X]: ReadonlyArray; - }; - `, - ` - import A from 'foo'; - export = A; - `, - ` - import type A from 'foo'; - export = A; - `, - ` - import type A from 'foo'; - export = {} as A; - `, - ` - import { type A } from 'foo'; - export = {} as A; - `, - - // semantically these are insane but syntactically they are valid - // we don't want to handle them because it means changing invalid code - // to valid code which is dangerous "undefined" behavior. - ` -import type T from 'mod'; -const x = T; - `, - ` -import type { T } from 'mod'; -const x = T; - `, - ` -import { type T } from 'mod'; -const x = T; - `, - ], - invalid: [ { code: ` -import Foo from 'foo'; -let foo: Foo; -type Bar = Foo; -interface Baz { - foo: Foo; -} -function fn(a: Foo): Foo {} - `, - output: ` -import type Foo from 'foo'; -let foo: Foo; -type Bar = Foo; -interface Baz { - foo: Foo; -} -function fn(a: Foo): Foo {} +import { A, B, C } from 'foo'; +const foo: A = B(); +let bar: C; `, errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"A" and "C"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type { A, C } from 'foo'; +import { B } from 'foo'; +const foo: A = B(); +let bar: C; + `, }, { code: ` -import Foo from 'foo'; -let foo: Foo; - `, - output: ` -import type Foo from 'foo'; -let foo: Foo; +import { A, B, C, D } from 'foo'; +const foo: A = B(); +type T = { bar: C; baz: D }; `, - options: [{ prefer: 'type-imports' }], errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"A", "C" and "D"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type { A, C, D } from 'foo'; +import { B } from 'foo'; +const foo: A = B(); +type T = { bar: C; baz: D }; + `, }, { code: ` -import Foo from 'foo'; -let foo: Foo; - `, - output: ` -import type Foo from 'foo'; -let foo: Foo; +import A, { B, C, D } from 'foo'; +B(); +type T = { foo: A; bar: C; baz: D }; `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"A", "C" and "D"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], - }, - { - code: ` -import { A, B } from 'foo'; -let foo: A; -let bar: B; + output: ` +import type { C, D } from 'foo'; +import type A from 'foo'; +import { B } from 'foo'; +B(); +type T = { foo: A; bar: C; baz: D }; `, - output: ` -import type { A, B } from 'foo'; -let foo: A; -let bar: B; + }, + { + code: ` +import A, { B } from 'foo'; +B(); +type T = A; `, errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"A"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type A from 'foo'; +import { B } from 'foo'; +B(); +type T = A; + `, }, { code: ` -import { A as a, B as b } from 'foo'; -let foo: a; -let bar: b; - `, - output: ` -import type { A as a, B as b } from 'foo'; -let foo: a; -let bar: b; +import type Already1Def from 'foo'; +import type { Already1 } from 'foo'; +import A, { B } from 'foo'; +import { C, D, E } from 'bar'; +import type { Already2 } from 'bar'; +type T = { b: B; c: C; d: D }; `, errors: [ { - messageId: 'typeOverValue', - line: 2, + data: { typeImports: '"B"' }, + line: 4, + messageId: 'someImportsAreOnlyTypes', + }, + { + data: { typeImports: '"C" and "D"' }, + line: 5, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type Already1Def from 'foo'; +import type { Already1 , B } from 'foo'; +import A from 'foo'; +import { E } from 'bar'; +import type { Already2 , C, D} from 'bar'; +type T = { b: B; c: C; d: D }; + `, }, { code: ` -import Foo from 'foo'; -type Bar = typeof Foo; // TSTypeQuery - `, - output: ` -import type Foo from 'foo'; -type Bar = typeof Foo; // TSTypeQuery +import A, { /* comment */ B } from 'foo'; +type T = B; `, errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"B"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type { /* comment */ B } from 'foo'; +import A from 'foo'; +type T = B; + `, }, { - code: ` -import foo from 'foo'; -type Bar = foo.Bar; // TSQualifiedName - `, - output: ` -import type foo from 'foo'; -type Bar = foo.Bar; // TSQualifiedName + code: noFormat` +import { A, B, C } from 'foo'; +import { D, E, F, } from 'bar'; +type T = A | D; `, errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"A"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', + }, + { + data: { typeImports: '"D"' }, + line: 3, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type { A} from 'foo'; +import { B, C } from 'foo'; +import type { D} from 'bar'; +import { E, F, } from 'bar'; +type T = A | D; + `, }, { - code: ` -import foo from 'foo'; -type Baz = (typeof foo.bar)['Baz']; // TSQualifiedName & TSTypeQuery - `, - output: ` -import type foo from 'foo'; -type Baz = (typeof foo.bar)['Baz']; // TSQualifiedName & TSTypeQuery + code: noFormat` +import { A, B, C } from 'foo'; +import { D, E, F, } from 'bar'; +type T = B | E; `, errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"B"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', + }, + { + data: { typeImports: '"E"' }, + line: 3, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type { B} from 'foo'; +import { A, C } from 'foo'; +import type { E} from 'bar'; +import { D, F, } from 'bar'; +type T = B | E; + `, }, { - code: ` -import * as A from 'foo'; -let foo: A.Foo; - `, - output: ` -import type * as A from 'foo'; -let foo: A.Foo; + code: noFormat` +import { A, B, C } from 'foo'; +import { D, E, F, } from 'bar'; +type T = C | F; `, errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"C"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', + }, + { + data: { typeImports: '"F"' }, + line: 3, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type { C } from 'foo'; +import { A, B } from 'foo'; +import type { F} from 'bar'; +import { D, E } from 'bar'; +type T = C | F; + `, }, { - // default and named + // all type fix cases code: ` -import A, { B } from 'foo'; -let foo: A; -let bar: B; - `, - output: ` -import type { B } from 'foo'; -import type A from 'foo'; -let foo: A; -let bar: B; +import { Type1, Type2 } from 'named_types'; +import Type from 'default_type'; +import * as Types from 'namespace_type'; +import Default, { Named } from 'default_and_named_type'; +type T = Type1 | Type2 | Type | Types.A | Default | Named; `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'typeOverValue', + }, + { + line: 3, + messageId: 'typeOverValue', + }, + { + line: 4, + messageId: 'typeOverValue', + }, + { + line: 5, + messageId: 'typeOverValue', }, ], + output: ` +import type { Type1, Type2 } from 'named_types'; +import type Type from 'default_type'; +import type * as Types from 'namespace_type'; +import type { Named } from 'default_and_named_type'; +import type Default from 'default_and_named_type'; +type T = Type1 | Type2 | Type | Types.A | Default | Named; + `, }, { - code: noFormat` -import A, {} from 'foo'; -let foo: A; - `, - output: ` -import type A from 'foo'; -let foo: A; + // some type fix cases + code: ` +import { Value1, Type1 } from 'named_import'; +import Type2, { Value2 } from 'default_import'; +import Value3, { Type3 } from 'default_import2'; +import Type4, { Type5, Value4 } from 'default_and_named_import'; +type T = Type1 | Type2 | Type3 | Type4 | Type5; `, errors: [ { - messageId: 'typeOverValue', + data: { typeImports: '"Type1"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', + }, + { + data: { typeImports: '"Type2"' }, + line: 3, + messageId: 'someImportsAreOnlyTypes', + }, + { + data: { typeImports: '"Type3"' }, + line: 4, + messageId: 'someImportsAreOnlyTypes', + }, + { + data: { typeImports: '"Type4" and "Type5"' }, + line: 5, + messageId: 'someImportsAreOnlyTypes', }, ], + output: ` +import type { Type1 } from 'named_import'; +import { Value1 } from 'named_import'; +import type Type2 from 'default_import'; +import { Value2 } from 'default_import'; +import type { Type3 } from 'default_import2'; +import Value3 from 'default_import2'; +import type { Type5} from 'default_and_named_import'; +import type Type4 from 'default_and_named_import'; +import { Value4 } from 'default_and_named_import'; +type T = Type1 | Type2 | Type3 | Type4 | Type5; + `, }, + // type annotations { code: ` -import { A, B } from 'foo'; -const foo: A = B(); - `, - output: ` -import type { A} from 'foo'; -import { B } from 'foo'; -const foo: A = B(); +let foo: import('foo'); +let bar: import('foo').Bar; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"A"' }, line: 2, + messageId: 'noImportTypeAnnotations', + }, + { + line: 3, + messageId: 'noImportTypeAnnotations', }, ], - }, - { - code: ` -import { A, B, C } from 'foo'; -const foo: A = B(); -let bar: C; - `, - output: ` -import type { A, C } from 'foo'; -import { B } from 'foo'; -const foo: A = B(); -let bar: C; + output: null, + }, + { + code: ` +let foo: import('foo'); `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"A" and "C"' }, line: 2, + messageId: 'noImportTypeAnnotations', }, ], + options: [{ prefer: 'type-imports' }], + output: null, }, { code: ` -import { A, B, C, D } from 'foo'; -const foo: A = B(); -type T = { bar: C; baz: D }; - `, - output: ` -import type { A, C, D } from 'foo'; -import { B } from 'foo'; -const foo: A = B(); -type T = { bar: C; baz: D }; +import type Foo from 'foo'; +let foo: Foo; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"A", "C" and "D"' }, line: 2, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import Foo from 'foo'; +let foo: Foo; + `, }, { code: ` -import A, { B, C, D } from 'foo'; -B(); -type T = { foo: A; bar: C; baz: D }; - `, - output: ` -import type { C, D } from 'foo'; -import type A from 'foo'; -import { B } from 'foo'; -B(); -type T = { foo: A; bar: C; baz: D }; +import type { Foo } from 'foo'; +let foo: Foo; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"A", "C" and "D"' }, line: 2, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import { Foo } from 'foo'; +let foo: Foo; + `, }, + // type queries { code: ` -import A, { B } from 'foo'; -B(); -type T = A; - `, - output: ` -import type A from 'foo'; -import { B } from 'foo'; -B(); -type T = A; +import Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"A"' }, line: 2, + messageId: 'typeOverValue', }, ], + output: ` +import type Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; + `, }, { code: ` -import type Already1Def from 'foo'; -import type { Already1 } from 'foo'; -import A, { B } from 'foo'; -import { C, D, E } from 'bar'; -import type { Already2 } from 'bar'; -type T = { b: B; c: C; d: D }; - `, - output: ` -import type Already1Def from 'foo'; -import type { Already1 , B } from 'foo'; -import A from 'foo'; -import { E } from 'bar'; -import type { Already2 , C, D} from 'bar'; -type T = { b: B; c: C; d: D }; +import { Type } from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"B"' }, - line: 4, - }, - { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"C" and "D"' }, - line: 5, + line: 2, + messageId: 'typeOverValue', }, ], + output: ` +import type { Type } from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; + `, }, { code: ` -import A, { /* comment */ B } from 'foo'; -type T = B; - `, - output: ` -import type { /* comment */ B } from 'foo'; -import A from 'foo'; -type T = B; +import * as Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"B"' }, line: 2, + messageId: 'typeOverValue', }, ], + output: ` +import type * as Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; + `, }, { - code: noFormat` -import { A, B, C } from 'foo'; -import { D, E, F, } from 'bar'; -type T = A | D; - `, - output: ` -import type { A} from 'foo'; -import { B, C } from 'foo'; -import type { D} from 'bar'; -import { E, F, } from 'bar'; -type T = A | D; + code: ` +import type Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"A"' }, line: 2, - }, - { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"D"' }, - line: 3, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; + `, }, { - code: noFormat` -import { A, B, C } from 'foo'; -import { D, E, F, } from 'bar'; -type T = B | E; - `, - output: ` -import type { B} from 'foo'; -import { A, C } from 'foo'; -import type { E} from 'bar'; -import { D, F, } from 'bar'; -type T = B | E; + code: ` +import type { Type } from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"B"' }, line: 2, - }, - { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"E"' }, - line: 3, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import { Type } from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; + `, }, { - code: noFormat` -import { A, B, C } from 'foo'; -import { D, E, F, } from 'bar'; -type T = C | F; - `, - output: ` -import type { C } from 'foo'; -import { A, B } from 'foo'; -import type { F} from 'bar'; -import { D, E } from 'bar'; -type T = C | F; + code: ` +import type * as Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"C"' }, line: 2, - }, - { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"F"' }, - line: 3, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import * as Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; + `, }, + // exports { - // all type fix cases code: ` -import { Type1, Type2 } from 'named_types'; -import Type from 'default_type'; -import * as Types from 'namespace_type'; -import Default, { Named } from 'default_and_named_type'; -type T = Type1 | Type2 | Type | Types.A | Default | Named; - `, - output: ` -import type { Type1, Type2 } from 'named_types'; -import type Type from 'default_type'; -import type * as Types from 'namespace_type'; -import type { Named } from 'default_and_named_type'; -import type Default from 'default_and_named_type'; -type T = Type1 | Type2 | Type | Types.A | Default | Named; +import Type from 'foo'; + +export type { Type }; // is a type-only export `, errors: [ { - messageId: 'typeOverValue', line: 2, - }, - { messageId: 'typeOverValue', - line: 3, - }, - { - messageId: 'typeOverValue', - line: 4, }, + ], + output: ` +import type Type from 'foo'; + +export type { Type }; // is a type-only export + `, + }, + { + code: ` +import { Type } from 'foo'; + +export type { Type }; // is a type-only export + `, + errors: [ { + line: 2, messageId: 'typeOverValue', - line: 5, }, ], + output: ` +import type { Type } from 'foo'; + +export type { Type }; // is a type-only export + `, }, { - // some type fix cases code: ` -import { Value1, Type1 } from 'named_import'; -import Type2, { Value2 } from 'default_import'; -import Value3, { Type3 } from 'default_import2'; -import Type4, { Type5, Value4 } from 'default_and_named_import'; -type T = Type1 | Type2 | Type3 | Type4 | Type5; - `, - output: ` -import type { Type1 } from 'named_import'; -import { Value1 } from 'named_import'; -import type Type2 from 'default_import'; -import { Value2 } from 'default_import'; -import type { Type3 } from 'default_import2'; -import Value3 from 'default_import2'; -import type { Type5} from 'default_and_named_import'; -import type Type4 from 'default_and_named_import'; -import { Value4 } from 'default_and_named_import'; -type T = Type1 | Type2 | Type3 | Type4 | Type5; +import * as Type from 'foo'; + +export type { Type }; // is a type-only export `, errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"Type1"' }, line: 2, - }, - { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"Type2"' }, - line: 3, - }, - { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"Type3"' }, - line: 4, - }, - { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"Type4" and "Type5"' }, - line: 5, + messageId: 'typeOverValue', }, ], + output: ` +import type * as Type from 'foo'; + +export type { Type }; // is a type-only export + `, }, - // type annotations { code: ` -let foo: import('foo'); -let bar: import('foo').Bar; +import type Type from 'foo'; + +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export `, - output: null, errors: [ { - messageId: 'noImportTypeAnnotations', line: 2, - }, - { - messageId: 'noImportTypeAnnotations', - line: 3, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import Type from 'foo'; + +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export + `, }, { code: ` -let foo: import('foo'); +import type { Type } from 'foo'; + +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export `, - output: null, - options: [{ prefer: 'type-imports' }], errors: [ { - messageId: 'noImportTypeAnnotations', line: 2, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import { Type } from 'foo'; + +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export + `, }, { code: ` -import type Foo from 'foo'; -let foo: Foo; +import type * as Type from 'foo'; + +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export `, + errors: [ + { + line: 2, + messageId: 'avoidImportType', + }, + ], options: [{ prefer: 'no-type-imports' }], output: ` -import Foo from 'foo'; -let foo: Foo; +import * as Type from 'foo'; + +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export + `, + }, + { + // type with comments + code: noFormat` +import type /*comment*/ * as AllType from 'foo'; +import type // comment +DefType from 'foo'; +import type /*comment*/ { Type } from 'foo'; + +type T = { a: AllType; b: DefType; c: Type }; `, errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'avoidImportType', + }, + { + line: 3, + messageId: 'avoidImportType', + }, + { + line: 5, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import /*comment*/ * as AllType from 'foo'; +import // comment +DefType from 'foo'; +import /*comment*/ { Type } from 'foo'; + +type T = { a: AllType; b: DefType; c: Type }; + `, }, { + // https://github.com/typescript-eslint/typescript-eslint/issues/2775 code: ` -import type { Foo } from 'foo'; -let foo: Foo; - `, - options: [{ prefer: 'no-type-imports' }], - output: ` -import { Foo } from 'foo'; -let foo: Foo; +import Default, * as Rest from 'module'; +const a: Rest.A = ''; `, errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [{ prefer: 'type-imports' }], + output: ` +import type * as Rest from 'module'; +import Default from 'module'; +const a: Rest.A = ''; + `, }, - // type queries { code: ` -import Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; - `, - output: ` -import type Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; +import Default, * as Rest from 'module'; +const a: Default = ''; `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [{ prefer: 'type-imports' }], + output: ` +import type Default from 'module'; +import * as Rest from 'module'; +const a: Default = ''; + `, }, { code: ` -import { Type } from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; - `, - output: ` -import type { Type } from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; +import Default, * as Rest from 'module'; +const a: Default = ''; +const b: Rest.A = ''; `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'typeOverValue', }, ], + options: [{ prefer: 'type-imports' }], + output: ` +import type * as Rest from 'module'; +import type Default from 'module'; +const a: Default = ''; +const b: Rest.A = ''; + `, }, { + // type with comments code: ` -import * as Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; +import Default, /*comment*/ * as Rest from 'module'; +const a: Default = ''; `, + errors: [ + { + line: 2, + messageId: 'someImportsAreOnlyTypes', + }, + ], + options: [{ prefer: 'type-imports' }], output: ` -import type * as Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; +import type Default from 'module'; +import /*comment*/ * as Rest from 'module'; +const a: Default = ''; + `, + }, + { + // type with comments + code: noFormat` +import Default /*comment1*/, /*comment2*/ { Data } from 'module'; +const a: Default = ''; `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [{ prefer: 'type-imports' }], + output: ` +import type Default /*comment1*/ from 'module'; +import /*comment2*/ { Data } from 'module'; +const a: Default = ''; + `, }, { code: ` -import type Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; - `, - options: [{ prefer: 'no-type-imports' }], - output: ` -import Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; +import Foo from 'foo'; +@deco +class A { + constructor(foo: Foo) {} +} `, errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'typeOverValue', }, ], + output: ` +import type Foo from 'foo'; +@deco +class A { + constructor(foo: Foo) {} +} + `, }, { code: ` -import type { Type } from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; - `, - options: [{ prefer: 'no-type-imports' }], - output: ` -import { Type } from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; +import { type A, B } from 'foo'; +type T = A; +const b = B; `, errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'avoidImportType', }, ], + options: [{ prefer: 'no-type-imports' }], + output: ` +import { A, B } from 'foo'; +type T = A; +const b = B; + `, }, { code: ` -import type * as Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; - `, - options: [{ prefer: 'no-type-imports' }], - output: ` -import * as Type from 'foo'; - -type T = typeof Type; -type T = typeof Type.foo; +import { A, B, type C } from 'foo'; +type T = A | C; +const b = B; `, errors: [ { - messageId: 'avoidImportType', + data: { typeImports: '"A"' }, line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [{ prefer: 'type-imports' }], + output: ` +import type { A} from 'foo'; +import { B, type C } from 'foo'; +type T = A | C; +const b = B; + `, }, - // exports + + // inline-type-imports { code: ` -import Type from 'foo'; - -export type { Type }; // is a type-only export - `, - output: ` -import type Type from 'foo'; - -export type { Type }; // is a type-only export +import { A, B } from 'foo'; +let foo: A; +let bar: B; `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'typeOverValue', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { type A, type B } from 'foo'; +let foo: A; +let bar: B; + `, }, { code: ` -import { Type } from 'foo'; - -export type { Type }; // is a type-only export - `, - output: ` -import type { Type } from 'foo'; +import { A, B } from 'foo'; -export type { Type }; // is a type-only export +let foo: A; +B(); `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { type A, B } from 'foo'; + +let foo: A; +B(); + `, }, { code: ` -import * as Type from 'foo'; - -export type { Type }; // is a type-only export - `, - output: ` -import type * as Type from 'foo'; - -export type { Type }; // is a type-only export +import { A, B } from 'foo'; +type T = A; +B(); `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { type A, B } from 'foo'; +type T = A; +B(); + `, }, { code: ` -import type Type from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export - `, - options: [{ prefer: 'no-type-imports' }], - output: ` -import Type from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export +import { A } from 'foo'; +import { B } from 'foo'; +type T = A; +type U = B; `, errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'typeOverValue', + }, + { + line: 3, + messageId: 'typeOverValue', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { type A } from 'foo'; +import { type B } from 'foo'; +type T = A; +type U = B; + `, }, { code: ` -import type { Type } from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export - `, - options: [{ prefer: 'no-type-imports' }], - output: ` -import { Type } from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export +import { A } from 'foo'; +import B from 'foo'; +type T = A; +type U = B; `, errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'typeOverValue', + }, + { + line: 3, + messageId: 'typeOverValue', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { type A } from 'foo'; +import type B from 'foo'; +type T = A; +type U = B; + `, }, { code: ` -import type * as Type from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export - `, - options: [{ prefer: 'no-type-imports' }], - output: ` -import * as Type from 'foo'; - -export { Type }; // is a type-only export -export default Type; // is a type-only export -export type { Type }; // is a type-only export +import A, { B, C } from 'foo'; +type T = B; +type U = C; +A(); `, errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import A, { type B, type C } from 'foo'; +type T = B; +type U = C; +A(); + `, }, { - // type with comments - code: noFormat` -import type /*comment*/ * as AllType from 'foo'; -import type // comment -DefType from 'foo'; -import type /*comment*/ { Type } from 'foo'; - -type T = { a: AllType; b: DefType; c: Type }; - `, - options: [{ prefer: 'no-type-imports' }], - output: ` -import /*comment*/ * as AllType from 'foo'; -import // comment -DefType from 'foo'; -import /*comment*/ { Type } from 'foo'; - -type T = { a: AllType; b: DefType; c: Type }; + code: ` +import A, { B, C } from 'foo'; +type T = B; +type U = C; +type V = A; `, errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'typeOverValue', }, + ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import {type B, type C} from 'foo'; +import type A from 'foo'; +type T = B; +type U = C; +type V = A; + `, + }, + { + code: ` +import A, { B, C as D } from 'foo'; +type T = B; +type U = D; +type V = A; + `, + errors: [ { - messageId: 'avoidImportType', - line: 3, - }, - { - messageId: 'avoidImportType', - line: 5, + line: 2, + messageId: 'typeOverValue', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import {type B, type C as D} from 'foo'; +import type A from 'foo'; +type T = B; +type U = D; +type V = A; + `, }, { - // https://github.com/typescript-eslint/typescript-eslint/issues/2775 code: ` -import Default, * as Rest from 'module'; -const a: Rest.A = ''; - `, - options: [{ prefer: 'type-imports' }], - output: ` -import type * as Rest from 'module'; -import Default from 'module'; -const a: Rest.A = ''; +import { /* comment */ A, B } from 'foo'; +type T = A; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { /* comment */ type A, B } from 'foo'; +type T = A; + `, }, { code: ` -import Default, * as Rest from 'module'; -const a: Default = ''; - `, - options: [{ prefer: 'type-imports' }], - output: ` -import type Default from 'module'; -import * as Rest from 'module'; -const a: Default = ''; +import { B, /* comment */ A } from 'foo'; +type T = A; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { B, /* comment */ type A } from 'foo'; +type T = A; + `, }, { code: ` -import Default, * as Rest from 'module'; -const a: Default = ''; -const b: Rest.A = ''; - `, - options: [{ prefer: 'type-imports' }], - output: ` -import type * as Rest from 'module'; -import type Default from 'module'; -const a: Default = ''; -const b: Rest.A = ''; +import { A, B, C } from 'foo'; +import type { D } from 'deez'; + +const foo: A = B(); +let bar: C; +let baz: D; `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { type A, B, type C } from 'foo'; +import type { D } from 'deez'; + +const foo: A = B(); +let bar: C; +let baz: D; + `, }, { - // type with comments code: ` -import Default, /*comment*/ * as Rest from 'module'; -const a: Default = ''; - `, - options: [{ prefer: 'type-imports' }], - output: ` -import type Default from 'module'; -import /*comment*/ * as Rest from 'module'; -const a: Default = ''; +import { A, B, type C } from 'foo'; +import type { D } from 'deez'; +const foo: A = B(); +let bar: C; +let baz: D; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', line: 2, + messageId: 'someImportsAreOnlyTypes', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { type A, B, type C } from 'foo'; +import type { D } from 'deez'; +const foo: A = B(); +let bar: C; +let baz: D; + `, }, { - // type with comments - code: noFormat` -import Default /*comment1*/, /*comment2*/ { Data } from 'module'; -const a: Default = ''; - `, - options: [{ prefer: 'type-imports' }], - output: ` -import type Default /*comment1*/ from 'module'; -import /*comment2*/ { Data } from 'module'; -const a: Default = ''; + code: ` +import A from 'foo'; +export = {} as A; `, errors: [ { - messageId: 'someImportsAreOnlyTypes', line: 2, + messageId: 'typeOverValue', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import type A from 'foo'; +export = {} as A; + `, }, { code: ` -import Foo from 'foo'; -@deco -class A { - constructor(foo: Foo) {} -} - `, - output: ` -import type Foo from 'foo'; -@deco -class A { - constructor(foo: Foo) {} -} +import { A } from 'foo'; +export = {} as A; `, errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'typeOverValue', }, ], + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, + ], + output: ` +import { type A } from 'foo'; +export = {} as A; + `, }, { code: ` -import { type A, B } from 'foo'; -type T = A; -const b = B; - `, - output: ` -import { A, B } from 'foo'; -type T = A; -const b = B; + import Foo from 'foo'; + @deco + class A { + constructor(foo: Foo) {} + } `, - options: [{ prefer: 'no-type-imports' }], errors: [ { - messageId: 'avoidImportType', line: 2, + messageId: 'typeOverValue', }, ], + output: ` + import type Foo from 'foo'; + @deco + class A { + constructor(foo: Foo) {} + } + `, }, { code: ` -import { A, B, type C } from 'foo'; -type T = A | C; -const b = B; - `, - output: ` -import type { A} from 'foo'; -import { B, type C } from 'foo'; -type T = A | C; -const b = B; + import Foo from 'foo'; + class A { + @deco + foo: Foo; + } `, - options: [{ prefer: 'type-imports' }], errors: [ { - messageId: 'someImportsAreOnlyTypes', - data: { typeImports: '"A"' }, line: 2, + messageId: 'typeOverValue', }, ], + output: ` + import type Foo from 'foo'; + class A { + @deco + foo: Foo; + } + `, }, - - // inline-type-imports { code: ` -import { A, B } from 'foo'; -let foo: A; -let bar: B; - `, - output: ` -import { type A, type B } from 'foo'; -let foo: A; -let bar: B; + import Foo from 'foo'; + class A { + @deco + foo(foo: Foo) {} + } `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'typeOverValue', }, ], + output: ` + import type Foo from 'foo'; + class A { + @deco + foo(foo: Foo) {} + } + `, }, { code: ` -import { A, B } from 'foo'; - -let foo: A; -B(); - `, - output: ` -import { type A, B } from 'foo'; - -let foo: A; -B(); + import Foo from 'foo'; + class A { + @deco + foo(): Foo {} + } `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ { - messageId: 'someImportsAreOnlyTypes', line: 2, + messageId: 'typeOverValue', }, ], + output: ` + import type Foo from 'foo'; + class A { + @deco + foo(): Foo {} + } + `, }, { code: ` -import { A, B } from 'foo'; -type T = A; -B(); - `, - output: ` -import { type A, B } from 'foo'; -type T = A; -B(); + import Foo from 'foo'; + class A { + foo(@deco foo: Foo) {} + } `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ { - messageId: 'someImportsAreOnlyTypes', line: 2, + messageId: 'typeOverValue', }, ], + output: ` + import type Foo from 'foo'; + class A { + foo(@deco foo: Foo) {} + } + `, }, { code: ` -import { A } from 'foo'; -import { B } from 'foo'; -type T = A; -type U = B; - `, - output: ` -import { type A } from 'foo'; -import { type B } from 'foo'; -type T = A; -type U = B; + import Foo from 'foo'; + class A { + @deco + set foo(value: Foo) {} + } `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ { - messageId: 'typeOverValue', line: 2, - }, - { messageId: 'typeOverValue', - line: 3, }, ], + output: ` + import type Foo from 'foo'; + class A { + @deco + set foo(value: Foo) {} + } + `, }, { code: ` -import { A } from 'foo'; -import B from 'foo'; -type T = A; -type U = B; - `, - output: ` -import { type A } from 'foo'; -import type B from 'foo'; -type T = A; -type U = B; + import Foo from 'foo'; + class A { + @deco + get foo() {} + + set foo(value: Foo) {} + } `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ { - messageId: 'typeOverValue', line: 2, - }, - { messageId: 'typeOverValue', - line: 3, }, ], + output: ` + import type Foo from 'foo'; + class A { + @deco + get foo() {} + + set foo(value: Foo) {} + } + `, }, { code: ` -import A, { B, C } from 'foo'; -type T = B; -type U = C; -A(); - `, - output: ` -import A, { type B, type C } from 'foo'; -type T = B; -type U = C; -A(); + import Foo from 'foo'; + class A { + @deco + get foo() {} + + set ['foo'](value: Foo) {} + } `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ { - messageId: 'someImportsAreOnlyTypes', line: 2, + messageId: 'typeOverValue', }, ], + output: ` + import type Foo from 'foo'; + class A { + @deco + get foo() {} + + set ['foo'](value: Foo) {} + } + `, }, { code: ` -import A, { B, C } from 'foo'; -type T = B; -type U = C; -type V = A; - `, - output: ` -import {type B, type C} from 'foo'; -import type A from 'foo'; -type T = B; -type U = C; -type V = A; + import * as foo from 'foo'; + @deco + class A { + constructor(foo: foo.Foo) {} + } `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ { - messageId: 'typeOverValue', line: 2, + messageId: 'typeOverValue', }, ], + output: ` + import type * as foo from 'foo'; + @deco + class A { + constructor(foo: foo.Foo) {} + } + `, }, + // https://github.com/typescript-eslint/typescript-eslint/issues/7209 { code: ` -import A, { B, C as D } from 'foo'; -type T = B; -type U = D; -type V = A; +import 'foo'; +import { Foo, Bar } from 'foo'; +function test(foo: Foo) {} `, + errors: [ + { column: 1, line: 3, messageId: 'someImportsAreOnlyTypes' }, + ], output: ` -import {type B, type C as D} from 'foo'; -import type A from 'foo'; -type T = B; -type U = D; -type V = A; +import 'foo'; +import type { Foo} from 'foo'; +import { Bar } from 'foo'; +function test(foo: Foo) {} + `, + }, + { + code: ` +import {} from 'foo'; +import { Foo, Bar } from 'foo'; +function test(foo: Foo) {} `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, + { column: 1, line: 3, messageId: 'someImportsAreOnlyTypes' }, ], + output: ` +import {} from 'foo'; +import type { Foo} from 'foo'; +import { Bar } from 'foo'; +function test(foo: Foo) {} + `, + }, + ], + valid: [ + ` + import Foo from 'foo'; + const foo: Foo = new Foo(); + `, + ` + import foo from 'foo'; + const foo: foo.Foo = foo.fn(); + `, + ` + import { A, B } from 'foo'; + const foo: A = B(); + const bar = new A(); + `, + ` + import Foo from 'foo'; + `, + ` + import Foo from 'foo'; + type T = Foo; // shadowing + `, + ` + import Foo from 'foo'; + function fn() { + type Foo = {}; // shadowing + let foo: Foo; + } + `, + ` + import { A, B } from 'foo'; + const b = B; + `, + ` + import { A, B, C as c } from 'foo'; + const d = c; + `, + ` + import {} from 'foo'; // empty + `, + { + code: ` +let foo: import('foo'); +let bar: import('foo').Bar; + `, + options: [{ disallowTypeAnnotations: false }], }, { code: ` -import { /* comment */ A, B } from 'foo'; -type T = A; +import Foo from 'foo'; +let foo: Foo; `, - output: ` -import { /* comment */ type A, B } from 'foo'; -type T = A; + options: [{ prefer: 'no-type-imports' }], + }, + // type queries + ` + import type Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + ` + import type { Type } from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + ` + import type * as Type from 'foo'; + + type T = typeof Type; + type T = typeof Type.foo; + `, + { + code: ` +import Type from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], - errors: [ - { - messageId: 'someImportsAreOnlyTypes', - line: 2, - }, - ], + options: [{ prefer: 'no-type-imports' }], }, { code: ` -import { B, /* comment */ A } from 'foo'; -type T = A; - `, - output: ` -import { B, /* comment */ type A } from 'foo'; -type T = A; +import { Type } from 'foo'; + +type T = typeof Type; +type T = typeof Type.foo; `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], - errors: [ - { - messageId: 'someImportsAreOnlyTypes', - line: 2, - }, - ], + options: [{ prefer: 'no-type-imports' }], }, { code: ` -import { A, B, C } from 'foo'; -import type { D } from 'deez'; - -const foo: A = B(); -let bar: C; -let baz: D; - `, - output: ` -import { type A, B, type C } from 'foo'; -import type { D } from 'deez'; +import * as Type from 'foo'; -const foo: A = B(); -let bar: C; -let baz: D; +type T = typeof Type; +type T = typeof Type.foo; `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], - errors: [ - { - messageId: 'someImportsAreOnlyTypes', - line: 2, - }, - ], + options: [{ prefer: 'no-type-imports' }], }, { code: ` -import { A, B, type C } from 'foo'; -import type { D } from 'deez'; -const foo: A = B(); -let bar: C; -let baz: D; - `, - output: ` -import { type A, B, type C } from 'foo'; -import type { D } from 'deez'; -const foo: A = B(); -let bar: C; -let baz: D; +import * as Type from 'foo' assert { type: 'json' }; +const a: typeof Type = Type; `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], - errors: [ - { - messageId: 'someImportsAreOnlyTypes', - line: 2, - }, - ], + options: [{ prefer: 'no-type-imports' }], }, + ` + import { type A } from 'foo'; + type T = A; + `, + ` + import { type A, B } from 'foo'; + type T = A; + const b = B; + `, + ` + import { type A, type B } from 'foo'; + type T = A; + type Z = B; + `, + ` + import { B } from 'foo'; + import { type A } from 'foo'; + type T = A; + const b = B; + `, { code: ` -import A from 'foo'; -export = {} as A; - `, - output: ` -import type A from 'foo'; -export = {} as A; +import { B, type A } from 'foo'; +type T = A; +const b = B; `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, - ], + options: [{ fixStyle: 'inline-type-imports' }], }, { code: ` -import { A } from 'foo'; -export = {} as A; - `, - output: ` -import { type A } from 'foo'; -export = {} as A; +import { B } from 'foo'; +import type A from 'baz'; +type T = A; +const b = B; `, - options: [ - { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, - ], - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, - ], + options: [{ fixStyle: 'inline-type-imports' }], }, { code: ` - import Foo from 'foo'; - @deco - class A { - constructor(foo: Foo) {} - } - `, - output: ` - import type Foo from 'foo'; - @deco - class A { - constructor(foo: Foo) {} - } +import { type B } from 'foo'; +import type { A } from 'foo'; +type T = A; +const b = B; `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, - ], + options: [{ fixStyle: 'inline-type-imports' }], }, { code: ` - import Foo from 'foo'; - class A { - @deco - foo: Foo; - } - `, - output: ` - import type Foo from 'foo'; - class A { - @deco - foo: Foo; - } +import { B, type C } from 'foo'; +import type A from 'baz'; +type T = A; +type Z = C; +const b = B; `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, ], }, { code: ` - import Foo from 'foo'; - class A { - @deco - foo(foo: Foo) {} - } - `, - output: ` - import type Foo from 'foo'; - class A { - @deco - foo(foo: Foo) {} - } +import { B } from 'foo'; +import type { A } from 'foo'; +type T = A; +const b = B; `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, + options: [ + { fixStyle: 'inline-type-imports', prefer: 'type-imports' }, ], }, { code: ` - import Foo from 'foo'; - class A { - @deco - foo(): Foo {} - } - `, - output: ` - import type Foo from 'foo'; - class A { - @deco - foo(): Foo {} - } +import { B } from 'foo'; +import { A } from 'foo'; +type T = A; +const b = B; `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, + options: [ + { fixStyle: 'inline-type-imports', prefer: 'no-type-imports' }, ], }, + // exports + ` + import Type from 'foo'; + + export { Type }; // is a value export + export default Type; // is a value export + `, + ` + import type Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + ` + import { Type } from 'foo'; + + export { Type }; // is a value export + export default Type; // is a value export + `, + ` + import type { Type } from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + ` + import * as Type from 'foo'; + + export { Type }; // is a value export + export default Type; // is a value export + `, + ` + import type * as Type from 'foo'; + + export { Type }; // is a type-only export + export default Type; // is a type-only export + export type { Type }; // is a type-only export + `, + { code: ` - import Foo from 'foo'; - class A { - foo(@deco foo: Foo) {} - } - `, - output: ` - import type Foo from 'foo'; - class A { - foo(@deco foo: Foo) {} - } +import Type from 'foo'; + +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, - ], + options: [{ prefer: 'no-type-imports' }], }, { code: ` - import Foo from 'foo'; - class A { - @deco - set foo(value: Foo) {} - } - `, - output: ` - import type Foo from 'foo'; - class A { - @deco - set foo(value: Foo) {} - } +import { Type } from 'foo'; + +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, - ], + options: [{ prefer: 'no-type-imports' }], }, { code: ` - import Foo from 'foo'; - class A { - @deco - get foo() {} - - set foo(value: Foo) {} - } - `, - output: ` - import type Foo from 'foo'; - class A { - @deco - get foo() {} +import * as Type from 'foo'; - set foo(value: Foo) {} - } +export { Type }; // is a type-only export +export default Type; // is a type-only export +export type { Type }; // is a type-only export `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, - ], + options: [{ prefer: 'no-type-imports' }], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/2455 { code: ` - import Foo from 'foo'; - class A { - @deco - get foo() {} - - set ['foo'](value: Foo) {} - } - `, - output: ` - import type Foo from 'foo'; - class A { - @deco - get foo() {} +import React from 'react'; - set ['foo'](value: Foo) {} - } +export const ComponentFoo: React.FC = () => { + return
Foo Foo
; +}; `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, }, - ], + }, }, { code: ` - import * as foo from 'foo'; - @deco - class A { - constructor(foo: foo.Foo) {} - } - `, - output: ` - import type * as foo from 'foo'; - @deco - class A { - constructor(foo: foo.Foo) {} - } +import { h } from 'some-other-jsx-lib'; + +export const ComponentFoo: h.FC = () => { + return
Foo Foo
; +}; `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + jsxPragma: 'h', }, - ], - }, - // https://github.com/typescript-eslint/typescript-eslint/issues/7209 - { - code: ` -import 'foo'; -import { Foo, Bar } from 'foo'; -function test(foo: Foo) {} - `, - output: ` -import 'foo'; -import type { Foo} from 'foo'; -import { Bar } from 'foo'; -function test(foo: Foo) {} - `, - errors: [ - { messageId: 'someImportsAreOnlyTypes', line: 3, column: 1 }, - ], + }, }, { code: ` -import {} from 'foo'; -import { Foo, Bar } from 'foo'; -function test(foo: Foo) {} - `, - output: ` -import {} from 'foo'; -import type { Foo} from 'foo'; -import { Bar } from 'foo'; -function test(foo: Foo) {} +import { Fragment } from 'react'; + +export const ComponentFoo: Fragment = () => { + return <>Foo Foo; +}; `, - errors: [ - { messageId: 'someImportsAreOnlyTypes', line: 3, column: 1 }, - ], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + jsxFragmentName: 'Fragment', + }, + }, }, + ` + import Default, * as Rest from 'module'; + const a: typeof Default = Default; + const b: typeof Rest = Rest; + `, + + // https://github.com/typescript-eslint/typescript-eslint/issues/2989 + ` + import type * as constants from './constants'; + + export type Y = { + [constants.X]: ReadonlyArray; + }; + `, + ` + import A from 'foo'; + export = A; + `, + ` + import type A from 'foo'; + export = A; + `, + ` + import type A from 'foo'; + export = {} as A; + `, + ` + import { type A } from 'foo'; + export = {} as A; + `, + + // semantically these are insane but syntactically they are valid + // we don't want to handle them because it means changing invalid code + // to valid code which is dangerous "undefined" behavior. + ` +import type T from 'mod'; +const x = T; + `, + ` +import type { T } from 'mod'; +const x = T; + `, + ` +import { type T } from 'mod'; +const x = T; + `, ], }); }); @@ -1953,13 +1953,31 @@ describe('experimentalDecorators: true + emitDecoratorMetadata: true', () => { const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - experimentalDecorators: true, emitDecoratorMetadata: true, + experimentalDecorators: true, }, }, }); ruleTester.run('consistent-type-imports', rule, { + invalid: [ + { + code: ` + import Foo from 'foo'; + export type T = Foo; + `, + errors: [ + { + line: 2, + messageId: 'typeOverValue', + }, + ], + output: ` + import type Foo from 'foo'; + export type T = Foo; + `, + }, + ], valid: [ ` import Foo from 'foo'; @@ -2104,23 +2122,5 @@ describe('experimentalDecorators: true + emitDecoratorMetadata: true', () => { } `, ], - invalid: [ - { - code: ` - import Foo from 'foo'; - export type T = Foo; - `, - output: ` - import type Foo from 'foo'; - export type T = Foo; - `, - errors: [ - { - messageId: 'typeOverValue', - line: 2, - }, - ], - }, - ], }); }); diff --git a/packages/eslint-plugin/tests/rules/default-param-last.test.ts b/packages/eslint-plugin/tests/rules/default-param-last.test.ts index bcad5e8a6d62..ac95d9a51766 100644 --- a/packages/eslint-plugin/tests/rules/default-param-last.test.ts +++ b/packages/eslint-plugin/tests/rules/default-param-last.test.ts @@ -5,121 +5,15 @@ import rule from '../../src/rules/default-param-last'; const ruleTester = new RuleTester(); ruleTester.run('default-param-last', rule, { - valid: [ - 'function foo() {}', - 'function foo(a: number) {}', - 'function foo(a = 1) {}', - 'function foo(a?: number) {}', - 'function foo(a: number, b: number) {}', - 'function foo(a: number, b: number, c?: number) {}', - 'function foo(a: number, b = 1) {}', - 'function foo(a: number, b = 1, c = 1) {}', - 'function foo(a: number, b = 1, c?: number) {}', - 'function foo(a: number, b?: number, c = 1) {}', - 'function foo(a: number, b = 1, ...c) {}', - - 'const foo = function () {};', - 'const foo = function (a: number) {};', - 'const foo = function (a = 1) {};', - 'const foo = function (a?: number) {};', - 'const foo = function (a: number, b: number) {};', - 'const foo = function (a: number, b: number, c?: number) {};', - 'const foo = function (a: number, b = 1) {};', - 'const foo = function (a: number, b = 1, c = 1) {};', - 'const foo = function (a: number, b = 1, c?: number) {};', - 'const foo = function (a: number, b?: number, c = 1) {};', - 'const foo = function (a: number, b = 1, ...c) {};', - - 'const foo = () => {};', - 'const foo = (a: number) => {};', - 'const foo = (a = 1) => {};', - 'const foo = (a?: number) => {};', - 'const foo = (a: number, b: number) => {};', - 'const foo = (a: number, b: number, c?: number) => {};', - 'const foo = (a: number, b = 1) => {};', - 'const foo = (a: number, b = 1, c = 1) => {};', - 'const foo = (a: number, b = 1, c?: number) => {};', - 'const foo = (a: number, b?: number, c = 1) => {};', - 'const foo = (a: number, b = 1, ...c) => {};', - ` -class Foo { - constructor(a: number, b: number, c: number) {} -} - `, - ` -class Foo { - constructor(a: number, b?: number, c = 1) {} -} - `, - ` -class Foo { - constructor(a: number, b = 1, c?: number) {} -} - `, - ` -class Foo { - constructor( - public a: number, - protected b: number, - private c: number, - ) {} -} - `, - ` -class Foo { - constructor( - public a: number, - protected b?: number, - private c = 10, - ) {} -} - `, - ` -class Foo { - constructor( - public a: number, - protected b = 10, - private c?: number, - ) {} -} - `, - ` -class Foo { - constructor( - a: number, - protected b?: number, - private c = 0, - ) {} -} - `, - ` -class Foo { - constructor( - a: number, - b?: number, - private c = 0, - ) {} -} - `, - ` -class Foo { - constructor( - a: number, - private b?: number, - c = 0, - ) {} -} - `, - ], invalid: [ { code: 'function foo(a = 1, b: number) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -127,16 +21,16 @@ class Foo { code: 'function foo(a = 1, b = 2, c: number) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 21, endColumn: 26, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -144,16 +38,16 @@ class Foo { code: 'function foo(a = 1, b: number, c = 2, d: number) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 32, endColumn: 37, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -161,10 +55,10 @@ class Foo { code: 'function foo(a = 1, b: number, c = 2) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -172,10 +66,10 @@ class Foo { code: 'function foo(a = 1, b: number, ...c) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -183,10 +77,10 @@ class Foo { code: 'function foo(a?: number, b: number) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 24, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -194,10 +88,10 @@ class Foo { code: 'function foo(a: number, b?: number, c: number) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 25, endColumn: 35, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -205,16 +99,16 @@ class Foo { code: 'function foo(a = 1, b?: number, c: number) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 21, endColumn: 31, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -222,10 +116,10 @@ class Foo { code: 'function foo(a = 1, { b }) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -233,10 +127,10 @@ class Foo { code: 'function foo({ a } = {}, b) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 24, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -244,10 +138,10 @@ class Foo { code: 'function foo({ a, b } = { a: 1, b: 2 }, c) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 39, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -255,10 +149,10 @@ class Foo { code: 'function foo([a] = [], b) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 22, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -266,10 +160,10 @@ class Foo { code: 'function foo([a, b] = [1, 2], c) {}', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 29, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -277,10 +171,10 @@ class Foo { code: 'const foo = function (a = 1, b: number) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 28, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -288,16 +182,16 @@ class Foo { code: 'const foo = function (a = 1, b = 2, c: number) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 28, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 30, endColumn: 35, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -305,16 +199,16 @@ class Foo { code: 'const foo = function (a = 1, b: number, c = 2, d: number) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 28, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 41, endColumn: 46, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -322,10 +216,10 @@ class Foo { code: 'const foo = function (a = 1, b: number, c = 2) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 28, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -333,10 +227,10 @@ class Foo { code: 'const foo = function (a = 1, b: number, ...c) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 28, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -344,10 +238,10 @@ class Foo { code: 'const foo = function (a?: number, b: number) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 33, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -355,10 +249,10 @@ class Foo { code: 'const foo = function (a: number, b?: number, c: number) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 34, endColumn: 44, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -366,16 +260,16 @@ class Foo { code: 'const foo = function (a = 1, b?: number, c: number) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 28, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 30, endColumn: 40, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -383,10 +277,10 @@ class Foo { code: 'const foo = function (a = 1, { b }) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 28, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -394,10 +288,10 @@ class Foo { code: 'const foo = function ({ a } = {}, b) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 33, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -405,10 +299,10 @@ class Foo { code: 'const foo = function ({ a, b } = { a: 1, b: 2 }, c) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 48, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -416,10 +310,10 @@ class Foo { code: 'const foo = function ([a] = [], b) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 31, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -427,10 +321,10 @@ class Foo { code: 'const foo = function ([a, b] = [1, 2], c) {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 23, endColumn: 38, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -438,10 +332,10 @@ class Foo { code: 'const foo = (a = 1, b: number) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -449,16 +343,16 @@ class Foo { code: 'const foo = (a = 1, b = 2, c: number) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 21, endColumn: 26, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -466,16 +360,16 @@ class Foo { code: 'const foo = (a = 1, b: number, c = 2, d: number) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 32, endColumn: 37, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -483,10 +377,10 @@ class Foo { code: 'const foo = (a = 1, b: number, c = 2) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -494,10 +388,10 @@ class Foo { code: 'const foo = (a = 1, b: number, ...c) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -505,10 +399,10 @@ class Foo { code: 'const foo = (a?: number, b: number) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 24, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -516,10 +410,10 @@ class Foo { code: 'const foo = (a: number, b?: number, c: number) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 25, endColumn: 35, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -527,16 +421,16 @@ class Foo { code: 'const foo = (a = 1, b?: number, c: number) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, { - messageId: 'shouldBeLast', - line: 1, column: 21, endColumn: 31, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -544,10 +438,10 @@ class Foo { code: 'const foo = (a = 1, { b }) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 19, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -555,10 +449,10 @@ class Foo { code: 'const foo = ({ a } = {}, b) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 24, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -566,10 +460,10 @@ class Foo { code: 'const foo = ({ a, b } = { a: 1, b: 2 }, c) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 39, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -577,10 +471,10 @@ class Foo { code: 'const foo = ([a] = [], b) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 22, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -588,10 +482,10 @@ class Foo { code: 'const foo = ([a, b] = [1, 2], c) => {};', errors: [ { - messageId: 'shouldBeLast', - line: 1, column: 14, endColumn: 29, + line: 1, + messageId: 'shouldBeLast', }, ], }, @@ -607,10 +501,10 @@ class Foo { `, errors: [ { - messageId: 'shouldBeLast', - line: 5, column: 5, endColumn: 25, + line: 5, + messageId: 'shouldBeLast', }, ], }, @@ -626,10 +520,10 @@ class Foo { `, errors: [ { - messageId: 'shouldBeLast', - line: 5, column: 5, endColumn: 20, + line: 5, + messageId: 'shouldBeLast', }, ], }, @@ -644,10 +538,10 @@ class Foo { `, errors: [ { - messageId: 'shouldBeLast', - line: 4, column: 5, endColumn: 22, + line: 4, + messageId: 'shouldBeLast', }, ], }, @@ -662,10 +556,10 @@ class Foo { `, errors: [ { - messageId: 'shouldBeLast', - line: 4, column: 5, endColumn: 17, + line: 4, + messageId: 'shouldBeLast', }, ], }, @@ -677,10 +571,10 @@ class Foo { `, errors: [ { - messageId: 'shouldBeLast', - line: 3, column: 15, endColumn: 20, + line: 3, + messageId: 'shouldBeLast', }, ], }, @@ -692,12 +586,118 @@ class Foo { `, errors: [ { - messageId: 'shouldBeLast', - line: 3, column: 15, endColumn: 25, + line: 3, + messageId: 'shouldBeLast', }, ], }, ], + valid: [ + 'function foo() {}', + 'function foo(a: number) {}', + 'function foo(a = 1) {}', + 'function foo(a?: number) {}', + 'function foo(a: number, b: number) {}', + 'function foo(a: number, b: number, c?: number) {}', + 'function foo(a: number, b = 1) {}', + 'function foo(a: number, b = 1, c = 1) {}', + 'function foo(a: number, b = 1, c?: number) {}', + 'function foo(a: number, b?: number, c = 1) {}', + 'function foo(a: number, b = 1, ...c) {}', + + 'const foo = function () {};', + 'const foo = function (a: number) {};', + 'const foo = function (a = 1) {};', + 'const foo = function (a?: number) {};', + 'const foo = function (a: number, b: number) {};', + 'const foo = function (a: number, b: number, c?: number) {};', + 'const foo = function (a: number, b = 1) {};', + 'const foo = function (a: number, b = 1, c = 1) {};', + 'const foo = function (a: number, b = 1, c?: number) {};', + 'const foo = function (a: number, b?: number, c = 1) {};', + 'const foo = function (a: number, b = 1, ...c) {};', + + 'const foo = () => {};', + 'const foo = (a: number) => {};', + 'const foo = (a = 1) => {};', + 'const foo = (a?: number) => {};', + 'const foo = (a: number, b: number) => {};', + 'const foo = (a: number, b: number, c?: number) => {};', + 'const foo = (a: number, b = 1) => {};', + 'const foo = (a: number, b = 1, c = 1) => {};', + 'const foo = (a: number, b = 1, c?: number) => {};', + 'const foo = (a: number, b?: number, c = 1) => {};', + 'const foo = (a: number, b = 1, ...c) => {};', + ` +class Foo { + constructor(a: number, b: number, c: number) {} +} + `, + ` +class Foo { + constructor(a: number, b?: number, c = 1) {} +} + `, + ` +class Foo { + constructor(a: number, b = 1, c?: number) {} +} + `, + ` +class Foo { + constructor( + public a: number, + protected b: number, + private c: number, + ) {} +} + `, + ` +class Foo { + constructor( + public a: number, + protected b?: number, + private c = 10, + ) {} +} + `, + ` +class Foo { + constructor( + public a: number, + protected b = 10, + private c?: number, + ) {} +} + `, + ` +class Foo { + constructor( + a: number, + protected b?: number, + private c = 0, + ) {} +} + `, + ` +class Foo { + constructor( + a: number, + b?: number, + private c = 0, + ) {} +} + `, + ` +class Foo { + constructor( + a: number, + private b?: number, + c = 0, + ) {} +} + `, + ], }); diff --git a/packages/eslint-plugin/tests/rules/dot-notation.test.ts b/packages/eslint-plugin/tests/rules/dot-notation.test.ts index 580ddef5715c..6b7f445e5d9f 100644 --- a/packages/eslint-plugin/tests/rules/dot-notation.test.ts +++ b/packages/eslint-plugin/tests/rules/dot-notation.test.ts @@ -8,8 +8,8 @@ const rootPath = getFixturesRootDir(); const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - tsconfigRootDir: rootPath, project: './tsconfig.json', + tsconfigRootDir: rootPath, }, }, }); @@ -23,134 +23,6 @@ function q(str: string): string { } ruleTester.run('dot-notation', rule, { - valid: [ - // baseRule - 'a.b;', - 'a.b.c;', - "a['12'];", - 'a[b];', - 'a[0];', - { code: 'a.b.c;', options: [{ allowKeywords: false }] }, - { code: 'a.arguments;', options: [{ allowKeywords: false }] }, - { code: 'a.let;', options: [{ allowKeywords: false }] }, - { code: 'a.yield;', options: [{ allowKeywords: false }] }, - { code: 'a.eval;', options: [{ allowKeywords: false }] }, - { code: 'a[0];', options: [{ allowKeywords: false }] }, - { code: "a['while'];", options: [{ allowKeywords: false }] }, - { code: "a['true'];", options: [{ allowKeywords: false }] }, - { code: "a['null'];", options: [{ allowKeywords: false }] }, - { code: 'a[true];', options: [{ allowKeywords: false }] }, - { code: 'a[null];', options: [{ allowKeywords: false }] }, - { code: 'a.true;', options: [{ allowKeywords: true }] }, - { code: 'a.null;', options: [{ allowKeywords: true }] }, - { - code: "a['snake_case'];", - options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }], - }, - { - code: "a['lots_of_snake_case'];", - options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }], - }, - { - code: 'a[`time${range}`];', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'a[`while`];', - options: [{ allowKeywords: false }], - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - { - code: 'a[`time range`];', - languageOptions: { parserOptions: { ecmaVersion: 6 } }, - }, - 'a.true;', - 'a.null;', - 'a[undefined];', - 'a[void 0];', - 'a[b()];', - { - code: 'a[/(?0)/];', - languageOptions: { parserOptions: { ecmaVersion: 2018 } }, - }, - - { - code: ` -class X { - private priv_prop = 123; -} - -const x = new X(); -x['priv_prop'] = 123; - `, - options: [{ allowPrivateClassPropertyAccess: true }], - }, - - { - code: ` -class X { - protected protected_prop = 123; -} - -const x = new X(); -x['protected_prop'] = 123; - `, - options: [{ allowProtectedClassPropertyAccess: true }], - }, - { - code: ` -class X { - prop: string; - [key: string]: number; -} - -const x = new X(); -x['hello'] = 3; - `, - options: [{ allowIndexSignaturePropertyAccess: true }], - }, - { - code: ` -interface Nested { - property: string; - [key: string]: number | string; -} - -class Dingus { - nested: Nested; -} - -let dingus: Dingus | undefined; - -dingus?.nested.property; -dingus?.nested['hello']; - `, - options: [{ allowIndexSignaturePropertyAccess: true }], - languageOptions: { parserOptions: { ecmaVersion: 2020 } }, - }, - { - code: ` -class X { - private priv_prop = 123; -} - -let x: X | undefined; -console.log(x?.['priv_prop']); - `, - options: [{ allowPrivateClassPropertyAccess: true }], - }, - { - code: ` -class X { - protected priv_prop = 123; -} - -let x: X | undefined; -console.log(x?.['priv_prop']); - `, - options: [{ allowProtectedClassPropertyAccess: true }], - }, - ], invalid: [ { code: ` @@ -161,6 +33,7 @@ class X { const x = new X(); x['priv_prop'] = 123; `, + errors: [{ messageId: 'useDot' }], options: [{ allowPrivateClassPropertyAccess: false }], output: ` class X { @@ -170,7 +43,6 @@ class X { const x = new X(); x.priv_prop = 123; `, - errors: [{ messageId: 'useDot' }], }, { code: ` @@ -181,6 +53,7 @@ class X { const x = new X(); x['pub_prop'] = 123; `, + errors: [{ messageId: 'useDot' }], output: ` class X { public pub_prop = 123; @@ -189,7 +62,6 @@ class X { const x = new X(); x.pub_prop = 123; `, - errors: [{ messageId: 'useDot' }], }, // baseRule @@ -201,69 +73,69 @@ x.pub_prop = 123; // }, { code: "a['true'];", + errors: [{ data: { key: q('true') }, messageId: 'useDot' }], output: 'a.true;', - errors: [{ messageId: 'useDot', data: { key: q('true') } }], }, { code: "a['time'];", - output: 'a.time;', + errors: [{ data: { key: '"time"' }, messageId: 'useDot' }], languageOptions: { parserOptions: { ecmaVersion: 6 } }, - errors: [{ messageId: 'useDot', data: { key: '"time"' } }], + output: 'a.time;', }, { code: 'a[null];', + errors: [{ data: { key: 'null' }, messageId: 'useDot' }], output: 'a.null;', - errors: [{ messageId: 'useDot', data: { key: 'null' } }], }, { code: 'a[true];', + errors: [{ data: { key: 'true' }, messageId: 'useDot' }], output: 'a.true;', - errors: [{ messageId: 'useDot', data: { key: 'true' } }], }, { code: 'a[false];', + errors: [{ data: { key: 'false' }, messageId: 'useDot' }], output: 'a.false;', - errors: [{ messageId: 'useDot', data: { key: 'false' } }], }, { code: "a['b'];", + errors: [{ data: { key: q('b') }, messageId: 'useDot' }], output: 'a.b;', - errors: [{ messageId: 'useDot', data: { key: q('b') } }], }, { code: "a.b['c'];", + errors: [{ data: { key: q('c') }, messageId: 'useDot' }], output: 'a.b.c;', - errors: [{ messageId: 'useDot', data: { key: q('c') } }], }, { code: "a['_dangle'];", - output: 'a._dangle;', + errors: [{ data: { key: q('_dangle') }, messageId: 'useDot' }], options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }], - errors: [{ messageId: 'useDot', data: { key: q('_dangle') } }], + output: 'a._dangle;', }, { code: "a['SHOUT_CASE'];", - output: 'a.SHOUT_CASE;', + errors: [{ data: { key: q('SHOUT_CASE') }, messageId: 'useDot' }], options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }], - errors: [{ messageId: 'useDot', data: { key: q('SHOUT_CASE') } }], + output: 'a.SHOUT_CASE;', }, { code: noFormat` a ['SHOUT_CASE']; - `, - output: ` -a - .SHOUT_CASE; `, errors: [ { - messageId: 'useDot', + column: 4, data: { key: q('SHOUT_CASE') }, line: 3, - column: 4, + messageId: 'useDot', }, ], + output: ` +a + .SHOUT_CASE; + `, }, { code: @@ -272,75 +144,75 @@ a ' ["catch"](function(){})\n' + ' .then(function(){})\n' + ' ["catch"](function(){});', - output: - 'getResource()\n' + - ' .then(function(){})\n' + - ' .catch(function(){})\n' + - ' .then(function(){})\n' + - ' .catch(function(){});', errors: [ { - messageId: 'useDot', + column: 6, data: { key: q('catch') }, line: 3, - column: 6, + messageId: 'useDot', }, { - messageId: 'useDot', + column: 6, data: { key: q('catch') }, line: 5, - column: 6, + messageId: 'useDot', }, ], + output: + 'getResource()\n' + + ' .then(function(){})\n' + + ' .catch(function(){})\n' + + ' .then(function(){})\n' + + ' .catch(function(){});', }, { code: noFormat` foo .while; `, + errors: [{ data: { key: 'while' }, messageId: 'useBrackets' }], + options: [{ allowKeywords: false }], output: ` foo ["while"]; `, - options: [{ allowKeywords: false }], - errors: [{ messageId: 'useBrackets', data: { key: 'while' } }], }, { code: "foo[/* comment */ 'bar'];", + errors: [{ data: { key: q('bar') }, messageId: 'useDot' }], output: null, // Not fixed due to comment - errors: [{ messageId: 'useDot', data: { key: q('bar') } }], }, { code: "foo['bar' /* comment */];", + errors: [{ data: { key: q('bar') }, messageId: 'useDot' }], output: null, // Not fixed due to comment - errors: [{ messageId: 'useDot', data: { key: q('bar') } }], }, { code: "foo['bar'];", + errors: [{ data: { key: q('bar') }, messageId: 'useDot' }], output: 'foo.bar;', - errors: [{ messageId: 'useDot', data: { key: q('bar') } }], }, { code: 'foo./* comment */ while;', - output: null, // Not fixed due to comment + errors: [{ data: { key: 'while' }, messageId: 'useBrackets' }], options: [{ allowKeywords: false }], - errors: [{ messageId: 'useBrackets', data: { key: 'while' } }], + output: null, // Not fixed due to comment }, { code: 'foo[null];', + errors: [{ data: { key: 'null' }, messageId: 'useDot' }], output: 'foo.null;', - errors: [{ messageId: 'useDot', data: { key: 'null' } }], }, { code: "foo['bar'] instanceof baz;", + errors: [{ data: { key: q('bar') }, messageId: 'useDot' }], output: 'foo.bar instanceof baz;', - errors: [{ messageId: 'useDot', data: { key: q('bar') } }], }, { code: 'let.if();', - output: null, // `let["if"]()` is a syntax error because `let[` indicates a destructuring variable declaration + errors: [{ data: { key: 'if' }, messageId: 'useBrackets' }], options: [{ allowKeywords: false }], - errors: [{ messageId: 'useBrackets', data: { key: 'if' } }], + output: null, // `let["if"]()` is a syntax error because `let[` indicates a destructuring variable declaration }, { code: ` @@ -351,6 +223,7 @@ class X { const x = new X(); x['protected_prop'] = 123; `, + errors: [{ messageId: 'useDot' }], options: [{ allowProtectedClassPropertyAccess: false }], output: ` class X { @@ -360,7 +233,6 @@ class X { const x = new X(); x.protected_prop = 123; `, - errors: [{ messageId: 'useDot' }], }, { code: ` @@ -372,8 +244,8 @@ class X { const x = new X(); x['prop'] = 'hello'; `, - options: [{ allowIndexSignaturePropertyAccess: true }], errors: [{ messageId: 'useDot' }], + options: [{ allowIndexSignaturePropertyAccess: true }], output: ` class X { prop: string; @@ -385,4 +257,132 @@ x.prop = 'hello'; `, }, ], + valid: [ + // baseRule + 'a.b;', + 'a.b.c;', + "a['12'];", + 'a[b];', + 'a[0];', + { code: 'a.b.c;', options: [{ allowKeywords: false }] }, + { code: 'a.arguments;', options: [{ allowKeywords: false }] }, + { code: 'a.let;', options: [{ allowKeywords: false }] }, + { code: 'a.yield;', options: [{ allowKeywords: false }] }, + { code: 'a.eval;', options: [{ allowKeywords: false }] }, + { code: 'a[0];', options: [{ allowKeywords: false }] }, + { code: "a['while'];", options: [{ allowKeywords: false }] }, + { code: "a['true'];", options: [{ allowKeywords: false }] }, + { code: "a['null'];", options: [{ allowKeywords: false }] }, + { code: 'a[true];', options: [{ allowKeywords: false }] }, + { code: 'a[null];', options: [{ allowKeywords: false }] }, + { code: 'a.true;', options: [{ allowKeywords: true }] }, + { code: 'a.null;', options: [{ allowKeywords: true }] }, + { + code: "a['snake_case'];", + options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }], + }, + { + code: "a['lots_of_snake_case'];", + options: [{ allowPattern: '^[a-z]+(_[a-z]+)+$' }], + }, + { + code: 'a[`time${range}`];', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + { + code: 'a[`while`];', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + options: [{ allowKeywords: false }], + }, + { + code: 'a[`time range`];', + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + }, + 'a.true;', + 'a.null;', + 'a[undefined];', + 'a[void 0];', + 'a[b()];', + { + code: 'a[/(?0)/];', + languageOptions: { parserOptions: { ecmaVersion: 2018 } }, + }, + + { + code: ` +class X { + private priv_prop = 123; +} + +const x = new X(); +x['priv_prop'] = 123; + `, + options: [{ allowPrivateClassPropertyAccess: true }], + }, + + { + code: ` +class X { + protected protected_prop = 123; +} + +const x = new X(); +x['protected_prop'] = 123; + `, + options: [{ allowProtectedClassPropertyAccess: true }], + }, + { + code: ` +class X { + prop: string; + [key: string]: number; +} + +const x = new X(); +x['hello'] = 3; + `, + options: [{ allowIndexSignaturePropertyAccess: true }], + }, + { + code: ` +interface Nested { + property: string; + [key: string]: number | string; +} + +class Dingus { + nested: Nested; +} + +let dingus: Dingus | undefined; + +dingus?.nested.property; +dingus?.nested['hello']; + `, + languageOptions: { parserOptions: { ecmaVersion: 2020 } }, + options: [{ allowIndexSignaturePropertyAccess: true }], + }, + { + code: ` +class X { + private priv_prop = 123; +} + +let x: X | undefined; +console.log(x?.['priv_prop']); + `, + options: [{ allowPrivateClassPropertyAccess: true }], + }, + { + code: ` +class X { + protected priv_prop = 123; +} + +let x: X | undefined; +console.log(x?.['priv_prop']); + `, + options: [{ allowProtectedClassPropertyAccess: true }], + }, + ], }); diff --git a/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts b/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts index ae80f009727b..151b7e3f4c42 100644 --- a/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts +++ b/packages/eslint-plugin/tests/rules/explicit-function-return-type.test.ts @@ -5,184 +5,395 @@ import rule from '../../src/rules/explicit-function-return-type'; const ruleTester = new RuleTester(); ruleTester.run('explicit-function-return-type', rule, { - valid: [ - 'return;', + invalid: [ { code: ` -function test(): void { +function test(a: number, b: number) { return; } `, + errors: [ + { + column: 1, + endColumn: 14, + endLine: 2, + line: 2, + messageId: 'missingReturnType', + }, + ], }, { code: ` -var fn = function (): number { +function test() { + return; +} + `, + errors: [ + { + column: 1, + endColumn: 14, + endLine: 2, + line: 2, + messageId: 'missingReturnType', + }, + ], + }, + { + code: ` +var fn = function () { return 1; }; `, + errors: [ + { + column: 10, + endColumn: 19, + endLine: 2, + line: 2, + messageId: 'missingReturnType', + }, + ], }, { code: ` -var arrowFn = (): string => 'test'; +var arrowFn = () => 'test'; `, + errors: [ + { + column: 18, + endColumn: 20, + endLine: 2, + line: 2, + messageId: 'missingReturnType', + }, + ], }, { code: ` class Test { constructor() {} - get prop(): number { + get prop() { return 1; } set prop() {} - method(): void { + method() { + return; + } + arrow = () => 'arrow'; + private method() { return; } - arrow = (): string => 'arrow'; } `, - }, - { - code: 'fn(() => {});', - options: [ + errors: [ { - allowExpressions: true, + column: 3, + endColumn: 11, + endLine: 4, + line: 4, + messageId: 'missingReturnType', + }, + { + column: 3, + endColumn: 9, + endLine: 8, + line: 8, + messageId: 'missingReturnType', + }, + { + column: 3, + endColumn: 11, + endLine: 11, + line: 11, + messageId: 'missingReturnType', + }, + { + column: 3, + endColumn: 17, + endLine: 12, + line: 12, + messageId: 'missingReturnType', }, ], }, { - code: 'fn(function () {});', - options: [ + code: ` +function test() { + return; +} + `, + errors: [ { - allowExpressions: true, + column: 1, + endColumn: 14, + endLine: 2, + line: 2, + messageId: 'missingReturnType', }, ], + options: [{ allowExpressions: true }], }, { - code: '[function () {}, () => {}];', - options: [ + code: 'const foo = () => {};', + errors: [ { - allowExpressions: true, + column: 16, + endColumn: 18, + endLine: 1, + line: 1, + messageId: 'missingReturnType', }, ], + options: [{ allowExpressions: true }], }, { - code: '(function () {});', - options: [ + code: 'const foo = function () {};', + errors: [ { - allowExpressions: true, + column: 13, + endColumn: 22, + endLine: 1, + line: 1, + messageId: 'missingReturnType', }, ], + options: [{ allowExpressions: true }], }, { - code: '(() => {})();', - options: [ + code: 'export default () => {};', + errors: [ { - allowExpressions: true, + column: 19, + endColumn: 21, + endLine: 1, + line: 1, + messageId: 'missingReturnType', }, ], + options: [{ allowExpressions: true }], }, { - code: 'export default (): void => {};', - options: [ + code: 'export default function () {}', + errors: [ { - allowExpressions: true, + column: 16, + endColumn: 25, + endLine: 1, + line: 1, + messageId: 'missingReturnType', }, ], + options: [{ allowExpressions: true }], }, { code: ` -var arrowFn: Foo = () => 'test'; +class Foo { + public a = () => {}; + public b = function () {}; + public c = function test() {}; + + static d = () => {}; + static e = function () {}; +} `, - options: [ + errors: [ { - allowTypedFunctionExpressions: true, + column: 3, + endColumn: 14, + endLine: 3, + line: 3, + messageId: 'missingReturnType', + }, + { + column: 3, + endColumn: 23, + endLine: 4, + line: 4, + messageId: 'missingReturnType', + }, + { + column: 3, + endColumn: 27, + endLine: 5, + line: 5, + messageId: 'missingReturnType', + }, + { + column: 3, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'missingReturnType', + }, + { + column: 3, + endColumn: 23, + endLine: 8, + line: 8, + messageId: 'missingReturnType', + }, + ], + options: [{ allowExpressions: true }], + }, + { + code: "var arrowFn = () => 'test';", + errors: [ + { + column: 18, + endColumn: 20, + endLine: 1, + line: 1, + messageId: 'missingReturnType', }, ], + options: [{ allowTypedFunctionExpressions: true }], }, { code: ` -var funcExpr: Foo = function () { - return 'test'; -}; +function foo(): any { + const bar = () => () => console.log('aa'); +} `, + errors: [ + { + column: 24, + endColumn: 26, + endLine: 3, + line: 3, + messageId: 'missingReturnType', + }, + ], options: [ { allowTypedFunctionExpressions: true, }, ], }, - { - code: 'const x = (() => {}) as Foo;', - options: [{ allowTypedFunctionExpressions: true }], - }, - { - code: 'const x = (() => {});', - options: [{ allowTypedFunctionExpressions: true }], - }, { code: ` -const x = { - foo: () => {}, -} as Foo; +let anyValue: any; +function foo(): any { + anyValue = () => () => console.log('aa'); +} `, - options: [{ allowTypedFunctionExpressions: true }], + errors: [ + { + column: 23, + endColumn: 25, + endLine: 4, + line: 4, + messageId: 'missingReturnType', + }, + ], + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], }, { code: ` -const x = { - foo: () => {}, -}; +class Foo { + foo(): any { + const bar = () => () => { + return console.log('foo'); + }; + } +} `, - options: [{ allowTypedFunctionExpressions: true }], + errors: [ + { + column: 26, + endColumn: 28, + endLine: 4, + line: 4, + messageId: 'missingReturnType', + }, + ], + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], }, { code: ` -const x: Foo = { - foo: () => {}, +var funcExpr = function () { + return 'test'; }; `, + errors: [ + { + column: 16, + endColumn: 25, + endLine: 2, + line: 2, + messageId: 'missingReturnType', + }, + ], options: [{ allowTypedFunctionExpressions: true }], }, - // https://github.com/typescript-eslint/typescript-eslint/issues/2864 + { - code: ` -const x = { - foo: { bar: () => {} }, -} as Foo; - `, - options: [{ allowTypedFunctionExpressions: true }], + code: 'const x = (() => {}) as Foo;', + errors: [ + { + column: 15, + endColumn: 17, + endLine: 1, + line: 1, + messageId: 'missingReturnType', + }, + ], + options: [{ allowTypedFunctionExpressions: false }], }, { code: ` -const x = { - foo: { bar: () => {} }, -}; +interface Foo {} +const x = { + foo: () => {}, +} as Foo; `, - options: [{ allowTypedFunctionExpressions: true }], + errors: [ + { + column: 3, + endColumn: 8, + endLine: 4, + line: 4, + messageId: 'missingReturnType', + }, + ], + options: [{ allowTypedFunctionExpressions: false }], }, { code: ` +interface Foo {} const x: Foo = { - foo: { bar: () => {} }, + foo: () => {}, }; `, - options: [{ allowTypedFunctionExpressions: true }], - }, - // https://github.com/typescript-eslint/typescript-eslint/issues/484 - { - code: ` -type MethodType = () => void; - -class App { - private method: MethodType = () => {}; -} - `, - options: [{ allowTypedFunctionExpressions: true }], + errors: [ + { + column: 3, + endColumn: 8, + endLine: 4, + line: 4, + messageId: 'missingReturnType', + }, + ], + options: [{ allowTypedFunctionExpressions: false }], }, - // https://github.com/typescript-eslint/typescript-eslint/issues/7552 { code: 'const foo =