mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
240 lines
8.9 KiB
Swift
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
|
|
}
|
|
}
|