Skip to content

Commit 3b87c69

Browse files
authored
Add new rule FailwithBadUsage (#515)
Fixes #508
1 parent a57b331 commit 3b87c69

File tree

9 files changed

+265
-1
lines changed

9 files changed

+265
-1
lines changed

docs/content/how-tos/rule-configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,5 @@ The following rules can be specified for linting.
111111
- [InternalValuesNames (FL0068)](rules/FL0068.html)
112112
- [GenericTypesNames (FL0069)](rules/FL0069.html)
113113
- [FavourTypedIgnore (FL0070)](rules/FL0070.html)
114-
- [CyclomaticComplexity (FL0071)](rules/FL0071.html)
114+
- [CyclomaticComplexity (FL0071)](rules/FL0071.html)
115+
- [FailwithBadUsage (FL0072)](rules/FL0072.html)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
title: FL0072
3+
category: how-to
4+
hide_menu: true
5+
---
6+
7+
# FailwithBadUsage (FL0072)
8+
9+
*Introduced in `0.20.3`*
10+
11+
## Cause
12+
13+
Rule to detect bad usage of failwith.
14+
15+
## Rationale
16+
17+
Passing empty strings or duplicate messages to failwith statements makes it much harder to understand/locate & subsequently fix bugs.
18+
It's also true when exceptions are swallowed in try...with blocks.
19+
20+
## How To Fix
21+
22+
Do not pass an empty string or a duplicate message to failwith.
23+
Instead of swallowing exception messages in try...with blocks, pass the exception as an innerException parameter of a new Exception.
24+
25+
## Rule Settings
26+
27+
{
28+
"failwithBadUsage": {
29+
"enabled": true
30+
}
31+
}

src/FSharpLint.Core/FSharpLint.Core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\InvalidOpWithSingleArgument.fs" />
5555
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\InvalidArgWithTwoArguments.fs" />
5656
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\FailwithfWithArgumentsMatchingFormatString.fs" />
57+
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\FailwithBadUsage.fs" />
5758
<Compile Include="Rules\Conventions\SourceLength\SourceLengthHelper.fs" />
5859
<Compile Include="Rules\Conventions\SourceLength\MaxLinesInLambdaFunction.fs" />
5960
<Compile Include="Rules\Conventions\SourceLength\MaxLinesInMatchLambdaFunction.fs" />
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
module FSharpLint.Rules.FailwithBadUsage
2+
3+
open FSharpLint.Framework
4+
open FSharpLint.Framework.Suggestion
5+
open FSharp.Compiler.Syntax
6+
open FSharpLint.Framework.Ast
7+
open FSharpLint.Framework.Rules
8+
open System
9+
open System.Collections.Generic
10+
11+
let mutable failwithErrorMessageList = Set.empty
12+
13+
type private BadUsageType =
14+
| EmptyMessage
15+
| DuplicateMessage
16+
| SwallowedException
17+
18+
let private runner (args: AstNodeRuleParams) =
19+
let generateError
20+
failwithKeyword
21+
failwithErrorMessage
22+
range
23+
(badUsageType: BadUsageType)
24+
(exceptionParam: Option<string>)
25+
=
26+
let suggestedFix =
27+
match exceptionParam with
28+
| Some param ->
29+
Some(
30+
lazy
31+
(Some
32+
{ FromText = sprintf "%s %s" failwithKeyword failwithErrorMessage
33+
FromRange = range
34+
ToText = sprintf "raise <| Exception(\"%s\", %s)" failwithErrorMessage param })
35+
)
36+
| _ -> None
37+
38+
let message =
39+
match badUsageType with
40+
| EmptyMessage -> "Consider using non-empty error messages with failwith"
41+
| DuplicateMessage -> "Consider using unique error messages with failwith"
42+
| SwallowedException ->
43+
"failwith must not swallow exception details with it, rather use raise passing the current exception as innerException (2nd parameter of Exception constructor)"
44+
45+
let error =
46+
{ Range = range
47+
Message = String.Format(Resources.GetString "RulesFailwithBadUsage", message)
48+
SuggestedFix = suggestedFix
49+
TypeChecks = List.Empty }
50+
|> Array.singleton
51+
52+
error
53+
54+
let rec checkExpr node maybeIdentifier =
55+
match node with
56+
| SynExpr.App (_, _, SynExpr.Ident failwithId, expression, range) when
57+
failwithId.idText = "failwith"
58+
|| failwithId.idText = "failwithf"
59+
->
60+
match expression with
61+
| SynExpr.Const (SynConst.String (id, _, _), _) when id = "" ->
62+
generateError failwithId.idText id range BadUsageType.EmptyMessage maybeIdentifier
63+
| SynExpr.Const (SynConst.String (id, _, _), _) ->
64+
if Set.contains id failwithErrorMessageList then
65+
generateError failwithId.idText id range BadUsageType.DuplicateMessage maybeIdentifier
66+
else
67+
match maybeIdentifier with
68+
| Some maybeId ->
69+
generateError failwithId.idText id range BadUsageType.SwallowedException (Some maybeId)
70+
| _ ->
71+
failwithErrorMessageList <- failwithErrorMessageList.Add(id)
72+
Array.empty
73+
| SynExpr.LongIdent (_, LongIdentWithDots (id, _), _, _) when
74+
(ExpressionUtilities.longIdentToString id) = "String.Empty"
75+
->
76+
generateError
77+
failwithId.idText
78+
(ExpressionUtilities.longIdentToString id)
79+
range
80+
(BadUsageType.EmptyMessage)
81+
(None)
82+
| _ -> Array.empty
83+
| SynExpr.TryWith (_, _, clauseList, _expression, _range, _, _) ->
84+
clauseList
85+
|> List.toArray
86+
|> Array.collect (fun clause ->
87+
match clause with
88+
| SynMatchClause (pat, _, app, _, _) ->
89+
match pat with
90+
| SynPat.Named (_, id, _, _, _) -> checkExpr app (Some id.idText)
91+
| _ -> checkExpr app None)
92+
| _ -> Array.empty
93+
| _ -> Array.empty
94+
95+
match args.AstNode with
96+
| AstNode.Expression expr -> checkExpr expr None
97+
| _ -> Array.empty
98+
99+
let cleanup () = failwithErrorMessageList <- Set.empty
100+
101+
let rule =
102+
{ Name = "FailwithBadUsage"
103+
Identifier = Identifiers.FailwithBadUsage
104+
RuleConfig =
105+
{ AstNodeRuleConfig.Runner = runner
106+
Cleanup = cleanup } }
107+
|> AstNodeRule

src/FSharpLint.Core/Rules/Identifiers.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,4 @@ let InternalValuesNames = identifier 68
7676
let GenericTypesNames = identifier 69
7777
let FavourTypedIgnore = identifier 70
7878
let CyclomaticComplexity = identifier 71
79+
let FailwithBadUsage = identifier 72

src/FSharpLint.Core/Text.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,7 @@
324324
<data name="RulesFavourTypedIgnore" xml:space="preserve">
325325
<value>Use a generic type parameter when calling the 'ignore' function.</value>
326326
</data>
327+
<data name="RulesFailwithBadUsage" xml:space="preserve">
328+
<value>Bad usage of failwith: {0}.</value>
329+
</data>
327330
</root>

src/FSharpLint.Core/fsharplint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"invalidOpWithSingleArgument": { "enabled": true },
4545
"invalidArgWithTwoArguments": { "enabled": true },
4646
"failwithfWithArgumentsMatchingFormatString": { "enabled": true },
47+
"failwithBadUsage": { "enabled": true },
4748
"maxLinesInLambdaFunction": {
4849
"enabled": false,
4950
"config": {

tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<Compile Include="Rules\Conventions\RecursiveAsyncFunction.fs" />
3030
<Compile Include="Rules\Conventions\RedundantNewKeyword.fs" />
3131
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments.fs" />
32+
<Compile Include="Rules\Conventions\FailwithBadUsage.fs" />
3233
<Compile Include="Rules\Conventions\NestedStatements.fs" />
3334
<Compile Include="Rules\Conventions\CyclomaticComplexity.fs" />
3435
<Compile Include="Rules\Conventions\FunctionReimplementation.fs" />
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
module FSharpLint.Core.Tests.Rules.Conventions.FailwithBadUsage
2+
3+
open NUnit.Framework
4+
open FSharpLint.Rules
5+
open System
6+
7+
[<TestFixture>]
8+
type TestConventionsFailwithBadUsage() =
9+
inherit TestAstNodeRuleBase.TestAstNodeRuleBase(FailwithBadUsage.rule)
10+
11+
[<Test>]
12+
member this.FailwithWithBadArgumentsEmptyMessage() =
13+
this.Parse """
14+
let foo () =
15+
failwith "" """
16+
Assert.IsTrue this.ErrorsExist
17+
Assert.IsTrue(this.ErrorExistsAt(3, 4))
18+
19+
[<Test>]
20+
member this.FailwithWithBadArgumentsEmptyMessage2() =
21+
this.Parse """
22+
let bar () =
23+
failwith String.Empty"""
24+
25+
Assert.IsTrue this.ErrorsExist
26+
Assert.IsTrue(this.ErrorExistsAt(3, 4))
27+
28+
[<Test>]
29+
member this.FailwithWithBadArgumentsEmptyMessage3() =
30+
this.Parse """
31+
let foo () =
32+
failwith "foo"
33+
let bar () =
34+
failwith String.Empty"""
35+
36+
Assert.IsTrue this.ErrorsExist
37+
Assert.IsTrue(this.ErrorExistsAt(5, 4))
38+
39+
[<Test>]
40+
member this.FailwithWithGoodArguments() =
41+
this.Parse """
42+
let foo () =
43+
failwith "foo"
44+
let bar () =
45+
failwith "bar" """
46+
47+
this.AssertNoWarnings()
48+
49+
[<Test>]
50+
member this.FailwithWithGoodArguments2() =
51+
this.Parse """
52+
let foo () =
53+
failwith "foo" """
54+
55+
this.AssertNoWarnings()
56+
57+
[<Test>]
58+
member this.FailwithWithBadArgumentsDuplicateExceptionMessage() =
59+
this.Parse """
60+
let foo () =
61+
failwith "foobar"
62+
let bar () =
63+
failwith "foobar" """
64+
65+
Assert.IsTrue this.ErrorsExist
66+
Assert.IsTrue(this.ErrorExistsAt(5, 4))
67+
68+
[<Test>]
69+
member this.FailwithShouldNotSwallowExceptions() =
70+
this.Parse """
71+
try
72+
foo()
73+
with
74+
| e ->
75+
failwith "bar" """
76+
77+
Assert.IsTrue this.ErrorsExist
78+
Assert.IsTrue(this.ErrorExistsAt(6, 4))
79+
80+
[<Test>]
81+
member this.FailwithWithProperExceptions() =
82+
this.Parse """
83+
try
84+
foo()
85+
with
86+
| e ->
87+
raise new Exception("bar",e) """
88+
89+
Assert.IsTrue this.NoErrorsExist
90+
91+
[<Test>]
92+
member this.FailwithfShouldNotSwallowExceptions() =
93+
this.Parse """
94+
try
95+
foo()
96+
with
97+
| e ->
98+
failwithf "bar" """
99+
100+
Assert.IsTrue this.ErrorsExist
101+
Assert.IsTrue(this.ErrorExistsAt(6, 4))
102+
103+
[<Test>]
104+
member this.FailwithWithfGoodArguments2() =
105+
this.Parse """
106+
let foo () =
107+
failwithf "foo" """
108+
109+
this.AssertNoWarnings()
110+
111+
[<Test>]
112+
member this.FailwithfWithBadArgumentsEmptyMessage3() =
113+
this.Parse """
114+
let foo () =
115+
failwithf String.Empty"""
116+
117+
Assert.IsTrue this.ErrorsExist
118+
Assert.IsTrue(this.ErrorExistsAt(3, 4))

0 commit comments

Comments
 (0)