Skip to content

Commit 873d145

Browse files
Add FavourAsKeyword rule (#709)
* FavourAsKeyword: add rule and config * FavourAsKeyword: add docs * FavourAsKeyword: add tests * FavourAsKeyword: add rule to fsharplint.json * FavourAsKeyword: refactor tests * FavourAsKeyword: refactor error message text
1 parent 77dbfd3 commit 873d145

File tree

10 files changed

+155
-2
lines changed

10 files changed

+155
-2
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,4 @@ The following rules can be specified for linting.
126126
- [UnneededRecKeyword (FL0083)](rules/FL0083.html)
127127
- [FavourNonMutablePropertyInitialization (FL0084)](rules/FL0084.html)
128128
- [EnsureTailCallDiagnosticsInRecursiveFunctions (FL0085)](rules/FL0085.html)
129+
- [FavourAsKeyword (FL0086)](rules/FL0086.html)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
title: FL0086
3+
category: how-to
4+
hide_menu: true
5+
---
6+
7+
# FavourAsKeyword (FL0086)
8+
9+
*Introduced in `0.24.3`*
10+
11+
## Cause
12+
13+
A named pattern is used just to be compared in the guard against a constant expression e.g. `match something with | bar when bar = "baz" -> ()`
14+
15+
## Rationale
16+
17+
The named pattern can be changed to an as pattern that uses a constant pattern, improving the pattern matching exhaustiveness check
18+
19+
## How To Fix
20+
21+
Remove the guard and replace the named pattern with the as pattern using a constant pattern, e.g. change `match something with | bar when bar = "baz" -> ()` to `match something with | "baz" as bar -> ()`
22+
23+
## Rule Settings
24+
25+
{
26+
"favourAsKeyword": {
27+
"enabled": true
28+
}
29+
}

src/FSharpLint.Core/Application/Configuration.fs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ type BindingConfig =
297297
wildcardNamedWithAsPattern:EnabledConfig option
298298
uselessBinding:EnabledConfig option
299299
tupleOfWildcards:EnabledConfig option
300+
favourAsKeyword:EnabledConfig option
300301
favourTypedIgnore:EnabledConfig option }
301302
with
302303
member this.Flatten() =
@@ -306,6 +307,7 @@ with
306307
this.wildcardNamedWithAsPattern |> Option.bind (constructRuleIfEnabled WildcardNamedWithAsPattern.rule) |> Option.toArray
307308
this.uselessBinding |> Option.bind (constructRuleIfEnabled UselessBinding.rule) |> Option.toArray
308309
this.tupleOfWildcards |> Option.bind (constructRuleIfEnabled TupleOfWildcards.rule) |> Option.toArray
310+
this.favourAsKeyword |> Option.bind (constructRuleIfEnabled FavourAsKeyword.rule) |> Option.toArray
309311
|] |> Array.concat
310312

311313
type ConventionsConfig =
@@ -479,7 +481,8 @@ type Configuration =
479481
NoTabCharacters:EnabledConfig option
480482
NoPartialFunctions:RuleConfig<NoPartialFunctions.Config> option
481483
SuggestUseAutoProperty:EnabledConfig option
482-
EnsureTailCallDiagnosticsInRecursiveFunctions:EnabledConfig option }
484+
EnsureTailCallDiagnosticsInRecursiveFunctions:EnabledConfig option
485+
FavourAsKeyword:EnabledConfig option }
483486
with
484487
static member Zero = {
485488
Global = None
@@ -571,6 +574,7 @@ with
571574
NoPartialFunctions = None
572575
SuggestUseAutoProperty = None
573576
EnsureTailCallDiagnosticsInRecursiveFunctions = None
577+
FavourAsKeyword = None
574578
}
575579

576580
// fsharplint:enable RecordFieldNames
@@ -725,6 +729,7 @@ let flattenConfig (config:Configuration) =
725729
config.NoTabCharacters |> Option.bind (constructRuleIfEnabled NoTabCharacters.rule)
726730
config.NoPartialFunctions |> Option.bind (constructRuleWithConfig NoPartialFunctions.rule)
727731
config.EnsureTailCallDiagnosticsInRecursiveFunctions |> Option.bind (constructRuleIfEnabled EnsureTailCallDiagnosticsInRecursiveFunctions.rule)
732+
config.FavourAsKeyword |> Option.bind (constructRuleIfEnabled FavourAsKeyword.rule)
728733
|] |> Array.choose id
729734

730735
if config.NonPublicValuesNames.IsSome &&

