mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
184 lines
7.4 KiB
Swift
184 lines
7.4 KiB
Swift
//
|
|
// WrapAttributes.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Nick Lockwood on 7/26/20.
|
|
// Copyright © 2024 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension FormatRule {
|
|
static let wrapAttributes = FormatRule(
|
|
help: "Wrap @attributes onto a separate line, or keep them on the same line.",
|
|
options: ["func-attributes", "type-attributes", "var-attributes", "stored-var-attributes", "computed-var-attributes", "complex-attributes", "non-complex-attributes"],
|
|
sharedOptions: ["linebreaks", "max-width"]
|
|
) { formatter in
|
|
formatter.forEach(.attribute) { i, _ in
|
|
// Ignore sequential attributes
|
|
guard let endIndex = formatter.endOfAttribute(at: i),
|
|
var keywordIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: endIndex),
|
|
formatter.tokens[keywordIndex].isKeyword || formatter.isModifier(at: keywordIndex)
|
|
else {
|
|
return
|
|
}
|
|
|
|
// Skip modifiers
|
|
while formatter.isModifier(at: keywordIndex),
|
|
let nextIndex = formatter.index(of: .keyword, after: keywordIndex)
|
|
{
|
|
keywordIndex = nextIndex
|
|
}
|
|
|
|
// Check which `AttributeMode` option to use
|
|
var attributeMode: AttributeMode
|
|
switch formatter.tokens[keywordIndex].string {
|
|
case "func", "init", "subscript":
|
|
attributeMode = formatter.options.funcAttributes
|
|
case "class", "actor", "struct", "enum", "protocol", "extension":
|
|
attributeMode = formatter.options.typeAttributes
|
|
case "var", "let":
|
|
let storedOrComputedAttributeMode: AttributeMode
|
|
if formatter.isStoredProperty(atIntroducerIndex: keywordIndex) {
|
|
storedOrComputedAttributeMode = formatter.options.storedVarAttributes
|
|
} else {
|
|
storedOrComputedAttributeMode = formatter.options.computedVarAttributes
|
|
}
|
|
|
|
// If the relevant `storedvarattrs` or `computedvarattrs` option hasn't been configured,
|
|
// fall back to the previous (now deprecated) `varattributes` option.
|
|
if storedOrComputedAttributeMode == .preserve {
|
|
attributeMode = formatter.options.varAttributes
|
|
} else {
|
|
attributeMode = storedOrComputedAttributeMode
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
|
|
// If the complexAttributes option is configured, it takes precedence over other options
|
|
// if this is a complex attributes with arguments.
|
|
let attributeName = formatter.tokens[i].string
|
|
let isComplexAttribute = formatter.isComplexAttribute(at: i)
|
|
&& !formatter.options.complexAttributesExceptions.contains(attributeName)
|
|
|
|
if isComplexAttribute, formatter.options.complexAttributes != .preserve {
|
|
attributeMode = formatter.options.complexAttributes
|
|
}
|
|
|
|
// Apply the `AttributeMode`
|
|
switch attributeMode {
|
|
case .preserve:
|
|
return
|
|
case .prevLine:
|
|
// Make sure there's a newline immediately following the attribute
|
|
if let nextIndex = formatter.index(of: .nonSpaceOrComment, after: endIndex),
|
|
formatter.token(at: nextIndex)?.isLinebreak != true
|
|
{
|
|
formatter.insertSpace(formatter.currentIndentForLine(at: i), at: nextIndex)
|
|
formatter.insertLinebreak(at: nextIndex)
|
|
// Remove any trailing whitespace left on the line with the attributes
|
|
if let prevToken = formatter.token(at: nextIndex - 1), prevToken.isSpace {
|
|
formatter.removeToken(at: nextIndex - 1)
|
|
}
|
|
}
|
|
case .sameLine:
|
|
// Make sure there isn't a newline immediately following the attribute
|
|
if let nextIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: endIndex),
|
|
formatter.tokens[(endIndex + 1) ..< nextIndex].contains(where: \.isLinebreak)
|
|
{
|
|
// If unwrapping the attribute causes the line to exceed the max width,
|
|
// leave it as-is. The existing formatting is likely better than how
|
|
// this would be re-unwrapped by the wrap rule.
|
|
let startOfLine = formatter.startOfLine(at: i)
|
|
let endOfLine = formatter.endOfLine(at: i)
|
|
let startOfNextLine = formatter.startOfLine(at: nextIndex, excludingIndent: true)
|
|
let endOfNextLine = formatter.endOfLine(at: nextIndex)
|
|
let combinedLine = formatter.tokens[startOfLine ... endOfLine].map(\.string).joined()
|
|
+ formatter.tokens[startOfNextLine ..< endOfNextLine].map(\.string).joined()
|
|
|
|
if formatter.options.maxWidth > 0, combinedLine.count > formatter.options.maxWidth {
|
|
return
|
|
}
|
|
|
|
// Replace the newline with a space so the attribute doesn't
|
|
// merge with the next token.
|
|
formatter.replaceTokens(in: (endIndex + 1) ..< nextIndex, with: .space(" "))
|
|
}
|
|
}
|
|
}
|
|
} examples: {
|
|
"""
|
|
`--func-attributes prev-line`
|
|
|
|
```diff
|
|
- @objc func foo() {}
|
|
|
|
+ @objc
|
|
+ func foo() { }
|
|
```
|
|
|
|
`--func-attributes same-line`
|
|
|
|
```diff
|
|
- @objc
|
|
- func foo() { }
|
|
|
|
+ @objc func foo() {}
|
|
```
|
|
|
|
`--type-attributes prev-line`
|
|
|
|
```diff
|
|
- @objc class Foo {}
|
|
|
|
+ @objc
|
|
+ class Foo { }
|
|
```
|
|
|
|
`--type-attributes same-line`
|
|
|
|
```diff
|
|
- @objc
|
|
- enum Foo { }
|
|
|
|
+ @objc enum Foo {}
|
|
```
|
|
"""
|
|
}
|
|
}
|
|
|
|
extension Formatter {
|
|
/// Whether or not the attribute starting at the given index is complex. That is, has:
|
|
/// - any named arguments
|
|
/// - more than one unnamed argument
|
|
func isComplexAttribute(at attributeIndex: Int) -> Bool {
|
|
assert(tokens[attributeIndex].isAttribute)
|
|
|
|
guard let startOfScopeIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: attributeIndex),
|
|
tokens[startOfScopeIndex] == .startOfScope("("),
|
|
let firstTokenInBody = index(of: .nonSpaceOrCommentOrLinebreak, after: startOfScopeIndex),
|
|
let endOfScopeIndex = endOfScope(at: startOfScopeIndex),
|
|
firstTokenInBody != endOfScopeIndex
|
|
else { return false }
|
|
|
|
// If the first argument is named with a parameter label, then this is a complex attribute:
|
|
if tokens[firstTokenInBody].isIdentifierOrKeyword,
|
|
let followingToken = index(of: .nonSpaceOrCommentOrLinebreak, after: firstTokenInBody),
|
|
tokens[followingToken] == .delimiter(":")
|
|
{
|
|
return true
|
|
}
|
|
|
|
// If there are any commas in the attribute body, then this attribute has
|
|
// multiple arguments and is thus complex:
|
|
for index in startOfScopeIndex ... endOfScopeIndex {
|
|
if tokens[index] == .delimiter(","), startOfScope(at: index) == startOfScopeIndex {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|