unifiedSignaturesRule.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. "use strict";
  2. /**
  3. * @license
  4. * Copyright 2017 Palantir Technologies, Inc.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. var tslib_1 = require("tslib");
  20. var utils = require("tsutils");
  21. var ts = require("typescript");
  22. var Lint = require("../index");
  23. var utils_1 = require("../utils");
  24. var adjacentOverloadSignaturesRule_1 = require("./adjacentOverloadSignaturesRule");
  25. var Rule = /** @class */ (function (_super) {
  26. tslib_1.__extends(Rule, _super);
  27. function Rule() {
  28. return _super !== null && _super.apply(this, arguments) || this;
  29. }
  30. /* tslint:enable:object-literal-sort-keys */
  31. Rule.FAILURE_STRING_OMITTING_SINGLE_PARAMETER = function (otherLine) {
  32. return this.FAILURE_STRING_START(otherLine) + " with an optional parameter.";
  33. };
  34. Rule.FAILURE_STRING_OMITTING_REST_PARAMETER = function (otherLine) {
  35. return this.FAILURE_STRING_START(otherLine) + " with a rest parameter.";
  36. };
  37. Rule.FAILURE_STRING_SINGLE_PARAMETER_DIFFERENCE = function (otherLine, type1, type2) {
  38. return this.FAILURE_STRING_START(otherLine) + " taking `" + type1 + " | " + type2 + "`.";
  39. };
  40. Rule.FAILURE_STRING_START = function (otherLine) {
  41. // For only 2 overloads we don't need to specify which is the other one.
  42. var overloads = otherLine === undefined ? "These overloads" : "This overload and the one on line " + otherLine;
  43. return overloads + " can be combined into one signature";
  44. };
  45. Rule.prototype.apply = function (sourceFile) {
  46. return this.applyWithFunction(sourceFile, walk);
  47. };
  48. /* tslint:disable:object-literal-sort-keys */
  49. Rule.metadata = {
  50. ruleName: "unified-signatures",
  51. description: "Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter.",
  52. optionsDescription: "Not configurable.",
  53. options: null,
  54. optionExamples: [true],
  55. type: "typescript",
  56. typescriptOnly: true,
  57. };
  58. return Rule;
  59. }(Lint.Rules.AbstractRule));
  60. exports.Rule = Rule;
  61. function walk(ctx) {
  62. var sourceFile = ctx.sourceFile;
  63. checkStatements(sourceFile.statements);
  64. return ts.forEachChild(sourceFile, function cb(node) {
  65. switch (node.kind) {
  66. case ts.SyntaxKind.ModuleBlock:
  67. checkStatements(node.statements);
  68. break;
  69. case ts.SyntaxKind.InterfaceDeclaration:
  70. case ts.SyntaxKind.ClassDeclaration: {
  71. var _a = node, members = _a.members, typeParameters = _a.typeParameters;
  72. checkMembers(members, typeParameters);
  73. break;
  74. }
  75. case ts.SyntaxKind.TypeLiteral:
  76. checkMembers(node.members);
  77. }
  78. return ts.forEachChild(node, cb);
  79. });
  80. function checkStatements(statements) {
  81. addFailures(checkOverloads(statements, undefined, function (statement) {
  82. if (utils.isFunctionDeclaration(statement)) {
  83. var body = statement.body, name = statement.name;
  84. return body === undefined && name !== undefined ? { signature: statement, key: name.text } : undefined;
  85. }
  86. else {
  87. return undefined;
  88. }
  89. }));
  90. }
  91. function checkMembers(members, typeParameters) {
  92. addFailures(checkOverloads(members, typeParameters, function (member) {
  93. switch (member.kind) {
  94. case ts.SyntaxKind.CallSignature:
  95. case ts.SyntaxKind.ConstructSignature:
  96. case ts.SyntaxKind.MethodSignature:
  97. break;
  98. case ts.SyntaxKind.MethodDeclaration:
  99. case ts.SyntaxKind.Constructor:
  100. if (member.body !== undefined) {
  101. return undefined;
  102. }
  103. break;
  104. default:
  105. return undefined;
  106. }
  107. var signature = member;
  108. var key = adjacentOverloadSignaturesRule_1.getOverloadKey(signature);
  109. return key === undefined ? undefined : { signature: signature, key: key };
  110. }));
  111. }
  112. function addFailures(failures) {
  113. for (var _i = 0, failures_1 = failures; _i < failures_1.length; _i++) {
  114. var failure = failures_1[_i];
  115. var unify = failure.unify, only2 = failure.only2;
  116. switch (unify.kind) {
  117. case "single-parameter-difference": {
  118. var p0 = unify.p0, p1 = unify.p1;
  119. var lineOfOtherOverload = only2 ? undefined : getLine(p0.getStart());
  120. ctx.addFailureAtNode(p1, Rule.FAILURE_STRING_SINGLE_PARAMETER_DIFFERENCE(lineOfOtherOverload, typeText(p0), typeText(p1)));
  121. break;
  122. }
  123. case "extra-parameter": {
  124. var extraParameter = unify.extraParameter, otherSignature = unify.otherSignature;
  125. var lineOfOtherOverload = only2 ? undefined : getLine(otherSignature.pos);
  126. ctx.addFailureAtNode(extraParameter, extraParameter.dotDotDotToken !== undefined
  127. ? Rule.FAILURE_STRING_OMITTING_REST_PARAMETER(lineOfOtherOverload)
  128. : Rule.FAILURE_STRING_OMITTING_SINGLE_PARAMETER(lineOfOtherOverload));
  129. }
  130. }
  131. }
  132. }
  133. function getLine(pos) {
  134. return ts.getLineAndCharacterOfPosition(sourceFile, pos).line + 1;
  135. }
  136. }
  137. function checkOverloads(signatures, typeParameters, getOverload) {
  138. var result = [];
  139. var isTypeParameter = getIsTypeParameter(typeParameters);
  140. for (var _i = 0, _a = collectOverloads(signatures, getOverload); _i < _a.length; _i++) {
  141. var overloads = _a[_i];
  142. if (overloads.length === 2) {
  143. var unify = compareSignatures(overloads[0], overloads[1], isTypeParameter);
  144. if (unify !== undefined) {
  145. result.push({ unify: unify, only2: true });
  146. }
  147. }
  148. else {
  149. forEachPair(overloads, function (a, b) {
  150. var unify = compareSignatures(a, b, isTypeParameter);
  151. if (unify !== undefined) {
  152. result.push({ unify: unify, only2: false });
  153. }
  154. });
  155. }
  156. }
  157. return result;
  158. }
  159. function compareSignatures(a, b, isTypeParameter) {
  160. if (!signaturesCanBeUnified(a, b, isTypeParameter)) {
  161. return undefined;
  162. }
  163. return a.parameters.length === b.parameters.length
  164. ? signaturesDifferBySingleParameter(a.parameters, b.parameters)
  165. : signaturesDifferByOptionalOrRestParameter(a.parameters, b.parameters);
  166. }
  167. function signaturesCanBeUnified(a, b, isTypeParameter) {
  168. // Must return the same type.
  169. return typesAreEqual(a.type, b.type) &&
  170. // Must take the same type parameters.
  171. utils_1.arraysAreEqual(a.typeParameters, b.typeParameters, typeParametersAreEqual) &&
  172. // If one uses a type parameter (from outside) and the other doesn't, they shouldn't be joined.
  173. signatureUsesTypeParameter(a, isTypeParameter) === signatureUsesTypeParameter(b, isTypeParameter);
  174. }
  175. /** Detect `a(x: number, y: number, z: number)` and `a(x: number, y: string, z: number)`. */
  176. function signaturesDifferBySingleParameter(types1, types2) {
  177. var index = getIndexOfFirstDifference(types1, types2, parametersAreEqual);
  178. if (index === undefined) {
  179. return undefined;
  180. }
  181. // If remaining arrays are equal, the signatures differ by just one parameter type
  182. if (!utils_1.arraysAreEqual(types1.slice(index + 1), types2.slice(index + 1), parametersAreEqual)) {
  183. return undefined;
  184. }
  185. var a = types1[index];
  186. var b = types2[index];
  187. // Can unify `a?: string` and `b?: number`. Can't unify `...args: string[]` and `...args: number[]`.
  188. // See https://github.com/Microsoft/TypeScript/issues/5077
  189. return parametersHaveEqualSigils(a, b) && a.dotDotDotToken === undefined
  190. ? { kind: "single-parameter-difference", p0: a, p1: b }
  191. : undefined;
  192. }
  193. /**
  194. * Detect `a(): void` and `a(x: number): void`.
  195. * Returns the parameter declaration (`x: number` in this example) that should be optional/rest, and overload it's a part of.
  196. */
  197. function signaturesDifferByOptionalOrRestParameter(sig1, sig2) {
  198. var minLength = Math.min(sig1.length, sig2.length);
  199. var longer = sig1.length < sig2.length ? sig2 : sig1;
  200. var shorter = sig1.length < sig2.length ? sig1 : sig2;
  201. // If one is has 2+ parameters more than the other, they must all be optional/rest.
  202. // Differ by optional parameters: f() and f(x), f() and f(x, ?y, ...z)
  203. // Not allowed: f() and f(x, y)
  204. for (var i = minLength + 1; i < longer.length; i++) {
  205. if (!parameterMayBeMissing(longer[i])) {
  206. return undefined;
  207. }
  208. }
  209. for (var i = 0; i < minLength; i++) {
  210. if (!typesAreEqual(sig1[i].type, sig2[i].type)) {
  211. return undefined;
  212. }
  213. }
  214. if (minLength > 0 && shorter[minLength - 1].dotDotDotToken !== undefined) {
  215. return undefined;
  216. }
  217. return { kind: "extra-parameter", extraParameter: longer[longer.length - 1], otherSignature: shorter };
  218. }
  219. /** Given type parameters, returns a function to test whether a type is one of those parameters. */
  220. function getIsTypeParameter(typeParameters) {
  221. if (typeParameters === undefined) {
  222. return function () { return false; };
  223. }
  224. var set = new Set();
  225. for (var _i = 0, typeParameters_1 = typeParameters; _i < typeParameters_1.length; _i++) {
  226. var t = typeParameters_1[_i];
  227. set.add(t.getText());
  228. }
  229. return function (typeName) { return set.has(typeName); };
  230. }
  231. /** True if any of the outer type parameters are used in a signature. */
  232. function signatureUsesTypeParameter(sig, isTypeParameter) {
  233. return sig.parameters.some(function (p) { return p.type !== undefined && typeContainsTypeParameter(p.type) === true; });
  234. function typeContainsTypeParameter(type) {
  235. if (utils.isTypeReferenceNode(type)) {
  236. var typeName = type.typeName;
  237. if (typeName.kind === ts.SyntaxKind.Identifier && isTypeParameter(typeName.text)) {
  238. return true;
  239. }
  240. }
  241. return ts.forEachChild(type, typeContainsTypeParameter);
  242. }
  243. }
  244. /**
  245. * Given all signatures, collects an array of arrays of signatures which are all overloads.
  246. * Does not rely on overloads being adjacent. This is similar to code in adjacentOverloadSignaturesRule.ts, but not the same.
  247. */
  248. function collectOverloads(nodes, getOverload) {
  249. var map = new Map();
  250. for (var _i = 0, nodes_1 = nodes; _i < nodes_1.length; _i++) {
  251. var sig = nodes_1[_i];
  252. var overload = getOverload(sig);
  253. if (overload === undefined) {
  254. continue;
  255. }
  256. var signature = overload.signature, key = overload.key;
  257. var overloads = map.get(key);
  258. if (overloads !== undefined) {
  259. overloads.push(signature);
  260. }
  261. else {
  262. map.set(key, [signature]);
  263. }
  264. }
  265. return Array.from(map.values());
  266. }
  267. function parametersAreEqual(a, b) {
  268. return parametersHaveEqualSigils(a, b) && typesAreEqual(a.type, b.type);
  269. }
  270. /** True for optional/rest parameters. */
  271. function parameterMayBeMissing(p) {
  272. return p.dotDotDotToken !== undefined || p.questionToken !== undefined;
  273. }
  274. /** False if one is optional and the other isn't, or one is a rest parameter and the other isn't. */
  275. function parametersHaveEqualSigils(a, b) {
  276. return (a.dotDotDotToken !== undefined) === (b.dotDotDotToken !== undefined) &&
  277. (a.questionToken !== undefined) === (b.questionToken !== undefined);
  278. }
  279. function typeParametersAreEqual(a, b) {
  280. return a.name.text === b.name.text && typesAreEqual(a.constraint, b.constraint);
  281. }
  282. function typesAreEqual(a, b) {
  283. // TODO: Could traverse AST so that formatting differences don't affect this.
  284. return a === b || a !== undefined && b !== undefined && a.getText() === b.getText();
  285. }
  286. /** Returns the first index where `a` and `b` differ. */
  287. function getIndexOfFirstDifference(a, b, equal) {
  288. for (var i = 0; i < a.length && i < b.length; i++) {
  289. if (!equal(a[i], b[i])) {
  290. return i;
  291. }
  292. }
  293. return undefined;
  294. }
  295. /** Calls `action` for every pair of values in `values`. */
  296. function forEachPair(values, action) {
  297. for (var i = 0; i < values.length; i++) {
  298. for (var j = i + 1; j < values.length; j++) {
  299. var result = action(values[i], values[j]);
  300. if (result !== undefined) {
  301. return result;
  302. }
  303. }
  304. }
  305. return undefined;
  306. }
  307. function typeText(_a) {
  308. var type = _a.type;
  309. return type === undefined ? "any" : type.getText();
  310. }