Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Commit

Permalink
unified-signatures: Convert to function
Browse files Browse the repository at this point in the history
  • Loading branch information
andy-hanson committed Mar 26, 2017
1 parent 147d686 commit b0f7e7b
Showing 1 changed file with 122 additions and 86 deletions.
208 changes: 122 additions & 86 deletions src/rules/unifiedSignaturesRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,109 +52,137 @@ export class Rule extends Lint.Rules.AbstractRule {
}

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new Walker(sourceFile, this.getOptions()));
return this.applyWithFunction(sourceFile, walk);
}
}

class Walker extends Lint.RuleWalker {
public visitSourceFile(node: ts.SourceFile) {
this.checkStatements(node.statements);
super.visitSourceFile(node);
}

public visitModuleDeclaration(node: ts.ModuleDeclaration) {
const { body } = node;
if (body && body.kind === ts.SyntaxKind.ModuleBlock) {
this.checkStatements((body as ts.ModuleBlock).statements);
function walk(ctx: Lint.WalkContext<void>): void {
const { sourceFile } = ctx;
checkStatements(sourceFile.statements);
return ts.forEachChild(sourceFile, function cb(node: ts.Node): void {
switch (node.kind) {
case ts.SyntaxKind.ModuleBlock:
checkStatements((node as ts.ModuleBlock).statements);
break;
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.ClassDeclaration: {
const { members, typeParameters } = node as ts.ClassDeclaration | ts.InterfaceDeclaration;
checkMembers(members, typeParameters);
break;
}
case ts.SyntaxKind.TypeLiteral:
checkMembers((node as ts.TypeLiteralNode).members);
break;
}
super.visitModuleDeclaration(node);
}

public visitInterfaceDeclaration(node: ts.InterfaceDeclaration) {
this.checkMembers(node.members, node.typeParameters);
super.visitInterfaceDeclaration(node);
}

public visitClassDeclaration(node: ts.ClassDeclaration) {
this.checkMembers(node.members, node.typeParameters);
super.visitClassDeclaration(node);
}

public visitTypeLiteral(node: ts.TypeLiteralNode) {
this.checkMembers(node.members);
super.visitTypeLiteral(node);
}
return ts.forEachChild(node, cb);
});

private checkStatements(statements: ts.Statement[]) {
this.checkOverloads(statements, (statement) => {
if (statement.kind === ts.SyntaxKind.FunctionDeclaration) {
const fn = statement as ts.FunctionDeclaration;
if (fn.body) {
return undefined;
}
return fn.name && { signature: fn, key: fn.name.text };
function checkStatements(statements: ts.Statement[]): void {
addFailures(checkOverloads(statements, undefined, (statement) => {
if (utils.isFunctionDeclaration(statement)) {
const { body, name } = statement;
return body === undefined && name !== undefined ? { signature: statement, key: name.text } : undefined;
} else {
return undefined;
}
});
}));
}

private checkMembers(members: Array<ts.TypeElement | ts.ClassElement>, typeParameters?: ts.TypeParameterDeclaration[]) {
this.checkOverloads(members, getOverloadName, typeParameters);
function getOverloadName(member: ts.TypeElement | ts.ClassElement) {
if (!utils.isSignatureDeclaration(member) || (member as ts.MethodDeclaration).body) {
return undefined;
function checkMembers(members: Array<ts.TypeElement | ts.ClassElement>, typeParameters?: ts.TypeParameterDeclaration[]): void {
addFailures(checkOverloads(members, typeParameters, (member) => {
switch (member.kind) {
case ts.SyntaxKind.CallSignature:
case ts.SyntaxKind.ConstructSignature:
case ts.SyntaxKind.MethodSignature:
break;
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.Constructor:
if ((member as ts.MethodDeclaration | ts.ConstructorDeclaration).body) {
return undefined;
}
break;
default:
return undefined;
}
const key = getOverloadKey(member);
return key === undefined ? undefined : { signature: member, key };
}

const signature = member as ts.CallSignatureDeclaration | ts.ConstructSignatureDeclaration | ts.MethodSignature |
ts.MethodDeclaration | ts.ConstructorDeclaration;
const key = getOverloadKey(signature);
return key === undefined ? undefined : { signature, key };
}));
}

private checkOverloads<T>(signatures: T[], getOverload: GetOverload<T>, typeParameters?: ts.TypeParameterDeclaration[]) {
const isTypeParameter = getIsTypeParameter(typeParameters);
for (const overloads of collectOverloads(signatures, getOverload)) {
if (overloads.length === 2) {
this.compareSignatures(overloads[0], overloads[1], isTypeParameter, /*only2*/ true);
} else {
forEachPair(overloads, (a, b) => {
this.compareSignatures(a, b, isTypeParameter, /*only2*/ false);
});
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 : getLine(p0.getStart());
ctx.addFailureAtNode(p1,
Rule.FAILURE_STRING_SINGLE_PARAMETER_DIFFERENCE(lineOfOtherOverload, typeText(p0), typeText(p1)));
break;
}
case "extra-parameter": {
const { extraParameter, otherSignature } = unify;
const lineOfOtherOverload = only2 ? undefined : getLine(otherSignature.pos);
ctx.addFailureAtNode(extraParameter, extraParameter.dotDotDotToken
? Rule.FAILURE_STRING_OMITTING_REST_PARAMETER(lineOfOtherOverload)
: Rule.FAILURE_STRING_OMITTING_SINGLE_PARAMETER(lineOfOtherOverload));
break;
}
}
}

}

private compareSignatures(a: ts.SignatureDeclaration, b: ts.SignatureDeclaration, isTypeParameter: IsTypeParameter, only2: boolean) {
if (!signaturesCanBeUnified(a, b, isTypeParameter)) {
return;
}
function getLine(pos: number): number {
return ts.getLineAndCharacterOfPosition(sourceFile, pos).line + 1;
}
}

if (a.parameters.length === b.parameters.length) {
const params = signaturesDifferBySingleParameter(a.parameters, b.parameters);
if (params) {
const [p0, p1] = params;
const lineOfOtherOverload = only2 ? undefined : this.getLine(p0);
this.addFailureAtNode(p1, Rule.FAILURE_STRING_SINGLE_PARAMETER_DIFFERENCE(lineOfOtherOverload, typeText(p0), typeText(p1)));
function checkOverloads<T>(
signatures: T[],
typeParameters: ts.TypeParameterDeclaration[] | undefined,
getOverload: GetOverload<T>): Failure[] {
const result: Failure[] = [];
const isTypeParameter = getIsTypeParameter(typeParameters);
for (const overloads of collectOverloads(signatures, getOverload)) {
if (overloads.length === 2) {
const unify = compareSignatures(overloads[0], overloads[1], isTypeParameter);
if (unify) {
result.push({ unify, only2: true });
}
} else {
const diff = signaturesDifferByOptionalOrRestParameter(a.parameters, b.parameters);
if (diff) {
const [extraParameter, signatureWithExtraParameter] = diff;
const lineOfOtherOverload = only2 ? undefined : this.getLine(signatureWithExtraParameter === a.parameters ? b : a);
this.addFailureAtNode(extraParameter, extraParameter.dotDotDotToken
? Rule.FAILURE_STRING_OMITTING_REST_PARAMETER(lineOfOtherOverload)
: Rule.FAILURE_STRING_OMITTING_SINGLE_PARAMETER(lineOfOtherOverload));
}
forEachPair(overloads, (a, b) => {
const unify = compareSignatures(a, b, isTypeParameter);
if (unify) {
result.push({ unify, only2: false });
}
});
}
}
return result;
}

private getLine(node: ts.Node): number {
return this.getLineAndCharacterOfPosition(node.getStart()).line + 1;
function compareSignatures(a: ts.SignatureDeclaration, b: ts.SignatureDeclaration, isTypeParameter: IsTypeParameter): Unify | undefined {
if (!signaturesCanBeUnified(a, b, isTypeParameter)) {
return undefined;
}

return a.parameters.length === b.parameters.length
? signaturesDifferBySingleParameter(a.parameters, b.parameters)
: signaturesDifferByOptionalOrRestParameter(a.parameters, b.parameters);
}

function typeText({ type }: ts.ParameterDeclaration) {
return type === undefined ? "any" : type.getText();
interface Failure {
unify: Unify;
only2: boolean;
}
type Unify =
| { kind: "single-parameter-difference", p0: ts.ParameterDeclaration, p1: ts.ParameterDeclaration }
| { kind: "extra-parameter", extraParameter: ts.ParameterDeclaration, otherSignature: ts.NodeArray<ts.ParameterDeclaration> };

function signaturesCanBeUnified(a: ts.SignatureDeclaration, b: ts.SignatureDeclaration, isTypeParameter: IsTypeParameter): boolean {
// Must return the same type.
Expand All @@ -167,7 +195,7 @@ function signaturesCanBeUnified(a: ts.SignatureDeclaration, b: ts.SignatureDecla

/** Detect `a(x: number, y: number, z: number)` and `a(x: number, y: string, z: number)`. */
function signaturesDifferBySingleParameter(types1: ts.ParameterDeclaration[], types2: ts.ParameterDeclaration[],
): [ts.ParameterDeclaration, ts.ParameterDeclaration] | undefined {
): Unify | undefined {
const index = getIndexOfFirstDifference(types1, types2, parametersAreEqual);
if (index === undefined) {
return undefined;
Expand All @@ -180,18 +208,18 @@ function signaturesDifferBySingleParameter(types1: ts.ParameterDeclaration[], ty

const a = types1[index];
const b = types2[index];
return parametersHaveEqualSigils(a, b) ? [a, b] : undefined;
return parametersHaveEqualSigils(a, b) ? { 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(types1: ts.ParameterDeclaration[], types2: ts.ParameterDeclaration[],
): [ts.ParameterDeclaration, ts.ParameterDeclaration[]] | undefined {
const minLength = Math.min(types1.length, types2.length);
const longer = types1.length < types2.length ? types2 : types1;
const shorter = types1.length < types2.length ? types1 : types2;
function signaturesDifferByOptionalOrRestParameter(sig1: ts.NodeArray<ts.ParameterDeclaration>, sig2: ts.NodeArray<ts.ParameterDeclaration>,
): Unify | undefined {
const minLength = Math.min(sig1.length, sig2.length);
const longer = sig1.length < sig2.length ? sig2 : sig1;
const shorter = sig1.length < sig2.length ? sig1 : sig2;

// 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)
Expand All @@ -203,7 +231,7 @@ function signaturesDifferByOptionalOrRestParameter(types1: ts.ParameterDeclarati
}

for (let i = 0; i < minLength; i++) {
if (!typesAreEqual(types1[i].type, types2[i].type)) {
if (!typesAreEqual(sig1[i].type, sig2[i].type)) {
return undefined;
}
}
Expand All @@ -212,7 +240,7 @@ function signaturesDifferByOptionalOrRestParameter(types1: ts.ParameterDeclarati
return undefined;
}

return [longer[longer.length - 1], longer];
return { kind: "extra-parameter", extraParameter: longer[longer.length - 1], otherSignature: shorter };
}

/**
Expand Down Expand Up @@ -312,10 +340,18 @@ function getIndexOfFirstDifference<T>(a: T[], b: T[], equal: Equal<T>): number |
}

/** Calls `action` for every pair of values in `values`. */
function forEachPair<T>(values: T[], action: (a: T, b: T) => void): void {
function forEachPair<T, Out>(values: T[], action: (a: T, b: T) => Out | undefined): Out | undefined {
for (let i = 0; i < values.length; i++) {
for (let j = i + 1; j < values.length; j++) {
action(values[i], values[j]);
const result = action(values[i], values[j]);
if (result) {
return result;
}
}
}
return undefined;
}

function typeText({ type }: ts.ParameterDeclaration) {
return type === undefined ? "any" : type.getText();
}

0 comments on commit b0f7e7b

Please sign in to comment.