268 lines
7.6 KiB
Swift
268 lines
7.6 KiB
Swift
import SwiftSyntax
|
|
|
|
struct UnhandledThrowingTaskRule: ConfigurationProviderRule, SwiftSyntaxRule {
|
|
var configuration = SeverityConfiguration(.error)
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "unhandled_throwing_task",
|
|
name: "Unhandled Throwing Task",
|
|
description: """
|
|
Errors thrown inside this task are not handled, which may be unexpected. \
|
|
Handle errors inside the task, or use `try await` to access the Tasks value and handle errors. \
|
|
See this forum thread for more details: \
|
|
https://forums.swift.org/t/task-initializer-with-throwing-closure-swallows-error/56066
|
|
""",
|
|
kind: .lint,
|
|
nonTriggeringExamples: [
|
|
Example("""
|
|
Task<Void, Never> {
|
|
try await myThrowingFunction()
|
|
}
|
|
"""),
|
|
Example("""
|
|
Task {
|
|
try? await myThrowingFunction()
|
|
}
|
|
"""),
|
|
Example("""
|
|
Task {
|
|
try! await myThrowingFunction()
|
|
}
|
|
"""),
|
|
Example("""
|
|
Task<Void, String> {
|
|
let text = try myThrowingFunction()
|
|
return text
|
|
}
|
|
"""),
|
|
Example("""
|
|
Task {
|
|
do {
|
|
try myThrowingFunction()
|
|
} catch let e {
|
|
print(e)
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
func someFunction() throws {
|
|
Task {
|
|
anotherFunction()
|
|
do {
|
|
try myThrowingFunction()
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
try something()
|
|
}
|
|
"""),
|
|
Example("""
|
|
let task = Task {
|
|
try await myThrowingFunction()
|
|
}
|
|
"""),
|
|
Example("""
|
|
var task = Task {
|
|
try await myThrowingFunction()
|
|
}
|
|
"""),
|
|
Example("""
|
|
try await Task {
|
|
try await myThrowingFunction()
|
|
}.value
|
|
"""),
|
|
Example("""
|
|
executor.task = Task {
|
|
try await isolatedOpen(.init(executor.asUnownedSerialExecutor()))
|
|
}
|
|
""")
|
|
],
|
|
triggeringExamples: [
|
|
Example("""
|
|
↓Task {
|
|
try await myThrowingFunction()
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task {
|
|
let text = try myThrowingFunction()
|
|
return text
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task {
|
|
do {
|
|
try myThrowingFunction()
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task {
|
|
do {
|
|
try myThrowingFunction()
|
|
} catch let e as FooError {
|
|
print(e)
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task {
|
|
do {
|
|
throw FooError.bar
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task {
|
|
throw FooError.bar
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task<_, _> {
|
|
throw FooError.bar
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task<Void,_> {
|
|
throw FooError.bar
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task {
|
|
do {
|
|
try foo()
|
|
} catch {
|
|
try bar()
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
↓Task {
|
|
do {
|
|
try foo()
|
|
} catch {
|
|
throw BarError()
|
|
}
|
|
}
|
|
""")
|
|
]
|
|
)
|
|
|
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
Visitor(viewMode: .sourceAccurate)
|
|
}
|
|
}
|
|
|
|
private extension UnhandledThrowingTaskRule {
|
|
final class Visitor: ViolationsSyntaxVisitor {
|
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
|
if node.hasViolation {
|
|
violations.append(node.calledExpression.positionAfterSkippingLeadingTrivia)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension FunctionCallExprSyntax {
|
|
var hasViolation: Bool {
|
|
isTaskWithImplicitErrorType &&
|
|
doesThrow &&
|
|
!(isAssigned || isValueAccessed)
|
|
}
|
|
|
|
var isTaskWithImplicitErrorType: Bool {
|
|
if let typeIdentifier = calledExpression.as(IdentifierExprSyntax.self),
|
|
typeIdentifier.identifier.text == "Task" {
|
|
return true
|
|
}
|
|
|
|
if let specializedExpression = calledExpression.as(SpecializeExprSyntax.self),
|
|
let typeIdentifier = specializedExpression.expression.as(IdentifierExprSyntax.self),
|
|
typeIdentifier.identifier.text == "Task",
|
|
let lastGeneric = specializedExpression.genericArgumentClause
|
|
.arguments.last?.argumentType.as(SimpleTypeIdentifierSyntax.self),
|
|
lastGeneric.typeName == "_" {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var isAssigned: Bool {
|
|
guard let parent else {
|
|
return false
|
|
}
|
|
|
|
if parent.is(InitializerClauseSyntax.self) {
|
|
return true
|
|
}
|
|
|
|
if let list = parent.as(ExprListSyntax.self),
|
|
list.contains(where: { $0.is(AssignmentExprSyntax.self) }) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var isValueAccessed: Bool {
|
|
guard let parent = parent?.as(MemberAccessExprSyntax.self) else {
|
|
return false
|
|
}
|
|
|
|
return parent.name.text == "value"
|
|
}
|
|
|
|
var doesThrow: Bool {
|
|
ThrowsVisitor(viewMode: .sourceAccurate)
|
|
.walk(tree: self, handler: \.doesThrow)
|
|
}
|
|
}
|
|
|
|
/// If the `doesThrow` property is true after visiting, then this node throws an error that is "unhandled."
|
|
/// Try statements inside a `do` with a `catch` that handles all errors will not be marked as throwing.
|
|
private final class ThrowsVisitor: SyntaxVisitor {
|
|
var doesThrow = false
|
|
|
|
override func visit(_ node: DoStmtSyntax) -> SyntaxVisitorContinueKind {
|
|
// No need to continue traversing if we already throw.
|
|
if doesThrow {
|
|
return .skipChildren
|
|
}
|
|
|
|
// If there are no catch clauses, visit children to see if there are any try expressions.
|
|
guard let lastCatchClause = node.catchClauses?.last else {
|
|
return .visitChildren
|
|
}
|
|
|
|
let catchItems = lastCatchClause.catchItems ?? []
|
|
|
|
// If we have a value binding pattern, only an IdentifierPatternSyntax will catch
|
|
// any error; if it's not an IdentifierPatternSyntax, we need to visit children.
|
|
if let pattern = catchItems.last?.pattern?.as(ValueBindingPatternSyntax.self),
|
|
!pattern.valuePattern.is(IdentifierPatternSyntax.self) {
|
|
return .visitChildren
|
|
}
|
|
|
|
// Check the catch clause tree for unhandled throws.
|
|
if ThrowsVisitor(viewMode: .sourceAccurate).walk(tree: lastCatchClause, handler: \.doesThrow) {
|
|
doesThrow = true
|
|
}
|
|
|
|
// We don't need to visit children of the `do` node, since all errors are handled by the catch.
|
|
return .skipChildren
|
|
}
|
|
|
|
override func visitPost(_ node: TryExprSyntax) {
|
|
if node.questionOrExclamationMark == nil {
|
|
doesThrow = true
|
|
}
|
|
}
|
|
|
|
override func visitPost(_ node: ThrowStmtSyntax) {
|
|
doesThrow = true
|
|
}
|
|
}
|