From 904ef9c7591ba2f30a6859bf9dcbe663408fb6a2 Mon Sep 17 00:00:00 2001 From: webwarrior Date: Thu, 21 Dec 2023 13:36:34 +0100 Subject: [PATCH 01/10] Core(Tests): add FavourNestedFunctions rule Added FavourNestedFunctions rule and tests for it. --- src/FSharpLint.Core/FSharpLint.Core.fsproj | 1 + .../Conventions/FavourNestedFunctions.fs | 19 +++++ src/FSharpLint.Core/Rules/Identifiers.fs | 1 + .../FSharpLint.Core.Tests.fsproj | 1 + .../Conventions/FavourNestedFunctions.fs | 76 +++++++++++++++++++ 5 files changed, 98 insertions(+) create mode 100644 src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs create mode 100644 tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs diff --git a/src/FSharpLint.Core/FSharpLint.Core.fsproj b/src/FSharpLint.Core/FSharpLint.Core.fsproj index e0810b760..e4ecd2c77 100644 --- a/src/FSharpLint.Core/FSharpLint.Core.fsproj +++ b/src/FSharpLint.Core/FSharpLint.Core.fsproj @@ -60,6 +60,7 @@ + diff --git a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs new file mode 100644 index 000000000..67f13141e --- /dev/null +++ b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs @@ -0,0 +1,19 @@ +module FSharpLint.Rules.FavourNestedFunctions + +open System +open FSharp.Compiler.Syntax +open FSharpLint.Framework.Ast +open FSharpLint.Framework.Rules +open FSharpLint.Framework +open FSharpLint.Framework.Suggestion + +let runner (args: AstNodeRuleParams) = + failwith "Not yet implemeted" + +let rule = + { Name = "FavourNestedFunctions" + Identifier = Identifiers.FavourNestedFunctions + RuleConfig = + { AstNodeRuleConfig.Runner = runner + Cleanup = ignore } } + |> AstNodeRule diff --git a/src/FSharpLint.Core/Rules/Identifiers.fs b/src/FSharpLint.Core/Rules/Identifiers.fs index 01889c1bc..d4a22b10d 100644 --- a/src/FSharpLint.Core/Rules/Identifiers.fs +++ b/src/FSharpLint.Core/Rules/Identifiers.fs @@ -95,3 +95,4 @@ let InterpolatedStringWithNoSubstitution = identifier 87 let IndexerAccessorStyleConsistency = identifier 88 let FavourSingleton = identifier 89 let NoAsyncRunSynchronouslyInLibrary = identifier 90 +let FavourNestedFunctions = identifier 91 diff --git a/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj b/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj index d57f7fff5..f83e1204f 100644 --- a/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj +++ b/tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj @@ -50,6 +50,7 @@ + diff --git a/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs new file mode 100644 index 000000000..da408c395 --- /dev/null +++ b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs @@ -0,0 +1,76 @@ +module FSharpLint.Core.Tests.Rules.Conventions.FavourNestedFunctions + +open NUnit.Framework +open FSharpLint.Rules +open FSharpLint.Core.Tests + +[] +type TestFavourNestedFunctions() = + inherit TestAstNodeRuleBase.TestAstNodeRuleBase(FavourNestedFunctions.rule) + + [] + member this.``Top level functions that are not used in another function should not give an error`` () = + this.Parse """ +let Foo () = + () + +let Bar () = + () +""" + + this.AssertNoWarnings() + + [] + member this.``Top level private functions that are not used in another function should not give an error`` () = + this.Parse """ +let private Foo () = + () + +let Bar () = + () +""" + + this.AssertNoWarnings() + + [] + member this.``Top level private function that is used in single function should give an error`` () = + this.Parse """ +let private Foo () = + () + +let Bar () = + Foo() + () +""" + + Assert.IsTrue this.ErrorsExist + + [] + member this.``Nested functions should not give an error`` () = + this.Parse """ +let Bar () = + let Foo() = + () + + Foo() + () +""" + + this.AssertNoWarnings() + + [] + member this.``Private function that is used in more than one function should not give an error`` () = + this.Parse """ +let private Foo () = + () + +let Bar () = + Foo() + () + +let Baz () = + Foo () + () +""" + + this.AssertNoWarnings() From f0f86af974ff509044b8df709a615c7647dbb979 Mon Sep 17 00:00:00 2001 From: webwarrior Date: Thu, 21 Dec 2023 13:37:23 +0100 Subject: [PATCH 02/10] FavourNestedFunctions: implement rule Implemented FavourNestedFunctions rule. Added rule text message to Text.resx. Fixes #638 --- .../Conventions/FavourNestedFunctions.fs | 57 ++++++++++++++++++- src/FSharpLint.Core/Text.resx | 3 + 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs index 67f13141e..bb82acaec 100644 --- a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs +++ b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs @@ -7,8 +7,63 @@ open FSharpLint.Framework.Rules open FSharpLint.Framework open FSharpLint.Framework.Suggestion +let private (|FunctionDeclaration|_|) (declaration: SynModuleDecl) = + match declaration with + | SynModuleDecl.Let(_, [ SynBinding(_, _, _, _, _, _, _, headPat, _, expr, _, _, _) ], _) -> + match headPat with + | SynPat.LongIdent(SynLongIdent([ident], _, _), _, _, _, accessibility, _) -> + Some(ident, expr, accessibility) + | _ -> None + | _ -> None + let runner (args: AstNodeRuleParams) = - failwith "Not yet implemeted" + match args.AstNode with + | AstNode.ModuleOrNamespace(SynModuleOrNamespace(_, _, _kind, declarations, _, _, _, _, _)) -> + let privateFunctionIdentifiers = + declarations + |> Seq.choose + (fun declaration -> + match declaration with + | FunctionDeclaration(ident, _body, Some(SynAccess.Private _)) -> + Some ident + | _ -> None) + |> Seq.toArray + + match args.CheckInfo with + | Some checkInfo when privateFunctionIdentifiers.Length > 0 -> + let otherFunctionBodies = + declarations + |> List.choose + (fun declaration -> + match declaration with + | FunctionDeclaration(ident, body, _) + when not(Array.exists (fun (each: Ident) -> each.idText = ident.idText) privateFunctionIdentifiers) -> + Some body + | _ -> None) + + privateFunctionIdentifiers + |> Array.choose + (fun currFunctionIdentifier -> + match ExpressionUtilities.getSymbolFromIdent args.CheckInfo (SynExpr.Ident currFunctionIdentifier) with + | Some symbolUse -> + let numberOfOtherFunctionsCurrFunctionIsUsedIn = + otherFunctionBodies + |> Seq.filter (fun funcBody -> + checkInfo.GetUsesOfSymbolInFile symbolUse.Symbol + |> Array.exists (fun usage -> ExpressionUtilities.rangeContainsOtherRange funcBody.Range usage.Range)) + |> Seq.length + if numberOfOtherFunctionsCurrFunctionIsUsedIn = 1 then + Some { + Range = currFunctionIdentifier.idRange + WarningDetails.Message = Resources.GetString "RulesFavourNestedFunctions" + SuggestedFix = None + TypeChecks = List.Empty + } + else + None + | None -> None) + | _ -> Array.empty + | _ -> Array.empty let rule = { Name = "FavourNestedFunctions" diff --git a/src/FSharpLint.Core/Text.resx b/src/FSharpLint.Core/Text.resx index a6f38a3f4..9493ce3f3 100644 --- a/src/FSharpLint.Core/Text.resx +++ b/src/FSharpLint.Core/Text.resx @@ -393,4 +393,7 @@ Async.RunSynchronously should not be used in libraries. + + Prefer using local functions over private module-level functions + From acf70ecfe0e3f0dea4d220db59f44396d05732ed Mon Sep 17 00:00:00 2001 From: webwarrior Date: Thu, 21 Dec 2023 13:51:50 +0100 Subject: [PATCH 03/10] FavourNestedFunctions: hook cfg & add docs Updated Configuration.fs and fsharplint.json to include FavourNestedFunctions rule. Added docs. --- docs/content/how-tos/rule-configuration.md | 1 + docs/content/how-tos/rules/FL0091.md | 31 +++++++++++++++++++ .../Application/Configuration.fs | 9 ++++-- src/FSharpLint.Core/fsharplint.json | 1 + 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 docs/content/how-tos/rules/FL0091.md diff --git a/docs/content/how-tos/rule-configuration.md b/docs/content/how-tos/rule-configuration.md index 7870db243..31e0c3906 100644 --- a/docs/content/how-tos/rule-configuration.md +++ b/docs/content/how-tos/rule-configuration.md @@ -131,3 +131,4 @@ The following rules can be specified for linting. - [IndexerAccessorStyleConsistency (FL0088)](rules/FL0088.html) - [FavourSingleton (FL0089)](rules/FL0089.html) - [NoAsyncRunSynchronouslyInLibrary (FL0090)](rules/FL0090.html) +- [FavourNestedFunctions (FL0091)](rules/FL0091.html) diff --git a/docs/content/how-tos/rules/FL0091.md b/docs/content/how-tos/rules/FL0091.md new file mode 100644 index 000000000..4bd8c0651 --- /dev/null +++ b/docs/content/how-tos/rules/FL0091.md @@ -0,0 +1,31 @@ +--- +title: FL0091 +category: how-to +hide_menu: true +--- + +# FavourNestedFunctions (FL0091) + +*Introduced in `0.26.12`* + +## Cause + +Prefer using local (nested) functions over private module-level functions. + +## Rationale + +With a nested function, one can clearly see from reading the code that there is only one consumer of the function. +The code being this way becomes more streamlined. +Code is also more concise because nested functions don't need accessibility modifiers. + +## How To Fix + +Move private function inside function that uses it. + +## Rule Settings + + { + "FavourNestedFunctions": { + "enabled": false + } + } \ No newline at end of file diff --git a/src/FSharpLint.Core/Application/Configuration.fs b/src/FSharpLint.Core/Application/Configuration.fs index f02086d1e..c26188051 100644 --- a/src/FSharpLint.Core/Application/Configuration.fs +++ b/src/FSharpLint.Core/Application/Configuration.fs @@ -385,7 +385,8 @@ type ConventionsConfig = favourConsistentThis:RuleConfig option suggestUseAutoProperty:EnabledConfig option usedUnderscorePrefixedElements:EnabledConfig option - ensureTailCallDiagnosticsInRecursiveFunctions:EnabledConfig option} + ensureTailCallDiagnosticsInRecursiveFunctions:EnabledConfig option + favourNestedFunctions:EnabledConfig option } with member this.Flatten() = Array.concat @@ -413,6 +414,7 @@ with this.suggestUseAutoProperty |> Option.bind (constructRuleIfEnabled SuggestUseAutoProperty.rule) |> Option.toArray this.ensureTailCallDiagnosticsInRecursiveFunctions |> Option.bind (constructRuleIfEnabled EnsureTailCallDiagnosticsInRecursiveFunctions.rule) |> Option.toArray this.indexerAccessorStyleConsistency |> Option.bind (constructRuleWithConfig IndexerAccessorStyleConsistency.rule) |> Option.toArray + this.favourNestedFunctions |> Option.bind (constructRuleIfEnabled FavourNestedFunctions.rule) |> Option.toArray |] [] @@ -555,7 +557,8 @@ type Configuration = FavourAsKeyword:EnabledConfig option InterpolatedStringWithNoSubstitution:EnabledConfig option FavourSingleton:EnabledConfig option - NoAsyncRunSynchronouslyInLibrary:EnabledConfig option} + NoAsyncRunSynchronouslyInLibrary:EnabledConfig option + FavourNestedFunctions:EnabledConfig option } with static member Zero = { Global = None @@ -658,6 +661,7 @@ with InterpolatedStringWithNoSubstitution = None FavourSingleton = None NoAsyncRunSynchronouslyInLibrary = None + FavourNestedFunctions = None } // fsharplint:enable RecordFieldNames @@ -862,6 +866,7 @@ let flattenConfig (config:Configuration) = config.InterpolatedStringWithNoSubstitution |> Option.bind (constructRuleIfEnabled InterpolatedStringWithNoSubstitution.rule) config.FavourSingleton |> Option.bind (constructRuleIfEnabled FavourSingleton.rule) config.NoAsyncRunSynchronouslyInLibrary |> Option.bind (constructRuleIfEnabled NoAsyncRunSynchronouslyInLibrary.rule) + config.FavourNestedFunctions |> Option.bind (constructRuleIfEnabled FavourNestedFunctions.rule) |] findDeprecation config deprecatedAllRules allRules diff --git a/src/FSharpLint.Core/fsharplint.json b/src/FSharpLint.Core/fsharplint.json index 7b32a6b17..ca598c2e3 100644 --- a/src/FSharpLint.Core/fsharplint.json +++ b/src/FSharpLint.Core/fsharplint.json @@ -342,6 +342,7 @@ }, "favourSingleton": { "enabled": false }, "noAsyncRunSynchronouslyInLibrary": { "enabled": true }, + "favourNestedFunctions": { "enabled": false }, "hints": { "add": [ "not (a = b) ===> a <> b", From 4d5d7dce62ddfce9591741e7bf15b050fbb4165d Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Wed, 7 Jan 2026 13:19:12 +0100 Subject: [PATCH 04/10] Tests(Core): new test cases for FavourNestedFunctions That test recursive function definitions. --- .../Conventions/FavourNestedFunctions.fs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs index da408c395..3820a5974 100644 --- a/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs +++ b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs @@ -45,6 +45,41 @@ let Bar () = Assert.IsTrue this.ErrorsExist + [] + member this.``Top level recursive private function that is used in single other function should give an error`` () = + this.Parse """ +let rec private Foo x = + if x = 0 then + Foo (x - 1) + else + 0 + +let Bar () = + Foo 3 |> ignore +""" + + Assert.IsTrue this.ErrorsExist + + [] + member this.``Top level recursive private function that is used in multiple other functions should not give an error`` () = + this.Parse """ +let rec private Foo x = + if x = 0 then + Foo (x - 1) + else + 0 + +let rec Bar x = + Foo x |> Baz +and Baz x = + Bar (x - 1) + +let FooBar () = + Foo 4 +""" + + this.AssertNoWarnings() + [] member this.``Nested functions should not give an error`` () = this.Parse """ From 2c80167933e3d8fb4df514222d5a7bf3853a86d3 Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Wed, 7 Jan 2026 13:21:06 +0100 Subject: [PATCH 05/10] FavourNestedFunctions: fix rule Fixed bug when rule was not correctly identifying functions when they are declared using let rec ... and ... syntax. --- .../Conventions/FavourNestedFunctions.fs | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs index bb82acaec..1343fb01d 100644 --- a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs +++ b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs @@ -7,24 +7,29 @@ open FSharpLint.Framework.Rules open FSharpLint.Framework open FSharpLint.Framework.Suggestion -let private (|FunctionDeclaration|_|) (declaration: SynModuleDecl) = - match declaration with - | SynModuleDecl.Let(_, [ SynBinding(_, _, _, _, _, _, _, headPat, _, expr, _, _, _) ], _) -> - match headPat with - | SynPat.LongIdent(SynLongIdent([ident], _, _), _, _, _, accessibility, _) -> - Some(ident, expr, accessibility) - | _ -> None - | _ -> None - let runner (args: AstNodeRuleParams) = + let getFunctionBindings (declaration: SynModuleDecl) = + match declaration with + | SynModuleDecl.Let(_, bindings, _) -> + bindings + |> List.choose + (fun binding -> + match binding with + | SynBinding(_, _, _, _, _, _, _, SynPat.LongIdent(SynLongIdent([ident], _, _), _, _, _, accessibility, _), _, expr, _, _, _) -> + Some(ident, expr, accessibility) + | _ -> None + ) + | _ -> List.Empty + match args.AstNode with | AstNode.ModuleOrNamespace(SynModuleOrNamespace(_, _, _kind, declarations, _, _, _, _, _)) -> let privateFunctionIdentifiers = declarations + |> Seq.collect getFunctionBindings |> Seq.choose - (fun declaration -> - match declaration with - | FunctionDeclaration(ident, _body, Some(SynAccess.Private _)) -> + (fun (ident, _expr, accessibility) -> + match accessibility with + | Some(SynAccess.Private _) -> Some ident | _ -> None) |> Seq.toArray @@ -33,13 +38,13 @@ let runner (args: AstNodeRuleParams) = | Some checkInfo when privateFunctionIdentifiers.Length > 0 -> let otherFunctionBodies = declarations - |> List.choose - (fun declaration -> - match declaration with - | FunctionDeclaration(ident, body, _) - when not(Array.exists (fun (each: Ident) -> each.idText = ident.idText) privateFunctionIdentifiers) -> + |> Seq.collect getFunctionBindings + |> Seq.choose + (fun (ident, body, accessibility) -> + if not(Array.exists (fun (each: Ident) -> each.idText = ident.idText) privateFunctionIdentifiers) then Some body - | _ -> None) + else + None) privateFunctionIdentifiers |> Array.choose From 0c5a4c31f0f65310099c2ed355d00ccce4efd410 Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Wed, 7 Jan 2026 13:25:01 +0100 Subject: [PATCH 06/10] Tests(Core): new test for FavourNestedFunctions That makes sure that literals are not triggering the rule. --- .../Rules/Conventions/FavourNestedFunctions.fs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs index 3820a5974..eb18b5337 100644 --- a/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs +++ b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs @@ -109,3 +109,14 @@ let Baz () = """ this.AssertNoWarnings() + + [] + member this.``Private literal should not give an error because literals can only be module-level`` () = + this.Parse """ +let [] private Foo = 6 + +let Bar () = + Foo +""" + + this.AssertNoWarnings() From 432cc16307477cbe2c3dfe67dc871f9f8d20a366 Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Wed, 7 Jan 2026 13:44:30 +0100 Subject: [PATCH 07/10] FavourNestedFunctions: ignore literals Do not fire the rule for bindings with [] attribute because they can only be module-level. --- .../Rules/Conventions/FavourNestedFunctions.fs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs index 1343fb01d..716402c4e 100644 --- a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs +++ b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs @@ -8,6 +8,14 @@ open FSharpLint.Framework open FSharpLint.Framework.Suggestion let runner (args: AstNodeRuleParams) = + let hasLiteralAttribute (attributes: SynAttributes) = + extractAttributes attributes + |> List.exists + (fun attr -> + match attr.TypeName with + | SynLongIdent([ident], _, _) -> ident.idText = "Literal" + | _ -> false ) + let getFunctionBindings (declaration: SynModuleDecl) = match declaration with | SynModuleDecl.Let(_, bindings, _) -> @@ -15,7 +23,8 @@ let runner (args: AstNodeRuleParams) = |> List.choose (fun binding -> match binding with - | SynBinding(_, _, _, _, _, _, _, SynPat.LongIdent(SynLongIdent([ident], _, _), _, _, _, accessibility, _), _, expr, _, _, _) -> + | SynBinding(_, _, _, _, attributes, _, _, SynPat.LongIdent(SynLongIdent([ident], _, _), _, _, _, accessibility, _), _, expr, _, _, _) + when not(hasLiteralAttribute attributes) -> Some(ident, expr, accessibility) | _ -> None ) From 167a7841bbfbb81e4847fd26d9616f7abbe4222d Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Wed, 7 Jan 2026 11:58:23 +0100 Subject: [PATCH 08/10] WIP: address FSharpLint violations --- src/FSharpLint.Console/Program.fs | 198 ++++++------ .../Application/Configuration.fs | 30 +- src/FSharpLint.Core/Framework/Suppression.fs | 24 +- .../Rules/Conventions/CyclomaticComplexity.fs | 112 +++---- ...TailCallDiagnosticsInRecursiveFunctions.fs | 20 +- .../Conventions/FavourNestedFunctions.fs | 41 +-- .../CanBeReplacedWithComposition.fs | 110 +++---- .../ReimplementsFunction.fs | 78 ++--- .../Conventions/Naming/AvoidTooShortNames.fs | 164 +++++----- .../Conventions/Naming/EnumCasesNames.fs | 12 +- .../Conventions/Naming/ExceptionNames.fs | 16 +- .../Conventions/Naming/GenericTypesNames.fs | 34 +-- .../Conventions/Naming/InterfaceNames.fs | 42 +-- .../Conventions/Naming/InternalValuesNames.fs | 60 ++-- .../Rules/Conventions/Naming/LiteralNames.fs | 32 +- .../Conventions/Naming/MeasureTypeNames.fs | 42 +-- .../Rules/Conventions/Naming/MemberNames.fs | 58 ++-- .../Rules/Conventions/Naming/ModuleNames.fs | 22 +- .../Conventions/Naming/NamespaceNames.fs | 22 +- .../Rules/Conventions/Naming/NamingHelper.fs | 213 +++++++------ .../Conventions/Naming/NestedFunctionNames.fs | 26 +- .../Conventions/Naming/ParameterNames.fs | 84 ++--- .../Conventions/Naming/PrivateValuesNames.fs | 78 ++--- .../Conventions/Naming/PublicValuesNames.fs | 70 ++--- .../Conventions/Naming/RecordFieldNames.fs | 18 +- .../Rules/Conventions/Naming/TypeNames.fs | 46 +-- .../Conventions/Naming/UnionCasesNames.fs | 12 +- .../Naming/UnnestedFunctionNames.fs | 26 +- .../Rules/Conventions/NestedStatements.fs | 52 ++-- .../Rules/Conventions/NoPartialFunctions.fs | 286 +++++++++--------- .../MaxNumberOfBooleanOperatorsInCondition.fs | 94 +++--- .../MaxNumberOfFunctionParameters.fs | 40 +-- .../NumberOfItems/MaxNumberOfItemsInTuple.fs | 50 +-- .../NumberOfItems/MaxNumberOfMembers.fs | 78 ++--- .../Rules/Conventions/RedundantNewKeyword.fs | 57 ++-- .../SourceLength/MaxLinesInFile.fs | 28 +- .../SourceLength/SourceLengthHelper.fs | 62 ++-- .../Rules/Conventions/UnneededRecKeyword.fs | 20 +- .../Rules/Formatting/TypedItemSpacing.fs | 104 +++---- .../Formatting/Typography/NoTabCharacters.fs | 6 +- .../Typography/TrailingWhitespaceOnLine.fs | 46 ++- .../Rules/Hints/HintMatcher.fs | 250 +++++++-------- .../Rules/Hints/HintsHelper.fs | 42 +-- 43 files changed, 1451 insertions(+), 1454 deletions(-) diff --git a/src/FSharpLint.Console/Program.fs b/src/FSharpLint.Console/Program.fs index d62691ed3..df27ae7f5 100644 --- a/src/FSharpLint.Console/Program.fs +++ b/src/FSharpLint.Console/Program.fs @@ -90,16 +90,6 @@ let internal expandWildcard (pattern:string) = else List.empty -let private parserProgress (output:Output.IOutput) = function - | Starting file -> - String.Format(Resources.GetString("ConsoleStartingFile"), file) |> output.WriteInfo - | ReachedEnd (_, warnings) -> - String.Format(Resources.GetString("ConsoleFinishedFile"), List.length warnings) |> output.WriteInfo - | Failed (file, parseException) -> - String.Format(Resources.GetString("ConsoleFailedToParseFile"), file) |> output.WriteError - output.WriteError - $"Exception Message:{Environment.NewLine}{parseException.Message}{Environment.NewLine}Exception Stack Trace:{Environment.NewLine}{parseException.StackTrace}{Environment.NewLine}" - /// Checks if a string contains wildcard characters. let internal containsWildcard (target:string) = target.Contains("*") || target.Contains("?") @@ -117,101 +107,111 @@ let internal inferFileType (target:string) = else FileType.Source -let private lint - (lintArgs: ParseResults) - (output: Output.IOutput) - (toolsPath:Ionide.ProjInfo.Types.ToolsPath) - : ExitCode = - let mutable exitCode = ExitCode.Success - - let handleError (str:string) = - output.WriteError str - exitCode <- ExitCode.Failure - - let handleLintResult = function - | LintResult.Success(warnings) -> - String.Format(Resources.GetString("ConsoleFinished"), List.length warnings) - |> output.WriteInfo - if not (List.isEmpty warnings) then - exitCode <- ExitCode.Failure - | LintResult.Failure(failure) -> - handleError failure.Description - - let lintConfig = lintArgs.TryGetResult Lint_Config - - let configParam = - match lintConfig with - | Some configPath -> FromFile configPath - | None -> Default - - let lintParams = - { - CancellationToken = None - ReceivedWarning = Some output.WriteWarning - Configuration = configParam - ReportLinterProgress = Some (parserProgress output) - } - - let target = lintArgs.GetResult Target - let fileType = lintArgs.TryGetResult File_Type |> Option.defaultValue (inferFileType target) - - try - let lintResult = - match fileType with - | FileType.File -> Lint.asyncLintFile lintParams target |> Async.RunSynchronously - | FileType.Source -> Lint.asyncLintSource lintParams target |> Async.RunSynchronously - | FileType.Solution -> Lint.asyncLintSolution lintParams target toolsPath |> Async.RunSynchronously - | FileType.Wildcard -> - output.WriteInfo "Wildcard detected, but not recommended. Using a project (slnx/sln/fsproj) can detect more issues." - let files = expandWildcard target - if List.isEmpty files then - output.WriteInfo $"No files matching pattern '%s{target}' were found." - LintResult.Success List.empty - else - output.WriteInfo $"Found %d{List.length files} file(s) matching pattern '%s{target}'." - Lint.asyncLintFiles lintParams files |> Async.RunSynchronously - | FileType.Project - | _ -> Lint.asyncLintProject lintParams target toolsPath |> Async.RunSynchronously - handleLintResult lintResult - with - | exn -> - let target = if fileType = FileType.Source then "source" else target - handleError - $"Lint failed while analysing %s{target}.{Environment.NewLine}Failed with: %s{exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}" - - exitCode - -let private start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo.Types.ToolsPath) = - let output = - match arguments.TryGetResult Format with - | Some OutputFormat.MSBuild -> Output.MSBuildOutput() :> Output.IOutput - | Some OutputFormat.Standard - | Some _ - | None -> Output.StandardOutput() :> Output.IOutput - - if arguments.Contains ToolArgs.Version then - let maybeVersion = - Assembly.GetExecutingAssembly().GetCustomAttributes false - |> Seq.tryPick (function | :? AssemblyInformationalVersionAttribute as aiva -> Some aiva.InformationalVersion | _ -> None) - match maybeVersion with - | Some version -> - output.WriteInfo $"Current version: {version}" - Environment.Exit 0 - | None -> - failwith "Error: unable to get version" - - match arguments.GetSubCommand() with - | Lint lintArgs -> - lint lintArgs output toolsPath - | _ -> - ExitCode.Failure - /// Must be called only once per process. /// We're calling it globally so we can call main multiple times from our tests. let toolsPath = Ionide.ProjInfo.Init.init (DirectoryInfo <| Directory.GetCurrentDirectory()) None [] let main argv = + let lint + (lintArgs: ParseResults) + (output: Output.IOutput) + (toolsPath:Ionide.ProjInfo.Types.ToolsPath) + : ExitCode = + let parserProgress (output:Output.IOutput) = function + | Starting file -> + String.Format(Resources.GetString("ConsoleStartingFile"), file) |> output.WriteInfo + | ReachedEnd (_, warnings) -> + String.Format(Resources.GetString("ConsoleFinishedFile"), List.length warnings) |> output.WriteInfo + | Failed (file, parseException) -> + String.Format(Resources.GetString("ConsoleFailedToParseFile"), file) |> output.WriteError + output.WriteError + $"Exception Message:{Environment.NewLine}{parseException.Message}{Environment.NewLine}Exception Stack Trace:{Environment.NewLine}{parseException.StackTrace}{Environment.NewLine}" + + let mutable exitCode = ExitCode.Success + + let handleError (str:string) = + output.WriteError str + exitCode <- ExitCode.Failure + + let handleLintResult = function + | LintResult.Success(warnings) -> + String.Format(Resources.GetString("ConsoleFinished"), List.length warnings) + |> output.WriteInfo + if not (List.isEmpty warnings) then + exitCode <- ExitCode.Failure + | LintResult.Failure(failure) -> + handleError failure.Description + + let lintConfig = lintArgs.TryGetResult Lint_Config + + let configParam = + match lintConfig with + | Some configPath -> FromFile configPath + | None -> Default + + let lintParams = + { + CancellationToken = None + ReceivedWarning = Some output.WriteWarning + Configuration = configParam + ReportLinterProgress = Some (parserProgress output) + } + + let target = lintArgs.GetResult Target + let fileType = lintArgs.TryGetResult File_Type |> Option.defaultValue (inferFileType target) + + try + let lintResult = + match fileType with + | FileType.File -> Lint.asyncLintFile lintParams target |> Async.RunSynchronously + | FileType.Source -> Lint.asyncLintSource lintParams target |> Async.RunSynchronously + | FileType.Solution -> Lint.asyncLintSolution lintParams target toolsPath |> Async.RunSynchronously + | FileType.Wildcard -> + output.WriteInfo "Wildcard detected, but not recommended. Using a project (slnx/sln/fsproj) can detect more issues." + let files = expandWildcard target + if List.isEmpty files then + output.WriteInfo $"No files matching pattern '%s{target}' were found." + LintResult.Success List.empty + else + output.WriteInfo $"Found %d{List.length files} file(s) matching pattern '%s{target}'." + Lint.asyncLintFiles lintParams files |> Async.RunSynchronously + | FileType.Project + | _ -> Lint.asyncLintProject lintParams target toolsPath |> Async.RunSynchronously + handleLintResult lintResult + with + | exn -> + let target = if fileType = FileType.Source then "source" else target + handleError + $"Lint failed while analysing %s{target}.{Environment.NewLine}Failed with: %s{exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}" + + exitCode + + let start (arguments:ParseResults) (toolsPath:Ionide.ProjInfo.Types.ToolsPath) = + let output = + match arguments.TryGetResult Format with + | Some OutputFormat.MSBuild -> Output.MSBuildOutput() :> Output.IOutput + | Some OutputFormat.Standard + | Some _ + | None -> Output.StandardOutput() :> Output.IOutput + + if arguments.Contains ToolArgs.Version then + let maybeVersion = + Assembly.GetExecutingAssembly().GetCustomAttributes false + |> Seq.tryPick (function | :? AssemblyInformationalVersionAttribute as aiva -> Some aiva.InformationalVersion | _ -> None) + match maybeVersion with + | Some version -> + output.WriteInfo $"Current version: {version}" + Environment.Exit 0 + | None -> + failwith "Error: unable to get version" + + match arguments.GetSubCommand() with + | Lint lintArgs -> + lint lintArgs output toolsPath + | _ -> + ExitCode.Failure + let errorHandler = ProcessExiter(colorizer = function | ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red) diff --git a/src/FSharpLint.Core/Application/Configuration.fs b/src/FSharpLint.Core/Application/Configuration.fs index c26188051..cd7adfc3b 100644 --- a/src/FSharpLint.Core/Application/Configuration.fs +++ b/src/FSharpLint.Core/Application/Configuration.fs @@ -439,8 +439,6 @@ with // -let private getOrEmptyList hints = Option.defaultValue Array.empty hints - type HintConfig = { add:string [] option ignore:string [] option @@ -712,19 +710,6 @@ let getGlobalConfig (globalConfig:GlobalConfig option) = Rules.GlobalRuleConfig.numIndentationSpaces = globalConfig.numIndentationSpaces |> Option.defaultValue Rules.GlobalRuleConfig.Default.numIndentationSpaces }) |> Option.defaultValue Rules.GlobalRuleConfig.Default -let private parseHints (hints:string []) = - let parseHint hint = - match FParsec.CharParsers.run HintParser.phint hint with - | FParsec.CharParsers.Success(hint, _, _) -> hint - | FParsec.CharParsers.Failure(error, _, _) -> - raise <| ConfigurationException $"Failed to parse hint: {hint}{Environment.NewLine}{error}" - - hints - |> Array.filter (System.String.IsNullOrWhiteSpace >> not) - |> Array.map parseHint - |> Array.toList - |> MergeSyntaxTrees.mergeHints - let findDeprecation config deprecatedAllRules allRules = if config.NonPublicValuesNames.IsSome && (config.PrivateValuesNames.IsSome || config.InternalValuesNames.IsSome) then @@ -760,6 +745,21 @@ let findDeprecation config deprecatedAllRules allRules = // fsharplint:disable MaxLinesInFunction let flattenConfig (config:Configuration) = + let parseHints (hints:string []) = + let parseHint hint = + match FParsec.CharParsers.run HintParser.phint hint with + | FParsec.CharParsers.Success(hint, _, _) -> hint + | FParsec.CharParsers.Failure(error, _, _) -> + raise <| ConfigurationException $"Failed to parse hint: {hint}{Environment.NewLine}{error}" + + hints + |> Array.filter (System.String.IsNullOrWhiteSpace >> not) + |> Array.map parseHint + |> Array.toList + |> MergeSyntaxTrees.mergeHints + + let getOrEmptyList hints = Option.defaultValue Array.empty hints + let deprecatedAllRules = Array.concat [| diff --git a/src/FSharpLint.Core/Framework/Suppression.fs b/src/FSharpLint.Core/Framework/Suppression.fs index 22fe49dfa..feb5c4e20 100644 --- a/src/FSharpLint.Core/Framework/Suppression.fs +++ b/src/FSharpLint.Core/Framework/Suppression.fs @@ -15,20 +15,20 @@ type SuppressionInfo = /// Specifies the suppressions for an individual line. type LineSuppression = { Line:int; Suppressions:SuppressionInfo list } -/// Extracts rule names from a whitespace separated string of rule names. -let private extractRules (rules:Set) (str:string) = - let (splitOnWhitespace:char[]) = null - let entries = - str.Split(splitOnWhitespace, StringSplitOptions.RemoveEmptyEntries) - |> Seq.map (fun entry -> entry.ToLowerInvariant()) - |> Seq.filter (fun entry -> rules.Contains(entry)) - |> Set.ofSeq - - // If no rules set, then all rules are applied. - if Seq.isEmpty entries then rules else entries - /// Parses a given file to find lines containing rule suppressions. let parseSuppressionInfo (rules:Set) (lines:string list) = + /// Extracts rule names from a whitespace separated string of rule names. + let extractRules (rules:Set) (str:string) = + let (splitOnWhitespace:char[]) = null + let entries = + str.Split(splitOnWhitespace, StringSplitOptions.RemoveEmptyEntries) + |> Seq.map (fun entry -> entry.ToLowerInvariant()) + |> Seq.filter (fun entry -> rules.Contains(entry)) + |> Set.ofSeq + + // If no rules set, then all rules are applied. + if Seq.isEmpty entries then rules else entries + let rules = Set.map (fun (rule: String) -> rule.ToLowerInvariant()) rules let choose lineNum line = diff --git a/src/FSharpLint.Core/Rules/Conventions/CyclomaticComplexity.fs b/src/FSharpLint.Core/Rules/Conventions/CyclomaticComplexity.fs index d99a6f411..d52bdf04d 100644 --- a/src/FSharpLint.Core/Rules/Conventions/CyclomaticComplexity.fs +++ b/src/FSharpLint.Core/Rules/Conventions/CyclomaticComplexity.fs @@ -89,68 +89,68 @@ type private BindingStack(maxComplexity: int) = /// A stack to track the current cyclomatic complexity of a binding scope. let mutable private bindingStackOpt : BindingStack option = None -/// gets the global binding stack -let private getBindingStack (maxComplexity: int) = - match bindingStackOpt with - | Some bs -> bs - | None -> let bs = BindingStack maxComplexity - bindingStackOpt <- Some bs - bs - -/// Determines the number of cases in a match clause. -let private countCasesInMatchClause (clause: SynMatchClause) = - // recursive function to count the number of cases in a pattern. - let rec countCases (pat: SynPat) (count: int) = - let mutable localSoFar = count + 1 - match pat with - | SynPat.Or (lhs, _, _, _) -> - countCases lhs localSoFar - | _ -> localSoFar - // apply countCases to the given clause. - match clause with - | SynMatchClause(pat, _, _, _, _, _) -> countCases pat 0 - /// Boolean operator functions. let private boolFunctions = Set.ofList ["op_BooleanOr"; "op_BooleanAnd"] -/// Returns the number of boolean operators in an expression. -/// If expression is Match, MatchLambda, or MatchBang, the 'when' expressions of the match clauses are examined for boolean operators, if applicable. -let private countBooleanOperators expression = - let rec countOperators count = function - | SynExpr.App(_, _, expr, SynExpr.Ident(ident), _) - | SynExpr.App(_, _, SynExpr.Ident(ident), expr, _) -> - if Set.contains ident.idText boolFunctions then - countOperators (count + 1) expr - else +/// Runner for the rule. +let runner (config:Config) (args:AstNodeRuleParams) : WarningDetails[] = + /// gets the global binding stack + let getBindingStack (maxComplexity: int) = + match bindingStackOpt with + | Some bs -> bs + | None -> let bs = BindingStack maxComplexity + bindingStackOpt <- Some bs + bs + + /// Determines the number of cases in a match clause. + let countCasesInMatchClause (clause: SynMatchClause) = + // recursive function to count the number of cases in a pattern. + let rec countCases (pat: SynPat) (count: int) = + let mutable localSoFar = count + 1 + match pat with + | SynPat.Or (lhs, _, _, _) -> + countCases lhs localSoFar + | _ -> localSoFar + // apply countCases to the given clause. + match clause with + | SynMatchClause(pat, _, _, _, _, _) -> countCases pat 0 + + /// Returns the number of boolean operators in an expression. + /// If expression is Match, MatchLambda, or MatchBang, the 'when' expressions of the match clauses are examined for boolean operators, if applicable. + let countBooleanOperators expression = + let rec countOperators count = function + | SynExpr.App(_, _, expr, SynExpr.Ident(ident), _) + | SynExpr.App(_, _, SynExpr.Ident(ident), expr, _) -> + if Set.contains ident.idText boolFunctions then + countOperators (count + 1) expr + else + countOperators count expr + | SynExpr.App(_, _, expr, expr2, _) -> + let left = countOperators 0 expr + let right = countOperators 0 expr2 + count + left + right + | SynExpr.Paren(expr, _, _, _) -> countOperators count expr - | SynExpr.App(_, _, expr, expr2, _) -> - let left = countOperators 0 expr - let right = countOperators 0 expr2 - count + left + right - | SynExpr.Paren(expr, _, _, _) -> - countOperators count expr - | ExpressionUtilities.Identifier([ ident ], _) -> - if Set.contains ident.idText boolFunctions then - count + 1 - else - count - // in match and match-like expressions, consider the when expressions of any clauses - | SynExpr.MatchBang(_, _, clauses, _, _) - | SynExpr.MatchLambda(_, _, clauses, _, _) - | SynExpr.Match(_, _, clauses, _, _) -> - List.sumBy (fun matchClause -> - match matchClause with - | SynMatchClause(_, whenExprOpt, _, _, _, _) -> - match whenExprOpt with - | Some whenExpr -> countOperators 0 whenExpr - | None -> 0) clauses + | ExpressionUtilities.Identifier([ ident ], _) -> + if Set.contains ident.idText boolFunctions then + count + 1 + else + count + // in match and match-like expressions, consider the when expressions of any clauses + | SynExpr.MatchBang(_, _, clauses, _, _) + | SynExpr.MatchLambda(_, _, clauses, _, _) + | SynExpr.Match(_, _, clauses, _, _) -> + List.sumBy (fun matchClause -> + match matchClause with + | SynMatchClause(_, whenExprOpt, _, _, _, _) -> + match whenExprOpt with + | Some whenExpr -> countOperators 0 whenExpr + | None -> 0) clauses - | _ -> count - // kick off the calculation - countOperators 0 expression + | _ -> count + // kick off the calculation + countOperators 0 expression -/// Runner for the rule. -let runner (config:Config) (args:AstNodeRuleParams) : WarningDetails[] = let bindingStack = getBindingStack config.MaxComplexity let mutable warningDetails = None diff --git a/src/FSharpLint.Core/Rules/Conventions/EnsureTailCallDiagnosticsInRecursiveFunctions.fs b/src/FSharpLint.Core/Rules/Conventions/EnsureTailCallDiagnosticsInRecursiveFunctions.fs index e6c25343a..97c1425b8 100644 --- a/src/FSharpLint.Core/Rules/Conventions/EnsureTailCallDiagnosticsInRecursiveFunctions.fs +++ b/src/FSharpLint.Core/Rules/Conventions/EnsureTailCallDiagnosticsInRecursiveFunctions.fs @@ -8,17 +8,17 @@ open FSharp.Compiler.Syntax open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules -let private emitWarning (func: UnneededRecKeyword.RecursiveFunctionInfo) = - { Range = func.Range - Message = - String.Format( - Resources.GetString "RulesEnsureTailCallDiagnosticsInRecursiveFunctions", - func.Identifier.idText - ) - SuggestedFix = None - TypeChecks = list.Empty } - let runner (args: AstNodeRuleParams) = + let emitWarning (func: UnneededRecKeyword.RecursiveFunctionInfo) = + { Range = func.Range + Message = + String.Format( + Resources.GetString "RulesEnsureTailCallDiagnosticsInRecursiveFunctions", + func.Identifier.idText + ) + SuggestedFix = None + TypeChecks = list.Empty } + match (args.AstNode, args.CheckInfo) with | UnneededRecKeyword.RecursiveFunctions(funcs), Some checkInfo -> let processFunction functionInfo = diff --git a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs index 716402c4e..81a13ec70 100644 --- a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs +++ b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs @@ -55,27 +55,28 @@ let runner (args: AstNodeRuleParams) = else None) + let emitWarningIfNeeded currFunctionIdentifier = + match ExpressionUtilities.getSymbolFromIdent args.CheckInfo (SynExpr.Ident currFunctionIdentifier) with + | Some symbolUse -> + let numberOfOtherFunctionsCurrFunctionIsUsedIn = + otherFunctionBodies + |> Seq.filter (fun funcBody -> + checkInfo.GetUsesOfSymbolInFile symbolUse.Symbol + |> Array.exists (fun usage -> ExpressionUtilities.rangeContainsOtherRange funcBody.Range usage.Range)) + |> Seq.length + if numberOfOtherFunctionsCurrFunctionIsUsedIn = 1 then + Some { + Range = currFunctionIdentifier.idRange + WarningDetails.Message = Resources.GetString "RulesFavourNestedFunctions" + SuggestedFix = None + TypeChecks = List.Empty + } + else + None + | None -> None + privateFunctionIdentifiers - |> Array.choose - (fun currFunctionIdentifier -> - match ExpressionUtilities.getSymbolFromIdent args.CheckInfo (SynExpr.Ident currFunctionIdentifier) with - | Some symbolUse -> - let numberOfOtherFunctionsCurrFunctionIsUsedIn = - otherFunctionBodies - |> Seq.filter (fun funcBody -> - checkInfo.GetUsesOfSymbolInFile symbolUse.Symbol - |> Array.exists (fun usage -> ExpressionUtilities.rangeContainsOtherRange funcBody.Range usage.Range)) - |> Seq.length - if numberOfOtherFunctionsCurrFunctionIsUsedIn = 1 then - Some { - Range = currFunctionIdentifier.idRange - WarningDetails.Message = Resources.GetString "RulesFavourNestedFunctions" - SuggestedFix = None - TypeChecks = List.Empty - } - else - None - | None -> None) + |> Array.choose emitWarningIfNeeded | _ -> Array.empty | _ -> Array.empty diff --git a/src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/CanBeReplacedWithComposition.fs b/src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/CanBeReplacedWithComposition.fs index fbfbb60bc..20c7e7253 100644 --- a/src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/CanBeReplacedWithComposition.fs +++ b/src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/CanBeReplacedWithComposition.fs @@ -8,70 +8,70 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Framework.ExpressionUtilities -let private validateLambdaCannotBeReplacedWithComposition fileContents _ lambda range = - let tryReplaceWithFunctionComposition expression = - let getLastElement = List.rev >> List.tryHead +let runner (args:AstNodeRuleParams) = + let validateLambdaCannotBeReplacedWithComposition fileContents _ lambda range = + let tryReplaceWithFunctionComposition expression = + let getLastElement = List.rev >> List.tryHead - let rec lambdaArgumentIsLastApplicationInFunctionCalls expression (lambdaArgument:Ident) (calledFunctionIdents: List) = - let rec appliedValuesAreConstants appliedValues = - match appliedValues with - | (SynExpr.Const(_)| SynExpr.Null(_))::rest -> appliedValuesAreConstants rest - | [SynExpr.App(_) | SynExpr.Ident(_)] -> true - | _ -> false + let rec lambdaArgumentIsLastApplicationInFunctionCalls expression (lambdaArgument:Ident) (calledFunctionIdents: List) = + let rec appliedValuesAreConstants appliedValues = + match appliedValues with + | (SynExpr.Const(_)| SynExpr.Null(_))::rest -> appliedValuesAreConstants rest + | [SynExpr.App(_) | SynExpr.Ident(_)] -> true + | _ -> false - match AstNode.Expression expression with - | FuncApp(exprs, _) -> - match List.map removeParens exprs with - | (ExpressionUtilities.Identifier(idents, _))::appliedValues - when appliedValuesAreConstants appliedValues -> + match AstNode.Expression expression with + | FuncApp(exprs, _) -> + match List.map removeParens exprs with + | (ExpressionUtilities.Identifier(idents, _))::appliedValues + when appliedValuesAreConstants appliedValues -> - let funcName = String.Join('.', idents) - let funcStringParts = - Seq.append - (Seq.singleton funcName) - (appliedValues - |> Seq.take (appliedValues.Length - 1) - |> Seq.choose (fun value -> ExpressionUtilities.tryFindTextOfRange value.Range fileContents)) - let funcString = String.Join(' ', funcStringParts) + let funcName = String.Join('.', idents) + let funcStringParts = + Seq.append + (Seq.singleton funcName) + (appliedValues + |> Seq.take (appliedValues.Length - 1) + |> Seq.choose (fun value -> ExpressionUtilities.tryFindTextOfRange value.Range fileContents)) + let funcString = String.Join(' ', funcStringParts) - match getLastElement appliedValues with - | Some(SynExpr.Ident(lastArgument)) when calledFunctionIdents.Length > 1 -> - if lastArgument.idText = lambdaArgument.idText then - funcString :: calledFunctionIdents - else - List.Empty - | Some(SynExpr.App(_, false, _, _, _) as nextFunction) -> - lambdaArgumentIsLastApplicationInFunctionCalls - nextFunction - lambdaArgument - (funcString :: calledFunctionIdents) + match getLastElement appliedValues with + | Some(SynExpr.Ident(lastArgument)) when calledFunctionIdents.Length > 1 -> + if lastArgument.idText = lambdaArgument.idText then + funcString :: calledFunctionIdents + else + List.Empty + | Some(SynExpr.App(_, false, _, _, _) as nextFunction) -> + lambdaArgumentIsLastApplicationInFunctionCalls + nextFunction + lambdaArgument + (funcString :: calledFunctionIdents) + | _ -> List.Empty | _ -> List.Empty | _ -> List.Empty - | _ -> List.Empty - match lambda.Arguments with - | [singleParameter] -> - match Helper.FunctionReimplementation.getLambdaParamIdent singleParameter with - | Some paramIdent -> - match lambdaArgumentIsLastApplicationInFunctionCalls expression paramIdent List.Empty with - | [] -> None - | funcStrings -> Some funcStrings - | None -> None - | _ -> None + match lambda.Arguments with + | [singleParameter] -> + match Helper.FunctionReimplementation.getLambdaParamIdent singleParameter with + | Some paramIdent -> + match lambdaArgumentIsLastApplicationInFunctionCalls expression paramIdent List.Empty with + | [] -> None + | funcStrings -> Some funcStrings + | None -> None + | _ -> None - match tryReplaceWithFunctionComposition lambda.Body with - | None -> Array.empty - | Some funcStrings -> - let suggestedFix = - lazy( - Some { FromRange = range; FromText = fileContents; ToText = String.Join(" >> ", funcStrings) }) - Array.singleton - { Range = range - Message = Resources.GetString("RulesCanBeReplacedWithComposition") - SuggestedFix = Some suggestedFix - TypeChecks = List.Empty } + match tryReplaceWithFunctionComposition lambda.Body with + | None -> Array.empty + | Some funcStrings -> + let suggestedFix = + lazy( + Some { FromRange = range; FromText = fileContents; ToText = String.Join(" >> ", funcStrings) }) + Array.singleton + { Range = range + Message = Resources.GetString("RulesCanBeReplacedWithComposition") + SuggestedFix = Some suggestedFix + TypeChecks = List.Empty } -let runner (args:AstNodeRuleParams) = Helper.FunctionReimplementation.checkLambda args (validateLambdaCannotBeReplacedWithComposition args.FileContent) let rule = diff --git a/src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/ReimplementsFunction.fs b/src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/ReimplementsFunction.fs index 60bb0c259..cb717f856 100644 --- a/src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/ReimplementsFunction.fs +++ b/src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/ReimplementsFunction.fs @@ -7,51 +7,51 @@ open FSharpLint.Framework.Suggestion open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules -let private validateLambdaIsNotPointless (text:string) lambda range = - let rec isFunctionPointless expression = function - | Some(parameter:Ident) :: parameters -> - match expression with - | SynExpr.App(_, _, expression, SynExpr.Ident(identifier), _) - when identifier.idText = parameter.idText -> - isFunctionPointless expression parameters - | _ -> None - | None :: _ -> None - | [] -> - match expression with - | ExpressionUtilities.Identifier(ident, _) -> Some(ident) - | _ -> None +let runner (args:AstNodeRuleParams) = + let validateLambdaIsNotPointless (text:string) lambda range = + let rec isFunctionPointless expression = function + | Some(parameter:Ident) :: parameters -> + match expression with + | SynExpr.App(_, _, expression, SynExpr.Ident(identifier), _) + when identifier.idText = parameter.idText -> + isFunctionPointless expression parameters + | _ -> None + | None :: _ -> None + | [] -> + match expression with + | ExpressionUtilities.Identifier(ident, _) -> Some(ident) + | _ -> None - let generateError (identifier:LongIdent) = - let identifier = - identifier - |> List.map (fun ident -> - if PrettyNaming.IsLogicalOpName ident.idText then - PrettyNaming.ConvertValLogicalNameToDisplayNameCore ident.idText |> sprintf "( %s )" - else - ident.idText) - |> String.concat "." + let generateError (identifier:LongIdent) = + let identifier = + identifier + |> List.map (fun ident -> + if PrettyNaming.IsLogicalOpName ident.idText then + PrettyNaming.ConvertValLogicalNameToDisplayNameCore ident.idText |> sprintf "( %s )" + else + ident.idText) + |> String.concat "." - let suggestedFix = lazy( - ExpressionUtilities.tryFindTextOfRange range text - |> Option.map (fun fromText -> { FromText = fromText; FromRange = range; ToText = identifier })) + let suggestedFix = lazy( + ExpressionUtilities.tryFindTextOfRange range text + |> Option.map (fun fromText -> { FromText = fromText; FromRange = range; ToText = identifier })) - { - Range = range - Message = String.Format(Resources.GetString("RulesReimplementsFunction"), identifier) - SuggestedFix = Some suggestedFix - TypeChecks = List.Empty - } + { + Range = range + Message = String.Format(Resources.GetString("RulesReimplementsFunction"), identifier) + SuggestedFix = Some suggestedFix + TypeChecks = List.Empty + } - let argumentsAsIdentifiers = - lambda.Arguments - |> List.map Helper.FunctionReimplementation.getLambdaParamIdent - |> List.rev + let argumentsAsIdentifiers = + lambda.Arguments + |> List.map Helper.FunctionReimplementation.getLambdaParamIdent + |> List.rev - isFunctionPointless lambda.Body argumentsAsIdentifiers - |> Option.map generateError - |> Option.toArray + isFunctionPointless lambda.Body argumentsAsIdentifiers + |> Option.map generateError + |> Option.toArray -let runner (args:AstNodeRuleParams) = Helper.FunctionReimplementation.checkLambda args validateLambdaIsNotPointless let rule = diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/AvoidTooShortNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/AvoidTooShortNames.fs index 7e6111f64..089c84c13 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/AvoidTooShortNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/AvoidTooShortNames.fs @@ -11,96 +11,96 @@ open FSharpLint.Rules.Helper let private isIdentifierTooShort (identifier: string) = identifier.Length < 2 && not (identifier.StartsWith '_') -let private checkIdentifierPart (identifier:Ident) (idText:string) = - let formatError errorName = - String.Format(Resources.GetString errorName, idText) +let runner (args:AstNodeRuleParams) = + let checkIdentifierPart (identifier:Ident) (idText:string) = + let formatError errorName = + String.Format(Resources.GetString errorName, idText) - "RulesAvoidTooShortNamesError" |> formatError |> Array.singleton + "RulesAvoidTooShortNamesError" |> formatError |> Array.singleton -let private checkIdentifier (identifier:Ident) (idText:string) = - if isIdentifierTooShort idText then - checkIdentifierPart identifier idText - |> Array.map (fun message -> - { Range = identifier.idRange - Message = message - SuggestedFix = None - TypeChecks = List.Empty }) - else - Array.empty + let checkIdentifier (identifier:Ident) (idText:string) = + if isIdentifierTooShort idText then + checkIdentifierPart identifier idText + |> Array.map (fun message -> + { Range = identifier.idRange + Message = message + SuggestedFix = None + TypeChecks = List.Empty }) + else + Array.empty -let private getParameterWithBelowMinimumLength (pats: SynPat list): (Ident * string * (unit -> bool) option) array = - let rec loop patArray acc = - match patArray with - | SynPat.Named(SynIdent(ident, _), _, _, _)::tail -> - if isIdentifierTooShort ident.idText then - Array.singleton (ident, ident.idText, None) |> Array.append acc |> loop tail - else - loop tail acc - | SynPat.Paren(pat, _)::tail -> - match pat with - | SynPat.Typed(typedPat, _, _) -> - loop (typedPat::tail) acc - | SynPat.Tuple(_, elementPats, _, _) -> - loop elementPats acc - | _ -> loop (pat::tail) acc - | SynPat.LongIdent(_, _, _, argPats, _, _)::tail -> - match argPats with - | SynArgPats.Pats(pats) -> - loop (List.append pats tail) acc - | _ -> - loop tail acc - | _ -> acc - loop pats Array.empty + let getIdentifiers (args:AstNodeRuleParams) = + let getParameterWithBelowMinimumLength (pats: SynPat list): (Ident * string * (unit -> bool) option) array = + let rec loop patArray acc = + match patArray with + | SynPat.Named(SynIdent(ident, _), _, _, _)::tail -> + if isIdentifierTooShort ident.idText then + Array.singleton (ident, ident.idText, None) |> Array.append acc |> loop tail + else + loop tail acc + | SynPat.Paren(pat, _)::tail -> + match pat with + | SynPat.Typed(typedPat, _, _) -> + loop (typedPat::tail) acc + | SynPat.Tuple(_, elementPats, _, _) -> + loop elementPats acc + | _ -> loop (pat::tail) acc + | SynPat.LongIdent(_, _, _, argPats, _, _)::tail -> + match argPats with + | SynArgPats.Pats(pats) -> + loop (List.append pats tail) acc + | _ -> + loop tail acc + | _ -> acc + loop pats Array.empty -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.Expression(SynExpr.LetOrUseBang(_, _, _, pat, _, _, _, _, _)) -> - getParameterWithBelowMinimumLength [pat] - | AstNode.Expression(SynExpr.Lambda(_, _, lambdaArgs, _, _, _, _)) -> - let lambdaIdent = FunctionReimplementation.getLambdaParamIdent lambdaArgs - match lambdaIdent with - | Some ident -> Array.singleton (ident, ident.idText, None) - | None -> Array.empty - | AstNode.Expression(SynExpr.ForEach(_, _, _, _, pat, _, _, _)) -> - getParameterWithBelowMinimumLength (List.singleton pat) - | AstNode.Expression(SynExpr.For(_, _, identifier, _, _, _, _, _, _)) when isIdentifierTooShort identifier.idText -> - Array.singleton (identifier, identifier.idText, None) - | AstNode.Match(SynMatchClause(namePattern, _, _, _, _, _)) -> - getParameterWithBelowMinimumLength [namePattern] - | AstNode.Binding(SynBinding(_, _, _, _, _, _, _, pattern, _, _, _, _, _)) -> - match pattern with - | SynPat.LongIdent(SynLongIdent(identifiers, _, _),_, _, SynArgPats.Pats(names), _, _) -> - match identifiers with - | head::_ -> - let result: (Ident * string * (unit -> bool) option) array = getParameterWithBelowMinimumLength names - if isIdentifierTooShort head.idText then - Array.append result (Array.singleton (head, head.idText, None)) - else - result + match args.AstNode with + | AstNode.Expression(SynExpr.LetOrUseBang(_, _, _, pat, _, _, _, _, _)) -> + getParameterWithBelowMinimumLength [pat] + | AstNode.Expression(SynExpr.Lambda(_, _, lambdaArgs, _, _, _, _)) -> + let lambdaIdent = FunctionReimplementation.getLambdaParamIdent lambdaArgs + match lambdaIdent with + | Some ident -> Array.singleton (ident, ident.idText, None) + | None -> Array.empty + | AstNode.Expression(SynExpr.ForEach(_, _, _, _, pat, _, _, _)) -> + getParameterWithBelowMinimumLength (List.singleton pat) + | AstNode.Expression(SynExpr.For(_, _, identifier, _, _, _, _, _, _)) when isIdentifierTooShort identifier.idText -> + Array.singleton (identifier, identifier.idText, None) + | AstNode.Match(SynMatchClause(namePattern, _, _, _, _, _)) -> + getParameterWithBelowMinimumLength [namePattern] + | AstNode.Binding(SynBinding(_, _, _, _, _, _, _, pattern, _, _, _, _, _)) -> + match pattern with + | SynPat.LongIdent(SynLongIdent(identifiers, _, _),_, _, SynArgPats.Pats(names), _, _) -> + match identifiers with + | head::_ -> + let result: (Ident * string * (unit -> bool) option) array = getParameterWithBelowMinimumLength names + if isIdentifierTooShort head.idText then + Array.append result (Array.singleton (head, head.idText, None)) + else + result + | _ -> Array.empty + | SynPat.Named(SynIdent(identifier, _), _, _, _) when isIdentifierTooShort identifier.idText -> + Array.singleton (identifier, identifier.idText, None) | _ -> Array.empty - | SynPat.Named(SynIdent(identifier, _), _, _, _) when isIdentifierTooShort identifier.idText -> + | AstNode.Field(SynField(_, _, Some identifier, _, _, _, _, _, _)) when isIdentifierTooShort identifier.idText -> Array.singleton (identifier, identifier.idText, None) - | _ -> Array.empty - | AstNode.Field(SynField(_, _, Some identifier, _, _, _, _, _, _)) when isIdentifierTooShort identifier.idText -> - Array.singleton (identifier, identifier.idText, None) - | AstNode.TypeDefinition(SynTypeDefn(componentInfo, _typeDef, _, _, _, _)) -> - let checkTypes types = - seq { - for SynTyparDecl(_attr, SynTypar(id, _, _), _, _) in types do - if isIdentifierTooShort id.idText then - yield (id, id.idText, None) - } + | AstNode.TypeDefinition(SynTypeDefn(componentInfo, _typeDef, _, _, _, _)) -> + let checkTypes types = + seq { + for SynTyparDecl(_attr, SynTypar(id, _, _), _, _) in types do + if isIdentifierTooShort id.idText then + yield (id, id.idText, None) + } - match componentInfo with - | SynComponentInfo(_attrs, maybeTypes, _, _identifier, _, _, _, _) -> - match maybeTypes with - | Some types -> checkTypes types.TyparDecls |> Array.ofSeq - | None -> Array.empty - | AstNode.Type(SynType.Var(SynTypar(id, _, _), _)) when isIdentifierTooShort id.idText -> - Array.singleton (id, id.idText, None) - | _ -> Array.empty + match componentInfo with + | SynComponentInfo(_attrs, maybeTypes, _, _identifier, _, _, _, _) -> + match maybeTypes with + | Some types -> checkTypes types.TyparDecls |> Array.ofSeq + | None -> Array.empty + | AstNode.Type(SynType.Var(SynTypar(id, _, _), _)) when isIdentifierTooShort id.idText -> + Array.singleton (id, id.idText, None) + | _ -> Array.empty -let runner (args:AstNodeRuleParams) = getIdentifiers args |> Array.collect (fun (identifier, idText, typeCheck) -> let suggestions = checkIdentifier identifier idText diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/EnumCasesNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/EnumCasesNames.fs index ab043c5d0..79f441aad 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/EnumCasesNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/EnumCasesNames.fs @@ -5,13 +5,13 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.EnumCase(SynEnumCase(_, SynIdent(identifier, _), _, _, _, _)) -> - Array.singleton (identifier, identifier.idText, None) - | _ -> Array.empty - let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.EnumCase(SynEnumCase(_, SynIdent(identifier, _), _, _, _, _)) -> + Array.singleton (identifier, identifier.idText, None) + | _ -> Array.empty + { Name = "EnumCasesNames" Identifier = Identifiers.EnumCasesNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/ExceptionNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/ExceptionNames.fs index 8c892152c..f081d01b9 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/ExceptionNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/ExceptionNames.fs @@ -5,15 +5,15 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.ExceptionRepresentation(SynExceptionDefnRepr.SynExceptionDefnRepr(_, unionCase, _, _, _, _)) -> - match unionCase with - | SynUnionCase(_, SynIdent(identifier, _), _, _, _, _, _) -> - Array.singleton (identifier, identifier.idText, None) - | _ -> Array.empty - let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.ExceptionRepresentation(SynExceptionDefnRepr.SynExceptionDefnRepr(_, unionCase, _, _, _, _)) -> + match unionCase with + | SynUnionCase(_, SynIdent(identifier, _), _, _, _, _, _) -> + Array.singleton (identifier, identifier.idText, None) + | _ -> Array.empty + { Name = "ExceptionNames" Identifier = Identifiers.ExceptionNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/GenericTypesNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/GenericTypesNames.fs index 1ee9e79d3..c1182ca11 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/GenericTypesNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/GenericTypesNames.fs @@ -5,25 +5,25 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args: AstNodeRuleParams) = - match args.AstNode with - | AstNode.TypeDefinition(SynTypeDefn(componentInfo, _typeDef, _, _, _, _)) -> - let checkTypes types = - seq { - for SynTyparDecl(_attr, SynTypar(id, _, _), _, _) in types do - yield (id, id.idText, None) - } +let rule config = + let getIdentifiers (args: AstNodeRuleParams) = + match args.AstNode with + | AstNode.TypeDefinition(SynTypeDefn(componentInfo, _typeDef, _, _, _, _)) -> + let checkTypes types = + seq { + for SynTyparDecl(_attr, SynTypar(id, _, _), _, _) in types do + yield (id, id.idText, None) + } - match componentInfo with - | SynComponentInfo(_attrs, maybeTypes, _, _identifier, _, _, _, _) -> - match maybeTypes with - | Some types -> checkTypes types.TyparDecls |> Array.ofSeq - | None -> Array.empty - | AstNode.Type(SynType.Var(SynTypar(id, _, _), _)) -> - Array.singleton (id, id.idText, None) - | _ -> Array.empty + match componentInfo with + | SynComponentInfo(_attrs, maybeTypes, _, _identifier, _, _, _, _) -> + match maybeTypes with + | Some types -> checkTypes types.TyparDecls |> Array.ofSeq + | None -> Array.empty + | AstNode.Type(SynType.Var(SynTypar(id, _, _), _)) -> + Array.singleton (id, id.idText, None) + | _ -> Array.empty -let rule config = { Name = "GenericTypesNames" Identifier = Identifiers.GenericTypesNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/InterfaceNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/InterfaceNames.fs index c1c8c60d8..95f8f9e89 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/InterfaceNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/InterfaceNames.fs @@ -5,29 +5,29 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.TypeDefinition(SynTypeDefn(componentInfo, typeDef, _, _, _, _)) -> - let isNotTypeExtension = - match typeDef with - | SynTypeDefnRepr.ObjectModel(SynTypeDefnKind.Augmentation(_), _, _) -> false - | _ -> true +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.TypeDefinition(SynTypeDefn(componentInfo, typeDef, _, _, _, _)) -> + let isNotTypeExtension = + match typeDef with + | SynTypeDefnRepr.ObjectModel(SynTypeDefnKind.Augmentation(_), _, _) -> false + | _ -> true - if isNotTypeExtension then - match componentInfo with - | SynComponentInfo(attrs, _, _, identifier, _, _, _, _) -> - match List.tryLast identifier with - | Some typeIdentifier -> - if not (isMeasureType attrs) && isInterface typeDef then - Array.singleton (typeIdentifier, typeIdentifier.idText, None) - else - Array.empty - | _ -> Array.empty - else - Array.empty - | _ -> Array.empty + if isNotTypeExtension then + match componentInfo with + | SynComponentInfo(attrs, _, _, identifier, _, _, _, _) -> + match List.tryLast identifier with + | Some typeIdentifier -> + if not (isMeasureType attrs) && isInterface typeDef then + Array.singleton (typeIdentifier, typeIdentifier.idText, None) + else + Array.empty + | _ -> Array.empty + else + Array.empty + | _ -> Array.empty -let rule config = { Name = "InterfaceNames" Identifier = Identifiers.InterfaceNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/InternalValuesNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/InternalValuesNames.fs index eeeaa46e5..d8c84165f 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/InternalValuesNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/InternalValuesNames.fs @@ -6,41 +6,41 @@ open FSharpLint.Framework.AstInfo open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getValueOrFunctionIdents typeChecker accessControlLevel pattern = - let checkNotUnionCase ident = fun () -> - typeChecker - |> Option.map (fun checker -> isNotUnionCase checker ident) - |> Option.defaultValue false +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + let getValueOrFunctionIdents typeChecker accessControlLevel pattern = + let checkNotUnionCase ident = fun () -> + typeChecker + |> Option.map (fun checker -> isNotUnionCase checker ident) + |> Option.defaultValue false + + match pattern with + | SynPat.LongIdent(longIdent, _, _, _, _, _) -> + // If a pattern identifier is made up of more than one part then it's not binding a new value. + let singleIdentifier = List.length longIdent.LongIdent = 1 - match pattern with - | SynPat.LongIdent(longIdent, _, _, _, _, _) -> - // If a pattern identifier is made up of more than one part then it's not binding a new value. - let singleIdentifier = List.length longIdent.LongIdent = 1 + match List.tryLast longIdent.LongIdent with + | Some ident when not (isActivePattern ident) && singleIdentifier -> + let checkNotUnionCase = checkNotUnionCase ident + if accessControlLevel = AccessControlLevel.Internal then + Array.singleton (ident, ident.idText, Some checkNotUnionCase) + else + Array.empty + | None | Some _ -> Array.empty + | _ -> Array.empty - match List.tryLast longIdent.LongIdent with - | Some ident when not (isActivePattern ident) && singleIdentifier -> - let checkNotUnionCase = checkNotUnionCase ident - if accessControlLevel = AccessControlLevel.Internal then - Array.singleton (ident, ident.idText, Some checkNotUnionCase) + match args.AstNode with + | AstNode.Binding(SynBinding(access, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> + if not (isLiteral attributes) then + match identifierTypeFromValData valData with + | Value | Function -> + let accessibility = getAccessControlLevel args.SyntaxArray args.NodeIndex + getPatternIdents accessibility (getValueOrFunctionIdents args.CheckInfo) true pattern + | _ -> Array.empty else Array.empty - | None | Some _ -> Array.empty - | _ -> Array.empty + | _ -> Array.empty -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.Binding(SynBinding(access, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> - if not (isLiteral attributes) then - match identifierTypeFromValData valData with - | Value | Function -> - let accessibility = getAccessControlLevel args.SyntaxArray args.NodeIndex - getPatternIdents accessibility (getValueOrFunctionIdents args.CheckInfo) true pattern - | _ -> Array.empty - else - Array.empty - | _ -> Array.empty - -let rule config = { Name = "InternalValuesNames" Identifier = Identifiers.InternalValuesNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/LiteralNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/LiteralNames.fs index 632847507..f181a3e2a 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/LiteralNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/LiteralNames.fs @@ -5,24 +5,24 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.Binding(SynBinding(_, _, _, _, attributes, _, _, pattern, _, _, _, _, _)) -> - if isLiteral attributes then - let rec getLiteralIdents = function - | SynPat.Named(SynIdent(identifier, _), _, _, _) -> - Array.singleton (identifier, identifier.idText, None) - | SynPat.LongIdent(SynLongIdent([identifier], _, _), _, _, _, _, _) -> - Array.singleton (identifier, identifier.idText, None) - | SynPat.Paren(pat, _) -> getLiteralIdents pat - | _ -> Array.empty +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.Binding(SynBinding(_, _, _, _, attributes, _, _, pattern, _, _, _, _, _)) -> + if isLiteral attributes then + let rec getLiteralIdents = function + | SynPat.Named(SynIdent(identifier, _), _, _, _) -> + Array.singleton (identifier, identifier.idText, None) + | SynPat.LongIdent(SynLongIdent([identifier], _, _), _, _, _, _, _) -> + Array.singleton (identifier, identifier.idText, None) + | SynPat.Paren(pat, _) -> getLiteralIdents pat + | _ -> Array.empty - getLiteralIdents pattern - else - Array.empty - | _ -> Array.empty + getLiteralIdents pattern + else + Array.empty + | _ -> Array.empty -let rule config = { Name = "LiteralNames" Identifier = Identifiers.LiteralNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/MeasureTypeNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/MeasureTypeNames.fs index a743f4dab..a1371b0d7 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/MeasureTypeNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/MeasureTypeNames.fs @@ -5,29 +5,29 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.TypeDefinition(SynTypeDefn(componentInfo, typeDef, _, _, _, _)) -> - let isNotTypeExtension = - match typeDef with - | SynTypeDefnRepr.ObjectModel(SynTypeDefnKind.Augmentation(_), _, _) -> false - | _ -> true +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.TypeDefinition(SynTypeDefn(componentInfo, typeDef, _, _, _, _)) -> + let isNotTypeExtension = + match typeDef with + | SynTypeDefnRepr.ObjectModel(SynTypeDefnKind.Augmentation(_), _, _) -> false + | _ -> true - if isNotTypeExtension then - match componentInfo with - | SynComponentInfo(attrs, _, _, identifier, _, _, _, _) -> - match List.tryLast identifier with - | Some typeIdentifier -> - if isMeasureType attrs then - Array.singleton (typeIdentifier, typeIdentifier.idText, None) - else - Array.empty - | _ -> Array.empty - else - Array.empty - | _ -> Array.empty + if isNotTypeExtension then + match componentInfo with + | SynComponentInfo(attrs, _, _, identifier, _, _, _, _) -> + match List.tryLast identifier with + | Some typeIdentifier -> + if isMeasureType attrs then + Array.singleton (typeIdentifier, typeIdentifier.idText, None) + else + Array.empty + | _ -> Array.empty + else + Array.empty + | _ -> Array.empty -let rule config = { Name = "MeasureTypeNames" Identifier = Identifiers.MeasureTypeNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/MemberNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/MemberNames.fs index be36e192b..4ad94a88c 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/MemberNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/MemberNames.fs @@ -6,40 +6,40 @@ open FSharpLint.Framework.AstInfo open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getMemberIdents _ = function - | SynPat.LongIdent(longIdent, _, _, _, _, _) -> - match List.tryLast longIdent.LongIdent with - | Some(ident) when ident.idText.StartsWith "op_" -> - // Ignore members prefixed with op_, they are a special case used for operator overloading. - Array.empty - | None -> Array.empty - | Some ident -> Array.singleton (ident, ident.idText, None) - | _ -> Array.empty +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + let getMemberIdents _ = function + | SynPat.LongIdent(longIdent, _, _, _, _, _) -> + match List.tryLast longIdent.LongIdent with + | Some(ident) when ident.idText.StartsWith "op_" -> + // Ignore members prefixed with op_, they are a special case used for operator overloading. + Array.empty + | None -> Array.empty + | Some ident -> Array.singleton (ident, ident.idText, None) + | _ -> Array.empty -let private isImplementingInterface parents = - List.exists (function - | AstNode.MemberDefinition (SynMemberDefn.Interface _) -> true - | _ -> false) parents + let isImplementingInterface parents = + List.exists (function + | AstNode.MemberDefinition (SynMemberDefn.Interface _) -> true + | _ -> false) parents -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.Binding(SynBinding(_, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> - let parents = args.GetParents 3 - if not (isLiteral attributes) && not (isImplementingInterface parents) then - match identifierTypeFromValData valData with - | Member | Property -> - getPatternIdents AccessControlLevel.Private getMemberIdents true pattern + match args.AstNode with + | AstNode.Binding(SynBinding(_, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> + let parents = args.GetParents 3 + if not (isLiteral attributes) && not (isImplementingInterface parents) then + match identifierTypeFromValData valData with + | Member | Property -> + getPatternIdents AccessControlLevel.Private getMemberIdents true pattern + | _ -> Array.empty + else + Array.empty + | AstNode.MemberDefinition(memberDef) -> + match memberDef with + | SynMemberDefn.AbstractSlot(SynValSig(_, SynIdent(identifier, _), _, _, _, _, _, _, _, _, _, _), _, _, _) -> + Array.singleton (identifier, identifier.idText, None) | _ -> Array.empty - else - Array.empty - | AstNode.MemberDefinition(memberDef) -> - match memberDef with - | SynMemberDefn.AbstractSlot(SynValSig(_, SynIdent(identifier, _), _, _, _, _, _, _, _, _, _, _), _, _, _) -> - Array.singleton (identifier, identifier.idText, None) | _ -> Array.empty - | _ -> Array.empty -let rule config = { Name = "MemberNames" Identifier = Identifiers.MemberNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/ModuleNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/ModuleNames.fs index 116e625d1..1004d587f 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/ModuleNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/ModuleNames.fs @@ -5,18 +5,18 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.ModuleOrNamespace(SynModuleOrNamespace.SynModuleOrNamespace(identifier, _, moduleKind, _, _, _, _, _, _) as synModule) -> - if not (isImplicitModule synModule) && isModule moduleKind then - identifier - |> List.map (fun identifier -> (identifier, identifier.idText, None)) - |> List.toArray - else - Array.empty - | _ -> Array.empty - let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.ModuleOrNamespace(SynModuleOrNamespace.SynModuleOrNamespace(identifier, _, moduleKind, _, _, _, _, _, _) as synModule) -> + if not (isImplicitModule synModule) && isModule moduleKind then + identifier + |> List.map (fun identifier -> (identifier, identifier.idText, None)) + |> List.toArray + else + Array.empty + | _ -> Array.empty + { Name = "ModuleNames" Identifier = Identifiers.ModuleNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/NamespaceNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/NamespaceNames.fs index 98b0faf73..b7e620332 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/NamespaceNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/NamespaceNames.fs @@ -5,18 +5,18 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.ModuleOrNamespace(SynModuleOrNamespace.SynModuleOrNamespace(identifier, _, moduleKind, _, _, _, _, _, _) as synModule) -> - if not (isImplicitModule synModule) && not (isModule moduleKind) then - identifier - |> List.map (fun identifier -> (identifier, identifier.idText, None)) - |> List.toArray - else - Array.empty - | _ -> Array.empty - let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.ModuleOrNamespace(SynModuleOrNamespace.SynModuleOrNamespace(identifier, _, moduleKind, _, _, _, _, _, _) as synModule) -> + if not (isImplicitModule synModule) && not (isModule moduleKind) then + identifier + |> List.map (fun identifier -> (identifier, identifier.idText, None)) + |> List.toArray + else + Array.empty + | _ -> Array.empty + { Name = "NamespaceNames" Identifier = Identifiers.NamespaceNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/NamingHelper.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/NamingHelper.fs index 8e8fd6f4e..7746f6ae4 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/NamingHelper.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/NamingHelper.fs @@ -114,114 +114,113 @@ let isAllLowercase (identifier:string) = let isAllUppercase (identifier:string) = Seq.forall (fun char -> Char.IsUpper char || (not <| Char.IsLetter char)) identifier -let private pascalCaseRule (identifier:string) = - if not (isPascalCase identifier) then Some "RulesNamingConventionsPascalCaseError" - else None - -let private camelCaseRule (identifier:string) = - if not (isCamelCase identifier) then Some "RulesNamingConventionsCamelCaseError" - else None - -let private uppercaseRule (identifier:string) = - if not (isAllUppercase identifier) then Some "RulesNamingConventionsUppercaseError" - else None - -let private lowercaseRule (identifier:string) = - if not (isAllLowercase identifier) then Some "RulesNamingConventionsLowercaseError" - else None - - -let private underscoreRule (underscoreMode: NamingUnderscores) (identifier:string) = - let errorKeyToRemoveUnderscores = "RulesNamingConventionsUnderscoreError" - let errorKeyToRemoveLeadingOrTrailingUnderscores = "RulesNamingConventionsNoInfixUnderscoreError" - match underscoreMode with - | NamingUnderscores.AllowPrefix when identifier.TrimStart('_').Contains '_' -> - Some errorKeyToRemoveUnderscores - | NamingUnderscores.None when identifier.Contains '_' -> - Some errorKeyToRemoveUnderscores - | NamingUnderscores.AllowInfix when (identifier.StartsWith '_' || identifier.EndsWith '_') -> - Some errorKeyToRemoveLeadingOrTrailingUnderscores - | _ -> - None - -let private prefixRule (prefix:string) (identifier:string) = - if not (identifier.StartsWith prefix) then Some "RulesNamingConventionsPrefixError" - else None - -let private suffixRule (suffix:string) (identifier:string) = - if not (identifier.EndsWith suffix) then Some "RulesNamingConventionsSuffixError" - else None - -let private checkIdentifierPart (config:NamingConfig) (identifier:Ident) (idText:string) = - let formatError errorName = - String.Format(Resources.GetString errorName, idText) - - let formatError2 additional errorName = - String.Format(Resources.GetString errorName, idText, additional) - - let tryAddFix fix message = (message, fix identifier) - - let casingError = - match config.Naming with - | Some NamingCase.PascalCase -> - pascalCaseRule idText - |> Option.map (formatError >> tryAddFix QuickFixes.toPascalCase) - | Some NamingCase.CamelCase -> - camelCaseRule idText - |> Option.map (formatError >> tryAddFix QuickFixes.toCamelCase) - | Some NamingCase.AllLowercase -> - lowercaseRule idText - |> Option.map (formatError >> tryAddFix (QuickFixes.toAllLowercase config.Underscores)) - | Some NamingCase.AllUppercase -> - uppercaseRule idText - |> Option.map (formatError >> tryAddFix (QuickFixes.toAllUppercase config.Underscores)) - | _ -> None - - let underscoresError = - match config.Underscores with - | Some (NamingUnderscores.None as nuCfg) -> - underscoreRule nuCfg idText - |> Option.map (formatError >> tryAddFix QuickFixes.removeAllUnderscores) - | Some (NamingUnderscores.AllowPrefix as nuCfg) -> - underscoreRule nuCfg idText - |> Option.map (formatError >> tryAddFix QuickFixes.removeNonPrefixingUnderscores) - | Some (NamingUnderscores.AllowInfix as nuCfg) -> - underscoreRule nuCfg idText - |> Option.map (formatError >> tryAddFix QuickFixes.removePrefixingAndSuffixingUnderscores) - | _ -> None - - let prefixError = - Option.bind (fun prefix -> - prefixRule prefix idText - |> Option.map (formatError2 prefix >> tryAddFix (QuickFixes.addPrefix prefix))) config.Prefix - - let suffixError = - Option.bind (fun suffix -> - suffixRule suffix idText - |> Option.map (formatError2 suffix >> tryAddFix (QuickFixes.addSuffix suffix))) config.Suffix - - Array.choose id - [| - casingError - underscoresError - prefixError - suffixError - |] - -let private checkIdentifier (namingConfig:NamingConfig) (identifier:Ident) (idText:string) = - if notOperator idText && isNotDoubleBackTickedIdent identifier then - checkIdentifierPart namingConfig identifier idText - |> Array.map (fun (message, suggestedFix) -> - { - Range = identifier.idRange - Message = message - SuggestedFix = Some suggestedFix - TypeChecks = List.Empty - }) - else - Array.empty - let toAstNodeRule (namingRule:RuleMetadata) = + let checkIdentifierPart (config:NamingConfig) (identifier:Ident) (idText:string) = + let formatError errorName = + String.Format(Resources.GetString errorName, idText) + + let formatError2 additional errorName = + String.Format(Resources.GetString errorName, idText, additional) + + let tryAddFix fix message = (message, fix identifier) + + let pascalCaseRule (identifier:string) = + if not (isPascalCase identifier) then Some "RulesNamingConventionsPascalCaseError" + else None + + let camelCaseRule (identifier:string) = + if not (isCamelCase identifier) then Some "RulesNamingConventionsCamelCaseError" + else None + + let uppercaseRule (identifier:string) = + if not (isAllUppercase identifier) then Some "RulesNamingConventionsUppercaseError" + else None + + let lowercaseRule (identifier:string) = + if not (isAllLowercase identifier) then Some "RulesNamingConventionsLowercaseError" + else None + + let casingError = + match config.Naming with + | Some NamingCase.PascalCase -> + pascalCaseRule idText + |> Option.map (formatError >> tryAddFix QuickFixes.toPascalCase) + | Some NamingCase.CamelCase -> + camelCaseRule idText + |> Option.map (formatError >> tryAddFix QuickFixes.toCamelCase) + | Some NamingCase.AllLowercase -> + lowercaseRule idText + |> Option.map (formatError >> tryAddFix (QuickFixes.toAllLowercase config.Underscores)) + | Some NamingCase.AllUppercase -> + uppercaseRule idText + |> Option.map (formatError >> tryAddFix (QuickFixes.toAllUppercase config.Underscores)) + | _ -> None + + let underscoresError = + let underscoreRule (underscoreMode: NamingUnderscores) (identifier:string) = + let errorKeyToRemoveUnderscores = "RulesNamingConventionsUnderscoreError" + let errorKeyToRemoveLeadingOrTrailingUnderscores = "RulesNamingConventionsNoInfixUnderscoreError" + match underscoreMode with + | NamingUnderscores.AllowPrefix when identifier.TrimStart('_').Contains '_' -> + Some errorKeyToRemoveUnderscores + | NamingUnderscores.None when identifier.Contains '_' -> + Some errorKeyToRemoveUnderscores + | NamingUnderscores.AllowInfix when (identifier.StartsWith '_' || identifier.EndsWith '_') -> + Some errorKeyToRemoveLeadingOrTrailingUnderscores + | _ -> + None + + match config.Underscores with + | Some (NamingUnderscores.None as nuCfg) -> + underscoreRule nuCfg idText + |> Option.map (formatError >> tryAddFix QuickFixes.removeAllUnderscores) + | Some (NamingUnderscores.AllowPrefix as nuCfg) -> + underscoreRule nuCfg idText + |> Option.map (formatError >> tryAddFix QuickFixes.removeNonPrefixingUnderscores) + | Some (NamingUnderscores.AllowInfix as nuCfg) -> + underscoreRule nuCfg idText + |> Option.map (formatError >> tryAddFix QuickFixes.removePrefixingAndSuffixingUnderscores) + | _ -> None + + let prefixRule (prefix:string) (identifier:string) = + if not (identifier.StartsWith prefix) then Some "RulesNamingConventionsPrefixError" + else None + + let suffixRule (suffix:string) (identifier:string) = + if not (identifier.EndsWith suffix) then Some "RulesNamingConventionsSuffixError" + else None + + let prefixError = + Option.bind (fun prefix -> + prefixRule prefix idText + |> Option.map (formatError2 prefix >> tryAddFix (QuickFixes.addPrefix prefix))) config.Prefix + + let suffixError = + Option.bind (fun suffix -> + suffixRule suffix idText + |> Option.map (formatError2 suffix >> tryAddFix (QuickFixes.addSuffix suffix))) config.Suffix + + Array.choose id + [| + casingError + underscoresError + prefixError + suffixError + |] + + let checkIdentifier (namingConfig:NamingConfig) (identifier:Ident) (idText:string) = + if notOperator idText && isNotDoubleBackTickedIdent identifier then + checkIdentifierPart namingConfig identifier idText + |> Array.map (fun (message, suggestedFix) -> + { + Range = identifier.idRange + Message = message + SuggestedFix = Some suggestedFix + TypeChecks = List.Empty + }) + else + Array.empty + let astNodeRunner (args:AstNodeRuleParams) = namingRule.RuleConfig.GetIdentifiersToCheck args |> Array.collect (fun (identifier, idText, typeCheck) -> diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/NestedFunctionNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/NestedFunctionNames.fs index 0b63518ba..e813b47c3 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/NestedFunctionNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/NestedFunctionNames.fs @@ -6,20 +6,20 @@ open FSharpLint.Framework.AstInfo open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args: AstNodeRuleParams) = - match args.AstNode with - | AstNode.Binding (SynBinding (_, _, _, _, _attributes, _, valData, pattern, _, _, _, _, _)) -> - if isNested args args.NodeIndex then - let maxAccessibility = AccessControlLevel.Public - match identifierTypeFromValData valData with - | Function | Member -> - getPatternIdents maxAccessibility (fun _a11y innerPattern -> getFunctionIdents innerPattern) true pattern - | _ -> Array.empty - else - Array.empty - | _ -> Array.empty - let rule config = + let getIdentifiers (args: AstNodeRuleParams) = + match args.AstNode with + | AstNode.Binding (SynBinding (_, _, _, _, _attributes, _, valData, pattern, _, _, _, _, _)) -> + if isNested args args.NodeIndex then + let maxAccessibility = AccessControlLevel.Public + match identifierTypeFromValData valData with + | Function | Member -> + getPatternIdents maxAccessibility (fun _a11y innerPattern -> getFunctionIdents innerPattern) true pattern + | _ -> Array.empty + else + Array.empty + | _ -> Array.empty + { Name = "NestedFunctionNames" Identifier = Identifiers.NestedFunctionNames RuleConfig = diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/ParameterNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/ParameterNames.fs index e34a105ef..e5056058b 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/ParameterNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/ParameterNames.fs @@ -6,54 +6,54 @@ open FSharpLint.Framework.AstInfo open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getMemberIdents _ = function - | SynPat.Named(SynIdent(ident, _), _, _, _) - | SynPat.OptionalVal(ident, _) -> - Array.singleton (ident, ident.idText, None) - | _ -> Array.empty +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + let getMemberIdents _ = function + | SynPat.Named(SynIdent(ident, _), _, _, _) + | SynPat.OptionalVal(ident, _) -> + Array.singleton (ident, ident.idText, None) + | _ -> Array.empty -let private getValueOrFunctionIdents typeChecker _accessibility pattern = - let checkNotUnionCase ident = fun () -> - typeChecker - |> Option.map (fun checker -> isNotUnionCase checker ident) - |> Option.defaultValue true + let getValueOrFunctionIdents typeChecker _accessibility pattern = + let checkNotUnionCase ident = fun () -> + typeChecker + |> Option.map (fun checker -> isNotUnionCase checker ident) + |> Option.defaultValue true - match pattern with - | SynPat.Named(SynIdent(ident, _), _, _, _) - | SynPat.OptionalVal(ident, _) when not (isActivePattern ident) -> - let checkNotUnionCase = checkNotUnionCase ident - Array.singleton (ident, ident.idText, Some checkNotUnionCase) - | SynPat.LongIdent(SynLongIdent([ident], _, _), _, _, SynArgPats.Pats([]), _, _) when not (isActivePattern ident) -> - // Handle constructor parameters that are represented as LongIdent (e.g., PascalCase parameters) - let checkNotUnionCase = checkNotUnionCase ident - Array.singleton (ident, ident.idText, Some checkNotUnionCase) - | _ -> Array.empty + match pattern with + | SynPat.Named(SynIdent(ident, _), _, _, _) + | SynPat.OptionalVal(ident, _) when not (isActivePattern ident) -> + let checkNotUnionCase = checkNotUnionCase ident + Array.singleton (ident, ident.idText, Some checkNotUnionCase) + | SynPat.LongIdent(SynLongIdent([ident], _, _), _, _, SynArgPats.Pats([]), _, _) when not (isActivePattern ident) -> + // Handle constructor parameters that are represented as LongIdent (e.g., PascalCase parameters) + let checkNotUnionCase = checkNotUnionCase ident + Array.singleton (ident, ident.idText, Some checkNotUnionCase) + | _ -> Array.empty -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.MemberDefinition(memberDef) -> - match memberDef with - | SynMemberDefn.ImplicitCtor(_, _, ctorArgs, _, _, _, _) -> - // ctorArgs is a SynPat, not SynSimplePats, so we need to handle it differently - let accessControlLevel = getAccessControlLevel args.SyntaxArray args.NodeIndex - getPatternIdents accessControlLevel (getValueOrFunctionIdents args.CheckInfo) true ctorArgs - | _ -> Array.empty - | AstNode.Binding(SynBinding(access, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> - if not (isLiteral attributes) then - match identifierTypeFromValData valData with - | Function -> + match args.AstNode with + | AstNode.MemberDefinition(memberDef) -> + match memberDef with + | SynMemberDefn.ImplicitCtor(_, _, ctorArgs, _, _, _, _) -> + // ctorArgs is a SynPat, not SynSimplePats, so we need to handle it differently let accessControlLevel = getAccessControlLevel args.SyntaxArray args.NodeIndex - getPatternIdents accessControlLevel (getValueOrFunctionIdents args.CheckInfo) true pattern - | Member | Property -> - getPatternIdents AccessControlLevel.Private getMemberIdents true pattern + getPatternIdents accessControlLevel (getValueOrFunctionIdents args.CheckInfo) true ctorArgs | _ -> Array.empty - else - Array.empty - | AstNode.Expression(SynExpr.ForEach(_, _, _, true, pattern, _, _, _)) -> - getPatternIdents AccessControlLevel.Private (getValueOrFunctionIdents args.CheckInfo) false pattern - | _ -> Array.empty + | AstNode.Binding(SynBinding(access, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> + if not (isLiteral attributes) then + match identifierTypeFromValData valData with + | Function -> + let accessControlLevel = getAccessControlLevel args.SyntaxArray args.NodeIndex + getPatternIdents accessControlLevel (getValueOrFunctionIdents args.CheckInfo) true pattern + | Member | Property -> + getPatternIdents AccessControlLevel.Private getMemberIdents true pattern + | _ -> Array.empty + else + Array.empty + | AstNode.Expression(SynExpr.ForEach(_, _, _, true, pattern, _, _, _)) -> + getPatternIdents AccessControlLevel.Private (getValueOrFunctionIdents args.CheckInfo) false pattern + | _ -> Array.empty -let rule config = { Name = "ParameterNames" Identifier = Identifiers.ParameterNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/PrivateValuesNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/PrivateValuesNames.fs index a871265ea..0421c12a3 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/PrivateValuesNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/PrivateValuesNames.fs @@ -6,52 +6,52 @@ open FSharpLint.Framework.AstInfo open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getValueOrFunctionIdents typeChecker accessibility pattern = - let checkNotUnionCase ident = fun () -> - typeChecker - |> Option.map (fun checker -> isNotUnionCase checker ident) - |> Option.defaultValue false +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + let getValueOrFunctionIdents typeChecker accessibility pattern = + let checkNotUnionCase ident = fun () -> + typeChecker + |> Option.map (fun checker -> isNotUnionCase checker ident) + |> Option.defaultValue false + + match pattern with + | SynPat.LongIdent(longIdent, _, _, _, _, _) -> + // If a pattern identifier is made up of more than one part then it's not binding a new value. + let singleIdentifier = List.length longIdent.LongIdent = 1 - match pattern with - | SynPat.LongIdent(longIdent, _, _, _, _, _) -> - // If a pattern identifier is made up of more than one part then it's not binding a new value. - let singleIdentifier = List.length longIdent.LongIdent = 1 + match List.tryLast longIdent.LongIdent with + | Some ident when not (isActivePattern ident) && singleIdentifier -> + let checkNotUnionCase = checkNotUnionCase ident + if accessibility = AccessControlLevel.Private then + Array.singleton (ident, ident.idText, Some checkNotUnionCase) + else + Array.empty + | None | Some _ -> Array.empty + | _ -> Array.empty - match List.tryLast longIdent.LongIdent with - | Some ident when not (isActivePattern ident) && singleIdentifier -> - let checkNotUnionCase = checkNotUnionCase ident - if accessibility = AccessControlLevel.Private then - Array.singleton (ident, ident.idText, Some checkNotUnionCase) + match args.AstNode with + | AstNode.Expression(SynExpr.ForEach(_, _, _, true, pattern, _, _, _)) -> + getPatternIdents AccessControlLevel.Private (getValueOrFunctionIdents args.CheckInfo) false pattern + | AstNode.Binding(SynBinding(access, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> + if not (isLiteral attributes) then + match identifierTypeFromValData valData with + | Value | Function -> + let accessibility = getAccessControlLevel args.SyntaxArray args.NodeIndex + getPatternIdents accessibility (getValueOrFunctionIdents args.CheckInfo) true pattern + | _ -> Array.empty else Array.empty - | None | Some _ -> Array.empty - | _ -> Array.empty - -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.Expression(SynExpr.ForEach(_, _, _, true, pattern, _, _, _)) -> - getPatternIdents AccessControlLevel.Private (getValueOrFunctionIdents args.CheckInfo) false pattern - | AstNode.Binding(SynBinding(access, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> - if not (isLiteral attributes) then - match identifierTypeFromValData valData with - | Value | Function -> - let accessibility = getAccessControlLevel args.SyntaxArray args.NodeIndex - getPatternIdents accessibility (getValueOrFunctionIdents args.CheckInfo) true pattern - | _ -> Array.empty - else - Array.empty - | AstNode.Expression(SynExpr.For(_, _, identifier, _, _, _, _, _, _)) -> - Array.singleton (identifier, identifier.idText, None) - | AstNode.Match(SynMatchClause(pattern, _, _, _, _, _)) -> - match pattern with - | SynPat.Named(SynIdent(identifier, _), isThis, _, _) when not isThis -> + | AstNode.Expression(SynExpr.For(_, _, identifier, _, _, _, _, _, _)) -> Array.singleton (identifier, identifier.idText, None) - | SynPat.As(_lshPat, rhsPat, _) -> - getPatternIdents AccessControlLevel.Private (getValueOrFunctionIdents args.CheckInfo) false rhsPat + | AstNode.Match(SynMatchClause(pattern, _, _, _, _, _)) -> + match pattern with + | SynPat.Named(SynIdent(identifier, _), isThis, _, _) when not isThis -> + Array.singleton (identifier, identifier.idText, None) + | SynPat.As(_lshPat, rhsPat, _) -> + getPatternIdents AccessControlLevel.Private (getValueOrFunctionIdents args.CheckInfo) false rhsPat + | _ -> Array.empty | _ -> Array.empty - | _ -> Array.empty -let rule config = { Name = "PrivateValuesNames" Identifier = Identifiers.PrivateValuesNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/PublicValuesNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/PublicValuesNames.fs index 4633a8d35..d1e9a70e9 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/PublicValuesNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/PublicValuesNames.fs @@ -6,47 +6,47 @@ open FSharpLint.Framework.AstInfo open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getValueOrFunctionIdents typeChecker accessControlLevel pattern = - let checkNotUnionCase ident = fun () -> - typeChecker - |> Option.map (fun checker -> isNotUnionCase checker ident) - |> Option.defaultValue false +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + let getValueOrFunctionIdents typeChecker accessControlLevel pattern = + let checkNotUnionCase ident = fun () -> + typeChecker + |> Option.map (fun checker -> isNotUnionCase checker ident) + |> Option.defaultValue false + + let isNotActivePattern (ident:Ident) = + ident.idText.StartsWith("|") + |> not - let isNotActivePattern (ident:Ident) = - ident.idText.StartsWith("|") - |> not + match pattern with + | SynPat.LongIdent(longIdent, _, _, _, _, _) -> + // If a pattern identifier is made up of more than one part then it's not binding a new value. + let singleIdentifier = List.length longIdent.LongIdent = 1 - match pattern with - | SynPat.LongIdent(longIdent, _, _, _, _, _) -> - // If a pattern identifier is made up of more than one part then it's not binding a new value. - let singleIdentifier = List.length longIdent.LongIdent = 1 + match List.tryLast longIdent.LongIdent with + | Some ident when singleIdentifier -> + let checkNotUnionCase = checkNotUnionCase ident + if accessControlLevel = AccessControlLevel.Public && isNotActivePattern ident then + Array.singleton (ident, ident.idText, Some checkNotUnionCase) + else + Array.empty + | None | Some _ -> Array.empty + | _ -> Array.empty - match List.tryLast longIdent.LongIdent with - | Some ident when singleIdentifier -> - let checkNotUnionCase = checkNotUnionCase ident - if accessControlLevel = AccessControlLevel.Public && isNotActivePattern ident then - Array.singleton (ident, ident.idText, Some checkNotUnionCase) + match args.AstNode with + | AstNode.Expression(SynExpr.ForEach(_, _, _, true, pattern, _, _, _)) -> + getPatternIdents AccessControlLevel.Private (getValueOrFunctionIdents args.CheckInfo) false pattern + | AstNode.Binding(SynBinding(_, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> + if not (isLiteral attributes || isExtern attributes || isNested args args.NodeIndex) then + match identifierTypeFromValData valData with + | Value | Function -> + let accessibility = getAccessControlLevel args.SyntaxArray args.NodeIndex + getPatternIdents accessibility (getValueOrFunctionIdents args.CheckInfo) true pattern + | _ -> Array.empty else Array.empty - | None | Some _ -> Array.empty - | _ -> Array.empty + | _ -> Array.empty -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.Expression(SynExpr.ForEach(_, _, _, true, pattern, _, _, _)) -> - getPatternIdents AccessControlLevel.Private (getValueOrFunctionIdents args.CheckInfo) false pattern - | AstNode.Binding(SynBinding(_, _, _, _, attributes, _, valData, pattern, _, _, _, _, _)) -> - if not (isLiteral attributes || isExtern attributes || isNested args args.NodeIndex) then - match identifierTypeFromValData valData with - | Value | Function -> - let accessibility = getAccessControlLevel args.SyntaxArray args.NodeIndex - getPatternIdents accessibility (getValueOrFunctionIdents args.CheckInfo) true pattern - | _ -> Array.empty - else - Array.empty - | _ -> Array.empty - -let rule config = { Name = "PublicValuesNames" Identifier = Identifiers.PublicValuesNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/RecordFieldNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/RecordFieldNames.fs index 18fe5876a..0dc01d902 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/RecordFieldNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/RecordFieldNames.fs @@ -5,16 +5,16 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.TypeSimpleRepresentation (SynTypeDefnSimpleRepr.Record (recordFields=recordFields)) -> - recordFields - |> List.choose (fun (SynField (idOpt=idOpt)) -> - Option.map (fun (identifier: Ident) -> (identifier, identifier.idText, None)) idOpt) - |> List.toArray - | _ -> Array.empty - let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.TypeSimpleRepresentation (SynTypeDefnSimpleRepr.Record (recordFields=recordFields)) -> + recordFields + |> List.choose (fun (SynField (idOpt=idOpt)) -> + Option.map (fun (identifier: Ident) -> (identifier, identifier.idText, None)) idOpt) + |> List.toArray + | _ -> Array.empty + { Name = "RecordFieldNames" Identifier = Identifiers.RecordFieldNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/TypeNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/TypeNames.fs index b2499429a..3fa321c00 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/TypeNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/TypeNames.fs @@ -5,31 +5,31 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.TypeDefinition(SynTypeDefn(componentInfo, typeDef, _, _, _, _)) -> - let isNotTypeExtension = - match typeDef with - | SynTypeDefnRepr.ObjectModel(SynTypeDefnKind.Augmentation(_), _, _) -> false - | _ -> true +let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.TypeDefinition(SynTypeDefn(componentInfo, typeDef, _, _, _, _)) -> + let isNotTypeExtension = + match typeDef with + | SynTypeDefnRepr.ObjectModel(SynTypeDefnKind.Augmentation(_), _, _) -> false + | _ -> true - if isNotTypeExtension then - match componentInfo with - | SynComponentInfo(attrs, _, _, identifier, _, _, _, _) -> - match List.tryLast identifier with - | Some _ -> - if not (isMeasureType attrs) && not (isInterface typeDef) then - identifier - |> List.map (fun identifier -> (identifier, identifier.idText, None)) - |> List.toArray - else - Array.empty - | _ -> Array.empty - else - Array.empty - | _ -> Array.empty + if isNotTypeExtension then + match componentInfo with + | SynComponentInfo(attrs, _, _, identifier, _, _, _, _) -> + match List.tryLast identifier with + | Some _ -> + if not (isMeasureType attrs) && not (isInterface typeDef) then + identifier + |> List.map (fun identifier -> (identifier, identifier.idText, None)) + |> List.toArray + else + Array.empty + | _ -> Array.empty + else + Array.empty + | _ -> Array.empty -let rule config = { Name = "TypeNames" Identifier = Identifiers.TypeNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/UnionCasesNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/UnionCasesNames.fs index 15e0328d6..7d986a05e 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/UnionCasesNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/UnionCasesNames.fs @@ -5,13 +5,13 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.UnionCase(SynUnionCase(_, SynIdent(identifier, _), _, _, _, _, _)) -> - Array.singleton (identifier, identifier.idText, None) - | _ -> Array.empty - let rule config = + let getIdentifiers (args:AstNodeRuleParams) = + match args.AstNode with + | AstNode.UnionCase(SynUnionCase(_, SynIdent(identifier, _), _, _, _, _, _)) -> + Array.singleton (identifier, identifier.idText, None) + | _ -> Array.empty + { Name = "UnionCasesNames" Identifier = Identifiers.UnionCasesNames RuleConfig = { NamingRuleConfig.Config = config; GetIdentifiersToCheck = getIdentifiers } } diff --git a/src/FSharpLint.Core/Rules/Conventions/Naming/UnnestedFunctionNames.fs b/src/FSharpLint.Core/Rules/Conventions/Naming/UnnestedFunctionNames.fs index 7abf678ae..dc9196fe3 100644 --- a/src/FSharpLint.Core/Rules/Conventions/Naming/UnnestedFunctionNames.fs +++ b/src/FSharpLint.Core/Rules/Conventions/Naming/UnnestedFunctionNames.fs @@ -6,20 +6,20 @@ open FSharpLint.Framework.AstInfo open FSharpLint.Framework.Rules open FSharpLint.Rules.Helper.Naming -let private getIdentifiers (args: AstNodeRuleParams) = - match args.AstNode with - | AstNode.Binding (SynBinding (_, _, _, _, _attributes, _, valData, pattern, _, _, _, _,_)) -> - if isNested args args.NodeIndex then - Array.empty - else - let maxAccessibility = AccessControlLevel.Public - match identifierTypeFromValData valData with - | Function | Member -> - getPatternIdents maxAccessibility (fun _a11y innerPattern -> getFunctionIdents innerPattern) true pattern - | _ -> Array.empty - | _ -> Array.empty - let rule config = + let getIdentifiers (args: AstNodeRuleParams) = + match args.AstNode with + | AstNode.Binding (SynBinding (_, _, _, _, _attributes, _, valData, pattern, _, _, _, _,_)) -> + if isNested args args.NodeIndex then + Array.empty + else + let maxAccessibility = AccessControlLevel.Public + match identifierTypeFromValData valData with + | Function | Member -> + getPatternIdents maxAccessibility (fun _a11y innerPattern -> getFunctionIdents innerPattern) true pattern + | _ -> Array.empty + | _ -> Array.empty + { Name = "UnnestedFunctionNames" Identifier = Identifiers.UnnestedFunctionNames RuleConfig = diff --git a/src/FSharpLint.Core/Rules/Conventions/NestedStatements.fs b/src/FSharpLint.Core/Rules/Conventions/NestedStatements.fs index 48a791b7e..b7d73ea20 100644 --- a/src/FSharpLint.Core/Rules/Conventions/NestedStatements.fs +++ b/src/FSharpLint.Core/Rules/Conventions/NestedStatements.fs @@ -10,10 +10,6 @@ open FSharpLint.Framework.Rules [] type Config = { Depth:int } -let private error (depth:int) = - let errorFormatString = Resources.GetString("RulesNestedStatementsError") - String.Format(errorFormatString, depth) - /// Lambda wildcard arguments are named internally as _argN, a match is then generated for them in the AST. /// e.g. fun _ -> () is represented in the AST as fun _arg1 -> match _arg1 with | _ -> (). /// This function returns true if the given match statement is compiler generated for a lambda wildcard argument. @@ -38,28 +34,6 @@ let private areChildrenNested = function | AstNode.Expression(SynExpr.Match(_) as matchExpr) when not (isCompilerGeneratedMatch matchExpr) -> true | _ -> false -let private getRange node = - match node with - | AstNode.Expression(node) -> Some node.Range - | AstNode.Binding(node) -> Some node.RangeOfBindingWithRhs - | _ -> None - -let private distanceToCommonParent (syntaxArray:AbstractSyntaxArray.Node []) iIndex jIndex = - let mutable iIndex = iIndex - let mutable jIndex = jIndex - let mutable distance = 0 - - while iIndex <> jIndex do - if iIndex > jIndex then - iIndex <- syntaxArray.[iIndex].ParentIndex - - if iIndex <> jIndex && areChildrenNested syntaxArray.[iIndex].Actual then - distance <- distance + 1 - else - jIndex <- syntaxArray.[jIndex].ParentIndex - - distance - /// Is node a duplicate of a node in the AST containing ExtraSyntaxInfo /// e.g. lambda arg being a duplicate of the lambda. let isMetaData args node index = @@ -82,6 +56,22 @@ let isElseIf args node index = let mutable depth = 0 let decrementDepthToCommonParent args iIndex jIndex = + let distanceToCommonParent (syntaxArray:AbstractSyntaxArray.Node []) iIndex jIndex = + let mutable iIndex = iIndex + let mutable jIndex = jIndex + let mutable distance = 0 + + while iIndex <> jIndex do + if iIndex > jIndex then + iIndex <- syntaxArray.[iIndex].ParentIndex + + if iIndex <> jIndex && areChildrenNested syntaxArray.[iIndex].Actual then + distance <- distance + 1 + else + jIndex <- syntaxArray.[jIndex].ParentIndex + + distance + if jIndex < args.SyntaxArray.Length then // If next node in array is not a sibling or child of the current node. let parent = args.SyntaxArray.[jIndex].ParentIndex @@ -92,6 +82,16 @@ let decrementDepthToCommonParent args iIndex jIndex = let mutable skipToIndex = None let runner (config:Config) (args:AstNodeRuleParams) = + let error (depth:int) = + let errorFormatString = Resources.GetString("RulesNestedStatementsError") + String.Format(errorFormatString, depth) + + let getRange node = + match node with + | AstNode.Expression(node) -> Some node.Range + | AstNode.Binding(node) -> Some node.RangeOfBindingWithRhs + | _ -> None + let skip = match skipToIndex with | Some skipTo when skipTo = args.NodeIndex -> diff --git a/src/FSharpLint.Core/Rules/Conventions/NoPartialFunctions.fs b/src/FSharpLint.Core/Rules/Conventions/NoPartialFunctions.fs index f09cdc138..10b01de4e 100644 --- a/src/FSharpLint.Core/Rules/Conventions/NoPartialFunctions.fs +++ b/src/FSharpLint.Core/Rules/Conventions/NoPartialFunctions.fs @@ -85,35 +85,6 @@ let private partialInstanceMemberIdentifiers = //("Foo.Bar.Baz", None, "string", PatternMatch) ] -let private checkIfPartialIdentifier (config:Config) (identifier:string) (range:Range) = - if List.contains identifier config.AllowedPartials then - None - elif List.contains identifier config.AdditionalPartials then - Some { - Range = range - Message = String.Format(Resources.GetString ("RulesConventionsNoPartialFunctionsAdditionalError"), identifier) - SuggestedFix = None - TypeChecks = List.Empty - } - else - Map.tryFind identifier partialFunctionIdentifiers - |> Option.filter (fun _ -> not (List.contains identifier config.AllowedPartials)) - |> Option.map (function - | PatternMatch -> - { - Range = range - Message = String.Format(Resources.GetString ("RulesConventionsNoPartialFunctionsPatternMatchError"), identifier) - SuggestedFix = None - TypeChecks = List.Empty - } - | Function replacementFunction -> - { - Range = range - Message = String.Format(Resources.GetString "RulesConventionsNoPartialFunctionsReplacementError", replacementFunction, identifier) - SuggestedFix = Some (lazy ( Some { FromText = identifier; FromRange = range; ToText = replacementFunction })) - TypeChecks = List.Empty - }) - [] let rec private tryFindTypedExpression (range: Range) (expressions: List): Option = match expressions with @@ -215,26 +186,6 @@ let rec private tryFindTypedExpression (range: Range) (expressions: List None -let private getTypedExpressionForRange (checkFile:FSharpCheckFileResults) (range: Range) = - let expressions = - match checkFile.ImplementationFile with - | Some implementationFile -> - let rec getExpressions declarations = - [ - for declaration in declarations do - match declaration with - | FSharpImplementationFileDeclaration.Entity(entity, subDecls) -> - yield! getExpressions subDecls - | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(_,_,body) -> - yield body - | _ -> () - ] - - getExpressions implementationFile.Declarations - | None -> List.empty - - tryFindTypedExpression range expressions - let private matchesBuiltinFSharpType (typeName: string) (fsharpType: FSharpType) : Option = let matchingPartialInstanceMember = List.tryFind (fun (memberName: string, _, _, _) -> memberName.Split('.').[0] = typeName) partialInstanceMemberIdentifiers @@ -248,103 +199,31 @@ let private matchesBuiltinFSharpType (typeName: string) (fsharpType: FSharpType) ) | None -> None -let private isNonStaticInstanceMemberCall (checkFile:FSharpCheckFileResults) names lineText (range: Range) :(Option) = - let typeChecks = - let map (replacement: string * option * string * Replacement) = - match replacement with - | (fullyQualifiedInstanceMember, _, _, replacementStrategy) -> - if not (fullyQualifiedInstanceMember.Contains ".") then - failwith "Please use fully qualified name for the instance member" - let nameSegments = fullyQualifiedInstanceMember.Split '.' - let instanceMemberNameOnly = - Array.tryLast nameSegments - |> Option.defaultWith (fun () -> failwith $"{nameof(nameSegments)} is empty") - let isSourcePropSameAsReplacementProp = List.tryFind (fun sourceInstanceMemberName -> sourceInstanceMemberName = instanceMemberNameOnly) names - match isSourcePropSameAsReplacementProp with - | Some _ -> - let typeName = fullyQualifiedInstanceMember.Substring(0, fullyQualifiedInstanceMember.Length - instanceMemberNameOnly.Length - 1) - - let instanceIdentifier = - String.concat - "." - (List.takeWhile - (fun sourceInstanceMemberName -> sourceInstanceMemberName <> instanceMemberNameOnly) - names) - - let instanceIdentifierSymbol = - let maybeSymbolUse = - checkFile.GetSymbolUseAtLocation( - range.EndLine, - range.EndColumn - ((String.concat "." names).Length - instanceIdentifier.Length), - lineText, - List.singleton instanceIdentifier) - match maybeSymbolUse with - | Some symbolUse -> - match symbolUse.Symbol with - | :? FSharpMemberOrFunctionOrValue as symbol -> Some symbol - | _ -> None - | _ -> None - - match instanceIdentifierSymbol with - | Some identifierSymbol -> - let typeMatches = - let fsharpType = identifierSymbol.FullType - match matchesBuiltinFSharpType typeName fsharpType with - | Some value -> value - | None -> identifierSymbol.FullType.TypeDefinition.FullName = typeName - - if typeMatches then - match replacementStrategy with - | PatternMatch -> - Some - { - Range = range - Message = - String.Format( - Resources.GetString - "RulesConventionsNoPartialFunctionsPatternMatchError", - fullyQualifiedInstanceMember - ) - SuggestedFix = None - TypeChecks = (fun () -> typeMatches) |> List.singleton - } - | Function replacementFunctionName -> - Some - { - Range = range - Message = - String.Format( - Resources.GetString "RulesConventionsNoPartialFunctionsReplacementError", - replacementFunctionName, - fullyQualifiedInstanceMember - ) - SuggestedFix = - Some( - lazy - (Some - { - FromText = (String.concat "." names) - FromRange = range - ToText = replacementFunctionName - }) - ) - TypeChecks = (fun () -> typeMatches) |> List.singleton - } - else - None - | _ -> None - | _ -> None - - List.map map partialInstanceMemberIdentifiers - match List.tryFind(fun (typeCheck:Option) -> typeCheck.IsSome) typeChecks with - | None -> None - | Some instanceMember -> instanceMember - -let private checkMemberCallOnExpression +let checkMemberCallOnExpression (checkFile: FSharpCheckFileResults) (flieContent: string) (range: Range) (originalRange: Range): array = + let getTypedExpressionForRange (checkFile:FSharpCheckFileResults) (range: Range) = + let expressions = + match checkFile.ImplementationFile with + | Some implementationFile -> + let rec getExpressions declarations = + [ + for declaration in declarations do + match declaration with + | FSharpImplementationFileDeclaration.Entity(entity, subDecls) -> + yield! getExpressions subDecls + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(_,_,body) -> + yield body + | _ -> () + ] + + getExpressions implementationFile.Declarations + | None -> List.empty + + tryFindTypedExpression range expressions + match getTypedExpressionForRange checkFile range with | Some expression -> let choose (fullyQualifiedInstanceMember: string) (replacementStrategy: Replacement) = @@ -405,7 +284,128 @@ let private checkMemberCallOnExpression |> List.toArray | None -> Array.empty -let private runner (config:Config) (args:AstNodeRuleParams) = +let runner (config:Config) (args:AstNodeRuleParams) = + let checkIfPartialIdentifier (config:Config) (identifier:string) (range:Range) = + if List.contains identifier config.AllowedPartials then + None + elif List.contains identifier config.AdditionalPartials then + Some { + Range = range + Message = String.Format(Resources.GetString ("RulesConventionsNoPartialFunctionsAdditionalError"), identifier) + SuggestedFix = None + TypeChecks = List.Empty + } + else + Map.tryFind identifier partialFunctionIdentifiers + |> Option.filter (fun _ -> not (List.contains identifier config.AllowedPartials)) + |> Option.map (function + | PatternMatch -> + { + Range = range + Message = String.Format(Resources.GetString ("RulesConventionsNoPartialFunctionsPatternMatchError"), identifier) + SuggestedFix = None + TypeChecks = List.Empty + } + | Function replacementFunction -> + { + Range = range + Message = String.Format(Resources.GetString "RulesConventionsNoPartialFunctionsReplacementError", replacementFunction, identifier) + SuggestedFix = Some (lazy ( Some { FromText = identifier; FromRange = range; ToText = replacementFunction })) + TypeChecks = List.Empty + }) + + let isNonStaticInstanceMemberCall (checkFile:FSharpCheckFileResults) names lineText (range: Range) :(Option) = + let typeChecks = + let map (replacement: string * option * string * Replacement) = + match replacement with + | (fullyQualifiedInstanceMember, _, _, replacementStrategy) -> + if not (fullyQualifiedInstanceMember.Contains ".") then + failwith "Please use fully qualified name for the instance member" + let nameSegments = fullyQualifiedInstanceMember.Split '.' + let instanceMemberNameOnly = + Array.tryLast nameSegments + |> Option.defaultWith (fun () -> failwith $"{nameof(nameSegments)} is empty") + let isSourcePropSameAsReplacementProp = List.tryFind (fun sourceInstanceMemberName -> sourceInstanceMemberName = instanceMemberNameOnly) names + match isSourcePropSameAsReplacementProp with + | Some _ -> + let typeName = fullyQualifiedInstanceMember.Substring(0, fullyQualifiedInstanceMember.Length - instanceMemberNameOnly.Length - 1) + + let instanceIdentifier = + String.concat + "." + (List.takeWhile + (fun sourceInstanceMemberName -> sourceInstanceMemberName <> instanceMemberNameOnly) + names) + + let instanceIdentifierSymbol = + let maybeSymbolUse = + checkFile.GetSymbolUseAtLocation( + range.EndLine, + range.EndColumn - ((String.concat "." names).Length - instanceIdentifier.Length), + lineText, + List.singleton instanceIdentifier) + match maybeSymbolUse with + | Some symbolUse -> + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue as symbol -> Some symbol + | _ -> None + | _ -> None + + match instanceIdentifierSymbol with + | Some identifierSymbol -> + let typeMatches = + let fsharpType = identifierSymbol.FullType + match matchesBuiltinFSharpType typeName fsharpType with + | Some value -> value + | None -> identifierSymbol.FullType.TypeDefinition.FullName = typeName + + if typeMatches then + match replacementStrategy with + | PatternMatch -> + Some + { + Range = range + Message = + String.Format( + Resources.GetString + "RulesConventionsNoPartialFunctionsPatternMatchError", + fullyQualifiedInstanceMember + ) + SuggestedFix = None + TypeChecks = (fun () -> typeMatches) |> List.singleton + } + | Function replacementFunctionName -> + Some + { + Range = range + Message = + String.Format( + Resources.GetString "RulesConventionsNoPartialFunctionsReplacementError", + replacementFunctionName, + fullyQualifiedInstanceMember + ) + SuggestedFix = + Some( + lazy + (Some + { + FromText = (String.concat "." names) + FromRange = range + ToText = replacementFunctionName + }) + ) + TypeChecks = (fun () -> typeMatches) |> List.singleton + } + else + None + | _ -> None + | _ -> None + + List.map map partialInstanceMemberIdentifiers + match List.tryFind(fun (typeCheck:Option) -> typeCheck.IsSome) typeChecks with + | None -> None + | Some instanceMember -> instanceMember + match (args.AstNode, args.CheckInfo) with | (AstNode.Identifier (identifier, range), Some checkInfo) -> let checkPartialIdentifier = diff --git a/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfBooleanOperatorsInCondition.fs b/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfBooleanOperatorsInCondition.fs index fba0c3c51..ca0ec94c8 100644 --- a/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfBooleanOperatorsInCondition.fs +++ b/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfBooleanOperatorsInCondition.fs @@ -10,59 +10,59 @@ open FSharpLint.Framework.Rules let private boolFunctions = Set.ofList ["op_BooleanOr"; "op_BooleanAnd"; "not"] -let private validateCondition (maxBooleanOperators:int) condition = - let rec countBooleanOperators total = function - | SynExpr.App(_, _, expr, SynExpr.Ident(ident), _) - | SynExpr.App(_, _, SynExpr.Ident(ident), expr, _) -> - if Set.contains ident.idText boolFunctions then - countBooleanOperators (total + 1) expr - else - countBooleanOperators total expr - | SynExpr.App(_, _, expr, expr2, _) -> - let left = countBooleanOperators 0 expr - let right = countBooleanOperators 0 expr2 - total + left + right - | SynExpr.Paren(expr, _, _, _) -> - countBooleanOperators total expr - | ExpressionUtilities.Identifier([ ident ], _) -> - if Set.contains ident.idText boolFunctions then - total + 1 - else - total - | _ -> - total +let rule config = + let runner (config:Helper.NumberOfItems.Config) (args:AstNodeRuleParams) = + let validateCondition (maxBooleanOperators:int) condition = + let rec countBooleanOperators total = function + | SynExpr.App(_, _, expr, SynExpr.Ident(ident), _) + | SynExpr.App(_, _, SynExpr.Ident(ident), expr, _) -> + if Set.contains ident.idText boolFunctions then + countBooleanOperators (total + 1) expr + else + countBooleanOperators total expr + | SynExpr.App(_, _, expr, expr2, _) -> + let left = countBooleanOperators 0 expr + let right = countBooleanOperators 0 expr2 + total + left + right + | SynExpr.Paren(expr, _, _, _) -> + countBooleanOperators total expr + | ExpressionUtilities.Identifier([ ident ], _) -> + if Set.contains ident.idText boolFunctions then + total + 1 + else + total + | _ -> + total - let ruleName = "MaxNumberOfBooleanOperatorsInCondition" + let ruleName = "MaxNumberOfBooleanOperatorsInCondition" - let numberOfBooleanOperators = countBooleanOperators 0 condition + let numberOfBooleanOperators = countBooleanOperators 0 condition - if numberOfBooleanOperators > maxBooleanOperators then - let errorFormatString = Resources.GetString("RulesNumberOfItemsBooleanConditionsError") - let error = String.Format(errorFormatString, maxBooleanOperators) - Array.singleton - { - Range = condition.Range - Message = error - SuggestedFix = None - TypeChecks = List.Empty - } - else - Array.empty + if numberOfBooleanOperators > maxBooleanOperators then + let errorFormatString = Resources.GetString("RulesNumberOfItemsBooleanConditionsError") + let error = String.Format(errorFormatString, maxBooleanOperators) + Array.singleton + { + Range = condition.Range + Message = error + SuggestedFix = None + TypeChecks = List.Empty + } + else + Array.empty -let private runner (config:Helper.NumberOfItems.Config) (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.Expression(expression) -> - match expression with - | SynExpr.IfThenElse(condition, _, _, _, _, _, _) - | SynExpr.While(_, condition, _, _) - | SynExpr.Assert(condition, _) -> - validateCondition config.MaxItems condition + match args.AstNode with + | AstNode.Expression(expression) -> + match expression with + | SynExpr.IfThenElse(condition, _, _, _, _, _, _) + | SynExpr.While(_, condition, _, _) + | SynExpr.Assert(condition, _) -> + validateCondition config.MaxItems condition + | _ -> Array.empty + | AstNode.Match(SynMatchClause(_, Some(whenExpr), _, _, _, _)) -> + validateCondition config.MaxItems whenExpr | _ -> Array.empty - | AstNode.Match(SynMatchClause(_, Some(whenExpr), _, _, _, _)) -> - validateCondition config.MaxItems whenExpr - | _ -> Array.empty -let rule config = AstNodeRule { Name = "MaxNumberOfBooleanOperatorsInCondition" diff --git a/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfFunctionParameters.fs b/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfFunctionParameters.fs index 36d792651..713f1851d 100644 --- a/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfFunctionParameters.fs +++ b/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfFunctionParameters.fs @@ -7,28 +7,28 @@ open FSharp.Compiler.Syntax open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules -let private validateFunction (maxParameters:int) (constructorArguments:SynArgPats) = - match constructorArguments with - | SynArgPats.Pats(parameters) - when List.length parameters > maxParameters -> - let errorFormatString = Resources.GetString("RulesNumberOfItemsFunctionError") - let error = String.Format(errorFormatString, maxParameters) - Array.singleton - { - Range = parameters.[maxParameters].Range - Message = error - SuggestedFix = None - TypeChecks = List.Empty - } - | _ -> Array.empty +let rule config = + let runner (config:Helper.NumberOfItems.Config) (args:AstNodeRuleParams) = + let validateFunction (maxParameters:int) (constructorArguments:SynArgPats) = + match constructorArguments with + | SynArgPats.Pats(parameters) + when List.length parameters > maxParameters -> + let errorFormatString = Resources.GetString("RulesNumberOfItemsFunctionError") + let error = String.Format(errorFormatString, maxParameters) + Array.singleton + { + Range = parameters.[maxParameters].Range + Message = error + SuggestedFix = None + TypeChecks = List.Empty + } + | _ -> Array.empty -let private runner (config:Helper.NumberOfItems.Config) (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.Pattern(SynPat.LongIdent(_, _, _, constructorArguments, _, _)) -> - validateFunction config.MaxItems constructorArguments - | _ -> Array.empty + match args.AstNode with + | AstNode.Pattern(SynPat.LongIdent(_, _, _, constructorArguments, _, _)) -> + validateFunction config.MaxItems constructorArguments + | _ -> Array.empty -let rule config = AstNodeRule { Name = "MaxNumberOfFunctionParameters" diff --git a/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfItemsInTuple.fs b/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfItemsInTuple.fs index 39208d1fc..c0fb8ce3a 100644 --- a/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfItemsInTuple.fs +++ b/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfItemsInTuple.fs @@ -7,34 +7,34 @@ open FSharp.Compiler.Syntax open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules -let private isInApplication (syntaxArray:AbstractSyntaxArray.Node[]) index = - let rec isApplicationNode nodeIndex = - if nodeIndex <= 0 then false - else - let node = syntaxArray.[nodeIndex] - match node.Actual with - | AstNode.Expression(SynExpr.Paren(_)) -> isApplicationNode node.ParentIndex - | AstNode.Expression(SynExpr.App(_) | SynExpr.New(_)) -> true - | _ -> false +let runner (config:Helper.NumberOfItems.Config) (args:AstNodeRuleParams) = + let isInApplication (syntaxArray:AbstractSyntaxArray.Node[]) index = + let rec isApplicationNode nodeIndex = + if nodeIndex <= 0 then false + else + let node = syntaxArray.[nodeIndex] + match node.Actual with + | AstNode.Expression(SynExpr.Paren(_)) -> isApplicationNode node.ParentIndex + | AstNode.Expression(SynExpr.App(_) | SynExpr.New(_)) -> true + | _ -> false - if index <= 0 then false - else isApplicationNode syntaxArray.[index].ParentIndex + if index <= 0 then false + else isApplicationNode syntaxArray.[index].ParentIndex -let private validateTuple (maxItems:int) (items:SynExpr list) = - if List.length items > maxItems then - let errorFormatString = Resources.GetString("RulesNumberOfItemsTupleError") - let error = String.Format(errorFormatString, maxItems) - Array.singleton - { - Range = items.[maxItems].Range - Message = error - SuggestedFix = None - TypeChecks = List.Empty - } - else - Array.empty + let validateTuple (maxItems:int) (items:SynExpr list) = + if List.length items > maxItems then + let errorFormatString = Resources.GetString("RulesNumberOfItemsTupleError") + let error = String.Format(errorFormatString, maxItems) + Array.singleton + { + Range = items.[maxItems].Range + Message = error + SuggestedFix = None + TypeChecks = List.Empty + } + else + Array.empty -let runner (config:Helper.NumberOfItems.Config) (args:AstNodeRuleParams) = match args.AstNode with | AstNode.Expression (expression) -> match expression with diff --git a/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfMembers.fs b/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfMembers.fs index 8f855e53d..433c54a25 100644 --- a/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfMembers.fs +++ b/src/FSharpLint.Core/Rules/Conventions/NumberOfItems/MaxNumberOfMembers.fs @@ -7,46 +7,46 @@ open FSharp.Compiler.Syntax open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules -let private getMembers (members:SynMemberDefn list) = - let isPublic = function - | Some(SynAccess.Public(_)) | None -> true - | Some(_) -> false - - let isPublicMember = function - | SynMemberDefn.AbstractSlot(_) -> true - | SynMemberDefn.Member(SynBinding(access, _, _, _, _, _, _, _, _, _, _, _, _), _) - | SynMemberDefn.AutoProperty(_, _, _, _, _, _, _, _, SynValSigAccess.Single (access), _, _, _) -> isPublic access - | SynMemberDefn.AutoProperty(_, _, _, _, _, _, _, _, SynValSigAccess.GetSet (access, _, _), _, _, _) -> isPublic access - | _ -> false - - List.filter isPublicMember members - -let private validateType (maxMembers:int) members typeRepresentation = - let members = - match typeRepresentation with - | SynTypeDefnRepr.Simple(_) | SynTypeDefnRepr.Exception(_) -> members - | SynTypeDefnRepr.ObjectModel(_, members, _) -> getMembers members - - if List.length members > maxMembers then - let errorFormatString = Resources.GetString("RulesNumberOfItemsClassMembersError") - let error = String.Format(errorFormatString, maxMembers) - Array.singleton - { - Range = members.[maxMembers].Range - Message = error - SuggestedFix = None - TypeChecks = List.Empty - } - else - Array.empty - -let private runner (config:Helper.NumberOfItems.Config) (args:AstNodeRuleParams) = - match args.AstNode with - | AstNode.TypeDefinition(SynTypeDefn(_, typeRepresentation, members, implicitCtor, _, _)) -> - validateType config.MaxItems (Option.toList implicitCtor @ members) typeRepresentation - | _ -> Array.empty - let rule config = + let runner (config:Helper.NumberOfItems.Config) (args:AstNodeRuleParams) = + let getMembers (members:SynMemberDefn list) = + let isPublic = function + | Some(SynAccess.Public(_)) | None -> true + | Some(_) -> false + + let isPublicMember = function + | SynMemberDefn.AbstractSlot(_) -> true + | SynMemberDefn.Member(SynBinding(access, _, _, _, _, _, _, _, _, _, _, _, _), _) + | SynMemberDefn.AutoProperty(_, _, _, _, _, _, _, _, SynValSigAccess.Single (access), _, _, _) -> isPublic access + | SynMemberDefn.AutoProperty(_, _, _, _, _, _, _, _, SynValSigAccess.GetSet (access, _, _), _, _, _) -> isPublic access + | _ -> false + + List.filter isPublicMember members + + let validateType (maxMembers:int) members typeRepresentation = + let members = + match typeRepresentation with + | SynTypeDefnRepr.Simple(_) | SynTypeDefnRepr.Exception(_) -> members + | SynTypeDefnRepr.ObjectModel(_, members, _) -> getMembers members + + if List.length members > maxMembers then + let errorFormatString = Resources.GetString("RulesNumberOfItemsClassMembersError") + let error = String.Format(errorFormatString, maxMembers) + Array.singleton + { + Range = members.[maxMembers].Range + Message = error + SuggestedFix = None + TypeChecks = List.Empty + } + else + Array.empty + + match args.AstNode with + | AstNode.TypeDefinition(SynTypeDefn(_, typeRepresentation, members, implicitCtor, _, _)) -> + validateType config.MaxItems (Option.toList implicitCtor @ members) typeRepresentation + | _ -> Array.empty + AstNodeRule { Name = "MaxNumberOfMembers" diff --git a/src/FSharpLint.Core/Rules/Conventions/RedundantNewKeyword.fs b/src/FSharpLint.Core/Rules/Conventions/RedundantNewKeyword.fs index 90d1a3064..3e4c180ab 100644 --- a/src/FSharpLint.Core/Rules/Conventions/RedundantNewKeyword.fs +++ b/src/FSharpLint.Core/Rules/Conventions/RedundantNewKeyword.fs @@ -10,40 +10,39 @@ open FSharp.Compiler.CodeAnalysis open FSharpLint.Framework.Ast open FSharpLint.Framework.Rules -let private implementsIDisposable (fsharpType:FSharpType) = - if fsharpType.HasTypeDefinition then - match fsharpType.TypeDefinition.TryFullName with - | Some(fullName) -> fullName = typeof.FullName - | None -> false - else - false - -let private doesNotImplementIDisposable (checkFile:FSharpCheckFileResults) (ident: SynLongIdent) = - let names = List.map (fun (identifier: Ident) -> identifier.idText) ident.LongIdent - let symbol = checkFile.GetSymbolUseAtLocation(ident.Range.StartLine, ident.Range.EndColumn, String.Empty, names) +let runner args = + let implementsIDisposable (fsharpType:FSharpType) = + if fsharpType.HasTypeDefinition then + match fsharpType.TypeDefinition.TryFullName with + | Some(fullName) -> fullName = typeof.FullName + | None -> false + else + false - match symbol with - | Some(symbol) when (symbol.Symbol :? FSharpMemberOrFunctionOrValue) -> - let ctor = symbol.Symbol :?> FSharpMemberOrFunctionOrValue + let doesNotImplementIDisposable (checkFile:FSharpCheckFileResults) (ident: SynLongIdent) = + let names = List.map (fun (identifier: Ident) -> identifier.idText) ident.LongIdent + let symbol = checkFile.GetSymbolUseAtLocation(ident.Range.StartLine, ident.Range.EndColumn, String.Empty, names) - Option.exists - (fun (ctorForType: FSharpEntity) -> Seq.forall (implementsIDisposable >> not) ctorForType.AllInterfaces) - ctor.DeclaringEntity - | Some symbol when (symbol.Symbol :? FSharpEntity) -> - let ctor = symbol.Symbol :?> FSharpEntity - Seq.forall (implementsIDisposable >> not) ctor.AllInterfaces - | Some _ -> false - | None -> true + match symbol with + | Some(symbol) when (symbol.Symbol :? FSharpMemberOrFunctionOrValue) -> + let ctor = symbol.Symbol :?> FSharpMemberOrFunctionOrValue -let private generateFix (text:string) range = lazy( - ExpressionUtilities.tryFindTextOfRange range text - |> Option.map (fun fromText -> - let withoutLeadingWhitespace = fromText.TrimStart() - let newKeywordRemoved = withoutLeadingWhitespace.Substring(3).TrimStart() - { FromText = fromText; FromRange = range; ToText = newKeywordRemoved })) + Option.exists + (fun (ctorForType: FSharpEntity) -> Seq.forall (implementsIDisposable >> not) ctorForType.AllInterfaces) + ctor.DeclaringEntity + | Some symbol when (symbol.Symbol :? FSharpEntity) -> + let ctor = symbol.Symbol :?> FSharpEntity + Seq.forall (implementsIDisposable >> not) ctor.AllInterfaces + | Some _ -> false + | None -> true + let generateFix (text:string) range = lazy( + ExpressionUtilities.tryFindTextOfRange range text + |> Option.map (fun fromText -> + let withoutLeadingWhitespace = fromText.TrimStart() + let newKeywordRemoved = withoutLeadingWhitespace.Substring(3).TrimStart() + { FromText = fromText; FromRange = range; ToText = newKeywordRemoved })) -let runner args = match (args.AstNode, args.CheckInfo) with | AstNode.Expression(SynExpr.New(_, SynType.LongIdent(identifier), _, range)), Some checkInfo | AstNode.Expression(SynExpr.New(_, SynType.App(SynType.LongIdent(identifier), _, _, _, _, _, _), _, range)), Some checkInfo -> diff --git a/src/FSharpLint.Core/Rules/Conventions/SourceLength/MaxLinesInFile.fs b/src/FSharpLint.Core/Rules/Conventions/SourceLength/MaxLinesInFile.fs index 55b69b462..2b43caa55 100644 --- a/src/FSharpLint.Core/Rules/Conventions/SourceLength/MaxLinesInFile.fs +++ b/src/FSharpLint.Core/Rules/Conventions/SourceLength/MaxLinesInFile.fs @@ -9,21 +9,21 @@ open FSharp.Compiler.Text [] type Config = { MaxLinesInFile:int } -let private checkNumberOfLinesInFile numberOfLines line maxLines = - if numberOfLines > maxLines then - let errorFormatString = Resources.GetString("RulesTypographyFileLengthError") - Array.singleton - { - Range = - Range.mkRange String.Empty (Position.mkPos (maxLines + 1) 0) (Position.mkPos numberOfLines (String.length line)) - Message = String.Format(errorFormatString, (maxLines + 1)) - SuggestedFix = None - TypeChecks = List.Empty - } - else - Array.empty - let checkMaxLinesInFile (config:Config) (args:LineRuleParams) = + let checkNumberOfLinesInFile numberOfLines line maxLines = + if numberOfLines > maxLines then + let errorFormatString = Resources.GetString("RulesTypographyFileLengthError") + Array.singleton + { + Range = + Range.mkRange String.Empty (Position.mkPos (maxLines + 1) 0) (Position.mkPos numberOfLines (String.length line)) + Message = String.Format(errorFormatString, (maxLines + 1)) + SuggestedFix = None + TypeChecks = List.Empty + } + else + Array.empty + if args.IsLastLine then checkNumberOfLinesInFile args.LineNumber args.Line config.MaxLinesInFile else diff --git a/src/FSharpLint.Core/Rules/Conventions/SourceLength/SourceLengthHelper.fs b/src/FSharpLint.Core/Rules/Conventions/SourceLength/SourceLengthHelper.fs index bfa26d6eb..b83b2e1a3 100644 --- a/src/FSharpLint.Core/Rules/Conventions/SourceLength/SourceLengthHelper.fs +++ b/src/FSharpLint.Core/Rules/Conventions/SourceLength/SourceLengthHelper.fs @@ -17,46 +17,46 @@ type private MultilineCommentMarker = | Begin of int | End of int -let private error name lineCount actual = - let errorFormatString = Resources.GetString("RulesSourceLengthError") - String.Format(errorFormatString, name, lineCount, actual) - let private singleLineCommentRegex = Regex(@"^[\s]*\/\/.*$", RegexOptions.Multiline) let private multilineCommentMarkerRegex = Regex @"(\(\*[^\)])|([^\(]\*\))" let private multilineCommentMarkerRegexCaptureGroupLength = 3 -let private stripMultilineComments (source: string) = - let markers = - multilineCommentMarkerRegex.Matches source - |> Seq.map (fun markerMatch -> - let index = markerMatch.Index - if source.[index] = '(' then - Begin index - else - End index) - |> Seq.sortBy (function | Begin index -> index | End index -> index) - |> Seq.toList +let checkSourceLengthRule (config:Config) range fileContents errorName = + let error name lineCount actual = + let errorFormatString = Resources.GetString("RulesSourceLengthError") + String.Format(errorFormatString, name, lineCount, actual) + + let stripMultilineComments (source: string) = + let markers = + multilineCommentMarkerRegex.Matches source + |> Seq.map (fun markerMatch -> + let index = markerMatch.Index + if source.[index] = '(' then + Begin index + else + End index) + |> Seq.sortBy (function | Begin index -> index | End index -> index) + |> Seq.toList - let rec getTopLevelBalancedPairs (toProcess: List) (stack: List) : List = - match toProcess with - | [] -> List.Empty - | Begin(index)::tail -> - getTopLevelBalancedPairs tail (index::stack) - | End(index)::tail -> - match stack with + let rec getTopLevelBalancedPairs (toProcess: List) (stack: List) : List = + match toProcess with | [] -> List.Empty - | [ beginIndex ] -> (beginIndex, index) :: getTopLevelBalancedPairs tail List.Empty - | _::restOfStack -> getTopLevelBalancedPairs tail restOfStack + | Begin(index)::tail -> + getTopLevelBalancedPairs tail (index::stack) + | End(index)::tail -> + match stack with + | [] -> List.Empty + | [ beginIndex ] -> (beginIndex, index) :: getTopLevelBalancedPairs tail List.Empty + | _::restOfStack -> getTopLevelBalancedPairs tail restOfStack - getTopLevelBalancedPairs markers List.Empty - |> List.fold - (fun (currSource: string) (startIndex, endIndex) -> - currSource.Substring(0, startIndex) - + currSource.Substring(endIndex + multilineCommentMarkerRegexCaptureGroupLength)) - source + getTopLevelBalancedPairs markers List.Empty + |> List.fold + (fun (currSource: string) (startIndex, endIndex) -> + currSource.Substring(0, startIndex) + + currSource.Substring(endIndex + multilineCommentMarkerRegexCaptureGroupLength)) + source -let checkSourceLengthRule (config:Config) range fileContents errorName = match tryFindTextOfRange range fileContents with | Some(sourceCode) -> let sourceCode = diff --git a/src/FSharpLint.Core/Rules/Conventions/UnneededRecKeyword.fs b/src/FSharpLint.Core/Rules/Conventions/UnneededRecKeyword.fs index 38a65f7f6..e71f8ccea 100644 --- a/src/FSharpLint.Core/Rules/Conventions/UnneededRecKeyword.fs +++ b/src/FSharpLint.Core/Rules/Conventions/UnneededRecKeyword.fs @@ -45,17 +45,17 @@ let internal functionIsCalledInOneOf (checkInfo: FSharpCheckFileResults) usage.Symbol.DisplayName = calleeName && ExpressionUtilities.rangeContainsOtherRange caller.Body.Range usage.Range)) -let private emitWarning (func: RecursiveFunctionInfo) = - { Range = func.Range - Message = - String.Format( - Resources.GetString "RulesUnneededRecKeyword", - func.Identifier.idText - ) - SuggestedFix = None - TypeChecks = list.Empty } - let runner (args: AstNodeRuleParams) = + let emitWarning (func: RecursiveFunctionInfo) = + { Range = func.Range + Message = + String.Format( + Resources.GetString "RulesUnneededRecKeyword", + func.Identifier.idText + ) + SuggestedFix = None + TypeChecks = list.Empty } + match (args.AstNode, args.CheckInfo) with | RecursiveFunctions(funcs), Some checkInfo -> funcs diff --git a/src/FSharpLint.Core/Rules/Formatting/TypedItemSpacing.fs b/src/FSharpLint.Core/Rules/Formatting/TypedItemSpacing.fs index 683e436a9..2490f4ebd 100644 --- a/src/FSharpLint.Core/Rules/Formatting/TypedItemSpacing.fs +++ b/src/FSharpLint.Core/Rules/Formatting/TypedItemSpacing.fs @@ -16,66 +16,66 @@ type TypedItemStyle = [] type Config = { TypedItemStyle:TypedItemStyle } -let private getLeadingSpaces (text:string) = - let rec loop index = - if index < text.Length && text.[index] = ' ' - then loop (index + 1) - else index +/// Checks for correct spacing around colon of a typed item. +let runner (config:Config) (args:AstNodeRuleParams) = + let getLeadingSpaces (text:string) = + let rec loop index = + if index < text.Length && text.[index] = ' ' + then loop (index + 1) + else index - loop 0 + loop 0 -let private getTrailingSpaces (text:string) = - let rec loop index count = - if index >= 0 && text.[index] = ' ' - then loop (index - 1) (count + 1) - else count + let getTrailingSpaces (text:string) = + let rec loop index count = + if index >= 0 && text.[index] = ' ' + then loop (index - 1) (count + 1) + else count - (loop (text.Length - 1) 0) + (loop (text.Length - 1) 0) -let private expectedSpacesFromConfig (typedItemStyle:TypedItemStyle) = - match typedItemStyle with - | TypedItemStyle.NoSpaces -> (0, 0) - | TypedItemStyle.SpaceAfter -> (0, 1) - | TypedItemStyle.SpacesAround -> (1, 1) - | _ -> (0, 0) + let expectedSpacesFromConfig (typedItemStyle:TypedItemStyle) = + match typedItemStyle with + | TypedItemStyle.NoSpaces -> (0, 0) + | TypedItemStyle.SpaceAfter -> (0, 1) + | TypedItemStyle.SpacesAround -> (1, 1) + | _ -> (0, 0) -/// Checks the provided range, containing a typed item, has valid spacing. -let private checkRange (config:Config) (args:AstNodeRuleParams) (range:Range) = - let (expectedSpacesBefore, expectedSpacesAfter) = - expectedSpacesFromConfig config.TypedItemStyle + /// Checks the provided range, containing a typed item, has valid spacing. + let checkRange (config:Config) (args:AstNodeRuleParams) (range:Range) = + let (expectedSpacesBefore, expectedSpacesAfter) = + expectedSpacesFromConfig config.TypedItemStyle - let bind (text: string) = - match text.Split(':') with - | [|otherText; typeText|] -> - let spacesBeforeColon = getTrailingSpaces otherText - let spacesAfterColon = getLeadingSpaces typeText - if spacesBeforeColon <> expectedSpacesBefore || spacesAfterColon <> expectedSpacesAfter then - let trimmedOtherText = otherText.TrimEnd(' ') - let trimmedTypeText = typeText.TrimStart(' ') - let spacesBeforeString = " " |> String.replicate expectedSpacesBefore - let spacesAfterString = " " |> String.replicate expectedSpacesAfter - let suggestedFix = lazy( - Some { FromRange = range; - FromText = text; - ToText = $"{trimmedOtherText}{spacesBeforeString}:{spacesAfterString}{trimmedTypeText}" } - ) - let errorFormatString = Resources.GetString("RulesFormattingTypedItemSpacingError") - Some - { - Range = range - Message = String.Format(errorFormatString, expectedSpacesBefore, expectedSpacesAfter) - SuggestedFix = Some suggestedFix - TypeChecks = List.Empty - } - else - None - | _ -> None + let bind (text: string) = + match text.Split(':') with + | [|otherText; typeText|] -> + let spacesBeforeColon = getTrailingSpaces otherText + let spacesAfterColon = getLeadingSpaces typeText + if spacesBeforeColon <> expectedSpacesBefore || spacesAfterColon <> expectedSpacesAfter then + let trimmedOtherText = otherText.TrimEnd(' ') + let trimmedTypeText = typeText.TrimStart(' ') + let spacesBeforeString = " " |> String.replicate expectedSpacesBefore + let spacesAfterString = " " |> String.replicate expectedSpacesAfter + let suggestedFix = lazy( + Some { FromRange = range; + FromText = text; + ToText = $"{trimmedOtherText}{spacesBeforeString}:{spacesAfterString}{trimmedTypeText}" } + ) + let errorFormatString = Resources.GetString("RulesFormattingTypedItemSpacingError") + Some + { + Range = range + Message = String.Format(errorFormatString, expectedSpacesBefore, expectedSpacesAfter) + SuggestedFix = Some suggestedFix + TypeChecks = List.Empty + } + else + None + | _ -> None - ExpressionUtilities.tryFindTextOfRange range args.FileContent - |> Option.bind bind + ExpressionUtilities.tryFindTextOfRange range args.FileContent + |> Option.bind bind -/// Checks for correct spacing around colon of a typed item. -let runner (config:Config) (args:AstNodeRuleParams) = match args.AstNode with | AstNode.Pattern (SynPat.Typed (range=range)) | AstNode.Field (SynField (range=range)) -> diff --git a/src/FSharpLint.Core/Rules/Formatting/Typography/NoTabCharacters.fs b/src/FSharpLint.Core/Rules/Formatting/Typography/NoTabCharacters.fs index 4fa34d427..689329bcc 100644 --- a/src/FSharpLint.Core/Rules/Formatting/Typography/NoTabCharacters.fs +++ b/src/FSharpLint.Core/Rules/Formatting/Typography/NoTabCharacters.fs @@ -17,10 +17,10 @@ module ContextBuilder = | _ -> current -let private isInLiteralString literalStrings range = - Seq.exists (fun (_, literalRange) -> ExpressionUtilities.rangeContainsOtherRange literalRange range) literalStrings - let checkNoTabCharacters literalStrings (args:LineRuleParams) = + let isInLiteralString literalStrings range = + Seq.exists (fun (_, literalRange) -> ExpressionUtilities.rangeContainsOtherRange literalRange range) literalStrings + let indexOfTab = args.Line.IndexOf('\t') if indexOfTab >= 0 then diff --git a/src/FSharpLint.Core/Rules/Formatting/Typography/TrailingWhitespaceOnLine.fs b/src/FSharpLint.Core/Rules/Formatting/Typography/TrailingWhitespaceOnLine.fs index ea01f8224..8cbd5b4fb 100644 --- a/src/FSharpLint.Core/Rules/Formatting/Typography/TrailingWhitespaceOnLine.fs +++ b/src/FSharpLint.Core/Rules/Formatting/Typography/TrailingWhitespaceOnLine.fs @@ -12,40 +12,38 @@ type Config = OneSpaceAllowedAfterOperator:bool IgnoreBlankLines:bool } -let private isSymbol character = - let symbols = - [ '>';'<';'+';'-';'*';'=';'~';'%';'&';'|';'@' - '#';'^';'!';'?';'/';'.';':';',';'(';')';'[';']';'{';'}' ] - - List.exists ((=) character) symbols - -let private doesStringNotEndWithWhitespace (config:Config) (str:string) = - match (config.NumberOfSpacesAllowed, config.OneSpaceAllowedAfterOperator) with - | (numberOfSpacesAllowed, _) when numberOfSpacesAllowed > 0 -> - str.Length - str.TrimEnd().Length <= numberOfSpacesAllowed - | (_, isOneSpaceAllowedAfterOperator) when isOneSpaceAllowedAfterOperator -> - let trimmedStr = str.TrimEnd() - - (trimmedStr.Length = str.Length) - || (str.Length - trimmedStr.Length = 1 - && trimmedStr.Length > 0 - && isSymbol trimmedStr.[trimmedStr.Length - 1]) - | _ -> - str.TrimEnd().Length = str.Length - -let private lengthOfWhitespaceOnEnd (str:string) = str.Length - str.TrimEnd().Length - let checkTrailingWhitespaceOnLine (config:Config) (args:LineRuleParams) = let line = args.Line let lineNumber = args.LineNumber let ignoringBlankLinesAndIsBlankLine = config.IgnoreBlankLines && System.String.IsNullOrWhiteSpace(line) + let isSymbol character = + let symbols = + [ '>';'<';'+';'-';'*';'=';'~';'%';'&';'|';'@' + '#';'^';'!';'?';'/';'.';':';',';'(';')';'[';']';'{';'}' ] + + List.exists ((=) character) symbols + + let doesStringNotEndWithWhitespace (config:Config) (str:string) = + match (config.NumberOfSpacesAllowed, config.OneSpaceAllowedAfterOperator) with + | (numberOfSpacesAllowed, _) when numberOfSpacesAllowed > 0 -> + str.Length - str.TrimEnd().Length <= numberOfSpacesAllowed + | (_, isOneSpaceAllowedAfterOperator) when isOneSpaceAllowedAfterOperator -> + let trimmedStr = str.TrimEnd() + + (trimmedStr.Length = str.Length) + || (str.Length - trimmedStr.Length = 1 + && trimmedStr.Length > 0 + && isSymbol trimmedStr.[trimmedStr.Length - 1]) + | _ -> + str.TrimEnd().Length = str.Length + let stringEndsWithWhitespace = not ignoringBlankLinesAndIsBlankLine && not <| doesStringNotEndWithWhitespace config line if stringEndsWithWhitespace then - let whitespaceLength = lengthOfWhitespaceOnEnd line + let whitespaceLength = line.Length - line.TrimEnd().Length let range = Range.mkRange String.Empty (Position.mkPos lineNumber (line.Length - whitespaceLength)) (Position.mkPos lineNumber line.Length) Array.singleton { diff --git a/src/FSharpLint.Core/Rules/Hints/HintMatcher.fs b/src/FSharpLint.Core/Rules/Hints/HintMatcher.fs index 12c5c24d6..806af7b38 100644 --- a/src/FSharpLint.Core/Rules/Hints/HintMatcher.fs +++ b/src/FSharpLint.Core/Rules/Hints/HintMatcher.fs @@ -666,72 +666,6 @@ type HintErrorConfig = ParentAstNode: AstNode option } -let private hintError (config: HintErrorConfig) = - let toStringConfig = - { - ToStringConfig.Replace = false - ToStringConfig.ParentAstNode = None - ToStringConfig.Args = config.Args - ToStringConfig.MatchedVariables = config.MatchedVariables - ToStringConfig.ParentHintNode = None - ToStringConfig.HintNode = config.Hint.MatchedNode - } - - let matched = FormatHint.toString toStringConfig - - match config.Hint.Suggestion with - | Suggestion.Expr(expr) -> - let suggestion = FormatHint.toString { toStringConfig with HintNode = (HintExpr expr) } - let errorFormatString = Resources.GetString("RulesHintRefactor") - let error = System.String.Format(errorFormatString, matched, suggestion) - - let toText = - FormatHint.toString - { - toStringConfig with - Replace = true - ParentAstNode = config.ParentAstNode - HintNode = (HintExpr expr) - } - - let suggestedFix = lazy( - ExpressionUtilities.tryFindTextOfRange config.Range config.Args.FileContent - |> Option.map (fun fromText -> { FromText = fromText; FromRange = config.Range; ToText = toText })) - - { Range = config.Range; Message = error; SuggestedFix = Some suggestedFix; TypeChecks = config.TypeChecks } - | Suggestion.Message(message) -> - let errorFormatString = Resources.GetString("RulesHintSuggestion") - let error = System.String.Format(errorFormatString, matched, message) - { Range = config.Range; Message = error; SuggestedFix = None; TypeChecks = config.TypeChecks } - -let private getMethodParameters (checkFile:FSharpCheckFileResults) (methodIdent: SynLongIdent) = - let symbol = - checkFile.GetSymbolUseAtLocation( - methodIdent.Range.StartLine, - methodIdent.Range.EndColumn, - String.Empty, - List.map (fun (ident: Ident) -> ident.idText) methodIdent.LongIdent) - - match symbol with - | Some(symbol) when (symbol.Symbol :? FSharpMemberOrFunctionOrValue) -> - let symbol = symbol.Symbol :?> FSharpMemberOrFunctionOrValue - - if symbol.IsMember then Seq.tryHead symbol.CurriedParameterGroups - else None - | _ -> None - -/// Check a lambda function can be replaced with a function, -/// it will not be if the lambda is automatically getting -/// converted to a delegate type e.g. Func. -let private canReplaceLambdaWithFunction checkFile methodIdent index = - let parameters = getMethodParameters checkFile methodIdent - - match parameters with - | Some(parameters) when index < Seq.length parameters -> - let parameter = parameters.[index] - not (parameter.Type.HasTypeDefinition && parameter.Type.TypeDefinition.IsDelegate) - | _ -> true - /// Check if lambda can be replaced with an identifier (cannot in the case when is a parameter with the type of a delegate). let private (|RequiresCheck|CanBeReplaced|CannotBeReplaced|) (breadcrumbs, range) = match filterParens breadcrumbs with @@ -752,73 +686,139 @@ let private (|SuggestingReplacementOfLambda|OtherSuggestion|) = function let [] private MaxBreadcrumbs = 6 let private suggestions = ResizeArray() -let private confirmFuzzyMatch (args:AstNodeRuleParams) (hint:HintParserTypes.Hint) = - let breadcrumbs = args.GetParents MaxBreadcrumbs - match (args.AstNode, hint.MatchedNode) with - | AstNode.Expression(SynExpr.Paren(_)), HintExpr(_) - | AstNode.Pattern(SynPat.Paren(_)), HintPat(_) -> () - | AstNode.Pattern(pattern), HintPat(hintPattern) when MatchPattern.matchHintPattern MatchPattern.returnTrue (pattern, hintPattern) -> - hintError - { - TypeChecks = List.Empty - Hint = hint - Args = args - Range = pattern.Range - MatchedVariables = (Dictionary<_, _>()) - ParentAstNode = None - } - |> suggestions.Add - | AstNode.Expression(expr), HintExpr(hintExpr) -> - let arguments = - { MatchExpression.LambdaArguments = Map.empty - MatchExpression.MatchedVariables = Dictionary<_, _>() - MatchExpression.Expression = args.AstNode - MatchExpression.Hint = hintExpr - MatchExpression.FSharpCheckFileResults = args.CheckInfo - MatchExpression.Breadcrumbs = breadcrumbs } - - match MatchExpression.matchHintExpr MatchExpression.returnEmptyMatch arguments with - | MatchExpression.Match(typeChecks) -> - let suggest checks = +let rule config = + /// Searches the abstract syntax array for possible hint matches using the hint trie. + /// Any possible matches that are found will be given to the callback function `notify`, + /// any matches found are not guaranteed and it's expected that the caller verify the match. + let runner (config:Config) (args:AstNodeRuleParams) = + let confirmFuzzyMatch (args:AstNodeRuleParams) (hint:HintParserTypes.Hint) = + let hintError (config: HintErrorConfig) = + let toStringConfig = + { + ToStringConfig.Replace = false + ToStringConfig.ParentAstNode = None + ToStringConfig.Args = config.Args + ToStringConfig.MatchedVariables = config.MatchedVariables + ToStringConfig.ParentHintNode = None + ToStringConfig.HintNode = config.Hint.MatchedNode + } + + let matched = FormatHint.toString toStringConfig + + match config.Hint.Suggestion with + | Suggestion.Expr(expr) -> + let suggestion = FormatHint.toString { toStringConfig with HintNode = (HintExpr expr) } + let errorFormatString = Resources.GetString("RulesHintRefactor") + let error = System.String.Format(errorFormatString, matched, suggestion) + + let toText = + FormatHint.toString + { + toStringConfig with + Replace = true + ParentAstNode = config.ParentAstNode + HintNode = (HintExpr expr) + } + + let suggestedFix = lazy( + ExpressionUtilities.tryFindTextOfRange config.Range config.Args.FileContent + |> Option.map (fun fromText -> { FromText = fromText; FromRange = config.Range; ToText = toText })) + + { Range = config.Range; Message = error; SuggestedFix = Some suggestedFix; TypeChecks = config.TypeChecks } + | Suggestion.Message(message) -> + let errorFormatString = Resources.GetString("RulesHintSuggestion") + let error = System.String.Format(errorFormatString, matched, message) + { Range = config.Range; Message = error; SuggestedFix = None; TypeChecks = config.TypeChecks } + + /// Check a lambda function can be replaced with a function, + /// it will not be if the lambda is automatically getting + /// converted to a delegate type e.g. Func. + let canReplaceLambdaWithFunction checkFile methodIdent index = + let getMethodParameters (checkFile:FSharpCheckFileResults) (methodIdent: SynLongIdent) = + let symbol = + checkFile.GetSymbolUseAtLocation( + methodIdent.Range.StartLine, + methodIdent.Range.EndColumn, + String.Empty, + List.map (fun (ident: Ident) -> ident.idText) methodIdent.LongIdent) + + match symbol with + | Some(symbol) when (symbol.Symbol :? FSharpMemberOrFunctionOrValue) -> + let symbol = symbol.Symbol :?> FSharpMemberOrFunctionOrValue + + if symbol.IsMember then Seq.tryHead symbol.CurriedParameterGroups + else None + | _ -> None + + let parameters = getMethodParameters checkFile methodIdent + + match parameters with + | Some(parameters) when index < Seq.length parameters -> + let parameter = parameters.[index] + not (parameter.Type.HasTypeDefinition && parameter.Type.TypeDefinition.IsDelegate) + | _ -> true + + let breadcrumbs = args.GetParents MaxBreadcrumbs + match (args.AstNode, hint.MatchedNode) with + | AstNode.Expression(SynExpr.Paren(_)), HintExpr(_) + | AstNode.Pattern(SynPat.Paren(_)), HintPat(_) -> () + | AstNode.Pattern(pattern), HintPat(hintPattern) when MatchPattern.matchHintPattern MatchPattern.returnTrue (pattern, hintPattern) -> hintError { - TypeChecks = checks + TypeChecks = List.Empty Hint = hint Args = args - Range = expr.Range - MatchedVariables = arguments.MatchedVariables - ParentAstNode = (List.tryHead breadcrumbs) + Range = pattern.Range + MatchedVariables = (Dictionary<_, _>()) + ParentAstNode = None } |> suggestions.Add + | AstNode.Expression(expr), HintExpr(hintExpr) -> + let arguments = + { MatchExpression.LambdaArguments = Map.empty + MatchExpression.MatchedVariables = Dictionary<_, _>() + MatchExpression.Expression = args.AstNode + MatchExpression.Hint = hintExpr + MatchExpression.FSharpCheckFileResults = args.CheckInfo + MatchExpression.Breadcrumbs = breadcrumbs } + + match MatchExpression.matchHintExpr MatchExpression.returnEmptyMatch arguments with + | MatchExpression.Match(typeChecks) -> + let suggest checks = + hintError + { + TypeChecks = checks + Hint = hint + Args = args + Range = expr.Range + MatchedVariables = arguments.MatchedVariables + ParentAstNode = (List.tryHead breadcrumbs) + } + |> suggestions.Add + + match (hint.MatchedNode, hint.Suggestion) with + | SuggestingReplacementOfLambda -> + match (breadcrumbs, expr.Range) with + | RequiresCheck(index, methodIdent) -> + match args.CheckInfo with + | Some checkFile -> + let typeCheck = fun () -> canReplaceLambdaWithFunction checkFile methodIdent index + suggest (typeCheck ::typeChecks) + | None -> () + | CanBeReplaced -> suggest typeChecks + | CannotBeReplaced -> () + | OtherSuggestion -> suggest typeChecks + | MatchExpression.NoMatch -> () + | _ -> () - match (hint.MatchedNode, hint.Suggestion) with - | SuggestingReplacementOfLambda -> - match (breadcrumbs, expr.Range) with - | RequiresCheck(index, methodIdent) -> - match args.CheckInfo with - | Some checkFile -> - let typeCheck = fun () -> canReplaceLambdaWithFunction checkFile methodIdent index - suggest (typeCheck ::typeChecks) - | None -> () - | CanBeReplaced -> suggest typeChecks - | CannotBeReplaced -> () - | OtherSuggestion -> suggest typeChecks - | MatchExpression.NoMatch -> () - | _ -> () - -/// Searches the abstract syntax array for possible hint matches using the hint trie. -/// Any possible matches that are found will be given to the callback function `notify`, -/// any matches found are not guaranteed and it's expected that the caller verify the match. -let private runner (config:Config) (args:AstNodeRuleParams) = - match config.HintTrie.Lookup.TryGetValue args.NodeHashcode with - | true, trie -> Helper.Hints.checkTrie (args.NodeIndex + 1) trie args.SyntaxArray (Dictionary<_, _>()) (confirmFuzzyMatch args) - | false, _ -> () - - let result = suggestions.ToArray() - suggestions.Clear() - result + match config.HintTrie.Lookup.TryGetValue args.NodeHashcode with + | true, trie -> Helper.Hints.checkTrie (args.NodeIndex + 1) trie args.SyntaxArray (Dictionary<_, _>()) (confirmFuzzyMatch args) + | false, _ -> () + + let result = suggestions.ToArray() + suggestions.Clear() + result -let rule config = AstNodeRule { Name = "Hints" diff --git a/src/FSharpLint.Core/Rules/Hints/HintsHelper.fs b/src/FSharpLint.Core/Rules/Hints/HintsHelper.fs index a37e17fb7..7c2732c85 100644 --- a/src/FSharpLint.Core/Rules/Hints/HintsHelper.fs +++ b/src/FSharpLint.Core/Rules/Hints/HintsHelper.fs @@ -18,31 +18,31 @@ open FSharpLint.Framework.Ast open FSharpLint.Framework.HintParser open MergeSyntaxTrees -/// Confirms if two parts of the ast look alike. -/// This is required as hints can bind variables: the bound location needs to be compared to -/// parts of the ast that the hint covers with the same variable. -let private isMatch iIndex jIndex (nodeArray:AbstractSyntaxArray.Node []) = - let numChildrenI = nodeArray.[iIndex].NumberOfChildren - let numChildrenJ = nodeArray.[jIndex].NumberOfChildren - - if numChildrenI = numChildrenJ then - let numChildren = numChildrenI - - Seq.forall (fun child -> - iIndex + child < nodeArray.Length && - jIndex + child < nodeArray.Length && - nodeArray.[iIndex + child].Hashcode = nodeArray.[jIndex + child].Hashcode) (seq { 0..numChildren }) - else false - -let inline private isParen (node:AbstractSyntaxArray.Node) = - match node.Actual with - | AstNode.Expression(SynExpr.Paren(_)) -> true - | _ -> false - // hard to turn into tail-recursive form // fsharplint:disable EnsureTailCallDiagnosticsInRecursiveFunctions /// Compares the hint trie against a given location in the abstract syntax array. let rec checkTrie index trie (nodeArray:AbstractSyntaxArray.Node []) (boundVariables:Dictionary<_, _>) notify = + /// Confirms if two parts of the ast look alike. + /// This is required as hints can bind variables: the bound location needs to be compared to + /// parts of the ast that the hint covers with the same variable. + let isMatch iIndex jIndex (nodeArray:AbstractSyntaxArray.Node []) = + let numChildrenI = nodeArray.[iIndex].NumberOfChildren + let numChildrenJ = nodeArray.[jIndex].NumberOfChildren + + if numChildrenI = numChildrenJ then + let numChildren = numChildrenI + + Seq.forall (fun child -> + iIndex + child < nodeArray.Length && + jIndex + child < nodeArray.Length && + nodeArray.[iIndex + child].Hashcode = nodeArray.[jIndex + child].Hashcode) (seq { 0..numChildren }) + else false + + let inline isParen (node:AbstractSyntaxArray.Node) = + match node.Actual with + | AstNode.Expression(SynExpr.Paren(_)) -> true + | _ -> false + List.iter notify trie.MatchedHint if index < nodeArray.Length then From 080f64f7f1d4c7af932422e8ec3afcb63f8f7a3f Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Thu, 8 Jan 2026 09:40:27 +0100 Subject: [PATCH 09/10] Tests(Core): new test for FavourNestedFunctions That makes sure that functions with attributes are not triggering the rule. --- .../Rules/Conventions/FavourNestedFunctions.fs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs index eb18b5337..4f84b36b2 100644 --- a/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs +++ b/tests/FSharpLint.Core.Tests/Rules/Conventions/FavourNestedFunctions.fs @@ -120,3 +120,21 @@ let Bar () = """ this.AssertNoWarnings() + + // Using attributes on nested functions (e.g []) will give syntax error: + // Unexpected symbol '[<' in expression + [] + member this.``Top level private function with attributes should not give an error`` () = + this.Parse """ +[] +let rec private Foo x = + if x = 0 then + Foo (x - 1) + else + 0 + +let Bar () = + Foo 3 |> ignore +""" + + this.AssertNoWarnings() From 233b263817808e3472a94091da07178c3c157913 Mon Sep 17 00:00:00 2001 From: webwarrior-ws Date: Thu, 8 Jan 2026 09:48:33 +0100 Subject: [PATCH 10/10] FavourNestedFunctions: ignore functions with attributes Because using attributes on nested functions (e.g []) will give syntax error: ``` Unexpected symbol '[<' in expression ``` --- .../Rules/Conventions/FavourNestedFunctions.fs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs index 81a13ec70..b3f54aa5a 100644 --- a/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs +++ b/src/FSharpLint.Core/Rules/Conventions/FavourNestedFunctions.fs @@ -8,14 +8,6 @@ open FSharpLint.Framework open FSharpLint.Framework.Suggestion let runner (args: AstNodeRuleParams) = - let hasLiteralAttribute (attributes: SynAttributes) = - extractAttributes attributes - |> List.exists - (fun attr -> - match attr.TypeName with - | SynLongIdent([ident], _, _) -> ident.idText = "Literal" - | _ -> false ) - let getFunctionBindings (declaration: SynModuleDecl) = match declaration with | SynModuleDecl.Let(_, bindings, _) -> @@ -24,7 +16,7 @@ let runner (args: AstNodeRuleParams) = (fun binding -> match binding with | SynBinding(_, _, _, _, attributes, _, _, SynPat.LongIdent(SynLongIdent([ident], _, _), _, _, _, accessibility, _), _, expr, _, _, _) - when not(hasLiteralAttribute attributes) -> + when extractAttributes attributes |> List.isEmpty -> Some(ident, expr, accessibility) | _ -> None )