Skip to content

Commit 3924085

Browse files
authored
Merge PR #646 from webwarrior-ws/rule-suggestions
Add quickfix suggestions for several rules.
2 parents b866821 + 510357a commit 3924085

File tree

10 files changed

+255
-45
lines changed

10 files changed

+255
-45
lines changed

src/FSharpLint.Core/Rules/Conventions/AvoidSinglePipeOperator.fs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@ open FSharpLint.Framework.Ast
99
open FSharpLint.Framework.Rules
1010

1111
let runner (args: AstNodeRuleParams) =
12-
let errors range =
12+
let errors range suggestedFix =
1313
{
1414
Range = range
1515
Message = String.Format(Resources.GetString ("RulesAvoidSinglePipeOperator"))
16-
SuggestedFix = None
16+
SuggestedFix = suggestedFix
1717
TypeChecks = List.Empty
1818
} |> Array.singleton
1919

20-
let rec checkExpr (expr: SynExpr) (parentList: AstNode list): WarningDetails array =
20+
let rec checkExpr (expr: SynExpr) (outerArgExpr: SynExpr) (range: FSharp.Compiler.Text.range) (parentList: AstNode list): WarningDetails array =
2121
let checkParentPiped (expr: AstNode) =
2222
match expr with
2323
| AstNode.Expression(SynExpr.App(_exprAtomicFlag, _isInfix, funcExpr, _argExpr, _range)) ->
24-
checkExpr funcExpr [] |> Seq.isEmpty
24+
checkExpr funcExpr outerArgExpr range [] |> Seq.isEmpty
2525
| _ -> false
2626

2727
match expr with
28-
| SynExpr.App(_exprAtomicFlag, _isInfix, funcExpr, argExpr, _range) ->
28+
| SynExpr.App(_exprAtomicFlag, _isInfix, funcExpr, argExpr, appRange) ->
2929
match funcExpr with
3030
| ExpressionUtilities.Identifier([ ident ], _) ->
3131
if ident.idText = "op_PipeRight" then
@@ -42,7 +42,15 @@ let runner (args: AstNodeRuleParams) =
4242
if isParentPiped then
4343
Array.empty
4444
else
45-
errors ident.idRange
45+
let suggestedFix = lazy(
46+
let maybeFuncText = ExpressionUtilities.tryFindTextOfRange outerArgExpr.Range args.FileContent
47+
let maybeArgText = ExpressionUtilities.tryFindTextOfRange argExpr.Range args.FileContent
48+
match maybeFuncText, maybeArgText with
49+
| Some(funcText), Some(argText) ->
50+
let replacementText = sprintf "%s %s" funcText argText
51+
Some { FromText=args.FileContent; FromRange=range; ToText=replacementText }
52+
| _ -> None)
53+
errors ident.idRange (Some suggestedFix)
4654
else
4755
Array.empty
4856
| _ ->
@@ -52,13 +60,13 @@ let runner (args: AstNodeRuleParams) =
5260

5361
let error =
5462
match args.AstNode with
55-
| AstNode.Expression(SynExpr.App(_exprAtomicFlag, _isInfix, funcExpr, argExpr, _range)) ->
63+
| AstNode.Expression(SynExpr.App(_exprAtomicFlag, _isInfix, funcExpr, argExpr, range)) ->
5664
match argExpr with
5765
| SynExpr.App(_) ->
5866
// function has extra arguments
5967
Array.empty
6068
| _ ->
61-
checkExpr funcExpr (args.GetParents args.NodeIndex)
69+
checkExpr funcExpr argExpr range (args.GetParents args.NodeIndex)
6270
| _ ->
6371
Array.empty
6472

src/FSharpLint.Core/Rules/Conventions/Binding/TupleOfWildcards.fs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ open FSharpLint.Framework.Suggestion
77
open FSharpLint.Framework.Ast
88
open FSharpLint.Framework.Rules
99

10-
let private checkTupleOfWildcards pattern identifier =
10+
let private checkTupleOfWildcards fileContents pattern identifier identifierRange =
1111
let rec isWildcard = function
1212
| SynPat.Paren(pattern, _) -> isWildcard pattern
1313
| SynPat.Wild(_) -> true
@@ -16,15 +16,20 @@ let private checkTupleOfWildcards pattern identifier =
1616
let constructorString numberOfWildcards =
1717
let constructorName = identifier |> String.concat "."
1818
let arguments = Array.create numberOfWildcards "_" |> String.concat ", "
19-
$"{constructorName}({arguments})"
19+
if numberOfWildcards = 1 then
20+
$"%s{constructorName} _"
21+
else
22+
$"%s{constructorName}(%s{arguments})"
2023

