Skip to content

Commit

Permalink
Merge pull request #623 from Mersho/AsyncFuncFailWith-squashed
Browse files Browse the repository at this point in the history
Rule that warns about forgetting "return" keyword when raising exceptions in async blocks.

Fixes #597
  • Loading branch information
knocte authored Dec 7, 2023
2 parents 317a51e + fa302d9 commit 7b351fb
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/content/how-tos/rule-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,4 @@ The following rules can be specified for linting.
- [AvoidTooShortNames (FL0075)](rules/FL0075.html)
- [FavourStaticEmptyFields (FL0076)](rules/FL0076.html)
- [AvoidSinglePipeOperator (FL0077)](rules/FL0077.html)
- [AsyncExceptionWithoutReturn (FL0078)](rules/FL0078.html)
27 changes: 27 additions & 0 deletions docs/content/how-tos/rules/FL0078.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
title: FL0078
category: how-to
hide_menu: true
---

# AsyncExceptionWithoutReturn (FL0078)

*Introduced in `0.21.6`*

## Cause

Missing "return" keyword inside async blocks when throwing exceptions.

## Rationale

When returning values or throwing exception in async blocks, the "return" keyword must be used.

## How To Fix

Add "return" keyword to your raise/failwith/failwithf statment.

## Rule Settings

{
"asyncExceptionWithoutReturn": { "enabled": false }
}
5 changes: 5 additions & 0 deletions src/FSharpLint.Core/Application/Configuration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ type ConventionsConfig =
avoidTooShortNames:EnabledConfig option
redundantNewKeyword:EnabledConfig option
favourStaticEmptyFields:EnabledConfig option
asyncExceptionWithoutReturn:EnabledConfig option
nestedStatements:RuleConfig<NestedStatements.Config> option
cyclomaticComplexity:RuleConfig<CyclomaticComplexity.Config> option
reimplementsFunction:EnabledConfig option
Expand All @@ -329,6 +330,7 @@ with
this.redundantNewKeyword |> Option.bind (constructRuleIfEnabled RedundantNewKeyword.rule) |> Option.toArray
this.favourReRaise |> Option.bind (constructRuleIfEnabled FavourReRaise.rule) |> Option.toArray
this.favourStaticEmptyFields |> Option.bind (constructRuleIfEnabled FavourStaticEmptyFields.rule) |> Option.toArray
this.asyncExceptionWithoutReturn |> Option.bind (constructRuleIfEnabled AsyncExceptionWithoutReturn.rule) |> Option.toArray
this.nestedStatements |> Option.bind (constructRuleWithConfig NestedStatements.rule) |> Option.toArray
this.favourConsistentThis |> Option.bind (constructRuleWithConfig FavourConsistentThis.rule) |> Option.toArray
this.cyclomaticComplexity |> Option.bind (constructRuleWithConfig CyclomaticComplexity.rule) |> Option.toArray
Expand Down Expand Up @@ -399,6 +401,7 @@ type Configuration =
RedundantNewKeyword:EnabledConfig option
FavourReRaise:EnabledConfig option
FavourStaticEmptyFields:EnabledConfig option
AsyncExceptionWithoutReturn:EnabledConfig option
NestedStatements:RuleConfig<NestedStatements.Config> option
FavourConsistentThis:RuleConfig<FavourConsistentThis.Config> option
CyclomaticComplexity:RuleConfig<CyclomaticComplexity.Config> option
Expand Down Expand Up @@ -484,6 +487,7 @@ with
RedundantNewKeyword = None
FavourReRaise = None
FavourStaticEmptyFields = None
AsyncExceptionWithoutReturn = None
NestedStatements = None
FavourConsistentThis = None
CyclomaticComplexity = None
Expand Down Expand Up @@ -632,6 +636,7 @@ let flattenConfig (config:Configuration) =
config.RedundantNewKeyword |> Option.bind (constructRuleIfEnabled RedundantNewKeyword.rule)
config.FavourReRaise |> Option.bind (constructRuleIfEnabled FavourReRaise.rule)
config.FavourStaticEmptyFields |> Option.bind (constructRuleIfEnabled FavourStaticEmptyFields.rule)
config.AsyncExceptionWithoutReturn |> Option.bind (constructRuleIfEnabled AsyncExceptionWithoutReturn.rule)
config.NestedStatements |> Option.bind (constructRuleWithConfig NestedStatements.rule)
config.FavourConsistentThis |> Option.bind (constructRuleWithConfig FavourConsistentThis.rule)
config.CyclomaticComplexity |> Option.bind (constructRuleWithConfig CyclomaticComplexity.rule)
Expand Down
1 change: 1 addition & 0 deletions src/FSharpLint.Core/FSharpLint.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="Rules\Formatting\TypedItemSpacing.fs" />
<Compile Include="Rules\Formatting\TypePrefixing.fs" />
<Compile Include="Rules\Formatting\UnionDefinitionIndentation.fs" />
<Compile Include="Rules\Conventions\AsyncExceptionWithoutReturn.fs" />
<Compile Include="Rules\Conventions\FavourStaticEmptyFields.fs" />
<Compile Include="Rules\Conventions\RecursiveAsyncFunction.fs" />
<Compile Include="Rules\Conventions\RedundantNewKeyword.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
module FSharpLint.Rules.AsyncExceptionWithoutReturn

