Skip to content

Commit 8e20e0b

Browse files
authored
Merge PR #843 from webwarrior-ws/async-related-rules-types
If output type of function/method is not specified, get it from typed syntax tree. This affects the following rules: - SynchronousFunctionNames - AsynchronousFunctionNames - SimpleAsyncComplementaryHelpers Contributes to #517
2 parents 4c701ea + 8a753e7 commit 8e20e0b

File tree

9 files changed

+310
-60
lines changed

9 files changed

+310
-60
lines changed

src/FSharpLint.Core/Rules/Conventions/Naming/AsynchronousFunctionNames.fs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ open FSharpLint.Framework.Ast
77
open FSharpLint.Framework.Rules
88
open FSharpLint.Rules.Utilities.LibraryHeuristics
99
open Helper.Naming.Asynchronous
10+
open Utilities.TypedTree
1011
open FSharp.Compiler.Syntax
1112

1213
[<RequireQualifiedAccess>]
@@ -41,6 +42,24 @@ let runner (config: Config) (args: AstNodeRuleParams) =
4142
match args.AstNode with
4243
| AstNode.Binding (SynBinding (_, _, _, _, attributes, _, _, SynPat.LongIdent(funcIdent, _, _, _, accessibility, identRange), returnInfo, _, _, _, _))
4344
when isAccessibilityLevelApplicable accessibility && not <| Helper.Naming.isAttribute "Obsolete" attributes ->
45+
let checkAsyncFunction () =
46+
match funcIdent with
47+
| HasAsyncPrefix _ ->
48+
Array.empty
49+
| HasAsyncSuffix name
50+
| HasNoAsyncPrefixOrSuffix name ->
51+
let nameWithAsync = asyncSuffixOrPrefix + name
52+
emitWarning identRange nameWithAsync "Async"
53+
54+
let checkTaskFunction () =
55+
match funcIdent with
56+
| HasAsyncSuffix _ ->
57+
Array.empty
58+
| HasAsyncPrefix name
59+
| HasNoAsyncPrefixOrSuffix name ->
60+
let nameWithAsync = name + asyncSuffixOrPrefix
61+
emitWarning identRange nameWithAsync "Task"
62+
4463
let parents = args.GetParents args.NodeIndex
4564
let hasEnclosingFunctionOrMethod =
4665
parents
@@ -54,25 +73,20 @@ let runner (config: Config) (args: AstNodeRuleParams) =
5473
Array.empty
5574
else
5675
match returnInfo with
57-
| Some ReturnsAsync ->
58-
match funcIdent with
59-
| HasAsyncPrefix _ ->
60-
Array.empty
61-
| HasAsyncSuffix name
62-
| HasNoAsyncPrefixOrSuffix name ->
63-
let nameWithAsync = asyncSuffixOrPrefix + name
64-
emitWarning identRange nameWithAsync "Async"
65-
| Some ReturnsTask ->
66-
match funcIdent with
67-
| HasAsyncSuffix _ ->
68-
Array.empty
69-
| HasAsyncPrefix name
70-
| HasNoAsyncPrefixOrSuffix name ->
71-
let nameWithAsync = name + asyncSuffixOrPrefix
72-
emitWarning identRange nameWithAsync "Task"
76+
| Some ReturnsAsync -> checkAsyncFunction()
77+
| Some ReturnsTask -> checkTaskFunction()
7378
| None ->
74-
// TODO: get type using typed tree in args.CheckInfo
75-
Array.empty
79+
match args.CheckInfo with
80+
| Some checkInfo ->
81+
match getFunctionReturnType checkInfo args.Lines funcIdent with
82+
| Some returnType ->
83+
match returnType with
84+
| FSharpTypeAsync -> checkAsyncFunction()
85+
| FSharpTypeTask
86+
| FSharpTypeTaskNonGeneric -> checkTaskFunction()
87+
| FSharpTypeNonAsync -> Array.empty
88+
| None -> Array.empty
89+
| None -> Array.empty
7690
| _ ->
7791
Array.empty
7892
| _ -> Array.empty

