From 42c91858fbfb821243e5e10f3c6310c11b9d4635 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 9 Sep 2025 12:14:46 +0900 Subject: [PATCH 01/23] feat(rules): Implement `strict_boolean_expressions` --- .gitignore | 4 +- cmd/tsgolint/main.go | 2 + .../strict_boolean_expressions.go | 516 +++++++ .../strict_boolean_expressions_test.go | 1342 +++++++++++++++++ 4 files changed, 1863 insertions(+), 1 deletion(-) create mode 100644 internal/rules/strict_boolean_expressions/strict_boolean_expressions.go create mode 100644 internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go 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..500f2927 --- /dev/null +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -0,0 +1,516 @@ +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 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 buildUnexpectedObjectContext() rule.RuleMessage { + return rule.RuleMessage{ + Id: "unexpectedObjectContext", + 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: "msgNoStrictNullCheck", + Description: "This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.", + } +} + +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(false) + } + 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{} + } + + checkNode := func(node *ast.Node) { + nodeType := ctx.TypeChecker.GetTypeAtLocation(node) + checkCondition(ctx, node, nodeType, opts) + } + + return rule.RuleListeners{ + ast.KindIfStatement: func(node *ast.Node) { + ifStmt := node.AsIfStatement() + checkNode(ifStmt.Expression) + }, + ast.KindWhileStatement: func(node *ast.Node) { + whileStmt := node.AsWhileStatement() + checkNode(whileStmt.Expression) + }, + ast.KindDoStatement: func(node *ast.Node) { + doStmt := node.AsDoStatement() + checkNode(doStmt.Expression) + }, + ast.KindForStatement: func(node *ast.Node) { + forStmt := node.AsForStatement() + if forStmt.Condition != nil { + checkNode(forStmt.Condition) + } + }, + ast.KindConditionalExpression: func(node *ast.Node) { + condExpr := node.AsConditionalExpression() + checkNode(condExpr.Condition) + }, + ast.KindBinaryExpression: func(node *ast.Node) { + binExpr := node.AsBinaryExpression() + switch binExpr.OperatorToken.Kind { + case ast.KindAmpersandAmpersandToken, ast.KindBarBarToken: + checkNode(binExpr.Left) + } + }, + ast.KindCallExpression: func(node *ast.Node) { + callExpr := node.AsCallExpression() + + //assertedArgument := findTruthinessAssertedArgument(ctx.TypeChecker, callExpr) + //if assertedArgument != nil { + // checkNode(assertedArgument) + //} + + if callExpr.Expression != nil && callExpr.Expression.Kind == ast.KindPropertyAccessExpression { + propAccess := callExpr.Expression.AsPropertyAccessExpression() + if propAccess == nil { + return + } + methodName := propAccess.Name().Text() + + switch methodName { + case "filter", "find", "some", "every", "findIndex", "findLast", "findLastIndex": + if callExpr.Arguments != nil && len(callExpr.Arguments.Nodes) > 0 { + arg := callExpr.Arguments.Nodes[0] + if arg != nil && (arg.Kind == ast.KindArrowFunction || arg.Kind == ast.KindFunctionExpression) { + funcType := ctx.TypeChecker.GetTypeAtLocation(arg) + signatures := ctx.TypeChecker.GetCallSignatures(funcType) + 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 + } + } + + if returnType != nil && !isBooleanType(returnType) { + checkCondition(ctx, node, returnType, opts) + } + } + } + } + } + } + }, + ast.KindPrefixUnaryExpression: func(node *ast.Node) { + unaryExpr := node.AsPrefixUnaryExpression() + if unaryExpr.Operator == ast.KindExclamationToken { + checkNode(unaryExpr.Operand) + } + }, + } + }, +} + +// TODO: Not exported TypePredicate struct variables +//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 { +// continue +// } +// 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 firstTypePredicateResult.kind != checker.TypePredicateKindAssertsIdentifier || +// firstTypePredicateResult.type == nil { +// return nil +// } +// return checkableArguments[firstTypePredicateResult.parameterIndex] +//} + +// 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 analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { + info := typeInfo{ + types: []*checker.Type{t}, + } + + if utils.IsUnionType(t) { + info.isUnion = true + parts := utils.UnionTypeParts(t) + variants := make(map[typeVariant]bool) + hasNullish := false + hasEnum := false + + for _, part := range parts { + partInfo := analyzeTypePart(typeChecker, part) + variants[partInfo.variant] = true + if partInfo.variant == typeVariantNullish { + hasNullish = true + } + if partInfo.isEnum { + hasEnum = true + } + } + + info.isNullable = hasNullish + info.isEnum = hasEnum + + if len(variants) == 1 { + for v := range variants { + info.variant = v + } + } else if len(variants) == 2 && hasNullish { + for v := range variants { + if v != typeVariantNullish { + info.variant = v + break + } + } + } else { + info.variant = typeVariantMixed + } + + return info + } + + if utils.IsIntersectionType(t) { + info.isIntersection = true + info.variant = typeVariantObject + return info + } + + return analyzeTypePart(typeChecker, t) +} + +func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { + info := typeInfo{} + flags := checker.Type_flags(t) + + 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) != 0 { + info.variant = typeVariantBoolean + return info + } + + if flags&(checker.TypeFlagsString|checker.TypeFlagsStringLiteral) != 0 { + info.variant = typeVariantString + if flags&checker.TypeFlagsStringLiteral != 0 && t.AsLiteralType().Value() != "" { + info.isTruthy = true + } + return info + } + + if flags&(checker.TypeFlagsNumber|checker.TypeFlagsNumberLiteral) != 0 { + info.variant = typeVariantNumber + if flags&checker.TypeFlagsNumberLiteral != 0 && t.AsLiteralType().Value() != 0 { + info.isTruthy = true + } + return info + } + + if flags&(checker.TypeFlagsEnum|checker.TypeFlagsEnumLiteral) != 0 { + if flags&checker.TypeFlagsStringLiteral != 0 { + info.variant = typeVariantString + } else { + info.variant = typeVariantNumber + } + info.isEnum = true + return info + } + + if flags&(checker.TypeFlagsBigInt|checker.TypeFlagsBigIntLiteral) != 0 { + info.variant = typeVariantBigInt + if flags&checker.TypeFlagsBigIntLiteral != 0 && t.AsLiteralType().Value() != 0 { + info.isTruthy = true + } + return info + } + + if flags&(checker.TypeFlagsESSymbol|checker.TypeFlagsUniqueESSymbol) != 0 { + info.variant = typeVariantObject + return info + } + + if flags&checker.TypeFlagsObject != 0 || + flags&checker.TypeFlagsNonPrimitive != 0 || + utils.IsObjectType(t) { + info.variant = typeVariantObject + return info + } + + info.variant = typeVariantMixed + return info +} + +func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts StrictBooleanExpressionsOptions) { + if isBooleanType(t) { + return + } + + info := analyzeType(ctx.TypeChecker, t) + + 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.isNullable && info.isTruthy { + return + } + + if info.isNullable { + if info.isEnum { + if !*opts.AllowNullableEnum { + ctx.ReportNode(node, buildUnexpectedNullableString()) + } + } 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.isNullable && info.isTruthy { + return + } + + if info.isNullable { + if info.isEnum { + if !*opts.AllowNullableEnum { + ctx.ReportNode(node, buildUnexpectedNullableNumber()) + } + } else { + if !*opts.AllowNullableNumber { + ctx.ReportNode(node, buildUnexpectedNullableNumber()) + } + } + } else if !*opts.AllowNumber { + ctx.ReportNode(node, buildUnexpectedNumber()) + } + case typeVariantBoolean: + 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, buildUnexpectedObjectContext()) + } + case typeVariantMixed: + 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()) + } + } +} + +func isBooleanType(t *checker.Type) bool { + flags := checker.Type_flags(t) + + if flags&(checker.TypeFlagsBoolean|checker.TypeFlagsBooleanLiteral) != 0 { + if utils.IsUnionType(t) { + for _, part := range utils.UnionTypeParts(t) { + partFlags := checker.Type_flags(part) + if partFlags&(checker.TypeFlagsNull|checker.TypeFlagsUndefined|checker.TypeFlagsVoid) != 0 { + return false + } + } + } + return true + } + + return false +} 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..df301a28 --- /dev/null +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -0,0 +1,1342 @@ +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) { + // Test with strictNullChecks enabled (in fixtures/tsconfig.json) + rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ + // ============================================ + // BOOLEAN TYPES - Always Valid + // ============================================ + + // Boolean literals + {Code: `true ? 'a' : 'b';`}, + {Code: `false ? 'a' : 'b';`}, + {Code: `if (true) {}`}, + {Code: `if (false) {}`}, + {Code: `while (true) {}`}, + {Code: `do {} while (true);`}, + {Code: `for (; true; ) {}`}, + {Code: `!true;`}, + + // Boolean variables + {Code: `const b = true; if (b) {}`}, + {Code: `const b = false; if (!b) {}`}, + {Code: `declare const b: boolean; if (b) {}`}, + + // Boolean expressions + {Code: `if (true && false) {}`}, + {Code: `if (true || false) {}`}, + {Code: `true && 'a';`}, + {Code: `false || 'a';`}, + + // Comparison operators return boolean + {Code: `1 > 2 ? 'a' : 'b';`}, + {Code: `1 < 2 ? 'a' : 'b';`}, + {Code: `1 >= 2 ? 'a' : 'b';`}, + {Code: `1 <= 2 ? 'a' : 'b';`}, + {Code: `1 == 2 ? 'a' : 'b';`}, + {Code: `1 != 2 ? 'a' : 'b';`}, + {Code: `1 === 2 ? 'a' : 'b';`}, + {Code: `1 !== 2 ? 'a' : 'b';`}, + + // Type guards + {Code: `declare const x: string | number; if (typeof x === 'string') {}`}, + {Code: `declare const x: any; if (x instanceof Error) {}`}, + {Code: `declare const x: any; if ('prop' in x) {}`}, + + // Function returning boolean + {Code: `function test(): boolean { return true; } if (test()) {}`}, + {Code: `declare function test(): boolean; if (test()) {}`}, + + // ============================================ + // STRING TYPES - Valid with Default Options + // ============================================ + + {Code: `'' ? 'a' : 'b';`}, + {Code: `'foo' ? 'a' : 'b';`}, + {Code: "`` ? 'a' : 'b';"}, + {Code: "`foo` ? 'a' : 'b';"}, + {Code: "`foo${bar}` ? 'a' : 'b';"}, + {Code: `if ('') {}`}, + {Code: `if ('foo') {}`}, + {Code: `while ('') {}`}, + {Code: `do {} while ('foo');`}, + {Code: `for (; 'foo'; ) {}`}, + {Code: `!!'foo';`}, + {Code: `declare const s: string; if (s) {}`}, + + // String with logical operators + {Code: `'' || 'foo';`}, + {Code: `'foo' && 'bar';`}, + {Code: `declare const s: string; s || 'default';`}, + + // ============================================ + // NUMBER TYPES - Valid with Default Options + // ============================================ + + {Code: `0 ? 'a' : 'b';`}, + {Code: `1 ? 'a' : 'b';`}, + {Code: `-1 ? 'a' : 'b';`}, + {Code: `0.5 ? 'a' : 'b';`}, + {Code: `NaN ? 'a' : 'b';`}, + {Code: `if (0) {}`}, + {Code: `if (1) {}`}, + {Code: `while (0) {}`}, + {Code: `do {} while (1);`}, + {Code: `for (; 1; ) {}`}, + {Code: `declare const n: number; if (n) {}`}, + + // Number with logical operators + {Code: `0 || 1;`}, + {Code: `1 && 2;`}, + {Code: `declare const n: number; n || 0;`}, + + // BigInt + {Code: `0n ? 'a' : 'b';`}, + {Code: `1n ? 'a' : 'b';`}, + {Code: `if (0n) {}`}, + {Code: `if (1n) {}`}, + {Code: `declare const b: bigint; if (b) {}`}, + + // ============================================ + // OBJECT TYPES in logical operators + // ============================================ + + // Note: Objects in logical operators are treated as boolean conditions + // and will be flagged as errors (always truthy) unless in the right side + // Right side of logical operators is not checked as a boolean condition + {Code: `'foo' || ({});`}, // Object on right side is OK + {Code: `false || [];`}, // Array on right side is OK + {Code: `(false && true) || [];`}, // Array on right side is OK + + // ============================================ + // ANY TYPE - Valid with Option + // ============================================ + + { + Code: `declare const x: any; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: `declare const x: any; x ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: `declare const x: any; x && 'a';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: `declare const x: any; x || 'a';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: `declare const x: any; !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + + // ============================================ + // UNKNOWN TYPE - Valid with Option + // ============================================ + + { + Code: `declare const x: unknown; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: `declare const x: unknown; x ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: `declare const x: unknown; x && 'a';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: `declare const x: unknown; x || 'a';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + { + Code: `declare const x: unknown; !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, + }, + + // ============================================ + // NULLABLE BOOLEAN - Valid with Option + // ============================================ + + { + Code: `declare const x: boolean | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, + }, + { + Code: `declare const x: boolean | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, + }, + { + Code: `declare const x: boolean | null | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, + }, + { + Code: `declare const x: true | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, + }, + { + Code: `declare const x: false | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, + }, + + // ============================================ + // NULLABLE STRING - Valid with Option + // ============================================ + + { + Code: `declare const x: string | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, + }, + { + Code: `declare const x: string | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, + }, + { + Code: `declare const x: string | null | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, + }, + { + Code: `declare const x: '' | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, + }, + { + Code: `declare const x: 'foo' | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, + }, + + // ============================================ + // NULLABLE NUMBER - Valid with Option + // ============================================ + + { + Code: `declare const x: number | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: `declare const x: number | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: `declare const x: number | null | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: `declare const x: 0 | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: `declare const x: 1 | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + { + Code: `declare const x: bigint | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, + }, + + // ============================================ + // NULLABLE OBJECT - Valid with Option + // ============================================ + + { + Code: `declare const x: object | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(true), + }, + }, + { + Code: `declare const x: object | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(true), + }, + }, + { + Code: `declare const x: object | null | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(true), + }, + }, + { + Code: `declare const x: {} | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(true), + }, + }, + { + Code: `declare const x: [] | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(true), + }, + }, + { + Code: `declare const x: symbol | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(true), + }, + }, + + // ============================================ + // NULLABLE ENUM - Valid with Option + // ============================================ + // NOTE: Enum detection from union types is complex - skipping for now + // { + // Code: `enum E { A = 0, B = 1 } declare const x: E | null; if (x) {}`, + // Options: StrictBooleanExpressionsOptions{ + // AllowNullableEnum: utils.Ref(true), + // }, + // }, + // { + // Code: `enum E { A = '', B = 'foo' } declare const x: E | undefined; if (x) {}`, + // Options: StrictBooleanExpressionsOptions{ + // AllowNullableEnum: utils.Ref(true), + // }, + // }, + + // ============================================ + // ARRAY METHOD PREDICATES + // ============================================ + + {Code: `[1, 2, 3].every(x => x > 0);`}, + {Code: `[1, 2, 3].some(x => x > 0);`}, + {Code: `[1, 2, 3].filter(x => x > 0);`}, + {Code: `declare const arr: string[]; arr.find(x => x === 'foo');`}, + {Code: `declare const arr: string[]; arr.findIndex(x => x === 'foo');`}, + + // With nullable predicates and options + { + Code: `[1, 2, 3].filter(x => x);`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), + }, + }, + { + Code: `['', 'foo'].filter(x => x);`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(true), + }, + }, + + // ============================================ + // ASSERT FUNCTIONS AND TYPE PREDICATES + // ============================================ + + {Code: `function isString(x: unknown): x is string { return typeof x === 'string'; }`}, + {Code: `declare function isString(x: unknown): x is string; if (isString(value)) {}`}, + + // ============================================ + // VOID TYPE - handled as nullish, so moved to invalid section + // ============================================ + + // ============================================ + // DOUBLE NEGATION - Always Valid + // ============================================ + + {Code: `!!true;`}, + {Code: `!!false;`}, + {Code: `!!'';`}, + {Code: `!!0;`}, + + // ============================================ + // BOOLEAN CONSTRUCTOR - Always Valid + // ============================================ + + {Code: `Boolean(true);`}, + {Code: `Boolean(false);`}, + {Code: `Boolean('');`}, + {Code: `Boolean(0);`}, + {Code: `Boolean({});`}, + {Code: `Boolean([]);`}, + {Code: `declare const x: any; Boolean(x);`}, + {Code: `declare const x: unknown; Boolean(x);`}, + + // ============================================ + // COMPLEX LOGICAL EXPRESSIONS + // ============================================ + + {Code: `true && true && true;`}, + {Code: `true || false || true;`}, + {Code: `(true && false) || (false && true);`}, + {Code: `true ? (false || true) : (true && false);`}, + + // Mixed types with default options + {Code: `'' || 0 || false;`}, + {Code: `'foo' && 1 && true;`}, + // NOTE: Objects in logical operators should be errors - commenting out incorrect test + // {Code: `({}) || [] || true;`}, + + // ============================================ + // SPECIAL CASES + // ============================================ + + // Always allow boolean in right side of logical operators + {Code: `'foo' && true;`}, + {Code: `0 || false;`}, + // NOTE: Object in logical operator should be error - commenting out incorrect test + // {Code: `({}) && (1 > 2);`}, + + // Template literals + {Code: "declare const x: string; `foo${x}` ? 'a' : 'b';"}, + {Code: "declare const x: number; `foo${x}` ? 'a' : 'b';"}, + + // Parenthesized expressions + {Code: `(true) ? 'a' : 'b';`}, + {Code: `((true)) ? 'a' : 'b';`}, + {Code: `if ((true)) {}`}, + + // Comma operator + {Code: `(0, true) ? 'a' : 'b';`}, + {Code: `('', false) ? 'a' : 'b';`}, + + // Assignment expressions + {Code: `let x; (x = true) ? 'a' : 'b';`}, + {Code: `let x; if (x = false) {}`}, + + // Never type - allowed per TypeScript ESLint + {Code: `declare const x: never; if (x) {}`}, + {Code: `declare const x: never; x ? 'a' : 'b';`}, + {Code: `declare const x: never; !x;`}, + }, []rule_tester.InvalidTestCase{ + // ============================================ + // ANY TYPE - Invalid without Option + // ============================================ + + { + Code: `declare const x: any; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, + }, + { + Code: `declare const x: any; x ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, + }, + { + Code: `declare const x: any; while (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, + }, + { + Code: `declare const x: any; do {} while (x);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, + }, + { + Code: `declare const x: any; for (; x; ) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, + }, + { + Code: `declare const x: any; x && 'a';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, + }, + { + Code: `declare const x: any; x || 'a';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, + }, + { + Code: `declare const x: any; !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, + }, + + // ============================================ + // NULLISH VALUES - Always Invalid + // ============================================ + + { + Code: `if (null) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `if (undefined) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `null ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `undefined ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `while (null) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `do {} while (undefined);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `for (; null; ) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `!null;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `!undefined;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `null && 'a';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `undefined || 'a';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + + // ============================================ + // STRING TYPE - Invalid with allowString: false + // ============================================ + + { + Code: `if ('') {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `if ('foo') {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `'' ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `'foo' ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `while ('') {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `do {} while ('foo');`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `for (; 'foo'; ) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `!'foo';`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `'foo' && 'bar';`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `'' || 'default';`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `declare const s: string; if (s) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + + // Template literals + { + Code: "`` ? 'a' : 'b';", + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: "`foo` ? 'a' : 'b';", + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: "declare const x: string; `foo${x}` ? 'a' : 'b';", + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + + // ============================================ + // NUMBER TYPE - Invalid with allowNumber: false + // ============================================ + + { + Code: `if (0) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `if (1) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `0 ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `1 ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `NaN ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `while (0) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `do {} while (1);`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `for (; 1; ) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `!0;`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `1 && 2;`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `0 || 1;`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `declare const n: number; if (n) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + + // BigInt + { + Code: `if (0n) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `if (1n) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `0n ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `declare const b: bigint; if (b) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + + // ============================================ + // OBJECT TYPE - Always Invalid (Always Truthy) + // ============================================ + + { + Code: `if ({}) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `if ([]) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `({}) ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `[] ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `while ({}) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `do {} while ([]);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `for (; {}; ) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `!{};`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `![];`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `({}) && 'a';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `[] || 'a';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `declare const o: object; if (o) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `declare const o: {}; if (o) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + + // Functions + { + Code: `function foo() {}; if (foo) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `const foo = () => {}; if (foo) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + + // Symbols + { + Code: `if (Symbol()) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `declare const s: symbol; if (s) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + + // ============================================ + // NULLABLE BOOLEAN - Invalid without Option + // ============================================ + + { + Code: `declare const x: boolean | null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, + }, + { + Code: `declare const x: boolean | undefined; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, + }, + { + Code: `declare const x: boolean | null | undefined; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, + }, + { + Code: `declare const x: true | null; x ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, + }, + { + Code: `declare const x: false | undefined; !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, + }, + + // ============================================ + // NULLABLE STRING - Invalid without Option + // ============================================ + + { + Code: `declare const x: string | null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, + }, + { + Code: `declare const x: string | undefined; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, + }, + { + Code: `declare const x: string | null | undefined; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, + }, + { + Code: `declare const x: '' | null; x ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, + }, + { + Code: `declare const x: 'foo' | undefined; !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, + }, + + // ============================================ + // NULLABLE NUMBER - Invalid without Option + // ============================================ + + { + Code: `declare const x: number | null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, + }, + { + Code: `declare const x: number | undefined; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, + }, + { + Code: `declare const x: number | null | undefined; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, + }, + { + Code: `declare const x: 0 | null; x ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, + }, + { + Code: `declare const x: 1 | undefined; !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, + }, + { + Code: `declare const x: bigint | null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, + }, + + // ============================================ + // NULLABLE OBJECT - Invalid without Option + // ============================================ + + { + Code: `declare const x: object | null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, + }, + { + Code: `declare const x: object | undefined; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, + }, + { + Code: `declare const x: object | null | undefined; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, + }, + { + Code: `declare const x: {} | null; x ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, + }, + { + Code: `declare const x: [] | undefined; !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, + }, + { + Code: `declare const x: symbol | null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, + }, + + // ============================================ + // MIXED TYPES - Invalid + // ============================================ + + { + Code: `declare const x: string | number; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 1}, + }, + }, + { + Code: `declare const x: string | boolean; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 1}, + }, + }, + { + Code: `declare const x: number | boolean; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 1}, + }, + }, + { + Code: `declare const x: string | number | boolean; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 1}, + }, + }, + + // ============================================ + // ENUM TYPES + // ============================================ + + { + Code: `enum E { A = 0, B = 1 } declare const x: E; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `enum E { A = '', B = 'foo' } declare const x: E; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + + // ============================================ + // ARRAY METHOD PREDICATES - Invalid + // ============================================ + // NOTE: Arrow function predicate checking not implemented - skipping for now + // { + // Code: `[1, 2, 3].filter(x => x);`, + // Options: StrictBooleanExpressionsOptions{ + // AllowNumber: utils.Ref(false), + // }, + // Errors: []rule_tester.InvalidTestCaseError{ + // {MessageId: "unexpectedNumber", Line: 1}, + // }, + // }, + // { + // Code: `['', 'foo'].filter(x => x);`, + // Options: StrictBooleanExpressionsOptions{ + // AllowString: utils.Ref(false), + // }, + // Errors: []rule_tester.InvalidTestCaseError{ + // {MessageId: "unexpectedString", Line: 1}, + // }, + // }, + // { + // Code: `[{}, []].filter(x => x);`, + // Errors: []rule_tester.InvalidTestCaseError{ + // {MessageId: "unexpectedObjectContext", Line: 1}, + // }, + // }, + + // ============================================ + // COMPLEX LOGICAL EXPRESSIONS - Invalid + // ============================================ + + { + Code: `'foo' && 1;`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `0 || 'bar';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `({}) && [];`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + { + Code: `'' || 0;`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + + // ============================================ + // SPECIAL CASES - Invalid + // ============================================ + + // Array.length + { + Code: `declare const arr: string[]; if (arr.length) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + + // Function calls returning non-boolean + { + Code: `declare function getString(): string; if (getString()) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `declare function getNumber(): number; if (getNumber()) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `declare function getObject(): object; if (getObject()) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, + + // Property access + { + Code: `declare const obj: { prop: string }; if (obj.prop) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `declare const obj: { prop: number }; if (obj.prop) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + + // Void type + { + Code: `declare const x: void; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + { + Code: `void 0 ? 'a' : 'b';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, + }, + }) +} From 4b6e5466ee9dc794741fd31a68a7e4257bb37aa6 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Thu, 11 Sep 2025 10:10:55 +0900 Subject: [PATCH 02/23] Uncomment --- .../strict_boolean_expressions_test.go | 75 +++++++------------ 1 file changed, 25 insertions(+), 50 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index df301a28..a5c8122f 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -338,23 +338,6 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, }, - // ============================================ - // NULLABLE ENUM - Valid with Option - // ============================================ - // NOTE: Enum detection from union types is complex - skipping for now - // { - // Code: `enum E { A = 0, B = 1 } declare const x: E | null; if (x) {}`, - // Options: StrictBooleanExpressionsOptions{ - // AllowNullableEnum: utils.Ref(true), - // }, - // }, - // { - // Code: `enum E { A = '', B = 'foo' } declare const x: E | undefined; if (x) {}`, - // Options: StrictBooleanExpressionsOptions{ - // AllowNullableEnum: utils.Ref(true), - // }, - // }, - // ============================================ // ARRAY METHOD PREDICATES // ============================================ @@ -386,10 +369,6 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { {Code: `function isString(x: unknown): x is string { return typeof x === 'string'; }`}, {Code: `declare function isString(x: unknown): x is string; if (isString(value)) {}`}, - // ============================================ - // VOID TYPE - handled as nullish, so moved to invalid section - // ============================================ - // ============================================ // DOUBLE NEGATION - Always Valid // ============================================ @@ -424,8 +403,6 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { // Mixed types with default options {Code: `'' || 0 || false;`}, {Code: `'foo' && 1 && true;`}, - // NOTE: Objects in logical operators should be errors - commenting out incorrect test - // {Code: `({}) || [] || true;`}, // ============================================ // SPECIAL CASES @@ -434,8 +411,6 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { // Always allow boolean in right side of logical operators {Code: `'foo' && true;`}, {Code: `0 || false;`}, - // NOTE: Object in logical operator should be error - commenting out incorrect test - // {Code: `({}) && (1 > 2);`}, // Template literals {Code: "declare const x: string; `foo${x}` ? 'a' : 'b';"}, @@ -1199,31 +1174,31 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { // ============================================ // ARRAY METHOD PREDICATES - Invalid // ============================================ - // NOTE: Arrow function predicate checking not implemented - skipping for now - // { - // Code: `[1, 2, 3].filter(x => x);`, - // Options: StrictBooleanExpressionsOptions{ - // AllowNumber: utils.Ref(false), - // }, - // Errors: []rule_tester.InvalidTestCaseError{ - // {MessageId: "unexpectedNumber", Line: 1}, - // }, - // }, - // { - // Code: `['', 'foo'].filter(x => x);`, - // Options: StrictBooleanExpressionsOptions{ - // AllowString: utils.Ref(false), - // }, - // Errors: []rule_tester.InvalidTestCaseError{ - // {MessageId: "unexpectedString", Line: 1}, - // }, - // }, - // { - // Code: `[{}, []].filter(x => x);`, - // Errors: []rule_tester.InvalidTestCaseError{ - // {MessageId: "unexpectedObjectContext", Line: 1}, - // }, - // }, + + { + Code: `[1, 2, 3].filter(x => x);`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, + }, + { + Code: `['', 'foo'].filter(x => x);`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, + }, + { + Code: `[{}, []].filter(x => x);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, + }, // ============================================ // COMPLEX LOGICAL EXPRESSIONS - Invalid From 029ddc0401434c7c9df95158d47c83fe36e4314b Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Fri, 12 Sep 2025 13:40:28 +0900 Subject: [PATCH 03/23] Apply patch --- .../strict_boolean_expressions.go | 118 ++++++++------- .../strict_boolean_expressions_test.go | 140 ++++++++++++++++-- .../0001-Parallel-readDirectory-visitor.patch | 2 +- ...-project-service-for-single-run-mode.patch | 2 +- ...unsafe.String-to-avoid-allocations-w.patch | 2 +- ...e-functions-via-the-shim-with-type-f.patch | 2 +- ...es-add-public-getter-from-TypePredic.patch | 31 ++++ 7 files changed, 232 insertions(+), 65 deletions(-) create mode 100644 patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 500f2927..9a287d6e 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -143,48 +143,39 @@ var StrictBooleanExpressionsRule = rule.Rule{ return rule.RuleListeners{} } - checkNode := func(node *ast.Node) { - nodeType := ctx.TypeChecker.GetTypeAtLocation(node) - checkCondition(ctx, node, nodeType, opts) - } - return rule.RuleListeners{ ast.KindIfStatement: func(node *ast.Node) { ifStmt := node.AsIfStatement() - checkNode(ifStmt.Expression) + traverseNode(ctx, ifStmt.Expression, opts, true) }, ast.KindWhileStatement: func(node *ast.Node) { whileStmt := node.AsWhileStatement() - checkNode(whileStmt.Expression) + traverseNode(ctx, whileStmt.Expression, opts, true) }, ast.KindDoStatement: func(node *ast.Node) { doStmt := node.AsDoStatement() - checkNode(doStmt.Expression) + traverseNode(ctx, doStmt.Expression, opts, true) }, ast.KindForStatement: func(node *ast.Node) { forStmt := node.AsForStatement() if forStmt.Condition != nil { - checkNode(forStmt.Condition) + traverseNode(ctx, forStmt.Condition, opts, true) } }, ast.KindConditionalExpression: func(node *ast.Node) { condExpr := node.AsConditionalExpression() - checkNode(condExpr.Condition) + traverseNode(ctx, condExpr.Condition, opts, true) }, ast.KindBinaryExpression: func(node *ast.Node) { - binExpr := node.AsBinaryExpression() - switch binExpr.OperatorToken.Kind { - case ast.KindAmpersandAmpersandToken, ast.KindBarBarToken: - checkNode(binExpr.Left) - } + traverseNode(ctx, node, opts, false) }, ast.KindCallExpression: func(node *ast.Node) { callExpr := node.AsCallExpression() - //assertedArgument := findTruthinessAssertedArgument(ctx.TypeChecker, callExpr) - //if assertedArgument != nil { - // checkNode(assertedArgument) - //} + assertedArgument := findTruthinessAssertedArgument(ctx.TypeChecker, callExpr) + if assertedArgument != nil { + traverseNode(ctx, assertedArgument, opts, true) + } if callExpr.Expression != nil && callExpr.Expression.Kind == ast.KindPropertyAccessExpression { propAccess := callExpr.Expression.AsPropertyAccessExpression() @@ -222,40 +213,68 @@ var StrictBooleanExpressionsRule = rule.Rule{ ast.KindPrefixUnaryExpression: func(node *ast.Node) { unaryExpr := node.AsPrefixUnaryExpression() if unaryExpr.Operator == ast.KindExclamationToken { - checkNode(unaryExpr.Operand) + traverseNode(ctx, unaryExpr.Operand, opts, true) } }, } }, } -// TODO: Not exported TypePredicate struct variables -//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 { -// continue -// } -// 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 firstTypePredicateResult.kind != checker.TypePredicateKindAssertsIdentifier || -// firstTypePredicateResult.type == nil { -// return nil -// } -// return checkableArguments[firstTypePredicateResult.parameterIndex] -//} +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 { + continue + } + 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 firstTypePredicateResult.Kind() != checker.TypePredicateKindAssertsIdentifier || + firstTypePredicateResult.Type() != nil { + return nil + } + if firstTypePredicateResult.ParameterIndex() >= int32(len(checkableArguments)) { + return nil + } + return checkableArguments[firstTypePredicateResult.ParameterIndex()] +} + +func checkNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpressionsOptions) { + nodeType := ctx.TypeChecker.GetTypeAtLocation(node) + checkCondition(ctx, node, 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 node.Kind == ast.KindBinaryExpression { + binExpr := node.AsBinaryExpression() + switch binExpr.OperatorToken.Kind { + case ast.KindAmpersandAmpersandToken, ast.KindBarBarToken: + traverseLogicalExpression(ctx, binExpr, opts, true) + } + } + + if !isCondition { + return + } + + checkNode(ctx, node, opts) +} // Type analysis types type typeVariant int @@ -440,7 +459,7 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts ctx.ReportNode(node, buildUnexpectedNullish()) case typeVariantString: // Known edge case: truthy primitives and nullish values are always valid boolean expressions - if *opts.AllowString && info.isNullable && info.isTruthy { + if *opts.AllowString && info.isTruthy { return } @@ -458,8 +477,7 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts ctx.ReportNode(node, buildUnexpectedString()) } case typeVariantNumber: - // Known edge case: truthy primitives and nullish values are always valid boolean expressions - if *opts.AllowNumber && info.isNullable && info.isTruthy { + if *opts.AllowNumber && info.isTruthy { return } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index a5c8122f..f923bcf1 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -105,17 +105,6 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { {Code: `if (1n) {}`}, {Code: `declare const b: bigint; if (b) {}`}, - // ============================================ - // OBJECT TYPES in logical operators - // ============================================ - - // Note: Objects in logical operators are treated as boolean conditions - // and will be flagged as errors (always truthy) unless in the right side - // Right side of logical operators is not checked as a boolean condition - {Code: `'foo' || ({});`}, // Object on right side is OK - {Code: `false || [];`}, // Array on right side is OK - {Code: `(false && true) || [];`}, // Array on right side is OK - // ============================================ // ANY TYPE - Valid with Option // ============================================ @@ -368,6 +357,112 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { {Code: `function isString(x: unknown): x is string { return typeof x === 'string'; }`}, {Code: `declare function isString(x: unknown): x is string; if (isString(value)) {}`}, + { + 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); + `, + }, + // Intentional use of `any` to test a function call with no call signatures. + { + Code: ` + declare const assert: any; + declare const nullableString: string | null; + assert(nullableString); + `, + }, + // Coverage for absent "test expression". + // Ensure that no crash or false positive occurs + { + Code: ` + for (let x = 0; ; x++) { + break; + } + `, + }, // ============================================ // DOUBLE NEGATION - Always Valid @@ -641,6 +736,7 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, }, }, { @@ -650,6 +746,7 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, }, }, { @@ -783,6 +880,7 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, }, }, { @@ -792,6 +890,7 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, }, }, { @@ -939,6 +1038,23 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, }, + // ============================================ + // ASSERT FUNCTIONS AND TYPE PREDICATES + // ============================================ + + { + 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}, + }, + }, + // Symbols { Code: `if (Symbol()) {}`, @@ -1226,6 +1342,7 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { Code: `({}) && [];`, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1}, }, }, { @@ -1236,6 +1353,7 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, }, }, diff --git a/patches/0001-Parallel-readDirectory-visitor.patch b/patches/0001-Parallel-readDirectory-visitor.patch index 37262808..91d8c97e 100644 --- a/patches/0001-Parallel-readDirectory-visitor.patch +++ b/patches/0001-Parallel-readDirectory-visitor.patch @@ -1,7 +1,7 @@ From beb5230504d73ec11bd5a9f746a9d4b6eb8a2e59 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 18 Jun 2025 15:24:34 +0300 -Subject: [PATCH 1/4] Parallel readDirectory visitor +Subject: [PATCH 1/5] Parallel readDirectory visitor --- internal/vfs/utilities.go | 104 ++++++++++++++++++++++++++++---------- diff --git a/patches/0002-Adapt-project-service-for-single-run-mode.patch b/patches/0002-Adapt-project-service-for-single-run-mode.patch index 66877969..47040f71 100644 --- a/patches/0002-Adapt-project-service-for-single-run-mode.patch +++ b/patches/0002-Adapt-project-service-for-single-run-mode.patch @@ -1,7 +1,7 @@ From 70e5921f3e00d1fb3e968fc8dc5b9db1f01160b0 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Aug 2025 11:39:11 +0000 -Subject: [PATCH 2/4] Adapt project service for single run mode +Subject: [PATCH 2/5] Adapt project service for single run mode --- internal/project/checkerpool.go | 9 +++++++++ diff --git a/patches/0003-perf-tspath-use-unsafe.String-to-avoid-allocations-w.patch b/patches/0003-perf-tspath-use-unsafe.String-to-avoid-allocations-w.patch index 35bc704c..a2b8cfca 100644 --- a/patches/0003-perf-tspath-use-unsafe.String-to-avoid-allocations-w.patch +++ b/patches/0003-perf-tspath-use-unsafe.String-to-avoid-allocations-w.patch @@ -1,7 +1,7 @@ From d55ed5ae63cf53e04abe24f5de8574beb244d5cf Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Fri, 15 Aug 2025 16:06:42 +0100 -Subject: [PATCH 3/4] perf(tspath): use `unsafe.String` to avoid allocations +Subject: [PATCH 3/5] perf(tspath): use `unsafe.String` to avoid allocations when lower casing string --- diff --git a/patches/0004-patch-expose-more-functions-via-the-shim-with-type-f.patch b/patches/0004-patch-expose-more-functions-via-the-shim-with-type-f.patch index 31872df6..919dc014 100644 --- a/patches/0004-patch-expose-more-functions-via-the-shim-with-type-f.patch +++ b/patches/0004-patch-expose-more-functions-via-the-shim-with-type-f.patch @@ -1,7 +1,7 @@ From c0aae26eba85f1c1255f2108af390b567f786f1b Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Fri, 29 Aug 2025 17:49:17 +0100 -Subject: [PATCH 4/4] patch: expose more functions via the shim with type fixes +Subject: [PATCH 4/5] patch: expose more functions via the shim with type fixes --- internal/project/compilerhost.go | 4 +- diff --git a/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch b/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch new file mode 100644 index 00000000..b49868ca --- /dev/null +++ b/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch @@ -0,0 +1,31 @@ +From 5f2b70a9908160df84f6b37c8480c8f94aad117d Mon Sep 17 00:00:00 2001 +From: Noel Kim +Date: Thu, 11 Sep 2025 11:46:56 +0900 +Subject: [PATCH] feat(checker/types): add public getter from `TypePredicate` + +--- + internal/checker/types.go | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/internal/checker/types.go b/internal/checker/types.go +index 5fd4b1583..c8bdf3e98 100644 +--- a/internal/checker/types.go ++++ b/internal/checker/types.go +@@ -1192,6 +1192,14 @@ type TypePredicate struct { + t *Type + } + ++func (t *TypePredicate) Kind() TypePredicateKind { return t.kind } ++ ++func (t *TypePredicate) ParameterIndex() int32 { return t.parameterIndex } ++ ++func (t *TypePredicate) ParameterName() string { return t.parameterName } ++ ++func (t *TypePredicate) Type() *Type { return t.t } ++ + // IndexInfo + + type IndexInfo struct { +-- +2.39.5 (Apple Git-154) + From a5cd739c917f2f690897d1cf76aea72b9e5f0a46 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Fri, 12 Sep 2025 13:55:46 +0900 Subject: [PATCH 04/23] patch subject --- ...5-feat-checker-types-add-public-getter-from-TypePredic.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch b/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch index b49868ca..44202c41 100644 --- a/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch +++ b/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch @@ -1,7 +1,7 @@ From 5f2b70a9908160df84f6b37c8480c8f94aad117d Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Thu, 11 Sep 2025 11:46:56 +0900 -Subject: [PATCH] feat(checker/types): add public getter from `TypePredicate` +Subject: [PATCH 5/5] feat(checker/types): add public getter from `TypePredicate` --- internal/checker/types.go | 8 ++++++++ From fa3132215daa77224373078ff661c8b4bec4ed38 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Fri, 12 Sep 2025 15:26:39 +0900 Subject: [PATCH 05/23] extra shim instead of patch --- .../strict_boolean_expressions.go | 9 +++--- .../0001-Parallel-readDirectory-visitor.patch | 2 +- ...-project-service-for-single-run-mode.patch | 2 +- ...unsafe.String-to-avoid-allocations-w.patch | 2 +- ...e-functions-via-the-shim-with-type-f.patch | 2 +- ...es-add-public-getter-from-TypePredic.patch | 31 ------------------- shim/checker/extra-shim.json | 5 +++ shim/checker/shim.go | 15 +++++++++ 8 files changed, 29 insertions(+), 39 deletions(-) delete mode 100644 patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 9a287d6e..7bea5e02 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -240,14 +240,15 @@ func findTruthinessAssertedArgument(typeChecker *checker.Checker, callExpr *ast. if firstTypePredicateResult == nil { return nil } - if firstTypePredicateResult.Kind() != checker.TypePredicateKindAssertsIdentifier || - firstTypePredicateResult.Type() != nil { + if checker.TypePredicate_kind(firstTypePredicateResult) != checker.TypePredicateKindAssertsIdentifier || + checker.TypePredicate_t(firstTypePredicateResult) != nil { return nil } - if firstTypePredicateResult.ParameterIndex() >= int32(len(checkableArguments)) { + parameterIndex := checker.TypePredicate_parameterIndex(firstTypePredicateResult) + if parameterIndex >= int32(len(checkableArguments)) { return nil } - return checkableArguments[firstTypePredicateResult.ParameterIndex()] + return checkableArguments[parameterIndex] } func checkNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpressionsOptions) { diff --git a/patches/0001-Parallel-readDirectory-visitor.patch b/patches/0001-Parallel-readDirectory-visitor.patch index 91d8c97e..37262808 100644 --- a/patches/0001-Parallel-readDirectory-visitor.patch +++ b/patches/0001-Parallel-readDirectory-visitor.patch @@ -1,7 +1,7 @@ From beb5230504d73ec11bd5a9f746a9d4b6eb8a2e59 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 18 Jun 2025 15:24:34 +0300 -Subject: [PATCH 1/5] Parallel readDirectory visitor +Subject: [PATCH 1/4] Parallel readDirectory visitor --- internal/vfs/utilities.go | 104 ++++++++++++++++++++++++++++---------- diff --git a/patches/0002-Adapt-project-service-for-single-run-mode.patch b/patches/0002-Adapt-project-service-for-single-run-mode.patch index 47040f71..66877969 100644 --- a/patches/0002-Adapt-project-service-for-single-run-mode.patch +++ b/patches/0002-Adapt-project-service-for-single-run-mode.patch @@ -1,7 +1,7 @@ From 70e5921f3e00d1fb3e968fc8dc5b9db1f01160b0 Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 6 Aug 2025 11:39:11 +0000 -Subject: [PATCH 2/5] Adapt project service for single run mode +Subject: [PATCH 2/4] Adapt project service for single run mode --- internal/project/checkerpool.go | 9 +++++++++ diff --git a/patches/0003-perf-tspath-use-unsafe.String-to-avoid-allocations-w.patch b/patches/0003-perf-tspath-use-unsafe.String-to-avoid-allocations-w.patch index a2b8cfca..35bc704c 100644 --- a/patches/0003-perf-tspath-use-unsafe.String-to-avoid-allocations-w.patch +++ b/patches/0003-perf-tspath-use-unsafe.String-to-avoid-allocations-w.patch @@ -1,7 +1,7 @@ From d55ed5ae63cf53e04abe24f5de8574beb244d5cf Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Fri, 15 Aug 2025 16:06:42 +0100 -Subject: [PATCH 3/5] perf(tspath): use `unsafe.String` to avoid allocations +Subject: [PATCH 3/4] perf(tspath): use `unsafe.String` to avoid allocations when lower casing string --- diff --git a/patches/0004-patch-expose-more-functions-via-the-shim-with-type-f.patch b/patches/0004-patch-expose-more-functions-via-the-shim-with-type-f.patch index 919dc014..31872df6 100644 --- a/patches/0004-patch-expose-more-functions-via-the-shim-with-type-f.patch +++ b/patches/0004-patch-expose-more-functions-via-the-shim-with-type-f.patch @@ -1,7 +1,7 @@ From c0aae26eba85f1c1255f2108af390b567f786f1b Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Fri, 29 Aug 2025 17:49:17 +0100 -Subject: [PATCH 4/5] patch: expose more functions via the shim with type fixes +Subject: [PATCH 4/4] patch: expose more functions via the shim with type fixes --- internal/project/compilerhost.go | 4 +- diff --git a/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch b/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch deleted file mode 100644 index 44202c41..00000000 --- a/patches/0005-feat-checker-types-add-public-getter-from-TypePredic.patch +++ /dev/null @@ -1,31 +0,0 @@ -From 5f2b70a9908160df84f6b37c8480c8f94aad117d Mon Sep 17 00:00:00 2001 -From: Noel Kim -Date: Thu, 11 Sep 2025 11:46:56 +0900 -Subject: [PATCH 5/5] feat(checker/types): add public getter from `TypePredicate` - ---- - internal/checker/types.go | 8 ++++++++ - 1 file changed, 8 insertions(+) - -diff --git a/internal/checker/types.go b/internal/checker/types.go -index 5fd4b1583..c8bdf3e98 100644 ---- a/internal/checker/types.go -+++ b/internal/checker/types.go -@@ -1192,6 +1192,14 @@ type TypePredicate struct { - t *Type - } - -+func (t *TypePredicate) Kind() TypePredicateKind { return t.kind } -+ -+func (t *TypePredicate) ParameterIndex() int32 { return t.parameterIndex } -+ -+func (t *TypePredicate) ParameterName() string { return t.parameterName } -+ -+func (t *TypePredicate) Type() *Type { return t.t } -+ - // IndexInfo - - type IndexInfo struct { --- -2.39.5 (Apple Git-154) - 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 From 708ffe77304f1a86ea30749a805626f2feee0b2c Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 11:36:58 +0900 Subject: [PATCH 06/23] wip --- .../strict_boolean_expressions.go | 44 +- .../strict_boolean_expressions_single_test.go | 16 + .../strict_boolean_expressions_test.go | 1428 ++++++++--------- 3 files changed, 672 insertions(+), 816 deletions(-) create mode 100644 internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 7bea5e02..1d8d8676 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -252,7 +252,7 @@ func findTruthinessAssertedArgument(typeChecker *checker.Checker, callExpr *ast. } func checkNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpressionsOptions) { - nodeType := ctx.TypeChecker.GetTypeAtLocation(node) + nodeType := utils.GetConstrainedTypeAtLocation(ctx.TypeChecker, node) checkCondition(ctx, node, nodeType, opts) } @@ -264,9 +264,8 @@ func traverseLogicalExpression(ctx rule.RuleContext, binExpr *ast.BinaryExpressi func traverseNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpressionsOptions, isCondition bool) { if node.Kind == ast.KindBinaryExpression { binExpr := node.AsBinaryExpression() - switch binExpr.OperatorToken.Kind { - case ast.KindAmpersandAmpersandToken, ast.KindBarBarToken: - traverseLogicalExpression(ctx, binExpr, opts, true) + if binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { + traverseLogicalExpression(ctx, binExpr, opts, false) } } @@ -313,28 +312,24 @@ func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { info.isUnion = true parts := utils.UnionTypeParts(t) variants := make(map[typeVariant]bool) - hasNullish := false - hasEnum := false for _, part := range parts { partInfo := analyzeTypePart(typeChecker, part) variants[partInfo.variant] = true if partInfo.variant == typeVariantNullish { - hasNullish = true + info.isNullable = true } if partInfo.isEnum { - hasEnum = true + info.isEnum = true } + info.isTruthy = partInfo.isTruthy } - info.isNullable = hasNullish - info.isEnum = hasEnum - if len(variants) == 1 { for v := range variants { info.variant = v } - } else if len(variants) == 2 && hasNullish { + } else if len(variants) == 2 && info.isNullable { for v := range variants { if v != typeVariantNullish { info.variant = v @@ -350,14 +345,26 @@ func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { if utils.IsIntersectionType(t) { info.isIntersection = true - info.variant = typeVariantObject + 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 } return analyzeTypePart(typeChecker, t) } -func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { +func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { info := typeInfo{} flags := checker.Type_flags(t) @@ -386,7 +393,10 @@ func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { return info } - if flags&(checker.TypeFlagsBoolean|checker.TypeFlagsBooleanLiteral) != 0 { + if flags&(checker.TypeFlagsBoolean|checker.TypeFlagsBooleanLiteral|checker.TypeFlagsBooleanLike) != 0 { + if flags&checker.TypeFlagsBooleanLike != 0 && t.AsIntrinsicType().IntrinsicName() == "true" { + info.isTruthy = true + } info.variant = typeVariantBoolean return info } @@ -430,9 +440,7 @@ func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { return info } - if flags&checker.TypeFlagsObject != 0 || - flags&checker.TypeFlagsNonPrimitive != 0 || - utils.IsObjectType(t) { + if flags&(checker.TypeFlagsObject|checker.TypeFlagsNonPrimitive) != 0 { info.variant = typeVariantObject return info } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go new file mode 100644 index 00000000..6180fe5b --- /dev/null +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -0,0 +1,16 @@ +// Code generated from strict-boolean-expressions.test.ts - DO NOT EDIT. + +package strict_boolean_expressions + +import ( + "testing" + + "github.com/typescript-eslint/tsgolint/internal/rule_tester" + "github.com/typescript-eslint/tsgolint/internal/rules/fixtures" +) + +func TestStrictBooleanExpressionsSingleRule(t *testing.T) { + rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ + {Code: `declare const x: null | object; if (x) {}`}, + }, []rule_tester.InvalidTestCase{}) +} \ No newline at end of file diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index f923bcf1..82007171 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -1,3 +1,5 @@ +// Code generated from strict-boolean-expressions.test.ts - DO NOT EDIT. + package strict_boolean_expressions import ( @@ -9,176 +11,59 @@ import ( ) func TestStrictBooleanExpressionsRule(t *testing.T) { - // Test with strictNullChecks enabled (in fixtures/tsconfig.json) rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ - // ============================================ - // BOOLEAN TYPES - Always Valid - // ============================================ - - // Boolean literals + // ======================================== + // BOOLEAN IN BOOLEAN CONTEXT + // ======================================== {Code: `true ? 'a' : 'b';`}, - {Code: `false ? 'a' : 'b';`}, - {Code: `if (true) {}`}, {Code: `if (false) {}`}, {Code: `while (true) {}`}, - {Code: `do {} while (true);`}, - {Code: `for (; 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) {}`}, - // Boolean variables - {Code: `const b = true; if (b) {}`}, - {Code: `const b = false; if (!b) {}`}, - {Code: `declare const b: boolean; if (b) {}`}, - - // Boolean expressions - {Code: `if (true && false) {}`}, - {Code: `if (true || false) {}`}, - {Code: `true && 'a';`}, - {Code: `false || 'a';`}, - - // Comparison operators return boolean - {Code: `1 > 2 ? 'a' : 'b';`}, - {Code: `1 < 2 ? 'a' : 'b';`}, - {Code: `1 >= 2 ? 'a' : 'b';`}, - {Code: `1 <= 2 ? 'a' : 'b';`}, - {Code: `1 == 2 ? 'a' : 'b';`}, - {Code: `1 != 2 ? 'a' : 'b';`}, - {Code: `1 === 2 ? 'a' : 'b';`}, - {Code: `1 !== 2 ? 'a' : 'b';`}, - - // Type guards - {Code: `declare const x: string | number; if (typeof x === 'string') {}`}, - {Code: `declare const x: any; if (x instanceof Error) {}`}, - {Code: `declare const x: any; if ('prop' in x) {}`}, - - // Function returning boolean - {Code: `function test(): boolean { return true; } if (test()) {}`}, - {Code: `declare function test(): boolean; if (test()) {}`}, - - // ============================================ - // STRING TYPES - Valid with Default Options - // ============================================ - - {Code: `'' ? 'a' : 'b';`}, - {Code: `'foo' ? 'a' : 'b';`}, - {Code: "`` ? 'a' : 'b';"}, - {Code: "`foo` ? 'a' : 'b';"}, - {Code: "`foo${bar}` ? 'a' : 'b';"}, + // ======================================== + // STRING IN BOOLEAN CONTEXT + // ======================================== {Code: `if ('') {}`}, - {Code: `if ('foo') {}`}, - {Code: `while ('') {}`}, - {Code: `do {} while ('foo');`}, - {Code: `for (; 'foo'; ) {}`}, - {Code: `!!'foo';`}, - {Code: `declare const s: string; if (s) {}`}, - - // String with logical operators - {Code: `'' || 'foo';`}, - {Code: `'foo' && 'bar';`}, - {Code: `declare const s: string; s || 'default';`}, - - // ============================================ - // NUMBER TYPES - Valid with Default Options - // ============================================ - - {Code: `0 ? 'a' : 'b';`}, - {Code: `1 ? 'a' : 'b';`}, - {Code: `-1 ? 'a' : 'b';`}, - {Code: `0.5 ? 'a' : 'b';`}, - {Code: `NaN ? 'a' : 'b';`}, + {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);`}, + + // ======================================== + // NUMBER IN BOOLEAN CONTEXT + // ======================================== {Code: `if (0) {}`}, - {Code: `if (1) {}`}, - {Code: `while (0) {}`}, - {Code: `do {} while (1);`}, - {Code: `for (; 1; ) {}`}, - {Code: `declare const n: number; if (n) {}`}, - - // Number with logical operators - {Code: `0 || 1;`}, - {Code: `1 && 2;`}, - {Code: `declare const n: number; n || 0;`}, - - // BigInt - {Code: `0n ? 'a' : 'b';`}, - {Code: `1n ? 'a' : 'b';`}, - {Code: `if (0n) {}`}, - {Code: `if (1n) {}`}, - {Code: `declare const b: bigint; if (b) {}`}, - - // ============================================ - // ANY TYPE - Valid with Option - // ============================================ - - { - Code: `declare const x: any; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - { - Code: `declare const x: any; x ? 'a' : 'b';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - { - Code: `declare const x: any; x && 'a';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - { - Code: `declare const x: any; x || 'a';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - { - Code: `declare const x: any; !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - - // ============================================ - // UNKNOWN TYPE - Valid with Option - // ============================================ - - { - Code: `declare const x: unknown; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - { - Code: `declare const x: unknown; x ? 'a' : 'b';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - { - Code: `declare const x: unknown; x && 'a';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - { - Code: `declare const x: unknown; x || 'a';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - { - Code: `declare const x: unknown; !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - - // ============================================ - // NULLABLE BOOLEAN - Valid with Option - // ============================================ - + {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);`}, + + // ======================================== + // NULLABLE OBJECT IN BOOLEAN CONTEXT + // ======================================== + {Code: `declare const x: null | object; if (x) {}`}, + {Code: `(x?: { a: any }) => !x;`}, + {Code: `(x: T) => (x ? 1 : 0);`}, + + // ======================================== + // NULLABLE BOOLEAN IN BOOLEAN CONTEXT + // ======================================== { Code: `declare const x: boolean | null; if (x) {}`, Options: StrictBooleanExpressionsOptions{ @@ -186,40 +71,27 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, }, { - Code: `declare const x: boolean | undefined; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableBoolean: utils.Ref(true), - }, - }, - { - Code: `declare const x: boolean | null | undefined; if (x) {}`, + Code: `(x?: boolean) => !x;`, Options: StrictBooleanExpressionsOptions{ AllowNullableBoolean: utils.Ref(true), }, }, { - Code: `declare const x: true | null; if (x) {}`, + Code: `(x: T) => (x ? 1 : 0);`, Options: StrictBooleanExpressionsOptions{ AllowNullableBoolean: utils.Ref(true), }, }, { - Code: `declare const x: false | undefined; if (x) {}`, + Code: `declare const test?: boolean; if (test ?? false) {}`, Options: StrictBooleanExpressionsOptions{ AllowNullableBoolean: utils.Ref(true), }, }, - // ============================================ - // NULLABLE STRING - Valid with Option - // ============================================ - - { - Code: `declare const x: string | null; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableString: utils.Ref(true), - }, - }, + // ======================================== + // NULLABLE STRING IN BOOLEAN CONTEXT + // ======================================== { Code: `declare const x: string | undefined; if (x) {}`, Options: StrictBooleanExpressionsOptions{ @@ -227,674 +99,648 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, }, { - Code: `declare const x: string | null | undefined; if (x) {}`, + Code: `(x?: string) => !x;`, Options: StrictBooleanExpressionsOptions{ AllowNullableString: utils.Ref(true), }, }, { - Code: `declare const x: '' | null; if (x) {}`, + Code: `(x: T) => (x ? 1 : 0);`, Options: StrictBooleanExpressionsOptions{ AllowNullableString: utils.Ref(true), }, }, { - Code: `declare const x: 'foo' | undefined; if (x) {}`, + Code: `'string' != null ? 'asd' : '';`, Options: StrictBooleanExpressionsOptions{ AllowNullableString: utils.Ref(true), }, }, - // ============================================ - // NULLABLE NUMBER - Valid with Option - // ============================================ - + // ======================================== + // NULLABLE NUMBER IN BOOLEAN CONTEXT + // ======================================== { - Code: `declare const x: number | null; if (x) {}`, + Code: `declare const x: number | undefined; if (x) {}`, Options: StrictBooleanExpressionsOptions{ AllowNullableNumber: utils.Ref(true), }, }, { - Code: `declare const x: number | undefined; if (x) {}`, + Code: `(x?: number) => !x;`, Options: StrictBooleanExpressionsOptions{ AllowNullableNumber: utils.Ref(true), }, }, { - Code: `declare const x: number | null | undefined; if (x) {}`, + Code: `(x: T) => (x ? 1 : 0);`, Options: StrictBooleanExpressionsOptions{ AllowNullableNumber: utils.Ref(true), }, }, { - Code: `declare const x: 0 | null; if (x) {}`, + Code: `declare const x: bigint | undefined; if (x ?? 0n) {}`, Options: StrictBooleanExpressionsOptions{ AllowNullableNumber: utils.Ref(true), }, }, + + // ======================================== + // ANY TYPE IN BOOLEAN CONTEXT + // ======================================== { - Code: `declare const x: 1 | undefined; if (x) {}`, + Code: `declare const x: any; if (x) {}`, Options: StrictBooleanExpressionsOptions{ - AllowNullableNumber: utils.Ref(true), + AllowAny: utils.Ref(true), }, }, { - Code: `declare const x: bigint | null; if (x) {}`, + Code: `(x?: any) => !x;`, Options: StrictBooleanExpressionsOptions{ - AllowNullableNumber: utils.Ref(true), + AllowAny: utils.Ref(true), }, }, - - // ============================================ - // NULLABLE OBJECT - Valid with Option - // ============================================ - { - Code: `declare const x: object | null; if (x) {}`, + Code: `(x: T) => (x ? 1 : 0);`, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(true), + AllowAny: utils.Ref(true), }, }, { - Code: `declare const x: object | undefined; if (x) {}`, + Code: `const foo: undefined | any = 0; if (foo) {}`, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(true), + AllowAny: utils.Ref(true), }, }, + + // ======================================== + // UNKNOWN TYPE IN BOOLEAN CONTEXT + // ======================================== { - Code: `declare const x: object | null | undefined; if (x) {}`, + Code: `declare const x: unknown; if (x) {}`, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(true), + AllowAny: utils.Ref(true), }, }, { - Code: `declare const x: {} | null; if (x) {}`, + Code: `(x: T) => (x ? 1 : 0);`, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(true), + AllowAny: utils.Ref(true), }, }, + + // ======================================== + // NULLABLE ENUM IN BOOLEAN CONTEXT + // ======================================== { - Code: `declare const x: [] | undefined; if (x) {}`, + Code: ` + enum E { A, B } + declare const x: E | null; + if (x) {} + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(true), + AllowNullableEnum: utils.Ref(true), }, }, { - Code: `declare const x: symbol | null; if (x) {}`, + Code: ` + enum E { A = 'a', B = 'b' } + declare const x: E | undefined; + if (x) {} + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(true), + AllowNullableEnum: utils.Ref(true), }, }, - // ============================================ - // ARRAY METHOD PREDICATES - // ============================================ - - {Code: `[1, 2, 3].every(x => x > 0);`}, - {Code: `[1, 2, 3].some(x => x > 0);`}, - {Code: `[1, 2, 3].filter(x => x > 0);`}, - {Code: `declare const arr: string[]; arr.find(x => x === 'foo');`}, - {Code: `declare const arr: string[]; arr.findIndex(x => x === 'foo');`}, + // ======================================== + // ALLOW RULE TO RUN WITHOUT STRICT NULL CHECKS + // ======================================== + { + Code: `if (true) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), + }, + }, - // With nullable predicates and options + // ======================================== + // COMPLEX BOOLEAN EXPRESSIONS + // ======================================== + {Code: `(x?: boolean) => x ?? false ? true : false;`}, + {Code: `(x: T) => x ?? false;`}, { - Code: `[1, 2, 3].filter(x => x);`, + Code: `(x?: string) => x ?? false;`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(true), + AllowNullableString: utils.Ref(true), }, }, { - Code: `['', 'foo'].filter(x => x);`, + Code: `(x?: number) => x ?? false;`, Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(true), + AllowNullableNumber: utils.Ref(true), }, }, - // ============================================ - // ASSERT FUNCTIONS AND TYPE PREDICATES - // ============================================ - - {Code: `function isString(x: unknown): x is string { return typeof x === 'string'; }`}, - {Code: `declare function isString(x: unknown): x is string; if (isString(value)) {}`}, - { - Code: ` + // ======================================== + // ASSERT FUNCTIONS + // ======================================== + {Code: ` + declare function assert(condition: unknown): asserts condition; + declare const x: string | null; + assert(x); + `}, + {Code: ` declare function assert(a: number, b: unknown): asserts a; declare const nullableString: string | null; declare const boo: boolean; assert(boo, nullableString); - `, - }, - { - Code: ` + `}, + {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: ` + `}, + {Code: ` declare function assert(a: number, b: unknown): asserts b; declare const nullableString: string | null; declare const boo: boolean; assert(nullableString, boo); - `, - }, - { - Code: ` + `}, + {Code: ` declare function assert(a: number, b: unknown): asserts b; declare const nullableString: string | null; declare const boo: boolean; assert(...nullableString, nullableString); - `, - }, - { - Code: ` + `}, + {Code: ` declare function assert( - this: object, - a: number, - b?: unknown, - c?: unknown, + 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, - }; + const o: { assert: typeof assert } = { assert }; o.assert(foo, nullableString); - `, - }, - { - Code: ` + `}, + {Code: ` declare function assert(x: unknown): x is string; declare const nullableString: string | null; assert(nullableString); - `, - }, - { - Code: ` + `}, + {Code: ` class ThisAsserter { - assertThis(this: unknown, arg2: unknown): asserts this {} + assertThis(this: unknown, arg2: unknown): asserts this {} } - declare const lol: string | number | unknown | null; - const thisAsserter: ThisAsserter = new ThisAsserter(); thisAsserter.assertThis(lol); - `, - }, - { - Code: ` + `}, + {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, + 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'); + throw new Error('lol'); } - declare const nullableString: string | null; assert(3 as any, nullableString); - `, - }, - // Intentional use of `any` to test a function call with no call signatures. - { - Code: ` + `}, + {Code: ` declare const assert: any; declare const nullableString: string | null; assert(nullableString); - `, - }, - // Coverage for absent "test expression". - // Ensure that no crash or false positive occurs - { - Code: ` - for (let x = 0; ; x++) { - break; - } - `, - }, - - // ============================================ - // DOUBLE NEGATION - Always Valid - // ============================================ - - {Code: `!!true;`}, - {Code: `!!false;`}, - {Code: `!!'';`}, - {Code: `!!0;`}, - - // ============================================ - // BOOLEAN CONSTRUCTOR - Always Valid - // ============================================ - - {Code: `Boolean(true);`}, - {Code: `Boolean(false);`}, - {Code: `Boolean('');`}, - {Code: `Boolean(0);`}, - {Code: `Boolean({});`}, - {Code: `Boolean([]);`}, - {Code: `declare const x: any; Boolean(x);`}, - {Code: `declare const x: unknown; Boolean(x);`}, - - // ============================================ - // COMPLEX LOGICAL EXPRESSIONS - // ============================================ - - {Code: `true && true && true;`}, - {Code: `true || false || true;`}, - {Code: `(true && false) || (false && true);`}, - {Code: `true ? (false || true) : (true && false);`}, - - // Mixed types with default options - {Code: `'' || 0 || false;`}, - {Code: `'foo' && 1 && true;`}, - - // ============================================ + `}, + + // ======================================== + // ARRAY PREDICATE FUNCTIONS + // ======================================== + {Code: `['one', 'two', ''].some(x => x);`}, + {Code: `['one', 'two', ''].find(x => x);`}, + {Code: `['one', 'two', ''].every(x => x);`}, + {Code: `['one', 'two', ''].filter((x): boolean => x);`}, + {Code: `['one', 'two', ''].filter(x => Boolean(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);`}, + + // ======================================== // SPECIAL CASES - // ============================================ - - // Always allow boolean in right side of logical operators - {Code: `'foo' && true;`}, - {Code: `0 || false;`}, - - // Template literals - {Code: "declare const x: string; `foo${x}` ? 'a' : 'b';"}, - {Code: "declare const x: number; `foo${x}` ? 'a' : 'b';"}, - - // Parenthesized expressions - {Code: `(true) ? 'a' : 'b';`}, - {Code: `((true)) ? 'a' : 'b';`}, - {Code: `if ((true)) {}`}, - - // Comma operator - {Code: `(0, true) ? 'a' : 'b';`}, - {Code: `('', false) ? 'a' : 'b';`}, - - // Assignment expressions - {Code: `let x; (x = true) ? 'a' : 'b';`}, - {Code: `let x; if (x = false) {}`}, - - // Never type - allowed per TypeScript ESLint - {Code: `declare const x: never; if (x) {}`}, - {Code: `declare const x: never; x ? 'a' : 'b';`}, - {Code: `declare const x: never; !x;`}, + // ======================================== + {Code: `for (let x = 0; ; x++) { break; }`}, }, []rule_tester.InvalidTestCase{ - // ============================================ - // ANY TYPE - Invalid without Option - // ============================================ - + // ======================================== + // NON-BOOLEAN IN RHS OF TEST EXPRESSION + // ======================================== { - Code: `declare const x: any; if (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + Code: `if (true && 1 + 1) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, - }, - { - Code: `declare const x: any; x ? 'a' : 'b';`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + { + MessageId: "unexpectedNumber", + Line: 1, + Column: 13, + }, }, }, { - Code: `declare const x: any; while (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + Code: `while (false || 'a' + 'b') {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, - }, - { - Code: `declare const x: any; do {} while (x);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + { + MessageId: "unexpectedString", + Line: 1, + Column: 17, + }, }, }, { - Code: `declare const x: any; for (; x; ) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + Code: `(x: object) => (true || false || x ? true : false);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, - }, - { - Code: `declare const x: any; x && 'a';`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + { + MessageId: "unexpectedObjectContext", + Line: 1, + Column: 34, + }, }, }, + + // ======================================== + // CHECK OUTERMOST OPERANDS + // ======================================== { - Code: `declare const x: any; x || 'a';`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + Code: `if (('' && {}) || (0 && void 0)) { }`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, - }, - { - Code: `declare const x: any; !x;`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + {MessageId: "unexpectedString", Line: 1, Column: 6}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 12}, + {MessageId: "unexpectedNumber", Line: 1, Column: 20}, + {MessageId: "unexpectedNullish", Line: 1, Column: 25}, }, }, - // ============================================ - // NULLISH VALUES - Always Invalid - // ============================================ - + // ======================================== + // ARRAY PREDICATE WITH NON-BOOLEAN + // ======================================== { - Code: `if (null) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + Code: `declare const array: string[]; array.some(x => x);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + AllowString: utils.Ref(false), }, - }, - { - Code: `if (undefined) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, - }, - }, - { - Code: `null ? 'a' : 'b';`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, }, }, + + // ======================================== + // BRANDED TYPES + // ======================================== { - Code: `undefined ? 'a' : 'b';`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + 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), }, - }, - { - Code: `while (null) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedString", Line: 3, Column: 6}, + {MessageId: "unexpectedNumber", Line: 3, Column: 21}, + {MessageId: "unexpectedNullish", Line: 3, Column: 26}, }, }, { - Code: `do {} while (undefined);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + 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), }, - }, - { - Code: `for (; null; ) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedString", Line: 3, Column: 6}, + {MessageId: "unexpectedObjectContext", Line: 3, Column: 12}, + {MessageId: "unexpectedNullish", Line: 3, Column: 27}, }, }, + + // ======================================== + // LOGICAL OPERANDS FOR CONTROL FLOW + // ======================================== { - Code: `!null;`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + Code: `'asd' && 123 && [] && null;`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, - }, - { - Code: `!undefined;`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedString", Line: 1, Column: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 10}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 17}, }, }, { - Code: `null && 'a';`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + Code: `'asd' || 123 || [] || null;`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, - }, - { - Code: `undefined || 'a';`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedString", Line: 1, Column: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 10}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 17}, }, }, - - // ============================================ - // STRING TYPE - Invalid with allowString: false - // ============================================ - { - Code: `if ('') {}`, + Code: `let x = (1 && 'a' && null) || 0 || '' || {};`, Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 10}, + {MessageId: "unexpectedString", Line: 1, Column: 15}, + {MessageId: "unexpectedNullish", Line: 1, Column: 22}, + {MessageId: "unexpectedNumber", Line: 1, Column: 31}, + {MessageId: "unexpectedString", Line: 1, Column: 36}, }, }, { - Code: `if ('foo') {}`, + Code: `return (1 || 'a' || null) && 0 && '' && {};`, Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 9}, + {MessageId: "unexpectedString", Line: 1, Column: 14}, + {MessageId: "unexpectedNullish", Line: 1, Column: 21}, + {MessageId: "unexpectedNumber", Line: 1, Column: 30}, + {MessageId: "unexpectedString", Line: 1, Column: 35}, }, }, { - Code: `'' ? 'a' : 'b';`, + Code: `console.log((1 && []) || ('a' && {}));`, Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 14}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 19}, + {MessageId: "unexpectedString", Line: 1, Column: 27}, }, }, + + // ======================================== + // CONDITIONALS WITH ALL OPERANDS CHECKED + // ======================================== { - Code: `'foo' ? 'a' : 'b';`, + Code: `if ((1 && []) || ('a' && {})) void 0;`, Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 6}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 11}, + {MessageId: "unexpectedString", Line: 1, Column: 19}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 26}, }, }, { - Code: `while ('') {}`, + Code: `let x = null || 0 || 'a' || [] ? {} : undefined;`, Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1, Column: 9}, + {MessageId: "unexpectedNumber", Line: 1, Column: 17}, + {MessageId: "unexpectedString", Line: 1, Column: 22}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 29}, }, }, { - Code: `do {} while ('foo');`, + Code: `return !(null || 0 || 'a' || []);`, Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1, Column: 10}, + {MessageId: "unexpectedNumber", Line: 1, Column: 18}, + {MessageId: "unexpectedString", Line: 1, Column: 23}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 30}, }, }, + + // ======================================== + // NULLISH VALUES IN BOOLEAN CONTEXT + // ======================================== { - Code: `for (; 'foo'; ) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: `null || {};`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1, Column: 1}, }, }, { - Code: `!'foo';`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: `undefined && [];`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1, Column: 1}, }, }, { - Code: `'foo' && 'bar';`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: `declare const x: null; if (x) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1}, }, }, { - Code: `'' || 'default';`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), + Code: `(x: undefined) => !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1, Column: 20}, }, + }, + { + Code: `(x: T) => (x ? 1 : 0);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1, Column: 40}, }, }, { - Code: `declare const s: string; if (s) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), + Code: `(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1, Column: 28}, }, + }, + { + Code: `(x: T) => (x ? 1 : 0);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1, Column: 33}, }, }, - // Template literals + // ======================================== + // OBJECT IN BOOLEAN CONTEXT + // ======================================== { - Code: "`` ? 'a' : 'b';", - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: `[] || 1;`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 1}, }, }, { - Code: "`foo` ? 'a' : 'b';", - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: `({}) && 'a';`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 2}, }, }, { - Code: "declare const x: string; `foo${x}` ? 'a' : 'b';", - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: `declare const x: symbol; if (x) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1}, }, }, - - // ============================================ - // NUMBER TYPE - Invalid with allowNumber: false - // ============================================ - { - Code: `if (0) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, + Code: `(x: () => void) => !x;`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 21}, }, }, { - Code: `if (1) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, + Code: `(x: T) => (x ? 1 : 0);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 30}, }, }, { - Code: `0 ? 'a' : 'b';`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, + Code: `(x: T) => (x ? 1 : 0);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 41}, }, }, { - Code: `1 ? 'a' : 'b';`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + Code: `(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1, Column: 37}, }, + }, + { + Code: ` void>(x: T) => (x ? 1 : 0);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 34}, }, }, + + // ======================================== + // STRING IN BOOLEAN CONTEXT WITH ALLOWSTRING: FALSE + // ======================================== { - Code: `NaN ? 'a' : 'b';`, + Code: `while ('') {}`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1, Column: 8}, }, }, { - Code: `while (0) {}`, + Code: `for (; 'foo'; ) {}`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1, Column: 8}, }, }, { - Code: `do {} while (1);`, + Code: `declare const x: string; if (x) {}`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, }, }, { - Code: `for (; 1; ) {}`, + Code: `(x: string) => !x;`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1, Column: 17}, }, }, { - Code: `!0;`, + Code: `(x: T) => (x ? 1 : 0);`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1, Column: 30}, }, }, + + // ======================================== + // NUMBER IN BOOLEAN CONTEXT WITH ALLOWNUMBER: FALSE + // ======================================== { - Code: `1 && 2;`, + Code: `while (0n) {}`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 8}, }, }, { - Code: `0 || 1;`, + Code: `for (; 123; ) {}`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 8}, }, }, { - Code: `declare const n: number; if (n) {}`, + Code: `declare const x: number; if (x) {}`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, @@ -902,37 +748,35 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { {MessageId: "unexpectedNumber", Line: 1}, }, }, - - // BigInt { - Code: `if (0n) {}`, + Code: `(x: bigint) => !x;`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 17}, }, }, { - Code: `if (1n) {}`, + Code: `(x: T) => (x ? 1 : 0);`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 30}, }, }, { - Code: `0n ? 'a' : 'b';`, + Code: `![]['length'];`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 2}, }, }, { - Code: `declare const b: bigint; if (b) {}`, + Code: `declare const a: any[] & { notLength: number }; if (a.notLength) {}`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, @@ -941,173 +785,136 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, }, - // ============================================ - // OBJECT TYPE - Always Invalid (Always Truthy) - // ============================================ - + // ======================================== + // ARRAY.LENGTH IN BOOLEAN CONTEXT + // ======================================== { - Code: `if ({}) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, - { - Code: `if ([]) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + Code: `if (![].length) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), }, - }, - { - Code: `({}) ? 'a' : 'b';`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 6}, }, }, { - Code: `[] ? 'a' : 'b';`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + Code: `(a: number[]) => a.length && '...';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), }, - }, - { - Code: `while ({}) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 18}, }, }, { - Code: `do {} while ([]);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + Code: `(...a: T) => a.length || 'empty';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), }, - }, - { - Code: `for (; {}; ) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 35}, }, }, + + // ======================================== + // MIXED STRING | NUMBER VALUE IN BOOLEAN CONTEXT + // ======================================== { - Code: `!{};`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + Code: `declare const x: string | number; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), + AllowString: utils.Ref(true), }, - }, - { - Code: `![];`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedMixedCondition", Line: 1}, }, }, { - Code: `({}) && 'a';`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + Code: `(x: bigint | string) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), + AllowString: utils.Ref(true), }, - }, - { - Code: `[] || 'a';`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedMixedCondition", Line: 1, Column: 26}, }, }, { - Code: `declare const o: object; if (o) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), + AllowString: utils.Ref(true), }, - }, - { - Code: `declare const o: {}; if (o) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedMixedCondition", Line: 1, Column: 48}, }, }, - // Functions + // ======================================== + // NULLABLE BOOLEAN WITHOUT OPTION + // ======================================== { - Code: `function foo() {}; if (foo) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + Code: `declare const x: boolean | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), }, - }, - { - Code: `const foo = () => {}; if (foo) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNullableBoolean", Line: 1}, }, }, - - // ============================================ - // ASSERT FUNCTIONS AND TYPE PREDICATES - // ============================================ - { - 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}, + Code: `(x?: boolean) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), }, - }, - - // Symbols - { - Code: `if (Symbol()) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNullableBoolean", Line: 1, Column: 19}, }, }, { - Code: `declare const s: symbol; if (s) {}`, + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNullableBoolean", Line: 1, Column: 50}, }, }, - // ============================================ - // NULLABLE BOOLEAN - Invalid without Option - // ============================================ - + // ======================================== + // NULLABLE OBJECT WITHOUT OPTION + // ======================================== { - Code: `declare const x: boolean | null; if (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1}, + Code: `declare const x: object | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), }, - }, - { - Code: `declare const x: boolean | undefined; if (x) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1}, + {MessageId: "unexpectedNullableObject", Line: 1}, }, }, { - Code: `declare const x: boolean | null | undefined; if (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1}, + Code: `(x?: { a: number }) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), }, - }, - { - Code: `declare const x: true | null; x ? 'a' : 'b';`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1}, + {MessageId: "unexpectedNullableObject", Line: 1, Column: 25}, }, }, { - Code: `declare const x: false | undefined; !x;`, + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1}, + {MessageId: "unexpectedNullableObject", Line: 1, Column: 45}, }, }, - // ============================================ - // NULLABLE STRING - Invalid without Option - // ============================================ - + // ======================================== + // NULLABLE STRING WITHOUT OPTION + // ======================================== { Code: `declare const x: string | null; if (x) {}`, Errors: []rule_tester.InvalidTestCaseError{ @@ -1115,34 +922,27 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, }, { - Code: `declare const x: string | undefined; if (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, - }, - }, - { - Code: `declare const x: string | null | undefined; if (x) {}`, + Code: `(x?: string) => !x;`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, + {MessageId: "unexpectedNullableString", Line: 1, Column: 18}, }, }, { - Code: `declare const x: '' | null; x ? 'a' : 'b';`, + Code: `(x: T) => (x ? 1 : 0);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, + {MessageId: "unexpectedNullableString", Line: 1, Column: 49}, }, }, { - Code: `declare const x: 'foo' | undefined; !x;`, + Code: `function foo(x: '' | 'bar' | null) { if (!x) {} }`, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 1}, }, }, - // ============================================ - // NULLABLE NUMBER - Invalid without Option - // ============================================ - + // ======================================== + // NULLABLE NUMBER WITHOUT OPTION + // ======================================== { Code: `declare const x: number | null; if (x) {}`, Errors: []rule_tester.InvalidTestCaseError{ @@ -1150,220 +950,254 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, }, { - Code: `declare const x: number | undefined; if (x) {}`, + Code: `(x?: number) => !x;`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, + {MessageId: "unexpectedNullableNumber", Line: 1, Column: 18}, }, }, { - Code: `declare const x: number | null | undefined; if (x) {}`, + Code: `(x: T) => (x ? 1 : 0);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, + {MessageId: "unexpectedNullableNumber", Line: 1, Column: 49}, }, }, { - Code: `declare const x: 0 | null; x ? 'a' : 'b';`, + Code: `function foo(x: 0 | 1 | null) { if (!x) {} }`, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableNumber", Line: 1}, }, }, + + // ======================================== + // NULLABLE ENUM WITHOUT OPTION + // ======================================== { - Code: `declare const x: 1 | undefined; !x;`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, + Code: ` + enum ExampleEnum { This = 0, That = 1 } + const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; + if (theEnum) {} + `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), }, - }, - { - Code: `declare const x: bigint | null; if (x) {}`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, + {MessageId: "unexpectedNullableNumber", Line: 4}, }, }, - - // ============================================ - // NULLABLE OBJECT - Invalid without Option - // ============================================ - { - Code: `declare const x: object | null; if (x) {}`, + 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: "unexpectedNullableObject", Line: 1}, + {MessageId: "unexpectedNullableNumber", Line: 4}, }, }, { - Code: `declare const x: object | undefined; if (x) {}`, + 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: "unexpectedNullableObject", Line: 1}, + {MessageId: "unexpectedNullableNumber", Line: 4}, }, }, { - Code: `declare const x: object | null | undefined; if (x) {}`, + 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: "unexpectedNullableObject", Line: 1}, + {MessageId: "unexpectedNullableString", Line: 4}, }, }, { - Code: `declare const x: {} | null; x ? 'a' : 'b';`, + 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: "unexpectedNullableObject", Line: 1}, + {MessageId: "unexpectedMixedCondition", Line: 4}, }, }, { - Code: `declare const x: [] | undefined; !x;`, + 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: "unexpectedNullableObject", Line: 1}, + {MessageId: "unexpectedNullableString", Line: 4}, }, }, { - Code: `declare const x: symbol | null; if (x) {}`, + 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: "unexpectedNullableObject", Line: 1}, + {MessageId: "unexpectedNullableNumber", Line: 4}, }, }, - // ============================================ - // MIXED TYPES - Invalid - // ============================================ - + // ======================================== + // NULLABLE MIXED ENUM WITHOUT OPTION + // ======================================== { - Code: `declare const x: string | number; if (x) {}`, + Code: ` + enum ExampleEnum { This = 0, That = 'one' } + (value?: ExampleEnum) => (value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - AllowNumber: utils.Ref(false), + AllowNullableEnum: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 1}, + {MessageId: "unexpectedMixedCondition", Line: 3}, }, }, { - Code: `declare const x: string | boolean; if (x) {}`, + Code: ` + enum ExampleEnum { This = '', That = 1 } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), + AllowNullableEnum: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 1}, + {MessageId: "unexpectedMixedCondition", Line: 3}, }, }, { - Code: `declare const x: number | boolean; if (x) {}`, + Code: ` + enum ExampleEnum { This = 'this', That = 1 } + (value?: ExampleEnum) => (value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowNullableEnum: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 1}, + {MessageId: "unexpectedMixedCondition", Line: 3}, }, }, + + // ======================================== + // ANY WITHOUT OPTION + // ======================================== { - Code: `declare const x: string | number | boolean; if (x) {}`, + Code: `if ((Boolean(x) || {}) || (typeof x === 'string' && x)) {}`, Options: StrictBooleanExpressionsOptions{ AllowString: utils.Ref(false), - AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1, Column: 20}, + {MessageId: "unexpectedString", Line: 1, Column: 53}, }, }, - - // ============================================ - // ENUM TYPES - // ============================================ - { - Code: `enum E { A = 0, B = 1 } declare const x: E; if (x) {}`, + Code: `if (1) {}`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1, Column: 5}, }, }, + + // ======================================== + // ASSERT FUNCTIONS + // ======================================== { - Code: `enum E { A = '', B = 'foo' } declare const x: E; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + 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: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullableString", Line: 6}, }, }, - // ============================================ - // ARRAY METHOD PREDICATES - Invalid - // ============================================ - + // ======================================== + // ARRAY FILTER PREDICATES + // ======================================== { - Code: `[1, 2, 3].filter(x => x);`, + Code: `declare const nullOrBool: boolean | null; [true, false, null].filter(x => nullOrBool);`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowNullableBoolean: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNullableBoolean", Line: 1}, }, }, { - Code: `['', 'foo'].filter(x => x);`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: `declare const nullOrString: string | null; ['', 'foo', null].filter(x => nullOrString);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNullableString", Line: 1}, }, }, { - Code: `[{}, []].filter(x => x);`, + Code: `declare const nullOrNumber: number | null; [0, null].filter(x => nullOrNumber);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNullableNumber", Line: 1}, }, }, - - // ============================================ - // COMPLEX LOGICAL EXPRESSIONS - Invalid - // ============================================ - { - Code: `'foo' && 1;`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: `const objectValue: object = {}; [{ a: 0 }, {}].filter(x => objectValue);`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1}, }, }, { - Code: `0 || 'bar';`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, + Code: `const objectValue: object = {}; [{ a: 0 }, {}].filter(x => { return objectValue; });`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1}, }, }, { - Code: `({}) && [];`, + Code: `declare const nullOrObject: object | null; [{ a: 0 }, null].filter(x => nullOrObject);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNullableObject", Line: 1}, }, }, { - Code: `'' || 0;`, + Code: `const numbers: number[] = [1]; [1, 2].filter(x => numbers.length);`, Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, {MessageId: "unexpectedNumber", Line: 1}, }, }, - - // ============================================ - // SPECIAL CASES - Invalid - // ============================================ - - // Array.length { - Code: `declare const arr: string[]; if (arr.length) {}`, + Code: `const numberValue: number = 1; [1, 2].filter(x => numberValue);`, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, @@ -1371,10 +1205,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { {MessageId: "unexpectedNumber", Line: 1}, }, }, - - // Function calls returning non-boolean { - Code: `declare function getString(): string; if (getString()) {}`, + Code: `const stringValue: string = 'hoge'; ['hoge', 'foo'].filter(x => stringValue);`, Options: StrictBooleanExpressionsOptions{ AllowString: utils.Ref(false), }, @@ -1382,54 +1214,54 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { {MessageId: "unexpectedString", Line: 1}, }, }, + + // ======================================== + // UNKNOWN TYPE WITHOUT OPTION + // ======================================== { - Code: `declare function getNumber(): number; if (getNumber()) {}`, + Code: `declare const x: unknown; if (x) {}`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowAny: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedAny", Line: 1}, }, }, { - Code: `declare function getObject(): object; if (getObject()) {}`, + Code: `declare const x: unknown; x ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(false), + }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedAny", Line: 1}, }, }, - - // Property access { - Code: `declare const obj: { prop: string }; if (obj.prop) {}`, + Code: `declare const x: unknown; x && 'a';`, Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), + AllowAny: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedAny", Line: 1}, }, }, { - Code: `declare const obj: { prop: number }; if (obj.prop) {}`, + Code: `declare const x: unknown; x || 'a';`, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowAny: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedAny", Line: 1}, }, }, - - // Void type { - Code: `declare const x: void; if (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + Code: `declare const x: unknown; !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(false), }, - }, - { - Code: `void 0 ? 'a' : 'b';`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedAny", Line: 1}, }, }, }) -} +} \ No newline at end of file From 294f75049b90277ef2ee4125c078e56c6f610dbb Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 12:18:12 +0900 Subject: [PATCH 07/23] wip --- .../strict_boolean_expressions.go | 25 ++++++++++++------- .../strict_boolean_expressions_single_test.go | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 1d8d8676..3f66225f 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -97,6 +97,8 @@ func buildNoStrictNullCheck() rule.RuleMessage { } } +var traversedNodes = utils.Set[*ast.Node]{} + var StrictBooleanExpressionsRule = rule.Rule{ Name: "strict-boolean-expressions", Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { @@ -121,7 +123,7 @@ var StrictBooleanExpressionsRule = rule.Rule{ opts.AllowNullableEnum = utils.Ref(false) } if opts.AllowNullableObject == nil { - opts.AllowNullableObject = utils.Ref(false) + opts.AllowNullableObject = utils.Ref(true) } if opts.AllowString == nil { opts.AllowString = utils.Ref(true) @@ -167,7 +169,10 @@ var StrictBooleanExpressionsRule = rule.Rule{ traverseNode(ctx, condExpr.Condition, opts, true) }, ast.KindBinaryExpression: func(node *ast.Node) { - traverseNode(ctx, node, opts, false) + binExpr := node.AsBinaryExpression() + if binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { + traverseLogicalExpression(ctx, binExpr, opts, false) + } }, ast.KindCallExpression: func(node *ast.Node) { callExpr := node.AsCallExpression() @@ -262,10 +267,16 @@ func traverseLogicalExpression(ctx rule.RuleContext, binExpr *ast.BinaryExpressi } func traverseNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpressionsOptions, isCondition bool) { + if traversedNodes.Has(node) { + return + } + traversedNodes.Add(node) + if node.Kind == ast.KindBinaryExpression { binExpr := node.AsBinaryExpression() if binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { - traverseLogicalExpression(ctx, binExpr, opts, false) + traverseLogicalExpression(ctx, binExpr, opts, isCondition) + return } } @@ -364,7 +375,7 @@ func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { return analyzeTypePart(typeChecker, t) } -func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { +func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { info := typeInfo{} flags := checker.Type_flags(t) @@ -394,7 +405,7 @@ func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { } if flags&(checker.TypeFlagsBoolean|checker.TypeFlagsBooleanLiteral|checker.TypeFlagsBooleanLike) != 0 { - if flags&checker.TypeFlagsBooleanLike != 0 && t.AsIntrinsicType().IntrinsicName() == "true" { + if utils.IsTrueLiteralType(t) { info.isTruthy = true } info.variant = typeVariantBoolean @@ -450,10 +461,6 @@ func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { } func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts StrictBooleanExpressionsOptions) { - if isBooleanType(t) { - return - } - info := analyzeType(ctx.TypeChecker, t) switch info.variant { diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 6180fe5b..69822971 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -11,6 +11,6 @@ import ( func TestStrictBooleanExpressionsSingleRule(t *testing.T) { rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ - {Code: `declare const x: null | object; if (x) {}`}, + {Code: `declare const test?: boolean; if (test ?? false) {}`}, }, []rule_tester.InvalidTestCase{}) } \ No newline at end of file From 1f888d9a3c94f3f4e926603ee76389e8227a19ff Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 12:23:18 +0900 Subject: [PATCH 08/23] pass valid cases --- .../strict_boolean_expressions_single_test.go | 20 +++++++++++++++++-- .../strict_boolean_expressions_test.go | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 69822971..1af4738e 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -7,10 +7,26 @@ import ( "github.com/typescript-eslint/tsgolint/internal/rule_tester" "github.com/typescript-eslint/tsgolint/internal/rules/fixtures" + "github.com/typescript-eslint/tsgolint/internal/utils" ) func TestStrictBooleanExpressionsSingleRule(t *testing.T) { rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ - {Code: `declare const test?: boolean; if (test ?? false) {}`}, - }, []rule_tester.InvalidTestCase{}) + }, []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: 1, + Column: 13, + }, + }, + }, + }) } \ No newline at end of file diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index 82007171..479b713a 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -83,7 +83,7 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, }, { - Code: `declare const test?: boolean; if (test ?? false) {}`, + Code: `const a: (undefined | boolean | null)[] = [true, undefined, null]; a.some(x => x);`, Options: StrictBooleanExpressionsOptions{ AllowNullableBoolean: utils.Ref(true), }, From 12e578215e1c13a45321fd6667778276ee94ee44 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 12:52:31 +0900 Subject: [PATCH 09/23] pass valid cases --- .../strict_boolean_expressions.go | 64 +- .../strict_boolean_expressions_single_test.go | 11 +- .../strict_boolean_expressions_test.go | 2119 +++++++++-------- 3 files changed, 1108 insertions(+), 1086 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 3f66225f..7b42c383 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -97,6 +97,13 @@ func buildNoStrictNullCheck() rule.RuleMessage { } } +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{ @@ -170,7 +177,7 @@ var StrictBooleanExpressionsRule = rule.Rule{ }, ast.KindBinaryExpression: func(node *ast.Node) { binExpr := node.AsBinaryExpression() - if binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { + if ast.IsLogicalExpression(node) && binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { traverseLogicalExpression(ctx, binExpr, opts, false) } }, @@ -182,33 +189,28 @@ var StrictBooleanExpressionsRule = rule.Rule{ traverseNode(ctx, assertedArgument, opts, true) } - if callExpr.Expression != nil && callExpr.Expression.Kind == ast.KindPropertyAccessExpression { - propAccess := callExpr.Expression.AsPropertyAccessExpression() - if propAccess == nil { - return - } - methodName := propAccess.Name().Text() - - switch methodName { - case "filter", "find", "some", "every", "findIndex", "findLast", "findLastIndex": - if callExpr.Arguments != nil && len(callExpr.Arguments.Nodes) > 0 { - arg := callExpr.Arguments.Nodes[0] - if arg != nil && (arg.Kind == ast.KindArrowFunction || arg.Kind == ast.KindFunctionExpression) { - funcType := ctx.TypeChecker.GetTypeAtLocation(arg) - signatures := ctx.TypeChecker.GetCallSignatures(funcType) - 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 - } + if utils.IsArrayMethodCallWithPredicate(ctx.TypeChecker, callExpr) { + if callExpr.Arguments != nil && len(callExpr.Arguments.Nodes) > 0 { + arg := callExpr.Arguments.Nodes[0] + if arg != nil && (arg.Kind == ast.KindArrowFunction || arg.Kind == ast.KindFunctionExpression || arg.Kind == ast.KindFunctionDeclaration) { + if checker.GetFunctionFlags(arg)&checker.FunctionFlagsAsync != 0 { + ctx.ReportNode(arg, buildPredicateCannotBeAsync()) + return + } + funcType := ctx.TypeChecker.GetTypeAtLocation(arg) + signatures := ctx.TypeChecker.GetCallSignatures(funcType) + 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 } + } - if returnType != nil && !isBooleanType(returnType) { - checkCondition(ctx, node, returnType, opts) - } + if returnType != nil && !isBooleanType(returnType) { + checkCondition(ctx, node, returnType, opts) } } } @@ -229,7 +231,7 @@ func findTruthinessAssertedArgument(typeChecker *checker.Checker, callExpr *ast. var checkableArguments []*ast.Node for _, argument := range callExpr.Arguments.Nodes { if argument.Kind == ast.KindSpreadElement { - continue + break } checkableArguments = append(checkableArguments, argument) } @@ -245,12 +247,12 @@ func findTruthinessAssertedArgument(typeChecker *checker.Checker, callExpr *ast. if firstTypePredicateResult == nil { return nil } - if checker.TypePredicate_kind(firstTypePredicateResult) != checker.TypePredicateKindAssertsIdentifier || - checker.TypePredicate_t(firstTypePredicateResult) != nil { + if !(checker.TypePredicate_kind(firstTypePredicateResult) == checker.TypePredicateKindAssertsIdentifier && + checker.TypePredicate_t(firstTypePredicateResult) == nil) { return nil } parameterIndex := checker.TypePredicate_parameterIndex(firstTypePredicateResult) - if parameterIndex >= int32(len(checkableArguments)) { + if int(parameterIndex) >= len(checkableArguments) { return nil } return checkableArguments[parameterIndex] @@ -274,7 +276,7 @@ func traverseNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpres if node.Kind == ast.KindBinaryExpression { binExpr := node.AsBinaryExpression() - if binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { + if ast.IsLogicalExpression(node) && binExpr.OperatorToken.Kind != ast.KindQuestionQuestionToken { traverseLogicalExpression(ctx, binExpr, opts, isCondition) return } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 1af4738e..998d3e9b 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -14,18 +14,17 @@ func TestStrictBooleanExpressionsSingleRule(t *testing.T) { rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ }, []rule_tester.InvalidTestCase{ { - Code: `if (true && 1 + 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: "unexpectedNumber", - Line: 1, - Column: 13, - }, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1}, }, }, }) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index 479b713a..dd471e91 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -1,5 +1,3 @@ -// Code generated from strict-boolean-expressions.test.ts - DO NOT EDIT. - package strict_boolean_expressions import ( @@ -11,1257 +9,1280 @@ import ( ) func TestStrictBooleanExpressionsRule(t *testing.T) { - rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ - // ======================================== - // BOOLEAN IN BOOLEAN CONTEXT - // ======================================== - {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) {}`}, + rule_tester.RunRuleTester( + fixtures.GetRootDir(), + "tsconfig.json", + t, + &StrictBooleanExpressionsRule, + []rule_tester.ValidTestCase{ + // ======================================== + // BOOLEAN IN BOOLEAN CONTEXT + // ======================================== + {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) {}`}, - // ======================================== - // STRING IN BOOLEAN CONTEXT - // ======================================== - {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);`}, + // ======================================== + // STRING IN BOOLEAN CONTEXT + // ======================================== + {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);`}, - // ======================================== - // NUMBER IN BOOLEAN CONTEXT - // ======================================== - {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);`}, + // ======================================== + // NUMBER IN BOOLEAN CONTEXT + // ======================================== + {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);`}, - // ======================================== - // NULLABLE OBJECT IN BOOLEAN CONTEXT - // ======================================== - {Code: `declare const x: null | object; if (x) {}`}, - {Code: `(x?: { a: any }) => !x;`}, - {Code: `(x: T) => (x ? 1 : 0);`}, + // ======================================== + // NULLABLE OBJECT IN BOOLEAN CONTEXT + // ======================================== + {Code: `declare const x: null | object; if (x) {}`}, + {Code: `(x?: { a: any }) => !x;`}, + {Code: `(x: T) => (x ? 1 : 0);`}, - // ======================================== - // NULLABLE BOOLEAN IN BOOLEAN CONTEXT - // ======================================== - { - Code: `declare const x: boolean | null; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableBoolean: utils.Ref(true), + // ======================================== + // NULLABLE BOOLEAN IN BOOLEAN CONTEXT + // ======================================== + { + 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?: boolean) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - 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: `const a: (undefined | boolean | null)[] = [true, undefined, null]; a.some(x => x);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), + }, }, - }, - // ======================================== - // NULLABLE STRING IN BOOLEAN CONTEXT - // ======================================== - { - Code: `declare const x: string | undefined; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableString: utils.Ref(true), + // ======================================== + // NULLABLE STRING IN BOOLEAN CONTEXT + // ======================================== + { + Code: `declare const x: string | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, }, - }, - { - Code: `(x?: string) => !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: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, }, - }, - { - Code: `'string' != null ? 'asd' : '';`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableString: utils.Ref(true), + { + Code: `'string' != null ? 'asd' : '';`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, }, - }, - // ======================================== - // NULLABLE NUMBER IN BOOLEAN CONTEXT - // ======================================== - { - Code: `declare const x: number | undefined; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableNumber: utils.Ref(true), + // ======================================== + // NULLABLE NUMBER IN BOOLEAN CONTEXT + // ======================================== + { + Code: `declare const x: number | undefined; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, }, - }, - { - Code: `(x?: number) => !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: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, }, - }, - { - Code: `declare const x: bigint | undefined; if (x ?? 0n) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableNumber: utils.Ref(true), + { + Code: `declare const x: bigint | undefined; if (x ?? 0n) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, }, - }, - // ======================================== - // ANY TYPE IN BOOLEAN CONTEXT - // ======================================== - { - Code: `declare const x: any; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), + // ======================================== + // ANY TYPE IN BOOLEAN CONTEXT + // ======================================== + { + Code: `declare const x: any; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, }, - }, - { - Code: `(x?: any) => !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), + { + Code: `(x?: any) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), + { + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, }, - }, - { - Code: `const foo: undefined | any = 0; if (foo) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), + { + Code: `const foo: undefined | any = 0; if (foo) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, }, - }, - // ======================================== - // UNKNOWN TYPE IN BOOLEAN CONTEXT - // ======================================== - { - Code: `declare const x: unknown; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), + // ======================================== + // UNKNOWN TYPE IN BOOLEAN CONTEXT + // ======================================== + { + Code: `declare const x: unknown; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), + { + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(true), + }, }, - }, - // ======================================== - // NULLABLE ENUM IN BOOLEAN CONTEXT - // ======================================== - { - Code: ` + // ======================================== + // NULLABLE ENUM IN BOOLEAN CONTEXT + // ======================================== + { + Code: ` enum E { A, B } declare const x: E | null; if (x) {} `, - Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(true), + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, }, - }, - { - Code: ` + { + Code: ` enum E { A = 'a', B = 'b' } declare const x: E | undefined; if (x) {} `, - Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(true), + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(true), + }, }, - }, - // ======================================== - // ALLOW RULE TO RUN WITHOUT STRICT NULL CHECKS - // ======================================== - { - Code: `if (true) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), + // ======================================== + // ALLOW RULE TO RUN WITHOUT STRICT NULL CHECKS + // ======================================== + { + Code: `if (true) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), + }, }, - }, - // ======================================== - // COMPLEX BOOLEAN EXPRESSIONS - // ======================================== - {Code: `(x?: boolean) => x ?? false ? true : false;`}, - {Code: `(x: T) => x ?? false;`}, - { - Code: `(x?: string) => x ?? false;`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableString: utils.Ref(true), + // ======================================== + // COMPLEX BOOLEAN EXPRESSIONS + // ======================================== + {Code: `(x?: boolean) => x ?? false ? true : false;`}, + {Code: `(x: T) => x ?? false;`}, + { + Code: `(x?: string) => x ?? false;`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableString: utils.Ref(true), + }, }, - }, - { - Code: `(x?: number) => x ?? false;`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableNumber: utils.Ref(true), + { + Code: `(x?: number) => x ?? false;`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableNumber: utils.Ref(true), + }, }, - }, - // ======================================== - // ASSERT FUNCTIONS - // ======================================== - {Code: ` - declare function assert(condition: unknown): asserts condition; - declare const x: string | null; - assert(x); - `}, - {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); - `}, + // ======================================== + // ASSERT FUNCTIONS + // ======================================== + { + 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 {} +} - // ======================================== - // ARRAY PREDICATE FUNCTIONS - // ======================================== - {Code: `['one', 'two', ''].some(x => x);`}, - {Code: `['one', 'two', ''].find(x => x);`}, - {Code: `['one', 'two', ''].every(x => x);`}, - {Code: `['one', 'two', ''].filter((x): boolean => x);`}, - {Code: `['one', 'two', ''].filter(x => Boolean(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);`}, +declare const lol: string | number | unknown | null; - // ======================================== - // SPECIAL CASES - // ======================================== - {Code: `for (let x = 0; ; x++) { break; }`}, - }, []rule_tester.InvalidTestCase{ - // ======================================== - // NON-BOOLEAN IN RHS OF TEST EXPRESSION - // ======================================== - { - 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: 1, - Column: 13, - }, - }, - }, - { - Code: `while (false || 'a' + 'b') {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), +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); + `, + }, + // Intentional use of `any` to test a function call with no call signatures. + { + Code: ` +declare const assert: any; +declare const nullableString: string | null; +assert(nullableString); + `, }, - Errors: []rule_tester.InvalidTestCaseError{ - { - MessageId: "unexpectedString", - Line: 1, - Column: 17, + + // ======================================== + // ARRAY PREDICATE FUNCTIONS + // ======================================== + {Code: `['one', 'two', ''].some(x => x);`}, + {Code: `['one', 'two', ''].find(x => x);`}, + {Code: `['one', 'two', ''].every(x => x);`}, + {Code: `['one', 'two', ''].filter((x): boolean => x);`}, + {Code: `['one', 'two', ''].filter(x => Boolean(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);`}, + + // ======================================== + // SPECIAL CASES + // ======================================== + {Code: `for (let x = 0; ; x++) { break; }`}, + }, + []rule_tester.InvalidTestCase{ + // ======================================== + // NON-BOOLEAN IN RHS OF TEST EXPRESSION + // ======================================== + { + 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: 1, + }, }, }, - }, - { - Code: `(x: object) => (true || false || x ? true : false);`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), + { + 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, + }, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - { - MessageId: "unexpectedObjectContext", - Line: 1, - Column: 34, + { + 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: "unexpectedObjectContext", + Line: 1, + }, }, }, - }, - // ======================================== - // CHECK OUTERMOST OPERANDS - // ======================================== - { - 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, Column: 6}, - {MessageId: "unexpectedObjectContext", Line: 1, Column: 12}, - {MessageId: "unexpectedNumber", Line: 1, Column: 20}, - {MessageId: "unexpectedNullish", Line: 1, Column: 25}, + // ======================================== + // CHECK OUTERMOST OPERANDS + // ======================================== + { + 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: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1}, + }, }, - }, - // ======================================== - // ARRAY PREDICATE WITH NON-BOOLEAN - // ======================================== - { - 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", Line: 1}, + // ======================================== + // ARRAY PREDICATE WITH NON-BOOLEAN + // ======================================== + { + 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", Line: 1}, + }, }, - }, - // ======================================== - // BRANDED TYPES - // ======================================== - { - Code: ` + // ======================================== + // BRANDED TYPES + // ======================================== + { + 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, Column: 6}, - {MessageId: "unexpectedNumber", Line: 3, Column: 21}, - {MessageId: "unexpectedNullish", Line: 3, Column: 26}, + 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}, + }, }, - }, - { - Code: ` + { + 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, Column: 6}, - {MessageId: "unexpectedObjectContext", Line: 3, Column: 12}, - {MessageId: "unexpectedNullish", Line: 3, Column: 27}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 3}, + {MessageId: "unexpectedObjectContext", Line: 3}, + {MessageId: "unexpectedNullish", Line: 3}, + }, }, - }, - // ======================================== - // LOGICAL OPERANDS FOR CONTROL FLOW - // ======================================== - { - Code: `'asd' && 123 && [] && null;`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1, Column: 1}, - {MessageId: "unexpectedNumber", Line: 1, Column: 10}, - {MessageId: "unexpectedObjectContext", Line: 1, Column: 17}, - }, - }, - { - Code: `'asd' || 123 || [] || null;`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1, Column: 1}, - {MessageId: "unexpectedNumber", Line: 1, Column: 10}, - {MessageId: "unexpectedObjectContext", Line: 1, Column: 17}, + // ======================================== + // LOGICAL OPERANDS FOR CONTROL FLOW + // ======================================== + { + 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: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - 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, Column: 10}, - {MessageId: "unexpectedString", Line: 1, Column: 15}, - {MessageId: "unexpectedNullish", Line: 1, Column: 22}, - {MessageId: "unexpectedNumber", Line: 1, Column: 31}, - {MessageId: "unexpectedString", Line: 1, Column: 36}, + { + 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: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `return (1 || 'a' || null) && 0 && '' && {};`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 9}, - {MessageId: "unexpectedString", Line: 1, Column: 14}, - {MessageId: "unexpectedNullish", Line: 1, Column: 21}, - {MessageId: "unexpectedNumber", Line: 1, Column: 30}, - {MessageId: "unexpectedString", Line: 1, Column: 35}, + { + 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}, + }, }, - }, - { - Code: `console.log((1 && []) || ('a' && {}));`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), + { + 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}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 14}, - {MessageId: "unexpectedObjectContext", Line: 1, Column: 19}, - {MessageId: "unexpectedString", Line: 1, Column: 27}, + { + Code: `console.log((1 && []) || ('a' && {}));`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + }, }, - }, - // ======================================== - // CONDITIONALS WITH ALL OPERANDS CHECKED - // ======================================== - { - Code: `if ((1 && []) || ('a' && {})) void 0;`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 6}, - {MessageId: "unexpectedObjectContext", Line: 1, Column: 11}, - {MessageId: "unexpectedString", Line: 1, Column: 19}, - {MessageId: "unexpectedObjectContext", Line: 1, Column: 26}, + // ======================================== + // CONDITIONALS WITH ALL OPERANDS CHECKED + // ======================================== + { + 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: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - 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, Column: 9}, - {MessageId: "unexpectedNumber", Line: 1, Column: 17}, - {MessageId: "unexpectedString", Line: 1, Column: 22}, - {MessageId: "unexpectedObjectContext", Line: 1, Column: 29}, + { + 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: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `return !(null || 0 || 'a' || []);`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1, Column: 10}, - {MessageId: "unexpectedNumber", Line: 1, Column: 18}, - {MessageId: "unexpectedString", Line: 1, Column: 23}, - {MessageId: "unexpectedObjectContext", Line: 1, Column: 30}, + { + 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: "unexpectedObjectContext", Line: 1}, + }, }, - }, - // ======================================== - // NULLISH VALUES IN BOOLEAN CONTEXT - // ======================================== - { - Code: `null || {};`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1, Column: 1}, + // ======================================== + // NULLISH VALUES IN BOOLEAN CONTEXT + // ======================================== + { + Code: `null || {};`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, }, - }, - { - Code: `undefined && [];`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1, Column: 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: 1}, + { + Code: `declare const x: null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, }, - }, - { - Code: `(x: undefined) => !x;`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullish", Line: 1, Column: 20}, + { + 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, Column: 40}, + { + 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, Column: 28}, + { + 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, Column: 33}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullish", Line: 1}, + }, }, - }, - // ======================================== - // OBJECT IN BOOLEAN CONTEXT - // ======================================== - { - Code: `[] || 1;`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1, Column: 1}, + // ======================================== + // OBJECT IN BOOLEAN CONTEXT + // ======================================== + { + Code: `[] || 1;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `({}) && 'a';`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1, Column: 2}, + { + Code: `({}) && 'a';`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `declare const x: symbol; if (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + { + Code: `declare const x: symbol; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `(x: () => void) => !x;`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1, Column: 21}, + { + Code: `(x: () => void) => !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1, Column: 30}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1, Column: 41}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1, Column: 37}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: ` void>(x: T) => (x ? 1 : 0);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1, Column: 34}, + { + Code: ` void>(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - // ======================================== - // STRING IN BOOLEAN CONTEXT WITH ALLOWSTRING: FALSE - // ======================================== - { - Code: `while ('') {}`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1, Column: 8}, - }, - }, - { - Code: `for (; 'foo'; ) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1, Column: 8}, - }, - }, - { - Code: `declare const x: string; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + // ======================================== + // STRING IN BOOLEAN CONTEXT WITH ALLOWSTRING: FALSE + // ======================================== + { + Code: `while ('') {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, }, - }, - { - Code: `(x: string) => !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), + { + Code: `for (; 'foo'; ) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1, Column: 17}, + { + Code: `declare const x: string; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), + { + Code: `(x: string) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1, Column: 30}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, }, - }, - // ======================================== - // NUMBER IN BOOLEAN CONTEXT WITH ALLOWNUMBER: FALSE - // ======================================== - { - Code: `while (0n) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 8}, - }, - }, - { - Code: `for (; 123; ) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 8}, - }, - }, - { - Code: `declare const x: number; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, - { - Code: `(x: bigint) => !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 17}, + // ======================================== + // NUMBER IN BOOLEAN CONTEXT WITH ALLOWNUMBER: FALSE + // ======================================== + { + Code: `while (0n) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + { + Code: `for (; 123; ) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 30}, + { + Code: `declare const x: number; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - }, - { - Code: `![]['length'];`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + { + Code: `(x: bigint) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 2}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - }, - { - Code: `declare const a: any[] & { notLength: number }; if (a.notLength) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + { + Code: `![]['length'];`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + { + Code: `declare const a: any[] & { notLength: number }; if (a.notLength) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - }, - // ======================================== - // ARRAY.LENGTH IN BOOLEAN CONTEXT - // ======================================== - { - Code: `if (![].length) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 6}, - }, - }, - { - Code: `(a: number[]) => a.length && '...';`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 18}, + // ======================================== + // ARRAY.LENGTH IN BOOLEAN CONTEXT + // ======================================== + { + Code: `if (![].length) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - }, - { - Code: `(...a: T) => a.length || 'empty';`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + { + Code: `(a: number[]) => a.length && '...';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 35}, + { + Code: `(...a: T) => a.length || 'empty';`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - }, - // ======================================== - // MIXED STRING | NUMBER VALUE IN BOOLEAN CONTEXT - // ======================================== - { - 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: 1}, - }, - }, - { - Code: `(x: bigint | string) => !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(true), - AllowString: utils.Ref(true), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 1, Column: 26}, + // ======================================== + // MIXED STRING | NUMBER VALUE IN BOOLEAN CONTEXT + // ======================================== + { + 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: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(true), - AllowString: utils.Ref(true), + { + Code: `(x: bigint | string) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), + AllowString: utils.Ref(true), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 1, Column: 48}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(true), + AllowString: utils.Ref(true), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 1}, + }, }, - }, - // ======================================== - // NULLABLE BOOLEAN WITHOUT OPTION - // ======================================== - { - Code: `declare const x: boolean | null; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableBoolean: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1}, - }, - }, - { - Code: `(x?: boolean) => !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableBoolean: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1, Column: 19}, + // ======================================== + // NULLABLE BOOLEAN WITHOUT OPTION + // ======================================== + { + Code: `declare const x: boolean | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableBoolean: utils.Ref(false), + { + Code: `(x?: boolean) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1, Column: 50}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, }, - }, - // ======================================== - // NULLABLE OBJECT WITHOUT OPTION - // ======================================== - { - Code: `declare const x: object | null; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableObject", Line: 1}, - }, - }, - { - Code: `(x?: { a: number }) => !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableObject", Line: 1, Column: 25}, + // ======================================== + // NULLABLE OBJECT WITHOUT OPTION + // ======================================== + { + Code: `declare const x: object | null; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), + { + Code: `(x?: { a: number }) => !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableObject", Line: 1, Column: 45}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableObject", Line: 1}, + }, }, - }, - // ======================================== - // NULLABLE STRING WITHOUT OPTION - // ======================================== - { - Code: `declare const x: string | null; if (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, + // ======================================== + // NULLABLE STRING WITHOUT OPTION + // ======================================== + { + Code: `declare const x: string | null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, }, - }, - { - Code: `(x?: string) => !x;`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1, Column: 18}, + { + Code: `(x?: string) => !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1, Column: 49}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, }, - }, - { - Code: `function foo(x: '' | 'bar' | null) { if (!x) {} }`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, + { + Code: `function foo(x: '' | 'bar' | null) { if (!x) {} }`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, }, - }, - // ======================================== - // NULLABLE NUMBER WITHOUT OPTION - // ======================================== - { - Code: `declare const x: number | null; if (x) {}`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, + // ======================================== + // NULLABLE NUMBER WITHOUT OPTION + // ======================================== + { + Code: `declare const x: number | null; if (x) {}`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, }, - }, - { - Code: `(x?: number) => !x;`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1, Column: 18}, + { + Code: `(x?: number) => !x;`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, }, - }, - { - Code: `(x: T) => (x ? 1 : 0);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1, Column: 49}, + { + Code: `(x: T) => (x ? 1 : 0);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, }, - }, - { - Code: `function foo(x: 0 | 1 | null) { if (!x) {} }`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, + { + Code: `function foo(x: 0 | 1 | null) { if (!x) {} }`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, }, - }, - // ======================================== - // NULLABLE ENUM WITHOUT OPTION - // ======================================== - { - Code: ` + // ======================================== + // NULLABLE ENUM WITHOUT OPTION + // ======================================== + { + 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: "unexpectedNullableNumber", Line: 4}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 4}, + }, }, - }, - { - Code: ` + { + 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: "unexpectedNullableNumber", Line: 4}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 4}, + }, }, - }, - { - Code: ` + { + 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: "unexpectedNullableNumber", Line: 4}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 4}, + }, }, - }, - { - Code: ` + { + 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: "unexpectedNullableString", Line: 4}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 4}, + }, }, - }, - { - Code: ` + { + 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: "unexpectedMixedCondition", Line: 4}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 4}, + }, }, - }, - { - Code: ` + { + 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: "unexpectedNullableString", Line: 4}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 4}, + }, }, - }, - { - Code: ` + { + 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: "unexpectedNullableNumber", Line: 4}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 4}, + }, }, - }, - // ======================================== - // NULLABLE MIXED ENUM WITHOUT OPTION - // ======================================== - { - Code: ` + // ======================================== + // NULLABLE MIXED ENUM WITHOUT OPTION + // ======================================== + { + Code: ` enum ExampleEnum { This = 0, That = 'one' } (value?: ExampleEnum) => (value ? 1 : 0); `, - Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 3}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 3}, + }, }, - }, - { - Code: ` + { + Code: ` enum ExampleEnum { This = '', That = 1 } (value?: ExampleEnum) => (!value ? 1 : 0); `, - Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 3}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 3}, + }, }, - }, - { - Code: ` + { + Code: ` enum ExampleEnum { This = 'this', That = 1 } (value?: ExampleEnum) => (value ? 1 : 0); `, - Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 3}, + Options: StrictBooleanExpressionsOptions{ + AllowNullableEnum: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 3}, + }, }, - }, - // ======================================== - // ANY WITHOUT OPTION - // ======================================== - { - Code: `if ((Boolean(x) || {}) || (typeof x === 'string' && x)) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1, Column: 20}, - {MessageId: "unexpectedString", Line: 1, Column: 53}, - }, - }, - { - Code: `if (1) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + // ======================================== + // ANY WITHOUT OPTION + // ======================================== + { + Code: `if ((Boolean(x) || {}) || (typeof x === 'string' && x)) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1, Column: 5}, + { + Code: `if (1) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - }, - // ======================================== - // ASSERT FUNCTIONS - // ======================================== - { - Code: ` + // ======================================== + // ASSERT FUNCTIONS + // ======================================== + { + 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}, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 6}, + }, }, - }, - // ======================================== - // ARRAY FILTER PREDICATES - // ======================================== - { - Code: `declare const nullOrBool: boolean | null; [true, false, null].filter(x => nullOrBool);`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableBoolean: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1}, - }, - }, - { - Code: `declare const nullOrString: string | null; ['', 'foo', null].filter(x => nullOrString);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, - }, - }, - { - Code: `declare const nullOrNumber: number | null; [0, null].filter(x => nullOrNumber);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, - }, - }, - { - Code: `const objectValue: object = {}; [{ a: 0 }, {}].filter(x => objectValue);`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, - { - Code: `const objectValue: object = {}; [{ a: 0 }, {}].filter(x => { return objectValue; });`, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, + // ======================================== + // ARRAY FILTER PREDICATES + // ======================================== + { + Code: `declare const nullOrBool: boolean | null; [true, false, null].filter(x => nullOrBool);`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableBoolean", Line: 1}, + }, }, - }, - { - Code: `declare const nullOrObject: object | null; [{ a: 0 }, null].filter(x => nullOrObject);`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), + { + Code: `declare const nullOrString: string | null; ['', 'foo', null].filter(x => nullOrString);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableString", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableObject", Line: 1}, + { + Code: `declare const nullOrNumber: number | null; [0, null].filter(x => nullOrNumber);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNullableNumber", Line: 1}, + }, }, - }, - { - Code: `const numbers: number[] = [1]; [1, 2].filter(x => numbers.length);`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + { + Code: `const objectValue: object = {}; [{ a: 0 }, {}].filter(x => objectValue);`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + { + Code: `const objectValue: object = {}; [{ a: 0 }, {}].filter(x => { return objectValue; });`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedObjectContext", Line: 1}, + }, }, - }, - { - Code: `const numberValue: number = 1; [1, 2].filter(x => numberValue);`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + { + 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: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, + { + Code: `const numbers: number[] = [1]; [1, 2].filter(x => numbers.length);`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - }, - { - Code: `const stringValue: string = 'hoge'; ['hoge', 'foo'].filter(x => stringValue);`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), + { + Code: `const numberValue: number = 1; [1, 2].filter(x => numberValue);`, + Options: StrictBooleanExpressionsOptions{ + AllowNumber: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedNumber", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + { + Code: `const stringValue: string = 'hoge'; ['hoge', 'foo'].filter(x => stringValue);`, + Options: StrictBooleanExpressionsOptions{ + AllowString: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 1}, + }, }, - }, - // ======================================== - // UNKNOWN TYPE WITHOUT OPTION - // ======================================== - { - Code: `declare const x: unknown; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, - }, - }, - { - Code: `declare const x: unknown; x ? 'a' : 'b';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, - }, - }, - { - Code: `declare const x: unknown; x && 'a';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + // ======================================== + // UNKNOWN TYPE WITHOUT OPTION + // ======================================== + { + Code: `declare const x: unknown; if (x) {}`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, }, - }, - { - Code: `declare const x: unknown; x || 'a';`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(false), + { + Code: `declare const x: unknown; x ? 'a' : 'b';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + { + Code: `declare const x: unknown; x && 'a';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, }, - }, - { - Code: `declare const x: unknown; !x;`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(false), + { + Code: `declare const x: unknown; x || 'a';`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, + { + Code: `declare const x: unknown; !x;`, + Options: StrictBooleanExpressionsOptions{ + AllowAny: utils.Ref(false), + }, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedAny", Line: 1}, + }, }, - }, - }) -} \ No newline at end of file + }) +} From e9524ba0a5557db691995369310a9a85da5399c9 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 12:52:51 +0900 Subject: [PATCH 10/23] format --- .../strict_boolean_expressions_single_test.go | 5 ++--- internal/rules/unbound_method/unbound_method.go | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 998d3e9b..7595194d 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -11,8 +11,7 @@ import ( ) func TestStrictBooleanExpressionsSingleRule(t *testing.T) { - rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ - }, []rule_tester.InvalidTestCase{ + rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{}, []rule_tester.InvalidTestCase{ { Code: `if (('' && {}) || (0 && void 0)) { }`, Options: StrictBooleanExpressionsOptions{ @@ -28,4 +27,4 @@ func TestStrictBooleanExpressionsSingleRule(t *testing.T) { }, }, }) -} \ No newline at end of file +} 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())) From e3e5fa23cb930f225322ad217c6fd61b6b7d6ad7 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 13:10:10 +0900 Subject: [PATCH 11/23] enum --- .../strict_boolean_expressions.go | 21 +++--- .../strict_boolean_expressions_single_test.go | 13 +--- .../strict_boolean_expressions_test.go | 68 ++++++++++++++++--- 3 files changed, 72 insertions(+), 30 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 7b42c383..16fba6b5 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -335,7 +335,6 @@ func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { if partInfo.isEnum { info.isEnum = true } - info.isTruthy = partInfo.isTruthy } if len(variants) == 1 { @@ -414,6 +413,16 @@ func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { 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) != 0 { info.variant = typeVariantString if flags&checker.TypeFlagsStringLiteral != 0 && t.AsLiteralType().Value() != "" { @@ -430,16 +439,6 @@ func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { return info } - if flags&(checker.TypeFlagsEnum|checker.TypeFlagsEnumLiteral) != 0 { - if flags&checker.TypeFlagsStringLiteral != 0 { - info.variant = typeVariantString - } else { - info.variant = typeVariantNumber - } - info.isEnum = true - return info - } - if flags&(checker.TypeFlagsBigInt|checker.TypeFlagsBigIntLiteral) != 0 { info.variant = typeVariantBigInt if flags&checker.TypeFlagsBigIntLiteral != 0 && t.AsLiteralType().Value() != 0 { diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 7595194d..9ef166f6 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -7,23 +7,14 @@ import ( "github.com/typescript-eslint/tsgolint/internal/rule_tester" "github.com/typescript-eslint/tsgolint/internal/rules/fixtures" - "github.com/typescript-eslint/tsgolint/internal/utils" ) func TestStrictBooleanExpressionsSingleRule(t *testing.T) { rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{}, []rule_tester.InvalidTestCase{ { - Code: `if (('' && {}) || (0 && void 0)) { }`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, + Code: `function foo(x: 0 | 1 | null) { if (!x) {} }`, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, - {MessageId: "unexpectedObjectContext", Line: 1}, - {MessageId: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedNullableNumber", Line: 1}, }, }, }) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index dd471e91..168ae1ac 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -197,20 +197,72 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { // ======================================== { Code: ` - enum E { A, B } - declare const x: E | null; - if (x) {} - `, + 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 E { A = 'a', B = 'b' } - declare const x: E | undefined; - if (x) {} - `, + 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), }, From ab48a2679181d16a823557359ba1cf0d771d6083 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 13:24:39 +0900 Subject: [PATCH 12/23] parentheses --- .../strict_boolean_expressions.go | 5 +++++ .../strict_boolean_expressions_single_test.go | 13 +++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 16fba6b5..a29f9422 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -274,6 +274,11 @@ func traverseNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpres } 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 { diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 9ef166f6..7595194d 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -7,14 +7,23 @@ import ( "github.com/typescript-eslint/tsgolint/internal/rule_tester" "github.com/typescript-eslint/tsgolint/internal/rules/fixtures" + "github.com/typescript-eslint/tsgolint/internal/utils" ) func TestStrictBooleanExpressionsSingleRule(t *testing.T) { rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{}, []rule_tester.InvalidTestCase{ { - Code: `function foo(x: 0 | 1 | null) { if (!x) {} }`, + Code: `if (('' && {}) || (0 && void 0)) { }`, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(false), + AllowNumber: utils.Ref(false), + AllowString: utils.Ref(false), + }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObjectContext", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1}, }, }, }) From ac8cf919f558a627309e0aa58edc6f8ded1dad84 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 15:26:10 +0900 Subject: [PATCH 13/23] gen tests --- .../strict_boolean_expressions.go | 48 +- .../strict_boolean_expressions_single_test.go | 24 +- .../strict_boolean_expressions_test.go | 2143 ++++++++++------- 3 files changed, 1379 insertions(+), 836 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index a29f9422..ebdbf7f0 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -76,9 +76,9 @@ func buildUnexpectedNumber() rule.RuleMessage { } } -func buildUnexpectedObjectContext() rule.RuleMessage { +func buildUnexpectedObject() rule.RuleMessage { return rule.RuleMessage{ - Id: "unexpectedObjectContext", + Id: "unexpectedObject", Description: "Unexpected object value in conditional. The condition is always true.", } } @@ -331,6 +331,8 @@ func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { parts := utils.UnionTypeParts(t) variants := make(map[typeVariant]bool) + metNotTruthy := false + for _, part := range parts { partInfo := analyzeTypePart(typeChecker, part) variants[partInfo.variant] = true @@ -340,6 +342,13 @@ func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { if partInfo.isEnum { info.isEnum = true } + if partInfo.variant == typeVariantNumber || partInfo.variant == typeVariantString { + if metNotTruthy { + continue + } + info.isTruthy = partInfo.isTruthy + metNotTruthy = !partInfo.isTruthy + } } if len(variants) == 1 { @@ -428,26 +437,36 @@ func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { return info } - if flags&(checker.TypeFlagsString|checker.TypeFlagsStringLiteral) != 0 { + if flags&(checker.TypeFlagsString|checker.TypeFlagsStringLiteral|checker.TypeFlagsStringLike) != 0 { info.variant = typeVariantString - if flags&checker.TypeFlagsStringLiteral != 0 && t.AsLiteralType().Value() != "" { - info.isTruthy = true + if t.IsStringLiteral() { + literal := t.AsLiteralType() + if literal != nil && literal.Value() != "" { + println(literal.Value()) + info.isTruthy = true + } } return info } - if flags&(checker.TypeFlagsNumber|checker.TypeFlagsNumberLiteral) != 0 { + if flags&(checker.TypeFlagsNumber|checker.TypeFlagsNumberLiteral|checker.TypeFlagsNumberLike) != 0 { info.variant = typeVariantNumber - if flags&checker.TypeFlagsNumberLiteral != 0 && t.AsLiteralType().Value() != 0 { - info.isTruthy = true + 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 flags&checker.TypeFlagsBigIntLiteral != 0 && t.AsLiteralType().Value() != 0 { - info.isTruthy = true + if t.IsBigIntLiteral() { + literal := t.AsLiteralType() + if literal != nil && literal.String() != "0" { + info.isTruthy = true + } } return info } @@ -499,6 +518,7 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts 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 } @@ -524,9 +544,15 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts if info.isNullable && !*opts.AllowNullableObject { ctx.ReportNode(node, buildUnexpectedNullableObject()) } else if !info.isNullable { - ctx.ReportNode(node, buildUnexpectedObjectContext()) + ctx.ReportNode(node, buildUnexpectedObject()) } case typeVariantMixed: + if info.isEnum { + if info.isNullable && !*opts.AllowNullableEnum { + ctx.ReportNode(node, buildUnexpectedNullableNumber()) + } + return + } ctx.ReportNode(node, buildUnexpectedMixedCondition()) case typeVariantBigInt: if info.isNullable && !*opts.AllowNullableNumber { diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 7595194d..fe67b8e4 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -7,24 +7,22 @@ import ( "github.com/typescript-eslint/tsgolint/internal/rule_tester" "github.com/typescript-eslint/tsgolint/internal/rules/fixtures" - "github.com/typescript-eslint/tsgolint/internal/utils" ) func TestStrictBooleanExpressionsSingleRule(t *testing.T) { - rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{}, []rule_tester.InvalidTestCase{ + rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ + + }, []rule_tester.InvalidTestCase{ { - Code: `if (('' && {}) || (0 && void 0)) { }`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, + Code: ` + function foo(x: 0 | 1 | null) { + if (!x) { + } + } + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, - {MessageId: "unexpectedObjectContext", Line: 1}, - {MessageId: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedNullish", Line: 1}, - }, + {MessageId: "unexpectedNullableNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ }, }) } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index 168ae1ac..048cf0c5 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -8,1333 +8,1852 @@ import ( "github.com/typescript-eslint/tsgolint/internal/utils" ) -func TestStrictBooleanExpressionsRule(t *testing.T) { +func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { rule_tester.RunRuleTester( fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ - // ======================================== - // BOOLEAN IN BOOLEAN CONTEXT - // ======================================== - {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) {}`}, - - // ======================================== - // STRING IN BOOLEAN CONTEXT - // ======================================== - {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);`}, - - // ======================================== - // NUMBER IN BOOLEAN CONTEXT - // ======================================== - {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);`}, - - // ======================================== - // NULLABLE OBJECT IN BOOLEAN CONTEXT - // ======================================== - {Code: `declare const x: null | object; if (x) {}`}, - {Code: `(x?: { a: any }) => !x;`}, - {Code: `(x: T) => (x ? 1 : 0);`}, - - // ======================================== - // NULLABLE BOOLEAN IN BOOLEAN CONTEXT - // ======================================== { - Code: `declare const x: boolean | null; if (x) {}`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableBoolean: utils.Ref(true), - }, + 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?: boolean) => !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: T) => (x ? 1 : 0);`, + Code: ` + (x?: boolean) => !x; + `, Options: StrictBooleanExpressionsOptions{ + AllowNullableBoolean: utils.Ref(true), }, }, { - Code: `const a: (undefined | boolean | null)[] = [true, undefined, null]; a.some(x => x);`, + Code: ` + (x: T) => (x ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ AllowNullableBoolean: utils.Ref(true), }, }, - - // ======================================== - // NULLABLE STRING IN BOOLEAN CONTEXT - // ======================================== { - Code: `declare const x: string | undefined; if (x) {}`, + Code: ` + const a: (undefined | boolean | null)[] = [true, undefined, null]; + a.some(x => x); + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableString: utils.Ref(true), + AllowNullableBoolean: utils.Ref(true), }, }, { - Code: `(x?: string) => !x;`, + Code: ` + declare const x: string | null; + if (x) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowNullableString: utils.Ref(true), }, }, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: ` + (x?: string) => !x; + `, Options: StrictBooleanExpressionsOptions{ AllowNullableString: utils.Ref(true), }, }, { - Code: `'string' != null ? 'asd' : '';`, + Code: ` + (x: T) => (x ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ AllowNullableString: utils.Ref(true), }, }, - - // ======================================== - // NULLABLE NUMBER IN BOOLEAN CONTEXT - // ======================================== { - Code: `declare const x: number | undefined; if (x) {}`, + Code: ` + declare const x: number | null; + if (x) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowNullableNumber: utils.Ref(true), }, }, { - Code: `(x?: number) => !x;`, + Code: ` + (x?: number) => !x; + `, Options: StrictBooleanExpressionsOptions{ AllowNullableNumber: utils.Ref(true), }, }, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: ` + (x: T) => (x ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ AllowNullableNumber: utils.Ref(true), }, }, { - Code: `declare const x: bigint | undefined; if (x ?? 0n) {}`, + Code: ` + declare const arrayOfArrays: (null | unknown[])[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array?.length); + `, Options: StrictBooleanExpressionsOptions{ AllowNullableNumber: utils.Ref(true), }, }, - - // ======================================== - // ANY TYPE IN BOOLEAN CONTEXT - // ======================================== { - Code: `declare const x: any; if (x) {}`, + Code: ` + declare const x: any; + if (x) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowAny: utils.Ref(true), }, }, { - Code: `(x?: any) => !x;`, + Code: ` + x => !x; + `, Options: StrictBooleanExpressionsOptions{ AllowAny: utils.Ref(true), }, }, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: ` + (x: T) => (x ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ AllowAny: utils.Ref(true), }, }, { - Code: `const foo: undefined | any = 0; if (foo) {}`, + Code: ` + declare const arrayOfArrays: any[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, Options: StrictBooleanExpressionsOptions{ AllowAny: utils.Ref(true), }, }, - - // ======================================== - // UNKNOWN TYPE IN BOOLEAN CONTEXT - // ======================================== { - Code: `declare const x: unknown; if (x) {}`, + Code: ` + 1 && true && 'x' && {}; + `, Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), }, }, { - Code: `(x: T) => (x ? 1 : 0);`, - Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(true), - }, - }, - - // ======================================== - // NULLABLE ENUM IN BOOLEAN CONTEXT - // ======================================== - { - 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) { - } + Code: ` + let x = 0 || false || '' || null; `, Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: 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) { - } + if (1 && true && 'x') void 0; `, Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: 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) { - } + if (0 || false || '') void 0; `, Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: 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) { - } - `, + 1 && true && 'x' ? {} : null; + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), }, }, - - // ======================================== - // ALLOW RULE TO RUN WITHOUT STRICT NULL CHECKS - // ======================================== { - Code: `if (true) {}`, + Code: ` + 0 || false || '' ? null : {}; + `, Options: StrictBooleanExpressionsOptions{ - AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), }, }, - - // ======================================== - // COMPLEX BOOLEAN EXPRESSIONS - // ======================================== - {Code: `(x?: boolean) => x ?? false ? true : false;`}, - {Code: `(x: T) => x ?? false;`}, { - Code: `(x?: string) => x ?? false;`, + Code: ` + declare const arrayOfArrays: string[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableString: utils.Ref(true), + AllowString: utils.Ref(true), }, }, { - Code: `(x?: number) => x ?? false;`, + Code: ` + declare const arrayOfArrays: number[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableNumber: utils.Ref(true), + AllowNumber: utils.Ref(true), }, }, - - // ======================================== - // ASSERT FUNCTIONS - // ======================================== { Code: ` -declare function assert(a: number, b: unknown): asserts a; -declare const nullableString: string | null; -declare const boo: boolean; -assert(boo, nullableString); + declare const arrayOfArrays: (null | object)[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); `, + Options: StrictBooleanExpressionsOptions{ + AllowNullableObject: utils.Ref(true), + }, }, { Code: ` -declare function assert(a: boolean, b: unknown): asserts b is string; -declare const nullableString: string | null; -declare const boo: boolean; -assert(boo, nullableString); + 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: ` -declare function assert(a: number, b: unknown): asserts b; -declare const nullableString: string | null; -declare const boo: boolean; -assert(nullableString, boo); + 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: ` -declare function assert(a: number, b: unknown): asserts b; -declare const nullableString: string | null; -declare const boo: boolean; -assert(...nullableString, nullableString); + 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: ` -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); + 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: ` -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); - `, - }, - // Intentional use of `any` to test a function call with no call signatures. - { - Code: ` -declare const assert: any; -declare const nullableString: string | null; -assert(nullableString); + enum ExampleEnum { + This = 0, + That = 'one', + } + (value?: ExampleEnum) => (value ? 1 : 0); `, - }, - - // ======================================== - // ARRAY PREDICATE FUNCTIONS - // ======================================== - {Code: `['one', 'two', ''].some(x => x);`}, - {Code: `['one', 'two', ''].find(x => x);`}, - {Code: `['one', 'two', ''].every(x => x);`}, - {Code: `['one', 'two', ''].filter((x): boolean => x);`}, - {Code: `['one', 'two', ''].filter(x => Boolean(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);`}, - - // ======================================== - // SPECIAL CASES - // ======================================== - {Code: `for (let x = 0; ; x++) { break; }`}, - }, - []rule_tester.InvalidTestCase{ - // ======================================== - // NON-BOOLEAN IN RHS OF TEST EXPRESSION - // ======================================== - { - 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: 1, - }, + AllowNullableEnum: utils.Ref(true), }, }, { - Code: `while (false || 'a' + 'b') {}`, + Code: ` + enum ExampleEnum { + This = '', + That = 1, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - { - MessageId: "unexpectedString", - Line: 1, - }, + AllowNullableEnum: utils.Ref(true), }, }, { - Code: `(x: object) => (true || false || x ? true : false);`, + Code: ` + enum ExampleEnum { + This = 'this', + That = 1, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - { - MessageId: "unexpectedObjectContext", - Line: 1, - }, + AllowNullableEnum: utils.Ref(true), }, }, - - // ======================================== - // CHECK OUTERMOST OPERANDS - // ======================================== { - Code: `if (('' && {}) || (0 && void 0)) { }`, + Code: ` + enum ExampleEnum { + This = '', + That = 0, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, - {MessageId: "unexpectedObjectContext", Line: 1}, - {MessageId: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedNullish", Line: 1}, + AllowNullableEnum: utils.Ref(true), }, }, - - // ======================================== - // ARRAY PREDICATE WITH NON-BOOLEAN - // ======================================== { - Code: `declare const array: string[]; array.some(x => x);`, + Code: ` + enum ExampleEnum { + This = '', + That = 0, + } + declare const arrayOfArrays: (ExampleEnum | null)[]; + const isAnyNonEmptyArray1 = arrayOfArrays.some(array => array); + `, Options: StrictBooleanExpressionsOptions{ - AllowNullableBoolean: utils.Ref(true), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, + AllowNullableEnum: utils.Ref(true), }, }, - - // ======================================== - // BRANDED TYPES - // ======================================== { Code: ` - declare const foo: true & { __BRAND: 'Foo' }; - if (('' && foo) || (0 && void 0)) { } - `, + declare const x: string[] | null; + // eslint-disable-next-line + if (x) { + } + `, + TSConfig: "tsconfig.unstrict.json", 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}, + AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), }, }, { Code: ` - declare const foo: false & { __BRAND: 'Foo' }; - if (('' && {}) || (foo && void 0)) { } - `, + 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{ - AllowNullableObject: utils.Ref(false), - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 3}, - {MessageId: "unexpectedObjectContext", Line: 3}, - {MessageId: "unexpectedNullish", Line: 3}, + 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; + } - // ======================================== - // LOGICAL OPERANDS FOR CONTROL FLOW - // ======================================== + function f(opts: Options): void { + if (opts.enableSomething) console.log('Do something'); + } + `}, { - 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: "unexpectedObjectContext", Line: 1}, - }, - }, + Code: ` + declare const x: true | null; + if (x) { + } + `}, { - Code: `'asd' || 123 || [] || null;`, + Code: ` + declare const x: 'a' | null; + declare const y: 'a'; + if (x) { + } + if (y) { + } + `, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), - }, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, - {MessageId: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedObjectContext", Line: 1}, + AllowString: utils.Ref(true), }, }, { - Code: `let x = (1 && 'a' && null) || 0 || '' || {};`, + 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{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), + 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: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedString", Line: 1}, - {MessageId: "unexpectedNullish", Line: 1}, - {MessageId: "unexpectedNumber", Line: 1}, {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: `return (1 || 'a' || null) && 0 && '' && {};`, + Code: "if (('' && {}) || (0 && void 0)) { }", Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), + AllowNullableObject: utils.Ref(false), 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}, + {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: `console.log((1 && []) || ('a' && {}));`, + Code: ` + declare const foo: true & { __BRAND: 'Foo' }; + if (('' && foo) || (0 && void 0)) { } + `, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), + AllowNullableObject: utils.Ref(false), AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedObjectContext", Line: 1}, - {MessageId: "unexpectedString", Line: 1}, + {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), }, - }, - - // ======================================== - // CONDITIONALS WITH ALL OPERANDS CHECKED - // ======================================== + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedString", Line: 3}, {MessageId: "unexpectedObject", Line: 3}, {MessageId: "unexpectedNullish", Line: 3}, + } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, { - Code: `if ((1 && []) || ('a' && {})) void 0;`, + Code: "'asd' && 123 && [] && null;", Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), + AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - {MessageId: "unexpectedObjectContext", Line: 1}, - {MessageId: "unexpectedString", Line: 1}, - {MessageId: "unexpectedObjectContext", Line: 1}, + {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 = null || 0 || 'a' || [] ? {} : undefined;`, + Code: "let x = (1 && 'a' && null) || 0 || '' || {};", Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), + 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: "unexpectedObjectContext", Line: 1}, + {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: `return !(null || 0 || 'a' || []);`, + Code: "console.log((1 && []) || ('a' && {}));", Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - AllowString: utils.Ref(false), + 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: "unexpectedObjectContext", Line: 1}, + {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), }, - }, - - // ======================================== - // NULLISH VALUES IN BOOLEAN CONTEXT - // ======================================== + 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: `null || {};`, + 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: "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: `undefined && [];`, + Code: "null || {};", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 1}, - }, - }, + }}, { - Code: `declare const x: null; if (x) {}`, + 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;`, + Code: "(x: undefined) => !x;", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 1}, - }, - }, + }}, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 1}, - }, - }, + }}, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 1}, - }, - }, + }}, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 1}, - }, - }, - - // ======================================== - // OBJECT IN BOOLEAN CONTEXT - // ======================================== + }}, { - Code: `[] || 1;`, + Code: "[] || 1;", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedObject", Line: 1}, + }}, { - Code: `({}) && 'a';`, + Code: "({}) && 'a';", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedObject", Line: 1}, + }}, { - Code: `declare const x: symbol; if (x) {}`, + Code: ` + declare const x: symbol; + if (x) { + } + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedObject", Line: 3}, + }}, { - Code: `(x: () => void) => !x;`, + Code: "(x: () => void) => !x;", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedObject", Line: 1}, + }}, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedObject", Line: 1}, + }}, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedObject", Line: 1}, + }}, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedObject", Line: 1}, + }}, { - Code: ` void>(x: T) => (x ? 1 : 0);`, + Code: " void>(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, - - // ======================================== - // STRING IN BOOLEAN CONTEXT WITH ALLOWSTRING: FALSE - // ======================================== + {MessageId: "unexpectedObject", Line: 1}, + }}, { - Code: `while ('') {}`, + Code: "while ('') {}", Options: StrictBooleanExpressionsOptions{ AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, - }, - }, + } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, { - Code: `for (; 'foo'; ) {}`, + 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) {}`, + Code: ` + declare const x: string; + if (x) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, - }, - }, + {MessageId: "unexpectedString", Line: 3}, + } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, { - Code: `(x: string) => !x;`, + 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);`, + Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, - }, - }, - - // ======================================== - // NUMBER IN BOOLEAN CONTEXT WITH ALLOWNUMBER: FALSE - // ======================================== + } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, { - Code: `while (0n) {}`, + Code: "while (0n) {}", Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, - }, - }, + } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, { - Code: `for (; 123; ) {}`, + 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) {}`, + Code: ` + declare const x: number; + if (x) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, + {MessageId: "unexpectedNumber", Line: 3}, + } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, { - Code: `(x: bigint) => !x;`, + 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);`, + 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'];`, + 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) {}`, + Code: ` + declare const a: any[] & { notLength: number }; + if (a.notLength) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, - - // ======================================== - // ARRAY.LENGTH IN BOOLEAN CONTEXT - // ======================================== + {MessageId: "unexpectedNumber", Line: 3}, + } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, { - Code: `if (![].length) {}`, + Code: ` + if (![].length) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, + {MessageId: "unexpectedNumber", Line: 2}, + } /* Suggestions: conditionFixCompareArrayLengthZero */}, { - Code: `(a: number[]) => a.length && '...';`, + Code: ` + (a: number[]) => a.length && '...'; + `, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, + {MessageId: "unexpectedNumber", Line: 2}, + } /* Suggestions: conditionFixCompareArrayLengthNonzero */}, { - Code: `(...a: T) => a.length || 'empty';`, + Code: ` + (...a: T) => a.length || 'empty'; + `, Options: StrictBooleanExpressionsOptions{ AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, - - // ======================================== - // MIXED STRING | NUMBER VALUE IN BOOLEAN CONTEXT - // ======================================== + {MessageId: "unexpectedNumber", Line: 2}, + } /* Suggestions: conditionFixCompareArrayLengthNonzero */}, { - Code: `declare const x: string | number; if (x) {}`, + Code: ` + declare const x: string | number; + if (x) { + } + `, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(true), - AllowString: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 1}, - }, - }, + {MessageId: "unexpectedMixedCondition", Line: 3}, + }}, { - Code: `(x: bigint | string) => !x;`, + Code: "(x: bigint | string) => !x;", Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(true), - AllowString: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedMixedCondition", Line: 1}, - }, - }, + }}, { - Code: `(x: T) => (x ? 1 : 0);`, + Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(true), - AllowString: utils.Ref(true), + AllowNumber: utils.Ref(true), AllowString: utils.Ref(true), }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedMixedCondition", Line: 1}, - }, - }, - - // ======================================== - // NULLABLE BOOLEAN WITHOUT OPTION - // ======================================== + }}, { - Code: `declare const x: boolean | null; if (x) {}`, + Code: ` + declare const x: boolean | null; + if (x) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowNullableBoolean: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableBoolean", Line: 1}, - }, - }, + {MessageId: "unexpectedNullableBoolean", Line: 3}, + } /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue */}, { - Code: `(x?: boolean) => !x;`, + 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);`, + Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ AllowNullableBoolean: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableBoolean", Line: 1}, - }, - }, - - // ======================================== - // NULLABLE OBJECT WITHOUT OPTION - // ======================================== + } /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue */}, { - Code: `declare const x: object | null; if (x) {}`, + Code: ` + declare const x: object | null; + if (x) { + } + `, Options: StrictBooleanExpressionsOptions{ AllowNullableObject: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableObject", Line: 1}, - }, - }, + {MessageId: "unexpectedNullableObject", Line: 3}, + } /* Suggestions: conditionFixCompareNullish */}, { - Code: `(x?: { a: number }) => !x;`, + 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);`, + Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ AllowNullableObject: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableObject", Line: 1}, - }, - }, - - // ======================================== - // NULLABLE STRING WITHOUT OPTION - // ======================================== + } /* Suggestions: conditionFixCompareNullish */}, { - Code: `declare const x: string | null; if (x) {}`, + Code: ` + declare const x: string | null; + if (x) { + } + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, - }, - }, + {MessageId: "unexpectedNullableString", Line: 3}, + } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, { - Code: `(x?: string) => !x;`, + Code: "(x?: string) => !x;", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 1}, - }, - }, + } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, { - Code: `(x: T) => (x ? 1 : 0);`, + 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) {} }`, + Code: ` + function foo(x: '' | 'bar' | null) { + if (!x) { + } + } + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, - }, - }, - - // ======================================== - // NULLABLE NUMBER WITHOUT OPTION - // ======================================== + {MessageId: "unexpectedNullableString", Line: 3}, + } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, { - Code: `declare const x: number | null; if (x) {}`, + Code: ` + declare const x: number | null; + if (x) { + } + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, - }, - }, + {MessageId: "unexpectedNullableNumber", Line: 3}, + } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */}, { - Code: `(x?: number) => !x;`, + Code: "(x?: number) => !x;", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableNumber", Line: 1}, - }, - }, + } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */}, { - Code: `(x: T) => (x ? 1 : 0);`, + 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) {} }`, + Code: ` + function foo(x: 0 | 1 | null) { + if (!x) { + } + } + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, - }, + {MessageId: "unexpectedNullableNumber", Line: 3}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ }, - - // ======================================== - // NULLABLE ENUM WITHOUT OPTION - // ======================================== { Code: ` - enum ExampleEnum { This = 0, That = 1 } - const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; - if (theEnum) {} - `, + 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: "unexpectedNullableNumber", Line: 4}, - }, + {MessageId: "unexpectedNullableEnum", Line: 7}, + }, /* Suggestions: conditionFixCompareNullish */ }, { Code: ` - enum ExampleEnum { This = 0, That = 1 } - const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; - if (!theEnum) {} - `, + 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: "unexpectedNullableNumber", Line: 4}, - }, - }, + {MessageId: "unexpectedNullableEnum", Line: 7}, + } /* Suggestions: conditionFixCompareNullish */}, { Code: ` - enum ExampleEnum { This, That } - const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; - if (!theEnum) {} - `, + 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: "unexpectedNullableNumber", Line: 4}, - }, - }, + {MessageId: "unexpectedNullableEnum", Line: 7}, + } /* Suggestions: conditionFixCompareNullish */}, { Code: ` - enum ExampleEnum { This = '', That = 'a' } - const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; - if (!theEnum) {} - `, + 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: "unexpectedNullableString", Line: 4}, - }, - }, + {MessageId: "unexpectedNullableEnum", Line: 7}, + } /* Suggestions: conditionFixCompareNullish */}, { Code: ` - enum ExampleEnum { This = '', That = 0 } - const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; - if (!theEnum) {} - `, + 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: "unexpectedMixedCondition", Line: 4}, - }, - }, + {MessageId: "unexpectedNullableEnum", Line: 7}, + } /* Suggestions: conditionFixCompareNullish */}, { Code: ` - enum ExampleEnum { This = 'one', That = 'two' } - const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; - if (!theEnum) {} - `, + 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: "unexpectedNullableString", Line: 4}, - }, - }, + {MessageId: "unexpectedNullableEnum", Line: 7}, + } /* Suggestions: conditionFixCompareNullish */}, { Code: ` - enum ExampleEnum { This = 1, That = 2 } - const theEnum = Math.random() < 0.3 ? ExampleEnum.This : null; - if (!theEnum) {} - `, + 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: "unexpectedNullableNumber", Line: 4}, - }, - }, - - // ======================================== - // NULLABLE MIXED ENUM WITHOUT OPTION - // ======================================== + {MessageId: "unexpectedNullableEnum", Line: 7}, + } /* Suggestions: conditionFixCompareNullish */}, { Code: ` - enum ExampleEnum { This = 0, That = 'one' } - (value?: ExampleEnum) => (value ? 1 : 0); - `, + enum ExampleEnum { + This = 0, + That = 'one', + } + (value?: ExampleEnum) => (value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ AllowNullableEnum: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 3}, - }, - }, + {MessageId: "unexpectedNullableEnum", Line: 6}, + } /* Suggestions: conditionFixCompareNullish */}, { Code: ` - enum ExampleEnum { This = '', That = 1 } - (value?: ExampleEnum) => (!value ? 1 : 0); - `, + enum ExampleEnum { + This = '', + That = 1, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ AllowNullableEnum: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 3}, - }, - }, + {MessageId: "unexpectedNullableEnum", Line: 6}, + } /* Suggestions: conditionFixCompareNullish */}, { Code: ` - enum ExampleEnum { This = 'this', That = 1 } - (value?: ExampleEnum) => (value ? 1 : 0); - `, + enum ExampleEnum { + This = 'this', + That = 1, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ AllowNullableEnum: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedMixedCondition", Line: 3}, - }, - }, - - // ======================================== - // ANY WITHOUT OPTION - // ======================================== + {MessageId: "unexpectedNullableEnum", Line: 6}, + } /* Suggestions: conditionFixCompareNullish */}, { - Code: `if ((Boolean(x) || {}) || (typeof x === 'string' && x)) {}`, + Code: ` + enum ExampleEnum { + This = '', + That = 0, + } + (value?: ExampleEnum) => (!value ? 1 : 0); + `, Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), + AllowNullableEnum: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - {MessageId: "unexpectedString", Line: 1}, - }, - }, + {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) { + } + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "noStrictNullCheck", Line: 0}, {MessageId: "unexpectedObject", Line: 3}, + }}, { - Code: `if (1) {}`, + Code: ` + declare const obj: { x: number } | null; + !obj ? 1 : 0 + !obj + obj || 0 + obj && 1 || 0 + `, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowNullableObject: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, + {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 */}, + { + Code: ` + function asserts1(x: string | number | undefined): asserts x {} + function asserts2(x: string | number | undefined): asserts x {} - // ======================================== - // ASSERT FUNCTIONS - // ======================================== + 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: ` - 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); - `, + 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), }, - }, - - // ======================================== - // ARRAY FILTER PREDICATES - // ======================================== + 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: `declare const nullOrBool: boolean | null; [true, false, null].filter(x => nullOrBool);`, + 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: 1}, - }, - }, + {MessageId: "unexpectedNullableBoolean", Line: 8}, + }}, { - Code: `declare const nullOrString: string | null; ['', 'foo', null].filter(x => nullOrString);`, + Code: ` + [1, null].every(async x => { + return x != null; + }); + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableString", Line: 1}, - }, - }, + {MessageId: "predicateCannotBeAsync", Line: 2}, + }}, { - Code: `declare const nullOrNumber: number | null; [0, null].filter(x => nullOrNumber);`, + Code: ` + const predicate = async x => { + return x != null; + }; + + [1, null].every(predicate); + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 1}, - }, - }, + {MessageId: "unexpectedObject", Line: 6}, + }}, { - Code: `const objectValue: object = {}; [{ a: 0 }, {}].filter(x => objectValue);`, + Code: ` + [1, null].every((x): boolean | number => { + return x != null; + }); + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedMixedCondition", Line: 2}, + }}, { - Code: `const objectValue: object = {}; [{ a: 0 }, {}].filter(x => { return objectValue; });`, + Code: ` + [1, null].every((x): boolean | undefined => { + return x != null; + }); + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedObjectContext", Line: 1}, - }, - }, + {MessageId: "unexpectedNullableBoolean", Line: 2}, + }}, { - Code: `declare const nullOrObject: object | null; [{ a: 0 }, null].filter(x => nullOrObject);`, - Options: StrictBooleanExpressionsOptions{ - AllowNullableObject: utils.Ref(false), - }, + Code: ` + [1, null].every((x, i) => {}); + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableObject", Line: 1}, - }, - }, + {MessageId: "unexpectedNullish", Line: 2}, + } /* Suggestions: explicitBooleanReturnType */}, { - Code: `const numbers: number[] = [1]; [1, 2].filter(x => numbers.length);`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, + Code: ` + [() => {}, null].every((x: () => void) => {}); + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, + {MessageId: "unexpectedNullish", Line: 2}, + } /* Suggestions: explicitBooleanReturnType */}, { - Code: `const numberValue: number = 1; [1, 2].filter(x => numberValue);`, - Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), - }, + Code: ` + [() => {}, null].every(function (x: () => void) {}); + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 1}, - }, - }, + {MessageId: "unexpectedNullish", Line: 2}, + } /* Suggestions: explicitBooleanReturnType */}, { - Code: `const stringValue: string = 'hoge'; ['hoge', 'foo'].filter(x => stringValue);`, - Options: StrictBooleanExpressionsOptions{ - AllowString: utils.Ref(false), - }, + Code: ` + [() => {}, null].every(() => {}); + `, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, - }, - }, + {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; - // ======================================== - // UNKNOWN TYPE WITHOUT OPTION - // ======================================== + [35].filter(f); + `, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "unexpectedMixedCondition", Line: 6}, + }}, { - Code: `declare const x: unknown; if (x) {}`, + 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{ - AllowAny: utils.Ref(false), + AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, - }, - }, + {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 x: unknown; x ? 'a' : 'b';`, + 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{ - AllowAny: utils.Ref(false), + AllowNullableObject: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, - }, - }, + {MessageId: "unexpectedNullableObject", Line: 3}, + } /* Suggestions: conditionFixCompareNullish, explicitBooleanReturnType */}, { - Code: `declare const x: unknown; x && 'a';`, + Code: ` + const numbers: number[] = [1]; + [1, 2].filter(x => numbers.length); + `, Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(false), + AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, - }, - }, + {MessageId: "unexpectedNumber", Line: 3}, + } /* Suggestions: conditionFixCompareArrayLengthNonzero, explicitBooleanReturnType */}, { - Code: `declare const x: unknown; x || 'a';`, + Code: ` + const numberValue: number = 1; + [1, 2].filter(x => numberValue); + `, Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(false), + AllowNumber: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, - }, - }, + {MessageId: "unexpectedNumber", Line: 3}, + } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, explicitBooleanReturnType */}, { - Code: `declare const x: unknown; !x;`, + Code: ` + const stringValue: string = 'hoge'; + ['hoge', 'foo'].filter(x => stringValue); + `, Options: StrictBooleanExpressionsOptions{ - AllowAny: utils.Ref(false), + AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedAny", Line: 1}, - }, - }, + {MessageId: "unexpectedString", Line: 3}, + } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */}, }) } From 8b3f7b14961f1676bab062d080a242007d374193 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 15:45:07 +0900 Subject: [PATCH 14/23] wip --- .../strict_boolean_expressions.go | 9 +- .../strict_boolean_expressions_single_test.go | 11 +-- .../strict_boolean_expressions_test.go | 96 ++++++++++++------- 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index ebdbf7f0..9c784cf7 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -342,7 +342,7 @@ func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { if partInfo.isEnum { info.isEnum = true } - if partInfo.variant == typeVariantNumber || partInfo.variant == typeVariantString { + if partInfo.variant == typeVariantBoolean || partInfo.variant == typeVariantNumber || partInfo.variant == typeVariantString { if metNotTruthy { continue } @@ -420,7 +420,7 @@ func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { } if flags&(checker.TypeFlagsBoolean|checker.TypeFlagsBooleanLiteral|checker.TypeFlagsBooleanLike) != 0 { - if utils.IsTrueLiteralType(t) { + if t.AsLiteralType().String() == "true" { info.isTruthy = true } info.variant = typeVariantBoolean @@ -537,6 +537,11 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts 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()) } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index fe67b8e4..42f30c7f 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -11,18 +11,13 @@ import ( func TestStrictBooleanExpressionsSingleRule(t *testing.T) { rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ - - }, []rule_tester.InvalidTestCase{ { Code: ` - function foo(x: 0 | 1 | null) { - if (!x) { - } + declare const foo: boolean & { __BRAND: 'Foo' }; + if (foo) { } `, - Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableNumber", Line: 3}, - }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ }, + }, []rule_tester.InvalidTestCase{ }) } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index 048cf0c5..079da9c2 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -489,13 +489,15 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { 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; @@ -514,13 +516,15 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { 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 { @@ -530,13 +534,15 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { 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; @@ -555,47 +561,54 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { 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( @@ -610,13 +623,15 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { assert, }; o.assert(foo, nullableString); - `}, + `, + }, { Code: ` declare function assert(x: unknown): x is string; declare const nullableString: string | null; assert(nullableString); - `}, + `, + }, { Code: ` class ThisAsserter { @@ -627,7 +642,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { const thisAsserter: ThisAsserter = new ThisAsserter(); thisAsserter.assertThis(lol); - `}, + `, + }, { Code: ` function assert(this: object, a: number, b: unknown): asserts b; @@ -648,55 +664,64 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { 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 { @@ -704,7 +729,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { return true; } }); - `}, + `, + }, { Code: ` ['one', 'two', ''].filter(function (x): boolean { @@ -714,34 +740,40 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { 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: ` From 602bae5427c35c1ecae7d0ec92bf56cb1d2e98d1 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Mon, 29 Sep 2025 15:51:01 +0900 Subject: [PATCH 15/23] branded boolean --- .../strict_boolean_expressions.go | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 9c784cf7..11b49fbe 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -390,10 +390,28 @@ func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { return analyzeTypePart(typeChecker, t) } -func analyzeTypePart(_ *checker.Checker, t *checker.Type) typeInfo { +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 From 987030a907ff87e839f1bd56b86b351f110bde6a Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 30 Sep 2025 10:55:23 +0900 Subject: [PATCH 16/23] unstrict --- .../strict_boolean_expressions.go | 3 +- .../strict_boolean_expressions_single_test.go | 22 +++- .../strict_boolean_expressions_test.go | 122 +++++++++++------- 3 files changed, 99 insertions(+), 48 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 11b49fbe..ee586e15 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -92,7 +92,7 @@ func buildUnexpectedMixedCondition() rule.RuleMessage { func buildNoStrictNullCheck() rule.RuleMessage { return rule.RuleMessage{ - Id: "msgNoStrictNullCheck", + Id: "noStrictNullCheck", Description: "This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.", } } @@ -149,7 +149,6 @@ var StrictBooleanExpressionsRule = rule.Rule{ core.NewTextRange(0, 0), buildNoStrictNullCheck(), ) - return rule.RuleListeners{} } return rule.RuleListeners{ diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 42f30c7f..0f31b38c 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -7,17 +7,35 @@ import ( "github.com/typescript-eslint/tsgolint/internal/rule_tester" "github.com/typescript-eslint/tsgolint/internal/rules/fixtures" + "github.com/typescript-eslint/tsgolint/internal/utils" ) func TestStrictBooleanExpressionsSingleRule(t *testing.T) { rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ { Code: ` - declare const foo: boolean & { __BRAND: 'Foo' }; - if (foo) { + declare const x: string[] | null; + // oxlint-disable-next-line + if (x) { } `, + TSConfig: "tsconfig.unstrict.json", + Options: StrictBooleanExpressionsOptions{ + AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), + }, }, }, []rule_tester.InvalidTestCase{ + { + Code: ` + declare const x: string[] | null; + if (x) { + } + `, + TSConfig: "tsconfig.unstrict.json", + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "noStrictNullCheck", Line: 0}, + {MessageId: "unexpectedObject", Line: 3}, + }, + }, }) } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index 079da9c2..9c58362c 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -472,18 +472,19 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { AllowNullableEnum: utils.Ref(true), }, }, - { - Code: ` - declare const x: string[] | null; - // eslint-disable-next-line - if (x) { - } - `, - TSConfig: "tsconfig.unstrict.json", - Options: StrictBooleanExpressionsOptions{ - AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), - }, - }, + // TODO: oxlint-disable-next-line should not check + // { + // 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) { @@ -801,7 +802,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedObject", Line: 1}, - }}, + }, + }, { Code: "if (('' && {}) || (0 && void 0)) { }", Options: StrictBooleanExpressionsOptions{ @@ -911,12 +913,14 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { 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; @@ -925,37 +929,44 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, 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; @@ -964,32 +975,38 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, 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{ @@ -1140,7 +1157,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedMixedCondition", Line: 3}, - }}, + }, + }, { Code: "(x: bigint | string) => !x;", Options: StrictBooleanExpressionsOptions{ @@ -1148,7 +1166,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedMixedCondition", Line: 1}, - }}, + }, + }, { Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ @@ -1156,7 +1175,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedMixedCondition", Line: 1}, - }}, + }, + }, { Code: ` declare const x: boolean | null; @@ -1448,12 +1468,14 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedAny", Line: 2}, - } /* Suggestions: conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCastBoolean */ + }, { Code: "x => !x;", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedAny", Line: 1}, - } /* Suggestions: conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCastBoolean */ + }, { Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ @@ -1470,9 +1492,12 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { if (x) { } `, + TSConfig: "tsconfig.unstrict.json", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "noStrictNullCheck", Line: 0}, {MessageId: "unexpectedObject", Line: 3}, - }}, + {MessageId: "noStrictNullCheck", Line: 0}, + {MessageId: "unexpectedObject", Line: 3}, + }, + }, { Code: ` declare const obj: { x: number } | null; @@ -1666,7 +1691,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableBoolean", Line: 8}, - }}, + }, + }, { Code: ` [1, null].every(async x => { @@ -1675,7 +1701,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "predicateCannotBeAsync", Line: 2}, - }}, + }, + }, { Code: ` const predicate = async x => { @@ -1686,7 +1713,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedObject", Line: 6}, - }}, + }, + }, { Code: ` [1, null].every((x): boolean | number => { @@ -1695,7 +1723,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedMixedCondition", Line: 2}, - }}, + }, + }, { Code: ` [1, null].every((x): boolean | undefined => { @@ -1704,7 +1733,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableBoolean", Line: 2}, - }}, + }, + }, { Code: ` [1, null].every((x, i) => {}); @@ -1742,7 +1772,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedMixedCondition", Line: 5}, - }}, + }, + }, { Code: ` declare function f(x: number): string; @@ -1753,7 +1784,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedMixedCondition", Line: 6}, - }}, + }, + }, { Code: ` declare function foo(x: number): T; @@ -1761,7 +1793,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedAny", Line: 3}, - }}, + }, + }, { Code: ` function foo(x: number): T {} @@ -1772,7 +1805,8 @@ func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 3}, - }}, + }, + }, { Code: ` declare const nullOrString: string | null; From 60b1920bbcfc0b85205eb310df9157788b94b1ff Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 30 Sep 2025 11:09:53 +0900 Subject: [PATCH 17/23] function and enum --- .../strict_boolean_expressions.go | 49 +++++++++++-------- .../strict_boolean_expressions_single_test.go | 23 +++------ .../strict_boolean_expressions_test.go | 2 +- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index ee586e15..9f461281 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -48,6 +48,13 @@ func buildUnexpectedNullableNumber() rule.RuleMessage { } } +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", @@ -191,26 +198,28 @@ var StrictBooleanExpressionsRule = rule.Rule{ if utils.IsArrayMethodCallWithPredicate(ctx.TypeChecker, callExpr) { if callExpr.Arguments != nil && len(callExpr.Arguments.Nodes) > 0 { arg := callExpr.Arguments.Nodes[0] - if arg != nil && (arg.Kind == ast.KindArrowFunction || arg.Kind == ast.KindFunctionExpression || arg.Kind == ast.KindFunctionDeclaration) { - if checker.GetFunctionFlags(arg)&checker.FunctionFlagsAsync != 0 { - ctx.ReportNode(arg, buildPredicateCannotBeAsync()) - return - } - funcType := ctx.TypeChecker.GetTypeAtLocation(arg) - signatures := ctx.TypeChecker.GetCallSignatures(funcType) - 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 - } + 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) + 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 } + } - if returnType != nil && !isBooleanType(returnType) { - checkCondition(ctx, node, returnType, opts) - } + if returnType != nil && !isBooleanType(returnType) { + checkCondition(ctx, node, returnType, opts) } } } @@ -524,7 +533,7 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts if info.isNullable { if info.isEnum { if !*opts.AllowNullableEnum { - ctx.ReportNode(node, buildUnexpectedNullableString()) + ctx.ReportNode(node, buildUnexpectedNullableEnum()) } } else { if !*opts.AllowNullableString { @@ -543,7 +552,7 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts if info.isNullable { if info.isEnum { if !*opts.AllowNullableEnum { - ctx.ReportNode(node, buildUnexpectedNullableNumber()) + ctx.ReportNode(node, buildUnexpectedNullableEnum()) } } else { if !*opts.AllowNullableNumber { diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 0f31b38c..119c3268 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -12,29 +12,18 @@ import ( func TestStrictBooleanExpressionsSingleRule(t *testing.T) { rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ + + }, []rule_tester.InvalidTestCase{ { Code: ` - declare const x: string[] | null; - // oxlint-disable-next-line - if (x) { - } + function foo(x: number): T {} + [1, null].every(foo); `, - TSConfig: "tsconfig.unstrict.json", Options: StrictBooleanExpressionsOptions{ - AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: utils.Ref(true), + AllowNumber: utils.Ref(false), }, - }, - }, []rule_tester.InvalidTestCase{ - { - Code: ` - declare const x: string[] | null; - if (x) { - } - `, - TSConfig: "tsconfig.unstrict.json", Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "noStrictNullCheck", Line: 0}, - {MessageId: "unexpectedObject", Line: 3}, + {MessageId: "unexpectedNumber", Line: 3}, }, }, }) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index 9c58362c..e5f270ee 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -8,7 +8,7 @@ import ( "github.com/typescript-eslint/tsgolint/internal/utils" ) -func TestStrictBooleanExpressionsRule_Generated(t *testing.T) { +func TestStrictBooleanExpressionsRule(t *testing.T) { rule_tester.RunRuleTester( fixtures.GetRootDir(), "tsconfig.json", From c303eca9a9d04dd1c68686c6951983c8df07b5e9 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 30 Sep 2025 11:18:17 +0900 Subject: [PATCH 18/23] enum more --- .../strict_boolean_expressions.go | 2 +- .../strict_boolean_expressions_single_test.go | 13 +- .../strict_boolean_expressions_test.go | 252 ++++++++++++------ 3 files changed, 177 insertions(+), 90 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 9f461281..170b66ee 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -580,7 +580,7 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts case typeVariantMixed: if info.isEnum { if info.isNullable && !*opts.AllowNullableEnum { - ctx.ReportNode(node, buildUnexpectedNullableNumber()) + ctx.ReportNode(node, buildUnexpectedNullableEnum()) } return } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 119c3268..7403d3f0 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -16,15 +16,18 @@ func TestStrictBooleanExpressionsSingleRule(t *testing.T) { }, []rule_tester.InvalidTestCase{ { Code: ` - function foo(x: number): T {} - [1, null].every(foo); + enum ExampleEnum { + This = 0, + That = 'one', + } + (value?: ExampleEnum) => (value ? 1 : 0); `, Options: StrictBooleanExpressionsOptions{ - AllowNumber: utils.Ref(false), + AllowNullableEnum: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNumber", Line: 3}, - }, + {MessageId: "unexpectedNullableEnum", Line: 6}, + }, /* Suggestions: conditionFixCompareNullish */ }, }) } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index e5f270ee..4a830753 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -786,7 +786,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 2}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: "while (false || 'a' + 'b') {}", Options: StrictBooleanExpressionsOptions{ @@ -794,7 +795,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "(x: object) => (true || false || x ? true : false);", Options: StrictBooleanExpressionsOptions{ @@ -811,7 +813,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, 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 */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: ` declare const array: string[]; @@ -822,7 +825,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString"}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */ + }, { Code: ` declare const foo: true & { __BRAND: 'Foo' }; @@ -833,7 +837,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 3}, {MessageId: "unexpectedNumber", Line: 3}, {MessageId: "unexpectedNullish", Line: 3}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: ` declare const foo: false & { __BRAND: 'Foo' }; @@ -844,7 +849,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 3}, {MessageId: "unexpectedObject", Line: 3}, {MessageId: "unexpectedNullish", Line: 3}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "'asd' && 123 && [] && null;", Options: StrictBooleanExpressionsOptions{ @@ -852,7 +858,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, {MessageId: "unexpectedNumber", Line: 1}, {MessageId: "unexpectedObject", Line: 1}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: "'asd' || 123 || [] || null;", Options: StrictBooleanExpressionsOptions{ @@ -860,7 +867,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, {MessageId: "unexpectedNumber", Line: 1}, {MessageId: "unexpectedObject", Line: 1}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: "let x = (1 && 'a' && null) || 0 || '' || {};", Options: StrictBooleanExpressionsOptions{ @@ -868,7 +876,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, 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 */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "return (1 || 'a' || null) && 0 && '' && {};", Options: StrictBooleanExpressionsOptions{ @@ -876,7 +885,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, 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 */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "console.log((1 && []) || ('a' && {}));", Options: StrictBooleanExpressionsOptions{ @@ -884,7 +894,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, {MessageId: "unexpectedObject", Line: 1}, {MessageId: "unexpectedString", Line: 1}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "if ((1 && []) || ('a' && {})) void 0;", Options: StrictBooleanExpressionsOptions{ @@ -892,7 +903,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, 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 */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "let x = null || 0 || 'a' || [] ? {} : undefined;", Options: StrictBooleanExpressionsOptions{ @@ -900,7 +912,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, 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 */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "return !(null || 0 || 'a' || []);", Options: StrictBooleanExpressionsOptions{ @@ -908,7 +921,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, 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 */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "null || {};", Errors: []rule_tester.InvalidTestCaseError{ @@ -1014,7 +1028,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "for (; 'foo'; ) {}", Options: StrictBooleanExpressionsOptions{ @@ -1022,7 +1037,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: ` declare const x: string; @@ -1034,7 +1050,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 3}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "(x: string) => !x;", Options: StrictBooleanExpressionsOptions{ @@ -1042,7 +1059,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ @@ -1050,7 +1068,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 1}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ + }, { Code: "while (0n) {}", Options: StrictBooleanExpressionsOptions{ @@ -1058,7 +1077,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: "for (; 123; ) {}", Options: StrictBooleanExpressionsOptions{ @@ -1066,7 +1086,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: ` declare const x: number; @@ -1078,7 +1099,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 3}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: "(x: bigint) => !x;", Options: StrictBooleanExpressionsOptions{ @@ -1086,7 +1108,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ @@ -1094,7 +1117,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: "![]['length']; // doesn't count as array.length when computed", Options: StrictBooleanExpressionsOptions{ @@ -1102,7 +1126,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 1}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: ` declare const a: any[] & { notLength: number }; @@ -1114,7 +1139,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 3}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ + }, { Code: ` if (![].length) { @@ -1125,7 +1151,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 2}, - } /* Suggestions: conditionFixCompareArrayLengthZero */}, + }, /* Suggestions: conditionFixCompareArrayLengthZero */ + }, { Code: ` (a: number[]) => a.length && '...'; @@ -1135,7 +1162,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 2}, - } /* Suggestions: conditionFixCompareArrayLengthNonzero */}, + }, /* Suggestions: conditionFixCompareArrayLengthNonzero */ + }, { Code: ` (...a: T) => a.length || 'empty'; @@ -1145,7 +1173,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 2}, - } /* Suggestions: conditionFixCompareArrayLengthNonzero */}, + }, /* Suggestions: conditionFixCompareArrayLengthNonzero */ + }, { Code: ` declare const x: string | number; @@ -1188,7 +1217,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableBoolean", Line: 3}, - } /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue */}, + }, /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue */ + }, { Code: "(x?: boolean) => !x;", Options: StrictBooleanExpressionsOptions{ @@ -1196,7 +1226,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableBoolean", Line: 1}, - } /* Suggestions: conditionFixDefaultFalse, conditionFixCompareFalse */}, + }, /* Suggestions: conditionFixDefaultFalse, conditionFixCompareFalse */ + }, { Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ @@ -1204,7 +1235,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableBoolean", Line: 1}, - } /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue */}, + }, /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue */ + }, { Code: ` declare const x: object | null; @@ -1216,7 +1248,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableObject", Line: 3}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: "(x?: { a: number }) => !x;", Options: StrictBooleanExpressionsOptions{ @@ -1224,7 +1257,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableObject", Line: 1}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: "(x: T) => (x ? 1 : 0);", Options: StrictBooleanExpressionsOptions{ @@ -1232,7 +1266,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableObject", Line: 1}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` declare const x: string | null; @@ -1241,17 +1276,20 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 3}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: "(x?: string) => !x;", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 1}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 1}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` function foo(x: '' | 'bar' | null) { @@ -1261,7 +1299,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 3}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` declare const x: number | null; @@ -1270,17 +1309,20 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableNumber", Line: 3}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ + }, { Code: "(x?: number) => !x;", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableNumber", Line: 1}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ + }, { Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableNumber", Line: 1}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean */ + }, { Code: ` function foo(x: 0 | 1 | null) { @@ -1324,7 +1366,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 7}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1340,7 +1383,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 7}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1356,7 +1400,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 7}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1372,7 +1417,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 7}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1388,7 +1434,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 7}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1404,7 +1451,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 7}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1418,7 +1466,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 6}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1432,7 +1481,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 6}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1446,7 +1496,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 6}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` enum ExampleEnum { @@ -1460,7 +1511,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 6}, - } /* Suggestions: conditionFixCompareNullish */}, + }, /* Suggestions: conditionFixCompareNullish */ + }, { Code: ` if (x) { @@ -1480,12 +1532,14 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedAny", Line: 1}, - } /* Suggestions: conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCastBoolean */ + }, { Code: "(x: T) => (x ? 1 : 0);", Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedAny", Line: 1}, - } /* Suggestions: conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCastBoolean */ + }, { Code: ` declare const x: string[] | null; @@ -1511,7 +1565,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, 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 */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixCompareNullish, conditionFixCompareNullish, conditionFixCompareNullish */ + }, { Code: ` declare function assert(x: unknown): asserts x; @@ -1520,7 +1575,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 4}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` declare function assert(a: number, b: unknown): asserts b; @@ -1529,7 +1585,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 4}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` declare function assert(a: number, b: unknown): asserts b; @@ -1539,7 +1596,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 5}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` declare function assert(this: object, a: number, b: unknown): asserts b; @@ -1548,7 +1606,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 4}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` function asserts1(x: string | number | undefined): asserts x {} @@ -1563,7 +1622,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString"}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` function assert(this: object, a: number, b: unknown): asserts b; @@ -1586,7 +1646,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 18}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` function assert(this: object, a: number, b: unknown): asserts b; @@ -1610,7 +1671,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 19}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` declare function assert(a: boolean, b: unknown): asserts b; @@ -1621,7 +1683,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 6}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` function assert(one: unknown): asserts one; @@ -1634,7 +1697,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 8}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` ['one', 'two', ''].find(x => { @@ -1646,7 +1710,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 2}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` ['one', 'two', ''].find(x => { @@ -1655,7 +1720,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 2}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` ['one', 'two', ''].findLast(x => { @@ -1664,7 +1730,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 2}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` ['one', 'two', ''].find(x => { @@ -1675,7 +1742,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableBoolean", Line: 2}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` const predicate = (x: string) => { @@ -1741,28 +1809,32 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 2}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` [() => {}, null].every((x: () => void) => {}); `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 2}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` [() => {}, null].every(function (x: () => void) {}); `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 2}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` [() => {}, null].every(() => {}); `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullish", Line: 2}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` declare function f(x: number): string; @@ -1814,7 +1886,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 3}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */ + }, { Code: ` declare const nullOrString: string | null; @@ -1822,7 +1895,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableString", Line: 3}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ + }, { Code: ` declare const anyValue: any; @@ -1830,7 +1904,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedAny", Line: 3}, - } /* Suggestions: conditionFixCastBoolean, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCastBoolean, explicitBooleanReturnType */ + }, { Code: ` declare const nullOrBoolean: boolean | null; @@ -1838,7 +1913,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableBoolean", Line: 3}, - } /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixDefaultFalse, conditionFixCompareTrue, explicitBooleanReturnType */ + }, { Code: ` enum ExampleEnum { @@ -1850,7 +1926,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableEnum", Line: 7}, - } /* Suggestions: conditionFixCompareNullish, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCompareNullish, explicitBooleanReturnType */ + }, { Code: ` declare const nullOrNumber: number | null; @@ -1858,7 +1935,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableNumber", Line: 3}, - } /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultZero, conditionFixCastBoolean, explicitBooleanReturnType */ + }, { Code: ` const objectValue: object = {}; @@ -1866,7 +1944,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedObject", Line: 3}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` const objectValue: object = {}; @@ -1876,7 +1955,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { `, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedObject", Line: 3}, - } /* Suggestions: explicitBooleanReturnType */}, + }, /* Suggestions: explicitBooleanReturnType */ + }, { Code: ` declare const nullOrObject: object | null; @@ -1887,7 +1967,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNullableObject", Line: 3}, - } /* Suggestions: conditionFixCompareNullish, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCompareNullish, explicitBooleanReturnType */ + }, { Code: ` const numbers: number[] = [1]; @@ -1898,7 +1979,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 3}, - } /* Suggestions: conditionFixCompareArrayLengthNonzero, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCompareArrayLengthNonzero, explicitBooleanReturnType */ + }, { Code: ` const numberValue: number = 1; @@ -1909,7 +1991,8 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedNumber", Line: 3}, - } /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, explicitBooleanReturnType */ + }, { Code: ` const stringValue: string = 'hoge'; @@ -1920,6 +2003,7 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, Errors: []rule_tester.InvalidTestCaseError{ {MessageId: "unexpectedString", Line: 3}, - } /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */}, + }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, explicitBooleanReturnType */ + }, }) } From 778b0e3f704bb6f13c6e0ea753ca185dffc8395d Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 30 Sep 2025 11:25:41 +0900 Subject: [PATCH 19/23] remove print --- .../strict_boolean_expressions.go | 1 - .../strict_boolean_expressions_single_test.go | 23 ++++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 170b66ee..1521746a 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -468,7 +468,6 @@ func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { if t.IsStringLiteral() { literal := t.AsLiteralType() if literal != nil && literal.Value() != "" { - println(literal.Value()) info.isTruthy = true } } diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go index 7403d3f0..50c29d31 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go @@ -7,7 +7,7 @@ import ( "github.com/typescript-eslint/tsgolint/internal/rule_tester" "github.com/typescript-eslint/tsgolint/internal/rules/fixtures" - "github.com/typescript-eslint/tsgolint/internal/utils" + //"github.com/typescript-eslint/tsgolint/internal/utils" ) func TestStrictBooleanExpressionsSingleRule(t *testing.T) { @@ -16,18 +16,19 @@ func TestStrictBooleanExpressionsSingleRule(t *testing.T) { }, []rule_tester.InvalidTestCase{ { Code: ` - enum ExampleEnum { - This = 0, - That = 'one', - } - (value?: ExampleEnum) => (value ? 1 : 0); + 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); `, - Options: StrictBooleanExpressionsOptions{ - AllowNullableEnum: utils.Ref(false), - }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableEnum", Line: 6}, - }, /* Suggestions: conditionFixCompareNullish */ + {MessageId: "unexpectedNullableString"}, + }, /* Suggestions: conditionFixCompareNullish, conditionFixDefaultEmptyString, conditionFixCastBoolean */ }, }) } From b39813f7b3e100e016854cb6bee044b0f64bc99d Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 30 Sep 2025 12:03:07 +0900 Subject: [PATCH 20/23] function overload --- .../strict_boolean_expressions.go | 94 +++++++------------ .../strict_boolean_expressions_single_test.go | 34 ------- .../strict_boolean_expressions_test.go | 6 ++ 3 files changed, 41 insertions(+), 93 deletions(-) delete mode 100644 internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 1521746a..14b4f68d 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -208,6 +208,7 @@ var StrictBooleanExpressionsRule = rule.Rule{ } 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) @@ -218,10 +219,9 @@ var StrictBooleanExpressionsRule = rule.Rule{ } } - if returnType != nil && !isBooleanType(returnType) { - checkCondition(ctx, node, returnType, opts) - } + types = append(types, utils.UnionTypeParts(returnType)...) } + checkCondition(ctx, node, types, opts) } } }, @@ -268,7 +268,7 @@ func findTruthinessAssertedArgument(typeChecker *checker.Checker, callExpr *ast. func checkNode(ctx rule.RuleContext, node *ast.Node, opts StrictBooleanExpressionsOptions) { nodeType := utils.GetConstrainedTypeAtLocation(ctx.TypeChecker, node) - checkCondition(ctx, node, nodeType, opts) + checkCondition(ctx, node, utils.UnionTypeParts(nodeType), opts) } func traverseLogicalExpression(ctx rule.RuleContext, binExpr *ast.BinaryExpression, opts StrictBooleanExpressionsOptions, isCondition bool) { @@ -329,73 +329,49 @@ type typeInfo struct { isEnum bool } -func analyzeType(typeChecker *checker.Checker, t *checker.Type) typeInfo { +func analyzeTypeParts(typeChecker *checker.Checker, types []*checker.Type) typeInfo { info := typeInfo{ - types: []*checker.Type{t}, + isUnion: len(types) > 1, + types: types, } + variants := make(map[typeVariant]bool) - if utils.IsUnionType(t) { - info.isUnion = true - parts := utils.UnionTypeParts(t) - variants := make(map[typeVariant]bool) - - metNotTruthy := false + metNotTruthy := false - for _, part := range parts { - 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 - } + for _, part := range info.types { + partInfo := analyzeTypePart(typeChecker, part) + variants[partInfo.variant] = true + if partInfo.variant == typeVariantNullish { + info.isNullable = true } - - 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 - } + if partInfo.isEnum { + info.isEnum = true + } + if partInfo.variant == typeVariantBoolean || partInfo.variant == typeVariantNumber || partInfo.variant == typeVariantString { + if metNotTruthy { + continue } - } else { - info.variant = typeVariantMixed + info.isTruthy = partInfo.isTruthy + metNotTruthy = !partInfo.isTruthy } - - return info } - if utils.IsIntersectionType(t) { - info.isIntersection = true - types := t.Types() - isBoolean := false - for _, t2 := range types { - if analyzeTypePart(typeChecker, t2).variant == typeVariantBoolean { - isBoolean = true + 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 } } - if isBoolean { - info.variant = typeVariantBoolean - } else { - info.variant = typeVariantObject - } - return info + } else { + info.variant = typeVariantMixed } - return analyzeTypePart(typeChecker, t) + return info } func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { @@ -510,8 +486,8 @@ func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { return info } -func checkCondition(ctx rule.RuleContext, node *ast.Node, t *checker.Type, opts StrictBooleanExpressionsOptions) { - info := analyzeType(ctx.TypeChecker, t) +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: diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go deleted file mode 100644 index 50c29d31..00000000 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_single_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Code generated from strict-boolean-expressions.test.ts - DO NOT EDIT. - -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 TestStrictBooleanExpressionsSingleRule(t *testing.T) { - rule_tester.RunRuleTester(fixtures.GetRootDir(), "tsconfig.json", t, &StrictBooleanExpressionsRule, []rule_tester.ValidTestCase{ - - }, []rule_tester.InvalidTestCase{ - { - 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 */ - }, - }) -} diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index 4a830753..9dccdb0e 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -1609,6 +1609,12 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { }, /* 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 {} From d8afb9672a4f14989016fa90564aee722699a6c1 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 30 Sep 2025 12:07:19 +0900 Subject: [PATCH 21/23] lint --- .../strict_boolean_expressions.go | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go index 14b4f68d..44638eb1 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions.go @@ -374,7 +374,7 @@ func analyzeTypeParts(typeChecker *checker.Checker, types []*checker.Type) typeI return info } -func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { +func analyzeTypePart(_typeChecker *checker.Checker, t *checker.Type) typeInfo { info := typeInfo{} flags := checker.Type_flags(t) @@ -383,7 +383,7 @@ func analyzeTypePart(typeChecker *checker.Checker, t *checker.Type) typeInfo { types := t.Types() isBoolean := false for _, t2 := range types { - if analyzeTypePart(typeChecker, t2).variant == typeVariantBoolean { + if analyzeTypePart(_typeChecker, t2).variant == typeVariantBoolean { isBoolean = true break } @@ -568,21 +568,3 @@ func checkCondition(ctx rule.RuleContext, node *ast.Node, types []*checker.Type, } } } - -func isBooleanType(t *checker.Type) bool { - flags := checker.Type_flags(t) - - if flags&(checker.TypeFlagsBoolean|checker.TypeFlagsBooleanLiteral) != 0 { - if utils.IsUnionType(t) { - for _, part := range utils.UnionTypeParts(t) { - partFlags := checker.Type_flags(part) - if partFlags&(checker.TypeFlagsNull|checker.TypeFlagsUndefined|checker.TypeFlagsVoid) != 0 { - return false - } - } - } - return true - } - - return false -} From 7800b8d908ce6a072adcd81cf5a9332c02f9fe8b Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 30 Sep 2025 12:08:37 +0900 Subject: [PATCH 22/23] indent --- .../strict_boolean_expressions_test.go | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index 9dccdb0e..d83335cf 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -812,7 +812,10 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedNullish", Line: 1}, }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ }, { @@ -836,7 +839,9 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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}, + {MessageId: "unexpectedString", Line: 3}, + {MessageId: "unexpectedNumber", Line: 3}, + {MessageId: "unexpectedNullish", Line: 3}, }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ }, { @@ -848,7 +853,9 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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}, + {MessageId: "unexpectedString", Line: 3}, + {MessageId: "unexpectedObject", Line: 3}, + {MessageId: "unexpectedNullish", Line: 3}, }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ }, { @@ -857,7 +864,9 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, {MessageId: "unexpectedNumber", Line: 1}, {MessageId: "unexpectedObject", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ }, { @@ -866,7 +875,9 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { AllowNumber: utils.Ref(false), AllowString: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedString", Line: 1}, {MessageId: "unexpectedNumber", Line: 1}, {MessageId: "unexpectedObject", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, }, /* Suggestions: conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean, conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean */ }, { @@ -875,7 +886,11 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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}, + {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 */ }, { @@ -884,7 +899,11 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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}, + {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 */ }, { @@ -893,7 +912,9 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ }, { @@ -902,7 +923,10 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ }, { @@ -911,7 +935,10 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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}, + {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ }, { @@ -920,7 +947,10 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { 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}, + {MessageId: "unexpectedNullish", Line: 1}, + {MessageId: "unexpectedNumber", Line: 1}, + {MessageId: "unexpectedString", Line: 1}, + {MessageId: "unexpectedObject", Line: 1}, }, /* Suggestions: conditionFixCompareZero, conditionFixCompareNaN, conditionFixCastBoolean, conditionFixCompareStringLength, conditionFixCompareEmptyString, conditionFixCastBoolean */ }, { @@ -1564,7 +1594,10 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { AllowNullableObject: utils.Ref(false), }, Errors: []rule_tester.InvalidTestCaseError{ - {MessageId: "unexpectedNullableObject", Line: 3}, {MessageId: "unexpectedNullableObject", Line: 4}, {MessageId: "unexpectedNullableObject", Line: 5}, {MessageId: "unexpectedNullableObject", Line: 6}, + {MessageId: "unexpectedNullableObject", Line: 3}, + {MessageId: "unexpectedNullableObject", Line: 4}, + {MessageId: "unexpectedNullableObject", Line: 5}, + {MessageId: "unexpectedNullableObject", Line: 6}, }, /* Suggestions: conditionFixCompareNullish, conditionFixCompareNullish, conditionFixCompareNullish, conditionFixCompareNullish */ }, { From de890342e71c51538338acc4f016c974b8302108 Mon Sep 17 00:00:00 2001 From: Noel Kim Date: Tue, 30 Sep 2025 12:11:21 +0900 Subject: [PATCH 23/23] oxlint-disable-next-line --- .../strict_boolean_expressions_test.go | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go index d83335cf..a86d173f 100644 --- a/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go +++ b/internal/rules/strict_boolean_expressions/strict_boolean_expressions_test.go @@ -472,19 +472,20 @@ func TestStrictBooleanExpressionsRule(t *testing.T) { AllowNullableEnum: utils.Ref(true), }, }, - // TODO: oxlint-disable-next-line should not check - // { - // Code: ` - // declare const x: string[] | null; - //// oxlint-disable-next-line - //if (x) { - //} - //`, - // TSConfig: "tsconfig.unstrict.json", - // Options: StrictBooleanExpressionsOptions{ - // AllowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: 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) {