Skip to content

Commit c726e19

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

File tree

7 files changed

+207
-0
lines changed

7 files changed

+207
-0
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
`redundant_void_return` rule.
3131
[Ryan Booker](https://github.com/ryanbooker)
3232
[#1761](https://github.com/realm/SwiftLint/issues/1761)
33+
* Add `closure_body_length` rule to enforce the maximum number of lines
34+
a closure should have.
35+
[Ornithologist Coder](https://github.com/ornithocoder)
36+
[#52](https://github.com/realm/SwiftLint/issues/52)
3337

3438
##### Bug Fixes
3539

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)
@@ -602,6 +603,137 @@ Closing brace with closing parenthesis should not have any whitespaces in the mi
602603

603604

604605

606+
## Closure Body Length
607+
608+
Identifier | Enabled by default | Supports autocorrection | Kind
609+
--- | --- | --- | ---
610+
`closure_body_length` | Disabled | No | metrics
611+
612+
Closure bodies should not span too many lines.
613+
614+
### Examples
615+
616+
<details>
617+
<summary>Non Triggering Examples</summary>
618+
619+
```swift
620+
foo.bar { $0 }
621+
```
622+
623+
```swift
624+
foo.bar { value in
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+
print("toto")
644+
return value
645+
}
646+
```
647+
648+
```swift
649+
foo.bar { value in
650+
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+
print("toto")
670+
671+
return value
672+
}
673+
```
674+
675+
</details>
676+
<details>
677+
<summary>Triggering Examples</summary>
678+
679+
```swift
680+
foo.bar { value in
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+
print("toto")
701+
return value
702+
}
703+
```
704+
705+
```swift
706+
foo.bar { value in
707+
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+
print("toto")
728+
729+
return value
730+
}
731+
```
732+
733+
</details>
734+
735+
736+
605737
## Closure End Indentation
606738

607739
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
@@ -71,6 +71,7 @@
7171
626D02971F31CBCC0054788D /* XCTFailMessageRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */; };
7272
62A498561F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */; };
7373
62A6E7931F3317E3003A0479 /* JoinedDefaultRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */; };
74+
62A6E7951F3344CF003A0479 /* ClosureBodyLengthRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */; };
7475
67932E2D1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */; };
7576
67EB4DFA1E4CC111004E9ACD /* CyclomaticComplexityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */; };
7677
67EB4DFC1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EB4DFB1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift */; };
@@ -379,6 +380,7 @@
379380
626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTFailMessageRule.swift; sourceTree = "<group>"; };
380381
62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitConfiguration.swift; sourceTree = "<group>"; };
381382
62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedDefaultRule.swift; sourceTree = "<group>"; };
383+
62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureBodyLengthRule.swift; sourceTree = "<group>"; };
382384
62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitRuleTests.swift; sourceTree = "<group>"; };
383385
65454F451B14D73800319A6C /* ControlStatementRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlStatementRule.swift; sourceTree = "<group>"; };
384386
67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CyclomaticComplexityConfigurationTests.swift; sourceTree = "<group>"; };
@@ -929,6 +931,7 @@
929931
D4FD4C841F2A260A00DD8AA8 /* BlockBasedKVORule.swift */,
930932
D4B0228D1E0CC608007E5297 /* ClassDelegateProtocolRule.swift */,
931933
1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */,
934+
62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */,
932935
D43B046A1E075905004016AF /* ClosureEndIndentationRule.swift */,
933936
D47079A81DFDBED000027086 /* ClosureParameterPositionRule.swift */,
934937
1E82D5581D7775C7009553D7 /* ClosureSpacingRule.swift */,
@@ -1408,6 +1411,7 @@
14081411
D4130D991E16CC1300242361 /* TypeNameRuleExamples.swift in Sources */,
14091412
24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */,
14101413
47ACC8981E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift in Sources */,
1414+
62A6E7951F3344CF003A0479 /* ClosureBodyLengthRule.swift in Sources */,
14111415
009E09281DFEE4C200B588A7 /* ProhibitedSuperRule.swift in Sources */,
14121416
E80E018F1B92C1350078EB70 /* Region.swift in Sources */,
14131417
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)