src/FSharpLint.Core/Rules/Conventions/Naming/SimpleAsyncComplementaryHelpers.fs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ open FSharpLint.Rules.Utilities.LibraryHeuristics
99
open Helper.Naming.Asynchronous
1010
open FSharp.Compiler.Syntax
1111
open FSharp.Compiler.Text
12+
open Utilities.TypedTree
13+
open FSharp.Compiler.Symbols
1214

1315
[<RequireQualifiedAccess>]
1416
type Config = {
1517
Mode: AsynchronousFunctionsMode
1618
}
1719

1820
type private ReturnType =
19-
| Async of typeParam: Option<SynType> * isLowercase: bool
20-
| Task of typeParam: Option<SynType>
21+
| Async of typeParamString: Option<string> * isLowercase: bool
22+
| Task of typeParamString: Option<string>
2123

2224
type private Func =
2325
{
@@ -49,11 +51,6 @@ let runner (config: Config) (args: AstNodeRuleParams) =
4951
| Some text -> text
5052
| None -> failwithf "Invalid range: %A" func.Range
5153

52-
let tryGetTypeParamString (maybeTypeParam: Option<SynType>) =
53-
match maybeTypeParam with
54-
| Some typeParam -> ExpressionUtilities.tryFindTextOfRange typeParam.Range args.FileContent
55-
| None -> None
56-
5754
let message =
5855
match func.ReturnType with
5956
| Async (typeParam, isLowercase) ->
@@ -69,7 +66,7 @@ let runner (config: Config) (args: AstNodeRuleParams) =
6966
else
7067
newFuncName
7168
let typeDefString =
72-
match tryGetTypeParamString typeParam with
69+
match typeParam with
7370
| Some "unit" -> ": Task"
7471
| Some str -> $": Task<{str}>"
7572
| None -> String.Empty
@@ -84,7 +81,7 @@ let runner (config: Config) (args: AstNodeRuleParams) =
8481
| Task typeParam ->
8582
let newFuncName = funcDefinitionString.Replace(func.BaseName + asyncSuffixOrPrefix, asyncSuffixOrPrefix + func.BaseName)
8683
let typeDefString =
87-
match tryGetTypeParamString typeParam with
84+
match typeParam with
8885
| Some str -> $": Async<{str}>"
8986
| None -> String.Empty
9087
String.Format(
@@ -112,16 +109,36 @@ let runner (config: Config) (args: AstNodeRuleParams) =
112109
| None
113110
| Some(SynAccess.Public _) -> true
114111
| _ -> config.Mode = AllAPIs
112+
113+
let tryGetReturnTypeParamDisplayName (displayContext: Option<FSharpDisplayContext>) (returnType: FSharpType) =
114+
match tryGetLastGenericArg returnType with
115+
| Some paramType ->
116+
Some <| paramType.Format (displayContext |> Option.defaultValue FSharpDisplayContext.Empty)
117+
| _ -> None
115118

116119
let tryGetFunction (binding: SynBinding) =
117120
match binding with
118-
| SynBinding(_, _, _, _, _, _, _, SynPat.LongIdent(funcIdent, _, _, argPats, accessibility, _), returnInfo, _, _, _, _)
121+
| SynBinding(_, _, _, _, _, _, _, SynPat.LongIdent(funcIdent, _, _, argPats, accessibility, _), returnInfo, _, bindingRange, _, _)
119122
when isAccessibilityLevelApplicable accessibility && not argPats.Patterns.IsEmpty ->
120123
let returnTypeParam =
121124
match returnInfo with
122125
| Some(SynBindingReturnInfo(SynType.App(SynType.LongIdent(SynLongIdent _), _, [ typeParam ], _, _, _, _), _, _, _)) ->
123-
Some typeParam
124-
| _ -> None
126+
ExpressionUtilities.tryFindTextOfRange typeParam.Range args.FileContent
127+
| _ ->
128+
match args.CheckInfo with
129+
| Some checkInfo ->
130+
match getFunctionReturnType checkInfo args.Lines funcIdent with
131+
| Some returnType ->
132+
match returnType with
133+
| FSharpTypeAsync
134+
| FSharpTypeTask ->
135+
tryGetReturnTypeParamDisplayName
136+
(checkInfo.GetDisplayContextForPos bindingRange.End)
137+
returnType
138+
| FSharpTypeTaskNonGeneric -> Some "unit"
139+
| FSharpTypeNonAsync -> None
140+
| None -> None
141+
| None -> None
125142

126143
let funcArgs =
127144
match argPats with

src/FSharpLint.Core/Rules/Conventions/Naming/SynchronousFunctionNames.fs

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ open FSharpLint.Framework
55
open FSharpLint.Framework.Suggestion
66
open FSharpLint.Framework.Ast
77
open FSharpLint.Framework.Rules
8-
open Helper.Naming.Asynchronous
98
open FSharp.Compiler.Syntax
10-
open FSharp.Compiler.Symbols
11-
12-
let asyncSuffixOrPrefix = "Async"
9+
open Helper.Naming.Asynchronous
10+
open Utilities.TypedTree
1311

1412
let runner (args: AstNodeRuleParams) =
1513
let emitWarning range (newFunctionName: string) =
@@ -21,28 +19,38 @@ let runner (args: AstNodeRuleParams) =
2119
TypeChecks = List.empty
2220
}
2321

22+
let checkIdentifier (funcIdent: SynLongIdent) identRange =
23+
match funcIdent with
24+
| HasAsyncPrefix name ->
25+
let startsWithLowercase = Char.IsLower name.[0]
26+
let nameWithoutAsync = name.Substring asyncSuffixOrPrefix.Length
27+
let suggestedName =
28+
if startsWithLowercase then
29+
sprintf "%c%s" (Char.ToLowerInvariant nameWithoutAsync.[0]) (nameWithoutAsync.Substring 1)
30+
else
31+
nameWithoutAsync
32+
emitWarning identRange suggestedName
33+
| HasAsyncSuffix name ->
34+
let nameWithoutAsync = name.Substring(0, name.Length - asyncSuffixOrPrefix.Length)
35+
emitWarning identRange nameWithoutAsync
36+
| HasNoAsyncPrefixOrSuffix _ -> Array.empty
37+
2438
match args.AstNode with
2539
| AstNode.Binding (SynBinding (_, _, _, _, attributes, _, _, SynPat.LongIdent(funcIdent, _, _, _, _, identRange), returnInfo, _, _, _, _))
2640
when not <| Helper.Naming.isAttribute "Obsolete" attributes ->
2741
match returnInfo with
2842
| Some ReturnsNonAsync ->
29-
match funcIdent with
30-
| HasAsyncPrefix name ->
31-
let startsWithLowercase = Char.IsLower name.[0]
32-
let nameWithoutAsync = name.Substring asyncSuffixOrPrefix.Length
33-
let suggestedName =
34-
if startsWithLowercase then
35-
sprintf "%c%s" (Char.ToLowerInvariant nameWithoutAsync.[0]) (nameWithoutAsync.Substring 1)
36-
else
37-
nameWithoutAsync
38-
emitWarning identRange suggestedName
39-
| HasAsyncSuffix name ->
40-
let nameWithoutAsync = name.Substring(0, name.Length - asyncSuffixOrPrefix.Length)
41-
emitWarning identRange nameWithoutAsync
42-
| HasNoAsyncPrefixOrSuffix _ -> Array.empty
43+
checkIdentifier funcIdent identRange
4344
| None ->
44-
// TODO: get type using typed tree in args.CheckInfo
45-
Array.empty
45+
match args.CheckInfo with
46+
| Some checkInfo ->
47+
match getFunctionReturnType checkInfo args.Lines funcIdent with
48+
| Some returnType ->
49+
match returnType with
50+
| FSharpTypeNonAsync -> checkIdentifier funcIdent identRange
51+
| _ -> Array.empty
52+
| None -> Array.empty
53+
| None -> Array.empty
4654
| _ ->
4755
Array.empty
4856
| _ -> Array.empty

src/FSharpLint.Core/Rules/NamingHelper.fs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,11 @@ module Asynchronous =
476476
let (|HasAsyncPrefix|HasAsyncSuffix|HasNoAsyncPrefixOrSuffix|) (pattern: SynLongIdent) =
477477
match List.tryLast pattern.LongIdent with
478478
| Some name ->
479-
if name.idText.StartsWith(asyncSuffixOrPrefix, StringComparison.InvariantCultureIgnoreCase) then
479+
if not (isNotDoubleBackTickedIdent name) then
480+
// Consider any identifier surrounded by double backticks (usually used as test names)
481+
// to not have Async prefix or suffix.
482+
HasNoAsyncPrefixOrSuffix name.idText
483+
elif name.idText.StartsWith(asyncSuffixOrPrefix, StringComparison.InvariantCultureIgnoreCase) then
480484
HasAsyncPrefix name.idText
481485
elif name.idText.EndsWith asyncSuffixOrPrefix then
482486
HasAsyncSuffix name.idText
@@ -493,3 +497,19 @@ module Asynchronous =
493497
| Some ident when ident.idText = "Task" -> ReturnsTask
494498
| _ -> ReturnsNonAsync
495499
| _ -> ReturnsNonAsync
500+
501+
let (|FSharpTypeAsync|FSharpTypeTask|FSharpTypeTaskNonGeneric|FSharpTypeNonAsync|) (fSharpType: FSharpType) =
502+
try
503+
// BasicQualifiedName may throw InvalidOperationException for some types
504+
// e.g. arrays, but not for Async<'T> or Task<'T>, so assume non-async type.
505+
match fSharpType.BasicQualifiedName with
506+
| "Microsoft.FSharp.Control.FSharpAsync`1" -> FSharpTypeAsync
507+
| name when name.StartsWith "System.Threading.Tasks.Task" ->
508+
if fSharpType.GenericArguments.Count > 0 then
509+
FSharpTypeTask
510+
else
511+
FSharpTypeTaskNonGeneric
512+
| _ -> FSharpTypeNonAsync
513+
with
514+
| :? InvalidOperationException ->
515+
FSharpTypeNonAsync

src/FSharpLint.Core/Rules/Utilities.fs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,43 @@
11
module FSharpLint.Rules.Utilities
22

3+
open FSharp.Compiler.CodeAnalysis
4+
open FSharp.Compiler.Symbols
5+
open FSharp.Compiler.Syntax
6+
7+
module TypedTree =
8+
let tryGetLastGenericArg (fSharpType: FSharpType) =
9+
Seq.tryLast fSharpType.GenericArguments
10+
11+
[<TailCall>]
12+
let rec private getReturnType (fSharpType: FSharpType) =
13+
if fSharpType.IsFunctionType then
14+
match tryGetLastGenericArg fSharpType with
15+
| Some argType -> getReturnType argType
16+
| None -> fSharpType
17+
else
18+
fSharpType
19+
20+
let getFunctionReturnType
21+
(checkInfo: FSharpCheckFileResults)
22+
(lines: array<string>)
23+
(funcIdent: SynLongIdent) : Option<FSharpType> =
24+
let maybeSymbolUse =
25+
match List.tryLast funcIdent.LongIdent with
26+
| Some ident ->
27+
checkInfo.GetSymbolUseAtLocation(
28+
ident.idRange.EndLine,
29+
ident.idRange.EndColumn,
30+
lines.[ident.idRange.EndLine - 1],
31+
List.singleton ident.idText)
32+
| None -> None
33+
match maybeSymbolUse with
34+
| Some symbolUse ->
35+
match symbolUse.Symbol with
36+
| :? FSharpMemberOrFunctionOrValue as func when func.IsFunction ->
37+
Some <| getReturnType func.FullType
38+
| _ -> None
39+
| _ -> None
40+
341
module LibraryHeuristics =
442
type LibraryHeuristicResultByProjectName =
543
| Likely

tests/FSharpLint.Core.Tests/Rules/Conventions/AsyncExceptionWithoutReturn.fs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type TestAsyncExceptionWithoutReturn() =
99
inherit FSharpLint.Core.Tests.TestAstNodeRuleBase.TestAstNodeRuleBase(AsyncExceptionWithoutReturn.rule)
1010

1111
[<Test>]
12-
member this.AsyncRaiseWithoutReturn() =
12+
member this.RaiseInAsyncBlockWithoutReturn() =
1313
this.Parse("""
1414
module Program
1515
@@ -22,7 +22,7 @@ let someAsyncFunction () =
2222
Assert.IsTrue this.ErrorsExist
2323

2424
[<Test>]
25-
member this.AsyncRaiseWithReturn() =
25+
member this.RaiseInAsyncBlockWithReturn() =
2626
this.Parse("""
2727
module Program
2828
@@ -34,7 +34,7 @@ let someAsyncFunction () =
3434
this.AssertNoWarnings()
3535

3636
[<Test>]
37-
member this.AsyncFailWithWithoutReturn() =
37+
member this.FailWithInAsyncBlockWithoutReturn() =
3838
this.Parse("""
3939
module Program
4040
@@ -47,7 +47,7 @@ let someAsyncFunction () =
4747
Assert.IsTrue this.ErrorsExist
4848

4949
[<Test>]
50-
member this.AsyncFailwithfWithoutReturn1() =
50+
member this.FailwithfInAsyncBlockWithoutReturn1() =
5151
this.Parse("""
5252
module Program
5353
@@ -61,7 +61,7 @@ let someAsyncFunction () =
6161
Assert.IsTrue this.ErrorsExist
6262

6363
[<Test>]
64-
member this.AsyncFailwithfWithoutReturn2() =
64+
member this.FailwithfInAsyncBlockWithoutReturn2() =
6565
this.Parse("""
6666
module Program
6767
@@ -74,7 +74,7 @@ let someAsyncFunction () =
7474
Assert.IsTrue this.ErrorsExist
7575

7676
[<Test>]
77-
member this.AsyncFailwithWithReturn() =
77+
member this.FailwithInAsyncBlockWithReturn() =
7878
this.Parse("""
7979
module Program
8080
@@ -86,7 +86,7 @@ let someAsyncFunction () =
8686
this.AssertNoWarnings()
8787

8888
[<Test>]
89-
member this.AsyncFailwithfWithReturn() =
89+
member this.FailwithfInAsyncBlockWithReturn() =
9090
this.Parse("""
9191
module Program
9292
@@ -99,7 +99,7 @@ let someAsyncFunction () =
9999
this.AssertNoWarnings()
100100

101101
[<Test>]
102-
member this.AsyncRaiseWithReturnInnerExpression() =
102+
member this.RaiseInAsyncBlockWithReturnInnerExpression() =
103103
this.Parse("""
104104
module Program
105105
@@ -114,7 +114,7 @@ let someAsyncFunction () =
114114
this.AssertNoWarnings()
115115

116116
[<Test>]
117-
member this.AsyncRaiseWithoutReturnInnerExpression() =
117+
member this.RaiseInAsyncBlockWithoutReturnInnerExpression() =
118118
this.Parse("""
119119
module Program
120120

0 commit comments

Comments
 (0)