2124
match pattern with
2225
| SynPat.Tuple(_isStruct, patterns, _, range) when List.length patterns > 1 && patterns |> List.forall isWildcard ->
2326
let errorFormat = Resources.GetString("RulesTupleOfWildcardsError")
2427
let refactorFrom = constructorString (List.length patterns)
2528
let refactorTo = constructorString 1
2629
let error = String.Format(errorFormat, refactorFrom, refactorTo)
27-
{ Range = range; Message = error; SuggestedFix = None; TypeChecks = [] } |> Array.singleton
30+
let suggestedFix = lazy(
31+
Some { SuggestedFix.FromRange = identifierRange; FromText = fileContents; ToText = refactorTo })
32+
{ Range = range; Message = error; SuggestedFix = Some suggestedFix; TypeChecks = [] } |> Array.singleton
2833
| _ -> Array.empty
2934

3035
let private isTupleMemberArgs breadcrumbs tupleRange =
@@ -44,11 +49,11 @@ let private isTupleMemberArgs breadcrumbs tupleRange =
4449

4550
let private runner (args:AstNodeRuleParams) =
4651
match args.AstNode with
47-
| AstNode.Pattern(SynPat.LongIdent(identifier, _, _, SynArgPats.Pats([SynPat.Paren(SynPat.Tuple(_, _, _, range) as pattern, _)]), _, _)) ->
52+
| AstNode.Pattern(SynPat.LongIdent(identifier, _, _, SynArgPats.Pats([SynPat.Paren(SynPat.Tuple(_, _, _, range) as pattern, _)]), _, identRange)) ->
4853
let breadcrumbs = args.GetParents 2
4954
if (not << isTupleMemberArgs breadcrumbs) range then
5055
let identifier = identifier.LongIdent |> List.map (fun x -> x.idText)
51-
checkTupleOfWildcards pattern identifier
56+
checkTupleOfWildcards args.FileContent pattern identifier identRange
5257
else
5358
Array.empty
5459
| _ -> Array.empty

src/FSharpLint.Core/Rules/Conventions/Binding/WildcardNamedWithAsPattern.fs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@ open FSharp.Compiler.Syntax
77
open FSharpLint.Framework.Ast
88
open FSharpLint.Framework.Rules
99

10-
let private checkForWildcardNamedWithAsPattern pattern =
10+
let private checkForWildcardNamedWithAsPattern fileContents pattern =
1111
match pattern with
12-
| SynPat.Wild(range) ->
12+
| SynPat.As(SynPat.Wild(wildcardRange), SynPat.Named(SynIdent(identifier, _), _, _, _), range)
13+
when wildcardRange <> range ->
14+
let suggestedFix =
15+
lazy(
16+
Some { FromRange = range; FromText = fileContents; ToText = identifier.idText })
1317
{ Range = range
1418
Message = Resources.GetString("RulesWildcardNamedWithAsPattern")
15-
SuggestedFix = None
19+
SuggestedFix = Some suggestedFix
1620
TypeChecks = [] } |> Array.singleton
1721
| _ -> Array.empty
1822

1923
let private runner (args:AstNodeRuleParams) =
2024
match args.AstNode with
21-
| AstNode.Pattern(SynPat.As(leftHandSide, _, _)) ->
22-
checkForWildcardNamedWithAsPattern leftHandSide
25+
| AstNode.Pattern(SynPat.As(SynPat.Wild(_), SynPat.Named(_), _) as pattern) ->
26+
checkForWildcardNamedWithAsPattern args.FileContent pattern
2327
| _ -> Array.empty
2428

2529
let rule =