open FSharpLint.Framework
open FSharpLint.Framework.Suggestion
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FSharpLint.Framework.Ast
open FSharpLint.Framework.Rules

let rec checkExpression (expression: SynExpr) (range: range) =
match expression with
| SynExpr.Sequential (_, _, firstExpression, secondExpression, _) ->
let result = checkExpression firstExpression range
Array.append result (checkExpression secondExpression secondExpression.Range)
| SynExpr.Paren (innerExpression, _, _, range) -> checkExpression innerExpression range
| SynExpr.While (_, _, innerExpression, range) -> checkExpression innerExpression range
| SynExpr.For (_, _, _, _, _, innerExpression, range) -> checkExpression innerExpression range
| SynExpr.ForEach (_, _, _, _, _, innerExpression, range) -> checkExpression innerExpression range
| SynExpr.Match (_, _, clauses, range) ->
clauses
|> List.map (fun (SynMatchClause (_, _, clause, range, _)) -> checkExpression clause range)
|> List.toArray
|> Array.concat
| SynExpr.Do (innerExpression, range) -> checkExpression innerExpression range
| SynExpr.TryWith (tryExpression, tryRange, withCases, _, _, _, _) ->
withCases
|> List.map (fun (SynMatchClause (_, _, withCase, withRange, _)) -> checkExpression withCase withRange)
|> List.toArray
|> Array.concat
|> Array.append (checkExpression tryExpression tryRange)
| SynExpr.TryFinally (tryExpression, finallyExpr, range, _, _) ->
checkExpression finallyExpr range
|> Array.append (checkExpression tryExpression range)
| SynExpr.IfThenElse (_, thenExpr, elseExpr, _, _, _, range) ->
let checkThen = checkExpression thenExpr range

match elseExpr with
| Some elseExpression ->
checkThen
|> Array.append (checkExpression elseExpression range)
| None -> checkThen
| SynExpr.App (_, _, SynExpr.Ident failwithId, _, _) when
failwithId.idText = "failwith"
|| failwithId.idText = "failwithf"
|| failwithId.idText = "raise"
->
{ Range = range
Message = Resources.GetString "RulesAsyncExceptionWithoutReturn"
SuggestedFix = None
TypeChecks = List.Empty }
|> Array.singleton
| SynExpr.App (_, _, funcExpr, _, range) ->
checkExpression funcExpr range
| SynExpr.LetOrUse (_, _, _, body, range) ->
checkExpression body range
| _ -> Array.empty


let runner args =
match args.AstNode with
| AstNode.Expression
(
SynExpr.App (_, _, (SynExpr.Ident compExprName), (SynExpr.CompExpr (_, _, innerExpression, _)), range)
) when compExprName.idText = "async" -> checkExpression innerExpression range
| _ -> Array.empty

