Fix some `unhandled_throwing_task` false positives (#5001)

Fixes some of the cases reported in
https://github.com/realm/SwiftLint/issues/4987
This commit is contained in:
JP Simard 2023-05-12 10:35:59 -04:00 committed by GitHub
parent 15a18fd4e8
commit 1b1b19a902
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 79 additions and 2 deletions

View File

@ -19,7 +19,12 @@
* Make `unhandled_throwing_task` opt-in instead of enabled by default. The rule * Make `unhandled_throwing_task` opt-in instead of enabled by default. The rule
is still prone to false positives at this point, so this makes enabling the is still prone to false positives at this point, so this makes enabling the
rule a conscious decision by end-users instead of enabled by default. rule a conscious decision by end-users.
[JP Simard](https://github.com/jpsim)
[#4987](https://github.com/realm/SwiftLint/issues/4987)
* Fix `unhandled_throwing_task` false positives when the `Task` is returned or
where the throwing code is handled in a `Result` initializer.
[JP Simard](https://github.com/jpsim) [JP Simard](https://github.com/jpsim)
[#4987](https://github.com/realm/SwiftLint/issues/4987) [#4987](https://github.com/realm/SwiftLint/issues/4987)

View File

@ -82,6 +82,28 @@ struct UnhandledThrowingTaskRule: ConfigurationProviderRule, SwiftSyntaxRule, Op
let result = await Task { let result = await Task {
throw CancellationError() throw CancellationError()
}.result }.result
"""),
Example("""
func makeTask() -> Task<String, Error> {
return Task {
try await someThrowingFunction()
}
}
"""),
Example("""
func makeTask() -> Task<String, Error> {
// Implicit return
Task {
try await someThrowingFunction()
}
}
"""),
Example("""
Task {
return Result {
try someThrowingFunc()
}
}
""") """)
], ],
triggeringExamples: [ triggeringExamples: [
@ -151,6 +173,13 @@ struct UnhandledThrowingTaskRule: ConfigurationProviderRule, SwiftSyntaxRule, Op
throw BarError() throw BarError()
} }
} }
"""),
Example("""
func doTask() {
Task {
try await someThrowingFunction()
}
}
""") """)
] ]
) )
@ -174,7 +203,7 @@ private extension FunctionCallExprSyntax {
var hasViolation: Bool { var hasViolation: Bool {
isTaskWithImplicitErrorType && isTaskWithImplicitErrorType &&
doesThrow && doesThrow &&
!(isAssigned || isValueOrResultAccessed) !(isAssigned || isValueOrResultAccessed || isReturnValue)
} }
var isTaskWithImplicitErrorType: Bool { var isTaskWithImplicitErrorType: Bool {
@ -260,6 +289,22 @@ private final class ThrowsVisitor: SyntaxVisitor {
return .skipChildren return .skipChildren
} }
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
// No need to continue traversing if we already throw.
if doesThrow {
return .skipChildren
}
// Result initializers with trailing closures handle thrown errors.
if let typeIdentifier = node.calledExpression.as(IdentifierExprSyntax.self),
typeIdentifier.identifier.text == "Result",
node.trailingClosure != nil {
return .skipChildren
}
return .visitChildren
}
override func visitPost(_ node: TryExprSyntax) { override func visitPost(_ node: TryExprSyntax) {
if node.questionOrExclamationMark == nil { if node.questionOrExclamationMark == nil {
doesThrow = true doesThrow = true
@ -270,3 +315,30 @@ private final class ThrowsVisitor: SyntaxVisitor {
doesThrow = true doesThrow = true
} }
} }
private extension SyntaxProtocol {
var isExplicitReturnValue: Bool {
parent?.is(ReturnStmtSyntax.self) == true
}
var isImplicitReturnValue: Bool {
// 4th parent: FunctionDecl
// 3rd parent: | CodeBlock
// 2nd parent: | CodeBlockItemList
// 1st parent: | CodeBlockItem
// Current node: | FunctionDeclSyntax
guard
let parentFunctionDecl = parent?.parent?.parent?.parent?.as(FunctionDeclSyntax.self),
parentFunctionDecl.body?.statements.count == 1,
parentFunctionDecl.signature.output != nil
else {
return false
}
return true
}
var isReturnValue: Bool {
isExplicitReturnValue || isImplicitReturnValue
}
}