src/FSharpLint.Core/Rules/Conventions/FavourStaticEmptyFields.fs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,28 @@ let private getStaticEmptyErrorMessage (range:FSharp.Compiler.Text.Range) (empt
2424

2525
errorMessageKey |> formatError
2626

27-
let private generateError (range:FSharp.Compiler.Text.Range) (emptyLiteralType: EmptyLiteralType) =
27+
let private generateError (fileContents: string) (range:FSharp.Compiler.Text.Range) (emptyLiteralType: EmptyLiteralType) =
28+
let suggestedFix = lazy(
29+
let replacementText =
30+
match emptyLiteralType with
31+
| EmptyStringLiteral -> "String.Empty"
32+
| EmptyListLiteral -> "List.Empty"
33+
| EmptyArrayLiteral -> "Array.empty"
34+
Some({ FromRange = range; FromText = fileContents; ToText = replacementText }))
2835
{ Range = range
2936
Message = getStaticEmptyErrorMessage range emptyLiteralType
30-
SuggestedFix = None
37+
SuggestedFix = Some suggestedFix
3138
TypeChecks = List.Empty }
3239
|> Array.singleton
3340

3441
let private runner (args: AstNodeRuleParams) =
3542
match args.AstNode with
3643
| AstNode.Expression(SynExpr.Const (SynConst.String ("", _, range), _)) ->
37-
generateError range EmptyStringLiteral
44+
generateError args.FileContent range EmptyStringLiteral
3845
| AstNode.Expression(SynExpr.ArrayOrList (isArray, [], range)) ->
3946
let emptyLiteralType =
4047
if isArray then EmptyArrayLiteral else EmptyListLiteral
41-
generateError range emptyLiteralType
48+
generateError args.FileContent range emptyLiteralType
4249
| AstNode.Expression(SynExpr.Record(_, _, synExprRecordField, _)) ->
4350
synExprRecordField
4451
|> List.map (fun field ->
@@ -47,11 +54,11 @@ let private runner (args: AstNodeRuleParams) =
4754
match expr with
4855
| Some(SynExpr.ArrayOrList(isArray, [], range)) ->
4956
let emptyLiteralType = if isArray then EmptyArrayLiteral else EmptyListLiteral
50-
generateError range emptyLiteralType
57+
generateError args.FileContent range emptyLiteralType
5158
| Some(SynExpr.Const (SynConst.String ("", _, range), _)) ->
52-
generateError range EmptyStringLiteral
59+
generateError args.FileContent range EmptyStringLiteral
5360
| Some(SynExpr.App(_, _, _, SynExpr.Const (SynConst.String ("", _, range), _), _)) ->
54-
generateError range EmptyStringLiteral
61+
generateError args.FileContent range EmptyStringLiteral
5562
| _ -> Array.empty)
5663
|> Array.concat
5764
| _ -> Array.empty

src/FSharpLint.Core/Rules/Conventions/FunctionReimplementation/CanBeReplacedWithComposition.fs

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ open FSharpLint.Framework.Ast
88
open FSharpLint.Framework.Rules
99
open FSharpLint.Framework.ExpressionUtilities
1010

11-
let private validateLambdaCannotBeReplacedWithComposition _ lambda range =
12-
let canBeReplacedWithFunctionComposition expression =
11+
let private validateLambdaCannotBeReplacedWithComposition fileContents _ lambda range =
12+
let tryReplaceWithFunctionComposition expression =
1313
let getLastElement = List.rev >> List.head
1414

15-
let rec lambdaArgumentIsLastApplicationInFunctionCalls expression (lambdaArgument:Ident) numFunctionCalls =
15+
let rec lambdaArgumentIsLastApplicationInFunctionCalls expression (lambdaArgument:Ident) (calledFunctionIdents: List<string>) =
1616
let rec appliedValuesAreConstants appliedValues =
1717
match appliedValues with
1818
| (SynExpr.Const(_)| SynExpr.Null(_))::rest -> appliedValuesAreConstants rest
@@ -22,34 +22,56 @@ let private validateLambdaCannotBeReplacedWithComposition _ lambda range =
2222
match AstNode.Expression expression with
2323
| FuncApp(exprs, _) ->
2424
match List.map removeParens exprs with
25-
| (SynExpr.Ident(_) | SynExpr.LongIdent(_))::appliedValues
25+
| (ExpressionUtilities.Identifier(idents, _))::appliedValues
2626
when appliedValuesAreConstants appliedValues ->
27-
27+
28+
let funcName = String.Join('.', idents)
29+
let funcStringParts =
30+
Seq.append
31+
(Seq.singleton funcName)
32+
(appliedValues
33+
|> Seq.take (appliedValues.Length - 1)
34+
|> Seq.choose (fun value -> ExpressionUtilities.tryFindTextOfRange value.Range fileContents))
35+
let funcString = String.Join(' ', funcStringParts)
36+
2837
match getLastElement appliedValues with
29-
| SynExpr.Ident(lastArgument) when numFunctionCalls > 1 ->
30-
lastArgument.idText = lambdaArgument.idText
38+
| SynExpr.Ident(lastArgument) when calledFunctionIdents.Length > 1 ->
39+
if lastArgument.idText = lambdaArgument.idText then
40+
funcString :: calledFunctionIdents
41+
else
42+
[]
3143
| SynExpr.App(_, false, _, _, _) as nextFunction ->
32-
lambdaArgumentIsLastApplicationInFunctionCalls nextFunction lambdaArgument (numFunctionCalls + 1)
33-
| _ -> false
34-
| _ -> false
35-
| _ -> false
44+
lambdaArgumentIsLastApplicationInFunctionCalls
45+
nextFunction
46+
lambdaArgument
47+
(funcString :: calledFunctionIdents)
48+
| _ -> []
49+
| _ -> []
50+
| _ -> []
3651

3752
match lambda.Arguments with
3853
| [singleParameter] ->
39-
Helper.FunctionReimplementation.getLambdaParamIdent singleParameter
40-
|> Option.exists (fun paramIdent -> lambdaArgumentIsLastApplicationInFunctionCalls expression paramIdent 1)
41-
| _ -> false
54+
match Helper.FunctionReimplementation.getLambdaParamIdent singleParameter with
55+
| Some paramIdent ->
56+
match lambdaArgumentIsLastApplicationInFunctionCalls expression paramIdent [] with
57+
| [] -> None
58+
| funcStrings -> Some funcStrings
59+
| None -> None
60+
| _ -> None
4261

43-
if canBeReplacedWithFunctionComposition lambda.Body then
62+
match tryReplaceWithFunctionComposition lambda.Body with
63+
| None -> Array.empty
64+
| Some funcStrings ->
65+
let suggestedFix =
66+
lazy(
67+
Some { FromRange = range; FromText = fileContents; ToText = String.Join(" >> ", funcStrings) })
4468
{ Range = range
4569
Message = Resources.GetString("RulesCanBeReplacedWithComposition")
46-
SuggestedFix = None
70+
SuggestedFix = Some suggestedFix
4771
TypeChecks = [] } |> Array.singleton
48-
else
49-
Array.empty
5072

5173
let runner (args:AstNodeRuleParams) =
52-
Helper.FunctionReimplementation.checkLambda args validateLambdaCannotBeReplacedWithComposition
74+
Helper.FunctionReimplementation.checkLambda args (validateLambdaCannotBeReplacedWithComposition args.FileContent)
5375

5476
let rule =
5577
{ Name = "CanBeReplacedWithComposition"

tests/FSharpLint.Core.Tests/Rules/Binding/TupleOfWildcards.fs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,38 @@ match Persian(1, 3) with
2121

2222
Assert.IsTrue(this.ErrorExistsAt(7, 10))
2323

24+
[<Test>]
25+
member this.``Suggested fix for tuple of wildcards should be single wildcard``() =
26+
let source = """
27+
match cat with
28+
| Persian(_, _) -> ()"""
29+
30+
let expected = """
31+
match cat with
32+
| Persian _ -> ()"""
33+
34+
this.Parse source
35+
36+
let result = this.ApplyQuickFix source
37+
38+
Assert.AreEqual(expected, result)
39+
40+
[<Test>]
41+
member this.``Suggested fix for tuple of wildcards in nested pattern should be single wildcard``() =
42+
let source = """
43+
match maybeCat with
44+
| Some(Persian(_, _)) -> ()"""
45+
46+
let expected = """
47+
match maybeCat with
48+
| Some(Persian _) -> ()"""
49+
50+
this.Parse source
51+
52+
let result = this.ApplyQuickFix source
53+
54+
Assert.AreEqual(expected, result)
55+
2456
[<Test>]
2557
member this.``Method's parameter list of wildcards should not be treated as tuple of wildcards.``() =
2658
this.Parse """

tests/FSharpLint.Core.Tests/Rules/Binding/WildcardNamedWithAsPattern.fs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,28 @@ type TestBindingWildcardNamedWithAsPattern() =
1010

1111
[<Test>]
1212
member this.WildcardNamedWithAsPattern() =
13-
this.Parse """
13+
let source = """
1414
module Program
1515
1616
match [] with
1717
| _ as x -> ()
1818
"""
1919

20+
let expected = """
21+
module Program
22+
23+
match [] with
24+
| x -> ()
25+
"""
26+
27+
this.Parse source
28+
2029
Assert.IsTrue(this.ErrorExistsAt(5, 2))
2130

31+
let result = this.ApplyQuickFix source
32+
33+
Assert.AreEqual(expected, result)
34+
2235
[<Test>]
2336
member this.NamedPattern() =
2437
this.Parse """

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,23 @@ module Foo
200200

201201
Assert.IsTrue this.NoErrorsExist
202202

203+
[<Test>]
204+
member this.``Suggest not using pipe operator if it's used once``() =
205+
let source = """
206+
let someFunc someParam =
207+
someParam
208+
|> someOtherFunc
209+
"""
210+
let expected = """
211+
let someFunc someParam =
212+
someOtherFunc someParam
213+
"""
214+
215+
this.Parse source
216+
let fixedSource = this.ApplyQuickFix source
217+
218+
Assert.AreEqual(expected, fixedSource)
219+
203220
[<Test>]
204221
member this.``Use pipe operator for function with more than 1 argument``() =
205222
this.Parse """

0 commit comments

Comments
 (0)