Files

214 lines
6.9 KiB
Swift

import SwiftLintCore
import SwiftSyntax
@SwiftSyntaxRule(correctable: true, optIn: true)
struct UnusedParameterRule: Rule {
var configuration = UnusedParameterConfiguration()
private static let allowUnderscorePrefixedNames = ["allow_underscore_prefixed_names": true]
static let description = RuleDescription(
identifier: "unused_parameter",
name: "Unused Parameter",
description: """
Other than unused local variable declarations, unused function/initializer/subscript parameters are not \
marked by the Swift compiler. Since unused parameters are code smells, they should either be removed \
or replaced/shadowed by a wildcard '_' to indicate that they are being deliberately disregarded.
""",
kind: .lint,
nonTriggeringExamples: [
Example("""
func f(a: Int) {
_ = a
}
"""),
Example("""
func f(case: Int) {
_ = `case`
}
"""),
Example("""
func f(a _: Int) {}
"""),
Example("""
func f(_: Int) {}
"""),
Example("""
func f(a: Int, b c: String) {
func g() {
_ = a
_ = c
}
}
"""),
Example("""
func f(a: Int, c: Int) -> Int {
struct S {
let b = 1
func f(a: Int, b: Int = 2) -> Int { a + b }
}
return a + c
}
"""),
Example("""
func f(a: Int?) {
if let a {}
}
"""),
Example("""
func f(a: Int) {
let a = a
return a
}
"""),
Example("""
func f(`operator`: Int) -> Int { `operator` }
"""),
Example("""
func f(_a: Int) {}
""", configuration: allowUnderscorePrefixedNames),
],
triggeringExamples: [
Example("""
func f(↓a: Int) {}
"""),
Example("""
func f(↓_a: Int) {}
"""),
Example("""
func f(↓a: Int, b ↓c: String) {}
"""),
Example("""
func f(↓a: Int, b ↓c: String) {
func g(a: Int, ↓b: Double) {
_ = a
}
}
"""),
Example("""
struct S {
let a: Int
init(a: Int, ↓b: Int) {
func f(↓a: Int, b: Int) -> Int { b }
self.a = f(a: a, b: 0)
}
}
"""),
Example("""
struct S {
subscript(a: Int, ↓b: Int) {
func f(↓a: Int, b: Int) -> Int { b }
return f(a: a, b: 0)
}
}
"""),
Example("""
func f(↓a: Int, ↓b: Int, c: Int) -> Int {
struct S {
let b = 1
func f(a: Int, ↓c: Int = 2) -> Int { a + b }
}
return S().f(a: c)
}
"""),
Example("""
func f(↓a: Int, c: String) {
let a = 1
return a + c
}
"""),
],
corrections: [
Example("""
func f(a: Int) {}
"""): Example("""
func f(a _: Int) {}
"""),
Example("""
func f(a b: Int) {}
"""): Example("""
func f(a _: Int) {}
"""),
Example("""
func f(_ a: Int) {}
"""): Example("""
func f(_: Int) {}
"""),
]
)
}
// MARK: Visitor
private extension UnusedParameterRule {
final class Visitor: DeclaredIdentifiersTrackingVisitor<ConfigurationType> {
private var referencedDeclarations = Set<IdentifierDeclaration>()
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
// MARK: Violation checking
override func visitPost(_ node: CodeBlockItemListSyntax) {
let declarations = scope.peek() ?? []
for declaration in declarations.reversed() where !referencedDeclarations.contains(declaration) {
guard case let .parameter(name) = declaration,
let previousToken = name.previousToken(viewMode: .sourceAccurate) else {
continue
}
if configuration.allowUnderscorePrefixedNames, name.text.hasPrefix("_") {
continue
}
let startPosReplacement =
if previousToken.tokenKind == .wildcard {
(previousToken.positionAfterSkippingLeadingTrivia, "_")
} else if case .identifier = previousToken.tokenKind {
(name.positionAfterSkippingLeadingTrivia, "_")
} else {
(name.positionAfterSkippingLeadingTrivia, name.text + " _")
}
violations.append(.init(
position: name.positionAfterSkippingLeadingTrivia,
reason: "Parameter '\(name.text)' is unused; consider removing or replacing it with '_'",
severity: configuration.severity,
correction: .init(
start: startPosReplacement.0,
end: name.endPositionBeforeTrailingTrivia,
replacement: startPosReplacement.1
)
))
}
super.visitPost(node)
}
// MARK: Reference collection
override func visitPost(_ node: DeclReferenceExprSyntax) {
if node.keyPathInParent != \MemberAccessExprSyntax.declName {
addReference(node.baseName.text)
}
}
override func visitPost(_ node: OptionalBindingConditionSyntax) {
if node.initializer == nil, let id = node.pattern.as(IdentifierPatternSyntax.self)?.identifier.text {
addReference(id)
}
}
// MARK: Private methods
private func addReference(_ id: String) {
for declarations in scope.reversed() {
if declarations.onlyElement == .lookupBoundary {
return
}
for declaration in declarations.reversed() where declaration.declares(id: id) {
if referencedDeclarations.insert(declaration).inserted {
return
}
}
}
}
}
}