diff --git a/docs/content/how-tos/rule-configuration.md b/docs/content/how-tos/rule-configuration.md index 10b14fbb6..9cba93ab3 100644 --- a/docs/content/how-tos/rule-configuration.md +++ b/docs/content/how-tos/rule-configuration.md @@ -114,4 +114,5 @@ The following rules can be specified for linting. - [CyclomaticComplexity (FL0071)](rules/FL0071.html) - [FailwithBadUsage (FL0072)](rules/FL0072.html) - [FavourReRaise (FL0073)](rules/FL0073.html) -- [FavourConsistentThis (FL0074)](rules/FL0074.html) \ No newline at end of file +- [FavourConsistentThis (FL0074)](rules/FL0074.html) +- [FavourSingleton (FL0075)](rules/FL0075.html) \ No newline at end of file diff --git a/docs/content/how-tos/rules/FL0075.md b/docs/content/how-tos/rules/FL0075.md new file mode 100644 index 000000000..62e7c5722 --- /dev/null +++ b/docs/content/how-tos/rules/FL0075.md @@ -0,0 +1,30 @@ +--- +title: FL0075 +category: how-to +hide_menu: true +--- + +# FavourSingleton (FL0075) + +*Introduced in `0.21.1`* + +## Cause + +Rule to detect usage of lists or arrays with only one item. + +## Rationale + +List.singleton/Array.singleton is more readable and explicit than [ foo]/[| foo |]. + +## How To Fix + +Replace all occurrences of single member lists/arrays with a call to List.singleton/Array.singleton. + +## Rule Settings + + { + "favourSingleton": { + "enabled": false + } + } + diff --git a/src/FSharpLint.Core/Application/Configuration.fs b/src/FSharpLint.Core/Application/Configuration.fs index 75458899e..91d944ddd 100644 --- a/src/FSharpLint.Core/Application/Configuration.fs +++ b/src/FSharpLint.Core/Application/Configuration.fs @@ -433,7 +433,8 @@ type Configuration = MaxLinesInFile:RuleConfig option TrailingNewLineInFile:EnabledConfig option NoTabCharacters:EnabledConfig option - NoPartialFunctions:RuleConfig option } + NoPartialFunctions:RuleConfig option + FavourSingleton:EnabledConfig option } with static member Zero = { Global = None @@ -511,6 +512,7 @@ with TrailingNewLineInFile = None NoTabCharacters = None NoPartialFunctions = None + FavourSingleton = None } // fsharplint:enable RecordFieldNames @@ -652,6 +654,7 @@ let flattenConfig (config:Configuration) = config.TrailingNewLineInFile |> Option.bind (constructRuleIfEnabled TrailingNewLineInFile.rule) config.NoTabCharacters |> Option.bind (constructRuleIfEnabled NoTabCharacters.rule) config.NoPartialFunctions |> Option.bind (constructRuleWithConfig NoPartialFunctions.rule) + config.FavourSingleton |> Option.bind (constructRuleIfEnabled FavourSingleton.rule) |] |> Array.choose id if config.NonPublicValuesNames.IsSome && diff --git a/src/FSharpLint.Core/FSharpLint.Core.fsproj b/src/FSharpLint.Core/FSharpLint.Core.fsproj index 2d471d4ef..08da89202 100644 --- a/src/FSharpLint.Core/FSharpLint.Core.fsproj +++ b/src/FSharpLint.Core/FSharpLint.Core.fsproj @@ -49,6 +49,7 @@ + diff --git a/src/FSharpLint.Core/Rules/Conventions/FavourSingleton.fs b/src/FSharpLint.Core/Rules/Conventions/FavourSingleton.fs new file mode 100644 index 000000000..6b8bbfc1d --- /dev/null +++ b/src/FSharpLint.Core/Rules/Conventions/FavourSingleton.fs @@ -0,0 +1,38 @@ +module FSharpLint.Rules.FavourSingleton + +open FSharpLint.Framework +open FSharpLint.Framework.Suggestion +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text +open FSharpLint.Framework.Ast +open FSharpLint.Framework.Rules +open System + +let runner args = + match args.AstNode with + | AstNode.Binding(SynBinding(_, _, _, _, _, _, _, _, _, expression, _, _)) -> + match expression with + | SynExpr.ArrayOrListOfSeqExpr(_, SynExpr.CompExpr(_, _, expr,range), _) -> + match expr with + | SynExpr.Const(_, range) -> + { Range = range + Message = String.Format(Resources.GetString "RulesFavourSingleton") + SuggestedFix = None + TypeChecks = List.Empty } + |> Array.singleton + | SynExpr.Ident _ -> + { Range = range + Message = String.Format(Resources.GetString "RulesFavourSingleton") + SuggestedFix = None + TypeChecks = List.Empty } + |> Array.singleton + | _ -> Array.empty + | _ -> Array.empty + | _ -> Array.empty +let rule = + { Name = "FavourSingleton" + Identifier = Identifiers.FavourSingleton + RuleConfig = + { AstNodeRuleConfig.Runner = runner + Cleanup = ignore } } + |> AstNodeRule diff --git a/src/FSharpLint.Core/Rules/Identifiers.fs b/src/FSharpLint.Core/Rules/Identifiers.fs index 8e736d176..bb67fce2f 100644 --- a/src/FSharpLint.Core/Rules/Identifiers.fs +++ b/src/FSharpLint.Core/Rules/Identifiers.fs @@ -79,3 +79,4 @@ let CyclomaticComplexity = identifier 71 let FailwithBadUsage = identifier 72 let FavourReRaise = identifier 73 let FavourConsistentThis = identifier 74 +let FavourSingleton = identifier 75 diff --git a/src/FSharpLint.Core/Text.resx b/src/FSharpLint.Core/Text.resx index f974958e5..379cab1a1 100644 --- a/src/FSharpLint.Core/Text.resx +++ b/src/FSharpLint.Core/Text.resx @@ -333,4 +333,7 @@ Prefer using '{0}' consistently. + + Consider using List.singleton/Array.singleton instead of single member list/array. + diff --git a/src/FSharpLint.Core/fsharplint.json b/src/FSharpLint.Core/fsharplint.json index fbe6a2a38..90ff2ff9c 100644 --- a/src/FSharpLint.Core/fsharplint.json +++ b/src/FSharpLint.Core/fsharplint.json @@ -261,6 +261,7 @@ } }, "favourIgnoreOverLetWild": { "enabled": true }, + "favourSingleton": { "enabled": false }, "wildcardNamedWithAsPattern": { "enabled": true }, "uselessBinding": { "enabled": true }, "tupleOfWildcards": { "enabled": true }, diff --git a/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj b/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj index e4541efe1..1616724f5 100644 --- a/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj +++ b/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj @@ -37,6 +37,7 @@ + diff --git a/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourSingleton.fs b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourSingleton.fs new file mode 100644 index 000000000..3bc71344a --- /dev/null +++ b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourSingleton.fs @@ -0,0 +1,93 @@ +module FSharpLint.Core.Tests.Rules.Conventions.FavourSingleton + +open NUnit.Framework +open FSharpLint.Rules +open System + +[] +type TestConventionsFavourSingleton() = + inherit TestAstNodeRuleBase.TestAstNodeRuleBase(FavourSingleton.rule) + + [] + member this.ListWithManyItemsShouldNotProduceError() = + this.Parse """ +let foo = [ 10; 20 ]""" + + this.AssertNoWarnings() + + [] + member this.ListWithASingleConstantShouldProduceError() = + this.Parse """ +let foo = [ 10 ]""" + + Assert.IsTrue this.ErrorsExist + Assert.IsTrue(this.ErrorExistsAt(2, 12)) + + [] + member this.ListWithASingleIdentShouldProduceError() = + this.Parse """ +let bar = true +let foo = [ bar ]""" + + Assert.IsTrue this.ErrorsExist + Assert.IsTrue(this.ErrorExistsAt(3, 12)) + + [] + member this.ListWithMultipleIdentsShouldNotProduceError() = + this.Parse """ +let bar = true +let foo = [ bar; false; true ]""" + + this.AssertNoWarnings() + + [] + member this.ListWithManyItemsShouldNotProduceError_Arrays() = + this.Parse """ +let foo = [| 10; 20 |]""" + + Assert.IsTrue this.NoErrorsExist + + [] + member this.ListWithASingleConstantShouldProduceError_Arrays() = + this.Parse """ +let foo = [| 10 |]""" + + Assert.IsTrue this.ErrorsExist + Assert.IsTrue(this.ErrorExistsAt(2, 13)) + + [] + member this.ListWithASingleIdentShouldProduceError_Arrays() = + this.Parse """ +let bar = true +let foo = [| bar |]""" + + Assert.IsTrue this.ErrorsExist + Assert.IsTrue(this.ErrorExistsAt(3, 13)) + + [] + member this.ListWithMultipleIdentsShouldNotProduceError_Arrays() = + this.Parse """ +let bar = true +let foo = [| bar; false; true |]""" + + this.AssertNoWarnings() + + [] + member this.SingletonListWithMatchCaseShouldNotProduceError() = + this.Parse """ +let foo = List.empty +match foo with +| [x] -> printf x +| _ -> printf "baz" """ + + this.AssertNoWarnings() + + [] + member this.SingletonArrayWithMatchCaseShouldNotProduceError() = + this.Parse """ +let foo = Array.empty +match foo with +| [| x |] -> printf x +| _ -> printf "baz" """ + + this.AssertNoWarnings()