Skip to content

Commit

Permalink
Add options to opening_brace rule that silence it on multiline stat…
Browse files Browse the repository at this point in the history
…ements and types (#5521)

Co-authored-by: Danny Mösch <danny.moesch@icloud.com>
  • Loading branch information
leonardosrodrigues0 and SimplyDanny authored Aug 24, 2024
1 parent 60a1d34 commit 3bb8014
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 27 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

#### Enhancements

* None.
* Add `ignore_multiline_type_headers` and `ignore_multiline_statement_conditions`
options to `opening_brace` rule to allow opening braces to be on a new line after
multiline type headers or statement conditions. Rename `allow_multiline_func` to
`ignore_multiline_function_signatures`.
[leonardosrodrigues0](https://github.com/leonardosrodrigues0)
[#3720](https://github.com/realm/SwiftLint/issues/3720)

#### Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ struct OpeningBraceConfiguration: SeverityBasedRuleConfiguration {

@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
@ConfigurationElement(key: "allow_multiline_func")
@ConfigurationElement(key: "ignore_multiline_type_headers")
private(set) var ignoreMultilineTypeHeaders = false
@ConfigurationElement(key: "ignore_multiline_statement_conditions")
private(set) var ignoreMultilineStatementConditions = false
@ConfigurationElement(key: "ignore_multiline_function_signatures")
private(set) var ignoreMultilineFunctionSignatures = false
// TODO: [08/23/2026] Remove deprecation warning after ~2 years.
@ConfigurationElement(key: "allow_multiline_func", deprecationNotice: .suggestAlternative(
ruleID: Parent.identifier, name: "ignore_multiline_function_signatures"))
private(set) var allowMultilineFunc = false

var shouldIgnoreMultilineFunctionSignatures: Bool {
ignoreMultilineFunctionSignatures || allowMultilineFunc
}
}
112 changes: 102 additions & 10 deletions Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,123 @@ struct OpeningBraceRule: SwiftSyntaxCorrectableRule {

private extension OpeningBraceRule {
final class Visitor: CodeBlockVisitor<ConfigurationType> {
override func visitPost(_ node: FunctionDeclSyntax) {
guard let body = node.body else {
// MARK: - Type Declarations

override func visitPost(_ node: ActorDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.actorKeyword) {
return
}
if configuration.allowMultilineFunc, refersToMultilineFunction(body, functionIndicator: node.funcKeyword) {

super.visitPost(node)
}

override func visitPost(_ node: ClassDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.classKeyword) {
return
}
collectViolations(for: body)

super.visitPost(node)
}

override func visitPost(_ node: InitializerDeclSyntax) {
guard let body = node.body else {
override func visitPost(_ node: EnumDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.enumKeyword) {
return
}

super.visitPost(node)
}

override func visitPost(_ node: ExtensionDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.extensionKeyword) {
return
}

super.visitPost(node)
}

override func visitPost(_ node: ProtocolDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.protocolKeyword) {
return
}
if configuration.allowMultilineFunc, refersToMultilineFunction(body, functionIndicator: node.initKeyword) {

super.visitPost(node)
}

override func visitPost(_ node: StructDeclSyntax) {
if configuration.ignoreMultilineTypeHeaders,
hasMultilinePredecessors(node.memberBlock, keyword: node.structKeyword) {
return
}
collectViolations(for: body)

super.visitPost(node)
}

private func refersToMultilineFunction(_ body: CodeBlockSyntax, functionIndicator: TokenSyntax) -> Bool {
// MARK: - Conditional Statements

override func visitPost(_ node: ForStmtSyntax) {
if configuration.ignoreMultilineStatementConditions,
hasMultilinePredecessors(node.body, keyword: node.forKeyword) {
return
}

super.visitPost(node)
}

override func visitPost(_ node: IfExprSyntax) {
if configuration.ignoreMultilineStatementConditions,
hasMultilinePredecessors(node.body, keyword: node.ifKeyword) {
return
}

super.visitPost(node)
}

override func visitPost(_ node: WhileStmtSyntax) {
if configuration.ignoreMultilineStatementConditions,
hasMultilinePredecessors(node.body, keyword: node.whileKeyword) {
return
}

super.visitPost(node)
}

// MARK: - Functions and Initializers

override func visitPost(_ node: FunctionDeclSyntax) {
if let body = node.body,
configuration.shouldIgnoreMultilineFunctionSignatures,
hasMultilinePredecessors(body, keyword: node.funcKeyword) {
return
}

super.visitPost(node)
}

override func visitPost(_ node: InitializerDeclSyntax) {
if let body = node.body,
configuration.shouldIgnoreMultilineFunctionSignatures,
hasMultilinePredecessors(body, keyword: node.initKeyword) {
return
}

super.visitPost(node)
}

// MARK: - Other Methods

/// Checks if a `BracedSyntax` has a multiline predecessor.
/// For type declarations, the predecessor is the header. For conditional statements,
/// it is the condition list, and for functions, it is the signature.
private func hasMultilinePredecessors(_ body: some BracedSyntax, keyword: TokenSyntax) -> Bool {
guard let endToken = body.previousToken(viewMode: .sourceAccurate) else {
return false
}
let startLocation = functionIndicator.endLocation(converter: locationConverter)
let startLocation = keyword.endLocation(converter: locationConverter)
let endLocation = endToken.endLocation(converter: locationConverter)
let braceLocation = body.leftBrace.endLocation(converter: locationConverter)
return startLocation.line != endLocation.line && endLocation.line != braceLocation.line
Expand Down
3 changes: 3 additions & 0 deletions Tests/IntegrationTests/default_rule_configurations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ one_declaration_per_file:
severity: warning
opening_brace:
severity: warning
ignore_multiline_type_headers: false
ignore_multiline_statement_conditions: false
ignore_multiline_function_signatures: false
allow_multiline_func: false
operator_usage_whitespace:
severity: warning
Expand Down
132 changes: 117 additions & 15 deletions Tests/SwiftLintFrameworkTests/OpeningBraceRuleTests.swift
Original file line number Diff line number Diff line change
@@ -1,25 +1,120 @@
@testable import SwiftLintBuiltInRules

final class OpeningBraceRuleTests: SwiftLintTestCase {
func testDefaultExamplesRunInMultilineMode() {
func testDefaultNonTriggeringExamplesWithMultilineOptionsTrue() {
let description = OpeningBraceRule.description
.with(triggeringExamples: OpeningBraceRule.description.triggeringExamples.removing([
Example("func abc(a: A,\n\tb: B)\n↓{"),
Example("""
internal static func getPointer()
-> UnsafeMutablePointer<_ThreadLocalStorage>
↓{
return _swift_stdlib_threadLocalStorageGet().assumingMemoryBound(
to: _ThreadLocalStorage.self)
}
"""),
]))
.with(triggeringExamples: [])
.with(corrections: [:])

verifyRule(description, ruleConfiguration: [
"ignore_multiline_statement_conditions": true,
"ignore_multiline_type_headers": true,
"ignore_multiline_function_signatures": true,
])
}

func testWithIgnoreMultilineTypeHeadersTrue() {
let nonTriggeringExamples = [
Example("""
extension A
where B: Equatable
{}
"""),
Example("""
struct S: Comparable,
Identifiable
{
init() {}
}
"""),
]

let triggeringExamples = [
Example("""
struct S
↓{}
"""),
Example("""
extension A where B: Equatable
↓{
}
"""),
Example("""
class C
// with comments
↓{}
"""),
]

let description = OpeningBraceRule.description
.with(nonTriggeringExamples: nonTriggeringExamples)
.with(triggeringExamples: triggeringExamples)
.with(corrections: [:])

verifyRule(description, ruleConfiguration: ["ignore_multiline_type_headers": true])
}

func testWithIgnoreMultilineStatementConditionsTrue() {
let nonTriggeringExamples = [
Example("""
while
abc
{}
"""),
Example("""
if x {
} else if
y,
z
{
}
"""),
Example("""
if
condition1,
let var1 = var1
{}
"""),
]

let triggeringExamples = [
Example("""
if x
↓{}
"""),
Example("""
if x {
verifyRule(description, ruleConfiguration: ["allow_multiline_func": true])
} else if y, z
↓{}
"""),
Example("""
if x {
} else
↓{}
"""),
Example("""
while abc
// comments
↓{
}
"""),
]

let description = OpeningBraceRule.description
.with(nonTriggeringExamples: nonTriggeringExamples)
.with(triggeringExamples: triggeringExamples)
.with(corrections: [:])

verifyRule(description, ruleConfiguration: ["ignore_multiline_statement_conditions": true])
}

// swiftlint:disable:next function_body_length
func testWithAllowMultilineTrue() {
func testWithIgnoreMultilineFunctionSignaturesTrue() {
let nonTriggeringExamples = [
Example("""
func abc(
Expand Down Expand Up @@ -80,14 +175,21 @@ final class OpeningBraceRuleTests: SwiftLintTestCase {
}
}
"""),
Example("""
class C {
init(a: Int)
// with comments
↓{}
}
"""),
]

let description = OpeningBraceRule.description
.with(nonTriggeringExamples: nonTriggeringExamples)
.with(triggeringExamples: triggeringExamples)
.with(corrections: [:])

verifyRule(description, ruleConfiguration: ["allow_multiline_func": true])
verifyRule(description, ruleConfiguration: ["ignore_multiline_function_signatures": true])
}
}

Expand Down

0 comments on commit 3bb8014

Please sign in to comment.