diff --git a/Rules.md b/Rules.md
index 5af47190..afeb5ec9 100644
--- a/Rules.md
+++ b/Rules.md
@@ -122,6 +122,7 @@
* [noExplicitOwnership](#noExplicitOwnership)
* [noGuardInTests](#noGuardInTests)
* [organizeDeclarations](#organizeDeclarations)
+* [preferExplicitFalse](#preferExplicitFalse)
* [preferFinalClasses](#preferFinalClasses)
* [preferSwiftTesting](#preferSwiftTesting)
* [privateStateVariables](#privateStateVariables)
@@ -2002,6 +2003,26 @@ Prefer `count(where:)` over `filter(_:).count`.
+## preferExplicitFalse
+
+Prefer `== false` over `!` prefix negation.
+
+
+Examples
+
+```diff
+- if !flag {
++ if flag == false {
+```
+
+```diff
+- guard !array.isEmpty else { return }
++ guard array.isEmpty == false else { return }
+```
+
+
+
+
## preferFinalClasses
Prefer defining `final` classes. To suppress this rule, add "Base" to the class name, add a doc comment with mentioning "base class" or "subclass", make the class `open`, or use a `// swiftformat:disable:next preferFinalClasses` directive.
diff --git a/Sources/RuleRegistry.generated.swift b/Sources/RuleRegistry.generated.swift
index 0e82d801..33e96b4a 100644
--- a/Sources/RuleRegistry.generated.swift
+++ b/Sources/RuleRegistry.generated.swift
@@ -62,6 +62,7 @@ let ruleRegistry: [String: FormatRule] = [
"opaqueGenericParameters": .opaqueGenericParameters,
"organizeDeclarations": .organizeDeclarations,
"preferCountWhere": .preferCountWhere,
+ "preferExplicitFalse": .preferExplicitFalse,
"preferFinalClasses": .preferFinalClasses,
"preferForLoop": .preferForLoop,
"preferKeyPath": .preferKeyPath,
diff --git a/Sources/Rules/PreferExplicitFalse.swift b/Sources/Rules/PreferExplicitFalse.swift
new file mode 100644
index 00000000..0b27f539
--- /dev/null
+++ b/Sources/Rules/PreferExplicitFalse.swift
@@ -0,0 +1,124 @@
+//
+// PreferExplicitFalse.swift
+// SwiftFormat
+//
+// Created by KYHyeon on 02/08/2026.
+// Copyright © 2026 Nick Lockwood. All rights reserved.
+//
+
+import Foundation
+
+public extension FormatRule {
+ /// Convert prefix `!` negation to explicit `== false` comparison.
+ /// This improves readability for teams who find the `!` prefix easy to miss.
+ static let preferExplicitFalse = FormatRule(
+ help: "Prefer `== false` over `!` prefix negation.",
+ disabledByDefault: true
+ ) { formatter in
+ formatter.forEach(.operator("!", .prefix)) { notIndex, _ in
+ guard let operandStart = formatter.index(
+ of: .nonSpaceOrCommentOrLinebreak,
+ after: notIndex
+ ) else {
+ return
+ }
+
+ if formatter.tokens[operandStart].isOperator(ofType: .prefix) {
+ return
+ }
+
+ if formatter.currentScope(at: notIndex) == .startOfScope("#if") {
+ return
+ }
+
+ // Skip if adjacent to a comparison or casting operator —
+ // inserting `== false` would create a non-associative chain
+ // or change precedence. e.g., `a == !b` → `a == b == false`
+ if let prevIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: notIndex),
+ formatter.isComparisonOrCastingOperator(at: prevIndex)
+ {
+ return
+ }
+
+ guard let operandEnd = formatter.endOfPrefixOperand(
+ at: operandStart
+ ) else {
+ return
+ }
+
+ if let nextIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: operandEnd),
+ formatter.isComparisonOrCastingOperator(at: nextIndex)
+ {
+ return
+ }
+
+ formatter.insert([
+ .space(" "),
+ .operator("==", .infix),
+ .space(" "),
+ .identifier("false"),
+ ], at: operandEnd + 1)
+
+ formatter.removeToken(at: notIndex)
+ }
+ } examples: {
+ """
+ ```diff
+ - if !flag {
+ + if flag == false {
+ ```
+
+ ```diff
+ - guard !array.isEmpty else { return }
+ + guard array.isEmpty == false else { return }
+ ```
+ """
+ }
+}
+
+// MARK: - Helpers
+
+extension Formatter {
+ static let comparisonOperators: Set = [
+ "==", "!=", "===", "!==", "~=", "<", ">", "<=", ">=",
+ ]
+
+ /// Finds the end of the postfix expression starting at `index`, which is the
+ /// first non-space token after a prefix `!`. Uses `parseExpressionRange` for
+ /// expression parsing, then finds the boundary before any infix operators,
+ /// since the prefix `!` only binds to the immediate postfix expression.
+ func endOfPrefixOperand(at index: Int) -> Int? {
+ guard let expressionRange = parseExpressionRange(
+ startingAt: index
+ ) else {
+ return nil
+ }
+
+ // parseExpressionRange includes infix operators in the expression range,
+ // but `!` binds tighter than any infix operator. Find the earliest
+ // infix operator (excluding member access `.`) or `is`/`as` keyword.
+ let searchRange = index ..< expressionRange.upperBound + 1
+ let infixIndex = self.index(in: searchRange, where: {
+ $0.isOperator(ofType: .infix) && $0 != .operator(".", .infix)
+ })
+ let isIndex = self.index(of: .keyword("is"), in: index ... expressionRange.upperBound)
+ let asIndex = self.index(of: .keyword("as"), in: index ... expressionRange.upperBound)
+
+ if let breakIndex = [infixIndex, isIndex, asIndex].compactMap({ $0 }).min() {
+ return self.index(of: .nonSpaceOrCommentOrLinebreak, before: breakIndex)
+ }
+
+ return expressionRange.upperBound
+ }
+
+ /// Whether the token at `index` is a comparison operator (`==`, `!=`, etc.)
+ /// or a casting keyword (`is`, `as`) — operators that would conflict with
+ /// an inserted `== false`.
+ func isComparisonOrCastingOperator(at index: Int) -> Bool {
+ let token = tokens[index]
+ if case let .operator(op, .infix) = token {
+ return Self.comparisonOperators.contains(op)
+ }
+ return token == .keyword("is") || token == .keyword("as")
+ }
+}
diff --git a/Tests/Rules/PreferExplicitFalseTests.swift b/Tests/Rules/PreferExplicitFalseTests.swift
new file mode 100644
index 00000000..d5716b71
--- /dev/null
+++ b/Tests/Rules/PreferExplicitFalseTests.swift
@@ -0,0 +1,487 @@
+//
+// PreferExplicitFalseTests.swift
+// SwiftFormatTests
+//
+// Created by KYHyeon on 02/08/2026.
+// Copyright © 2026 Nick Lockwood. All rights reserved.
+//
+
+import XCTest
+@testable import SwiftFormat
+
+final class PreferExplicitFalseTests: XCTestCase {
+ func testBasicNegation() {
+ let input = """
+ if !flag {
+ print("false")
+ }
+ """
+ let output = """
+ if flag == false {
+ print("false")
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testGuardNegation() {
+ let input = """
+ guard !array.isEmpty else { return }
+ """
+ let output = """
+ guard array.isEmpty == false else { return }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse,
+ exclude: [.wrapConditionalBodies])
+ }
+
+ func testWhileNegation() {
+ let input = """
+ while !finished {
+ doWork()
+ }
+ """
+ let output = """
+ while finished == false {
+ doWork()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testPropertyNegation() {
+ let input = """
+ if !view.isHidden {
+ view.show()
+ }
+ """
+ let output = """
+ if view.isHidden == false {
+ view.show()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testFunctionCallNegation() {
+ let input = """
+ if !foo.bar() {
+ handleFalse()
+ }
+ """
+ let output = """
+ if foo.bar() == false {
+ handleFalse()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testMethodCallNegation() {
+ let input = """
+ if !array.contains(value) {
+ addValue(value)
+ }
+ """
+ let output = """
+ if array.contains(value) == false {
+ addValue(value)
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testParenthesizedExpressionNegation() {
+ let input = """
+ if !(a && b) {
+ handleBothFalse()
+ }
+ """
+ let output = """
+ if (a && b) == false {
+ handleBothFalse()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testComplexExpressionNegation() {
+ let input = """
+ if !(foo.bar() && baz.qux()) {
+ handleComplexFalse()
+ }
+ """
+ let output = """
+ if (foo.bar() && baz.qux()) == false {
+ handleComplexFalse()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNestedPropertyNegation() {
+ let input = """
+ if !self.view.subviews.isEmpty {
+ addSubviews()
+ }
+ """
+ let output = """
+ if self.view.subviews.isEmpty == false {
+ addSubviews()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse,
+ exclude: [.redundantSelf])
+ }
+
+ func testChainedMethodCallNegation() {
+ let input = """
+ if !foo.bar().baz() {
+ handleChainedFalse()
+ }
+ """
+ let output = """
+ if foo.bar().baz() == false {
+ handleChainedFalse()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testMultipleNegationsInSameLine() {
+ let input = """
+ if !a && !b {
+ handleBothFalse()
+ }
+ """
+ let output = """
+ if a == false && b == false {
+ handleBothFalse()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse,
+ exclude: [.andOperator])
+ }
+
+ func testNegationInTernary() {
+ let input = """
+ let result = !condition ? "false" : "true"
+ """
+ let output = """
+ let result = condition == false ? "false" : "true"
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationInReturnStatement() {
+ let input = """
+ func check() -> Bool {
+ return !isValid
+ }
+ """
+ let output = """
+ func check() -> Bool {
+ return isValid == false
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationInAssignment() {
+ let input = """
+ let isFalse = !someCondition
+ """
+ let output = """
+ let isFalse = someCondition == false
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationInFunctionParameter() {
+ let input = """
+ processData(data: !isProcessed)
+ """
+ let output = """
+ processData(data: isProcessed == false)
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationWithComments() {
+ let input = """
+ if !flag { // check if false
+ doSomething()
+ }
+ """
+ let output = """
+ if flag == false { // check if false
+ doSomething()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForPostfixNot() {
+ let input = """
+ let value = optional!
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForComparisonOperators() {
+ let input = """
+ if a != b {
+ doSomething()
+ }
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForExistingEqualFalse() {
+ let input = """
+ if flag == false {
+ doSomething()
+ }
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForExistingEqualTrue() {
+ let input = """
+ if flag == true {
+ doSomething()
+ }
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForOptionalBool() {
+ let input = """
+ if optionalBool! {
+ doSomething()
+ }
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForBinaryNot() {
+ let input = """
+ let result = ~value
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testSubscriptNegation() {
+ let input = """
+ if !array[0] {
+ processFirstElement()
+ }
+ """
+ let output = """
+ if array[0] == false {
+ processFirstElement()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testForceUnwrapPropertyNegation() {
+ let input = """
+ if !foo!.isValid {
+ handleInvalidFoo()
+ }
+ """
+ let output = """
+ if foo!.isValid == false {
+ handleInvalidFoo()
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationInClosure() {
+ let input = """
+ let closure = {
+ if !condition {
+ return false
+ }
+ return true
+ }
+ """
+ let output = """
+ let closure = {
+ if condition == false {
+ return false
+ }
+ return true
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse, exclude: [.wrapFunctionBodies])
+ }
+
+ func testNegationInSwitchCase() {
+ let input = """
+ switch value {
+ case let x where !x.isValid:
+ handleInvalid(x)
+ default:
+ break
+ }
+ """
+ let output = """
+ switch value {
+ case let x where x.isValid == false:
+ handleInvalid(x)
+ default:
+ break
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationInWhereClause() {
+ let input = """
+ for item in items where !item.isProcessed {
+ process(item)
+ }
+ """
+ let output = """
+ for item in items where item.isProcessed == false {
+ process(item)
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationInComputedProperty() {
+ let input = """
+ var isEmpty: Bool {
+ return !items.isEmpty
+ }
+ """
+ let output = """
+ var isEmpty: Bool {
+ return items.isEmpty == false
+ }
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationInArrayLiteral() {
+ let input = """
+ let array = [!a, !b, !c]
+ """
+ let output = """
+ let array = [a == false, b == false, c == false]
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNegationInDictionaryLiteral() {
+ let input = """
+ let dict = ["a": !value, "b": !other]
+ """
+ let output = """
+ let dict = ["a": value == false, "b": other == false]
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testClosureArgumentNegation() {
+ let input = """
+ let result = !items.contains(where: { $0.isValid })
+ """
+ let output = """
+ let result = items.contains(where: { $0.isValid }) == false
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testTrailingClosureNegation() {
+ let input = """
+ let result = !myArray.contains {
+ $0 == value
+ }
+ """
+ let output = """
+ let result = myArray.contains {
+ $0 == value
+ } == false
+ """
+ testFormatting(for: input, output, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForNegationBeforeEquals() {
+ let input = """
+ print(!a == b)
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForNegationBeforeNotEquals() {
+ let input = """
+ print(!a != b)
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForNegationAfterEquals() {
+ let input = """
+ print(a == !b)
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForNegationAfterNotEquals() {
+ let input = """
+ print(a != !b)
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForPreprocessorDirective() {
+ let input = """
+ #if !DEBUG
+ #error("Not supported")
+ #endif
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse, exclude: [.indent])
+ }
+
+ func testNoChangeForPreprocessorCanImport() {
+ let input = """
+ #if !canImport(UIKit)
+ #error("UIKit required")
+ #endif
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse, exclude: [.indent])
+ }
+
+ func testNoChangeForNegationBeforeIs() {
+ let input = """
+ print(!foo is Bar)
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testNoChangeForNegationBeforeAs() {
+ let input = """
+ print(!foo as? Bar)
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testForceUnwrappedNegationBeforeEquals() {
+ let input = """
+ print(!foo! == bar)
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+
+ func testDoubleNegationAfterEquals() {
+ let input = """
+ print(a == !!b)
+ """
+ testFormatting(for: input, rule: .preferExplicitFalse)
+ }
+}
diff --git a/Tests/XCTestCase+testFormatting.swift b/Tests/XCTestCase+testFormatting.swift
index 7dedaac3..4cf05e73 100644
--- a/Tests/XCTestCase+testFormatting.swift
+++ b/Tests/XCTestCase+testFormatting.swift
@@ -83,6 +83,7 @@ extension XCTestCase {
.blockComments,
.unusedPrivateDeclarations,
.preferFinalClasses,
+ .preferExplicitFalse,
]
let exclude = exclude + defaultExclusions.filter { !rules.contains($0) }
let formatResult: (output: String, changes: [SwiftFormat.Formatter.Change])