Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededEscapingRule.swift
2026-05-06 06:43:24 +02:00

240 lines
8.9 KiB
Swift

import SwiftLexicalLookup
import SwiftLintCore
import SwiftSyntax
@SwiftSyntaxRule(foldExpressions: true, correctable: true, optIn: true)
struct UnneededEscapingRule: Rule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "unneeded_escaping",
name: "Unneeded Escaping",
description: "The `@escaping` attribute should only be used when the closure actually escapes.",
kind: .lint,
nonTriggeringExamples: UnneededEscapingRuleExamples.nonTriggeringExamples,
triggeringExamples: UnneededEscapingRuleExamples.triggeringExamples,
corrections: UnneededEscapingRuleExamples.corrections
)
}
private extension UnneededEscapingRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] {
[ProtocolDeclSyntax.self]
}
override func visitPost(_ node: FunctionDeclSyntax) {
checkFunction(parameters: node.signature.parameterClause, body: node.body?.statements)
}
override func visitPost(_ node: InitializerDeclSyntax) {
checkFunction(parameters: node.signature.parameterClause, body: node.body?.statements)
}
override func visitPost(_ node: SubscriptDeclSyntax) {
if case let .getter(items) = node.accessorBlock?.accessors {
checkFunction(parameters: node.parameterClause, body: items)
} else if let getter = node.accessorBlock?.getAccessor {
checkFunction(parameters: node.parameterClause, body: getter.body?.statements)
}
}
private func checkFunction(parameters: FunctionParameterClauseSyntax, body: CodeBlockItemListSyntax?) {
guard let body else {
return
}
for param in parameters.parameters {
if let escapingAttr = param.type.attribute(named: "escaping") {
validate(
paramName: (param.secondName ?? param.firstName).text,
with: escapingAttr,
isAutoclosure: param.type.attribute(named: "autoclosure") != nil,
in: body
)
}
}
}
private func validate(paramName: String,
with attr: AttributeSyntax,
isAutoclosure: Bool,
in body: CodeBlockItemListSyntax) {
if EscapeChecker(paramName: paramName, isAutoclosure: isAutoclosure)
.walk(tree: body, handler: \.doesEscape) {
return
}
let correctionEndPosition =
if case let .spaces(count) = attr.trailingTrivia.first, count > 0 {
attr.endPositionBeforeTrailingTrivia.advanced(by: 1)
} else {
attr.endPositionBeforeTrailingTrivia
}
violations.append(
.init(
position: attr.positionAfterSkippingLeadingTrivia,
reason: "@escaping attribute not required as '\(paramName)' does not escape",
correction: .init(
start: attr.positionAfterSkippingLeadingTrivia,
end: correctionEndPosition,
replacement: ""
)
)
)
}
}
}
private final class EscapeChecker: SyntaxVisitor {
var taintedVariables = Set<String>()
var doesEscape = false
var inClosureContext = Stack<Bool>()
let isAutoclosure: Bool
init(paramName: String, isAutoclosure: Bool) {
taintedVariables.insert(paramName)
self.isAutoclosure = isAutoclosure
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: CodeBlockItemListSyntax) {
if case let .expr(returnExpr) = node.onlyElement?.item, isTainted(returnExpr) {
doesEscape = true
}
}
override func visitPost(_ node: VariableDeclSyntax) {
for binding in node.bindings {
guard let initializer = binding.initializer else {
continue
}
if let pattern = binding.pattern.as(IdentifierPatternSyntax.self), isTainted(initializer.value) {
taintedVariables.insert(pattern.identifier.text)
} else if let pattern = binding.pattern.as(TuplePatternSyntax.self) {
if let initTuple = initializer.value.as(TupleExprSyntax.self) {
for (element, initExpr) in zip(pattern.elements, initTuple.elements) {
if let pattern = element.pattern.as(IdentifierPatternSyntax.self),
isTainted(initExpr.expression) {
taintedVariables.insert(pattern.identifier.text)
}
}
} else if isTainted(initializer.value) {
for element in pattern.elements {
if let pattern = element.pattern.as(IdentifierPatternSyntax.self) {
taintedVariables.insert(pattern.identifier.text)
}
}
}
}
}
}
override func visitPost(_ node: InfixOperatorExprSyntax) {
guard node.operator.is(AssignmentExprSyntax.self), isTainted(node.rightOperand) else {
return
}
if node.leftOperand.isLocalVariable {
if let leftName = node.leftOperand.baseNameToken?.text {
taintedVariables.insert(leftName)
}
} else {
doesEscape = true
}
}
override func visitPost(_ node: ReturnStmtSyntax) {
if let expr = node.expression, isTainted(expr) {
doesEscape = true
}
}
override func visitPost(_ node: FunctionCallExprSyntax) {
for argument in node.arguments where isTainted(argument.expression) || usesTaintedCallee(argument.expression) {
doesEscape = true
}
}
override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
inClosureContext.push(true)
return .visitChildren
}
override func visitPost(_: ClosureExprSyntax) {
inClosureContext.pop()
}
override func visitPost(_ node: DeclReferenceExprSyntax) {
guard isTainted(ExprSyntax(node)) else {
return
}
if (inClosureContext.peek() ?? false) || [.arrayElement, .dictionaryElement].contains(node.parent?.kind) {
doesEscape = true
}
}
private func isTainted(_ expr: ExprSyntax) -> Bool {
if let declRef = expr.as(DeclReferenceExprSyntax.self) {
return taintedVariables.contains(declRef.baseName.text)
}
if let optChain = expr.as(OptionalChainingExprSyntax.self),
let declRef = optChain.expression.as(DeclReferenceExprSyntax.self) {
return taintedVariables.contains(declRef.baseName.text)
}
if let ternary = expr.as(TernaryExprSyntax.self) {
return isTainted(ternary.thenExpression) || isTainted(ternary.elseExpression)
}
if let tuple = expr.as(TupleExprSyntax.self) {
return tuple.elements.contains { isTainted($0.expression) }
}
return false
}
private func usesTaintedCallee(_ expr: ExprSyntax) -> Bool {
guard isAutoclosure,
let callExpr = expr.as(FunctionCallExprSyntax.self),
callExpr.arguments.isEmpty,
callExpr.trailingClosure == nil,
callExpr.additionalTrailingClosures.isEmpty else {
return false
}
return isTainted(callExpr.calledExpression)
}
}
private extension TypeSyntax {
func attribute(named name: String) -> AttributeSyntax? {
if let attributeType = `as`(AttributedTypeSyntax.self) {
return attributeType.attributes
.compactMap { $0.as(AttributeSyntax.self) }
.first { $0.attributeName.as(IdentifierTypeSyntax.self)?.name.text == name }
}
return `as`(OptionalTypeSyntax.self)?
.wrappedType
.as(TupleTypeSyntax.self)?
.elements.onlyElement?.type
.attribute(named: name)
}
}
private extension ExprSyntax {
var baseNameToken: TokenSyntax? {
`as`(DeclReferenceExprSyntax.self)?.baseName
?? `as`(MemberAccessExprSyntax.self)?.base?.baseNameToken
}
var isLocalVariable: Bool {
guard !`is`(DiscardAssignmentExprSyntax.self) else {
return true
}
if let baseNameToken {
let results = lookup(.init(baseNameToken))
return results.isNotEmpty && results.allSatisfy {
switch $0 {
case .fromScope: true
default: false
}
}
}
return false
}
}