let rule =
{ Name = "AsyncExceptionWithoutReturn"
Identifier = Identifiers.AsyncExceptionWithoutReturn
RuleConfig =
{ AstNodeRuleConfig.Runner = runner
Cleanup = ignore } }
|> AstNodeRule
1 change: 1 addition & 0 deletions src/FSharpLint.Core/Rules/Identifiers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,4 @@ let FavourConsistentThis = identifier 74
let AvoidTooShortNames = identifier 75
let FavourStaticEmptyFields = identifier 76
let AvoidSinglePipeOperator = identifier 77
let AsyncExceptionWithoutReturn = identifier 78
3 changes: 3 additions & 0 deletions src/FSharpLint.Core/Text.resx
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,7 @@
<data name="RulesAvoidSinglePipeOperator" xml:space="preserve">
<value>Can use normal function call (instead of pipe operator) because there is no invocation chain (single invocation only).</value>
</data>
<data name="RulesAsyncExceptionWithoutReturn" xml:space="preserve">
<value>You should use the 'return' keyword when raising an exception in an async computation expression.</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/FSharpLint.Core/fsharplint.json
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@
}
},
"avoidTooShortNames": { "enabled": false },
"asyncExceptionWithoutReturn": { "enabled": false },
"indentation": {
"enabled": false
},
Expand Down
1 change: 1 addition & 0 deletions tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<Compile Include="Rules\Formatting\TypedItemSpacing.fs" />
<Compile Include="Rules\Formatting\UnionDefinitionIndentation.fs" />
<Compile Include="Rules\Formatting\TypePrefixing.fs" />
<Compile Include="Rules\Conventions\AsyncExceptionWithoutReturn.fs" />
<Compile Include="Rules\Conventions\FavourStaticEmptyFields.fs" />
<Compile Include="Rules\Conventions\RecursiveAsyncFunction.fs" />
<Compile Include="Rules\Conventions\RedundantNewKeyword.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
module FSharpLint.Core.Tests.Rules.Conventions.AsyncExceptionWithoutReturn

open NUnit.Framework
open FSharpLint.Framework.Rules
open FSharpLint.Rules

[<TestFixture>]
type TestAsyncExceptionWithoutReturn() =
inherit TestAstNodeRuleBase.TestAstNodeRuleBase(AsyncExceptionWithoutReturn.rule)

[<Test>]
member this.AsyncRaiseWithoutReturn() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
raise (new System.Exception("An error occurred."))
return true
}""")

Assert.IsTrue this.ErrorsExist

[<Test>]
member this.AsyncRaiseWithReturn() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
return raise (new System.Exception("An error occurred."))
}""")

this.AssertNoWarnings()

[<Test>]
member this.AsyncFailWithWithoutReturn() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
failwith "An error occurred."
return true
}""")

Assert.IsTrue this.ErrorsExist

[<Test>]
member this.AsyncFailwithfWithoutReturn_1() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
let errCode = 78
failwithf "Dummy Error Message: %i" errCode
return true
}""")

Assert.IsTrue this.ErrorsExist

[<Test>]
member this.AsyncFailwithfWithoutReturn_2() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
let errCode = 78
failwithf "Dummy Error Message: %i" errCode
}""")

Assert.IsTrue this.ErrorsExist

[<Test>]
member this.AsyncFailwithWithReturn() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
return failwith "An error occurred."
}""")

this.AssertNoWarnings()

[<Test>]
member this.AsyncFailwithfWithReturn() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
let errCode = 78
return failwithf "Dummy Error Message: %i" errCode
}""")

this.AssertNoWarnings()

[<Test>]
member this.AsyncRaiseWithReturnInnerExpression() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
if 2 = 2 then
return raise (new System.Exception("An error occurred."))
return true
}""")

this.AssertNoWarnings()

[<Test>]
member this.AsyncRaiseWithoutReturnInnerExpression() =
this.Parse("""
namespace Program
let someAsyncFunction =
async {
if 2 = 2 then
raise (new System.Exception("An error occurred."))
return true
}""")

Assert.IsTrue this.ErrorsExist

0 comments on commit 7b351fb

Please sign in to comment.