Skip to content

Commit

Permalink
Add new rule C#-FriendlyAsyncOverload
Browse files Browse the repository at this point in the history
su8898 authored and parhamsaremi committed Nov 18, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 0daea9c commit 894f29a
Showing 11 changed files with 193 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/content/how-tos/rule-configuration.md
Original file line number Diff line number Diff line change
@@ -117,3 +117,4 @@ The following rules can be specified for linting.
- [FavourConsistentThis (FL0074)](rules/FL0074.html)
- [AvoidTooShortNames (FL0075)](rules/FL0075.html)
- [FavourStaticEmptyFields (FL0076)](rules/FL0076.html)
- [CSharpFriendlyAsyncOverload (FL0077)](rules/FL0077.html)
2 changes: 1 addition & 1 deletion docs/content/how-tos/rules/FL0075.md
Original file line number Diff line number Diff line change
@@ -24,6 +24,6 @@ Use longer names for the flagged occurrences.

{
"avoidTooShortNames": {
"enabled": false
"enabled": false
}
}
29 changes: 29 additions & 0 deletions docs/content/how-tos/rules/FL0077.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: FL0077
category: how-to
hide_menu: true
---

# CSharpFriendlyAsyncOverload (FL0077)

*Introduced in `0.21.1`*

## Cause

Rule to suggest adding C#-friendly async overloads.

## Rationale

Exposing public async APIs in a C#-friendly manner for better C# interoperability.

## How To Fix

Add an `Async`-suffixed version of the API that returns a `Task<'T>`

## Rule Settings

{
"csharpFriendlyAsyncOverload": {
"enabled": false
}
}
5 changes: 4 additions & 1 deletion src/FSharpLint.Core/Application/Configuration.fs
Original file line number Diff line number Diff line change
@@ -454,7 +454,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
@@ -539,6 +540,7 @@ with
TrailingNewLineInFile = None
NoTabCharacters = None
NoPartialFunctions = None
CSharpFriendlyAsyncOverload = None
}

// fsharplint:enable RecordFieldNames
@@ -687,6 +689,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 &&
1 change: 1 addition & 0 deletions src/FSharpLint.Core/FSharpLint.Core.fsproj
Original file line number Diff line number Diff line change
@@ -50,6 +50,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" />
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: array<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 = "CSharpFriendlyAsyncOverload"
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
@@ -81,3 +81,4 @@ let FavourReRaise = identifier 73
let FavourConsistentThis = identifier 74
let AvoidTooShortNames = identifier 75
let FavourStaticEmptyFields = identifier 76
let CSharpFriendlyAsyncOverload = identifier 77
3 changes: 3 additions & 0 deletions src/FSharpLint.Core/Text.resx
Original file line number Diff line number Diff line change
@@ -345,4 +345,7 @@
<data name="RulesFavourStaticEmptyFieldsForArray" xml:space="preserve">
<value>Consider using 'Array.empty' instead.</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
@@ -45,6 +45,7 @@
"invalidArgWithTwoArguments": { "enabled": true },
"failwithfWithArgumentsMatchingFormatString": { "enabled": true },
"failwithBadUsage": { "enabled": true },
"csharpFriendlyAsyncOverload": { "enabled": false },
"maxLinesInLambdaFunction": {
"enabled": false,
"config": {
1 change: 1 addition & 0 deletions tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@
<Compile Include="Rules\Conventions\FavourReRaise.fs" />
<Compile Include="Rules\Conventions\FavourConsistentThis.fs" />
<Compile Include="Rules\Conventions\AvoidTooShortNames.fs" />
<Compile Include="Rules\Conventions\CSharpFriendlyAsyncOverload.fs" />
<Compile Include="Rules\Conventions\Naming\NamingHelpers.fs" />
<Compile Include="Rules\Conventions\Naming\InterfaceNames.fs" />
<Compile Include="Rules\Conventions\Naming\ExceptionNames.fs" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module FSharpLint.Core.Tests.Rules.Conventions.CSharpFriendlyAsyncOverload

open NUnit.Framework
open FSharpLint.Rules

[<TestFixture>]
type TestConventionsCSharpFriendlyAsyncOverload() =
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 894f29a

Please sign in to comment.