Skip to content

Commit

Permalink
Make no_empty_block rule opt-in and add configurations (#5685)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ueeek authored Jul 22, 2024
1 parent d46bfcd commit b9f3843
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 48 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
[Martin Redington](https://github.com/mildm8nnered)
[#5552](https://github.com/realm/SwiftLint/issues/5552)

* Add `no_empty_block` default rule to validate that code blocks are not empty.
* Add `no_empty_block` opt-in rule to validate that code blocks are not empty.
They should at least contain a comment.
[Ueeek](https://github.com/Ueeek)
[#5615](https://github.com/realm/SwiftLint/issues/5615)
Expand Down
152 changes: 105 additions & 47 deletions Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoEmptyBlockRule.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule
struct NoEmptyBlockRule: Rule {
var configuration = SeverityConfiguration<Self>(.warning)
struct NoEmptyBlockRule: OptInRule {
var configuration = NoEmptyBlockConfiguration()

static let description = RuleDescription(
identifier: "no_empty_block",
Expand All @@ -11,85 +12,122 @@ struct NoEmptyBlockRule: Rule {
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
func f() {
/* do something */
}
var flag = true {
willSet { /* do something */ }
}
"""),

Example("""
do {
/* do something */
} catch {
/* do something */
class Apple {
init() { /* do something */ }
deinit { /* do something */ }
}
"""),

Example("""
defer {
for _ in 0..<10 { /* do something */ }
do {
/* do something */
} catch {
/* do something */
}
"""),
Example("""
deinit { /* do something */ }
"""),
Example("""
for _ in 0..<10 { /* do something */ }
"""),
Example("""
func f() {
/* do something */
defer {
/* do something */
}
print("other code")
}
"""),
Example("""
if flag {
/* do something */
} else {
/* do something */
}
repeat { /* do something */ } while (flag)
while i < 10 { /* do something */ }
"""),

Example("""
init() { /* do something */ }
"""),
func f() {}
var flag = true {
willSet {}
}
""", configuration: ["disabled_block_types": ["function_bodies"]]),

Example("""
repeat { /* do something */ } while (flag)
"""),
class Apple {
init() {}
deinit {}
}
""", configuration: ["disabled_block_types": ["initializer_bodies"]]),

Example("""
while i < 10 { /* do something */ }
"""),
for _ in 0..<10 {}
do {
} catch {
}
func f() {
defer {}
print("other code")
}
if flag {
} else {
}
repeat {} while (flag)
while i < 10 {}
""", configuration: ["disabled_block_types": ["statement_blocks"]]),
],
triggeringExamples: [
Example("""
func f() ↓{}
var flag = true {
willSet ↓{}
}
"""),

Example("""
do ↓{
} catch ↓{
class Apple {
init() ↓{}
deinit ↓{}
}
"""),
Example("""
defer ↓{}
"""),
Example("""
deinit ↓{}
"""),

Example("""
for _ in 0..<10 ↓{}
"""),
Example("""
func f() ↓{}
"""),
Example("""
do ↓{
} catch ↓{
}
func f() {
defer ↓{}
print("other code")
}
if flag ↓{
} else ↓{
}
"""),
Example("""
init() ↓{}
"""),
Example("""
repeat ↓{} while (flag)
"""),
Example("""
while i < 10 ↓{}
"""),
]
Expand All @@ -99,16 +137,36 @@ struct NoEmptyBlockRule: Rule {
private extension NoEmptyBlockRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: CodeBlockSyntax) {
// No need to show a warning since Empty Block of `guard` is compile error.
guard node.parent?.kind != .guardStmt else { return }
if let codeBlockType = node.codeBlockType, configuration.enabledBlockTypes.contains(codeBlockType) {
validate(node: node)
}
}

func validate(node: CodeBlockSyntax) {
guard node.statements.isEmpty,
!node.leftBrace.trailingTrivia.containsComments,
!node.rightBrace.leadingTrivia.containsComments else {
return
}

violations.append(node.leftBrace.positionAfterSkippingLeadingTrivia)
}
}
}

private extension CodeBlockSyntax {
var codeBlockType: NoEmptyBlockConfiguration.CodeBlockType? {
switch parent?.kind {
case .functionDecl, .accessorDecl:
.functionBodies
case .initializerDecl, .deinitializerDecl:
.initializerBodies
case .forStmt, .doStmt, .whileStmt, .repeatStmt, .ifExpr, .catchClause, .deferStmt:
.statementBlocks
case .guardStmt:
// No need to handle this case since Empty Block of `guard` is compile error.
nil
default:
nil
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftLintCore

@AutoApply
struct NoEmptyBlockConfiguration: SeverityBasedRuleConfiguration {
typealias Parent = NoEmptyBlockRule

@MakeAcceptableByConfigurationElement
enum CodeBlockType: String, CaseIterable {
case functionBodies = "function_bodies"
case initializerBodies = "initializer_bodies"
case statementBlocks = "statement_blocks"

static let all = Set(allCases)
}

@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)

@ConfigurationElement(key: "disabled_block_types")
private(set) var disabledBlockTypes: [CodeBlockType] = []

var enabledBlockTypes: Set<CodeBlockType> {
CodeBlockType.all.subtracting(disabledBlockTypes)
}
}
1 change: 1 addition & 0 deletions Tests/IntegrationTests/default_rule_configurations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ nimble_operator:
severity: warning
no_empty_block:
severity: warning
disabled_block_types: []
no_extension_access_modifier:
severity: error
no_fallthrough_only:
Expand Down
54 changes: 54 additions & 0 deletions Tests/SwiftLintFrameworkTests/NoEmptyBlockConfigurationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@testable import SwiftLintBuiltInRules
@testable import SwiftLintCore
import XCTest

final class NoEmptyBlockConfigurationTests: SwiftLintTestCase {
func testDefaultConfiguration() {
let config = NoEmptyBlockConfiguration()
XCTAssertEqual(config.severityConfiguration.severity, .warning)
XCTAssertEqual(config.enabledBlockTypes, NoEmptyBlockConfiguration.CodeBlockType.all)
}

func testApplyingCustomConfiguration() throws {
var config = NoEmptyBlockConfiguration()
try config.apply(
configuration: [
"severity": "error",
"disabled_block_types": ["function_bodies"],
] as [String: any Sendable]
)
XCTAssertEqual(config.severityConfiguration.severity, .error)
XCTAssertEqual(config.enabledBlockTypes, Set([.initializerBodies, .statementBlocks]))
}

func testInvalidKeyInCustomConfiguration() {
var config = NoEmptyBlockConfiguration()
XCTAssertEqual(
try Issue.captureConsole { try config.apply(configuration: ["invalidKey": "error"]) },
"warning: Configuration for 'no_empty_block' rule contains the invalid key(s) 'invalidKey'."
)
}

func testInvalidTypeOfCustomConfiguration() {
var config = NoEmptyBlockConfiguration()
checkError(Issue.invalidConfiguration(ruleID: NoEmptyBlockRule.description.identifier)) {
try config.apply(configuration: "invalidKey")
}
}

func testInvalidTypeOfValueInCustomConfiguration() {
var config = NoEmptyBlockConfiguration()
checkError(Issue.invalidConfiguration(ruleID: NoEmptyBlockRule.description.identifier)) {
try config.apply(configuration: ["severity": "foo"])
}
}

func testConsoleDescription() throws {
var config = NoEmptyBlockConfiguration()
try config.apply(configuration: ["disabled_block_types": ["initializer_bodies", "statement_blocks"]])
XCTAssertEqual(
RuleConfigurationDescription.from(configuration: config).oneLiner(),
"severity: warning; disabled_block_types: [initializer_bodies, statement_blocks]"
)
}
}

0 comments on commit b9f3843

Please sign in to comment.