Skip to content

Commit 1d555c8

Browse files
authored
Merge pull request #1070 from CodaFi/literally
Add Refactoring Actions to Reformat Integer Literals
2 parents 2c46bac + b1072c0 commit 1d555c8

File tree

6 files changed

+241
-2
lines changed

6 files changed

+241
-2
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import SwiftSyntax
2+
3+
/// Format an integer literal by inserting underscores at base-appropriate
4+
/// locations.
5+
///
6+
/// This pass will also clean up any errant underscores.
7+
///
8+
/// ## Before
9+
///
10+
/// ```swift
11+
/// 123456789
12+
/// 0xFFFFFFFFF
13+
/// 0b1_0_1_0
14+
/// ```
15+
///
16+
/// ## After
17+
///
18+
/// ```swift
19+
/// 123_456_789
20+
/// 0xF_FFFF_FFFF
21+
/// 0b1_010
22+
/// ```
23+
public struct AddSeparatorsToIntegerLiteral: RefactoringProvider {
24+
public static func refactor(syntax lit: IntegerLiteralExprSyntax, in context: Void) -> IntegerLiteralExprSyntax? {
25+
if lit.digits.text.contains("_") {
26+
guard let strippedLiteral = RemoveSeparatorsFromIntegerLiteral.refactor(syntax: lit) else {
27+
return nil
28+
}
29+
return self.addSeparators(to: strippedLiteral)
30+
} else {
31+
return self.addSeparators(to: lit)
32+
}
33+
}
34+
35+
private static func addSeparators(to lit: IntegerLiteralExprSyntax) -> IntegerLiteralExprSyntax {
36+
var formattedText = ""
37+
let (prefix, value) = lit.split()
38+
formattedText += prefix
39+
formattedText += value.byAddingGroupSeparators(at: lit.idealGroupSize)
40+
return lit
41+
.withDigits(lit.digits.withKind(.integerLiteral(formattedText)))
42+
}
43+
}
44+
45+
extension Substring {
46+
fileprivate func byAddingGroupSeparators(at interval: Int) -> String {
47+
var result = ""
48+
result.reserveCapacity(self.count)
49+
for (i, char) in self.filter({ $0 != "_" }).reversed().enumerated() {
50+
if i > 0 && i % interval == 0 {
51+
result.append("_")
52+
}
53+
result.append(char)
54+
}
55+
return String(result.reversed())
56+
}
57+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import SwiftSyntax
2+
3+
extension IntegerLiteralExprSyntax {
4+
public enum Radix: CaseIterable {
5+
case binary
6+
case octal
7+
case decimal
8+
case hex
9+
10+
public var size: Int {
11+
switch self {
12+
case .binary: return 2
13+
case .octal: return 8
14+
case .decimal: return 10
15+
case .hex: return 16
16+
}
17+
}
18+
}
19+
20+
public var radix: Radix {
21+
let text = self.digits.text
22+
if text.starts(with: "0b") {
23+
return .binary
24+
} else if text.starts(with: "0o") {
25+
return .octal
26+
} else if text.starts(with: "0x") {
27+
return .hex
28+
} else {
29+
return .decimal
30+
}
31+
}
32+
33+
/// Returns an (arbitrarily) "ideal" number of digits that should constitute
34+
/// a separator-delimited "group" in an integer literal.
35+
var idealGroupSize: Int {
36+
switch self.radix {
37+
case .binary: return 4
38+
case .octal: return 3
39+
case .decimal: return 3
40+
case .hex: return 4
41+
}
42+
}
43+
44+
/// Split the leading radix prefix from the value part of this integer literal.
45+
///
46+
/// ```
47+
/// 10 -> ("", "10")
48+
/// 0xFFFF -> ("0x", "FFFF")
49+
/// 0o77 -> ("0o", "77")
50+
/// 0b1010101 -> ("0b", "1010101")
51+
/// ```
52+
public func split() -> (prefix: String, value: Substring) {
53+
let text = self.digits.text
54+
switch self.radix {
55+
case .binary:
56+
return ("0b", text.dropFirst(2))
57+
case .octal:
58+
return ("0o", text.dropFirst(2))
59+
case .decimal:
60+
return ("", Substring(text))
61+
case .hex:
62+
return ("0x", text.dropFirst(2))
63+
}
64+
}
65+
}

Sources/SwiftRefactor/RefactoringProvider.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import SwiftSyntax
33
/// A type that transforms syntax to provide a (context-sensitive)
44
/// refactoring.
55
///
6-
/// A type conforming to the `RefactoringProvider` protocol defines the
6+
/// A type conforming to the `RefactoringProvider` protocol defines
77
/// a refactoring action against a family of Swift syntax trees.
88
///
99
/// Refactoring
@@ -43,7 +43,7 @@ import SwiftSyntax
4343
public protocol RefactoringProvider {
4444
/// The type of syntax this refactoring action accepts.
4545
associatedtype Input: SyntaxProtocol = SourceFileSyntax
46-
/// The type of syntax this refactorign action returns.
46+
/// The type of syntax this refactoring action returns.
4747
associatedtype Output: SyntaxProtocol = SourceFileSyntax
4848
/// Contextual information used by the refactoring action.
4949
associatedtype Context = Void
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import SwiftSyntax
2+
3+
/// Format an integer literal by removing any existing separators.
4+
///
5+
/// ## Before
6+
///
7+
/// ```swift
8+
/// 123_456_789
9+
/// 0xF_FFFF_FFFF
10+
/// ```
11+
/// ## After
12+
///
13+
/// ```swift
14+
/// 123456789
15+
/// 0xFFFFFFFFF
16+
/// ```
17+
public struct RemoveSeparatorsFromIntegerLiteral: RefactoringProvider {
18+
public static func refactor(syntax lit: IntegerLiteralExprSyntax, in context: Void) -> IntegerLiteralExprSyntax? {
19+
guard lit.digits.text.contains("_") else {
20+
return lit
21+
}
22+
let formattedText = lit.digits.text.filter({ $0 != "_" })
23+
return lit
24+
.withDigits(lit.digits.withKind(.integerLiteral(formattedText)))
25+
}
26+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftRefactor
14+
import SwiftSyntax
15+
import SwiftSyntaxBuilder
16+
17+
import XCTest
18+
import _SwiftSyntaxTestSupport
19+
20+
final class IntegerLiteralUtilitiesTest: XCTestCase {
21+
func testRadixMatching() {
22+
XCTAssertEqual(("0b1010101" as IntegerLiteralExpr).radix, .binary)
23+
XCTAssertEqual(("0xFF" as IntegerLiteralExpr).radix, .hex)
24+
XCTAssertEqual(("0o777" as IntegerLiteralExpr).radix, .octal)
25+
XCTAssertEqual(("42" as IntegerLiteralExpr).radix, .decimal)
26+
}
27+
28+
func testSplit() {
29+
XCTAssertEqual(("0b1010101" as IntegerLiteralExpr).split().prefix, "0b")
30+
XCTAssertEqual(("0xFF" as IntegerLiteralExpr).split().prefix, "0x")
31+
XCTAssertEqual(("0o777" as IntegerLiteralExpr).split().prefix, "0o")
32+
XCTAssertEqual(("42" as IntegerLiteralExpr).split().prefix, "")
33+
}
34+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftRefactor
14+
import SwiftSyntax
15+
import SwiftSyntaxBuilder
16+
17+
import XCTest
18+
import _SwiftSyntaxTestSupport
19+
20+
final class ReformatIntegerLiteralTest: XCTestCase {
21+
func testSeparatorPlacement() throws {
22+
let tests = [
23+
(#line, literal: "0b101010101" as IntegerLiteralExpr, expectation: "0b1_0101_0101" as IntegerLiteralExpr),
24+
(#line, literal: "0xFFFFFFFF" as IntegerLiteralExpr, expectation: "0xFFFF_FFFF" as IntegerLiteralExpr),
25+
(#line, literal: "0xFFFFF" as IntegerLiteralExpr, expectation: "0xF_FFFF" as IntegerLiteralExpr),
26+
(#line, literal: "0o777777" as IntegerLiteralExpr, expectation: "0o777_777" as IntegerLiteralExpr),
27+
(#line, literal: "424242424242" as IntegerLiteralExpr, expectation: "424_242_424_242" as IntegerLiteralExpr),
28+
(#line, literal: "100" as IntegerLiteralExpr, expectation: "100" as IntegerLiteralExpr),
29+
(#line, literal: "0xF_F_F_F_F_F_F_F" as IntegerLiteralExpr, expectation: "0xFFFF_FFFF" as IntegerLiteralExpr),
30+
(#line, literal: "0xFF_F_FF" as IntegerLiteralExpr, expectation: "0xF_FFFF" as IntegerLiteralExpr),
31+
(#line, literal: "0o7_77777" as IntegerLiteralExpr, expectation: "0o777_777" as IntegerLiteralExpr),
32+
(#line, literal: "4_24242424242" as IntegerLiteralExpr, expectation: "424_242_424_242" as IntegerLiteralExpr),
33+
]
34+
35+
for (line, literal, expectation) in tests {
36+
let refactored = try XCTUnwrap(AddSeparatorsToIntegerLiteral.refactor(syntax: literal))
37+
AssertStringsEqualWithDiff(refactored.description, expectation.description, line: UInt(line))
38+
}
39+
}
40+
41+
func testSeparatorRemoval() throws {
42+
let tests = [
43+
(#line, literal: "0b1_0_1_0_1_0_1_0_1" as IntegerLiteralExpr, expectation: "0b101010101" as IntegerLiteralExpr),
44+
(#line, literal: "0xFFF_F_FFFF" as IntegerLiteralExpr, expectation: "0xFFFFFFFF" as IntegerLiteralExpr),
45+
(#line, literal: "0xFF_FFF" as IntegerLiteralExpr, expectation: "0xFFFFF" as IntegerLiteralExpr),
46+
(#line, literal: "0o777_777" as IntegerLiteralExpr, expectation: "0o777777" as IntegerLiteralExpr),
47+
(#line, literal: "424_242_424_242" as IntegerLiteralExpr, expectation: "424242424242" as IntegerLiteralExpr),
48+
(#line, literal: "100" as IntegerLiteralExpr, expectation: "100" as IntegerLiteralExpr),
49+
]
50+
51+
for (line, literal, expectation) in tests {
52+
let refactored = try XCTUnwrap(RemoveSeparatorsFromIntegerLiteral.refactor(syntax: literal))
53+
AssertStringsEqualWithDiff(refactored.description, expectation.description, line: UInt(line))
54+
}
55+
}
56+
}
57+

0 commit comments

Comments
 (0)