Skip to content

Commit c0bc848

Browse files
committed
Create multiline_call_arguments rule
1 parent 110c45e commit c0bc848

File tree

10 files changed

+228
-30
lines changed

10 files changed

+228
-30
lines changed

Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public let builtInRules: [any Rule.Type] = [
118118
ModifierOrderRule.self,
119119
MultilineArgumentsBracketsRule.self,
120120
MultilineArgumentsRule.self,
121+
MultilineCallArgumentsRule.self,
121122
MultilineFunctionChainsRule.self,
122123
MultilineLiteralBracketsRule.self,
123124
MultilineParametersBracketsRule.self,
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import SwiftLintCore
2+
import SwiftSyntax
3+
4+
@SwiftSyntaxRule(optIn: true)
5+
struct MultilineCallArgumentsRule: Rule {
6+
var configuration = MultilineCallArgumentsConfiguration()
7+
8+
static let description = RuleDescription(
9+
identifier: "multiline_call_arguments",
10+
name: "Multiline Call Arguments",
11+
description: """
12+
Arguments in function and method calls should be either on the same line \
13+
or one per line when the call spans multiple lines
14+
""",
15+
kind: .style,
16+
nonTriggeringExamples: [
17+
Example("""
18+
foo(
19+
param1: "param1",
20+
param2: false,
21+
param3: []
22+
)
23+
""", configuration: ["max_number_of_single_line_parameters": 2]),
24+
Example("""
25+
foo(param1: 1,
26+
param2: false,
27+
param3: [])
28+
""", configuration: ["max_number_of_single_line_parameters": 1]),
29+
Example("foo(param1: 1, param2: false)",
30+
configuration: ["max_number_of_single_line_parameters": 2]),
31+
Example("Enum.foo(param1: 1, param2: false)",
32+
configuration: ["max_number_of_single_line_parameters": 2]),
33+
Example("""
34+
foo(param1: 1)
35+
""",
36+
configuration: [
37+
"allows_single_line": false
38+
]),
39+
Example("""
40+
Enum.foo(param1: 1)
41+
""",
42+
configuration: [
43+
"allows_single_line": false
44+
]),
45+
Example("""
46+
Enum.foo(param1: 1, param2: 2, param3: 3)
47+
""",
48+
configuration: [
49+
"allows_single_line": true
50+
]),
51+
Example("""
52+
foo(
53+
param1: 1,
54+
param2: 2,
55+
param3: 3
56+
)
57+
""",
58+
configuration: [
59+
"allows_single_line": false
60+
]),
61+
],
62+
triggeringExamples: [
63+
Example("↓foo(param1: 1, param2: false, param3: [])",
64+
configuration: [
65+
"max_number_of_single_line_parameters": 2
66+
]),
67+
Example("↓Enum.foo(param1: 1, param2: false, param3: [])",
68+
configuration: [
69+
"max_number_of_single_line_parameters": 2
70+
]),
71+
Example("""
72+
↓foo(param1: 1, param2: false,
73+
param3: [])
74+
""", configuration: [
75+
"max_number_of_single_line_parameters": 3
76+
]),
77+
Example("""
78+
↓Enum.foo(param1: 1, param2: false,
79+
param3: [])
80+
""", configuration: [
81+
"max_number_of_single_line_parameters": 3
82+
]),
83+
Example("""
84+
↓foo(param1: 1, param2: false)
85+
""",
86+
configuration: [
87+
"allows_single_line": false
88+
]),
89+
Example("""
90+
↓Enum.foo(param1: 1, param2: false)
91+
""",
92+
configuration: [
93+
"allows_single_line": false
94+
]),
95+
]
96+
)
97+
}
98+
99+
private extension MultilineCallArgumentsRule {
100+
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
101+
override func visitPost(_ node: FunctionCallExprSyntax) {
102+
guard node.arguments.isNotEmpty else { return }
103+
guard node.trailingClosure == nil else { return }
104+
105+
if containsViolation(for: node.arguments) {
106+
let anchor = node.calledExpression.positionAfterSkippingLeadingTrivia
107+
violations.append(anchor)
108+
}
109+
}
110+
111+
private func containsViolation(for arguments: LabeledExprListSyntax) -> Bool {
112+
let argumentPositions = arguments.map(\.positionAfterSkippingLeadingTrivia)
113+
return containsViolation(parameterPositions: argumentPositions)
114+
}
115+
116+
private func containsViolation(parameterPositions: [AbsolutePosition]) -> Bool {
117+
guard parameterPositions.isNotEmpty else {
118+
return false
119+
}
120+
121+
var numberOfParameters = 0
122+
var linesWithParameters: Set<Int> = []
123+
var hasMultipleParametersOnSameLine = false
124+
125+
for position in parameterPositions {
126+
let line = locationConverter.location(for: position).line
127+
128+
if !linesWithParameters.insert(line).inserted {
129+
hasMultipleParametersOnSameLine = true
130+
}
131+
132+
numberOfParameters += 1
133+
}
134+
135+
if linesWithParameters.count == 1 {
136+
guard configuration.allowsSingleLine else {
137+
return numberOfParameters > 1
138+
}
139+
140+
if let maxNumberOfSingleLineParameters = configuration.maxNumberOfSingleLineParameters {
141+
return numberOfParameters > maxNumberOfSingleLineParameters
142+
}
143+
144+
return false
145+
}
146+
147+
return hasMultipleParametersOnSameLine
148+
}
149+
}
150+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import SwiftLintCore
2+
3+
@AutoConfigParser
4+
struct MultilineCallArgumentsConfiguration: SeverityBasedRuleConfiguration {
5+
typealias Parent = MultilineCallArgumentsRule
6+
7+
@ConfigurationElement(key: "severity")
8+
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
9+
@ConfigurationElement(key: "allows_single_line")
10+
private(set) var allowsSingleLine = true
11+
@ConfigurationElement(key: "max_number_of_single_line_parameters")
12+
private(set) var maxNumberOfSingleLineParameters: Int?
13+
14+
func validate() throws(Issue) {
15+
guard let maxNumberOfSingleLineParameters else {
16+
return
17+
}
18+
guard maxNumberOfSingleLineParameters >= 1 else {
19+
throw Issue.inconsistentConfiguration(
20+
ruleID: Parent.identifier,
21+
message: "Option '\($maxNumberOfSingleLineParameters.key)' should be >= 1."
22+
)
23+
}
24+
25+
if maxNumberOfSingleLineParameters > 1, !allowsSingleLine {
26+
throw Issue.inconsistentConfiguration(
27+
ruleID: Parent.identifier,
28+
message: """
29+
Option '\($maxNumberOfSingleLineParameters.key)' has no effect when \
30+
'\($allowsSingleLine.key)' is false.
31+
"""
32+
)
33+
}
34+
}
35+
}

Tests/GeneratedTests/GeneratedTests_05.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ final class MultilineArgumentsRuleGeneratedTests: SwiftLintTestCase {
103103
}
104104
}
105105

