From 6ffaa0b02a56efce3f62e1dd8f9d9ec5478a00fd Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Tue, 19 Mar 2019 17:01:04 -0700 Subject: [PATCH] feat(eslint-plugin): Add unified-signature rule (#178) --- packages/eslint-plugin/README.md | 1 + packages/eslint-plugin/ROADMAP.md | 3 +- .../docs/rules/unified-signatures.md | 33 + .../src/rules/unified-signatures.ts | 576 +++++++++++++++++ packages/eslint-plugin/src/util/misc.ts | 17 + .../tests/rules/unified-signatures.test.ts | 595 ++++++++++++++++++ packages/eslint-plugin/typings/ts-eslint.d.ts | 10 + 7 files changed, 1234 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-plugin/docs/rules/unified-signatures.md create mode 100644 packages/eslint-plugin/src/rules/unified-signatures.ts create mode 100644 packages/eslint-plugin/tests/rules/unified-signatures.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 10268b00635..ba235a7bc63 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -152,5 +152,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | :heavy_check_mark: | | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string. (`restrict-plus-operands` from TSLint) | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | :heavy_check_mark: | :wrench: | +| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one. (`unified-signatures` from TSLint) | | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index e10dc03292a..11ccb8ac22b 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -34,7 +34,7 @@ | [`promise-function-async`] | ✅ | [`@typescript-eslint/promise-function-async`] | | [`typedef`] | 🛑 | N/A | | [`typedef-whitespace`] | ✅ | [`@typescript-eslint/type-annotation-spacing`] | -| [`unified-signatures`] | 🛑 | N/A | +| [`unified-signatures`] | ✅ | [`@typescript-eslint/unified-signatures`] | ### Functionality @@ -589,6 +589,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/no-unnecessary-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.md [`@typescript-eslint/no-var-requires`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-var-requires.md [`@typescript-eslint/type-annotation-spacing`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/type-annotation-spacing.md +[`@typescript-eslint/unified-signatures`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/unified-signatures.md [`@typescript-eslint/no-misused-new`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-new.md [`@typescript-eslint/no-object-literal-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-object-literal-type-assertion.md [`@typescript-eslint/no-this-alias`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-this-alias.md diff --git a/packages/eslint-plugin/docs/rules/unified-signatures.md b/packages/eslint-plugin/docs/rules/unified-signatures.md new file mode 100644 index 00000000000..929c82ae159 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/unified-signatures.md @@ -0,0 +1,33 @@ +# Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter. (unified-signatures) + +Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter. + +## Rule Details + +This rule aims to keep the source code as maintanable as posible by reducing the amount of overloads. + +Examples of **incorrect** code for this rule: + +```ts +function f(x: number): void; +function f(x: string): void; +``` + +```ts +f(): void; +f(...x: number[]): void; +``` + +Examples of **correct** code for this rule: + +```ts +function f(x: number | string): void; +``` + +```ts +function f(x?: ...number[]): void; +``` + +## Related to + +- TSLint: ['unified-signatures`](https://palantir.github.io/tslint/rules/unified-signatures/) diff --git a/packages/eslint-plugin/src/rules/unified-signatures.ts b/packages/eslint-plugin/src/rules/unified-signatures.ts new file mode 100644 index 00000000000..1b54bd4217f --- /dev/null +++ b/packages/eslint-plugin/src/rules/unified-signatures.ts @@ -0,0 +1,576 @@ +import * as util from '../util'; +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; + +interface Failure { + unify: Unify; + only2: boolean; +} + +type Unify = + | { + kind: 'single-parameter-difference'; + p0: TSESTree.Parameter; + p1: TSESTree.Parameter; + } + | { + kind: 'extra-parameter'; + extraParameter: TSESTree.Parameter; + otherSignature: SignatureDefinition; + }; + +/** + * Returns true if typeName is the name of an *outer* type parameter. + * In: `interface I { m(x: U): T }`, only `T` is an outer type parameter. + */ +type IsTypeParameter = (typeName: string) => boolean; + +type ScopeNode = + | TSESTree.Program + | TSESTree.TSModuleBlock + | TSESTree.TSInterfaceBody + | TSESTree.ClassBody + | TSESTree.TSTypeLiteral; + +type OverloadNode = MethodDefinition | SignatureDefinition; + +type SignatureDefinition = + | TSESTree.FunctionExpression + | TSESTree.TSCallSignatureDeclaration + | TSESTree.TSConstructSignatureDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.TSEmptyBodyFunctionExpression + | TSESTree.TSMethodSignature; + +type MethodDefinition = + | TSESTree.MethodDefinition + | TSESTree.TSAbstractMethodDefinition; + +export default util.createRule({ + name: 'unified-signatures', + meta: { + docs: { + description: + 'Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter.', + category: 'Variables', + recommended: false, + tslintName: 'unified-signatures', + }, + type: 'suggestion', + messages: { + omittingRestParameter: '{{failureStringStart}} with a rest parameter.', + omittingSingleParameter: + '{{failureStringStart}} with an optional parameter.', + singleParameterDifference: + '{{failureStringStart}} taking `{{type1}} | {{type2}}`.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.getSourceCode(); + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + function failureStringStart(otherLine?: number): string { + // For only 2 overloads we don't need to specify which is the other one. + const overloads = + otherLine === undefined + ? 'These overloads' + : `This overload and the one on line ${otherLine}`; + return `${overloads} can be combined into one signature`; + } + + function addFailures(failures: Failure[]): void { + for (const failure of failures) { + const { unify, only2 } = failure; + switch (unify.kind) { + case 'single-parameter-difference': { + const { p0, p1 } = unify; + const lineOfOtherOverload = only2 ? undefined : p0.loc.start.line; + + const typeAnnotation0 = isTSParameterProperty(p0) + ? p0.parameter.typeAnnotation + : p0.typeAnnotation; + const typeAnnotation1 = isTSParameterProperty(p1) + ? p1.parameter.typeAnnotation + : p1.typeAnnotation; + + context.report({ + loc: p1.loc, + messageId: 'singleParameterDifference', + data: { + failureStringStart: failureStringStart(lineOfOtherOverload), + type1: sourceCode.getText( + typeAnnotation0 && typeAnnotation0.typeAnnotation, + ), + type2: sourceCode.getText( + typeAnnotation1 && typeAnnotation1.typeAnnotation, + ), + }, + node: p1, + }); + break; + } + case 'extra-parameter': { + const { extraParameter, otherSignature } = unify; + const lineOfOtherOverload = only2 + ? undefined + : otherSignature.loc.start.line; + + context.report({ + loc: extraParameter.loc, + messageId: + extraParameter.type === AST_NODE_TYPES.RestElement + ? 'omittingRestParameter' + : 'omittingSingleParameter', + data: { + failureStringStart: failureStringStart(lineOfOtherOverload), + }, + node: extraParameter, + }); + } + } + } + } + + function checkOverloads( + signatures: ReadonlyArray, + typeParameters?: TSESTree.TSTypeParameterDeclaration, + ): Failure[] { + const result: Failure[] = []; + const isTypeParameter = getIsTypeParameter(typeParameters); + for (const overloads of signatures) { + if (overloads.length === 2) { + const signature0 = + (overloads[0] as MethodDefinition).value || overloads[0]; + const signature1 = + (overloads[1] as MethodDefinition).value || overloads[1]; + + const unify = compareSignatures( + signature0, + signature1, + isTypeParameter, + ); + if (unify !== undefined) { + result.push({ unify, only2: true }); + } + } else { + forEachPair(overloads, (a, b) => { + const signature0 = (a as MethodDefinition).value || a; + const signature1 = (b as MethodDefinition).value || b; + + const unify = compareSignatures( + signature0, + signature1, + isTypeParameter, + ); + if (unify !== undefined) { + result.push({ unify, only2: false }); + } + }); + } + } + return result; + } + + function compareSignatures( + a: SignatureDefinition, + b: SignatureDefinition, + isTypeParameter: IsTypeParameter, + ): Unify | undefined { + if (!signaturesCanBeUnified(a, b, isTypeParameter)) { + return undefined; + } + + return a.params.length === b.params.length + ? signaturesDifferBySingleParameter(a.params, b.params) + : signaturesDifferByOptionalOrRestParameter(a, b); + } + + function signaturesCanBeUnified( + a: SignatureDefinition, + b: SignatureDefinition, + isTypeParameter: IsTypeParameter, + ): boolean { + // Must return the same type. + + const aTypeParams = + a.typeParameters !== undefined ? a.typeParameters.params : undefined; + const bTypeParams = + b.typeParameters !== undefined ? b.typeParameters.params : undefined; + + return ( + typesAreEqual(a.returnType, b.returnType) && + // Must take the same type parameters. + // If one uses a type parameter (from outside) and the other doesn't, they shouldn't be joined. + util.arraysAreEqual(aTypeParams, bTypeParams, typeParametersAreEqual) && + signatureUsesTypeParameter(a, isTypeParameter) === + signatureUsesTypeParameter(b, isTypeParameter) + ); + } + + /** Detect `a(x: number, y: number, z: number)` and `a(x: number, y: string, z: number)`. */ + function signaturesDifferBySingleParameter( + types1: ReadonlyArray, + types2: ReadonlyArray, + ): Unify | undefined { + const index = getIndexOfFirstDifference( + types1, + types2, + parametersAreEqual, + ); + if (index === undefined) { + return undefined; + } + + // If remaining arrays are equal, the signatures differ by just one parameter type + if ( + !util.arraysAreEqual( + types1.slice(index + 1), + types2.slice(index + 1), + parametersAreEqual, + ) + ) { + return undefined; + } + + const a = types1[index]; + const b = types2[index]; + // Can unify `a?: string` and `b?: number`. Can't unify `...args: string[]` and `...args: number[]`. + // See https://github.com/Microsoft/TypeScript/issues/5077 + return parametersHaveEqualSigils(a, b) && + a.type !== AST_NODE_TYPES.RestElement + ? { kind: 'single-parameter-difference', p0: a, p1: b } + : undefined; + } + + /** + * Detect `a(): void` and `a(x: number): void`. + * Returns the parameter declaration (`x: number` in this example) that should be optional/rest, and overload it's a part of. + */ + function signaturesDifferByOptionalOrRestParameter( + a: SignatureDefinition, + b: SignatureDefinition, + ): Unify | undefined { + const sig1 = a.params; + const sig2 = b.params; + + const minLength = Math.min(sig1.length, sig2.length); + const longer = sig1.length < sig2.length ? sig2 : sig1; + const shorter = sig1.length < sig2.length ? sig1 : sig2; + const shorterSig = sig1.length < sig2.length ? a : b; + + // If one is has 2+ parameters more than the other, they must all be optional/rest. + // Differ by optional parameters: f() and f(x), f() and f(x, ?y, ...z) + // Not allowed: f() and f(x, y) + for (let i = minLength + 1; i < longer.length; i++) { + if (!parameterMayBeMissing(longer[i])) { + return undefined; + } + } + + for (let i = 0; i < minLength; i++) { + const sig1i = sig1[i]; + const sig2i = sig2[i]; + const typeAnnotation1 = isTSParameterProperty(sig1i) + ? sig1i.parameter.typeAnnotation + : sig1i.typeAnnotation; + const typeAnnotation2 = isTSParameterProperty(sig2i) + ? sig2i.parameter.typeAnnotation + : sig2i.typeAnnotation; + + if (!typesAreEqual(typeAnnotation1, typeAnnotation2)) { + return undefined; + } + } + + if ( + minLength > 0 && + shorter[minLength - 1].type === AST_NODE_TYPES.RestElement + ) { + return undefined; + } + + return { + extraParameter: longer[longer.length - 1], + kind: 'extra-parameter', + otherSignature: shorterSig, + }; + } + + /** Given type parameters, returns a function to test whether a type is one of those parameters. */ + function getIsTypeParameter( + typeParameters?: TSESTree.TSTypeParameterDeclaration, + ): IsTypeParameter { + if (typeParameters === undefined) { + return () => false; + } + + const set = new Set(); + for (const t of typeParameters.params) { + set.add(t.name.name); + } + return typeName => set.has(typeName); + } + + /** True if any of the outer type parameters are used in a signature. */ + function signatureUsesTypeParameter( + sig: SignatureDefinition, + isTypeParameter: IsTypeParameter, + ): boolean { + return sig.params.some((p: TSESTree.Parameter) => + typeContainsTypeParameter( + isTSParameterProperty(p) + ? p.parameter.typeAnnotation + : p.typeAnnotation, + ), + ); + + function typeContainsTypeParameter( + type?: TSESTree.TSTypeAnnotation | TSESTree.TypeNode, + ): boolean { + if (!type) { + return false; + } + + if (type.type === AST_NODE_TYPES.TSTypeReference) { + const typeName = type.typeName; + if (isIdentifier(typeName) && isTypeParameter(typeName.name)) { + return true; + } + } + + return typeContainsTypeParameter( + (type as TSESTree.TSTypeAnnotation).typeAnnotation || + (type as TSESTree.TSArrayType).elementType, + ); + } + } + + function isTSParameterProperty( + node: TSESTree.Node, + ): node is TSESTree.TSParameterProperty { + return ( + (node as TSESTree.TSParameterProperty).type === + AST_NODE_TYPES.TSParameterProperty + ); + } + + function parametersAreEqual( + a: TSESTree.Parameter, + b: TSESTree.Parameter, + ): boolean { + const typeAnnotationA = isTSParameterProperty(a) + ? a.parameter.typeAnnotation + : a.typeAnnotation; + const typeAnnotationB = isTSParameterProperty(b) + ? b.parameter.typeAnnotation + : b.typeAnnotation; + + return ( + parametersHaveEqualSigils(a, b) && + typesAreEqual(typeAnnotationA, typeAnnotationB) + ); + } + + /** True for optional/rest parameters. */ + function parameterMayBeMissing(p: TSESTree.Parameter): boolean | undefined { + const optional = isTSParameterProperty(p) + ? p.parameter.optional + : p.optional; + + return p.type === AST_NODE_TYPES.RestElement || optional; + } + + /** False if one is optional and the other isn't, or one is a rest parameter and the other isn't. */ + function parametersHaveEqualSigils( + a: TSESTree.Parameter, + b: TSESTree.Parameter, + ): boolean { + const optionalA = isTSParameterProperty(a) + ? a.parameter.optional + : a.optional; + const optionalB = isTSParameterProperty(b) + ? b.parameter.optional + : b.optional; + + return ( + (a.type === AST_NODE_TYPES.RestElement) === + (b.type === AST_NODE_TYPES.RestElement) && + (optionalA !== undefined) === (optionalB !== undefined) + ); + } + + function typeParametersAreEqual( + a: TSESTree.TSTypeParameter, + b: TSESTree.TSTypeParameter, + ): boolean { + return ( + a.name.name === b.name.name && + constraintsAreEqual(a.constraint, b.constraint) + ); + } + + function typesAreEqual( + a: TSESTree.TSTypeAnnotation | undefined, + b: TSESTree.TSTypeAnnotation | undefined, + ): boolean { + return ( + a === b || + (a !== undefined && + b !== undefined && + a.typeAnnotation.type === b.typeAnnotation.type) + ); + } + + function constraintsAreEqual( + a: TSESTree.TypeNode | undefined, + b: TSESTree.TypeNode | undefined, + ): boolean { + return ( + a === b || (a !== undefined && b !== undefined && a.type === b.type) + ); + } + + /* Returns the first index where `a` and `b` differ. */ + function getIndexOfFirstDifference( + a: ReadonlyArray, + b: ReadonlyArray, + equal: util.Equal, + ): number | undefined { + for (let i = 0; i < a.length && i < b.length; i++) { + if (!equal(a[i], b[i])) { + return i; + } + } + return undefined; + } + + /** Calls `action` for every pair of values in `values`. */ + function forEachPair( + values: ReadonlyArray, + action: (a: T, b: T) => void, + ): void { + for (let i = 0; i < values.length; i++) { + for (let j = i + 1; j < values.length; j++) { + action(values[i], values[j]); + } + } + } + + interface Scope { + overloads: Map; + parent?: ScopeNode; + typeParameters?: TSESTree.TSTypeParameterDeclaration; + } + + const scopes: Scope[] = []; + let currentScope: Scope = { + overloads: new Map(), + }; + + function createScope( + parent: ScopeNode, + typeParameters?: TSESTree.TSTypeParameterDeclaration, + ) { + currentScope && scopes.push(currentScope); + currentScope = { + overloads: new Map(), + parent, + typeParameters, + }; + } + + function checkScope() { + const failures = checkOverloads( + Array.from(currentScope.overloads.values()), + currentScope.typeParameters, + ); + addFailures(failures); + currentScope = scopes.pop()!; + } + + function addOverload(signature: OverloadNode, key?: string) { + key = key || getOverloadKey(signature); + if (currentScope && signature.parent === currentScope.parent && key) { + const overloads = currentScope.overloads.get(key); + if (overloads !== undefined) { + overloads.push(signature); + } else { + currentScope.overloads.set(key, [signature]); + } + } + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + Program: createScope, + TSModuleBlock: createScope, + TSInterfaceDeclaration(node) { + createScope(node.body, node.typeParameters); + }, + ClassDeclaration(node) { + createScope(node.body, node.typeParameters); + }, + TSTypeLiteral: createScope, + // collect overloads + TSDeclareFunction(node) { + if (node.id && !node.body) { + addOverload(node, node.id.name); + } + }, + TSCallSignatureDeclaration: addOverload, + TSConstructSignatureDeclaration: addOverload, + TSMethodSignature: addOverload, + TSAbstractMethodDefinition(node) { + if (!node.value.body) { + addOverload(node); + } + }, + MethodDefinition(node) { + if (!node.value.body) { + addOverload(node); + } + }, + // validate scopes + 'Program:exit': checkScope, + 'TSModuleBlock:exit': checkScope, + 'TSInterfaceDeclaration:exit': checkScope, + 'ClassDeclaration:exit': checkScope, + 'TSTypeLiteral:exit': checkScope, + }; + }, +}); + +function getOverloadKey(node: OverloadNode): string | undefined { + const info = getOverloadInfo(node); + + return ( + ((node as MethodDefinition).computed ? '0' : '1') + + ((node as MethodDefinition).static ? '0' : '1') + + info + ); +} + +function getOverloadInfo(node: OverloadNode): string { + switch (node.type) { + case AST_NODE_TYPES.TSConstructSignatureDeclaration: + return 'constructor'; + case AST_NODE_TYPES.TSCallSignatureDeclaration: + return '()'; + default: { + const { key } = node as MethodDefinition; + + return isIdentifier(key) ? key.name : (key as TSESTree.Literal).raw; + } + } +} + +function isIdentifier(node: TSESTree.Node): node is TSESTree.Identifier { + return node.type === AST_NODE_TYPES.Identifier; +} diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index ab56cd4e694..4e3d34937f9 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -65,6 +65,23 @@ export function getNameFromPropertyName( return `${propertyName.value}`; } +/** Return true if both parameters are equal. */ +export type Equal = (a: T, b: T) => boolean; + +export function arraysAreEqual( + a: T[] | undefined, + b: T[] | undefined, + eq: (a: T, b: T) => boolean, +): boolean { + return ( + a === b || + (a !== undefined && + b !== undefined && + a.length === b.length && + a.every((x, idx) => eq(x, b[idx]))) + ); +} + /** * Gets a string name representation of the name of the given MethodDefinition * or ClassProperty node, with handling for computed property names. diff --git a/packages/eslint-plugin/tests/rules/unified-signatures.test.ts b/packages/eslint-plugin/tests/rules/unified-signatures.test.ts new file mode 100644 index 00000000000..94b6520de81 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/unified-signatures.test.ts @@ -0,0 +1,595 @@ +import rule from '../../src/rules/unified-signatures'; +import { RuleTester } from '../RuleTester'; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +var ruleTester = new RuleTester({ parser: '@typescript-eslint/parser' }); + +ruleTester.run('unified-signatures', rule, { + valid: [ + ` +function g(): void; +function g(a: number, b: number): void; +function g(a?: number, b?: number): void {} + `, + ` +function rest(...xs: number[]): void; +function rest(xs: number[], y: string): void; +function rest(...args: any[]) {} +`, + ` +class C { + constructor(); + constructor(a: number, b: number); + constructor(a?: number, b?: number) {} + + a(): void; + a(a: number, b: number): void; + a(a?: number, b?: number): void {} +} +`, + // No error for arity difference greater than 1. + ` +interface I { + a2(): void; + a2(x: number, y: number): void; +} +`, + // No error for different return types. + ` +interface I { + a4(): void; + a4(x: number): number; +} +`, + // No error if one takes a type parameter and the other doesn't. + ` +interface I { + a5(x: T): T; + a5(x: number): number; +} +`, + // No error if one is a rest parameter and other isn't. + ` +interface I { + b2(x: string): void; + b2(...x: number[]): void; +} +`, + // No error if both are rest parameters. (https://github.com/Microsoft/TypeScript/issues/5077) + ` +interface I { + b3(...x: number[]): void; + b3(...x: string[]): void; +} +`, + // No error if one is optional and the other isn't. + ` +interface I { + c3(x: number): void; + c3(x?: string): void; +} +`, + // No error if they differ by 2 or more parameters. + ` +interface I { + d2(x: string, y: number): void; + d2(x: number, y: string): void; +} +`, + // No conflict between static/non-static members. + ` +declare class D { + static a(); + a(x: number); +} +`, + // Allow separate overloads if one is generic and the other isn't. + ` +interface Generic { + x(): void; + x(x: T[]): void; +} +`, + // Allow signatures if the type is not equal. + ` +interface I { + f(x1:number): void; + f(x1:boolean, x2?: number): void; +} +`, + // AllowType parameters that are not equal + ` +function f(x: T[]): void; +function f(x: T): void; + `, + ], + invalid: [ + { + code: ` +function f(x: number): void; +function f(x: string): void; +function f(x: any): any { + return x; +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'number', + type2: 'string', + }, + line: 3, + column: 12, + }, + ], + }, + { + code: ` +function opt(xs?: number[]): void; +function opt(xs: number[], y: string): void; +function opt(...args: any[]) {} +`, + errors: [ + { + messageId: 'omittingSingleParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 3, + column: 28, + }, + ], + }, + { + // For 3 or more overloads, mentions the line. + code: ` +interface I { + a0(): void; + a0(x: string): string; + a0(x: number): void; +} +`, + errors: [ + { + messageId: 'omittingSingleParameter', + data: { + failureStringStart: + 'This overload and the one on line 3 can be combined into one signature', + }, + line: 5, + column: 8, + }, + ], + }, + { + // Error for extra parameter. + code: ` +interface I { + a1(): void; + a1(x: number): void; +} +`, + errors: [ + { + messageId: 'omittingSingleParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 4, + column: 8, + }, + ], + }, + { + // Error for arity difference greater than 1 if the additional parameters are all optional/rest. + code: ` +interface I { + a3(): void; + a3(x: number, y?: number, ...z: number[]): void; +} +`, + errors: [ + { + messageId: 'omittingRestParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 4, + column: 31, + }, + ], + }, + { + // Error if only one defines a rest parameter. + code: ` +interface I { + b(): void; + b(...x: number[]): void; +} +`, + errors: [ + { + messageId: 'omittingRestParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 4, + column: 7, + }, + ], + }, + { + // Error if only one defines an optional parameter. + code: ` +interface I { + c(): void; + c(x?: number): void; +} +`, + errors: [ + { + messageId: 'omittingSingleParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 4, + column: 7, + }, + ], + }, + { + // Error if both are optional. + code: ` +interface I { + c2(x?: number): void; + c2(x?: string): void; +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'number', + type2: 'string', + }, + line: 4, + column: 8, + }, + ], + }, + { + // Error for different types (could be a union) + code: ` +interface I { + d(x: number): void; + d(x: string): void; +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'number', + type2: 'string', + }, + line: 4, + column: 7, + }, + ], + }, + { + // Works for type literal and call signature too. + code: ` +type T = { + (): void; + (x: number): void; +} +`, + errors: [ + { + messageId: 'omittingSingleParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 4, + column: 6, + }, + ], + }, + { + // Works for constructor. + code: ` +declare class C { + constructor(); + constructor(x: number); +} +`, + errors: [ + { + messageId: 'omittingSingleParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 4, + column: 17, + }, + ], + }, + { + // Works with unions. + code: ` +interface I { + f(x: number); + f(x: string | boolean); +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'number', + type2: 'string | boolean', + }, + line: 4, + column: 7, + }, + ], + }, + { + // Works with tuples. + code: ` +interface I { + f(x: number); + f(x: [string, boolean]); +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'number', + type2: '[string, boolean]', + }, + line: 4, + column: 7, + }, + ], + }, + { + code: ` +interface Generic { + y(x: T[]): void; + y(x: T): void; +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'T[]', + type2: 'T', + }, + line: 4, + column: 7, + }, + ], + }, + { + // Check type parameters when equal + code: ` +function f(x: T[]): void; +function f(x: T): void; +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'T[]', + type2: 'T', + }, + line: 3, + column: 15, + }, + ], + }, + { + // Verifies type parameters and constraints + code: ` +function f(x: T[]): void; +function f(x: T): void; +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'T[]', + type2: 'T', + }, + line: 3, + column: 30, + }, + ], + }, + { + // Works with abstract + code: ` +abstract class Foo { + public abstract f(x: number): void; + public abstract f(x: string): void; +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'number', + type2: 'string', + }, + line: 4, + column: 23, + }, + ], + }, + { + // Works with literals + code: ` +interface Foo { + "f"(x: string): void; + "f"(x: number): void; +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'string', + type2: 'number', + }, + line: 4, + column: 9, + }, + ], + }, + { + // Works with new constructor + code: ` +interface Foo { + new(x: string): Foo; + new(x: number): Foo; +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'string', + type2: 'number', + }, + line: 4, + column: 9, + }, + ], + }, + { + // Works with new computed properties + code: ` +enum Enum { + Func = "function", +} + +interface IFoo { + [Enum.Func](x: string): void; + [Enum.Func](x: number): void; +} +`, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'string', + type2: 'number', + }, + line: 8, + column: 17, + }, + ], + }, + { + // Works with parameter properties. Note that this is invalid TypeScript syntax. + code: ` +class Foo { + constructor(readonly x: number); + constructor(readonly x: string); +} + `, + errors: [ + { + messageId: 'singleParameterDifference', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + type1: 'number', + type2: 'string', + }, + line: 4, + column: 17, + }, + ], + }, + { + // Works with parameter properties. Note that this is invalid TypeScript syntax. + code: ` +class Foo { + constructor(readonly x: number); + constructor(readonly x: number, readonly y: string); +} +`, + errors: [ + { + messageId: 'omittingSingleParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 4, + column: 37, + }, + ], + }, + { + // Works with parameter properties. Note that this is invalid TypeScript syntax. + code: ` +class Foo { + constructor(readonly x: number); + constructor(readonly x: number, readonly y?: string, readonly z?: string); +} +`, + errors: [ + { + messageId: 'omittingSingleParameter', + data: { + failureStringStart: + 'These overloads can be combined into one signature', + }, + line: 4, + column: 58, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/ts-eslint.d.ts b/packages/eslint-plugin/typings/ts-eslint.d.ts index b757a1b5982..33b667dab46 100644 --- a/packages/eslint-plugin/typings/ts-eslint.d.ts +++ b/packages/eslint-plugin/typings/ts-eslint.d.ts @@ -473,14 +473,24 @@ declare module 'ts-eslint' { Token?: RuleFunction; TryStatement?: RuleFunction; TSAbstractKeyword?: RuleFunction; + TSAbstractMethodDefinition?: RuleFunction< + TSESTree.TSAbstractMethodDefinition + >; TSAnyKeyword?: RuleFunction; TSArrayType?: RuleFunction; TSAsExpression?: RuleFunction; TSAsyncKeyword?: RuleFunction; TSBigIntKeyword?: RuleFunction; TSBooleanKeyword?: RuleFunction; + TSCallSignatureDeclaration?: RuleFunction< + TSESTree.TSCallSignatureDeclaration + >; TSConditionalType?: RuleFunction; + TSConstructSignatureDeclaration?: RuleFunction< + TSESTree.TSConstructSignatureDeclaration + >; TSDeclareKeyword?: RuleFunction; + TSDeclareFunction?: RuleFunction; TSEnumDeclaration?: RuleFunction; TSEnumMember?: RuleFunction; TSExportAssignment?: RuleFunction;