Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
[SimplyDanny](https://github.com/SimplyDanny)
[#6451](https://github.com/realm/SwiftLint/issues/6451)

* `multiline_call_arguments` no longer reports violations for enum-case patterns in
pattern matching (e.g. if case, switch case, for case, catch).
[GandaLF2006](https://github.com/GandaLF2006)

## 0.63.1: High-Speed Extraction

### Breaking
Expand Down
226 changes: 150 additions & 76 deletions Source/SwiftLintBuiltInRules/Rules/Lint/MultilineCallArgumentsRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,105 +5,179 @@ import SwiftSyntax
struct MultilineCallArgumentsRule: Rule {
var configuration = MultilineCallArgumentsConfiguration()

enum Reason {
static let singleLineMultipleArgumentsNotAllowed =
"Single-line calls with multiple arguments are not allowed"

static func tooManyArgumentsOnSingleLine(max: Int) -> String {
"Too many arguments on a single line (max: \(max))"
}

static let eachArgumentMustStartOnOwnLine =
"In multi-line calls, each argument must start on its own line"
}

static let description = RuleDescription(
identifier: "multiline_call_arguments",
name: "Multiline Call Arguments",
description: "Call should have each parameter on a separate line",
description: """
Enforces one-argument-per-line for multi-line calls; \
optionally limits or forbids multi-argument single-line calls via configuration
""",
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]),
]
nonTriggeringExamples: MultilineCallArgumentsRuleExamples.nonTriggeringExamples,
triggeringExamples: MultilineCallArgumentsRuleExamples.triggeringExamples
)
}

private extension MultilineCallArgumentsRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
/// Cache line lookups by utf8Offset (stable, cheap key)
private var lineCache: [Int: Int] = [:]

override init(configuration: ConfigurationType, file: SwiftLintFile) {
super.init(configuration: configuration, file: file)

// Most files trigger O(10–100) unique line lookups for this rule.
// Reserving a small initial capacity reduces rehashing; it is NOT a hard limit.
lineCache.reserveCapacity(64)
}

override func visitPost(_ node: FunctionCallExprSyntax) {
if containsViolation(parameterPositions: node.arguments.map(\.positionAfterSkippingLeadingTrivia)) {
violations.append(node.calledExpression.positionAfterSkippingLeadingTrivia)
}
// Ignore calls that are part of pattern-matching syntax (patterns only, not bodies).
guard !node.isInPatternMatchingPatternPosition else { return }

let args = node.arguments
guard args.count > 1 else { return }

let argumentPositions = args.map(\.positionAfterSkippingLeadingTrivia)
guard let violation = reasonedViolation(argumentPositions: argumentPositions) else { return }
violations.append(violation)
}

private func containsViolation(parameterPositions: [AbsolutePosition]) -> Bool {
var numberOfParameters = 0
var linesWithParameters: Set<Int> = []
var hasMultipleParametersOnSameLine = false
private func reasonedViolation(argumentPositions: [AbsolutePosition]) -> ReasonedRuleViolation? {
guard let firstPos = argumentPositions.first else { return nil }

let firstLine = line(for: firstPos)
var allOnSameLine = true

for pos in argumentPositions.dropFirst() where line(for: pos) != firstLine {
allOnSameLine = false
break
}

for position in parameterPositions {
let line = locationConverter.location(for: position).line
if allOnSameLine {
if !configuration.allowsSingleLine {
return ReasonedRuleViolation(
position: argumentPositions[1],
reason: Reason.singleLineMultipleArgumentsNotAllowed
)
}

if !linesWithParameters.insert(line).inserted {
hasMultipleParametersOnSameLine = true
if let max = configuration.maxNumberOfSingleLineParameters,
argumentPositions.count > max {
return ReasonedRuleViolation(
position: argumentPositions[max],
reason: Reason.tooManyArgumentsOnSingleLine(max: max)
)
}

numberOfParameters += 1
return nil
}

if linesWithParameters.count == 1 {
guard configuration.allowsSingleLine else {
return numberOfParameters > 1
var seen: Set<Int> = []
for pos in argumentPositions {
let line = line(for: pos)
if !seen.insert(line).inserted {
return ReasonedRuleViolation(
position: pos,
reason: Reason.eachArgumentMustStartOnOwnLine
)
}
}

if let maxNumberOfSingleLineParameters = configuration.maxNumberOfSingleLineParameters {
return numberOfParameters > maxNumberOfSingleLineParameters
}
return nil
}

private func line(for position: AbsolutePosition) -> Int {
let key = position.utf8Offset
if let cached = lineCache[key] { return cached }
let line = locationConverter.location(for: position).line
lineCache[key] = line
return line
}
}
}

// MARK: - Pattern filtering (precise, pattern-part only)

private extension FunctionCallExprSyntax {
/// `true` only when this FunctionCall is used inside a *pattern* (e.g. `.caseOne(...)`),
/// not just somewhere inside `if case` / `switch case` bodies.
var isInPatternMatchingPatternPosition: Bool {
let selfSyntax = Syntax(self)
var current: Syntax? = parent

// Low-level pattern nodes can appear inside multiple contexts; we only need to check each once.
var checkedExpressionPattern = false
var checkedValueBindingPattern = false

while let node = current {
if !checkedExpressionPattern, let expressionPattern = node.as(ExpressionPatternSyntax.self) {
checkedExpressionPattern = true
if selfSyntax.isInside(Syntax(expressionPattern.expression)) { return true }
}

if !checkedValueBindingPattern, let valueBindingPattern = node.as(ValueBindingPatternSyntax.self) {
checkedValueBindingPattern = true
if selfSyntax.isInside(Syntax(valueBindingPattern.pattern)) { return true }
}

// Once we reach a *top-level* pattern container (if/switch/for/catch),
// we can safely stop walking up the parent chain after checking its pattern subtree.

if let condition = node.as(MatchingPatternConditionSyntax.self) {
if selfSyntax.isInside(Syntax(condition.pattern)) { return true }
break
}

return false
if let caseItem = node.as(SwitchCaseItemSyntax.self) {
if selfSyntax.isInside(Syntax(caseItem.pattern)) { return true }
break
}

return hasMultipleParametersOnSameLine
if let forStmt = node.as(ForStmtSyntax.self) {
if selfSyntax.isInside(Syntax(forStmt.pattern)) { return true }
break
}

if let catchClause = node.as(CatchClauseSyntax.self) {
for item in catchClause.catchItems {
if let pattern = item.pattern,
selfSyntax.isInside(Syntax(pattern)) {
return true
}
}
break
}

current = node.parent
}

return false
}
}

// MARK: - Generic helpers

private extension Syntax {
/// Returns `true` if `self` is the `ancestor` node itself or is located inside its subtree.
func isInside(_ ancestor: Syntax) -> Bool {
var current: Syntax? = self
while let node = current {
if node.id == ancestor.id { return true }
current = node.parent
}
return false
}
}
Loading