106+
final class MultilineCallArgumentsRuleGeneratedTests: SwiftLintTestCase {
107+
func testWithDefaultConfiguration() {
108+
verifyRule(MultilineCallArgumentsRule.description)
109+
}
110+
}
111+
106112
final class MultilineFunctionChainsRuleGeneratedTests: SwiftLintTestCase {
107113
func testWithDefaultConfiguration() {
108114
verifyRule(MultilineFunctionChainsRule.description)
@@ -150,9 +156,3 @@ final class NSNumberInitAsFunctionReferenceRuleGeneratedTests: SwiftLintTestCase
150156
verifyRule(NSNumberInitAsFunctionReferenceRule.description)
151157
}
152158
}
153-
154-
final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(NSObjectPreferIsEqualRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_06.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(NSObjectPreferIsEqualRule.description)
13+
}
14+
}
15+
1016
final class NestingRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(NestingRule.description)
@@ -150,9 +156,3 @@ final class PreferAssetSymbolsRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(PreferAssetSymbolsRule.description)
151157
}
152158
}
153-
154-
final class PreferConditionListRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(PreferConditionListRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_07.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class PreferConditionListRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(PreferConditionListRule.description)
13+
}
14+
}
15+
1016
final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(PreferKeyPathRule.description)
@@ -150,9 +156,3 @@ final class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(RedundantNilCoalescingRule.description)
151157
}
152158
}
153-
154-
final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(RedundantObjcAttributeRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_08.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(RedundantObjcAttributeRule.description)
13+
}
14+
}
15+
1016
final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(RedundantSelfInClosureRule.description)
@@ -150,9 +156,3 @@ final class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(StrongIBOutletRule.description)
151157
}
152158
}
153-
154-
final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(SuperfluousElseRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_09.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(SuperfluousElseRule.description)
13+
}
14+
}
15+
1016
final class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(SwitchCaseAlignmentRule.description)
@@ -150,9 +156,3 @@ final class UnneededThrowsRuleGeneratedTests: SwiftLintTestCase {
150156
verifyRule(UnneededThrowsRule.description)
151157
}
152158
}
153-
154-
final class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase {
155-
func testWithDefaultConfiguration() {
156-
verifyRule(UnownedVariableCaptureRule.description)
157-
}
158-
}

Tests/GeneratedTests/GeneratedTests_10.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
@testable import SwiftLintCore
88
import TestHelpers
99

10+
final class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase {
11+
func testWithDefaultConfiguration() {
12+
verifyRule(UnownedVariableCaptureRule.description)
13+
}
14+
}
15+
1016
final class UntypedErrorInCatchRuleGeneratedTests: SwiftLintTestCase {
1117
func testWithDefaultConfiguration() {
1218
verifyRule(UntypedErrorInCatchRule.description)

Tests/IntegrationTests/default_rule_configurations.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,12 @@ multiline_arguments_brackets:
678678
meta:
679679
opt-in: true
680680
correctable: false
681+
multiline_call_arguments:
682+
severity: warning
683+
allows_single_line: true
684+
meta:
685+
opt-in: true
686+
correctable: false
681687
multiline_function_chains:
682688
severity: warning
683689
meta:

0 commit comments

Comments
 (0)