Skip to content

Commit

Permalink
Update complex attributes check to exclude attributes with a single u…
Browse files Browse the repository at this point in the history
…nnamed argument
  • Loading branch information
calda authored and nicklockwood committed Jun 9, 2024
1 parent 6826bfe commit f4d2c2b
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Sources/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,7 @@ public struct FormatOptions: CustomStringConvertible {
storedVarAttributes: AttributeMode = .preserve,
computedVarAttributes: AttributeMode = .preserve,
complexAttributes: AttributeMode = .preserve,
complexAttributesExceptions: Set<String> = ["@Environment"],
complexAttributesExceptions: Set<String> = [],
markTypes: MarkMode = .always,
typeMarkComment: String = "MARK: - %t",
markExtensions: MarkMode = .always,
Expand Down
32 changes: 32 additions & 0 deletions Sources/ParsingHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,38 @@ 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].string.hasPrefix("@"))

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
}

/// Determine if next line after this token should be indented
func isEndOfStatement(at i: Int, in scope: Token? = nil) -> Bool {
guard let token = token(at: i) else { return true }
Expand Down
4 changes: 2 additions & 2 deletions Sources/Rules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5521,10 +5521,10 @@ public struct _FormatRules {
return
}

// If the complexAttriubtes option is configured, it takes precedence over other options
// 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.next(.nonSpaceOrCommentOrLinebreak, after: i) == .startOfScope("(")
let isComplexAttribute = formatter.isComplexAttribute(at: i)
&& !formatter.options.complexAttributesExceptions.contains(attributeName)

if isComplexAttribute, formatter.options.complexAttributes != .preserve {
Expand Down
62 changes: 61 additions & 1 deletion Tests/RulesTests+Wrapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4675,7 +4675,56 @@ class WrappingTests: RulesTests {
var foo: Foo
"""

let options = FormatOptions(varAttributes: .sameLine, storedVarAttributes: .sameLine, computedVarAttributes: .prevLine, complexAttributes: .prevLine, complexAttributesExceptions: ["@Environment", "@SomeCustomAttr"])
let options = FormatOptions(varAttributes: .sameLine, storedVarAttributes: .sameLine, computedVarAttributes: .prevLine, complexAttributes: .prevLine, complexAttributesExceptions: ["@SomeCustomAttr"])
testFormatting(for: input, output, rule: FormatRules.wrapAttributes, options: options)
}

func testMixedComplexAndSimpleAttributes() {
let input = """
/// Simple attributes stay on a single line:
@State private var warpDriveEnabled: Bool
@ObservedObject private var lifeSupportService: LifeSupportService
@Environment(\\.controlPanelStyle) private var controlPanelStyle
@AppStorage("ControlsConfig") private var controlsConfig: ControlConfiguration
/// Complex attributes are wrapped:
@AppStorage("ControlPanelState", store: myCustomUserDefaults) private var controlPanelState: ControlPanelState
@Tweak(name: "Aspect ratio") private var aspectRatio = AspectRatio.stretch
@available(*, unavailable) var saturn5Builder: Saturn5Builder
@available(*, unavailable, message: "No longer in production") var saturn5Builder: Saturn5Builder
"""

let output = """
/// Simple attributes stay on a single line:
@State private var warpDriveEnabled: Bool
@ObservedObject private var lifeSupportService: LifeSupportService
@Environment(\\.controlPanelStyle) private var controlPanelStyle
@AppStorage("ControlsConfig") private var controlsConfig: ControlConfiguration
/// Complex attributes are wrapped:
@AppStorage("ControlPanelState", store: myCustomUserDefaults)
private var controlPanelState: ControlPanelState
@Tweak(name: "Aspect ratio")
private var aspectRatio = AspectRatio.stretch
@available(*, unavailable)
var saturn5Builder: Saturn5Builder
@available(*, unavailable, message: "No longer in production")
var saturn5Builder: Saturn5Builder
"""

let options = FormatOptions(storedVarAttributes: .sameLine, complexAttributes: .prevLine)
testFormatting(for: input, output, rule: FormatRules.wrapAttributes, options: options)
}

Expand All @@ -4690,6 +4739,17 @@ class WrappingTests: RulesTests {
testFormatting(for: input, rule: FormatRules.wrapAttributes, options: options)
}

func testEscapingTypedThrowClosureNotMistakenForComplexAttribute() {
let input = """
func foo(_ fooClosure: @escaping () throws(Foo) -> Void) {
try fooClosure()
}
"""

let options = FormatOptions(complexAttributes: .prevLine)
testFormatting(for: input, rule: FormatRules.wrapAttributes, options: options)
}

func testWrapOrDontWrapMultipleDeclarationsInClass() {
let input = """
class Foo {
Expand Down

0 comments on commit f4d2c2b

Please sign in to comment.