Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift
2026-01-24 14:52:12 +00:00

122 lines
5.9 KiB
Swift

import SwiftSyntax
@SwiftSyntaxRule
struct LargeTupleRule: Rule {
var configuration = LargeTupleConfiguration()
static let description = RuleDescription(
identifier: "large_tuple",
name: "Large Tuple",
description: "Tuples shouldn't have too many members. Create a custom type instead.",
kind: .metrics,
nonTriggeringExamples: [
Example("let foo: (Int, Int)"),
Example("let foo: (start: Int, end: Int)"),
Example("let foo: (Int, (Int, String))"),
Example("func foo() -> (Int, Int)"),
Example("func foo() -> (Int, Int) {}"),
Example("func foo(bar: String) -> (Int, Int)"),
Example("func foo(bar: String) -> (Int, Int) {}"),
Example("func foo() throws -> (Int, Int)"),
Example("func foo() throws -> (Int, Int) {}"),
Example("let foo: (Int, Int, Int) -> Void"),
Example("let foo: (Int, Int, Int) throws -> Void"),
Example("func foo(bar: (Int, String, Float) -> Void)"),
Example("func foo(bar: (Int, String, Float) throws -> Void)"),
Example("var completionHandler: ((_ data: Data?, _ resp: URLResponse?, _ e: NSError?) -> Void)!"),
Example("func getDictionaryAndInt() -> (Dictionary<Int, String>, Int)?"),
Example("func getGenericTypeAndInt() -> (Type<Int, String, Float>, Int)?"),
Example("func foo() async -> (Int, Int)"),
Example("func foo() async -> (Int, Int) {}"),
Example("func foo(bar: String) async -> (Int, Int)"),
Example("func foo(bar: String) async -> (Int, Int) {}"),
Example("func foo() async throws -> (Int, Int)"),
Example("func foo() async throws -> (Int, Int) {}"),
Example("let foo: (Int, Int, Int) async -> Void"),
Example("let foo: (Int, Int, Int) async throws -> Void"),
Example("func foo(bar: (Int, String, Float) async -> Void)"),
Example("func foo(bar: (Int, String, Float) async throws -> Void)"),
Example("func getDictionaryAndInt() async -> (Dictionary<Int, String>, Int)?"),
Example("func getGenericTypeAndInt() async -> (Type<Int, String, Float>, Int)?"),
Example(
"func foo() -> Regex<(Substring, foo: Substring, bar: Substring)>.Match? { nil }",
configuration: ["ignore_regex": true]
),
Example(
"let regex: Regex<(Substring, Substring, Substring, Substring)>? = nil",
configuration: ["ignore_regex": true]
),
Example(
"var regex: Regex<(Substring, Substring, Substring, Substring)?>.Match? { nil }",
configuration: ["ignore_regex": true]
),
],
triggeringExamples: [
Example("let foo: ↓(Int, Int, Int)"),
Example("let foo: ↓(start: Int, end: Int, value: String)"),
Example("let foo: (Int, ↓(Int, Int, Int))"),
Example("func foo(bar: ↓(Int, Int, Int))"),
Example("func foo() -> ↓(Int, Int, Int)"),
Example("func foo() -> ↓(Int, Int, Int) {}"),
Example("func foo(bar: String) -> ↓(Int, Int, Int)"),
Example("func foo(bar: String) -> ↓(Int, Int, Int) {}"),
Example("func foo() throws -> ↓(Int, Int, Int)"),
Example("func foo() throws -> ↓(Int, Int, Int) {}"),
Example("func foo() throws -> ↓(Int, ↓(String, String, String), Int) {}"),
Example("func getDictionaryAndInt() -> (Dictionary<Int, ↓(String, String, String)>, Int)?"),
Example("func foo(bar: ↓(Int, Int, Int)) async"),
Example("func foo() async -> ↓(Int, Int, Int)"),
Example("func foo() async -> ↓(Int, Int, Int) {}"),
Example("func foo(bar: String) async -> ↓(Int, Int, Int)"),
Example("func foo(bar: String) async -> ↓(Int, Int, Int) {}"),
Example("func foo() async throws -> ↓(Int, Int, Int)"),
Example(
"func foo() async throws -> ↓(Int, Int, Int) {}",
configuration: ["ignore_regex": false]
),
Example("func foo() async throws -> ↓(Int, ↓(String, String, String), Int) {}"),
Example("func getDictionaryAndInt() async -> (Dictionary<Int, ↓(String, String, String)>, Int)?"),
]
)
}
private extension LargeTupleRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: TupleTypeSyntax) {
if configuration.ignoreRegex, node.isInsideRegexType {
return
}
let memberCount = node.elements.count
for parameter in configuration.severityConfiguration.params where memberCount > parameter.value {
violations.append(.init(
position: node.positionAfterSkippingLeadingTrivia,
reason: "Tuples should have at most \(configuration.severityConfiguration.warning) members",
severity: parameter.severity
))
return
}
}
}
}
private extension TupleTypeSyntax {
var isInsideRegexType: Bool {
var current: Syntax? = Syntax(self)
// Skip OptionalType wrapper if present (for Regex<(A, B)?>)
if current?.parent?.is(OptionalTypeSyntax.self) == true {
current = current?.parent
}
guard let genericArgument = current?.parent?.as(GenericArgumentSyntax.self),
let genericArgumentList = genericArgument.parent?.as(GenericArgumentListSyntax.self),
let genericArgumentClause = genericArgumentList.parent?.as(GenericArgumentClauseSyntax.self),
let identifierType = genericArgumentClause.parent?.as(IdentifierTypeSyntax.self),
identifierType.name.text == "Regex" else {
return false
}
return true
}
}