Skip to content

Commit aaf92cf

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

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
@@ -34,6 +34,10 @@
3434
`redundant_void_return` rule.
3535
[Ryan Booker](https://github.com/ryanbooker)
3636
[#1761](https://github.com/realm/SwiftLint/issues/1761)
37+
* Add `closure_body_length` rule to enforce the maximum number of lines
38+
a closure should have.
39+
[Ornithologist Coder](https://github.com/ornithocoder)
40+
[#52](https://github.com/realm/SwiftLint/issues/52)
3741

3842
* Add `single_test_class` opt-in rule to validate that test files
3943
only contain a single `QuickSpec` or `XCTestCase` subclass.

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

607608

608609

610+
## Closure Body Length
611+
612+
Identifier | Enabled by default | Supports autocorrection | Kind
613+
--- | --- | --- | ---
614+
`closure_body_length` | Disabled | No | metrics
615+
616+
Closure bodies should not span too many lines.
617+
618+
### Examples
619+
620+
<details>
621+
<summary>Non Triggering Examples</summary>
622+
623+
```swift
624+
foo.bar { $0 }
625+
```
626+
627+
```swift
628+
foo.bar { value in
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+
print("toto")
645+
print("toto")
646+
print("toto")
647+
print("toto")
648+
return value
649+
}
650+
```
651+
652+
```swift
653+
foo.bar { value in
654+
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+
print("toto")
671+
print("toto")
672+
print("toto")
673+
print("toto")
674+
675+
return value
676+
}
677+
```
678+
679+
</details>
680+
<details>
681+
<summary>Triggering Examples</summary>
682+
683+
```swift
684+
foo.bar { value in
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+
print("toto")
702+
print("toto")
703+
print("toto")
704+
print("toto")
705+
return value
706+
}
707+
```
708+
709+
```swift
710+
foo.bar { value in
711+
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+
print("toto")
729+
print("toto")
730+
print("toto")
731+
print("toto")
732+
733+
return value
734+
}
735+
```
736+
737+
</details>
738+
739+
740+
609741
## Closure End Indentation
610742

611743
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
@@ -74,6 +74,7 @@
7474
629C60D91F43906700B4AF92 /* SingleTestClassRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629C60D81F43906700B4AF92 /* SingleTestClassRule.swift */; };
7575
62A498561F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */; };
7676
62A6E7931F3317E3003A0479 /* JoinedDefaultRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */; };
77+
62A6E7951F3344CF003A0479 /* ClosureBodyLengthRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */; };
7778
67932E2D1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */; };
7879
67EB4DFA1E4CC111004E9ACD /* CyclomaticComplexityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */; };
7980
67EB4DFC1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EB4DFB1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift */; };
@@ -387,6 +388,7 @@
387388
629C60D81F43906700B4AF92 /* SingleTestClassRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleTestClassRule.swift; sourceTree = "<group>"; };
388389
62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitConfiguration.swift; sourceTree = "<group>"; };
389390
62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedDefaultRule.swift; sourceTree = "<group>"; };
391+
62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureBodyLengthRule.swift; sourceTree = "<group>"; };
390392
62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitRuleTests.swift; sourceTree = "<group>"; };
391393
65454F451B14D73800319A6C /* ControlStatementRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlStatementRule.swift; sourceTree = "<group>"; };
392394
67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CyclomaticComplexityConfigurationTests.swift; sourceTree = "<group>"; };
@@ -939,6 +941,7 @@
939941
D4FD4C841F2A260A00DD8AA8 /* BlockBasedKVORule.swift */,
940942
D4B0228D1E0CC608007E5297 /* ClassDelegateProtocolRule.swift */,
941943
1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */,
944+
62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */,
942945
D43B046A1E075905004016AF /* ClosureEndIndentationRule.swift */,
943946
D47079A81DFDBED000027086 /* ClosureParameterPositionRule.swift */,
944947
1E82D5581D7775C7009553D7 /* ClosureSpacingRule.swift */,
@@ -1426,6 +1429,7 @@
14261429
D4130D991E16CC1300242361 /* TypeNameRuleExamples.swift in Sources */,
14271430
24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */,
14281431
47ACC8981E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift in Sources */,
1432+
62A6E7951F3344CF003A0479 /* ClosureBodyLengthRule.swift in Sources */,
14291433
009E09281DFEE4C200B588A7 /* ProhibitedSuperRule.swift in Sources */,
14301434
E80E018F1B92C1350078EB70 /* Region.swift in Sources */,
14311435
E88198581BEA956C00333A11 /* FunctionBodyLengthRule.swift in Sources */,

Tests/LinuxMain.swift

+1
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ extension RulesTests {
344344
("testBlockBasedKVO", testBlockBasedKVO),
345345
("testClassDelegateProtocol", testClassDelegateProtocol),
346346
("testClosingBrace", testClosingBrace),
347+
("testClosureBodyLength", testClosureBodyLength),
347348
("testClosureEndIndentation", testClosureEndIndentation),
348349
("testClosureParameterPosition", testClosureParameterPosition),
349350
("testClosureSpacing", testClosureSpacing),

Tests/SwiftLintFrameworkTests/RulesTests.swift

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

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

0 commit comments

Comments
 (0)