diff --git a/.gitignore b/.gitignore index 3663a5a5..7793d79d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /tsgolint internal/collections/ e2e/node_modules/ -node_modules/ \ No newline at end of file +node_modules/ + +.idea/ diff --git a/cmd/tsgolint/main.go b/cmd/tsgolint/main.go index 701d4dbe..11918161 100644 --- a/cmd/tsgolint/main.go +++ b/cmd/tsgolint/main.go @@ -57,6 +57,7 @@ import ( "github.com/typescript-eslint/tsgolint/internal/rules/restrict_plus_operands" "github.com/typescript-eslint/tsgolint/internal/rules/restrict_template_expressions" "github.com/typescript-eslint/tsgolint/internal/rules/return_await" + "github.com/typescript-eslint/tsgolint/internal/rules/strict_boolean_expressions" "github.com/typescript-eslint/tsgolint/internal/rules/switch_exhaustiveness_check" "github.com/typescript-eslint/tsgolint/internal/rules/unbound_method" "github.com/typescript-eslint/tsgolint/internal/rules/use_unknown_in_catch_callback_variable" @@ -156,6 +157,7 @@ var allRules = []rule.Rule{ restrict_plus_operands.RestrictPlusOperandsRule, restrict_template_expressions.RestrictTemplateExpressionsRule, return_await.ReturnAwaitRule, + strict_boolean_expressions.StrictBooleanExpressionsRule, switch_exhaustiveness_check.SwitchExhaustivenessCheckRule, unbound_method.UnboundMethodRule, use_unknown_in_catch_callback_variable.UseUnknownInCatchCallbackVariableRule, diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go new file mode 100644 index 00000000..44638eb1 --- /dev/null +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -0,0 +1,570 @@ +package strict_boolean_expressions + +import ( + "github.com/microsoft/typescript-go/shim/ast" + "github.com/microsoft/typescript-go/shim/checker" + "github.com/microsoft/typescript-go/shim/core" + "github.com/typescript-eslint/tsgolint/internal/rule" + "github.com/typescript-eslint/tsgolint/internal/utils" +) + +type StrictBooleanExpressionsOptions struct { + AllowAny *bool + AllowNullableBoolean *bool + AllowNullableNumber *bool + AllowNullableString *bool + AllowNullableEnum *bool + AllowNullableObject *bool + AllowString *bool + AllowNumber *bool + AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing *bool +} + +func buildUnexpectedAny() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedAny", + Description: "Unexpected any value in conditional. An explicit comparison or type cast is required.", + } +} + +func buildUnexpectedNullableBoolean() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedNullableBoolean", + Description: "Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly.", + } +} + +func buildUnexpectedNullableString() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedNullableString", + Description: "Unexpected nullable string value in conditional. Please handle the nullish case explicitly.", + } +} + +func buildUnexpectedNullableNumber() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedNullableNumber", + Description: "Unexpected nullable number value in conditional. Please handle the nullish case explicitly.", + } +} + +func buildUnexpectedNullableEnum() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedNullableEnum", + Description: "Unexpected nullable enum value in conditional. Please handle the nullish case explicitly.", + } +} + +func buildUnexpectedNullableObject() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedNullableObject", + Description: "Unexpected nullable object value in conditional. An explicit null check is required.", + } +} + +func buildUnexpectedNullish() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedNullish", + Description: "Unexpected nullish value in conditional. An explicit null check is required.", + } +} + +func buildUnexpectedString() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedString", + Description: "Unexpected string value in conditional. An explicit empty string check is required.", + } +} + +func buildUnexpectedNumber() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedNumber", + Description: "Unexpected number value in conditional. An explicit zero/NaN check is required.", + } +} + +func buildUnexpectedObject() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedObject", + Description: "Unexpected object value in conditional. The condition is always true.", + } +} + +func buildUnexpectedMixedCondition() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedMixedCondition", + Description: "Unexpected mixed type in conditional. The constituent types do not have a best common type.", + } +} + +func buildNoStrictNullCheck() rule.RuleMessage { + return rule.RuleMessage{ + Id: "noStrictNullCheck", + Description: "This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.", + } +} + +func buildPredicateCannotBeAsync() rule.RuleMessage { + return rule.RuleMessage{ + Id: "predicateCannotBeAsync", + Description: "Predicate function should not be 'async'; expected a boolean return type.", + } +} + +var traversedNodes = utils.Set[*ast.Node]{} + +var StrictBooleanExpressionsRule = rule.Rule{ + Name: "strict-boolean-expressions", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + opts, ok := options.(StrictBooleanExpressionsOptions) + if !ok { + opts = StrictBooleanExpressionsOptions{} + } + + if opts.AllowAny == nil { + opts.AllowAny = utils.Ref(false) + } + if opts.AllowNullableBoolean == nil { + opts.AllowNullableBoolean = utils.Ref(false) + } + if opts.AllowNullableNumber == nil { + opts.AllowNullableNumber = utils.Ref(false) + } + if opts.AllowNullableString == nil { + opts.AllowNullableString = utils.Ref(false) + } + if opts.AllowNullableEnum == nil { + opts.AllowNullableEnum = utils.Ref(false) + } + if opts.AllowNullableObject == nil { + opts.AllowNullableObject = utils.Ref(true) + } + if opts.AllowString == nil { + opts.AllowString = utils.Ref(true) + } + if opts.AllowNumber == nil { + opts.AllowNumber = utils.Ref(true) + } + if opts.AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing == nil { + opts.AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing = utils.Ref(false) + } + + compilerOptions := ctx.Program.Options() + if !*opts.AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing && + !utils.IsStrictCompilerOptionEnabled(compilerOptions, compilerOptions.StrictNullChecks) { + ctx.ReportRange( + core.NewTextRange(0, 0), + buildNoStrictNullCheck(), + ) + } + + return rule.RuleListeners{ + ast.KindIfStatement: func(node *ast.Node) { + ifStmt := node.AsIfStatement() + traverseNode(ctx, ifStmt.Expression, opts, true) + }, + ast.KindWhileStatement: func(node *ast.Node) { + whileStmt := node.AsWhileStatement() + traverseNode(ctx, whileStmt.Expression, opts, true) + }, + ast.KindDoStatement: func(node *ast.Node) { + doStmt := node.AsDoStatement() + traverseNode(ctx, doStmt.Expression, opts, true) + }, + ast.KindForStatement: func(node *ast.Node) { + forStmt := node.AsForStatement() + if forStmt.Condition != nil { + traverseNode(ctx, forStmt.Condition, opts, true) + } + }, + ast.KindConditionalExpression: func(node *ast.Node) { + condExpr := node.AsConditionalExpression() + traverseNode(ctx, condExpr.Condition, opts, true) + }, + ast.KindBinaryExpression: func(node *ast.Node) { + binExpr := node.AsBinaryExpression() + if ast.IsLogicalExpression(node) && binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { + traverseLogicalExpression(ctx, binExpr, opts, false) + } + }, + ast.KindCallExpression: func(node *ast.Node) { + callExpr := node.AsCallExpression() + + assertedArgument := findTruthinessAssertedArgument(ctx.TypeChecker, callExpr) + if assertedArgument != nil { + traverseNode(ctx, assertedArgument, opts, true) + } + + if utils.IsArrayMethodCallWithPredicate(ctx.TypeChecker, callExpr) { + if callExpr.Arguments != nil && len(callExpr.Arguments.Nodes) > 0 { + arg := callExpr.Arguments.Nodes[0] + if arg == nil { + return + } + isFunction := arg.Kind&(ast.KindArrowFunction|ast.KindFunctionExpression|ast.KindFunctionDeclaration) != 0 + if isFunction && checker.GetFunctionFlags(arg)&checker.FunctionFlagsAsync != 0 { + ctx.ReportNode(arg, buildPredicateCannotBeAsync()) + return + } + funcType := ctx.TypeChecker.GetTypeAtLocation(arg) + signatures := ctx.TypeChecker.GetCallSignatures(funcType) + var types []*checker.Type + for _, signature := range signatures { + returnType := ctx.TypeChecker.GetReturnTypeOfSignature(signature) + typeFlags := checker.Type_flags(returnType) + if typeFlags&checker.TypeFlagsTypeParameter != 0 { + constraint := ctx.TypeChecker.GetConstraintOfTypeParameter(returnType) + if constraint != nil { + returnType = constraint + } + } + + types = append(types, utils.UnionTypeParts(returnType)...) + } + checkCondition(ctx, node, types, opts) + } + } + }, + ast.KindPrefixUnaryExpression: func(node *ast.Node) { + unaryExpr := node.AsPrefixUnaryExpression() + if unaryExpr.Operator == ast.KindExclamationToken { + traverseNode(ctx, unaryExpr.Operand, opts, true) + } + }, + } + }, +} + +func findTruthinessAssertedArgument(typeChecker *checker.Checker, callExpr *ast.CallExpression) *ast.Node { + var checkableArguments []*ast.Node + for _, argument := range callExpr.Arguments.Nodes { + if argument.Kind == ast.KindSpreadElement { + break + } + checkableArguments = append(checkableArguments, argument) + } + if len(checkableArguments) == 0 { + return nil + } + node := callExpr.AsNode() + signature := typeChecker.GetResolvedSignature(node) + if signature == nil { + return nil + } + firstTypePredicateResult := typeChecker.GetTypePredicateOfSignature(signature) + if firstTypePredicateResult == nil { + return nil + } + if !(checker.TypePredicate_kind(firstTypePredicateResult) == checker.TypePredicateKindAssertsIdentifier && + checker.TypePredicate_t(firstTypePredicateResult) == nil) { + return nil + } + parameterIndex := checker.TypePredicate_parameterIndex(firstTypePredicateResult) + if int(parameterIndex) >= len(checkableArguments) { + return nil + } + return checkableArguments[parameterIndex] +} + +func checkNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpressionsOptions) { + nodeType := utils.GetConstrainedTypeAtLocation(ctx.TypeChecker, node) + checkCondition(ctx, node, utils.UnionTypeParts(nodeType), opts) +} + +func traverseLogicalExpression(ctx rule.RuleContext, binExpr *ast.BinaryExpression, opts StrictBooleanExpressionsOptions, isCondition bool) { + traverseNode(ctx, binExpr.Left, opts, true) + traverseNode(ctx, binExpr.Right, opts, isCondition) +} + +func traverseNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpressionsOptions, isCondition bool) { + if traversedNodes.Has(node) { + return + } + traversedNodes.Add(node) + + if node.Kind == ast.KindParenthesizedExpression { + traverseNode(ctx, node.AsParenthesizedExpression().Expression, opts, isCondition) + return + } + + if node.Kind == ast.KindBinaryExpression { + binExpr := node.AsBinaryExpression() + if ast.IsLogicalExpression(node) && binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { + traverseLogicalExpression(ctx, binExpr, opts, isCondition) + return + } + } + + if !isCondition { + return + } + + checkNode(ctx, node, opts) +} + +// Type analysis types +type typeVariant int + +const ( + typeVariantNullish typeVariant = iota + typeVariantBoolean + typeVariantString + typeVariantNumber + typeVariantBigInt + typeVariantObject + typeVariantAny + typeVariantUnknown + typeVariantNever + typeVariantMixed + typeVariantGeneric +) + +type typeInfo struct { + variant typeVariant + isNullable bool + isTruthy bool + types []*checker.Type + isUnion bool + isIntersection bool + isEnum bool +} + +func analyzeTypeParts(typeChecker *checker.Checker, types []*checker.Type) typeInfo { + info := typeInfo{ + isUnion: len(types) > 1, + types: types, + } + variants := make(map[typeVariant]bool) + + metNotTruthy := false + + for _, part := range info.types { + partInfo := analyzeTypePart(typeChecker, part) + variants[partInfo.variant] = true + if partInfo.variant == typeVariantNullish { + info.isNullable = true + } + if partInfo.isEnum { + info.isEnum = true + } + if partInfo.variant == typeVariantBoolean || partInfo.variant == typeVariantNumber || partInfo.variant == typeVariantString { + if metNotTruthy { + continue + } + info.isTruthy = partInfo.isTruthy + metNotTruthy = !partInfo.isTruthy + } + } + + if len(variants) == 1 { + for v := range variants { + info.variant = v + } + } else if len(variants) == 2 && info.isNullable { + for v := range variants { + if v != typeVariantNullish { + info.variant = v + break + } + } + } else { + info.variant = typeVariantMixed + } + + return info +} + +func analyzeTypePart(_typeChecker *checker.Checker, t *checker.Type) typeInfo { + info := typeInfo{} + flags := checker.Type_flags(t) + + if utils.IsIntersectionType(t) { + info.isIntersection = true + types := t.Types() + isBoolean := false + for _, t2 := range types { + if analyzeTypePart(_typeChecker, t2).variant == typeVariantBoolean { + isBoolean = true + break + } + } + if isBoolean { + info.variant = typeVariantBoolean + } else { + info.variant = typeVariantObject + } + return info + } + + if flags&checker.TypeFlagsTypeParameter != 0 { + info.variant = typeVariantGeneric + return info + } + + if flags&checker.TypeFlagsAny != 0 { + info.variant = typeVariantAny + return info + } + + if flags&checker.TypeFlagsUnknown != 0 { + info.variant = typeVariantUnknown + return info + } + + if flags&checker.TypeFlagsNever != 0 { + info.variant = typeVariantNever + return info + } + + if flags&(checker.TypeFlagsNull|checker.TypeFlagsUndefined|checker.TypeFlagsVoid) != 0 { + info.variant = typeVariantNullish + return info + } + + if flags&(checker.TypeFlagsBoolean|checker.TypeFlagsBooleanLiteral|checker.TypeFlagsBooleanLike) != 0 { + if t.AsLiteralType().String() == "true" { + info.isTruthy = true + } + info.variant = typeVariantBoolean + return info + } + + if flags&(checker.TypeFlagsEnum|checker.TypeFlagsEnumLiteral|checker.TypeFlagsEnumLike) != 0 { + if flags&checker.TypeFlagsStringLiteral != 0 { + info.variant = typeVariantString + } else { + info.variant = typeVariantNumber + } + info.isEnum = true + return info + } + + if flags&(checker.TypeFlagsString|checker.TypeFlagsStringLiteral|checker.TypeFlagsStringLike) != 0 { + info.variant = typeVariantString + if t.IsStringLiteral() { + literal := t.AsLiteralType() + if literal != nil && literal.Value() != "" { + info.isTruthy = true + } + } + return info + } + + if flags&(checker.TypeFlagsNumber|checker.TypeFlagsNumberLiteral|checker.TypeFlagsNumberLike) != 0 { + info.variant = typeVariantNumber + if t.IsNumberLiteral() { + literal := t.AsLiteralType() + if literal != nil && literal.String() != "0" { + info.isTruthy = true + } + } + return info + } + + if flags&(checker.TypeFlagsBigInt|checker.TypeFlagsBigIntLiteral) != 0 { + info.variant = typeVariantBigInt + if t.IsBigIntLiteral() { + literal := t.AsLiteralType() + if literal != nil && literal.String() != "0" { + info.isTruthy = true + } + } + return info + } + + if flags&(checker.TypeFlagsESSymbol|checker.TypeFlagsUniqueESSymbol) != 0 { + info.variant = typeVariantObject + return info + } + + if flags&(checker.TypeFlagsObject|checker.TypeFlagsNonPrimitive) != 0 { + info.variant = typeVariantObject + return info + } + + info.variant = typeVariantMixed + return info +} + +func checkCondition(ctx rule.RuleContext, node *ast.Node, types []*checker.Type, opts StrictBooleanExpressionsOptions) { + info := analyzeTypeParts(ctx.TypeChecker, types) + + switch info.variant { + case typeVariantAny, typeVariantUnknown, typeVariantGeneric: + if !*opts.AllowAny { + ctx.ReportNode(node, buildUnexpectedAny()) + } + return + case typeVariantNever: + return + case typeVariantNullish: + ctx.ReportNode(node, buildUnexpectedNullish()) + case typeVariantString: + // Known edge case: truthy primitives and nullish values are always valid boolean expressions + if *opts.AllowString && info.isTruthy { + return + } + + if info.isNullable { + if info.isEnum { + if !*opts.AllowNullableEnum { + ctx.ReportNode(node, buildUnexpectedNullableEnum()) + } + } else { + if !*opts.AllowNullableString { + ctx.ReportNode(node, buildUnexpectedNullableString()) + } + } + } else if !*opts.AllowString { + ctx.ReportNode(node, buildUnexpectedString()) + } + case typeVariantNumber: + // Known edge case: truthy primitives and nullish values are always valid boolean expressions + if *opts.AllowNumber && info.isTruthy { + return + } + + if info.isNullable { + if info.isEnum { + if !*opts.AllowNullableEnum { + ctx.ReportNode(node, buildUnexpectedNullableEnum()) + } + } else { + if !*opts.AllowNullableNumber { + ctx.ReportNode(node, buildUnexpectedNullableNumber()) + } + } + } else if !*opts.AllowNumber { + ctx.ReportNode(node, buildUnexpectedNumber()) + } + case typeVariantBoolean: + // Known edge case: truthy primitives and nullish values are always valid boolean expressions + if info.isTruthy { + return + } + + if info.isNullable && !*opts.AllowNullableBoolean { + ctx.ReportNode(node, buildUnexpectedNullableBoolean()) + } + case typeVariantObject: + if info.isNullable && !*opts.AllowNullableObject { + ctx.ReportNode(node, buildUnexpectedNullableObject()) + } else if !info.isNullable { + ctx.ReportNode(node, buildUnexpectedObject()) + } + case typeVariantMixed: + if info.isEnum { + if info.isNullable && !*opts.AllowNullableEnum { + ctx.ReportNode(node, buildUnexpectedNullableEnum()) + } + return + } + ctx.ReportNode(node, buildUnexpectedMixedCondition()) + case typeVariantBigInt: + if info.isNullable && !*opts.AllowNullableNumber { + ctx.ReportNode(node, buildUnexpectedNullableNumber()) + } else if !info.isNullable && !*opts.AllowNumber { + ctx.ReportNode(node, buildUnexpectedNumber()) + } + } +} diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go new file mode 100644 index 00000000..a86d173f --- /dev/null +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -0,0 +1,2049 @@ +package strict_boolean_expressions + +import ( + "testing" + + "github.com/typescript-eslint/tsgolint/internal/rule_tester" + "github.com/typescript-eslint/tsgolint/internal/rules/fixtures" + "github.com/typescript-eslint/tsgolint/internal/utils" +) + +func TestStrictBooleanExpressionsRule(t *testing.T) { + rule_tester.RunRuleTester( + fixtures.GetRootDir(), + "tsconfig.json", + t, + &StrictBooleanExpressionsRule, + []rule_tester.ValidTestCase{ + { + Code: "true ? 'a' : 'b';", + }, + { + Code: ` + if (false) { + } + `, + }, + { + Code: "while (true) {}", + }, + { + Code: "for (; false; ) {}", + }, + { + Code: "!true;", + }, + { + Code: "false || 123;", + }, + { + Code: "true && 'foo';", + }, + { + Code: "!(false || true);", + }, + { + Code: "true && false ? true : false;", + }, + { + Code: "(false && true) || false;", + }, + { + Code: "(false && true) || [];", + }, + { + Code: "(false && 1) || (true && 2);", + }, + { + Code: ` + declare const x: boolean; + if (x) { + } + `, + }, + { + Code: "(x: boolean) => !x;", + }, + { + Code: "(x: T) => (x ? 1 : 0);", + }, + { + Code: ` + declare const x: never; + if (x) { + } + `, + }, + { + Code: ` + if ('') { + } + `, + }, + { + Code: "while ('x') {}"}, + { + Code: "for (; ''; ) {}"}, + { + Code: "('' && '1') || x;"}, + { + Code: ` + declare const x: string; + if (x) { + } + `, + }, + { + Code: "(x: string) => !x;"}, + { + Code: "(x: T) => (x ? 1 : 0);"}, + { + Code: ` + if (0) { + } + `, + }, + { + Code: "while (1n) {}"}, + { + Code: "for (; Infinity; ) {}"}, + { + Code: "(0 / 0 && 1 + 2) || x;"}, + { + Code: ` + declare const x: number; + if (x) { + } + `, + }, + { + Code: "(x: bigint) => !x;"}, + { + Code: "(x: T) => (x ? 1 : 0);"}, + { + Code: ` + declare const x: null | object; + if (x) { + } + `, + }, + { + Code: "(x?: { a: any }) => !x;"}, + { + Code: "(x: T) => (x ? 1 : 0);"}, + { + Code: ` + declare const x: boolean | null; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + + AllowNullableBoolean: utils.Ref(true), + }, + }, + { + Code: ` + (x?: boolean) => !x; + `, + Options: StrictBooleanExpressionsOptions{ + + AllowNullableBoolean: utils.Ref(true), + }, + }, + { + Code: ` + (x: T) => (x ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, + }, + { + Code: ` + const a: (undefined | boolean | null)[] = [true, undefined, null]; + a.some(x => x); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, + }, + { + Code: ` + declare const x: string | null; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, + }, + { + Code: ` + (x?: string) => !x; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, + }, + { + Code: ` + (x: T) => (x ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, + }, + { + Code: ` + declare const x: number | null; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: ` + (x?: number) => !x; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: ` + (x: T) => (x ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: ` + declare const arrayOfArrays: (null | unknown[])[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array?.length); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: ` + declare const x: any; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: ` + x => !x; + `, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: ` + (x: T) => (x ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: ` + declare const arrayOfArrays: any[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: ` + 1 && true && 'x' && {}; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + }, + { + Code: ` + let x = 0 || false || '' || null; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + }, + { + Code: ` + if (1 && true && 'x') void 0; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + }, + { + Code: ` + if (0 || false || '') void 0; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + }, + { + Code: ` + 1 && true && 'x' ? {} : null; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + }, + { + Code: ` + 0 || false || '' ? null : {}; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + }, + { + Code: ` + declare const arrayOfArrays: string[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(true), + }, + }, + { + Code: ` + declare const arrayOfArrays: number[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), + }, + }, + { + Code: ` + declare const arrayOfArrays: (null | object)[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const rand = Math.random(); + let theEnum: ExampleEnum | null = null; + if (rand < 0.3) { + theEnum = ExampleEnum.This; + } + if (theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const rand = Math.random(); + let theEnum: ExampleEnum | null = null; + if (rand < 0.3) { + theEnum = ExampleEnum.This; + } + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = 1, + That = 2, + } + const rand = Math.random(); + let theEnum: ExampleEnum | null = null; + if (rand < 0.3) { + theEnum = ExampleEnum.This; + } + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = 'one', + That = 'two', + } + const rand = Math.random(); + let theEnum: ExampleEnum | null = null; + if (rand < 0.3) { + theEnum = ExampleEnum.This; + } + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = 0, + That = 'one', + } + (value?: ExampleEnum) => (value ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = '', + That = 1, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = 'this', + That = 1, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = '', + That = 0, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + { + Code: ` + enum ExampleEnum { + This = '', + That = 0, + } + declare const arrayOfArrays: (ExampleEnum | null)[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, + }, + //TODO: oxlint-disable-next-line should not check + { + Skip: true, + Code: ` + declare const x: string[] | null; + // oxlint-disable-next-line + if (x) { + } + `, + TSConfig: "tsconfig.unstrict.json", + Options: StrictBooleanExpressionsOptions{ + AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), + }, + }, + { + Code: ` + function f(arg: 'a' | null) { + if (arg) console.log(arg); + } + `, + }, + { + Code: ` + function f(arg: 'a' | 'b' | null) { + if (arg) console.log(arg); + } + `, + }, + { + Code: ` + declare const x: 1 | null; + declare const y: 1; + if (x) { + } + if (y) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), + }, + }, + { + Code: ` + function f(arg: 1 | null) { + if (arg) console.log(arg); + } + `, + }, + { + Code: ` + function f(arg: 1 | 2 | null) { + if (arg) console.log(arg); + } + `, + }, + { + Code: ` + interface Options { + readonly enableSomething?: true; + } + + function f(opts: Options): void { + if (opts.enableSomething) console.log('Do something'); + } + `, + }, + { + Code: ` + declare const x: true | null; + if (x) { + } + `, + }, + { + Code: ` + declare const x: 'a' | null; + declare const y: 'a'; + if (x) { + } + if (y) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(true), + }, + }, + { + Code: ` + declare const foo: boolean & { __BRAND: 'Foo' }; + if (foo) { + } + `, + }, + { + Code: ` + declare const foo: true & { __BRAND: 'Foo' }; + if (foo) { + } + `, + }, + { + Code: ` + declare const foo: false & { __BRAND: 'Foo' }; + if (foo) { + } + `, + }, + { + Code: ` + declare function assert(a: number, b: unknown): asserts a; + declare const nullableString: string | null; + declare const boo: boolean; + assert(boo, nullableString); + `, + }, + { + Code: ` + declare function assert(a: boolean, b: unknown): asserts b is string; + declare const nullableString: string | null; + declare const boo: boolean; + assert(boo, nullableString); + `, + }, + { + Code: ` + declare function assert(a: number, b: unknown): asserts b; + declare const nullableString: string | null; + declare const boo: boolean; + assert(nullableString, boo); + `, + }, + { + Code: ` + declare function assert(a: number, b: unknown): asserts b; + declare const nullableString: string | null; + declare const boo: boolean; + assert(...nullableString, nullableString); + `, + }, + { + Code: ` + declare function assert( + this: object, + a: number, + b?: unknown, + c?: unknown, + ): asserts c; + declare const nullableString: string | null; + declare const foo: number; + const o: { assert: typeof assert } = { + assert, + }; + o.assert(foo, nullableString); + `, + }, + { + Code: ` + declare function assert(x: unknown): x is string; + declare const nullableString: string | null; + assert(nullableString); + `, + }, + { + Code: ` + class ThisAsserter { + assertThis(this: unknown, arg2: unknown): asserts this {} + } + + declare const lol: string | number | unknown | null; + + const thisAsserter: ThisAsserter = new ThisAsserter(); + thisAsserter.assertThis(lol); + `, + }, + { + Code: ` + function assert(this: object, a: number, b: unknown): asserts b; + function assert(a: bigint, b: unknown): asserts b; + function assert(this: object, a: string, two: string): asserts two; + function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, + ): asserts assertee; + function assert(...args: any[]): void; + + function assert(...args: any[]) { + throw new Error('lol'); + } + + declare const nullableString: string | null; + assert(3 as any, nullableString); + `, + }, + { + Code: ` + declare const assert: any; + declare const nullableString: string | null; + assert(nullableString); + `, + }, + { + Code: ` + for (let x = 0; ; x++) { + break; + } + `, + }, + { + Code: ` + [true, false].some(function (x) { + return x; + }); + `, + }, + { + Code: ` + [true, false].some(function check(x) { + return x; + }); + `, + }, + { + Code: ` + [true, false].some(x => { + return x; + }); + `, + }, + { + Code: ` + [1, null].filter(function (x) { + return x != null; + }); + `, + }, + { + Code: ` + ['one', 'two', ''].filter(function (x) { + return !!x; + }); + `, + }, + { + Code: ` + ['one', 'two', ''].filter(function (x): boolean { + return !!x; + }); + `, + }, + { + Code: ` + ['one', 'two', ''].filter(function (x): boolean { + if (x) { + return true; + } + }); + `, + }, + { + Code: ` + ['one', 'two', ''].filter(function (x): boolean { + if (x) { + return true; + } + + throw new Error('oops'); + }); + `, + }, + { + Code: ` + declare const predicate: (string) => boolean; + ['one', 'two', ''].filter(predicate); + `, + }, + { + Code: ` + declare function notNullish(x: T): x is NonNullable; + ['one', null].filter(notNullish); + `, + }, + { + Code: ` + declare function predicate(x: string | null): x is string; + ['one', null].filter(predicate); + `, + }, + { + Code: ` + declare function predicate(x: string | null): T; + ['one', null].filter(predicate); + `, + }, + { + Code: ` + declare function f(x: number): boolean; + declare function f(x: string | null): boolean; + + [35].filter(f); + `, + }, + }, []rule_tester.InvalidTestCase{ + { + Code: ` + if (true && 1 + 1) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 2}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: "while (false || 'a' + 'b') {}", + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "(x: object) => (true || false || x ? true : false);", + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 1}, + }, + }, + { + Code: "if (('' && {}) || (0 && void 0)) { }", + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: ` + declare const array: string[]; + array.some(x => x); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString"}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */ + }, + { + Code: ` + declare const foo: true & { __BRAND: 'Foo' }; + if (('' && foo) || (0 && void 0)) { } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 3}, + {MessageId: "unexpectedNumber", Line: 3}, + {MessageId: "unexpectedNullish", Line: 3}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: ` + declare const foo: false & { __BRAND: 'Foo' }; + if (('' && {}) || (foo && void 0)) { } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 3}, + {MessageId: "unexpectedObject", Line: 3}, + {MessageId: "unexpectedNullish", Line: 3}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "'asd' && 123 && [] && null;", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: "'asd' || 123 || [] || null;", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: "let x = (1 && 'a' && null) || 0 || '' || {};", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "return (1 || 'a' || null) && 0 && '' && {};", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "console.log((1 && []) || ('a' && {}));", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "if ((1 && []) || ('a' && {})) void 0;", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "let x = null || 0 || 'a' || [] ? {} : undefined;", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "return !(null || 0 || 'a' || []);", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "null || {};", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: "undefined && [];", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: ` + declare const x: null; + if (x) { + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 3}, + }, + }, + { + Code: "(x: undefined) => !x;", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: "[] || 1;", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 1}, + }, + }, + { + Code: "({}) && 'a';", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 1}, + }, + }, + { + Code: ` + declare const x: symbol; + if (x) { + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 3}, + }, + }, + { + Code: "(x: () => void) => !x;", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 1}, + }, + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 1}, + }, + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 1}, + }, + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 1}, + }, + }, + { + Code: " void>(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 1}, + }, + }, + { + Code: "while ('') {}", + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "for (; 'foo'; ) {}", + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + declare const x: string; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 3}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "(x: string) => !x;", + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, + { + Code: "while (0n) {}", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: "for (; 123; ) {}", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: ` + declare const x: number; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: "(x: bigint) => !x;", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: "![]['length']; // doesn't count as array.length when computed", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: ` + declare const a: any[] & { notLength: number }; + if (a.notLength) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, + { + Code: ` + if (![].length) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 2}, + }, /* Suggestions: conditionFixCompareArrayLengthZero */ + }, + { + Code: ` + (a: number[]) => a.length && '...'; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 2}, + }, /* Suggestions: conditionFixCompareArrayLengthNonzero */ + }, + { + Code: ` + (...a: T) => a.length || 'empty'; + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 2}, + }, /* Suggestions: conditionFixCompareArrayLengthNonzero */ + }, + { + Code: ` + declare const x: string | number; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 3}, + }, + }, + { + Code: "(x: bigint | string) => !x;", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 1}, + }, + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 1}, + }, + }, + { + Code: ` + declare const x: boolean | null; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 3}, + }, /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue */ + }, + { + Code: "(x?: boolean) => !x;", + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, /* Suggestions: conditionFixDefaultFalse, conditionFixCompareFalse */ + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue */ + }, + { + Code: ` + declare const x: object | null; + if (x) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: "(x?: { a: number }) => !x;", + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + declare const x: string | null; + if (x) { + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: "(x?: string) => !x;", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + function foo(x: '' | 'bar' | null) { + if (!x) { + } + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + declare const x: number | null; + if (x) { + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ + }, + { + Code: "(x?: number) => !x;", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ + }, + { + Code: ` + function foo(x: 0 | 1 | null) { + if (!x) { + } + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ + }, + { + Code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This, + That, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = '', + That = 'a', + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = '', + That = 0, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = 'one', + That = 'two', + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = 1, + That = 2, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (!theEnum) { + } + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = 0, + That = 'one', + } + (value?: ExampleEnum) => (value ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 6}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = '', + That = 1, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 6}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = 'this', + That = 1, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 6}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + enum ExampleEnum { + This = '', + That = 0, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 6}, + }, /* Suggestions: conditionFixCompareNullish */ + }, + { + Code: ` + if (x) { + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 2}, + }, /* Suggestions: conditionFixCastBoolean */ + }, + { + Code: "x => !x;", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, /* Suggestions: conditionFixCastBoolean */ + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, /* Suggestions: conditionFixCastBoolean */ + }, + { + Code: "(x: T) => (x ? 1 : 0);", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, /* Suggestions: conditionFixCastBoolean */ + }, + { + Code: ` + declare const x: string[] | null; + if (x) { + } + `, + TSConfig: "tsconfig.unstrict.json", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "noStrictNullCheck", Line: 0}, + {MessageId: "unexpectedObject", Line: 3}, + }, + }, + { + Code: ` + declare const obj: { x: number } | null; + !obj ? 1 : 0 + !obj + obj || 0 + obj && 1 || 0 + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 3}, + {MessageId: "unexpectedNullableObject", Line: 4}, + {MessageId: "unexpectedNullableObject", Line: 5}, + {MessageId: "unexpectedNullableObject", Line: 6}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixCompareNullish, conditionFixCompareNullish, conditionFixCompareNullish */ + }, + { + Code: ` + declare function assert(x: unknown): asserts x; + declare const nullableString: string | null; + assert(nullableString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 4}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + declare function assert(a: number, b: unknown): asserts b; + declare const nullableString: string | null; + assert(foo, nullableString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 4}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + declare function assert(a: number, b: unknown): asserts b; + declare function assert(one: number, two: unknown): asserts two; + declare const nullableString: string | null; + assert(foo, nullableString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 5}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + declare function assert(this: object, a: number, b: unknown): asserts b; + declare const nullableString: string | null; + assert(foo, nullableString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 4}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + // This should be checkable, but the TS API doesn't currently report + // `someAssert(maybeString)` as a type predicate call, which appears to be + // a bug. + // + // See https://github.com/microsoft/TypeScript/issues/59707 + Skip: true, + Code: ` + function asserts1(x: string | number | undefined): asserts x {} + function asserts2(x: string | number | undefined): asserts x {} + + const maybeString = Math.random() ? 'string'.slice() : undefined; + + const someAssert: typeof asserts1 | typeof asserts2 = + Math.random() > 0.5 ? asserts1 : asserts2; + + someAssert(maybeString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString"}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + function assert(this: object, a: number, b: unknown): asserts b; + function assert(a: bigint, b: unknown): asserts b; + function assert(this: object, a: string, two: string): asserts two; + function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, + ): asserts assertee; + + function assert(...args: any[]) { + throw new Error('lol'); + } + + declare const nullableString: string | null; + assert(3 as any, nullableString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 18}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + function assert(this: object, a: number, b: unknown): asserts b; + function assert(a: bigint, b: unknown): asserts b; + function assert(this: object, a: string, two: string): asserts two; + function assert( + this: object, + a: string, + assertee: string, + c: bigint, + d: object, + ): asserts assertee; + function assert(a: any, two: unknown, ...rest: any[]): asserts two; + + function assert(...args: any[]) { + throw new Error('lol'); + } + + declare const nullableString: string | null; + assert(3 as any, nullableString, 'more', 'args', 'afterwards'); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 19}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + declare function assert(a: boolean, b: unknown): asserts b; + declare function assert({ a }: { a: boolean }, b: unknown): asserts b; + declare const nullableString: string | null; + declare const boo: boolean; + assert(boo, nullableString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 6}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + function assert(one: unknown): asserts one; + function assert(one: unknown, two: unknown): asserts two; + function assert(...args: unknown[]) { + throw new Error('not implemented'); + } + declare const nullableString: string | null; + assert(nullableString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 8}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + ['one', 'two', ''].find(x => { + return x; + }); + `, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 2}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + ['one', 'two', ''].find(x => { + return; + }); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 2}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + ['one', 'two', ''].findLast(x => { + return undefined; + }); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 2}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + ['one', 'two', ''].find(x => { + if (x) { + return Math.random() > 0.5; + } + }); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 2}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + const predicate = (x: string) => { + if (x) { + return Math.random() > 0.5; + } + }; + + ['one', 'two', ''].find(predicate); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 8}, + }, + }, + { + Code: ` + [1, null].every(async x => { + return x != null; + }); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "predicateCannotBeAsync", Line: 2}, + }, + }, + { + Code: ` + const predicate = async x => { + return x != null; + }; + + [1, null].every(predicate); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 6}, + }, + }, + { + Code: ` + [1, null].every((x): boolean | number => { + return x != null; + }); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 2}, + }, + }, + { + Code: ` + [1, null].every((x): boolean | undefined => { + return x != null; + }); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 2}, + }, + }, + { + Code: ` + [1, null].every((x, i) => {}); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 2}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + [() => {}, null].every((x: () => void) => {}); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 2}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + [() => {}, null].every(function (x: () => void) {}); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 2}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + [() => {}, null].every(() => {}); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 2}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + declare function f(x: number): string; + declare function f(x: string | null): boolean; + + [35].filter(f); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 5}, + }, + }, + { + Code: ` + declare function f(x: number): string; + declare function f(x: number | boolean): boolean; + declare function f(x: string | null): boolean; + + [35].filter(f); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 6}, + }, + }, + { + Code: ` + declare function foo(x: number): T; + [1, null].every(foo); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 3}, + }, + }, + { + Code: ` + function foo(x: number): T {} + [1, null].every(foo); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 3}, + }, + }, + { + Code: ` + declare const nullOrString: string | null; + ['one', null].filter(x => nullOrString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */ + }, + { + Code: ` + declare const nullOrString: string | null; + ['one', null].filter(x => !nullOrString); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, + { + Code: ` + declare const anyValue: any; + ['one', null].filter(x => anyValue); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 3}, + }, /* Suggestions: conditionFixCastBoolean, explicitBooleanReturnType */ + }, + { + Code: ` + declare const nullOrBoolean: boolean | null; + [true, null].filter(x => nullOrBoolean); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 3}, + }, /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue, explicitBooleanReturnType */ + }, + { + Code: ` + enum ExampleEnum { + This = 0, + That = 1, + } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + [0, 1].filter(x => theEnum); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish, explicitBooleanReturnType */ + }, + { + Code: ` + declare const nullOrNumber: number | null; + [0, null].filter(x => nullOrNumber); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean, explicitBooleanReturnType */ + }, + { + Code: ` + const objectValue: object = {}; + [{ a: 0 }, {}].filter(x => objectValue); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 3}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + const objectValue: object = {}; + [{ a: 0 }, {}].filter(x => { + return objectValue; + }); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObject", Line: 3}, + }, /* Suggestions: explicitBooleanReturnType */ + }, + { + Code: ` + declare const nullOrObject: object | null; + [{ a: 0 }, null].filter(x => nullOrObject); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, explicitBooleanReturnType */ + }, + { + Code: ` + const numbers: number[] = [1]; + [1, 2].filter(x => numbers.length); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareArrayLengthNonzero, explicitBooleanReturnType */ + }, + { + Code: ` + const numberValue: number = 1; + [1, 2].filter(x => numberValue); + `, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, explicitBooleanReturnType */ + }, + { + Code: ` + const stringValue: string = 'hoge'; + ['hoge', 'foo'].filter(x => stringValue); + `, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 3}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */ + }, + }) +} diff --git a/internal/rules/unbound_method/unbound_method.go b/internal/rules/unbound_method/unbound_method.go index bbd82e5e..e181c43f 100644 --- a/internal/rules/unbound_method/unbound_method.go +++ b/internal/rules/unbound_method/unbound_method.go @@ -241,7 +241,7 @@ var UnboundMethodRule = rule.Rule{ if ast.IsComputedPropertyName(propertyName) { return } - + if initNode != nil { if !isNativelyBound(initNode, propertyName) { reported := checkIfMethodAndReport(propertyName, checker.Checker_getPropertyOfType(ctx.TypeChecker, ctx.TypeChecker.GetTypeAtLocation(initNode), propertyName.Text())) diff --git a/shim/checker/extra-shim.json b/shim/checker/extra-shim.json index a8d46d79..2be1c93d 100644 --- a/shim/checker/extra-shim.json +++ b/shim/checker/extra-shim.json @@ -53,6 +53,11 @@ ], "InterfaceType": [ "thisType" + ], + "TypePredicate": [ + "kind", + "t", + "parameterIndex" ] } } diff --git a/shim/checker/shim.go b/shim/checker/shim.go index 252e6a56..248f73cc 100644 --- a/shim/checker/shim.go +++ b/shim/checker/shim.go @@ -1148,6 +1148,21 @@ const TypeMapperKindUnknown = checker.TypeMapperKindUnknown type TypeNodeLinks = checker.TypeNodeLinks type TypeParameter = checker.TypeParameter type TypePredicate = checker.TypePredicate +type extra_TypePredicate struct { + kind checker.TypePredicateKind + parameterIndex int32 + parameterName string + t *checker.Type +} +func TypePredicate_kind(v *checker.TypePredicate) checker.TypePredicateKind { + return ((*extra_TypePredicate)(unsafe.Pointer(v))).kind +} +func TypePredicate_t(v *checker.TypePredicate) *checker.Type { + return ((*extra_TypePredicate)(unsafe.Pointer(v))).t +} +func TypePredicate_parameterIndex(v *checker.TypePredicate) int32 { + return ((*extra_TypePredicate)(unsafe.Pointer(v))).parameterIndex +} type TypePredicateKind = checker.TypePredicateKind const TypePredicateKindAssertsIdentifier = checker.TypePredicateKindAssertsIdentifier const TypePredicateKindAssertsThis = checker.TypePredicateKindAssertsThis