Add new multiline_call_arguments rule (#6223)

Co-authored-by: Danny Mösch <danny.moesch@icloud.com>
This commit is contained in:
Rodion Ivashkov
2025-12-01 02:16:11 +03:00
committed by GitHub
parent 3a896df138
commit 31cdc24101
12 changed files with 192 additions and 30 deletions
+1
View File
@@ -32,6 +32,7 @@ disabled_rules:
- missing_docs
- multiline_arguments
- multiline_arguments_brackets
- multiline_call_arguments
- multiline_function_chains
- multiline_parameters_brackets
- no_extension_access_modifier
+4
View File
@@ -31,6 +31,10 @@
* Add new `unneeded_escaping` rule that detects closure parameters marked with
`@escaping` that are never stored or captured escapingly.
[SimplyDanny](https://github.com/SimplyDanny)
* Add `multiline_call_arguments` opt-in rule to enforce consistent multiline
formatting for function and method call arguments.
[GandaLF2006](https://github.com/GandaLF2006)
### Bug Fixes
@@ -118,6 +118,7 @@ public let builtInRules: [any Rule.Type] = [
ModifierOrderRule.self,
MultilineArgumentsBracketsRule.self,
MultilineArgumentsRule.self,
MultilineCallArgumentsRule.self,
MultilineFunctionChainsRule.self,
MultilineLiteralBracketsRule.self,
MultilineParametersBracketsRule.self,
@@ -0,0 +1,109 @@
import SwiftLintCore
import SwiftSyntax
@SwiftSyntaxRule(optIn: true)
struct MultilineCallArgumentsRule: Rule {
var configuration = MultilineCallArgumentsConfiguration()
static let description = RuleDescription(
identifier: "multiline_call_arguments",
name: "Multiline Call Arguments",
description: "Call should have each parameter on a separate line",
kind: .style,
nonTriggeringExamples: [
Example("""
foo(
param1: "param1",
param2: false,
param3: []
)
""",
configuration: ["max_number_of_single_line_parameters": 2]),
Example("""
foo(param1: 1,
param2: false,
param3: [])
""",
configuration: ["max_number_of_single_line_parameters": 1]),
Example(
"foo(param1: 1, param2: false)",
configuration: ["max_number_of_single_line_parameters": 2]),
Example(
"Enum.foo(param1: 1, param2: false)",
configuration: ["max_number_of_single_line_parameters": 2]),
Example("foo(param1: 1)", configuration: ["allows_single_line": false]),
Example("Enum.foo(param1: 1)", configuration: ["allows_single_line": false]),
Example(
"Enum.foo(param1: 1, param2: 2, param3: 3)",
configuration: ["allows_single_line": true]),
Example("""
foo(
param1: 1,
param2: 2,
param3: 3
)
""",
configuration: ["allows_single_line": false]),
],
triggeringExamples: [
Example(
"↓foo(param1: 1, param2: false, param3: [])",
configuration: ["max_number_of_single_line_parameters": 2]),
Example(
"↓Enum.foo(param1: 1, param2: false, param3: [])",
configuration: ["max_number_of_single_line_parameters": 2]),
Example("""
↓foo(param1: 1, param2: false,
param3: [])
""",
configuration: ["max_number_of_single_line_parameters": 3]),
Example("""
↓Enum.foo(param1: 1, param2: false,
param3: [])
""",
configuration: ["max_number_of_single_line_parameters": 3]),
Example("↓foo(param1: 1, param2: false)", configuration: ["allows_single_line": false]),
Example("↓Enum.foo(param1: 1, param2: false)", configuration: ["allows_single_line": false]),
]
)
}
private extension MultilineCallArgumentsRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: FunctionCallExprSyntax) {
if containsViolation(parameterPositions: node.arguments.map(\.positionAfterSkippingLeadingTrivia)) {
violations.append(node.calledExpression.positionAfterSkippingLeadingTrivia)
}
}
private func containsViolation(parameterPositions: [AbsolutePosition]) -> Bool {
var numberOfParameters = 0
var linesWithParameters: Set<Int> = []
var hasMultipleParametersOnSameLine = false
for position in parameterPositions {
let line = locationConverter.location(for: position).line
if !linesWithParameters.insert(line).inserted {
hasMultipleParametersOnSameLine = true
}
numberOfParameters += 1
}
if linesWithParameters.count == 1 {
guard configuration.allowsSingleLine else {
return numberOfParameters > 1
}
if let maxNumberOfSingleLineParameters = configuration.maxNumberOfSingleLineParameters {
return numberOfParameters > maxNumberOfSingleLineParameters
}
return false
}
return hasMultipleParametersOnSameLine
}
}
}
@@ -0,0 +1,35 @@
import SwiftLintCore
@AutoConfigParser
struct MultilineCallArgumentsConfiguration: SeverityBasedRuleConfiguration {
typealias Parent = MultilineCallArgumentsRule
@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
@ConfigurationElement(key: "allows_single_line")
private(set) var allowsSingleLine = true
@ConfigurationElement(key: "max_number_of_single_line_parameters")
private(set) var maxNumberOfSingleLineParameters: Int?
func validate() throws(Issue) {
guard let maxNumberOfSingleLineParameters else {
return
}
guard maxNumberOfSingleLineParameters >= 1 else {
throw Issue.inconsistentConfiguration(
ruleID: Parent.identifier,
message: "Option '\($maxNumberOfSingleLineParameters.key)' should be >= 1."
)
}
if maxNumberOfSingleLineParameters > 1, !allowsSingleLine {
throw Issue.inconsistentConfiguration(
ruleID: Parent.identifier,
message: """
Option '\($maxNumberOfSingleLineParameters.key)' has no effect when \
'\($allowsSingleLine.key)' is false.
"""
)
}
}
}
+6 -6
View File
@@ -103,6 +103,12 @@ final class MultilineArgumentsRuleGeneratedTests: SwiftLintTestCase {
}
}
final class MultilineCallArgumentsRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(MultilineCallArgumentsRule.description)
}
}
final class MultilineFunctionChainsRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(MultilineFunctionChainsRule.description)
@@ -150,9 +156,3 @@ final class NSNumberInitAsFunctionReferenceRuleGeneratedTests: SwiftLintTestCase
verifyRule(NSNumberInitAsFunctionReferenceRule.description)
}
}
final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(NSObjectPreferIsEqualRule.description)
}
}
+6 -6
View File
@@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers
final class NSObjectPreferIsEqualRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(NSObjectPreferIsEqualRule.description)
}
}
final class NestingRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(NestingRule.description)
@@ -150,9 +156,3 @@ final class PreferAssetSymbolsRuleGeneratedTests: SwiftLintTestCase {
verifyRule(PreferAssetSymbolsRule.description)
}
}
final class PreferConditionListRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(PreferConditionListRule.description)
}
}
+6 -6
View File
@@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers
final class PreferConditionListRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(PreferConditionListRule.description)
}
}
final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(PreferKeyPathRule.description)
@@ -150,9 +156,3 @@ final class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase {
verifyRule(RedundantNilCoalescingRule.description)
}
}
final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(RedundantObjcAttributeRule.description)
}
}
+6 -6
View File
@@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers
final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(RedundantObjcAttributeRule.description)
}
}
final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(RedundantSelfInClosureRule.description)
@@ -150,9 +156,3 @@ final class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase {
verifyRule(StrongIBOutletRule.description)
}
}
final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(SuperfluousElseRule.description)
}
}
+6 -6
View File
@@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers
final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(SuperfluousElseRule.description)
}
}
final class SwitchCaseAlignmentRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(SwitchCaseAlignmentRule.description)
@@ -150,9 +156,3 @@ final class UnneededThrowsRuleGeneratedTests: SwiftLintTestCase {
verifyRule(UnneededThrowsRule.description)
}
}
final class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(UnownedVariableCaptureRule.description)
}
}
@@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers
final class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(UnownedVariableCaptureRule.description)
}
}
final class UntypedErrorInCatchRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(UntypedErrorInCatchRule.description)
@@ -678,6 +678,12 @@ multiline_arguments_brackets:
meta:
opt-in: true
correctable: false
multiline_call_arguments:
severity: warning
allows_single_line: true
meta:
opt-in: true
correctable: false
multiline_function_chains:
severity: warning
meta: