From fbee59610c44572678eb72c2c7fa139e8b21f2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Huy=20Dang=20L=C3=AA-Ng=C3=B4?= Date: Wed, 22 May 2019 02:47:01 -0700 Subject: [PATCH 1/2] feat(eslint-plugin): [no-explicit-any] ignoreRestArgs (#397) --- .../docs/rules/no-explicit-any.md | 17 ++++++ .../src/rules/no-explicit-any.ts | 61 ++++++++++++++++++- .../tests/rules/no-explicit-any.test.ts | 24 ++++++++ 3 files changed, 99 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 2439f43c2f3f..3eb10ae9698c 100644 --- a/packages/eslint-plugin/docs/rules/no-explicit-any.md +++ b/packages/eslint-plugin/docs/rules/no-explicit-any.md @@ -87,6 +87,23 @@ function greet(param: Array): string {} function greet(param: Array): Array {} ``` +### Options + +This rule accepts a single object option with the following default configuration: + +```json +{ + "@typescript-eslint/no-explicit-any": [ + "error", + { + "ignoreRestArgs": false + } + ] +} +``` + +- `ignoreRestArgs: true` will disallow usages of `any` as a type declaration except rest spread parameters. + ## 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 541b0acb1df1..d2bbbb54ea39 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,63 @@ 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 parent is a function declaration + * @param node the node to be validated. + * @returns true if the node parent is a function declaration + * @private + */ + function isParentFunctionDeclaration(node: TSESTree.Node): boolean { + return ( + typeof node.parent !== 'undefined' && + node.parent.type === AST_NODE_TYPES.FunctionDeclaration + ); + } + + /** + * Checks if the node great granparent is a rest element + * @param node the node to be validated. + * @returns true if the node great granparent is a rest element + * @private + */ + function isGreatGrandparentRestElementInFunctionDeclaration( + node: TSESTree.Node, + ): boolean { + return ( + typeof node.parent !== 'undefined' && + typeof node.parent.parent !== 'undefined' && + typeof node.parent.parent.parent !== 'undefined' && + node.parent.parent.parent.type === AST_NODE_TYPES.RestElement && + isParentFunctionDeclaration(node.parent.parent.parent) + ); + } + return { TSAnyKeyword(node) { + if ( + ignoreRestArgs && + isGreatGrandparentRestElementInFunctionDeclaration(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 d892a310103c..897ea13617fc 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,15 @@ 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 }], + }, ], invalid: [ { @@ -679,5 +688,20 @@ 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, + }, + ], + }, ], }); From 57587c00748fc8a7ca50b2da0635656b761e5178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Huy=20Dang=20L=C3=AA-Ng=C3=B4?= Date: Wed, 22 May 2019 02:47:01 -0700 Subject: [PATCH 2/2] fix(eslint-plugin): [no-explicit-any] Add more test cases --- .../docs/rules/no-explicit-any.md | 53 +++++++-- .../src/rules/no-explicit-any.ts | 112 +++++++++++++++--- .../tests/rules/no-explicit-any.test.ts | 81 +++++++++++++ 3 files changed, 219 insertions(+), 27 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-explicit-any.md b/packages/eslint-plugin/docs/rules/no-explicit-any.md index 3eb10ae9698c..56cafcae79b2 100644 --- a/packages/eslint-plugin/docs/rules/no-explicit-any.md +++ b/packages/eslint-plugin/docs/rules/no-explicit-any.md @@ -87,22 +87,51 @@ function greet(param: Array): string {} function greet(param: Array): Array {} ``` -### Options +### ignoreRestArgs -This rule accepts a single object option with the following default configuration: +A boolean to specify if arrays from the rest operator are considered okay. `false` by default. -```json -{ - "@typescript-eslint/no-explicit-any": [ - "error", - { - "ignoreRestArgs": false - } - ] -} +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) {} ``` -- `ignoreRestArgs: true` will disallow usages of `any` as a type declaration except rest spread parameters. +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 diff --git a/packages/eslint-plugin/src/rules/no-explicit-any.ts b/packages/eslint-plugin/src/rules/no-explicit-any.ts index d2bbbb54ea39..c27b4ab59923 100644 --- a/packages/eslint-plugin/src/rules/no-explicit-any.ts +++ b/packages/eslint-plugin/src/rules/no-explicit-any.ts @@ -35,42 +35,124 @@ export default util.createRule({ ], create(context, [{ ignoreRestArgs }]) { /** - * Checks if the node parent is a function declaration + * 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 parent is a function declaration + * @returns true if the node is an arrow function, function declaration or function expression * @private */ - function isParentFunctionDeclaration(node: TSESTree.Node): boolean { + 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' && - node.parent.type === AST_NODE_TYPES.FunctionDeclaration + isNodeValidFunction(node.parent) ); } /** - * Checks if the node great granparent is a rest element + * Checks if the node is a TSTypeOperator node with a readonly operator * @param node the node to be validated. - * @returns true if the node great granparent is a rest element + * @returns true if the node is a TSTypeOperator node with a readonly operator * @private */ - function isGreatGrandparentRestElementInFunctionDeclaration( - node: TSESTree.Node, - ): boolean { + 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' && - node.parent.parent.parent.type === AST_NODE_TYPES.RestElement && - isParentFunctionDeclaration(node.parent.parent.parent) + 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 && - isGreatGrandparentRestElementInFunctionDeclaration(node) - ) { + if (ignoreRestArgs && isNodeDescendantOfRestElementInFunction(node)) { return; } context.report({ 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 897ea13617fc..cf596ffecbc3 100644 --- a/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts +++ b/packages/eslint-plugin/tests/rules/no-explicit-any.test.ts @@ -138,6 +138,54 @@ type obj = { `, 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: [ { @@ -703,5 +751,38 @@ type obj = { }, ], }, + { + 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, + }, + ], + }, ], });