Skip to content

Commit 8f81ffd

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

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
* Add `single_test_class` opt-in rule to enforce test files
3539
to contain a single QuickSpec or XCTestCase class.

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

605606

606607

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

609741
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 */; };
@@ -385,6 +386,7 @@
385386
629C60D81F43906700B4AF92 /* SingleTestClassRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleTestClassRule.swift; sourceTree = "<group>"; };
386387
62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitConfiguration.swift; sourceTree = "<group>"; };
387388
62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedDefaultRule.swift; sourceTree = "<group>"; };
389+
62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureBodyLengthRule.swift; sourceTree = "<group>"; };
388390
62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitRuleTests.swift; sourceTree = "<group>"; };
389391
65454F451B14D73800319A6C /* ControlStatementRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlStatementRule.swift; sourceTree = "<group>"; };
390392
67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CyclomaticComplexityConfigurationTests.swift; sourceTree = "<group>"; };
@@ -935,6 +937,7 @@
935937
D4FD4C841F2A260A00DD8AA8 /* BlockBasedKVORule.swift */,
936938
D4B0228D1E0CC608007E5297 /* ClassDelegateProtocolRule.swift */,
937939
1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */,
940+
62A6E7941F3344CF003A0479 /* ClosureBodyLengthRule.swift */,
938941
D43B046A1E075905004016AF /* ClosureEndIndentationRule.swift */,
939942
D47079A81DFDBED000027086 /* ClosureParameterPositionRule.swift */,
940943
1E82D5581D7775C7009553D7 /* ClosureSpacingRule.swift */,
@@ -1419,6 +1422,7 @@
14191422
D4130D991E16CC1300242361 /* TypeNameRuleExamples.swift in Sources */,
14201423
24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */,
14211424
47ACC8981E7DC74E0088EEB2 /* ImplicitlyUnwrappedOptionalConfiguration.swift in Sources */,
1425+
62A6E7951F3344CF003A0479 /* ClosureBodyLengthRule.swift in Sources */,
14221426
009E09281DFEE4C200B588A7 /* ProhibitedSuperRule.swift in Sources */,
14231427
E80E018F1B92C1350078EB70 /* Region.swift in Sources */,
14241428
E88198581BEA956C00333A11 /* FunctionBodyLengthRule.swift in Sources */,

Tests/LinuxMain.swift

+1
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ extension RulesTests {
340340
("testBlockBasedKVO", testBlockBasedKVO),
341341
("testClassDelegateProtocol", testClassDelegateProtocol),
342342
("testClosingBrace", testClosingBrace),
343+
("testClosureBodyLength", testClosureBodyLength),
343344
("testClosureEndIndentation", testClosureEndIndentation),
344345
("testClosureParameterPosition", testClosureParameterPosition),
345346
("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)