Files
SwiftFormat/Sources/Rules/SpaceAroundOperators.swift

172 lines
7.6 KiB
Swift

//
// SpaceAroundOperators.swift
// SwiftFormat
//
// Created by Nick Lockwood on 8/22/16.
// Copyright © 2024 Nick Lockwood. All rights reserved.
//
import Foundation
public extension FormatRule {
/// Implement the following rules with respect to the spacing around operators:
/// * Infix operators are separated from their operands by a space on either
/// side. Does not affect prefix/postfix operators, as required by syntax.
/// * Delimiters, such as commas and colons, are consistently followed by a
/// single space, unless it appears at the end of a line, and is not
/// preceded by a space, unless it appears at the beginning of a line.
static let spaceAroundOperators = FormatRule(
help: "Add or remove space around operators or delimiters.",
options: ["operator-func", "no-space-operators", "ranges", "type-delimiter"]
) { formatter in
formatter.forEachToken { i, token in
switch token {
case .operator(_, .none):
switch formatter.token(at: i + 1) {
case nil, .linebreak?, .endOfScope?, .operator?, .delimiter?,
.startOfScope("(")? where formatter.options.spaceAroundOperatorDeclarations != .insert:
break
case .space?:
switch formatter.next(.nonSpaceOrLinebreak, after: i) {
case nil, .linebreak?, .endOfScope?, .delimiter?,
.startOfScope("(")? where formatter.options.spaceAroundOperatorDeclarations == .remove:
formatter.removeToken(at: i + 1)
default:
break
}
default:
formatter.insert(.space(" "), at: i + 1)
}
case let token where token.isUnwrapOperator:
if let prevToken = formatter.token(at: i - 1),
formatter.token(at: i + 1)?.isSpaceOrLinebreak == false,
[.keyword("as"), .keyword("try")].contains(prevToken)
{
formatter.insert(.space(" "), at: i + 1)
}
case .operator("::", _):
if formatter.token(at: i + 1)?.isSpace == true {
formatter.removeToken(at: i + 1)
}
if formatter.token(at: i - 1)?.isSpace == true {
formatter.removeToken(at: i - 1)
}
case .operator(".", _):
if formatter.token(at: i + 1)?.isSpace == true {
formatter.removeToken(at: i + 1)
}
guard let prevIndex = formatter.index(of: .nonSpace, before: i) else {
formatter.removeTokens(in: 0 ..< i)
break
}
let spaceRequired: Bool
switch formatter.tokens[prevIndex] {
case .operator(_, .infix), .startOfScope:
return
case let token where [.identifier("unsafe")].contains(token):
return // `unsafe` is contextual, so leave existing spacing unchanged.
case let token where token.isUnwrapOperator:
if let prevToken = formatter.last(.nonSpace, before: prevIndex),
[.keyword("as"), .keyword("try")].contains(prevToken)
{
spaceRequired = true
} else {
spaceRequired = false
}
case .operator(_, .prefix):
spaceRequired = false
case let token:
spaceRequired = !token.isAttribute && !token.isLvalue
}
if formatter.token(at: i - 1)?.isSpaceOrLinebreak == true {
if !spaceRequired {
formatter.removeToken(at: i - 1)
}
} else if spaceRequired {
formatter.insertSpace(" ", at: i)
}
case .operator("?", .infix):
break // Spacing around ternary ? is not optional
case let .operator(name, .infix) where formatter.options.noSpaceOperators.contains(name) ||
(formatter.options.spaceAroundRangeOperators == .remove && token.isRangeOperator):
if formatter.token(at: i + 1)?.isSpace == true,
formatter.token(at: i - 1)?.isSpace == true,
let nextToken = formatter.next(.nonSpace, after: i),
!nextToken.isCommentOrLinebreak, !nextToken.isOperator,
let prevToken = formatter.last(.nonSpace, before: i),
!prevToken.isCommentOrLinebreak, !prevToken.isOperator || prevToken.isUnwrapOperator
{
formatter.removeToken(at: i + 1)
formatter.removeToken(at: i - 1)
}
case .operator(_, .infix):
if token.isRangeOperator, formatter.options.spaceAroundRangeOperators != .insert {
break
}
if formatter.token(at: i + 1)?.isSpaceOrLinebreak == false {
formatter.insert(.space(" "), at: i + 1)
}
if formatter.token(at: i - 1)?.isSpaceOrLinebreak == false {
formatter.insert(.space(" "), at: i)
}
case .operator(_, .prefix):
if let prevIndex = formatter.index(of: .nonSpace, before: i, if: {
[.startOfScope("["), .startOfScope("("), .startOfScope("<")].contains($0)
}) {
formatter.removeTokens(in: prevIndex + 1 ..< i)
} else if let prevToken = formatter.token(at: i - 1),
!prevToken.isSpaceOrLinebreak, !prevToken.isOperator
{
formatter.insert(.space(" "), at: i)
}
case .delimiter(":"):
// TODO: make this check more robust, and remove redundant space
if formatter.token(at: i + 1)?.isIdentifier == true,
formatter.token(at: i + 2) == .delimiter(":")
{
// It's a selector
break
}
fallthrough
case .operator(_, .postfix), .delimiter(","), .delimiter(";"), .startOfScope(":"):
switch formatter.token(at: i + 1) {
case nil, .space?, .linebreak?, .endOfScope?, .operator?, .delimiter?:
break
default:
// Ensure there is a space after the token
formatter.insert(.space(" "), at: i + 1)
}
let spaceBeforeToken = formatter.token(at: i - 1)?.isSpace == true
&& formatter.token(at: i - 2)?.isLinebreak == false
if spaceBeforeToken, formatter.options.typeDelimiterSpacing == .spaceAfter {
// Remove space before the token
formatter.removeToken(at: i - 1)
} else if !spaceBeforeToken, formatter.options.typeDelimiterSpacing == .spaced {
formatter.insertSpace(" ", at: i)
}
default:
break
}
}
} examples: {
"""
```diff
- foo . bar()
+ foo.bar()
```
```diff
- a+b+c
+ a + b + c
```
```diff
- func ==(lhs: Int, rhs: Int) -> Bool
+ func == (lhs: Int, rhs: Int) -> Bool
```
"""
}
}