Files
SwiftFormat/Sources/Rules/YodaConditions.swift

157 lines
5.9 KiB
Swift

//
// YodaConditions.swift
// SwiftFormat
//
// Created by Nick Lockwood on 3/9/19.
// Copyright © 2024 Nick Lockwood. All rights reserved.
//
import Foundation
public extension FormatRule {
/// Reorders "yoda conditions" where constant is placed on lhs of a comparison
static let yodaConditions = FormatRule(
help: "Prefer constant values to be on the right-hand-side of expressions.",
options: ["yoda-swap"]
) { formatter in
formatter.forEachToken { i, token in
guard case let .operator(op, .infix) = token,
let opIndex = ["==", "!=", "<", "<=", ">", ">="].firstIndex(of: op),
let prevIndex = formatter.index(of: .nonSpace, before: i),
formatter.isConstant(at: prevIndex), let startIndex = formatter.startOfValue(at: prevIndex),
!formatter.isOperator(at: formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: startIndex)),
let nextIndex = formatter.index(of: .nonSpace, after: i), !formatter.isConstant(at: nextIndex) ||
formatter.isOperator(at: formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: nextIndex)),
let endIndex = formatter.endOfExpression(at: nextIndex, upTo: [
.operator("&&", .infix), .operator("||", .infix),
.operator("?", .infix), .operator(":", .infix),
])
else {
return
}
let inverseOp = ["==", "!=", ">", ">=", "<", "<="][opIndex]
let expression = Array(formatter.tokens[nextIndex ... endIndex])
let constant = Array(formatter.tokens[startIndex ... prevIndex])
formatter.replaceTokens(in: nextIndex ... endIndex, with: constant)
formatter.replaceToken(at: i, with: .operator(inverseOp, .infix))
formatter.replaceTokens(in: startIndex ... prevIndex, with: expression)
}
} examples: {
"""
```diff
- if 5 == foo,
+ if foo == 5,
- nil != bar,
+ bar != nil,
- .default == baaz,
+ baaz == .default,
{ ... }
```
"""
}
}
extension Formatter {
func valuesInRangeAreConstant(_ range: CountableRange<Int>) -> Bool {
var index = index(of: .nonSpaceOrCommentOrLinebreak, in: range)
while var i = index {
switch tokens[i] {
case .startOfScope where isConstant(at: i):
guard let endIndex = self.index(of: .endOfScope, after: i) else {
return false
}
i = endIndex
fallthrough
case _ where isConstant(at: i), .delimiter(","), .delimiter(":"):
index = self.index(of: .nonSpaceOrCommentOrLinebreak, in: i + 1 ..< range.upperBound)
case .identifier:
guard let nextIndex =
self.index(of: .nonSpaceOrComment, in: i + 1 ..< range.upperBound),
tokens[nextIndex] == .delimiter(":")
else {
return false
}
// Identifier is a label
index = nextIndex
default:
return false
}
}
return true
}
func isConstant(at index: Int) -> Bool {
var index = index
while case .operator(_, .postfix) = tokens[index] {
index -= 1
}
guard let token = token(at: index) else {
return false
}
switch token {
case .number, .identifier("true"), .identifier("false"), .identifier("nil"):
return true
case .endOfScope("]"), .endOfScope(")"):
guard let startIndex = self.index(of: .startOfScope, before: index),
!isSubscriptOrFunctionCall(at: startIndex)
else {
return false
}
return valuesInRangeAreConstant(startIndex + 1 ..< index)
case .startOfScope("["), .startOfScope("("):
guard !isSubscriptOrFunctionCall(at: index),
let endIndex = self.index(of: .endOfScope, after: index)
else {
return false
}
return valuesInRangeAreConstant(index + 1 ..< endIndex)
case .startOfScope, .endOfScope:
// TODO: what if string contains interpolation?
return token.isStringDelimiter
case _ where options.yodaSwap == .literalsOnly:
// Don't treat .members as constant
return false
case .operator(".", .prefix) where self.token(at: index + 1)?.isIdentifier == true,
.identifier where self.token(at: index - 1) == .operator(".", .prefix) &&
self.token(at: index - 2) != .operator("\\", .prefix):
return true
default:
return false
}
}
func isOperator(at index: Int?) -> Bool {
guard let index else {
return false
}
switch tokens[index] {
// Discount operators with higher precedence than ==
case .operator("=", .infix),
.operator("&&", .infix), .operator("||", .infix),
.operator("?", .infix), .operator(":", .infix):
return false
case .operator(_, .infix), .keyword("as"), .keyword("is"):
return true
default:
return false
}
}
func startOfValue(at index: Int) -> Int? {
var index = index
while case .operator(_, .postfix)? = token(at: index) {
index -= 1
}
if case .endOfScope? = token(at: index) {
guard let i = self.index(of: .startOfScope, before: index) else {
return nil
}
index = i
}
while case .operator(_, .prefix)? = token(at: index - 1) {
index -= 1
}
return index
}
}