Files
SwiftFormat/Sources/Rules/WrapAttributes.swift
2026-01-25 09:02:03 -08:00

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
}
}