Skip to content

Commit 596de01

Browse files
su8898parhamsaremi
authored andcommitted
Add new rule FavourSingleton
Fixes #526 Co-authored-by: Parham Saremi <[email protected]>
1 parent 9bec3ed commit 596de01

File tree

10 files changed

+172
-1
lines changed

10 files changed

+172
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,4 @@ The following rules can be specified for linting.
129129
- [FavourAsKeyword (FL0086)](rules/FL0086.html)
130130
- [InterpolatedStringWithNoSubstitution (FL0087)](rules/FL0087.html)
131131
- [IndexerAccessorStyleConsistency (FL0088)](rules/FL0088.html)
132+
- [FavourSingleton (FL0089)](rules/FL0089.html)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
title: FL0089
3+
category: how-to
4+
hide_menu: true
5+
---
6+
7+
# FavourSingleton (FL0089)
8+
9+
*Introduced in `0.26.10`*
10+
11+
## Cause
12+
13+
Rule to detect usage of lists or arrays with only one item.
14+
15+
## Rationale
16+
17+
List.singleton/Array.singleton is more readable and explicit than [ foo]/[| foo |].
18+
19+
## How To Fix
20+
21+
Replace all occurrences of single member lists/arrays with a call to List.singleton/Array.singleton.
22+
23+
## Rule Settings
24+
25+
{
26+
"favourSingleton": {
27+
"enabled": false
28+
}
29+
}

src/FSharpLint.Core/Application/Configuration.fs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,8 @@ type Configuration =
513513
SuggestUseAutoProperty:EnabledConfig option
514514
EnsureTailCallDiagnosticsInRecursiveFunctions:EnabledConfig option
515515
FavourAsKeyword:EnabledConfig option
516-
InterpolatedStringWithNoSubstitution:EnabledConfig option }
516+
InterpolatedStringWithNoSubstitution:EnabledConfig option
517+
FavourSingleton:EnabledConfig option }
517518
with
518519
static member Zero = {
519520
Global = None
@@ -610,6 +611,7 @@ with
610611
EnsureTailCallDiagnosticsInRecursiveFunctions = None
611612
FavourAsKeyword = None
612613
InterpolatedStringWithNoSubstitution = None
614+
FavourSingleton = None
613615
}
614616

615617
// fsharplint:enable RecordFieldNames
@@ -807,6 +809,7 @@ let flattenConfig (config:Configuration) =
807809
config.EnsureTailCallDiagnosticsInRecursiveFunctions |> Option.bind (constructRuleIfEnabled EnsureTailCallDiagnosticsInRecursiveFunctions.rule)
808810
config.FavourAsKeyword |> Option.bind (constructRuleIfEnabled FavourAsKeyword.rule)
809811
config.InterpolatedStringWithNoSubstitution |> Option.bind (constructRuleIfEnabled InterpolatedStringWithNoSubstitution.rule)
812+
config.FavourSingleton |> Option.bind (constructRuleIfEnabled FavourSingleton.rule)
810813
|]
811814

812815
findDeprecation config deprecatedAllRules allRules

src/FSharpLint.Core/FSharpLint.Core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
<Compile Include="Rules\Smells\RaiseWithTooManyArguments\InvalidArgWithTwoArguments.fs" />
7070
<Compile Include="Rules\Smells\RaiseWithTooManyArguments\FailwithfWithArgumentsMatchingFormatString.fs" />
7171
<Compile Include="Rules\Conventions\FailwithBadUsage.fs" />
72+
<Compile Include="Rules\Conventions\FavourSingleton.fs" />
7273
<Compile Include="Rules\Conventions\SourceLength\SourceLengthHelper.fs" />
7374
<Compile Include="Rules\Conventions\SourceLength\MaxLinesInLambdaFunction.fs" />
7475
<Compile Include="Rules\Conventions\SourceLength\MaxLinesInMatchLambdaFunction.fs" />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module FSharpLint.Rules.FavourSingleton
2+
3+
open FSharpLint.Framework
4+
open FSharpLint.Framework.Suggestion
5+
open FSharp.Compiler.Syntax
6+
open FSharp.Compiler.Text
7+
open FSharpLint.Framework.Ast
8+
open FSharpLint.Framework.Rules
9+
open System
10+
11+
let runner args =
12+
match args.AstNode with
13+
| AstNode.Binding(SynBinding(_, _, _, _, _, _, _, _, _, expression, _, _)) ->
14+
match expression with
15+
| SynExpr.ArrayOrListOfSeqExpr(_, SynExpr.CompExpr(_, _, expr,range), _) ->
16+
match expr with
17+
| SynExpr.Const(_, range) ->
18+
{ Range = range
19+
Message = String.Format(Resources.GetString "RulesFavourSingleton")
20+
SuggestedFix = None
21+
TypeChecks = List.Empty }
22+
|> Array.singleton
23+
| SynExpr.Ident _ ->
24+
{ Range = range
25+
Message = String.Format(Resources.GetString "RulesFavourSingleton")
26+
SuggestedFix = None
27+
TypeChecks = List.Empty }
28+
|> Array.singleton
29+
| _ -> Array.empty
30+
| _ -> Array.empty
31+
| _ -> Array.empty
32+
let rule =
33+
{ Name = "FavourSingleton"
34+
Identifier = Identifiers.FavourSingleton
35+
RuleConfig =
36+
{ AstNodeRuleConfig.Runner = runner
37+
Cleanup = ignore } }
38+
|> AstNodeRule

