strictBooleanExpressionsRule.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. "use strict";
  2. /**
  3. * @license
  4. * Copyright 2016 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 tsutils_1 = require("tsutils");
  21. var ts = require("typescript");
  22. var Lint = require("../index");
  23. var OPTION_ALLOW_NULL_UNION = "allow-null-union";
  24. var OPTION_ALLOW_UNDEFINED_UNION = "allow-undefined-union";
  25. var OPTION_ALLOW_STRING = "allow-string";
  26. var OPTION_ALLOW_NUMBER = "allow-number";
  27. var OPTION_ALLOW_MIX = "allow-mix";
  28. var OPTION_ALLOW_BOOLEAN_OR_UNDEFINED = "allow-boolean-or-undefined";
  29. // tslint:disable object-literal-sort-keys
  30. var Rule = /** @class */ (function (_super) {
  31. tslib_1.__extends(Rule, _super);
  32. function Rule() {
  33. return _super !== null && _super.apply(this, arguments) || this;
  34. }
  35. Rule.prototype.applyWithProgram = function (sourceFile, program) {
  36. var options = parseOptions(this.ruleArguments, Lint.isStrictNullChecksEnabled(program.getCompilerOptions()));
  37. return this.applyWithFunction(sourceFile, walk, options, program.getTypeChecker());
  38. };
  39. Rule.metadata = {
  40. ruleName: "strict-boolean-expressions",
  41. description: Lint.Utils.dedent(templateObject_1 || (templateObject_1 = tslib_1.__makeTemplateObject(["\n Restricts the types allowed in boolean expressions. By default only booleans are allowed.\n\n The following nodes are checked:\n\n * Arguments to the `!`, `&&`, and `||` operators\n * The condition in a conditional expression (`cond ? x : y`)\n * Conditions for `if`, `for`, `while`, and `do-while` statements."], ["\n Restricts the types allowed in boolean expressions. By default only booleans are allowed.\n\n The following nodes are checked:\n\n * Arguments to the \\`!\\`, \\`&&\\`, and \\`||\\` operators\n * The condition in a conditional expression (\\`cond ? x : y\\`)\n * Conditions for \\`if\\`, \\`for\\`, \\`while\\`, and \\`do-while\\` statements."]))),
  42. optionsDescription: Lint.Utils.dedent(templateObject_2 || (templateObject_2 = tslib_1.__makeTemplateObject(["\n These options may be provided:\n\n * `", "` allows union types containing `null`.\n - It does *not* allow `null` itself.\n - Without the '--strictNullChecks' compiler option, this will allow anything other than a string, number, or enum.\n * `", "` allows union types containing `undefined`.\n - It does *not* allow `undefined` itself.\n - Without the '--strictNullChecks' compiler option, this will allow anything other than a string, number, or enum.\n * `", "` allows strings.\n - It does *not* allow unions containing `string`.\n - It does *not* allow string literal types.\n * `", "` allows numbers.\n - It does *not* allow unions containing `number`.\n - It does *not* allow enums or number literal types.\n * `", "` allows multiple of the above to appear together.\n - For example, `string | number` or `RegExp | null | undefined` would normally not be allowed.\n - A type like `\"foo\" | \"bar\" | undefined` is always allowed, because it has only one way to be false.\n * `", "` allows `boolean | undefined`.\n - Also allows `true | false | undefined`.\n - Does not allow `false | undefined`.\n - This option is a subset of `", "`, so you don't need to enable both options at the same time.\n "], ["\n These options may be provided:\n\n * \\`", "\\` allows union types containing \\`null\\`.\n - It does *not* allow \\`null\\` itself.\n - Without the '--strictNullChecks' compiler option, this will allow anything other than a string, number, or enum.\n * \\`", "\\` allows union types containing \\`undefined\\`.\n - It does *not* allow \\`undefined\\` itself.\n - Without the '--strictNullChecks' compiler option, this will allow anything other than a string, number, or enum.\n * \\`", "\\` allows strings.\n - It does *not* allow unions containing \\`string\\`.\n - It does *not* allow string literal types.\n * \\`", "\\` allows numbers.\n - It does *not* allow unions containing \\`number\\`.\n - It does *not* allow enums or number literal types.\n * \\`", "\\` allows multiple of the above to appear together.\n - For example, \\`string | number\\` or \\`RegExp | null | undefined\\` would normally not be allowed.\n - A type like \\`\"foo\" | \"bar\" | undefined\\` is always allowed, because it has only one way to be false.\n * \\`", "\\` allows \\`boolean | undefined\\`.\n - Also allows \\`true | false | undefined\\`.\n - Does not allow \\`false | undefined\\`.\n - This option is a subset of \\`", "\\`, so you don't need to enable both options at the same time.\n "])), OPTION_ALLOW_NULL_UNION, OPTION_ALLOW_UNDEFINED_UNION, OPTION_ALLOW_STRING, OPTION_ALLOW_NUMBER, OPTION_ALLOW_MIX, OPTION_ALLOW_BOOLEAN_OR_UNDEFINED, OPTION_ALLOW_UNDEFINED_UNION),
  43. options: {
  44. type: "array",
  45. items: {
  46. type: "string",
  47. enum: [
  48. OPTION_ALLOW_NULL_UNION,
  49. OPTION_ALLOW_UNDEFINED_UNION,
  50. OPTION_ALLOW_STRING,
  51. OPTION_ALLOW_NUMBER,
  52. OPTION_ALLOW_BOOLEAN_OR_UNDEFINED,
  53. ],
  54. },
  55. minLength: 0,
  56. maxLength: 5,
  57. },
  58. optionExamples: [
  59. true,
  60. [true, OPTION_ALLOW_NULL_UNION, OPTION_ALLOW_UNDEFINED_UNION, OPTION_ALLOW_STRING, OPTION_ALLOW_NUMBER],
  61. [true, OPTION_ALLOW_BOOLEAN_OR_UNDEFINED],
  62. ],
  63. type: "functionality",
  64. typescriptOnly: true,
  65. requiresTypeInfo: true,
  66. };
  67. return Rule;
  68. }(Lint.Rules.TypedRule));
  69. exports.Rule = Rule;
  70. function parseOptions(ruleArguments, strictNullChecks) {
  71. return {
  72. strictNullChecks: strictNullChecks,
  73. allowNullUnion: has(OPTION_ALLOW_NULL_UNION),
  74. allowUndefinedUnion: has(OPTION_ALLOW_UNDEFINED_UNION),
  75. allowString: has(OPTION_ALLOW_STRING),
  76. allowNumber: has(OPTION_ALLOW_NUMBER),
  77. allowMix: has(OPTION_ALLOW_MIX),
  78. allowBooleanOrUndefined: has(OPTION_ALLOW_BOOLEAN_OR_UNDEFINED),
  79. };
  80. function has(name) {
  81. return ruleArguments.indexOf(name) !== -1;
  82. }
  83. }
  84. function walk(ctx, checker) {
  85. var sourceFile = ctx.sourceFile, options = ctx.options;
  86. ts.forEachChild(sourceFile, function cb(node) {
  87. switch (node.kind) {
  88. case ts.SyntaxKind.BinaryExpression: {
  89. var b_1 = node;
  90. if (binaryBooleanExpressionKind(b_1) !== undefined) {
  91. var left = b_1.left, right = b_1.right;
  92. var checkHalf = function (expr) {
  93. // If it's another boolean binary expression, we'll check it when recursing.
  94. if (!isBooleanBinaryExpression(expr)) {
  95. checkExpression(expr, b_1);
  96. }
  97. };
  98. checkHalf(left);
  99. checkHalf(right);
  100. }
  101. break;
  102. }
  103. case ts.SyntaxKind.PrefixUnaryExpression: {
  104. var _a = node, operator = _a.operator, operand = _a.operand;
  105. if (operator === ts.SyntaxKind.ExclamationToken) {
  106. checkExpression(operand, node);
  107. }
  108. break;
  109. }
  110. case ts.SyntaxKind.IfStatement:
  111. case ts.SyntaxKind.WhileStatement:
  112. case ts.SyntaxKind.DoStatement: {
  113. var c = node;
  114. // If it's a boolean binary expression, we'll check it when recursing.
  115. if (!isBooleanBinaryExpression(c.expression)) {
  116. checkExpression(c.expression, c);
  117. }
  118. break;
  119. }
  120. case ts.SyntaxKind.ConditionalExpression:
  121. checkExpression(node.condition, node);
  122. break;
  123. case ts.SyntaxKind.ForStatement: {
  124. var condition = node.condition;
  125. if (condition !== undefined) {
  126. checkExpression(condition, node);
  127. }
  128. }
  129. }
  130. return ts.forEachChild(node, cb);
  131. });
  132. function checkExpression(node, location) {
  133. var type = checker.getTypeAtLocation(node);
  134. var failure = getTypeFailure(type, options);
  135. if (failure !== undefined) {
  136. if (failure === 0 /* AlwaysTruthy */ &&
  137. !options.strictNullChecks &&
  138. (options.allowNullUnion || options.allowUndefinedUnion)) {
  139. // OK; It might be null/undefined.
  140. return;
  141. }
  142. ctx.addFailureAtNode(node, showFailure(location, failure, isUnionType(type), options));
  143. }
  144. }
  145. }
  146. function getTypeFailure(type, options) {
  147. if (isUnionType(type)) {
  148. return handleUnion(type, options);
  149. }
  150. var kind = getKind(type);
  151. var failure = failureForKind(kind, /*isInUnion*/ false, options);
  152. if (failure !== undefined) {
  153. return failure;
  154. }
  155. switch (triState(kind)) {
  156. case true:
  157. // Allow 'any'. Allow 'true' itself, but not any other always-truthy type.
  158. // tslint:disable-next-line no-bitwise
  159. return tsutils_1.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.BooleanLiteral) ? undefined : 0 /* AlwaysTruthy */;
  160. case false:
  161. // Allow 'false' itself, but not any other always-falsy type
  162. return tsutils_1.isTypeFlagSet(type, ts.TypeFlags.BooleanLiteral) ? undefined : 1 /* AlwaysFalsy */;
  163. case undefined:
  164. return undefined;
  165. }
  166. }
  167. function isBooleanUndefined(type) {
  168. var isTruthy = false;
  169. for (var _i = 0, _a = type.types; _i < _a.length; _i++) {
  170. var ty = _a[_i];
  171. if (tsutils_1.isTypeFlagSet(ty, ts.TypeFlags.Boolean)) {
  172. isTruthy = true;
  173. }
  174. else if (tsutils_1.isTypeFlagSet(ty, ts.TypeFlags.BooleanLiteral)) {
  175. isTruthy = isTruthy || ty.intrinsicName === "true";
  176. }
  177. else if (!tsutils_1.isTypeFlagSet(ty, ts.TypeFlags.Void | ts.TypeFlags.Undefined)) {
  178. return undefined;
  179. }
  180. }
  181. return isTruthy;
  182. }
  183. function handleUnion(type, options) {
  184. if (options.allowBooleanOrUndefined) {
  185. switch (isBooleanUndefined(type)) {
  186. case true:
  187. return undefined;
  188. case false:
  189. return 1 /* AlwaysFalsy */;
  190. }
  191. }
  192. // Tracks whether it's possibly truthy.
  193. var anyTruthy = false;
  194. // Counts falsy kinds to see if there's a mix. Also tracks whether it's possibly falsy.
  195. var seenFalsy = 0;
  196. for (var _i = 0, _a = type.types; _i < _a.length; _i++) {
  197. var ty = _a[_i];
  198. var kind = getKind(ty);
  199. var failure = failureForKind(kind, /*isInUnion*/ true, options);
  200. if (failure !== undefined) {
  201. return failure;
  202. }
  203. switch (triState(kind)) {
  204. case true:
  205. anyTruthy = true;
  206. break;
  207. case false:
  208. seenFalsy++;
  209. break;
  210. default:
  211. anyTruthy = true;
  212. seenFalsy++;
  213. }
  214. }
  215. return seenFalsy === 0 ? 0 /* AlwaysTruthy */
  216. : !anyTruthy ? 1 /* AlwaysFalsy */
  217. : !options.allowMix && seenFalsy > 1 ? 7 /* Mixes */ : undefined;
  218. }
  219. /** Fails if a kind of falsiness is not allowed. */
  220. function failureForKind(kind, isInUnion, options) {
  221. switch (kind) {
  222. case 0 /* String */:
  223. case 1 /* FalseStringLiteral */:
  224. return options.allowString ? undefined : 2 /* String */;
  225. case 2 /* Number */:
  226. case 3 /* FalseNumberLiteral */:
  227. return options.allowNumber ? undefined : 3 /* Number */;
  228. case 8 /* Enum */:
  229. return 6 /* Enum */;
  230. case 6 /* Null */:
  231. return isInUnion && !options.allowNullUnion ? 4 /* Null */ : undefined;
  232. case 7 /* Undefined */:
  233. return isInUnion && !options.allowUndefinedUnion ? 5 /* Undefined */ : undefined;
  234. default:
  235. return undefined;
  236. }
  237. }
  238. /** Divides a type into always true, always false, or unknown. */
  239. function triState(kind) {
  240. switch (kind) {
  241. case 0 /* String */:
  242. case 2 /* Number */:
  243. case 4 /* Boolean */:
  244. case 8 /* Enum */:
  245. return undefined;
  246. case 6 /* Null */:
  247. case 7 /* Undefined */:
  248. case 3 /* FalseNumberLiteral */:
  249. case 1 /* FalseStringLiteral */:
  250. case 5 /* FalseBooleanLiteral */:
  251. return false;
  252. case 9 /* AlwaysTruthy */:
  253. return true;
  254. }
  255. }
  256. function getKind(type) {
  257. return is(ts.TypeFlags.String) ? 0 /* String */
  258. : is(ts.TypeFlags.Number) ? 2 /* Number */
  259. : is(ts.TypeFlags.Boolean) ? 4 /* Boolean */
  260. : is(ts.TypeFlags.Null) ? 6 /* Null */
  261. : is(ts.TypeFlags.Undefined | ts.TypeFlags.Void) ? 7 /* Undefined */
  262. : is(ts.TypeFlags.EnumLike) ? 8 /* Enum */
  263. : is(ts.TypeFlags.NumberLiteral) ?
  264. (numberLiteralIsZero(type) ? 3 /* FalseNumberLiteral */ : 9 /* AlwaysTruthy */)
  265. : is(ts.TypeFlags.StringLiteral) ?
  266. (stringLiteralIsEmpty(type) ? 1 /* FalseStringLiteral */ : 9 /* AlwaysTruthy */)
  267. : is(ts.TypeFlags.BooleanLiteral) ?
  268. (type.intrinsicName === "true" ? 9 /* AlwaysTruthy */ : 5 /* FalseBooleanLiteral */)
  269. : 9 /* AlwaysTruthy */;
  270. function is(flags) {
  271. return tsutils_1.isTypeFlagSet(type, flags);
  272. }
  273. }
  274. function numberLiteralIsZero(type) {
  275. // for compatibility with typescript@<2.4.0
  276. return type.value !== undefined ? type.value === 0 : type.text === "0";
  277. }
  278. function stringLiteralIsEmpty(type) {
  279. // for compatibility with typescript@<2.4.0
  280. return (type.value !== undefined ? type.value : type.text) === "";
  281. }
  282. /** Matches `&&` and `||` operators. */
  283. function isBooleanBinaryExpression(node) {
  284. return node.kind === ts.SyntaxKind.BinaryExpression && binaryBooleanExpressionKind(node) !== undefined;
  285. }
  286. function binaryBooleanExpressionKind(node) {
  287. switch (node.operatorToken.kind) {
  288. case ts.SyntaxKind.AmpersandAmpersandToken:
  289. return "&&";
  290. case ts.SyntaxKind.BarBarToken:
  291. return "||";
  292. default:
  293. return undefined;
  294. }
  295. }
  296. function stringOr(parts) {
  297. switch (parts.length) {
  298. case 1:
  299. return parts[0];
  300. case 2:
  301. return parts[0] + " or " + parts[1];
  302. default:
  303. var res = "";
  304. for (var i = 0; i < parts.length - 1; i++) {
  305. res += parts[i] + ", ";
  306. }
  307. return res + "or " + parts[parts.length - 1];
  308. }
  309. }
  310. function isUnionType(type) {
  311. return tsutils_1.isTypeFlagSet(type, ts.TypeFlags.Union) && !tsutils_1.isTypeFlagSet(type, ts.TypeFlags.Enum);
  312. }
  313. function showLocation(n) {
  314. switch (n.kind) {
  315. case ts.SyntaxKind.PrefixUnaryExpression:
  316. return "operand for the '!' operator";
  317. case ts.SyntaxKind.ConditionalExpression:
  318. return "condition";
  319. case ts.SyntaxKind.ForStatement:
  320. return "'for' condition";
  321. case ts.SyntaxKind.IfStatement:
  322. return "'if' condition";
  323. case ts.SyntaxKind.WhileStatement:
  324. return "'while' condition";
  325. case ts.SyntaxKind.DoStatement:
  326. return "'do-while' condition";
  327. case ts.SyntaxKind.BinaryExpression:
  328. return "operand for the '" + binaryBooleanExpressionKind(n) + "' operator";
  329. }
  330. }
  331. function showFailure(location, ty, unionType, options) {
  332. var expectedTypes = showExpectedTypes(options);
  333. var expected = expectedTypes.length === 1
  334. ? "Only " + expectedTypes[0] + "s are allowed"
  335. : "Allowed types are " + stringOr(expectedTypes);
  336. var tyFail = showTypeFailure(ty, unionType, options.strictNullChecks);
  337. return "This type is not allowed in the " + showLocation(location) + " because it " + tyFail + ". " + expected + ".";
  338. }
  339. function showExpectedTypes(options) {
  340. var parts = ["boolean"];
  341. if (options.allowNullUnion) {
  342. parts.push("null-union");
  343. }
  344. if (options.allowUndefinedUnion) {
  345. parts.push("undefined-union");
  346. }
  347. if (options.allowString) {
  348. parts.push("string");
  349. }
  350. if (options.allowNumber) {
  351. parts.push("number");
  352. }
  353. if (options.allowBooleanOrUndefined) {
  354. parts.push("boolean-or-undefined");
  355. }
  356. return parts;
  357. }
  358. function showTypeFailure(ty, unionType, strictNullChecks) {
  359. var is = unionType ? "could be" : "is";
  360. switch (ty) {
  361. case 0 /* AlwaysTruthy */:
  362. return strictNullChecks
  363. ? "is always truthy"
  364. : "is always truthy. It may be null/undefined, but neither " +
  365. ("'" + OPTION_ALLOW_NULL_UNION + "' nor '" + OPTION_ALLOW_UNDEFINED_UNION + "' is set");
  366. case 1 /* AlwaysFalsy */: return "is always falsy";
  367. case 2 /* String */: return is + " a string";
  368. case 3 /* Number */: return is + " a number";
  369. case 4 /* Null */: return is + " null";
  370. case 5 /* Undefined */: return is + " undefined";
  371. case 6 /* Enum */: return is + " an enum";
  372. case 7 /* Mixes */: return "unions more than one truthy/falsy type";
  373. }
  374. }
  375. var templateObject_1, templateObject_2;