309 lines
13 KiB
JavaScript
309 lines
13 KiB
JavaScript
"use strict";
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Palantir Technologies, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
var tslib_1 = require("tslib");
|
|
var utils = require("tsutils");
|
|
var ts = require("typescript");
|
|
var Lint = require("../index");
|
|
var utils_1 = require("../utils");
|
|
var adjacentOverloadSignaturesRule_1 = require("./adjacentOverloadSignaturesRule");
|
|
var Rule = (function (_super) {
|
|
tslib_1.__extends(Rule, _super);
|
|
function Rule() {
|
|
return _super !== null && _super.apply(this, arguments) || this;
|
|
}
|
|
/* tslint:enable:object-literal-sort-keys */
|
|
Rule.FAILURE_STRING_OMITTING_SINGLE_PARAMETER = function (otherLine) {
|
|
return this.FAILURE_STRING_START(otherLine) + " with an optional parameter.";
|
|
};
|
|
Rule.FAILURE_STRING_OMITTING_REST_PARAMETER = function (otherLine) {
|
|
return this.FAILURE_STRING_START(otherLine) + " with a rest parameter.";
|
|
};
|
|
Rule.FAILURE_STRING_SINGLE_PARAMETER_DIFFERENCE = function (otherLine, type1, type2) {
|
|
return this.FAILURE_STRING_START(otherLine) + " taking `" + type1 + " | " + type2 + "`.";
|
|
};
|
|
Rule.FAILURE_STRING_START = function (otherLine) {
|
|
// For only 2 overloads we don't need to specify which is the other one.
|
|
var overloads = otherLine === undefined ? "These overloads" : "This overload and the one on line " + otherLine;
|
|
return overloads + " can be combined into one signature";
|
|
};
|
|
Rule.prototype.apply = function (sourceFile) {
|
|
return this.applyWithFunction(sourceFile, walk);
|
|
};
|
|
return Rule;
|
|
}(Lint.Rules.AbstractRule));
|
|
/* tslint:disable:object-literal-sort-keys */
|
|
Rule.metadata = {
|
|
ruleName: "unified-signatures",
|
|
description: "Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter.",
|
|
optionsDescription: "Not configurable.",
|
|
options: null,
|
|
optionExamples: [true],
|
|
type: "typescript",
|
|
typescriptOnly: true,
|
|
};
|
|
exports.Rule = Rule;
|
|
function walk(ctx) {
|
|
var sourceFile = ctx.sourceFile;
|
|
checkStatements(sourceFile.statements);
|
|
return ts.forEachChild(sourceFile, function cb(node) {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.ModuleBlock:
|
|
checkStatements(node.statements);
|
|
break;
|
|
case ts.SyntaxKind.InterfaceDeclaration:
|
|
case ts.SyntaxKind.ClassDeclaration: {
|
|
var _a = node, members = _a.members, typeParameters = _a.typeParameters;
|
|
checkMembers(members, typeParameters);
|
|
break;
|
|
}
|
|
case ts.SyntaxKind.TypeLiteral:
|
|
checkMembers(node.members);
|
|
break;
|
|
}
|
|
return ts.forEachChild(node, cb);
|
|
});
|
|
function checkStatements(statements) {
|
|
addFailures(checkOverloads(statements, undefined, function (statement) {
|
|
if (utils.isFunctionDeclaration(statement)) {
|
|
var body = statement.body, name = statement.name;
|
|
return body === undefined && name !== undefined ? { signature: statement, key: name.text } : undefined;
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}));
|
|
}
|
|
function checkMembers(members, typeParameters) {
|
|
addFailures(checkOverloads(members, typeParameters, function (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.body !== undefined) {
|
|
return undefined;
|
|
}
|
|
break;
|
|
default:
|
|
return undefined;
|
|
}
|
|
var signature = member;
|
|
var key = adjacentOverloadSignaturesRule_1.getOverloadKey(signature);
|
|
return key === undefined ? undefined : { signature: signature, key: key };
|
|
}));
|
|
}
|
|
function addFailures(failures) {
|
|
for (var _i = 0, failures_1 = failures; _i < failures_1.length; _i++) {
|
|
var failure = failures_1[_i];
|
|
var unify = failure.unify, only2 = failure.only2;
|
|
switch (unify.kind) {
|
|
case "single-parameter-difference": {
|
|
var p0 = unify.p0, p1 = unify.p1;
|
|
var lineOfOtherOverload = only2 ? undefined : getLine(p0.getStart());
|
|
ctx.addFailureAtNode(p1, Rule.FAILURE_STRING_SINGLE_PARAMETER_DIFFERENCE(lineOfOtherOverload, typeText(p0), typeText(p1)));
|
|
break;
|
|
}
|
|
case "extra-parameter": {
|
|
var extraParameter = unify.extraParameter, otherSignature = unify.otherSignature;
|
|
var lineOfOtherOverload = only2 ? undefined : getLine(otherSignature.pos);
|
|
ctx.addFailureAtNode(extraParameter, extraParameter.dotDotDotToken !== undefined
|
|
? Rule.FAILURE_STRING_OMITTING_REST_PARAMETER(lineOfOtherOverload)
|
|
: Rule.FAILURE_STRING_OMITTING_SINGLE_PARAMETER(lineOfOtherOverload));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function getLine(pos) {
|
|
return ts.getLineAndCharacterOfPosition(sourceFile, pos).line + 1;
|
|
}
|
|
}
|
|
function checkOverloads(signatures, typeParameters, getOverload) {
|
|
var result = [];
|
|
var isTypeParameter = getIsTypeParameter(typeParameters);
|
|
for (var _i = 0, _a = collectOverloads(signatures, getOverload); _i < _a.length; _i++) {
|
|
var overloads = _a[_i];
|
|
if (overloads.length === 2) {
|
|
var unify = compareSignatures(overloads[0], overloads[1], isTypeParameter);
|
|
if (unify !== undefined) {
|
|
result.push({ unify: unify, only2: true });
|
|
}
|
|
}
|
|
else {
|
|
forEachPair(overloads, function (a, b) {
|
|
var unify = compareSignatures(a, b, isTypeParameter);
|
|
if (unify !== undefined) {
|
|
result.push({ unify: unify, only2: false });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function compareSignatures(a, b, isTypeParameter) {
|
|
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 signaturesCanBeUnified(a, b, isTypeParameter) {
|
|
// Must return the same type.
|
|
return typesAreEqual(a.type, b.type) &&
|
|
// Must take the same type parameters.
|
|
utils_1.arraysAreEqual(a.typeParameters, b.typeParameters, typeParametersAreEqual) &&
|
|
// If one uses a type parameter (from outside) and the other doesn't, they shouldn't be joined.
|
|
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, types2) {
|
|
var index = getIndexOfFirstDifference(types1, types2, parametersAreEqual);
|
|
if (index === undefined) {
|
|
return undefined;
|
|
}
|
|
// If remaining arrays are equal, the signatures differ by just one parameter type
|
|
if (!utils_1.arraysAreEqual(types1.slice(index + 1), types2.slice(index + 1), parametersAreEqual)) {
|
|
return undefined;
|
|
}
|
|
var a = types1[index];
|
|
var b = types2[index];
|
|
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(sig1, sig2) {
|
|
var minLength = Math.min(sig1.length, sig2.length);
|
|
var longer = sig1.length < sig2.length ? sig2 : sig1;
|
|
var 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)
|
|
// Not allowed: f() and f(x, y)
|
|
for (var i = minLength + 1; i < longer.length; i++) {
|
|
if (!parameterMayBeMissing(longer[i])) {
|
|
return undefined;
|
|
}
|
|
}
|
|
for (var i = 0; i < minLength; i++) {
|
|
if (!typesAreEqual(sig1[i].type, sig2[i].type)) {
|
|
return undefined;
|
|
}
|
|
}
|
|
if (minLength > 0 && shorter[minLength - 1].dotDotDotToken !== undefined) {
|
|
return undefined;
|
|
}
|
|
return { kind: "extra-parameter", extraParameter: longer[longer.length - 1], otherSignature: shorter };
|
|
}
|
|
/** Given type parameters, returns a function to test whether a type is one of those parameters. */
|
|
function getIsTypeParameter(typeParameters) {
|
|
if (typeParameters === undefined) {
|
|
return function () { return false; };
|
|
}
|
|
var set = new Set();
|
|
for (var _i = 0, typeParameters_1 = typeParameters; _i < typeParameters_1.length; _i++) {
|
|
var t = typeParameters_1[_i];
|
|
set.add(t.getText());
|
|
}
|
|
return function (typeName) { return set.has(typeName); };
|
|
}
|
|
/** True if any of the outer type parameters are used in a signature. */
|
|
function signatureUsesTypeParameter(sig, isTypeParameter) {
|
|
return sig.parameters.some(function (p) { return p.type !== undefined && typeContainsTypeParameter(p.type); });
|
|
function typeContainsTypeParameter(type) {
|
|
if (utils.isTypeReferenceNode(type)) {
|
|
var typeName = type.typeName;
|
|
if (typeName.kind === ts.SyntaxKind.Identifier && isTypeParameter(typeName.text)) {
|
|
return true;
|
|
}
|
|
}
|
|
return ts.forEachChild(type, typeContainsTypeParameter);
|
|
}
|
|
}
|
|
/**
|
|
* Given all signatures, collects an array of arrays of signatures which are all overloads.
|
|
* Does not rely on overloads being adjacent. This is similar to code in adjacentOverloadSignaturesRule.ts, but not the same.
|
|
*/
|
|
function collectOverloads(nodes, getOverload) {
|
|
var map = new Map();
|
|
for (var _i = 0, nodes_1 = nodes; _i < nodes_1.length; _i++) {
|
|
var sig = nodes_1[_i];
|
|
var overload = getOverload(sig);
|
|
if (overload === undefined) {
|
|
continue;
|
|
}
|
|
var signature = overload.signature, key = overload.key;
|
|
var overloads = map.get(key);
|
|
if (overloads !== undefined) {
|
|
overloads.push(signature);
|
|
}
|
|
else {
|
|
map.set(key, [signature]);
|
|
}
|
|
}
|
|
return Array.from(map.values());
|
|
}
|
|
function parametersAreEqual(a, b) {
|
|
return parametersHaveEqualSigils(a, b) && typesAreEqual(a.type, b.type);
|
|
}
|
|
/** True for optional/rest parameters. */
|
|
function parameterMayBeMissing(p) {
|
|
return p.dotDotDotToken !== undefined || p.questionToken !== undefined;
|
|
}
|
|
/** False if one is optional and the other isn't, or one is a rest parameter and the other isn't. */
|
|
function parametersHaveEqualSigils(a, b) {
|
|
return (a.dotDotDotToken !== undefined) === (b.dotDotDotToken !== undefined) &&
|
|
(a.questionToken !== undefined) === (b.questionToken !== undefined);
|
|
}
|
|
function typeParametersAreEqual(a, b) {
|
|
return a.name.text === b.name.text && typesAreEqual(a.constraint, b.constraint);
|
|
}
|
|
function typesAreEqual(a, b) {
|
|
// TODO: Could traverse AST so that formatting differences don't affect this.
|
|
return a === b || a !== undefined && b !== undefined && a.getText() === b.getText();
|
|
}
|
|
/** Returns the first index where `a` and `b` differ. */
|
|
function getIndexOfFirstDifference(a, b, equal) {
|
|
for (var 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, action) {
|
|
for (var i = 0; i < values.length; i++) {
|
|
for (var j = i + 1; j < values.length; j++) {
|
|
var result = action(values[i], values[j]);
|
|
if (result !== undefined) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
function typeText(_a) {
|
|
var type = _a.type;
|
|
return type === undefined ? "any" : type.getText();
|
|
}
|