mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-06-16 10:34:34 +00:00
Add rule to capitalize acronyms (#943)
This commit is contained in:
committed by
Nick Lockwood
parent
9d91b00749
commit
b128fe83ba
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user