Skip to content

Commit ede5c57

Browse files
committed
Add new rule C#-FriendlyAsyncOverload
Fixes #517
1 parent 67ad63f commit ede5c57

File tree

10 files changed

+193
-2
lines changed

10 files changed

+193
-2
lines changed

docs/content/how-tos/rule-configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,5 @@ The following rules can be specified for linting.
114114
- [CyclomaticComplexity (FL0071)](rules/FL0071.html)
115115
- [FailwithBadUsage (FL0072)](rules/FL0072.html)
116116
- [FavourReRaise (FL0073)](rules/FL0073.html)
117-
- [FavourConsistentThis (FL0074)](rules/FL0074.html)
117+
- [FavourConsistentThis (FL0074)](rules/FL0074.html)
118+
- [CSharpFriendlyAsyncOverload (FL0075)](rules/FL0075.html)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
title: FL0075
3+
category: how-to
4+
hide_menu: true
5+
---
6+
7+
# CSharpFriendlyAsyncOverload (FL0075)
8+
9+
*Introduced in `0.21.1`*
10+
11+
## Cause
12+
13+
Rule to suggest adding C#-friendly async overloads.
14+
15+
## Rationale
16+
17+
Exposing public async APIs in a C#-friendly manner for better C# interoperability.
18+
19+
## How To Fix
20+
21+
Add an `Async`-suffixed version of the API that returns a `Task<'T>`
22+
23+
## Rule Settings
24+
25+
{
26+
"csharpFriendlyAsyncOverload": {
27+
"enabled": false
28+
}
29+
}

src/FSharpLint.Core/Application/Configuration.fs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,8 @@ type Configuration =
448448
MaxLinesInFile:RuleConfig<MaxLinesInFile.Config> option
449449
TrailingNewLineInFile:EnabledConfig option
450450
NoTabCharacters:EnabledConfig option
451-
NoPartialFunctions:RuleConfig<NoPartialFunctions.Config> option }
451+
NoPartialFunctions:RuleConfig<NoPartialFunctions.Config> option
452+
CSharpFriendlyAsyncOverload:EnabledConfig option }
452453
with
453454
static member Zero = {
454455
Global = None
@@ -531,6 +532,7 @@ with
531532
TrailingNewLineInFile = None
532533
NoTabCharacters = None
533534
NoPartialFunctions = None
535+
CSharpFriendlyAsyncOverload = None
534536
}
535537

536538
// fsharplint:enable RecordFieldNames
@@ -677,6 +679,7 @@ let flattenConfig (config:Configuration) =
677679
config.TrailingNewLineInFile |> Option.bind (constructRuleIfEnabled TrailingNewLineInFile.rule)
678680
config.NoTabCharacters |> Option.bind (constructRuleIfEnabled NoTabCharacters.rule)
679681
config.NoPartialFunctions |> Option.bind (constructRuleWithConfig NoPartialFunctions.rule)
682+
config.CSharpFriendlyAsyncOverload |> Option.bind (constructRuleIfEnabled CSharpFriendlyAsyncOverload.rule)
680683
|] |> Array.choose id
681684

682685
if config.NonPublicValuesNames.IsSome &&

