From 753ad75d2bc292074a0d149002d3018238ba3d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Huy=20Dang=20L=C3=AA-Ng=C3=B4?= Date: Sat, 15 Jun 2019 19:03:35 -0700 Subject: [PATCH] feat(eslint-plugin): [no-explicit-any] ignoreRestArgs (#548) --- .../docs/rules/no-explicit-any.md | 46 ++++++ .../src/rules/no-explicit-any.ts | 143 +++++++++++++++++- .../tests/rules/no-explicit-any.test.ts | 105 +++++++++++++ 3 files changed, 291 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-explicit-any.md b/packages/eslint-plugin/docs/rules/no-explicit-any.md index 2439f43c2f3..56cafcae79b 100644 --- a/packages/eslint-plugin/docs/rules/no-explicit-any.md +++ b/packages/eslint-plugin/docs/rules/no-explicit-any.md @@ -87,6 +87,52 @@ function greet(param: Array): string {} function greet(param: Array): Array {} ``` +### ignoreRestArgs + +A boolean to specify if arrays from the rest operator are considered okay. `false` by default. + +Examples of **incorrect** code for the `{ "ignoreRestArgs": false }` option: + +```ts +/*eslint @typescript-eslint/no-explicit-any: ["error", { "ignoreRestArgs": false }]*/ + +function foo1(...args: any[]): void {} +function foo2(...args: readonly any[]): void {} +function foo3(...args: Array): void {} +function foo4(...args: ReadonlyArray): void {} + +const bar1 = (...args: any[]): void {} +const bar2 = (...args: readonly any[]): void {} +const bar3 = (...args: Array): void {} +const bar4 = (...args: ReadonlyArray): void {} + +const baz1 = function (...args: any[]) {} +const baz2 = function (...args: readonly any[]) {} +const baz3 = function (...args: Array) {} +const baz4 = function (...args: ReadonlyArray) {} +``` + +Examples of **correct** code for the `{ "ignoreRestArgs": true }` option: + +```ts +/*eslint @typescript-eslint/no-explicit-any: ["error", { "ignoreRestArgs": true }]*/ + +function foo1(...args: any[]): void {} +function foo2(...args: readonly any[]): void {} +function foo3(...args: Array): void {} +function foo4(...args: ReadonlyArray): void {} + +const bar1 = (...args: any[]): void {} +const bar2 = (...args: readonly any[]): void {} +const bar3 = (...args: Array): void {} +const bar4 = (...args: ReadonlyArray): void {} + +const baz1 = function (...args: any[]) {} +const baz2 = function (...args: readonly any[]) {} +const baz3 = function (...args: Array) {} +const baz4 = function (...args: ReadonlyArray) {} +``` + ## When Not To Use It If an unknown type or a library without typings is used diff --git a/packages/eslint-plugin/src/rules/no-explicit-any.ts b/packages/eslint-plugin/src/rules/no-explicit-any.ts index 541b0acb1df..c27b4ab5992 100644 --- a/packages/eslint-plugin/src/rules/no-explicit-any.ts +++ b/packages/eslint-plugin/src/rules/no-explicit-any.ts @@ -1,3 +1,7 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; import * as util from '../util'; export default util.createRule({ @@ -12,12 +16,145 @@ export default util.createRule({ messages: { unexpectedAny: 'Unexpected any. Specify a different type.', }, - schema: [], + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + ignoreRestArgs: { + type: 'boolean', + }, + }, + }, + ], }, - defaultOptions: [], - create(context) { + defaultOptions: [ + { + ignoreRestArgs: false, + }, + ], + create(context, [{ ignoreRestArgs }]) { + /** + * Checks if the node is an arrow function, function declaration or function expression + * @param node the node to be validated. + * @returns true if the node is an arrow function, function declaration or function expression + * @private + */ + function isNodeValidFunction(node: TSESTree.Node): boolean { + return [ + AST_NODE_TYPES.ArrowFunctionExpression, + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.FunctionExpression, + ].includes(node.type); + } + + /** + * Checks if the node is a rest element child node of a function + * @param node the node to be validated. + * @returns true if the node is a rest element child node of a function + * @private + */ + function isNodeRestElementInFunction(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.RestElement && + typeof node.parent !== 'undefined' && + isNodeValidFunction(node.parent) + ); + } + + /** + * Checks if the node is a TSTypeOperator node with a readonly operator + * @param node the node to be validated. + * @returns true if the node is a TSTypeOperator node with a readonly operator + * @private + */ + function isNodeReadonlyTSTypeOperator(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.TSTypeOperator && + node.operator === 'readonly' + ); + } + + /** + * Checks if the node is a TSTypeReference node with an Array identifier + * @param node the node to be validated. + * @returns true if the node is a TSTypeReference node with an Array identifier + * @private + */ + function isNodeValidArrayTSTypeReference(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.TSTypeReference && + typeof node.typeName !== 'undefined' && + node.typeName.type === AST_NODE_TYPES.Identifier && + ['Array', 'ReadonlyArray'].includes(node.typeName.name) + ); + } + + /** + * Checks if the node is a valid TSTypeOperator or TSTypeReference node + * @param node the node to be validated. + * @returns true if the node is a valid TSTypeOperator or TSTypeReference node + * @private + */ + function isNodeValidTSType(node: TSESTree.Node): boolean { + return ( + isNodeReadonlyTSTypeOperator(node) || + isNodeValidArrayTSTypeReference(node) + ); + } + + /** + * Checks if the great grand-parent node is a RestElement node in a function + * @param node the node to be validated. + * @returns true if the great grand-parent node is a RestElement node in a function + * @private + */ + function isGreatGrandparentRestElement(node: TSESTree.Node): boolean { + return ( + typeof node.parent !== 'undefined' && + typeof node.parent.parent !== 'undefined' && + typeof node.parent.parent.parent !== 'undefined' && + isNodeRestElementInFunction(node.parent.parent.parent) + ); + } + + /** + * Checks if the great great grand-parent node is a valid RestElement node in a function + * @param node the node to be validated. + * @returns true if the great great grand-parent node is a valid RestElement node in a function + * @private + */ + function isGreatGreatGrandparentRestElement(node: TSESTree.Node): boolean { + return ( + typeof node.parent !== 'undefined' && + typeof node.parent.parent !== 'undefined' && + isNodeValidTSType(node.parent.parent) && + typeof node.parent.parent.parent !== 'undefined' && + typeof node.parent.parent.parent.parent !== 'undefined' && + isNodeRestElementInFunction(node.parent.parent.parent.parent) + ); + } + + /** + * Checks if the great grand-parent or the great great grand-parent node is a RestElement node + * @param node the node to be validated. + * @returns true if the great grand-parent or the great great grand-parent node is a RestElement node + * @private + */ + function isNodeDescendantOfRestElementInFunction( + node: TSESTree.Node, + ): boolean { + return ( + isGreatGrandparentRestElement(node) || + isGreatGreatGrandparentRestElement(node) + ); + } + return { TSAnyKeyword(node) { + if (ignoreRestArgs && isNodeDescendantOfRestElementInFunction(node)) { + return; + } context.report({ node, messageId: 'unexpectedAny', diff --git a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts index d892a310103..cf596ffecbc 100644 --- a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts +++ b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts @@ -129,6 +129,63 @@ type obj = { message: string & Array>; } `, + // https://github.com/eslint/typescript-eslint-parser/issues/397 + { + code: ` + function foo(a: number, ...rest: any[]): void { + return; + } + `, + options: [{ ignoreRestArgs: true }], + }, + { + code: `function foo1(...args: any[]) {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `const bar1 = function (...args: any[]) {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `const baz1 = (...args: any[]) => {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `function foo2(...args: readonly any[]) {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `const bar2 = function (...args: readonly any[]) {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `const baz2 = (...args: readonly any[]) => {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `function foo3(...args: Array) {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `const bar3 = function (...args: Array) {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `const baz3 = (...args: Array) => {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `function foo4(...args: ReadonlyArray) {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `const bar4 = function (...args: ReadonlyArray) {}`, + options: [{ ignoreRestArgs: true }], + }, + { + code: `const baz4 = (...args: ReadonlyArray) => {}`, + options: [{ ignoreRestArgs: true }], + }, ], invalid: [ { @@ -679,5 +736,53 @@ type obj = { }, ], }, + { + // https://github.com/eslint/typescript-eslint-parser/issues/397 + code: ` + function foo(a: number, ...rest: any[]): void { + return; + } + `, + errors: [ + { + messageId: 'unexpectedAny', + line: 2, + column: 42, + }, + ], + }, + { + code: `function foo5(...args: any) {}`, + options: [{ ignoreRestArgs: true }], + errors: [ + { + messageId: 'unexpectedAny', + line: 1, + column: 24, + }, + ], + }, + { + code: `const bar5 = function (...args: any) {}`, + options: [{ ignoreRestArgs: true }], + errors: [ + { + messageId: 'unexpectedAny', + line: 1, + column: 33, + }, + ], + }, + { + code: `const baz5 = (...args: any) => {}`, + options: [{ ignoreRestArgs: true }], + errors: [ + { + messageId: 'unexpectedAny', + line: 1, + column: 24, + }, + ], + }, ], });