From 64b7c9b06c600d49cea3f507deb1471b4c443076 Mon Sep 17 00:00:00 2001 From: Zzzen Date: Sat, 14 Aug 2021 20:14:37 +0800 Subject: [PATCH 1/3] feat(eslint-plugin): migrate no-unnecessary-generics from dtslint --- .../src/rules/no-unnecessary-generics.ts | 145 ++++++++++++++++++ .../rules/no-unnecessary-generics.test.ts | 135 ++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/no-unnecessary-generics.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unnecessary-generics.test.ts diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts b/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts new file mode 100644 index 000000000000..9b2883093b3f --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts @@ -0,0 +1,145 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; +import * as ts from 'typescript'; + +export default util.createRule({ + name: 'no-unnecessary-generics', + meta: { + docs: { + description: 'Forbids signatures using a generic parameter only once', + category: 'Best Practices', + recommended: false, + }, + messages: { + failureString: 'Type parameter {{typeParameter}} is used only once.', + failureStringNever: 'Type parameter {{typeParameter}} is never used', + }, + schema: [], + type: 'suggestion', + }, + defaultOptions: [], + create(context) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + return { + 'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, TSCallSignatureDeclaration, TSMethodSignature, TSFunctionType, TSConstructorType, TSDeclareFunction'( + node: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression + | TSESTree.TSCallSignatureDeclaration + | TSESTree.TSMethodSignature + | TSESTree.TSFunctionType + | TSESTree.TSConstructorType + | TSESTree.TSDeclareFunction, + ): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + if (tsNode && ts.isFunctionLike(tsNode)) { + checkSignature(tsNode); + } + + // copied from https://github.com/microsoft/dtslint/blob/88b6f8975bb3ae9091c7150ddd52a11732324009/src/rules/noUnnecessaryGenericsRule.ts#L42 + function checkSignature(sig: ts.SignatureDeclaration): void { + if (!sig.typeParameters) { + return; + } + + for (const tp of sig.typeParameters) { + const typeParameter = tp.name.text; + const res = getSoleUse( + sig, + assertDefined(checker.getSymbolAtLocation(tp.name)), + checker, + ); + switch (res.type) { + case 'ok': + break; + case 'sole': + context.report({ + node: parserServices.tsNodeToESTreeNodeMap.get(res.soleUse), + messageId: 'failureString', + data: { + typeParameter, + }, + }); + break; + case 'never': + context.report({ + node: parserServices.tsNodeToESTreeNodeMap.get(tp), + messageId: 'failureString', + data: { + typeParameter, + }, + }); + break; + default: + assertNever(res); + } + } + } + }, + }; + }, +}); + +type Result = + | { type: 'ok' | 'never' } + | { type: 'sole'; soleUse: ts.Identifier }; +function getSoleUse( + sig: ts.SignatureDeclaration, + typeParameterSymbol: ts.Symbol, + checker: ts.TypeChecker, +): Result { + const exit = {}; + let soleUse: ts.Identifier | undefined; + + try { + if (sig.typeParameters) { + for (const tp of sig.typeParameters) { + if (tp.constraint) { + recur(tp.constraint); + } + } + } + for (const param of sig.parameters) { + if (param.type) { + recur(param.type); + } + } + if (sig.type) { + recur(sig.type); + } + } catch (err) { + if (err === exit) { + return { type: 'ok' }; + } + throw err; + } + + return soleUse ? { type: 'sole', soleUse } : { type: 'never' }; + + function recur(node: ts.TypeNode): void { + if (ts.isIdentifier(node)) { + if (checker.getSymbolAtLocation(node) === typeParameterSymbol) { + if (soleUse === undefined) { + soleUse = node; + } else { + throw exit; + } + } + } else { + node.forEachChild(recur as (node: ts.Node) => void); + } + } +} + +function assertDefined(value: T | undefined): T { + if (value === undefined) { + throw new Error('unreachable'); + } + return value; +} +function assertNever(_: never): never { + throw new Error('unreachable'); +} diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-generics.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-generics.test.ts new file mode 100644 index 000000000000..bf57a5b36cc8 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-generics.test.ts @@ -0,0 +1,135 @@ +import rule from '../../src/rules/no-unnecessary-generics'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('no-unnecessary-generics', rule, { + valid: [ + ` +// Uses type parameter twice +function foo(m: Map): void {} + `, + ` +// 'T appears in a constraint, so it appears twice. +function f(t: T, u: U): U; + `, + ], + + invalid: [ + { + code: ` +interface I { + (value: T): void; + m(x: T): void; +} + `, + errors: [ + { + messageId: 'failureString', + line: 3, + column: 14, + }, + { + messageId: 'failureString', + line: 4, + column: 11, + }, + ], + }, + { + code: ` +class C { + constructor(x: T) {} +} + `, + errors: [ + { + messageId: 'failureString', + line: 3, + column: 21, + }, + ], + }, + { + code: ` +type Fn = () => T; + `, + errors: [ + { + messageId: 'failureString', + line: 2, + column: 20, + }, + ], + }, + { + code: ` +type Ctr = new () => T; + `, + errors: [ + { + messageId: 'failureString', + line: 2, + column: 25, + }, + ], + }, + { + code: ` +function f(): T {} + `, + errors: [ + { + messageId: 'failureString', + line: 2, + column: 18, + }, + ], + }, + { + code: ` +const f2 = (): T => {}; + `, + errors: [ + { + messageId: 'failureString', + line: 2, + column: 19, + }, + ], + }, + { + code: ` +function f(x: { T: number }): void; + `, + errors: [ + { + messageId: 'failureString', + line: 2, + column: 12, + }, + ], + }, + { + code: ` +function f(u: U): U; + `, + errors: [ + { + messageId: 'failureString', + line: 2, + column: 25, + }, + ], + }, + ], +}); From 52780d0376095a29a2c5b52b9de602c3a5cc9034 Mon Sep 17 00:00:00 2001 From: Zzzen Date: Thu, 19 Aug 2021 12:21:19 +0800 Subject: [PATCH 2/3] feat(eslint-plugin): try to check inferred return type --- packages/eslint-plugin/src/rules/index.ts | 2 ++ .../src/rules/no-unnecessary-generics.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 1b6fa0f6ea12..787eb42c3c81 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -64,6 +64,7 @@ import noThrowLiteral from './no-throw-literal'; import noTypeAlias from './no-type-alias'; import noUnnecessaryBooleanLiteralCompare from './no-unnecessary-boolean-literal-compare'; import noUnnecessaryCondition from './no-unnecessary-condition'; +import noUnnecessaryGenerics from './no-unnecessary-generics'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; @@ -183,6 +184,7 @@ export default { 'no-type-alias': noTypeAlias, 'no-unnecessary-boolean-literal-compare': noUnnecessaryBooleanLiteralCompare, 'no-unnecessary-condition': noUnnecessaryCondition, + 'no-unnecessary-generics': noUnnecessaryGenerics, 'no-unnecessary-qualifier': noUnnecessaryQualifier, 'no-unnecessary-type-arguments': noUnnecessaryTypeArguments, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts b/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts index 9b2883093b3f..befd693cbcd2 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts @@ -109,6 +109,18 @@ function getSoleUse( } if (sig.type) { recur(sig.type); + } else if (sig.kind !== ts.SyntaxKind.Constructor && 'body' in sig) { + // if body is missing, there is no point inferring return type. + const sigType = checker.getSignatureFromDeclaration(sig); + if (!sigType) { + return { type: 'ok' }; + } + + const returnType = checker.getReturnTypeOfSignature(sigType); + // TODO: is it possiable to check if a TypeParameter is used in a type? + if (returnType.flags & ts.TypeFlags.StructuredOrInstantiable) { + return { type: 'ok' }; + } } } catch (err) { if (err === exit) { From e320d462a5e9caa0a5a69a2e4f1a28ff717bb550 Mon Sep 17 00:00:00 2001 From: Zzzen Date: Sat, 25 Sep 2021 00:25:59 +0800 Subject: [PATCH 3/3] feat(eslint-plugin): try to check inferred return type --- .../src/rules/no-unnecessary-generics.ts | 182 ++++++++++++++++-- .../rules/no-unnecessary-generics.test.ts | 96 +++++++++ 2 files changed, 266 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts b/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts index befd693cbcd2..3e04a7936c03 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-generics.ts @@ -24,15 +24,16 @@ export default util.createRule({ return { 'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, TSCallSignatureDeclaration, TSMethodSignature, TSFunctionType, TSConstructorType, TSDeclareFunction'( - node: - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.ArrowFunctionExpression - | TSESTree.TSCallSignatureDeclaration - | TSESTree.TSMethodSignature - | TSESTree.TSFunctionType - | TSESTree.TSConstructorType - | TSESTree.TSDeclareFunction, + node: // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + // | TSESTree.FunctionDeclaration + // | TSESTree.FunctionExpression + // | TSESTree.ArrowFunctionExpression + // | TSESTree.TSCallSignatureDeclaration + // | TSESTree.TSMethodSignature + // | TSESTree.TSFunctionType + // | TSESTree.TSConstructorType + // | TSESTree.TSDeclareFunction, ): void { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); if (tsNode && ts.isFunctionLike(tsNode)) { @@ -93,6 +94,7 @@ function getSoleUse( ): Result { const exit = {}; let soleUse: ts.Identifier | undefined; + const seenTypes = new Set(); try { if (sig.typeParameters) { @@ -117,9 +119,22 @@ function getSoleUse( } const returnType = checker.getReturnTypeOfSignature(sigType); - // TODO: is it possiable to check if a TypeParameter is used in a type? - if (returnType.flags & ts.TypeFlags.StructuredOrInstantiable) { - return { type: 'ok' }; + try { + forEachType( + returnType, + type => { + if (type.getSymbol() === typeParameterSymbol) { + throw exit; + } + }, + checker, + ); + } catch (error) { + if (error === exit) { + return { type: 'ok' }; + } else { + throw error; + } } } } catch (err) { @@ -144,6 +159,149 @@ function getSoleUse( node.forEachChild(recur as (node: ts.Node) => void); } } + + function forEachType( + type: ts.Type, + cb: (type: ts.Type) => void, + checker: ts.TypeChecker, + ): void { + // prevent infinite recursion + if (seenTypes.has(type)) { + return; + } else { + seenTypes.add(type); + } + + cb(type); + + // T | P + if (type.flags & ts.TypeFlags.UnionOrIntersection) { + const types = (type as ts.UnionOrIntersectionType).types; + types.forEach(type => { + forEachType(type, cb, checker); + }); + return; + } + + // a simple type + if (!(type.flags & ts.TypeFlags.StructuredOrInstantiable)) { + return; + } + + // keyof T + if (type.flags & ts.TypeFlags.Index) { + forEachType((type as ts.IndexType).type, cb, checker); + return; + } + + // Indexed access types (TypeFlags.IndexedAccess) + // Possible forms are T[xxx], xxx[T], or xxx[keyof T], where T is a type variable + if (type.flags & ts.TypeFlags.IndexedAccess) { + forEachType((type as ts.IndexedAccessType).objectType, cb, checker); + forEachType((type as ts.IndexedAccessType).indexType, cb, checker); + return; + } + + // TODO: true type and false type are not exposed. https://github.com/microsoft/TypeScript/issues/45537 + if (type.flags & ts.TypeFlags.Conditional) { + throw exit; + } + + // TODO: MappedType is not exposed. + if (getObjectFlags(type) & ts.ObjectFlags.Mapped) { + throw exit; + } + + if (type.flags & ts.TypeFlags.TemplateLiteral) { + (type as ts.TemplateLiteralType).types.forEach(type => + forEachType(type, cb, checker), + ); + return; + } + + if (type.flags & ts.TypeFlags.Object) { + type.aliasTypeArguments?.forEach(arg => { + forEachType(arg, cb, checker); + }); + + const properties = checker.getPropertiesOfType(type); + properties.forEach(prop => { + const declType = checker.getTypeOfSymbolAtLocation(prop, sig); + forEachType(declType, cb, checker); + }); + + const visitSignature = (signature: ts.Signature): void => { + signature.parameters.forEach(param => { + const decl = param.getDeclarations()?.[0]; + if (decl) { + const declType = checker.getTypeOfSymbolAtLocation(param, decl); + forEachType(declType, cb, checker); + } + }); + + signature.getTypeParameters()?.forEach(typeParam => { + const decl = typeParam.symbol.getDeclarations()?.[0]; + if (decl?.kind === ts.SyntaxKind.TypeParameter) { + const constrain = (decl as ts.TypeParameterDeclaration).constraint; + if (constrain) { + forEachType(checker.getTypeFromTypeNode(constrain), cb, checker); + } + } + }); + + forEachType(signature.getReturnType(), cb, checker); + + // const predicate = checker.getTypePredicateOfSignature(); + const decl = signature?.getDeclaration(); + if (decl.type?.kind === ts.SyntaxKind.TypePredicate) { + const typeNode = (decl.type as ts.TypePredicateNode).type; + if (typeNode) { + forEachType(checker.getTypeAtLocation(typeNode), cb, checker); + } + } + }; + + const calls = type.getCallSignatures(); + calls.forEach(signature => { + visitSignature(signature); + }); + const constructors = type.getConstructSignatures(); + constructors.forEach(signature => { + visitSignature(signature); + }); + + // TODO: `checker.getIndexInfosOfType()` might be better + const numberIndexType = type.getNumberIndexType(); + if (numberIndexType) { + forEachType(numberIndexType, cb, checker); + } + const stringIndexType = type.getStringIndexType(); + if (stringIndexType) { + forEachType(stringIndexType, cb, checker); + } + + return; + } + + // TODO: unknown type, abort? + throw exit; + } + + // internal of ts compiler + + function getObjectFlags(type: ts.Type): ts.ObjectFlags { + const ObjectFlagsType = + ts.TypeFlags.Any | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.Never | + ts.TypeFlags.Object | + ts.TypeFlags.Union | + ts.TypeFlags.Intersection; + return type.flags & ObjectFlagsType + ? (type as ts.ObjectType).objectFlags + : 0; + } } function assertDefined(value: T | undefined): T { diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-generics.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-generics.test.ts index bf57a5b36cc8..28acda2be510 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-generics.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-generics.test.ts @@ -22,6 +22,90 @@ function foo(m: Map): void {} // 'T appears in a constraint, so it appears twice. function f(t: T, u: U): U; `, + ` +function f(t: T) { + return t; +} + `, + ` +function f(t: T) { + return null as { t: T }; +} + `, + ` +function f(t: T) { + return null as T | null; +} + `, + ` +function f(t: T) { + return null as (t: T) => void; +} + `, + ` +function f(t: T) { + return (t: any): t is T => { + return true; + }; +} + `, + ` +function f(t: T) { + return (t: any): t is () => T => { + return true; + }; +} + `, + ` +function f(t: T) { + return null as { [k in keyof T]: string }; +} + `, + ` +function f() { + return null as T[P]; +} + `, + ` +// not supported +function f() { + return null as A extends B ? 1 : 2; +} + `, + ` +function f() { + return null as A extends B ? string : number; +} +`, + ` +function f() { + return null as [A, B]; +} +`, + ` +function f() { + return [] as A[]; +} +`, + ` +function f(t: T) { + return null as Record; +} + `, + ` +function f1(a: T) { + return class Foo extends a { + f = 1; + }; +} + `, + ` +function f1(a: T) { + return class Foo extends a { + f = 1; + }; +} + `, ], invalid: [ @@ -131,5 +215,17 @@ function f(u: U): U; }, ], }, + { + code: ` +function f(t: T) {} + `, + errors: [ + { + messageId: 'failureString', + line: 2, + column: 18, + }, + ], + }, ], });