Merge pull request #1862 from sirlantis/add-new-multiline-arguments-rule

Add MultilineArgumentsRule
This commit is contained in:
Marcelo Fabri
2017-10-02 00:20:28 -03:00
committed by GitHub
9 changed files with 431 additions and 0 deletions
+5
View File
@@ -38,6 +38,11 @@
[JP Simard](https://github.com/jpsim)
[#1002](https://github.com/realm/SwiftLint/issues/1002)
* Add `multiline_arguments` opt-in rule that warns to either keep
all the arguments of a function call on the same line,
or one per line.
[Marcel Jackwerth](https://github.com/sirlantis)
##### Bug Fixes
* Improve how `opening_brace` rule reports violations locations.
+126
View File
@@ -56,6 +56,7 @@
* [Variable Declaration Whitespace](#variable-declaration-whitespace)
* [Line Length](#line-length)
* [Mark](#mark)
* [Multiline Arguments](#multiline-arguments)
* [Multiline Parameters](#multiline-parameters)
* [Multiple Closures with Trailing Closure](#multiple-closures-with-trailing-closure)
* [Nesting](#nesting)
@@ -6529,6 +6530,131 @@ extension MarkTest {}
## Multiline Arguments
Identifier | Enabled by default | Supports autocorrection | Kind
--- | --- | --- | ---
`multiline_arguments` | Disabled | No | style
Arguments should be either on the same line, or one per line.
### Examples
<details>
<summary>Non Triggering Examples</summary>
```swift
foo()
```
```swift
foo(
)
```
```swift
foo { }
```
```swift
foo {
}
```
```swift
foo(0)
```
```swift
foo(0, 1)
```
```swift
foo(0, 1) { }
```
```swift
foo(0, param1: 1)
```
```swift
foo(0, param1: 1) { }
```
```swift
foo(param1: 1)
```
```swift
foo(param1: 1) { }
```
```swift
foo(param1: 1, param2: true) { }
```
```swift
foo(param1: 1, param2: true, param3: [3]) { }
```
```swift
foo(param1: 1, param2: true, param3: [3]) {
bar()
}
```
```swift
foo(param1: 1,
param2: true,
param3: [3])
```
```swift
foo(
param1: 1, param2: true, param3: [3]
)
```
```swift
foo(
param1: 1,
param2: true,
param3: [3]
)
```
</details>
<details>
<summary>Triggering Examples</summary>
```swift
foo(0,
param1: 1, param2: true, param3: [3])
```
```swift
foo(0, param1: 1,
param2: true, param3: [3])
```
```swift
foo(0, param1: 1, param2: true,
param3: [3])
```
```swift
foo(
0, param1: 1,
param2: true, param3: [3]
)
```
</details>
## Multiline Parameters
Identifier | Enabled by default | Supports autocorrection | Kind
@@ -64,6 +64,7 @@ public let masterRuleList = RuleList(rules: [
LetVarWhitespaceRule.self,
LineLengthRule.self,
MarkRule.self,
MultilineArgumentsRule.self,
MultilineParametersRule.self,
MultipleClosuresWithTrailingClosureRule.self,
NestingRule.self,
@@ -0,0 +1,59 @@
//
// MultilineArgumentsConfiguration.swift
// SwiftLint
//
// Created by Marcel Jackwerth on 9/29/17.
// Copyright © 2017 Realm. All rights reserved.
//
import Foundation
private enum ConfigurationKey: String {
case firstArgumentLocation = "first_argument_location"
}
public struct MultilineArgumentsConfiguration: RuleConfiguration, Equatable {
public enum FirstArgumentLocation: String {
case anyLine = "any_line"
case sameLine = "same_line"
case nextLine = "next_line"
init(value: Any) throws {
guard
let string = (value as? String)?.lowercased(),
let value = FirstArgumentLocation(rawValue: string) else {
throw ConfigurationError.unknownConfiguration
}
self = value
}
}
private(set) var severityConfiguration = SeverityConfiguration(.warning)
private(set) var firstArgumentLocation = FirstArgumentLocation.anyLine
public var consoleDescription: String {
return severityConfiguration.consoleDescription +
", \(ConfigurationKey.firstArgumentLocation.rawValue): \(firstArgumentLocation.rawValue)"
}
public mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw ConfigurationError.unknownConfiguration
}
if let modeString = configuration[ConfigurationKey.firstArgumentLocation.rawValue] {
try firstArgumentLocation = FirstArgumentLocation(value: modeString)
}
if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}
}
public static func == (lhs: MultilineArgumentsConfiguration,
rhs: MultilineArgumentsConfiguration) -> Bool {
return lhs.severityConfiguration == rhs.severityConfiguration &&
lhs.firstArgumentLocation == rhs.firstArgumentLocation
}
}
@@ -0,0 +1,88 @@
//
// MultilineArgumentsRule.swift
// SwiftLint
//
// Created by Marcel Jackwerth on 09/29/17.
// Copyright © 2017 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
public struct MultilineArgumentsRule: ASTRule, OptInRule, ConfigurationProviderRule {
public var configuration = MultilineArgumentsConfiguration()
public init() {}
public static let description = RuleDescription(
identifier: "multiline_arguments",
name: "Multiline Arguments",
description: "Arguments should be either on the same line, or one per line.",
kind: .style,
nonTriggeringExamples: MultilineArgumentsRuleExamples.nonTriggeringExamples,
triggeringExamples: MultilineArgumentsRuleExamples.triggeringExamples
)
public func validate(file: File,
kind: SwiftExpressionKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
guard
kind == .call,
case let arguments = dictionary.enclosedArguments,
arguments.count > 1,
case let contents = file.contents.bridge(),
let nameOffset = dictionary.nameOffset,
let (nameLine, _) = contents.lineAndCharacter(forByteOffset: nameOffset) else {
return []
}
var visitedLines = Set<Int>()
if configuration.firstArgumentLocation == .sameLine {
visitedLines.insert(nameLine)
}
let lastIndex = arguments.count - 1
let violatingOffsets: [Int] = arguments.enumerated().flatMap { idx, argument in
guard
let offset = argument.offset,
let (line, _) = contents.lineAndCharacter(forByteOffset: offset) else {
return nil
}
let (firstVisit, _) = visitedLines.insert(line)
if idx == lastIndex && isTrailingClosure(dictionary: dictionary, file: file) {
return nil
} else if idx == 0 {
switch configuration.firstArgumentLocation {
case .anyLine: return nil
case .nextLine: return line > nameLine ? nil : offset
case .sameLine: return line > nameLine ? offset : nil
}
} else {
return firstVisit ? nil : offset
}
}
// only report violations if multiline
guard visitedLines.count > 1 else { return [] }
return violatingOffsets.map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: $0))
}
}
private func isTrailingClosure(dictionary: [String: SourceKitRepresentable], file: File) -> Bool {
guard let offset = dictionary.offset,
let length = dictionary.length,
case let start = min(offset, offset + length - 1),
let text = file.contents.bridge().substringWithByteRange(start: start, length: length) else {
return false
}
return !text.hasSuffix(")")
}
}
@@ -0,0 +1,58 @@
//
// MultilineArgumentsRuleExamples.swift
// SwiftLint
//
// Created by Marcel Jackwerth on 09/29/17.
// Copyright © 2017 Realm. All rights reserved.
//
import Foundation
internal struct MultilineArgumentsRuleExamples {
static let nonTriggeringExamples = [
"foo()",
"foo(\n" +
" \n" +
")",
"foo { }",
"foo {\n" +
" \n" +
"}",
"foo(0)",
"foo(0, 1)",
"foo(0, 1) { }",
"foo(0, param1: 1)",
"foo(0, param1: 1) { }",
"foo(param1: 1)",
"foo(param1: 1) { }",
"foo(param1: 1, param2: true) { }",
"foo(param1: 1, param2: true, param3: [3]) { }",
"foo(param1: 1, param2: true, param3: [3]) {\n" +
" bar()\n" +
"}",
"foo(param1: 1,\n" +
" param2: true,\n" +
" param3: [3])",
"foo(\n" +
" param1: 1, param2: true, param3: [3]\n" +
")",
"foo(\n" +
" param1: 1,\n" +
" param2: true,\n" +
" param3: [3]\n" +
")"
]
static let triggeringExamples = [
"foo(0,\n" +
" param1: 1, ↓param2: true, ↓param3: [3])",
"foo(0, ↓param1: 1,\n" +
" param2: true, ↓param3: [3])",
"foo(0, ↓param1: 1, ↓param2: true,\n" +
" param3: [3])",
"foo(\n" +
" 0, ↓param1: 1,\n" +
" param2: true, ↓param3: [3]\n" +
")"
]
}
+16
View File
@@ -105,6 +105,10 @@
92CCB2D71E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92CCB2D61E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift */; };
93E0C3CE1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E0C3CD1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift */; };
A1A6F3F21EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A6F3F11EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift */; };
B25DCD0B1F7E9F9E0028A199 /* MultilineArgumentsRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25DCD091F7E9BB50028A199 /* MultilineArgumentsRuleExamples.swift */; };
B25DCD0C1F7E9FA20028A199 /* MultilineArgumentsRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25DCD071F7E9B5F0028A199 /* MultilineArgumentsRule.swift */; };
B25DCD0E1F7EF2280028A199 /* MultilineArgumentsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25DCD0D1F7EF2280028A199 /* MultilineArgumentsConfiguration.swift */; };
B25DCD101F7EF6DC0028A199 /* MultilineArgumentsRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25DCD0F1F7EF6DC0028A199 /* MultilineArgumentsRuleTests.swift */; };
B2902A0C1D66815600BFCCF7 /* PrivateUnitTestRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2902A0B1D66815600BFCCF7 /* PrivateUnitTestRule.swift */; };
B2902A0E1D6681F700BFCCF7 /* PrivateUnitTestConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2902A0D1D6681F700BFCCF7 /* PrivateUnitTestConfiguration.swift */; };
B3935371E92E0CF3F7668303 /* CannedJunitReporterOutput.xml in Resources */ = {isa = PBXBuildFile; fileRef = B39359A325FE84B7EDD1C455 /* CannedJunitReporterOutput.xml */; };
@@ -428,6 +432,10 @@
92CCB2D61E1EEFA300C8E5A3 /* UnusedOptionalBindingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedOptionalBindingRule.swift; sourceTree = "<group>"; };
93E0C3CD1D67BD7F007FA25D /* ConditionalReturnsOnNewlineRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionalReturnsOnNewlineRule.swift; sourceTree = "<group>"; };
A1A6F3F11EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectLiteralConfiguration.swift; sourceTree = "<group>"; };
B25DCD071F7E9B5F0028A199 /* MultilineArgumentsRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultilineArgumentsRule.swift; sourceTree = "<group>"; };
B25DCD091F7E9BB50028A199 /* MultilineArgumentsRuleExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultilineArgumentsRuleExamples.swift; sourceTree = "<group>"; };
B25DCD0D1F7EF2280028A199 /* MultilineArgumentsConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultilineArgumentsConfiguration.swift; sourceTree = "<group>"; };
B25DCD0F1F7EF6DC0028A199 /* MultilineArgumentsRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultilineArgumentsRuleTests.swift; sourceTree = "<group>"; };
B2902A0B1D66815600BFCCF7 /* PrivateUnitTestRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateUnitTestRule.swift; sourceTree = "<group>"; };
B2902A0D1D6681F700BFCCF7 /* PrivateUnitTestConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateUnitTestConfiguration.swift; sourceTree = "<group>"; };
B3935001033261E5A70CE101 /* CannedEmojiReporterOutput.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CannedEmojiReporterOutput.txt; sourceTree = "<group>"; };
@@ -889,6 +897,7 @@
3B63D46E1E1F09DF0057BE35 /* LineLengthRuleTests.swift */,
D4C27BFF1E12DFF500DF713E /* LinterCacheTests.swift */,
D4CA758E1E2DEEA500A40E8A /* NumberSeparatorRuleTests.swift */,
B25DCD0F1F7EF6DC0028A199 /* MultilineArgumentsRuleTests.swift */,
825F19D01EEFF19700969EF1 /* ObjectLiteralRuleTests.swift */,
D4246D6E1F30DB260097E658 /* PrivateOverFilePrivateRuleTests.swift */,
E81ADD711ED5ED9D000CD451 /* RegionTests.swift */,
@@ -1023,6 +1032,9 @@
C946FEC91EAE5E20007DD778 /* LetVarWhitespaceRule.swift */,
E88DEA7B1B098D7D00A66CB0 /* LineLengthRule.swift */,
856651A61D6B395F005E6B29 /* MarkRule.swift */,
B25DCD0D1F7EF2280028A199 /* MultilineArgumentsConfiguration.swift */,
B25DCD071F7E9B5F0028A199 /* MultilineArgumentsRule.swift */,
B25DCD091F7E9BB50028A199 /* MultilineArgumentsRuleExamples.swift */,
6238AE411ED4D734006C3601 /* MultilineParametersRule.swift */,
621061BE1ED57E640082D51E /* MultilineParametersRuleExamples.swift */,
BB00B4E71F5216070079869F /* MultipleClosuresWithTrailingClosureRule.swift */,
@@ -1403,6 +1415,7 @@
623E36F01F3DB1B1002E5B71 /* QuickDiscouragedCallRule.swift in Sources */,
BFF028AE1CBCF8A500B38A9D /* TrailingWhitespaceConfiguration.swift in Sources */,
3B034B6E1E0BE549005D49A9 /* LineLengthConfiguration.swift in Sources */,
B25DCD0C1F7E9FA20028A199 /* MultilineArgumentsRule.swift in Sources */,
D4C4A34C1DEA4FF000E0E04C /* AttributesConfiguration.swift in Sources */,
83D71E281B131ECE000395DE /* RuleDescription.swift in Sources */,
D4470D571EB69225008A1B2E /* ImplicitReturnRule.swift in Sources */,
@@ -1515,12 +1528,14 @@
E88DEA771B098D0C00A66CB0 /* Rule.swift in Sources */,
D47079AB1DFDCF7A00027086 /* SwiftExpressionKind.swift in Sources */,
00B8D9791E2D1223004E0EEC /* LegacyConstantRuleExamples.swift in Sources */,
B25DCD0E1F7EF2280028A199 /* MultilineArgumentsConfiguration.swift in Sources */,
24B4DF0D1D6DFDE90097803B /* RedundantNilCoalescingRule.swift in Sources */,
D4130D971E16183F00242361 /* IdentifierNameRuleExamples.swift in Sources */,
7250948A1D0859260039B353 /* StatementPositionConfiguration.swift in Sources */,
E81619531BFC162C00946723 /* QueuedPrint.swift in Sources */,
E87E4A051BFB927C00FCFE46 /* TrailingSemicolonRule.swift in Sources */,
D4B472411F66486300BD6EF1 /* FallthroughRule.swift in Sources */,
B25DCD0B1F7E9F9E0028A199 /* MultilineArgumentsRuleExamples.swift in Sources */,
E88198421BEA929F00333A11 /* NestingRule.swift in Sources */,
D46A317F1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift in Sources */,
D4470D591EB6B4D1008A1B2E /* EmptyEnumArgumentsRule.swift in Sources */,
@@ -1604,6 +1619,7 @@
3B63D46D1E1F05160057BE35 /* LineLengthConfigurationTests.swift in Sources */,
6C7045441C6ADA450003F15A /* SourceKitCrashTests.swift in Sources */,
D4246D6F1F30DB260097E658 /* PrivateOverFilePrivateRuleTests.swift in Sources */,
B25DCD101F7EF6DC0028A199 /* MultilineArgumentsRuleTests.swift in Sources */,
3BB47D871C51DE6E00AE6A10 /* CustomRulesTests.swift in Sources */,
E812249A1B04F85B001783D2 /* TestHelpers.swift in Sources */,
3B20CD0C1EB699C20069EF2E /* TypeNameRuleTests.swift in Sources */,
+9
View File
@@ -253,6 +253,14 @@ extension LinterCacheTests {
]
}
extension MultilineArgumentsRuleTests {
static var allTests: [(String, (MultilineArgumentsRuleTests) -> () throws -> Void)] = [
("testMultilineArgumentsWithDefaultConfiguration", testMultilineArgumentsWithDefaultConfiguration),
("testMultilineArgumentsWithWithNextLine", testMultilineArgumentsWithWithNextLine),
("testMultilineArgumentsWithWithSameLine", testMultilineArgumentsWithWithSameLine)
]
}
extension NumberSeparatorRuleTests {
static var allTests: [(String, (NumberSeparatorRuleTests) -> () throws -> Void)] = [
("testNumberSeparatorWithDefaultConfiguration", testNumberSeparatorWithDefaultConfiguration),
@@ -533,6 +541,7 @@ XCTMain([
testCase(LineLengthConfigurationTests.allTests),
testCase(LineLengthRuleTests.allTests),
testCase(LinterCacheTests.allTests),
testCase(MultilineArgumentsRuleTests.allTests),
testCase(NumberSeparatorRuleTests.allTests),
testCase(ObjectLiteralRuleTests.allTests),
testCase(PrivateOverFilePrivateRuleTests.allTests),
@@ -0,0 +1,69 @@
//
// MultilineArgumentsRuleTests.swift
// SwiftLint
//
// Created by Marcel Jackwerth on 09/29/17.
// Copyright © 2017 Realm. All rights reserved.
//
import SwiftLintFramework
import XCTest
class MultilineArgumentsRuleTests: XCTestCase {
func testMultilineArgumentsWithDefaultConfiguration() {
verifyRule(MultilineArgumentsRule.description)
}
func testMultilineArgumentsWithWithNextLine() {
let nonTriggeringExamples = [
"foo()",
"foo(0)",
"foo(1, bar: baz) { }",
"foo(2, bar: baz) {\n}",
"foo(\n" +
" 3,\n" +
" bar: baz) { }",
"foo(\n" +
" 4, bar: baz) { }"
]
let triggeringExamples = [
"foo(↓1,\n" +
" bar: baz) { }"
]
let description = MultilineArgumentsRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExamples)
verifyRule(description, ruleConfiguration: ["first_argument_location": "next_line"])
}
func testMultilineArgumentsWithWithSameLine() {
let nonTriggeringExamples = [
"foo()",
"foo(0)",
"foo(1, bar: 1) { }",
"foo(2, bar: 2) {\n" +
" bar()\n" +
"}",
"foo(3,\n" +
" bar: 3) { }"
]
let triggeringExamples = [
"foo(\n" +
" ↓1, ↓bar: baz) { }",
"foo(\n" +
" ↓2,\n" +
" bar: baz) { }"
]
let description = MultilineArgumentsRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExamples)
verifyRule(description, ruleConfiguration: ["first_argument_location": "same_line"])
}
}