Skip to content

Commit fd1780d

Browse files
atamez31allevato
authored andcommitted
Implementation of BinaryOperatorWhitespace (swiftlang#53)
1 parent 58d3d99 commit fd1780d

File tree

2 files changed

+293
-15
lines changed

2 files changed

+293
-15
lines changed
+214-15
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,214 @@
1-
import Core
2-
import Foundation
3-
import SwiftSyntax
4-
5-
/// Exactly one space must appear before and after each binary operator token.
6-
///
7-
/// Lint: If an invalid number of spaces appear before or after a binary operator, a lint error is
8-
/// raised.
9-
///
10-
/// Format: All binary operators will have a single space before and after.
11-
///
12-
/// - SeeAlso: https://google.github.io/swift#horizontal-whitespace
13-
public final class OperatorWhitespace: SyntaxFormatRule {
14-
15-
}
1+
import Core
2+
import Foundation
3+
import SwiftSyntax
4+
5+
/// Exactly one space must appear before and after each binary operator token.
6+
///
7+
/// Lint: If an invalid number of spaces appear before or after a binary operator, a lint error is
8+
/// raised.
9+
///
10+
/// Format: All binary operators will have a single space before and after.
11+
///
12+
/// - SeeAlso: https://google.github.io/swift#horizontal-whitespace
13+
public final class OperatorWhitespace: SyntaxFormatRule {
14+
let rangeOperators = ["...", "..<", ">.."]
15+
public override func visit(_ node: ExprListSyntax) -> Syntax {
16+
var expressions = [ExprSyntax]()
17+
var hasInvalidNumSpaces = false
18+
for expr in node { expressions.append(expr) }
19+
20+
// Iterates through all the elements of the expression to find the position of
21+
// a binary operator and ensures that the spacing before and after are valid.
22+
for index in 0..<expressions.count - 1 {
23+
let expr = expressions[index]
24+
let nextExpr = expressions[index + 1]
25+
guard let exprToken = expr.lastToken else { continue }
26+
27+
// Ensures all binary operators have one space before and after them, except
28+
// for the rangeOperators.
29+
if expr is BinaryOperatorExprSyntax {
30+
// All range operators must have zero spaces surrounding them.
31+
if rangeOperators.contains(exprToken.text) {
32+
expressions[index - 1] = expressionWithoutTrailingSpaces(
33+
expr: expressions[index - 1],
34+
invalidNumSpaces: &hasInvalidNumSpaces
35+
)
36+
37+
expressions[index] = expressionWithoutTrailingSpaces(
38+
expr: expr,
39+
invalidNumSpaces: &hasInvalidNumSpaces
40+
)
41+
42+
if exprToken.tokenKind == .spacedBinaryOperator(exprToken.text) &&
43+
nextExpr is PrefixOperatorExprSyntax {
44+
hasInvalidNumSpaces = true
45+
expressions[index + 1] = addParenthesisToElement(expressions[index + 1])
46+
}
47+
}
48+
else {
49+
expressions[index - 1] = exprWithOneTrailingSpace(
50+
expr: expressions[index - 1],
51+
invalidNumSpaces: &hasInvalidNumSpaces
52+
)
53+
expressions[index] = exprWithOneTrailingSpace(
54+
expr: expr,
55+
invalidNumSpaces: &hasInvalidNumSpaces
56+
)
57+
}
58+
}
59+
}
60+
return hasInvalidNumSpaces ? SyntaxFactory.makeExprList(expressions) : node
61+
}
62+
63+
public override func visit(_ node: CompositionTypeElementListSyntax) -> Syntax {
64+
var elements = [CompositionTypeElementSyntax]()
65+
var hasInvalidNumSpaces = false
66+
67+
for element in node {
68+
// Ensures that the ampersand of the composition has one space before and after it.
69+
if compositeHasInvalidNumberOfSpaces(element) {
70+
hasInvalidNumSpaces = true
71+
72+
let elementWithOneTrailingSpace = replaceTrivia(
73+
on: element,
74+
token: element.ampersand!.previousToken!,
75+
trailingTrivia: element.ampersand!.previousToken!.trailingTrivia.withOneTrailingSpace()
76+
) as! CompositionTypeElementSyntax
77+
78+
let ampersandWithOneTrailingSpace = replaceTrivia(
79+
on: element.ampersand!,
80+
token: element.ampersand!,
81+
trailingTrivia: element.ampersand!.trailingTrivia.withOneTrailingSpace()
82+
) as! TokenSyntax
83+
84+
let replacedElement = SyntaxFactory.makeCompositionTypeElement(
85+
type: elementWithOneTrailingSpace.type,
86+
ampersand: ampersandWithOneTrailingSpace)
87+
88+
elements.append(replacedElement)
89+
}
90+
else {
91+
elements.append(element)
92+
}
93+
}
94+
return hasInvalidNumSpaces ? SyntaxFactory.makeCompositionTypeElementList(elements) : node
95+
}
96+
97+
/// Indicates ampersand of the given composition doesn't have one space after and before it.
98+
func compositeHasInvalidNumberOfSpaces(_ element: CompositionTypeElementSyntax) -> Bool {
99+
guard let elementAmpersand = element.ampersand else { return false }
100+
guard let prevToken = elementAmpersand.previousToken else { return false }
101+
102+
switch elementAmpersand.tokenKind {
103+
case .unspacedBinaryOperator(elementAmpersand.text), .postfixOperator(elementAmpersand.text):
104+
return true
105+
case .spacedBinaryOperator(elementAmpersand.text):
106+
return elementAmpersand.trailingTrivia.numberOfSpaces > 1 ||
107+
prevToken.trailingTrivia.numberOfSpaces > 1 ? true : false
108+
default:
109+
return false
110+
}
111+
}
112+
113+
/// Ensures that the trailing trivia of the given expression doesn't contain
114+
/// any spaces.
115+
func expressionWithoutTrailingSpaces(
116+
expr: ExprSyntax,
117+
invalidNumSpaces: inout Bool
118+
) -> ExprSyntax {
119+
guard let exprTrailingTrivia = expr.trailingTrivia else { return expr }
120+
guard let exprLastToken = expr.lastToken else { return expr }
121+
let numSpaces = exprTrailingTrivia.numberOfSpaces
122+
123+
if numSpaces > 0 {
124+
invalidNumSpaces = true
125+
let replacedExpression = replaceTrivia(
126+
on: expr,
127+
token: exprLastToken,
128+
trailingTrivia: exprTrailingTrivia.withoutSpaces()
129+
) as! ExprSyntax
130+
131+
diagnose(
132+
.removesSpacesOfRangeOperator(count: numSpaces, tokenText: exprLastToken.text),
133+
on: expr
134+
)
135+
136+
return exprLastToken.tokenKind == .spacedBinaryOperator(exprLastToken.text) ?
137+
changeSpacedOperatorToUnspaced(replacedExpression) : replacedExpression
138+
}
139+
return expr
140+
}
141+
142+
/// Ensures that the trailing trivia of the given expression only has one
143+
/// trailing space.
144+
func exprWithOneTrailingSpace(
145+
expr: ExprSyntax,
146+
invalidNumSpaces: inout Bool
147+
) -> ExprSyntax {
148+
guard let elementTrailingTrivia = expr.trailingTrivia else { return expr }
149+
guard let exprLastToken = expr.lastToken else { return expr }
150+
if elementTrailingTrivia.numberOfSpaces != 1 {
151+
invalidNumSpaces = true
152+
let replacedExpr = replaceTrivia(
153+
on: expr,
154+
token: exprLastToken,
155+
trailingTrivia: elementTrailingTrivia.withOneTrailingSpace()
156+
) as! ExprSyntax
157+
158+
diagnose(.addSpaceAfterOperator(tokenText: exprLastToken.text), on: expr)
159+
return exprLastToken.tokenKind == .unspacedBinaryOperator(exprLastToken.text) ?
160+
changeSpacedOperatorToUnspaced(replacedExpr) : replacedExpr
161+
}
162+
return expr
163+
}
164+
165+
/// Given an BinaryOperatorExprSyntax replace the operator type from spacedBinaryOperator
166+
/// to unspacedBinaryOperator.
167+
func changeSpacedOperatorToUnspaced(_ expr: ExprSyntax) -> ExprSyntax {
168+
guard let lastToken = expr.lastToken else { return expr }
169+
let unspacedExpr = SyntaxFactory.makeBinaryOperatorExpr(
170+
operatorToken: lastToken.withKind(.unspacedBinaryOperator(lastToken.text))
171+
)
172+
return unspacedExpr
173+
}
174+
175+
/// Given an BinaryOperatorExprSyntax replace the operator type from unspacedBinaryOperator
176+
/// to spacedBinaryOperator.
177+
func changeUnspacedOperatorToSpaced(_ expr: ExprSyntax) -> ExprSyntax {
178+
guard let lastToken = expr.lastToken else { return expr }
179+
let unspacedExpr = SyntaxFactory.makeBinaryOperatorExpr(
180+
operatorToken: lastToken.withKind(.spacedBinaryOperator(lastToken.text))
181+
)
182+
return unspacedExpr
183+
}
184+
185+
/// Converts the given expression to a Tuple in order to wrap it with parenthesis.
186+
func addParenthesisToElement(_ element: ExprSyntax) -> TupleExprSyntax {
187+
let expr = replaceTrivia(
188+
on: element,
189+
token: element.lastToken!,
190+
trailingTrivia: element.trailingTrivia!.withoutSpaces()
191+
) as! ExprSyntax
192+
let leftParen = SyntaxFactory.makeLeftParenToken()
193+
let rightParen = SyntaxFactory.makeRightParenToken().withOneTrailingSpace()
194+
let tupleElem = SyntaxFactory.makeBlankTupleElement().withExpression(expr)
195+
let tupleList = SyntaxFactory.makeTupleElementList([tupleElem])
196+
197+
return SyntaxFactory.makeTupleExpr(
198+
leftParen: leftParen,
199+
elementList: tupleList,
200+
rightParen: rightParen
201+
)
202+
}
203+
}
204+
205+
extension Diagnostic.Message {
206+
static func removesSpacesOfRangeOperator(count: Int, tokenText: String) -> Diagnostic.Message {
207+
let ending = count == 1 ? "" : "s"
208+
return Diagnostic.Message(.warning, "remove \(count) space\(ending) after the '\(tokenText)'")
209+
}
210+
211+
static func addSpaceAfterOperator(tokenText: String) -> Diagnostic.Message {
212+
return Diagnostic.Message(.warning, "place only one space after the '\(tokenText)'")
213+
}
214+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import SwiftSyntax
2+
import XCTest
3+
4+
@testable import Rules
5+
6+
public class OperatorWhitespaceTests: DiagnosingTestCase {
7+
public func testInvalidOperatorWhitespace() {
8+
XCTAssertFormatting(
9+
OperatorWhitespace.self,
10+
input: """
11+
var a = -10 + 3
12+
var e = 1 + 2 * (10 / 7)
13+
a*=2
14+
let b: UInt8 = 4
15+
b << 1
16+
b>>=2
17+
let c: UInt8 = 0b00001111
18+
let d = ~c
19+
struct AnyEquatable<Wrapped : Equatable> : Equatable {}
20+
func foo(param: x & y) {}
21+
""",
22+
expected: """
23+
var a = -10 + 3
24+
var e = 1 + 2 * (10 / 7)
25+
a *= 2
26+
let b: UInt8 = 4
27+
b << 1
28+
b >>= 2
29+
let c: UInt8 = 0b00001111
30+
let d = ~c
31+
struct AnyEquatable<Wrapped : Equatable> : Equatable {}
32+
func foo(param: x & y) {}
33+
""")
34+
}
35+
36+
public func testRangeOperators() {
37+
XCTAssertFormatting(
38+
OperatorWhitespace.self,
39+
input: """
40+
for number in 1 ... 5 {}
41+
for number in -10 ... -5 {}
42+
var elements = [1,2,3]
43+
let rangeA = elements.count ... 10
44+
for number in 1...5 {}
45+
""",
46+
expected: """
47+
for number in 1...5 {}
48+
for number in -10...(-5) {}
49+
var elements = [1,2,3]
50+
let rangeA = elements.count...10
51+
for number in 1...5 {}
52+
""")
53+
}
54+
55+
public func testCompositeTypes() {
56+
XCTAssertFormatting(
57+
OperatorWhitespace.self,
58+
input: """
59+
func foo(param: x & y) {}
60+
func foo(param: x&y) {}
61+
func foo(param: x & y) {}
62+
func foo(param: x& y) {}
63+
""",
64+
expected: """
65+
func foo(param: x & y) {}
66+
func foo(param: x & y) {}
67+
func foo(param: x & y) {}
68+
func foo(param: x & y) {}
69+
""")
70+
}
71+
72+
#if !os(macOS)
73+
static let allTests = [
74+
OperatorWhitespaceTests.testInvalidOperatorWhitespace,
75+
OperatorWhitespaceTests.testRangeOperators,
76+
OperatorWhitespaceTests.testCompositeTypes
77+
]
78+
#endif
79+
}

0 commit comments

Comments
 (0)