Add rule to capitalize acronyms (#943)

This commit is contained in:
Cal Stephens
2021-11-06 16:09:02 +00:00
committed by Nick Lockwood
parent 9d91b00749
commit b128fe83ba
8 changed files with 211 additions and 1 deletions
+27
View File
@@ -1,5 +1,6 @@
# Rules
* [acronyms](#acronyms)
* [andOperator](#andOperator)
* [anyObjectProtocol](#anyObjectProtocol)
* [assertionFailures](#assertionFailures)
@@ -79,6 +80,32 @@
----------
## acronyms
Capitalizes acronyms when the first character is capitalized.
Option | Description
--- | ---
`--acronyms` | Acronyms to auto-capitalize. Defaults to "ID,URL,UUID".
<details>
<summary>Examples</summary>
```diff
- let destinationUrl: URL
- let urlRouter: UrlRouter
- let screenId: String
- let entityUuid: UUID
+ let destinationURL: URL
+ let urlRouter: URLRouter
+ let screenID: String
+ let entityUUID: UUID
```
</details>
<br/>
## andOperator
Prefer comma over `&&` in `if`, `guard` or `while` conditions.
+14
View File
@@ -1260,4 +1260,18 @@ private struct Examples {
+ preconditionFailure("message", 2, 1)
```
"""
let acronyms = """
```diff
- let destinationUrl: URL
- let urlRouter: UrlRouter
- let screenId: String
- let entityUuid: UUID
+ let destinationURL: URL
+ let urlRouter: URLRouter
+ let screenID: String
+ let entityUUID: UUID
```
"""
}
+27
View File
@@ -600,3 +600,30 @@ public extension Formatter {
return .linebreak(options.linebreak, lineNumber)
}
}
extension String {
/// https://stackoverflow.com/a/32306142
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<Index>] {
var result: [Range<Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex, let range = self[startIndex...].range(of: string, options: options) {
result.append(range)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return result
}
}
// `Swift.Character.isUppercase` isn't available until Swift 5.0+ / Xcode 10.2+
#if !swift(>=5.0)
extension Character {
var isUppercase: Bool {
return String(self).uppercased() == String(self)
}
var isWhitespace: Bool {
return String(self).trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}
#endif
+6
View File
@@ -816,6 +816,12 @@ struct _Descriptors {
help: "Empty braces: \"no-space\" (default), \"spaced\" or \"linebreak\"",
keyPath: \.emptyBracesSpacing
)
let acronyms = OptionDescriptor(
argumentName: "acronyms",
displayName: "Acronyms",
help: "Acronyms to auto-capitalize. Defaults to \"ID,URL,UUID\".",
keyPath: \.acronyms
)
// MARK: - Internal
+3
View File
@@ -381,6 +381,7 @@ public struct FormatOptions: CustomStringConvertible {
public var extensionACLPlacement: ExtensionACLPlacement
public var redundantType: RedundantType
public var emptyBracesSpacing: EmptyBracesSpacing
public var acronyms: Set<String>
// Deprecated
public var indentComments: Bool
@@ -461,6 +462,7 @@ public struct FormatOptions: CustomStringConvertible {
extensionACLPlacement: ExtensionACLPlacement = .onExtension,
redundantType: RedundantType = .inferLocalsOnly,
emptyBracesSpacing: EmptyBracesSpacing = .noSpace,
acronyms: Set<String> = ["ID", "URL", "UUID"],
// Doesn't really belong here, but hard to put elsewhere
fragment: Bool = false,
ignoreConflictMarkers: Bool = false,
@@ -535,6 +537,7 @@ public struct FormatOptions: CustomStringConvertible {
self.extensionACLPlacement = extensionACLPlacement
self.redundantType = redundantType
self.emptyBracesSpacing = emptyBracesSpacing
self.acronyms = acronyms
// Doesn't really belong here, but hard to put elsewhere
self.fragment = fragment
self.ignoreConflictMarkers = ignoreConflictMarkers
+65
View File
@@ -5782,4 +5782,69 @@ public struct _FormatRules {
}
}
}
public let acronyms = FormatRule(
help: "Capitalizes acronyms when the first character is capitalized.",
disabledByDefault: true,
options: ["acronyms"]
) { formatter in
formatter.forEachToken { index, token in
guard token.is(.identifier) || token.isComment else { return }
var updatedText = token.string
for acronym in formatter.options.acronyms {
let find = acronym.capitalized
let replace = acronym.uppercased()
for replaceCandidateRange in token.string.ranges(of: find) {
let acronymShouldBeCapitalized: Bool
if replaceCandidateRange.upperBound < token.string.indices.last! {
let indexAfterMatch = replaceCandidateRange.upperBound
let characterAfterMatch = token.string[indexAfterMatch]
// Only treat this as an acronym if the next character is uppercased,
// to prevent "Id" from matching strings like "Identifier".
if characterAfterMatch.isUppercase || characterAfterMatch.isWhitespace {
acronymShouldBeCapitalized = true
}
// But if the next character is 's', and then the character after the 's' is uppercase,
// allow the acronym to be capitalized (to handle the plural case, `Ids` to `IDs`)
else if characterAfterMatch == Character("s") {
if indexAfterMatch < token.string.indices.last! {
let characterAfterNext = token.string[token.string.index(after: indexAfterMatch)]
acronymShouldBeCapitalized = (characterAfterNext.isUppercase || characterAfterNext.isWhitespace)
} else {
acronymShouldBeCapitalized = true
}
} else {
acronymShouldBeCapitalized = false
}
} else {
acronymShouldBeCapitalized = true
}
if acronymShouldBeCapitalized {
updatedText.replaceSubrange(replaceCandidateRange, with: replace)
}
}
}
if token.string != updatedText {
let updatedToken: Token
switch token {
case .identifier:
updatedToken = .identifier(updatedText)
case .commentBody:
updatedToken = .commentBody(updatedText)
default:
return
}
formatter.replaceToken(at: index, with: updatedToken)
}
}
}
}
+68
View File
@@ -2106,4 +2106,72 @@ class SyntaxTests: RulesTests {
let output = "preconditionFailure(msg, 0, 1)"
testFormatting(for: input, output, rule: FormatRules.assertionFailures)
}
// MARK: - acronyms
func testUppercaseAcronyms() {
let input = """
let url: URL
let destinationUrl: URL
let id: ID
let screenId = "screenId" // We intentionally don't change the content of strings
let validUrls: Set<URL>
let validUrlschemes: Set<URL>
let uniqueIdentifier = UUID()
/// Opens Urls based on their scheme
struct UrlRouter {}
/// The Id of a screen that can be displayed in the app
struct ScreenId {}
"""
let output = """
let url: URL
let destinationURL: URL
let id: ID
let screenID = "screenId" // We intentionally don't change the content of strings
let validURLs: Set<URL>
let validUrlschemes: Set<URL>
let uniqueIdentifier = UUID()
/// Opens URLs based on their scheme
struct URLRouter {}
/// The ID of a screen that can be displayed in the app
struct ScreenID {}
"""
testFormatting(for: input, output, rule: FormatRules.acronyms)
}
func testUppercaseCustomAcronym() {
let input = """
let url: URL
let destinationUrl: URL
let pngData: Data
let imageInPngFormat: UIImage
"""
let output = """
let url: URL
let destinationUrl: URL
let pngData: Data
let imageInPNGFormat: UIImage
"""
testFormatting(for: input, output, rule: FormatRules.acronyms, options: FormatOptions(acronyms: ["png"]))
}
func testDisableUppercaseAcronym() {
let input = """
// swiftformat:disable:next acronyms
typeNotOwnedByAuthor.destinationUrl = URL()
typeOwnedByAuthor.destinationURL = URL()
"""
testFormatting(for: input, rule: FormatRules.acronyms)
}
}
+1 -1
View File
@@ -2824,7 +2824,7 @@ class WrappingTests: RulesTests {
enum CodingKeys: String, CodingKey {
case name
case type
case categoryId = "category_id"
case categoryID = "category_id"
case attributes
}
}