Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/how-tos/rule-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -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
Expand Down Expand Up @@ -539,6 +540,7 @@ with
TrailingNewLineInFile = None
NoTabCharacters = None
NoPartialFunctions = None
CSharpFriendlyAsyncOverload = None
}

// fsharplint:enable RecordFieldNames
Expand Down Expand Up @@ -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 &&
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 @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 getErrorMessage (ident: string) (range: range) =
{ Range = range
Message = String.Format(Resources.GetString "RulesCSharpFriendlyAsyncOverload", ident)
SuggestedFix = None
TypeChecks = List.Empty }
|> Array.singleton

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") ->
if (ident.StartsWith "Async") then
match hasAsync args.SyntaxArray args.NodeIndex (ident.Replace("Async", "", StringComparison.Ordinal)) with
| Some _ -> Array.empty
| None ->
getErrorMessage ident range
else
getErrorMessage ident range
| _ -> 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
Expand Up @@ -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
Expand Up @@ -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>Async functions in F# should have the "Async" prefix and a C# friendly overload. Check these conventions 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 },
"csharpFriendlyAsyncOverload": { "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 @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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 should have the prefix Async``() =
this.Parse("""
module Foo =
let Bar(): Async<unit> =
Async.Sleep 5""")

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

[<Test>]
member this.``async function should have the prefix Async 2``() =
this.Parse("""
module Foo =
let Bar(): Async<unit> =
Async.Sleep 5
let BarAsync(): Task<unit> =
Bar() |> Async.StartAsTask""")

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


[<Test>]
member this.``async function must suggest friendly implementation``() =
this.Parse("""
module Foo =
let AsyncBar(): 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 AsyncBar(): 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 AsyncBar(): 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 AsyncBar(): Async<unit> =
Async.Sleep 5
let RandomFunction() =
()
let BarAsync(): Task<unit> =
Bar() |> Async.StartAsTask
let AsyncFoo(): 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 AsyncBar(): Async<unit> =
Async.Sleep 5
let RandomFunction() =
()
let BarAsync(): Task<unit> =
Bar() |> Async.StartAsTask
let AsyncFoo(): Async<unit> =
Async.Sleep 10
let FooAsync(): Task<unit> =
Foo() |> Async.StartAsTask""")

this.AssertNoWarnings()