src/FSharpLint.Core/FSharpLint.Core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<Compile Include="Rules\Conventions\CyclomaticComplexity.fs" />
5050
<Compile Include="Rules\Conventions\FavourReRaise.fs" />
5151
<Compile Include="Rules\Conventions\FavourConsistentThis.fs" />
52+
<Compile Include="Rules\Conventions\CSharpFriendlyAsyncOverload.fs" />
5253
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\RaiseWithTooManyArgumentsHelper.fs" />
5354
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\FailwithWithSingleArgument.fs" />
5455
<Compile Include="Rules\Conventions\RaiseWithTooManyArguments\RaiseWithSingleArgument.fs" />
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
module FSharpLint.Rules.CSharpFriendlyAsyncOverload
2+
3+
open FSharpLint.Framework
4+
open FSharpLint.Framework.Suggestion
5+
open FSharp.Compiler.Syntax
6+
open FSharp.Compiler.Text
7+
open FSharpLint.Framework.Ast
8+
open FSharpLint.Framework.Rules
9+
open System
10+
11+
type NodeDetails = { Ident: string; Range: range }
12+
13+
let rec private getIdentFromSynPat =
14+
function
15+
| SynPat.LongIdent (longDotId = longDotId) ->
16+
longDotId
17+
|> ExpressionUtilities.longIdentWithDotsToString
18+
|> Some
19+
| SynPat.Typed (pat, _, _) -> getIdentFromSynPat pat
20+
| _ -> None
21+
22+
let runner (args: AstNodeRuleParams) =
23+
let hasAsync (syntaxArray: array<AbstractSyntaxArray.Node>) nodeIndex fnIdent =
24+
let rec hasAsync index =
25+
if index >= syntaxArray.Length then
26+
None
27+
else
28+
let node = syntaxArray.[index].Actual
29+
match node with
30+
| AstNode.Binding (SynBinding (_, _, _, _, _attributes, _, _, pattern, _, _, range, _)) ->
31+
match getIdentFromSynPat pattern with
32+
| Some ident when ident = fnIdent + "Async" ->
33+
{ Ident = fnIdent
34+
Range = range } |> Some
35+
| _ -> hasAsync (index + 1)
36+
| _ -> hasAsync (index + 1)
37+
38+
hasAsync nodeIndex
39+
40+
match args.AstNode with
41+
| AstNode.Binding (SynBinding (_, _, _, _, _, _, _, pattern, synInfo, _, range, _)) ->
42+
match synInfo with
43+
| Some (SynBindingReturnInfo (SynType.App(SynType.LongIdent(LongIdentWithDots(ident, _)), _, _, _, _, _, _), _, _)) ->
44+
match ident with
45+
| head::_ when head.idText = "Async" ->
46+
let idents = getIdentFromSynPat pattern
47+
match idents with
48+
| Some ident when not (ident.EndsWith "Async") ->
49+
match hasAsync args.SyntaxArray args.NodeIndex ident with
50+
| Some _ -> Array.empty
51+
| None ->
52+
{ Range = range
53+
Message = String.Format(Resources.GetString "RulesCSharpFriendlyAsyncOverload", ident)
54+
SuggestedFix = None
55+
TypeChecks = List.Empty }
56+
|> Array.singleton
57+
| _ -> Array.empty
58+
| _ -> Array.empty
59+
| _ -> Array.empty
60+
| _ -> Array.empty
61+
62+
63+
let rule =
64+
{ Name = "CSharpFriendlyAsyncOverload"
65+
Identifier = Identifiers.CSharpFriendlyAsyncOverload
66+
RuleConfig =
67+
{ AstNodeRuleConfig.Runner = runner
68+
Cleanup = ignore } }
69+
|> AstNodeRule

src/FSharpLint.Core/Rules/Identifiers.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,4 @@ let CyclomaticComplexity = identifier 71
7979
let FailwithBadUsage = identifier 72
8080
let FavourReRaise = identifier 73
8181
let FavourConsistentThis = identifier 74
82+
let CSharpFriendlyAsyncOverload = identifier 75

src/FSharpLint.Core/Text.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,7 @@
333333
<data name="RulesFavourConsistentThis" xml:space="preserve">
334334
<value>Prefer using '{0}' consistently.</value>
335335
</data>
336+
<data name="RulesCSharpFriendlyAsyncOverload" xml:space="preserve">
337+
<value>Consider using a C#-friendly async overload for {0}.</value>
338+
</data>
336339
</root>

src/FSharpLint.Core/fsharplint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"invalidArgWithTwoArguments": { "enabled": true },
4646
"failwithfWithArgumentsMatchingFormatString": { "enabled": true },
4747
"failwithBadUsage": { "enabled": true },
48+
"csharpFriendlyAsyncOverload": { "enabled": false },
4849
"maxLinesInLambdaFunction": {
4950
"enabled": false,
5051
"config": {

tests/FSharpLint.Core.Tests/FSharpLint.Core.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<Compile Include="Rules\Conventions\NoPartialFunctions.fs" />
3838
<Compile Include="Rules\Conventions\FavourReRaise.fs" />
3939
<Compile Include="Rules\Conventions\FavourConsistentThis.fs" />
40+
<Compile Include="Rules\Conventions\CSharpFriendlyAsyncOverload.fs" />
4041
<Compile Include="Rules\Conventions\Naming\NamingHelpers.fs" />
4142
<Compile Include="Rules\Conventions\Naming\InterfaceNames.fs" />
4243
<Compile Include="Rules\Conventions\Naming\ExceptionNames.fs" />
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
module FSharpLint.Core.Tests.Rules.Conventions.CSharpFriendlyAsyncOverload
2+
3+
open NUnit.Framework
4+
open FSharpLint.Rules
5+
6+
[<TestFixture>]
7+
type TestConventionsCSharpFriendlyAsyncOverload() =
8+
inherit TestAstNodeRuleBase.TestAstNodeRuleBase(CSharpFriendlyAsyncOverload.rule)
9+
10+
[<Test>]
11+
member this.``async function must suggest friendly implementation``() =
12+
this.Parse("""
13+
module Foo =
14+
let Bar(): Async<unit> =
15+
Async.Sleep 5""")
16+
17+
Assert.IsTrue(this.ErrorExistsAt(3, 8))
18+
19+
[<Test>]
20+
member this.``async function with friendly implementation must not have errors``() =
21+
this.Parse("""
22+
module Foo =
23+
let Bar(): Async<unit> =
24+
Async.Sleep 5
25+
let BarAsync(): Task<unit> =
26+
Bar() |> Async.StartAsTask""")
27+
28+
this.AssertNoWarnings()
29+
30+
[<Test>]
31+
member this.``non async function must not create warnings``() =
32+
this.Parse("""
33+
module Foo =
34+
let Bar() =
35+
()""")
36+
37+
this.AssertNoWarnings()
38+
39+
[<Test>]
40+
member this.``async function must not have errors when not delcared immediately following the parent function``() =
41+
this.Parse("""
42+
module Foo =
43+
let Bar(): Async<unit> =
44+
Async.Sleep 5
45+
let RandomFunction() =
46+
()
47+
let BarAsync(): Task<unit> =
48+
Bar() |> Async.StartAsTask""")
49+
50+
this.AssertNoWarnings()
51+
52+
[<Test>]
53+
member this.``multiple async functions must have errors``() =
54+
this.Parse("""
55+
module Foo =
56+
let Bar(): Async<unit> =
57+
Async.Sleep 5
58+
let RandomFunction() =
59+
()
60+
let BarAsync(): Task<unit> =
61+
Bar() |> Async.StartAsTask
62+
let Foo(): Async<unit> =
63+
Async.Sleep 10""")
64+
65+
Assert.IsTrue(this.ErrorExistsAt(9, 8))
66+
67+
[<Test>]
68+
member this.``multiple async functions must not have errors``() =
69+
this.Parse("""
70+
module Foo =
71+
let Bar(): Async<unit> =
72+
Async.Sleep 5
73+
let RandomFunction() =
74+
()
75+
let BarAsync(): Task<unit> =
76+
Bar() |> Async.StartAsTask
77+
let Foo(): Async<unit> =
78+
Async.Sleep 10
79+
let FooAsync(): Task<unit> =
80+
Foo() |> Async.StartAsTask""")
81+
82+
this.AssertNoWarnings()

0 commit comments

Comments
 (0)