src/FSharpLint.Core/FSharpLint.Core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
<Compile Include="Rules\Conventions\Binding\WildcardNamedWithAsPattern.fs" />
113113
<Compile Include="Rules\Conventions\Binding\UselessBinding.fs" />
114114
<Compile Include="Rules\Conventions\Binding\TupleOfWildcards.fs" />
115+
<Compile Include="Rules\Conventions\Binding\FavourAsKeyword.fs" />
115116
<Compile Include="Rules\Conventions\AvoidTooShortNames.fs" />
116117
<Compile Include="Rules\Typography\Indentation.fs" />
117118
<Compile Include="Rules\Typography\MaxCharactersOnLine.fs" />
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module FSharpLint.Rules.FavourAsKeyword
2+
3+
open System
4+
open FSharpLint.Framework
5+
open FSharpLint.Framework.Suggestion
6+
open FSharp.Compiler.Syntax
7+
open FSharp.Compiler.Text
8+
open FSharpLint.Framework.Ast
9+
open FSharpLint.Framework.Rules
10+
11+
let private checkForNamedPatternEqualsConstant (args:AstNodeRuleParams) pattern whenExpr (range:Range) =
12+
let patternIdent =
13+
match pattern with
14+
| SynPat.Named(SynIdent.SynIdent(ident, _), _, _, _) -> Some(ident.idText)
15+
| _ -> None
16+
17+
18+
match whenExpr with
19+
| SynExpr.App(_, _, funcExpr, SynExpr.Const(_, constRange), _) ->
20+
match funcExpr with
21+
| SynExpr.App(_, _, ExpressionUtilities.Identifier([opIdent], _), SynExpr.Ident(ident), _)
22+
when opIdent.idText = "op_Equality" && Option.contains ident.idText patternIdent ->
23+
24+
let fromRange = Range.mkRange "" range.Start constRange.End
25+
26+
let suggestedFix =
27+
ExpressionUtilities.tryFindTextOfRange fromRange args.FileContent
28+
|> Option.bind (fun text ->
29+
30+
ExpressionUtilities.tryFindTextOfRange constRange args.FileContent
31+
|> Option.bind (fun constText ->
32+
33+
lazy (Some { FromText = text; FromRange = fromRange; ToText = constText + " as " + ident.idText})
34+
|> Some
35+
)
36+
37+
)
38+
39+
{ Range = fromRange
40+
Message = Resources.GetString("RulesFavourAsKeyword")
41+
SuggestedFix = suggestedFix
42+
TypeChecks = [] } |> Array.singleton
43+
44+
| _ -> Array.empty
45+
| _ -> Array.empty
46+
47+
let private runner (args:AstNodeRuleParams) =
48+
match args.AstNode with
49+
| AstNode.Match(SynMatchClause.SynMatchClause(pat, Some(whenExpr), _, range, _, _)) ->
50+
checkForNamedPatternEqualsConstant args pat whenExpr range
51+
52+
| _ -> Array.empty
53+
54+
let rule =
55+
{ Name = "FavourAsKeyword"
56+
Identifier = Identifiers.FavourAsKeyword
57+
RuleConfig = { AstNodeRuleConfig.Runner = runner; Cleanup = ignore } }
58+
|> AstNodeRule

src/FSharpLint.Core/Rules/Identifiers.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,4 @@ let UsedUnderscorePrefixedElements = identifier 82
9090
let UnneededRecKeyword = identifier 83
9191
let FavourNonMutablePropertyInitialization = identifier 84
9292
let EnsureTailCallDiagnosticsInRecursiveFunctions = identifier 85
93+
let FavourAsKeyword = identifier 86

src/FSharpLint.Core/Text.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,5 +368,8 @@
368368
</data>
369369
<data name="RulesEnsureTailCallDiagnosticsInRecursiveFunctions" xml:space="preserve">
370370
<value>The '{0}' function has a "rec" keyword, but no [&lt;TailCall&gt;] attribute. Consider adding [&lt;TailCall&gt;] attribute to the function and &lt;WarningsAsErrors&gt;FS3569&lt;/WarningsAsErrors&gt; property to project file (but only on .NET 8 and higher).</value>
371-
</data>
371+
</data>
372+
<data name="RulesFavourAsKeyword" xml:space="preserve">
373+
<value>Prefer using the 'as' pattern to match a constant and bind it to a variable.</value>
374+
</data>
372375
</root>

src/FSharpLint.Core/fsharplint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@
332332
}
333333
},
334334
"ensureTailCallDiagnosticsInRecursiveFunctions": { "enabled": false },
335+
"favourAsKeyword": { "enabled": true },
335336
"hints": {
336337
"add": [
337338
"not (a = b) ===> a <> b",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
<Compile Include="Rules\Binding\UselessBinding.fs" />
8181
<Compile Include="Rules\Binding\WildcardNamedWithAsPattern.fs" />
8282
<Compile Include="Rules\Binding\TupleOfWildcards.fs" />
83+
<Compile Include="Rules\Binding\FavourAsKeyword.fs" />
8384
<Compile Include="Rules\Hints\HintMatcher.fs" />
8485
</ItemGroup>
8586

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
module FSharpLint.Core.Tests.Rules.Binding.FavourAsKeyword
2+
3+
open NUnit.Framework
4+
open FSharpLint.Rules
5+
6+
[<TestFixture>]
7+
type TestBindingFavourAsKeyword() =
8+
inherit TestAstNodeRuleBase.TestAstNodeRuleBase(FavourAsKeyword.rule)
9+
10+
[<Test>]
11+
member this.FavourAsKeywordShouldQuickFix() =
12+
let source = """
13+
module Program
14+
15+
match "" with
16+
| bar when bar = "baz" -> ()
17+
"""
18+
19+
this.Parse(source)
20+
21+
let expected = """
22+
module Program
23+
24+
match "" with
25+
| "baz" as bar -> ()
26+
"""
27+
28+
Assert.AreEqual(expected, this.ApplyQuickFix source)
29+
30+
31+
[<Test>]
32+
member this.FavourAsKeywordShouldProduceError() =
33+
this.Parse """
34+
module Program
35+
36+
match "" with
37+
| bar when bar = "baz" -> ()
38+
"""
39+
40+
this.AssertErrorWithMessageExists("Prefer using the 'as' pattern to match a constant and bind it to a variable.")
41+
42+
43+
[<Test>]
44+
member this.FavourAsKeywordShouldNotProduceError() =
45+
this.Parse """
46+
module Program
47+
48+
match "" with
49+
| "baz" as bar -> ()
50+
"""
51+
52+
Assert.IsTrue(this.NoErrorsExist)
53+

0 commit comments

Comments
 (0)