diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 10268b006351..ba235a7bc63d 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 e10dc03292af..11ccb8ac22b7 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 000000000000..929c82ae1598 --- /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 000000000000..1b54bd4217f7 --- /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 ab56cd4e694e..4e3d34937f91 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 000000000000..94b6520de811 --- /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 b757a1b59820..33b667dab46a 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; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy