Skip to content

Commit

Permalink
Add new rule C#-FriendlyAsyncOverload
Browse files Browse the repository at this point in the history
  • Loading branch information
su8898 committed Dec 20, 2021
1 parent 67ad63f commit 8b3636b
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 2 deletions.
3 changes: 2 additions & 1 deletion docs/content/how-tos/rule-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ The following rules can be specified for linting.
- [CyclomaticComplexity (FL0071)](rules/FL0071.html)
- [FailwithBadUsage (FL0072)](rules/FL0072.html)
- [FavourReRaise (FL0073)](rules/FL0073.html)
- [FavourConsistentThis (FL0074)](rules/FL0074.html)
- [FavourConsistentThis (FL0074)](rules/FL0074.html)
- [CSharpFriendlyAsyncOverload (FL0075)](rules/FL0075.html)
5 changes: 4 additions & 1 deletion src/FSharpLint.Core/Application/Configuration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,8 @@ type Configuration =
MaxLinesInFile:RuleConfig<MaxLinesInFile.Config> option
TrailingNewLineInFile:EnabledConfig option
NoTabCharacters:EnabledConfig option
NoPartialFunctions:RuleConfig<NoPartialFunctions.Config> option }
NoPartialFunctions:RuleConfig<NoPartialFunctions.Config> option
CSharpFriendlyAsyncOverload:EnabledConfig option }
with
static member Zero = {
Global = None
Expand Down Expand Up @@ -531,6 +532,7 @@ with
TrailingNewLineInFile = None
NoTabCharacters = None
NoPartialFunctions = None
CSharpFriendlyAsyncOverload = None
}

// fsharplint:enable RecordFieldNames
Expand Down Expand Up @@ -677,6 +679,7 @@ let flattenConfig (config:Configuration) =
config.TrailingNewLineInFile |> Option.bind (constructRuleIfEnabled TrailingNewLineInFile.rule)
config.NoTabCharacters |> Option.bind (constructRuleIfEnabled NoTabCharacters.rule)
config.NoPartialFunctions |> Option.bind (constructRuleWithConfig NoPartialFunctions.rule)
config.CSharpFriendlyAsyncOverload |> Option.bind (constructRuleIfEnabled CSharpFriendlyAsyncOverload.rule)
|] |> Array.choose id

if config.NonPublicValuesNames.IsSome &&
Expand Down
1 change: 1 addition & 0 deletions src/FSharpLint.Core/FSharpLint.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<Compile Include="Rules\Conventions\CyclomaticComplexity.fs" />
<Compile Include="Rules\Conventions\FavourReRaise.fs" />
<Compile Include="Rules\Conventions\FavourConsistentThis.fs" />
<Compile Include="Rules\Conventions\CSharpFriendlyAsyncOverload.fs" />
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\RaiseWithTooManyArgumentsHelper.fs" />
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\FailwithWithSingleArgument.fs" />
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\RaiseWithSingleArgument.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module FSharpLint.Rules.CSharpFriendlyAsyncOverload

open FSharpLint.Framework
open FSharpLint.Framework.Suggestion
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FSharpLint.Framework.Ast
open FSharpLint.Framework.Rules
open System

type NodeDetails = { Ident: string; Range: range }

let rec private getIdentFromSynPat =
function
| SynPat.LongIdent (longDotId = longDotId) ->
longDotId
|> ExpressionUtilities.longIdentWithDotsToString
|> Some
| SynPat.Typed (pat, _, _) -> getIdentFromSynPat pat
| _ -> None

let runner (args: AstNodeRuleParams) =
let hasAsync (syntaxArray: AbstractSyntaxArray.Node []) nodeIndex fnIdent =
let rec hasAsync index =
if index >= syntaxArray.Length then
None
else
let node = syntaxArray.[index].Actual
match node with
| AstNode.Binding (SynBinding (_, _, _, _, _attributes, _, _, pattern, _, _, range, _)) ->
match getIdentFromSynPat pattern with
| Some ident when ident = fnIdent + "Async" ->
{ Ident = fnIdent
Range = range } |> Some
| _ -> hasAsync (index + 1)
| _ -> hasAsync (index + 1)

hasAsync nodeIndex

match args.AstNode with
| AstNode.Binding (SynBinding (_, _, _, _, _, _, _, pattern, synInfo, _, range, _)) ->
match synInfo with
| Some (SynBindingReturnInfo ((SynType.App(SynType.LongIdent(LongIdentWithDots(ident,_)),_,_,_,_,_,_)),_,_)) ->
match ident with
| head::_ when head.idText = "Async" ->
let idents = getIdentFromSynPat pattern
match idents with
| Some ident when not (ident.EndsWith "Async") ->
match hasAsync args.SyntaxArray args.NodeIndex ident with
| Some _ -> Array.empty
| None ->
{ Range = range
Message = String.Format(Resources.GetString "RulesCSharpFriendlyAsyncOverload", ident)
SuggestedFix = None
TypeChecks = List.Empty }
|> Array.singleton
| _ -> Array.empty
| _ -> Array.empty
| _ -> Array.empty
| _ -> Array.empty


let rule =
{ Name = "FriendlyAsyncOverload"
Identifier = Identifiers.CSharpFriendlyAsyncOverload
RuleConfig =
{ AstNodeRuleConfig.Runner = runner
Cleanup = ignore } }
|> AstNodeRule
1 change: 1 addition & 0 deletions src/FSharpLint.Core/Rules/Identifiers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ let CyclomaticComplexity = identifier 71
let FailwithBadUsage = identifier 72
let FavourReRaise = identifier 73
let FavourConsistentThis = identifier 74
let CSharpFriendlyAsyncOverload = identifier 75
3 changes: 3 additions & 0 deletions src/FSharpLint.Core/Text.resx
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,7 @@
<data name="RulesFavourConsistentThis" xml:space="preserve">
<value>Prefer using '{0}' consistently.</value>
</data>
<data name="RulesCSharpFriendlyAsyncOverload" xml:space="preserve">
<value>Consider using a C#-friendly async overload for {0}.</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/FSharpLint.Core/fsharplint.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"invalidArgWithTwoArguments": { "enabled": true },
"failwithfWithArgumentsMatchingFormatString": { "enabled": true },
"failwithBadUsage": { "enabled": true },
"friendlyAsyncOverload": { "enabled": false },
"maxLinesInLambdaFunction": {
"enabled": false,
"config": {
Expand Down
1 change: 1 addition & 0 deletions tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<Compile Include="Rules\Conventions\NoPartialFunctions.fs" />
<Compile Include="Rules\Conventions\FavourReRaise.fs" />
<Compile Include="Rules\Conventions\FavourConsistentThis.fs" />
<Compile Include="Rules\Conventions\FriendlyAsyncOverload.fs" />
<Compile Include="Rules\Conventions\Naming\NamingHelpers.fs" />
<Compile Include="Rules\Conventions\Naming\InterfaceNames.fs" />
<Compile Include="Rules\Conventions\Naming\ExceptionNames.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module FSharpLint.Core.Tests.Rules.Conventions.FriendlyAsyncOverload

open NUnit.Framework
open FSharpLint.Rules

[<TestFixture>]
type TestConventionsFriendlyAsyncOverload() =
inherit TestAstNodeRuleBase.TestAstNodeRuleBase(CSharpFriendlyAsyncOverload.rule)

[<Test>]
member this.``async function must suggest friendly implementation``() =
this.Parse("""
module Foo =
let Bar(): Async<unit> =
Async.Sleep 5""")

Assert.IsTrue(this.ErrorExistsAt(3, 8))

[<Test>]
member this.``async function with friendly implementation must not have errors``() =
this.Parse("""
module Foo =
let Bar(): Async<unit> =
Async.Sleep 5
let BarAsync(): Task<unit> =
Bar() |> Async.StartAsTask""")

this.AssertNoWarnings()

[<Test>]
member this.``non async function must not create warnings``() =
this.Parse("""
module Foo =
let Bar() =
()""")

this.AssertNoWarnings()

[<Test>]
member this.``async function must not have errors when not delcared immediately following the parent function``() =
this.Parse("""
module Foo =
let Bar(): Async<unit> =
Async.Sleep 5
let RandomFunction() =
()
let BarAsync(): Task<unit> =
Bar() |> Async.StartAsTask""")

this.AssertNoWarnings()

[<Test>]
member this.``multiple async functions must have errors``() =
this.Parse("""
module Foo =
let Bar(): Async<unit> =
Async.Sleep 5
let RandomFunction() =
()
let BarAsync(): Task<unit> =
Bar() |> Async.StartAsTask
let Foo(): Async<unit> =
Async.Sleep 10""")

Assert.IsTrue(this.ErrorExistsAt(9, 8))

[<Test>]
member this.``multiple async functions must not have errors``() =
this.Parse("""
module Foo =
let Bar(): Async<unit> =
Async.Sleep 5
let RandomFunction() =
()
let BarAsync(): Task<unit> =
Bar() |> Async.StartAsTask
let Foo(): Async<unit> =
Async.Sleep 10
let FooAsync(): Task<unit> =
Foo() |> Async.StartAsTask""")

this.AssertNoWarnings()

0 comments on commit 8b3636b

Please sign in to comment.