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

strict-boolean-expressions: new option allow-boolean-or-undefined #2820

Merged
merged 3 commits into from
May 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 41 additions & 3 deletions src/rules/strictBooleanExpressionsRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const OPTION_ALLOW_UNDEFINED_UNION = "allow-undefined-union";
const OPTION_ALLOW_STRING = "allow-string";
const OPTION_ALLOW_NUMBER = "allow-number";
const OPTION_ALLOW_MIX = "allow-mix";
const OPTION_ALLOW_BOOLEAN_OR_UNDEFINED = "allow-boolean-or-undefined";

// tslint:disable object-literal-sort-keys switch-default

Expand Down Expand Up @@ -52,21 +53,33 @@ export class Rule extends Lint.Rules.TypedRule {
* \`${OPTION_ALLOW_NUMBER}\` allows numbers.
- It does *not* allow unions containing \`number\`.
- It does *not* allow enums or number literal types.
* \`${OPTION_ALLOW_MIX}\` allow multiple of the above to appear together.
* \`${OPTION_ALLOW_MIX}\` allows multiple of the above to appear together.
- For example, \`string | number\` or \`RegExp | null | undefined\` would normally not be allowed.
- A type like \`"foo" | "bar" | undefined\` is always allowed, because it has only one way to be false.`,
- A type like \`"foo" | "bar" | undefined\` is always allowed, because it has only one way to be false.
* \`${OPTION_ALLOW_BOOLEAN_OR_UNDEFINED}\` allows \`boolean | undefined\`.
- Also allows \`true | false | undefined\`.
- Does not allow \`false | undefined\`.
- This option is a subset of \`${OPTION_ALLOW_UNDEFINED_UNION}\`, so you don't need to enable both options at the same time.
`,
options: {
type: "array",
items: {
type: "string",
enum: [OPTION_ALLOW_NULL_UNION, OPTION_ALLOW_UNDEFINED_UNION, OPTION_ALLOW_STRING, OPTION_ALLOW_NUMBER],
enum: [
OPTION_ALLOW_NULL_UNION,
OPTION_ALLOW_UNDEFINED_UNION,
OPTION_ALLOW_STRING,
OPTION_ALLOW_NUMBER,
OPTION_ALLOW_BOOLEAN_OR_UNDEFINED,
],
},
minLength: 0,
maxLength: 5,
},
optionExamples: [
true,
[true, OPTION_ALLOW_NULL_UNION, OPTION_ALLOW_UNDEFINED_UNION, OPTION_ALLOW_STRING, OPTION_ALLOW_NUMBER],
[true, OPTION_ALLOW_BOOLEAN_OR_UNDEFINED],
],
type: "functionality",
typescriptOnly: true,
Expand All @@ -86,6 +99,7 @@ interface Options {
allowString: boolean;
allowNumber: boolean;
allowMix: boolean;
allowBooleanOrUndefined: boolean;
}

function parseOptions(ruleArguments: string[], strictNullChecks: boolean): Options {
Expand All @@ -96,6 +110,7 @@ function parseOptions(ruleArguments: string[], strictNullChecks: boolean): Optio
allowString: has(OPTION_ALLOW_STRING),
allowNumber: has(OPTION_ALLOW_NUMBER),
allowMix: has(OPTION_ALLOW_MIX),
allowBooleanOrUndefined: has(OPTION_ALLOW_BOOLEAN_OR_UNDEFINED),
};

function has(name: string): boolean {
Expand Down Expand Up @@ -196,7 +211,29 @@ function getTypeFailure(type: ts.Type, options: Options): TypeFailure | undefine
}
}

function isBooleanUndefined(type: ts.UnionType): boolean | undefined {
let isTruthy = false;
for (const ty of type.types) {
if (Lint.isTypeFlagSet(ty, ts.TypeFlags.Boolean)) {
isTruthy = true;
} else if (Lint.isTypeFlagSet(ty, ts.TypeFlags.BooleanLiteral)) {
isTruthy = isTruthy || (ty as ts.IntrinsicType).intrinsicName === "true";
} else if (!Lint.isTypeFlagSet(ty, ts.TypeFlags.Void | ts.TypeFlags.Undefined)) { // tslint:disable-line:no-bitwise
return undefined;
}
}
return isTruthy;
}

function handleUnion(type: ts.UnionType, options: Options): TypeFailure | undefined {
if (options.allowBooleanOrUndefined) {
switch (isBooleanUndefined(type)) {
case true:
return undefined;
case false:
return TypeFailure.AlwaysFalsy;
}
}
// Tracks whether it's possibly truthy.
let anyTruthy = false;
// Counts falsy kinds to see if there's a mix. Also tracks whether it's possibly falsy.
Expand Down Expand Up @@ -398,6 +435,7 @@ function showExpectedTypes(options: Options): string[] {
if (options.allowUndefinedUnion) { parts.push("undefined-union"); }
if (options.allowString) { parts.push("string"); }
if (options.allowNumber) { parts.push("number"); }
if (options.allowBooleanOrUndefined) { parts.push("boolean-or-undefined"); }
return parts;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
declare function get<T>(): T;

declare const bu: boolean | undefined;
if (get<boolean | undefined>()) {}
if (get<true | undefined>()) {}
if (get<true | false | undefined>()) {}
if (get<true | false | undefined | void>()) {}

if (get<true>()) {}
~~~~~~~~~~~ [err % ("'if' condition", 'is always truthy')]
if (get<true | true>()) {}
~~~~~~~~~~~~~~~~~~ [err % ("'if' condition", 'is always truthy')]
if (get<false | undefined>()) {}
~~~~~~~~~~~~~~~~~~~~~~~~ [err % ("'if' condition", 'is always falsy')]
if (get<undefined>()) {}
~~~~~~~~~~~~~~~~ [err % ("'if' condition", 'is always falsy')]

if (get<void>()) {}
~~~~~~~~~~~ [err % ("'if' condition", 'is always falsy')]

if (get<RegExp | undefined>()) {}
~~~~~~~~~~~~~~~~~~~~~~~~~ [err % ("'if' condition", 'could be undefined')]
if (get<RegExp | null>()) {}
~~~~~~~~~~~~~~~~~~~~ [err % ("'if' condition", 'could be null')]

// Type of the condition is actually boolean | RegExp, but OK since we check each part separately.
if (get<RegExp | undefined>() || get<boolean>()) {}
~~~~~~~~~~~~~~~~~~~~~~~~~ [err % ("operand for the '||' operator", 'could be undefined')]

// This still fails of course!
if (get<RegExp>() || get<RegExp>()) {}
~~~~~~~~~~~~~ [err % ("operand for the '||' operator", 'is always truthy')]
~~~~~~~~~~~~~ [err % ("operand for the '||' operator", 'is always truthy')]

if (get<number | undefined>()) {}
~~~~~~~~~~~~~~~~~~~~~~~~~ [err % ("'if' condition", 'could be undefined')]

[err]: This type is not allowed in the %s because it %s. Allowed types are boolean or boolean-or-undefined.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"strictNullChecks": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"strict-boolean-expressions": [true, "allow-boolean-or-undefined"]
}
}
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"options": ["log"]
},
"no-switch-case-fall-through": true,
"strict-boolean-expressions": [true, "allow-boolean-or-undefined"],
"switch-default": false,
"variable-name": [true,
"ban-keywords",
Expand Down