Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option for opening_brace rule not to trigger with multiline statements #5521

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
leonardosrodrigues0 marked this conversation as resolved.
Show resolved Hide resolved

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 {
leonardosrodrigues0 marked this conversation as resolved.
Show resolved Hide resolved
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