src/FSharpLint.Core/Rules/Identifiers.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,4 @@ let EnsureTailCallDiagnosticsInRecursiveFunctions = identifier 85
9393
let FavourAsKeyword = identifier 86
9494
let InterpolatedStringWithNoSubstitution = identifier 87
9595
let IndexerAccessorStyleConsistency = identifier 88
96+
let FavourSingleton = identifier 89

src/FSharpLint.Core/Text.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,7 @@
387387
<data name="RulesIndexerAccessorStyleConsistency" xml:space="preserve">
388388
<value>Consider switching the indexer accessor to {0} style.</value>
389389
</data>
390+
<data name="RulesFavourSingleton" xml:space="preserve">
391+
<value>Consider using List.singleton/Array.singleton instead of single member list/array.</value>
392+
</data>
390393
</root>

src/FSharpLint.Core/fsharplint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@
282282
}
283283
},
284284
"favourIgnoreOverLetWild": { "enabled": true },
285+
"favourSingleton": { "enabled": false },
285286
"wildcardNamedWithAsPattern": { "enabled": true },
286287
"uselessBinding": { "enabled": true },
287288
"tupleOfWildcards": { "enabled": true },

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<Compile Include="Rules\Conventions\FavourNonMutablePropertyInitialization.fs" />
4949
<Compile Include="Rules\Conventions\EnsureTailCallDiagnosticsInRecursiveFunctions.fs" />
5050
<Compile Include="Rules\Conventions\IndexerAccessorStyleConsistency.fs" />
51+
<Compile Include="Rules\Conventions\FavourSingleton.fs" />
5152
<Compile Include="Rules\Conventions\Naming\NamingHelpers.fs" />
5253
<Compile Include="Rules\Conventions\Naming\InterfaceNames.fs" />
5354
<Compile Include="Rules\Conventions\Naming\ExceptionNames.fs" />
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
module FSharpLint.Core.Tests.Rules.Conventions.FavourSingleton
2+
3+
open NUnit.Framework
4+
open FSharpLint.Rules
5+
open System
6+
7+
[<TestFixture>]
8+
type TestConventionsFavourSingleton() =
9+
inherit TestAstNodeRuleBase.TestAstNodeRuleBase(FavourSingleton.rule)
10+
11+
[<Test>]
12+
member this.ListWithManyItemsShouldNotProduceError() =
13+
this.Parse """
14+
let foo = [ 10; 20 ]"""
15+
16+
this.AssertNoWarnings()
17+
18+
[<Test>]
19+
member this.ListWithASingleConstantShouldProduceError() =
20+
this.Parse """
21+
let foo = [ 10 ]"""
22+
23+
Assert.IsTrue this.ErrorsExist
24+
Assert.IsTrue(this.ErrorExistsAt(2, 12))
25+
26+
[<Test>]
27+
member this.ListWithASingleIdentShouldProduceError() =
28+
this.Parse """
29+
let bar = true
30+
let foo = [ bar ]"""
31+
32+
Assert.IsTrue this.ErrorsExist
33+
Assert.IsTrue(this.ErrorExistsAt(3, 12))
34+
35+
[<Test>]
36+
member this.ListWithMultipleIdentsShouldNotProduceError() =
37+
this.Parse """
38+
let bar = true
39+
let foo = [ bar; false; true ]"""
40+
41+
this.AssertNoWarnings()
42+
43+
[<Test>]
44+
member this.ListWithManyItemsShouldNotProduceError_Arrays() =
45+
this.Parse """
46+
let foo = [| 10; 20 |]"""
47+
48+
Assert.IsTrue this.NoErrorsExist
49+
50+
[<Test>]
51+
member this.ListWithASingleConstantShouldProduceError_Arrays() =
52+
this.Parse """
53+
let foo = [| 10 |]"""
54+
55+
Assert.IsTrue this.ErrorsExist
56+
Assert.IsTrue(this.ErrorExistsAt(2, 13))
57+
58+
[<Test>]
59+
member this.ListWithASingleIdentShouldProduceError_Arrays() =
60+
this.Parse """
61+
let bar = true
62+
let foo = [| bar |]"""
63+
64+
Assert.IsTrue this.ErrorsExist
65+
Assert.IsTrue(this.ErrorExistsAt(3, 13))
66+
67+
[<Test>]
68+
member this.ListWithMultipleIdentsShouldNotProduceError_Arrays() =
69+
this.Parse """
70+
let bar = true
71+
let foo = [| bar; false; true |]"""
72+
73+
this.AssertNoWarnings()
74+
75+
[<Test>]
76+
member this.SingletonListWithMatchCaseShouldNotProduceError() =
77+
this.Parse """
78+
let foo = List.empty
79+
match foo with
80+
| [x] -> printf x
81+
| _ -> printf "baz" """
82+
83+
this.AssertNoWarnings()
84+
85+
[<Test>]
86+
member this.SingletonArrayWithMatchCaseShouldNotProduceError() =
87+
this.Parse """
88+
let foo = Array.empty
89+
match foo with
90+
| [| x |] -> printf x
91+
| _ -> printf "baz" """
92+
93+
this.AssertNoWarnings()

0 commit comments

Comments
 (0)