From 0861d20ca5a4f8c48eebbb1b746e510553006c17 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Mon, 22 Jul 2019 09:25:58 +1000 Subject: [PATCH 1/6] feat(eslint-plugin): add strict-type-predicates --- packages/eslint-plugin/ROADMAP.md | 2 +- packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/strict-type-predicates.ts | 358 ++++++++++ .../rules/strict-type-predicates.test.ts | 626 ++++++++++++++++++ 4 files changed, 987 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-plugin/src/rules/strict-type-predicates.ts create mode 100644 packages/eslint-plugin/tests/rules/strict-type-predicates.test.ts diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index bca59dc84cf..d1169c745ee 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -97,7 +97,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th | [`radix`] | 🌟 | [`radix`][radix] | | [`restrict-plus-operands`] | βœ… | [`@typescript-eslint/restrict-plus-operands`] | | [`strict-boolean-expressions`] | βœ… | [`@typescript-eslint/strict-boolean-expressions`] | -| [`strict-type-predicates`] | πŸ›‘ | N/A | +| [`strict-type-predicates`] | βœ… | [`@typescript-eslint/strict-type-predicates`] | | [`switch-default`] | 🌟 | [`default-case`][default-case] | | [`triple-equals`] | 🌟 | [`eqeqeq`][eqeqeq] | | [`typeof-compare`] | 🌟 | [`valid-typeof`][valid-typeof] | diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 5d2f53cce25..50011eb85c8 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -70,6 +70,7 @@ import returnAwait from './return-await'; import semi from './semi'; import spaceBeforeFunctionParen from './space-before-function-paren'; import strictBooleanExpressions from './strict-boolean-expressions'; +import strictTypePredicates from './strict-type-predicates'; import tripleSlashReference from './triple-slash-reference'; import typeAnnotationSpacing from './type-annotation-spacing'; import typedef from './typedef'; @@ -149,6 +150,7 @@ export default { semi: semi, 'space-before-function-paren': spaceBeforeFunctionParen, 'strict-boolean-expressions': strictBooleanExpressions, + 'strict-type-predicates': strictTypePredicates, 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, typedef: typedef, diff --git a/packages/eslint-plugin/src/rules/strict-type-predicates.ts b/packages/eslint-plugin/src/rules/strict-type-predicates.ts new file mode 100644 index 00000000000..2b78311d9f6 --- /dev/null +++ b/packages/eslint-plugin/src/rules/strict-type-predicates.ts @@ -0,0 +1,358 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import ts from 'typescript'; +import * as util from '../util'; +import { + isIdentifier, + isLiteralExpression, + isTypeFlagSet, + isUnionType, + isStrictCompilerOptionEnabled, +} from 'tsutils'; + +type Options = [ + { + typesToIgnore?: string[]; + } +]; +type MessageIds = + | 'expressionAlwaysFalse' + | 'expressionAlwaysTrue' + | 'badTypeof' + | 'useStrictlyUndefined' + | 'useStrictlyNotUndefined' + | 'useStrictlyNull' + | 'useStrictlyNotNull'; + +export default util.createRule({ + name: 'strict-type-predicates', + meta: { + type: 'suggestion', + docs: { + description: + 'Warns for type predicates that are always true or always false', + category: 'Best Practices', + recommended: false, + }, + messages: { + expressionAlwaysFalse: 'Expression is always false.', + expressionAlwaysTrue: 'Expression is always true.', + badTypeof: "Bad comparison for 'typeof'.", + useStrictlyUndefined: "Use '=== undefined' instead.", + useStrictlyNotUndefined: "Use '=== undefined' instead.", + useStrictlyNull: "Use '=== null' instead.", + useStrictlyNotNull: "Use '!== null' instead.", + }, + schema: [], + }, + defaultOptions: [{}], + create(context) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + const compilerOptions = parserServices.program.getCompilerOptions(); + + function checkEquals( + node: ts.BinaryExpression, + esNode: TSESTree.Node, + { isStrict, isPositive }: EqualsKind, + ): void { + isPositive; + + const exprPred = getTypePredicate(node, isStrict); + if (exprPred === undefined) { + return; + } + + if (exprPred.kind === TypePredicateKind.TypeofTypo) { + fail('badTypeof'); + return; + } + + const exprType = checker.getTypeAtLocation(exprPred.expression); + // TODO: could use checker.getBaseConstraintOfType to help with type parameters, but it's not publicly exposed. + if ( + isTypeFlagSet( + exprType, + ts.TypeFlags.Any | ts.TypeFlags.TypeParameter | ts.TypeFlags.Unknown, + ) + ) { + return; + } + + switch (exprPred.kind) { + case TypePredicateKind.Plain: { + const { predicate, isNullOrUndefined } = exprPred; + const value = getConstantBoolean(exprType, predicate); + // 'null'/'undefined' are the only two values *not* assignable to '{}'. + if ( + value !== undefined && + (isNullOrUndefined || !isEmptyType(checker, exprType)) + ) { + fail( + value === isPositive + ? 'expressionAlwaysTrue' + : 'expressionAlwaysFalse', + ); + } + break; + } + + case TypePredicateKind.NonStructNullUndefined: { + const result = testNonStrictNullUndefined(exprType); + if (result !== undefined) { + fail( + typeof result === 'boolean' + ? result === isPositive + ? 'expressionAlwaysTrue' + : 'expressionAlwaysFalse' + : result === 'null' + ? isPositive + ? 'useStrictlyNull' + : 'useStrictlyNotNull' + : isPositive + ? 'useStrictlyUndefined' + : 'useStrictlyNotUndefined', + ); + } + } + } + + function fail(failure: MessageIds): void { + context.report({ node: esNode, messageId: failure }); + } + } + + /** Detects a type predicate given `left === right`. */ + function getTypePredicate( + node: ts.BinaryExpression, + isStrictEquals: boolean, + ): TypePredicate | undefined { + const { left, right } = node; + const lr = getTypePredicateOneWay(left, right, isStrictEquals); + return lr !== undefined + ? lr + : getTypePredicateOneWay(right, left, isStrictEquals); + } + + /** Only gets the type predicate if the expression is on the left. */ + function getTypePredicateOneWay( + left: ts.Expression, + right: ts.Expression, + isStrictEquals: boolean, + ): TypePredicate | undefined { + switch (right.kind) { + case ts.SyntaxKind.TypeOfExpression: { + const expression = (right as ts.TypeOfExpression).expression; + if (!isLiteralExpression(left)) { + if ( + (isIdentifier(left) && left.text === 'undefined') || + left.kind === ts.SyntaxKind.NullKeyword || + left.kind === ts.SyntaxKind.TrueKeyword || + left.kind === ts.SyntaxKind.FalseKeyword + ) { + return { kind: TypePredicateKind.TypeofTypo }; + } + return undefined; + } + const predicate = getTypePredicateForKind(left.text); + return predicate === undefined + ? { kind: TypePredicateKind.TypeofTypo } + : { + expression, + isNullOrUndefined: left.text === 'undefined', + kind: TypePredicateKind.Plain, + predicate, + }; + } + + case ts.SyntaxKind.NullKeyword: + return nullOrUndefined(ts.TypeFlags.Null); + + case ts.SyntaxKind.Identifier: + if ( + (right as ts.Identifier).originalKeywordKind === + ts.SyntaxKind.UndefinedKeyword + ) { + return nullOrUndefined(undefinedFlags); + } + return undefined; + default: + return undefined; + } + + function nullOrUndefined(flags: ts.TypeFlags): TypePredicate { + return isStrictEquals + ? { + expression: left, + isNullOrUndefined: true, + kind: TypePredicateKind.Plain, + predicate: flagPredicate(flags), + } + : { + kind: TypePredicateKind.NonStructNullUndefined, + expression: left, + }; + } + } + + return isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks') + ? { + BinaryExpression(node: TSESTree.BinaryExpression) { + const equals = getEqualsKind(node.operator); + if (equals !== undefined) { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + checkEquals(tsNode as ts.BinaryExpression, node, equals); + } + }, + } + : // TODO: emit warning that strictNullChecks is required + {}; + }, +}); + +function isEmptyType(checker: ts.TypeChecker, type: ts.Type) { + return checker.typeToString(type) === '{}'; +} + +const undefinedFlags = ts.TypeFlags.Undefined | ts.TypeFlags.Void; + +type TypePredicate = + | PlainTypePredicate + | NonStrictNullUndefinedPredicate + | { kind: TypePredicateKind.TypeofTypo }; +interface PlainTypePredicate { + kind: TypePredicateKind.Plain; + expression: ts.Expression; + predicate: Predicate; + isNullOrUndefined: boolean; +} + +/** For `== null` and the like. */ +interface NonStrictNullUndefinedPredicate { + kind: TypePredicateKind.NonStructNullUndefined; + expression: ts.Expression; +} +const enum TypePredicateKind { + Plain, + NonStructNullUndefined, + TypeofTypo, +} +type Predicate = (type: ts.Type) => boolean; + +export interface EqualsKind { + isPositive: boolean; // True for "===" and "==" + isStrict: boolean; // True for "===" and "!==" +} + +export function getEqualsKind(operator: string): EqualsKind | undefined { + switch (operator) { + case '==': + return { isPositive: true, isStrict: false }; + case '===': + return { isPositive: true, isStrict: true }; + case '!=': + return { isPositive: false, isStrict: false }; + case '!==': + return { isPositive: false, isStrict: true }; + default: + return undefined; + } +} + +function unionParts(type: ts.Type) { + return isUnionType(type) ? type.types : [type]; +} + +function flagPredicate(testedFlag: ts.TypeFlags): Predicate { + return type => isTypeFlagSet(type, testedFlag); +} + +function getTypePredicateForKind(kind: string): Predicate | undefined { + switch (kind) { + case 'undefined': + return flagPredicate(undefinedFlags); + case 'boolean': + return flagPredicate(ts.TypeFlags.BooleanLike); + case 'number': + return flagPredicate(ts.TypeFlags.NumberLike); + case 'string': + return flagPredicate(ts.TypeFlags.StringLike); + case 'symbol': + return flagPredicate(ts.TypeFlags.ESSymbol); + case 'function': + return isFunction; + case 'object': { + // It's an object if it's not any of the above. + const allFlags = + ts.TypeFlags.Undefined | + ts.TypeFlags.Void | + ts.TypeFlags.BooleanLike | + ts.TypeFlags.NumberLike | + ts.TypeFlags.StringLike | + ts.TypeFlags.ESSymbol; + return type => !isTypeFlagSet(type, allFlags) && !isFunction(type); + } + default: + return undefined; + } +} + +function isFunction(t: ts.Type): boolean { + if ( + t.getConstructSignatures().length !== 0 || + t.getCallSignatures().length !== 0 + ) { + return true; + } + const symbol = t.getSymbol(); + return symbol !== undefined && symbol.getName() === 'Function'; +} + +/** Returns bool for always/never true, or a string to recommend strict equality. */ +function testNonStrictNullUndefined( + type: ts.Type, +): boolean | 'null' | 'undefined' | undefined { + let anyNull = false; + let anyUndefined = false; + let anyOther = false; + for (const ty of unionParts(type)) { + if (isTypeFlagSet(ty, ts.TypeFlags.Null)) { + anyNull = true; + } else if (isTypeFlagSet(ty, undefinedFlags)) { + anyUndefined = true; + } else { + anyOther = true; + } + } + + return !anyOther + ? true + : anyNull && anyUndefined + ? undefined + : anyNull + ? 'null' + : anyUndefined + ? 'undefined' + : false; +} + +/** Returns a boolean value if that should always be the result of a type predicate. */ +function getConstantBoolean( + type: ts.Type, + predicate: (t: ts.Type) => boolean, +): boolean | undefined { + let anyTrue = false; + let anyFalse = false; + for (const ty of unionParts(type)) { + if (predicate(ty)) { + anyTrue = true; + } else { + anyFalse = true; + } + + if (anyTrue && anyFalse) { + return undefined; + } + } + + return anyTrue; +} diff --git a/packages/eslint-plugin/tests/rules/strict-type-predicates.test.ts b/packages/eslint-plugin/tests/rules/strict-type-predicates.test.ts new file mode 100644 index 00000000000..22728faa590 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/strict-type-predicates.test.ts @@ -0,0 +1,626 @@ +import path from 'path'; +import rule from '../../src/rules/strict-type-predicates'; +import { RuleTester } from '../RuleTester'; + +const rootDir = path.resolve(__dirname, '../fixtures/'); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('strict-type-predicates', rule, { + valid: [ + ` +declare function get(): T; + +// typeof undefined +typeof get() === "undefined"; +typeof get() === "undefined"; + +// typeof boolean +typeof get() === "boolean"; +typeof get<{}>() === "boolean"; + +// typeof string +typeof get<"abc" | undefined>() === "string"; + +// typeof symbol +typeof get() === "symbol"; + +// typeof function +{ + typeof get void)>() === "function"; + + // Works with union + class Foo { } + typeof get() === "function"; +} + +// typeof object +typeof get<{}>() === "object"; + +// === null / undefined +// get() === null; +// get() === undefined; +// get() == null; +// get() != undefined; + +// negation +get() !== null; +get() !== undefined; +get() !== null; +get() !== undefined; + +// type parameters +{ + function f(t: T) { + typeof t === "boolean"; + } + + // TODO: Would be nice to catch this. + function g(t: T) { + typeof t === "boolean"; + } + + function f(t: T) { + typeof t === "boolean"; + } +} + +// Detects bad typeof +{ + typeof get() === \`string\`; + let a: string, b: string; + typeof a === typeof b; + typeof a === b; + a === typeof b; +} + +// unknown +typeof get() === "undefined"; +typeof get() === "boolean"; +typeof get() === "number"; +typeof get() === "string"; +typeof get() === "symbol"; +typeof get() === "function"; +typeof get() === "object"; +"string" === typeof get(); +undefined === get(); + +// other +{ + const body: unknown = 'test'; + if (typeof body === 'object') + console.log('a'); + + let test: unknown = undefined; + if (test !== undefined) + console.log('b'); +} + `, + ], + + invalid: [ + // typeof undefined + { + code: ` +declare function get(): T; +typeof get() === "undefined";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "undefined";`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +// 'undefined' is not assignable to '{}' +typeof get<{}>() === "undefined";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 4, + column: 1, + }, + ], + }, + + // typeof boolean + { + code: ` +declare function get(): T; +typeof get() === "boolean";`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "boolean";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // typeof number + { + code: ` +declare function get(): T; +enum E {} +typeof get() === "number";`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 4, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "number";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // typeof string + { + code: ` +declare function get(): T; +typeof get<"abc" | "def">() === "string";`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "string";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // typeof symbol + { + code: ` +declare function get(): T; +typeof get() === "symbol";`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "symbol";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // typeof function + { + code: ` +declare function get(): T; +typeof get<() => void>() === "function";`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "function";`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "function";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +class X {} +typeof X === "function";`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +class X {} +typeof X === "object";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // typeof object + { + code: ` +declare function get(): T; +typeof get void) | Function>() === "object";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // === null / undefined + { + code: ` +declare function get(): T; +get() === null;`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() === undefined;`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // 'null' and 'undefined' are not assignable to '{}' + { + code: ` +declare function get(): T; +get<{}>() === null;`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get<{}>() === undefined;`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() == null;`, + errors: [ + { + messageId: 'useStrictlyUndefined', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() == undefined;`, + errors: [ + { + messageId: 'useStrictlyNull', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() == null;`, + errors: [ + { + messageId: 'useStrictlyNull', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() == undefined;`, + errors: [ + { + messageId: 'useStrictlyUndefined', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() != null;`, + errors: [ + { + messageId: 'useStrictlyNotUndefined', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get<{}>() == null;`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() == null;`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() != undefined;`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // negation + { + code: ` +declare function get(): T; +get() !== null;`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +get() !== undefined;`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() !== "string";`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + + // reverse left/right + { + code: ` +declare function get(): T; +"string" === typeof get();`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +undefined === get();`, + errors: [ + { + messageId: 'expressionAlwaysTrue', + line: 3, + column: 1, + }, + ], + }, + + // Detects bad typeof + { + code: ` +declare function get(): T; +typeof get() === true;`, + errors: [ + { + messageId: 'badTypeof', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "orbject";`, + errors: [ + { + messageId: 'badTypeof', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === \`stirng\`;`, + errors: [ + { + messageId: 'badTypeof', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof get() === "unknown";`, + errors: [ + { + messageId: 'badTypeof', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +typeof a === undefined;`, + errors: [ + { + messageId: 'expressionAlwaysFalse', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +undefined === typeof a;`, + errors: [ + { + messageId: 'badTypeof', + line: 3, + column: 1, + }, + ], + }, + { + code: ` +declare function get(): T; +null === typeof b;`, + errors: [ + { + messageId: 'badTypeof', + line: 3, + column: 1, + }, + ], + }, + ], +}); From 5a642e1ad2817a38fc13d3b55922738596f9ab46 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Sat, 7 Sep 2019 19:49:36 +1000 Subject: [PATCH 2/6] fix(eslint-plugin): [strict-type-predicates] add rule doc --- .../docs/rules/strict-type-predicates.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/strict-type-predicates.md diff --git a/packages/eslint-plugin/docs/rules/strict-type-predicates.md b/packages/eslint-plugin/docs/rules/strict-type-predicates.md new file mode 100644 index 00000000000..2b6c6118e41 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/strict-type-predicates.md @@ -0,0 +1,56 @@ +# Avoid type predicates that are always true or false (strict-type-predicates) + +Warns for type predicates that are always true or always false. Works for +`typeof` comparisons to constants (e.g. `typeof foo === 'string'`), and equality +comparison to `null`/`undefined`. (TypeScript won’t let you compare `1 === 2`, +but it has an exception for `1 === undefined`.) Does not yet work for +`instanceof`. Does not warn for `if (x.y)` where `x.y` is always truthy. For +that, see [`strict-boolean-expressions`](./strict-boolean-expressions.md). + +This rule requires `strictNullChecks` to be enabled. + +Examples of **incorrect** code for this rule: + +```ts +const numberOrNull: number | null = 0; +// Implicitly checks for `!== undefined`, which is always false. +if (numberOrNull != null) { + return; +} + +const numberOrUndefined: number | undefined = 0; +// Implicitly checks for `!== null`, which is always false. +if (numberOrNull != undefined) { + return; +} + +const number: number = 0; +// Always false. +if (typeof number === 'string') { + return; +} +``` + +Examples of **correct** code for this rule: + +```ts +const numberOrNull: number | null = 0; +if (numberOrNull !== null) { + return; +} + +const numberOrUndefined: number | undefined = 0; +if (numberOrNull !== undefined) { + return; +} + +const number: number = 0; +// Always false. +if (typeof number === 'number') { + return; +} +``` + +## Related To + +- TSLint: [strict-type-predicates](https://palantir.github.io/tslint/rules/strict-type-predicates) From 1df0433b9fe2b20200f96a7e2d86655e4ace9afe Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Mon, 9 Sep 2019 08:23:32 +1000 Subject: [PATCH 3/6] fix(eslint-plugin): add strict-type-predicates to readme --- packages/eslint-plugin/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index a332bafec80..b49b9b71dd9 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -168,6 +168,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | | | [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | | [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | :heavy_check_mark: | | | +| [`@typescript-eslint/strict-type-predicates`](./docs/rules/strict-type-predicates.md) | Disallow always true (or false) type predicates | | | :thought_balloon: | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | :heavy_check_mark: | | :thought_balloon: | From 97fa25321a616143a0a1bde156225dc7357083e3 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Mon, 9 Sep 2019 08:27:40 +1000 Subject: [PATCH 4/6] fix(eslint-plugin): yarn generate:configs --- packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/strict-type-predicates.ts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index f9eed1fca9c..adcb691ec21 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -90,6 +90,7 @@ "space-before-function-paren": "off", "@typescript-eslint/space-before-function-paren": "error", "@typescript-eslint/strict-boolean-expressions": "error", + "@typescript-eslint/strict-type-predicates": "error", "@typescript-eslint/triple-slash-reference": "error", "@typescript-eslint/type-annotation-spacing": "error", "@typescript-eslint/typedef": "error", diff --git a/packages/eslint-plugin/src/rules/strict-type-predicates.ts b/packages/eslint-plugin/src/rules/strict-type-predicates.ts index 2b78311d9f6..b9b525bcfa0 100644 --- a/packages/eslint-plugin/src/rules/strict-type-predicates.ts +++ b/packages/eslint-plugin/src/rules/strict-type-predicates.ts @@ -12,7 +12,7 @@ import { type Options = [ { typesToIgnore?: string[]; - } + }, ]; type MessageIds = | 'expressionAlwaysFalse' @@ -28,8 +28,7 @@ export default util.createRule({ meta: { type: 'suggestion', docs: { - description: - 'Warns for type predicates that are always true or always false', + description: 'Disallow always true (or false) type predicates', category: 'Best Practices', recommended: false, }, From a05498208c55ec77fea1f37e2207594bede0c433 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Wed, 8 Jan 2020 12:09:01 +1100 Subject: [PATCH 5/6] fix(eslint-plugin): fix docs for strict-type-predicates --- packages/eslint-plugin/README.md | 2 +- packages/eslint-plugin/docs/rules/strict-type-predicates.md | 2 +- packages/eslint-plugin/src/rules/strict-type-predicates.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index b49b9b71dd9..4e9c93e5817 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -167,8 +167,8 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | | [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | | | [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | -| [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | :heavy_check_mark: | | | | [`@typescript-eslint/strict-type-predicates`](./docs/rules/strict-type-predicates.md) | Disallow always true (or false) type predicates | | | :thought_balloon: | +| [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | :heavy_check_mark: | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | :heavy_check_mark: | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/strict-type-predicates.md b/packages/eslint-plugin/docs/rules/strict-type-predicates.md index 2b6c6118e41..bb84ae9f928 100644 --- a/packages/eslint-plugin/docs/rules/strict-type-predicates.md +++ b/packages/eslint-plugin/docs/rules/strict-type-predicates.md @@ -1,4 +1,4 @@ -# Avoid type predicates that are always true or false (strict-type-predicates) +# Disallow always true (or false) type predicates (`strict-type-predicates`) Warns for type predicates that are always true or always false. Works for `typeof` comparisons to constants (e.g. `typeof foo === 'string'`), and equality diff --git a/packages/eslint-plugin/src/rules/strict-type-predicates.ts b/packages/eslint-plugin/src/rules/strict-type-predicates.ts index b9b525bcfa0..891840bdc01 100644 --- a/packages/eslint-plugin/src/rules/strict-type-predicates.ts +++ b/packages/eslint-plugin/src/rules/strict-type-predicates.ts @@ -31,6 +31,7 @@ export default util.createRule({ description: 'Disallow always true (or false) type predicates', category: 'Best Practices', recommended: false, + requiresTypeChecking: true, }, messages: { expressionAlwaysFalse: 'Expression is always false.', From f5f1d27887854fc2365f222d310532729b7e5525 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Wed, 8 Jan 2020 12:11:40 +1100 Subject: [PATCH 6/6] fix(eslint-plugin): lint --- .../src/rules/strict-type-predicates.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/strict-type-predicates.ts b/packages/eslint-plugin/src/rules/strict-type-predicates.ts index 891840bdc01..159e38b7422 100644 --- a/packages/eslint-plugin/src/rules/strict-type-predicates.ts +++ b/packages/eslint-plugin/src/rules/strict-type-predicates.ts @@ -1,5 +1,5 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; -import ts from 'typescript'; +import * as ts from 'typescript'; import * as util from '../util'; import { isIdentifier, @@ -196,7 +196,7 @@ export default util.createRule({ return isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks') ? { - BinaryExpression(node: TSESTree.BinaryExpression) { + BinaryExpression(node: TSESTree.BinaryExpression): void { const equals = getEqualsKind(node.operator); if (equals !== undefined) { const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); @@ -209,7 +209,7 @@ export default util.createRule({ }, }); -function isEmptyType(checker: ts.TypeChecker, type: ts.Type) { +function isEmptyType(checker: ts.TypeChecker, type: ts.Type): boolean { return checker.typeToString(type) === '{}'; } @@ -258,12 +258,12 @@ export function getEqualsKind(operator: string): EqualsKind | undefined { } } -function unionParts(type: ts.Type) { +function unionParts(type: ts.Type): ts.Type[] { return isUnionType(type) ? type.types : [type]; } function flagPredicate(testedFlag: ts.TypeFlags): Predicate { - return type => isTypeFlagSet(type, testedFlag); + return (type): boolean => isTypeFlagSet(type, testedFlag); } function getTypePredicateForKind(kind: string): Predicate | undefined { @@ -289,7 +289,8 @@ function getTypePredicateForKind(kind: string): Predicate | undefined { ts.TypeFlags.NumberLike | ts.TypeFlags.StringLike | ts.TypeFlags.ESSymbol; - return type => !isTypeFlagSet(type, allFlags) && !isFunction(type); + return (type): boolean => + !isTypeFlagSet(type, allFlags) && !isFunction(type); } default: return undefined;