Skip to content

Commit f1f5248

Browse files
committed
Adds closure_body_length opt-in rule
because closure bodies should not span too many lines. Implements realm#52.
1 parent 54d6199 commit f1f5248

File tree

7 files changed

+208
-0
lines changed

7 files changed

+208
-0
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
[Ornithologist Coder](https://github.com/ornithocoder)
1616
[#1370](https://github.com/realm/SwiftLint/issues/1370)
1717

18+
* Add `closure_body_length` rule to enforce the maximum number of lines
19+
a closure should have.
20+
[Ornithologist Coder](https://github.com/ornithocoder)
21+
[#52](https://github.com/realm/SwiftLint/issues/52)
22+
1823
##### Bug Fixes
1924

2025
* Fix false positive on `force_unwrapping` rule when declaring

Rules.md

+132
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* [Block Based KVO](#block-based-kvo)
66
* [Class Delegate Protocol](#class-delegate-protocol)
77
* [Closing Brace Spacing](#closing-brace-spacing)
8+
* [Closure Body Length](#closure-body-length)
89
* [Closure End Indentation](#closure-end-indentation)
910
* [Closure Parameter Position](#closure-parameter-position)
1011
* [Closure Spacing](#closure-spacing)
@@ -601,6 +602,137 @@ Closing brace with closing parenthesis should not have any whitespaces in the mi
601602

602603

603604

605+
## Closure Body Length
606+
607+
Identifier | Enabled by default | Supports autocorrection | Kind
608+
--- | --- | --- | ---
609+
`closure_body_length` | Disabled | No | metrics
610+
611+
Closure bodies should not span too many lines.
612+
613+
### Examples
614+
615+
<details>
616+
<summary>Non Triggering Examples</summary>
617+
618+
```swift
619+
foo.bar { $0 }
620+
```
621+
622+
```swift
623+
foo.bar { value in
624+
print("toto")
625+
print("toto")
626+
print("toto")
627+
print("toto")
628+
print("toto")
629+
print("toto")
630+
print("toto")
631+
print("toto")
632+
print("toto")
633+
print("toto")
634+
print("toto")
635+
print("toto")
636+
print("toto")
637+
print("toto")
638+
print("toto")
639+
print("toto")
640+
print("toto")
641+
print("toto")
642+
print("toto")
643+
return value
644+
}
645+
```
646+
647+
```swift
648+
foo.bar { value in
649+
650+
print("toto")
651+
print("toto")
652+
print("toto")
653+
print("toto")
654+
print("toto")
655+
print("toto")
656+
print("toto")
657+
print("toto")
658+
print("toto")
659+
print("toto")
660+
print("toto")
661+
print("toto")
662+
print("toto")
663+
print("toto")
664+
print("toto")
665+
print("toto")
666+
print("toto")
667+
print("toto")
668+
print("toto")
669+
670+
return value
671+
}
672+
```
673+
674+
</details>
675+
<details>
676+
<summary>Triggering Examples</summary>
677+
678+
```swift
679+
foo.bar { value in
680+
print("toto")
681+
print("toto")
682+
print("toto")
683+
print("toto")
684+
print("toto")
685+
print("toto")
686+
print("toto")
687+
print("toto")
688+
print("toto")
689+
print("toto")
690+
print("toto")
691+
print("toto")
692+
print("toto")
693+
print("toto")
694+
print("toto")
695+
print("toto")
696+
print("toto")
697+
print("toto")
698+
print("toto")
699+
print("toto")
700+
return value
701+
}
702+
```
703+
704+
```swift
705+
foo.bar { value in
706+
707+
print("toto")
708+
print("toto")
709+
print("toto")
710+
print("toto")
711+
print("toto")
712+
print("toto")
713+
print("toto")
714+
print("toto")
715+
print("toto")
716+
print("toto")
717+
print("toto")
718+
print("toto")
719+
print("toto")
720+
print("toto")
721+
print("toto")
722+
print("toto")
723+
print("toto")
724+
print("toto")
725+
print("toto")
726+
print("toto")
727+
728+
return value
729+
}
730+
```
731+
732+
</details>
733+
734+
735+
604736
## Closure End Indentation
605737

606738
Identifier | Enabled by default | Supports autocorrection | Kind

Source/SwiftLintFramework/Models/MasterRuleList.swift

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public let masterRuleList = RuleList(rules: [
1313
BlockBasedKVORule.self,
1414
ClassDelegateProtocolRule.self,
1515
ClosingBraceRule.self,
16+
ClosureBodyLengthRule.self,
1617
ClosureEndIndentationRule.self,
1718
ClosureParameterPositionRule.self,
1819
ClosureSpacingRule.self,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// ClosureBodyLengthRule.swift
3+
// SwiftLint
4+
//
5+
// Created by Ornithologist Coder on 8/3/17.
6+
// Copyright © 2017 Realm. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import SourceKittenFramework
11+
12+
public struct ClosureBodyLengthRule: ASTRule, OptInRule, ConfigurationProviderRule {
13+
public var configuration = SeverityLevelsConfiguration(warning: 20, error: 100)
14+
15+
public init() {}
16+
17+
public static let description = RuleDescription(
18+
identifier: "closure_body_length",
19+
name: "Closure Body Length",
20+
description: "Closure bodies should not span too many lines.",
21+
kind: .metrics,
22+
nonTriggeringExamples: [
23+
"foo.bar { $0 }",
24+
"foo.bar { value in\n" + repeatElement("\tprint(\"toto\")\n", count: 19).joined() + "\treturn value\n}",
25+
"foo.bar { value in\n\n" + repeatElement("\tprint(\"toto\")\n", count: 19).joined() + "\n\treturn value\n}"
26+
],
27+
triggeringExamples: [
28+
"foo.bar {↓ value in\n" + repeatElement("\tprint(\"toto\")\n", count: 20).joined() + "\treturn value\n}",
29+
"foo.bar {↓ value in\n\n" + repeatElement("\tprint(\"toto\")\n", count: 20).joined() + "\n\treturn value\n}"
30+
]
31+
)
32+
33+
public func validate(file: File,
34+
kind: SwiftExpressionKind,
35+
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
36+
guard
37+
kind == .call,
38+
let bodyOffset = dictionary.bodyOffset,
39+
let bodyLength = dictionary.bodyLength,
40+
case let contents = file.contents.bridge(),
41+
contents.substringWithByteRange(start: bodyOffset - 1, length: 1) == "{",
42+
let startLine = contents.lineAndCharacter(forByteOffset: bodyOffset)?.line,
43+
let endLine = contents.lineAndCharacter(forByteOffset: bodyOffset + bodyLength)?.line
44+
else {
45+
return []
46+
}
47+
48+
return configuration.params.flatMap { parameter in
49+
let (exceeds, lineCount) = file.exceedsLineCountExcludingCommentsAndWhitespace(startLine,
50+
endLine,
51+
parameter.value)
52+
guard exceeds else { return nil }
53+
54+
return StyleViolation(ruleDescription: type(of: self).description,
55+
severity: parameter.severity,
56+
location: Location(file: file, byteOffset: bodyOffset),
57+
reason: "Closure body should span \(configuration.warning) lines or less " +
58+
"excluding comments and whitespace: currently spans \(lineCount) " + "lines")
59+
}
60+
}
61+
}

SwiftLint.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
62622F6B1F2F2E3500D5D099 /* DiscouragedDirectInitRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62622F6A1F2F2E3500D5D099 /* DiscouragedDirectInitRule.swift */; };
7171
626D02971F31CBCC0054788D /* XCTFailMessageRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */; };
7272
62A498561F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */; };
73+
62A6E7951F3344CF003A0479 /* ClosureBodyLengthRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */; };
7374
67932E2D1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */; };
7475
67EB4DFA1E4CC111004E9ACD /* CyclomaticComplexityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */; };
7576
67EB4DFC1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EB4DFB1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift */; };
@@ -377,6 +378,7 @@
377378
62622F6A1F2F2E3500D5D099 /* DiscouragedDirectInitRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitRule.swift; sourceTree = "<group>"; };
378379
626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTFailMessageRule.swift; sourceTree = "<group>"; };
379380
62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitConfiguration.swift; sourceTree = "<group>"; };
381+
62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureBodyLengthRule.swift; sourceTree = "<group>"; };
380382
62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitRuleTests.swift; sourceTree = "<group>"; };
381383
65454F451B14D73800319A6C /* ControlStatementRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlStatementRule.swift; sourceTree = "<group>"; };
382384
67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CyclomaticComplexityConfigurationTests.swift; sourceTree = "<group>"; };
@@ -927,6 +929,7 @@
927929
D4FD4C841F2A260A00DD8AA8 /* BlockBasedKVORule.swift */,
928930
D4B0228D1E0CC608007E5297 /* ClassDelegateProtocolRule.swift */,
929931
1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */,
932+
62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */,
930933
D43B046A1E075905004016AF /* ClosureEndIndentationRule.swift */,
931934
D47079A81DFDBED000027086 /* ClosureParameterPositionRule.swift */,
932935
1E82D5581D7775C7009553D7 /* ClosureSpacingRule.swift */,
@@ -1405,6 +1408,7 @@
14051408
D4130D991E16CC1300242361 /* TypeNameRuleExamples.swift in Sources */,
14061409
24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */,
14071410
47ACC8981E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift in Sources */,
1411+
62A6E7951F3344CF003A0479 /* ClosureBodyLengthRule.swift in Sources */,
14081412
009E09281DFEE4C200B588A7 /* ProhibitedSuperRule.swift in Sources */,
14091413
E80E018F1B92C1350078EB70 /* Region.swift in Sources */,
14101414
E88198581BEA956C00333A11 /* FunctionBodyLengthRule.swift in Sources */,

Tests/LinuxMain.swift

+1
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ extension RulesTests {
338338
("testBlockBasedKVO", testBlockBasedKVO),
339339
("testClassDelegateProtocol", testClassDelegateProtocol),
340340
("testClosingBrace", testClosingBrace),
341+
("testClosureBodyLength", testClosureBodyLength),
341342
("testClosureEndIndentation", testClosureEndIndentation),
342343
("testClosureParameterPosition", testClosureParameterPosition),
343344
("testClosureSpacing", testClosureSpacing),

Tests/SwiftLintFrameworkTests/RulesTests.swift

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class RulesTests: XCTestCase {
2626
verifyRule(ClosingBraceRule.description)
2727
}
2828

29+
func testClosureBodyLength() {
30+
verifyRule(ClosureBodyLengthRule.description)
31+
}
32+
2933
func testClosureEndIndentation() {
3034
verifyRule(ClosureEndIndentationRule.description)
3135
}

0 commit comments

Comments
 (0)