Skip to content

Commit 4c6ed37

Browse files
Wrap arg in parens when needed when adding new keyword (#18179)
1 parent 749853e commit 4c6ed37

File tree

3 files changed

+209
-8
lines changed

3 files changed

+209
-8
lines changed

docs/release-notes/.VisualStudio/17.13.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
### Fixed
2+
* Wrap arg in parens when needed when adding `new` keyword. ([PR #18179](https://github.com/dotnet/fsharp/pull/18179))
23

34
### Added
45
* Code fix for adding missing `seq`. ([PR #17772](https://github.com/dotnet/fsharp/pull/17772))

vsintegration/src/FSharp.Editor/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ namespace Microsoft.VisualStudio.FSharp.Editor
55
open System.Composition
66
open System.Collections.Immutable
77

8+
open FSharp.Compiler.Syntax
9+
open FSharp.Compiler.Text
10+
811
open Microsoft.CodeAnalysis.Text
912
open Microsoft.CodeAnalysis.CodeFixes
1013

@@ -24,11 +27,95 @@ type internal AddNewKeywordCodeFixProvider() =
2427

2528
interface IFSharpCodeFixProvider with
2629
member _.GetCodeFixIfAppliesAsync context =
27-
CancellableTask.singleton (
28-
ValueSome
29-
{
30-
Name = CodeFix.AddNewKeyword
31-
Message = title
32-
Changes = [ TextChange(TextSpan(context.Span.Start, 0), "new ") ]
33-
}
34-
)
30+
cancellableTask {
31+
let! sourceText = context.GetSourceTextAsync()
32+
let! parseFileResults = context.Document.GetFSharpParseResultsAsync(nameof AddNewKeywordCodeFixProvider)
33+
34+
let getSourceLineStr line =
35+
sourceText.Lines[Line.toZ line].ToString()
36+
37+
let range =
38+
RoslynHelpers.TextSpanToFSharpRange(context.Document.FilePath, context.Span, sourceText)
39+
40+
// Constructor arg
41+
// Qualified.Constructor arg
42+
// Constructor<TypeArg> arg
43+
// Qualified.Constructor<TypeArg> arg
44+
let matchingApp path node =
45+
let (|TargetTy|_|) expr =
46+
match expr with
47+
| SynExpr.Ident id -> Some(SynType.LongIdent(SynLongIdent([ id ], [], [])))
48+
| SynExpr.LongIdent(longDotId = longDotId) -> Some(SynType.LongIdent longDotId)
49+
| SynExpr.TypeApp(SynExpr.Ident id, lessRange, typeArgs, commaRanges, greaterRange, _, range) ->
50+
Some(
51+
SynType.App(
52+
SynType.LongIdent(SynLongIdent([ id ], [], [])),
53+
Some lessRange,
54+
typeArgs,
55+
commaRanges,
56+
greaterRange,
57+
false,
58+
range
59+
)
60+
)
61+
| SynExpr.TypeApp(SynExpr.LongIdent(longDotId = longDotId), lessRange, typeArgs, commaRanges, greaterRange, _, range) ->
62+
Some(
63+
SynType.App(SynType.LongIdent longDotId, Some lessRange, typeArgs, commaRanges, greaterRange, false, range)
64+
)
65+
| _ -> None
66+
67+
match node with
68+
| SyntaxNode.SynExpr(SynExpr.App(funcExpr = TargetTy targetTy; argExpr = argExpr; range = m)) when
69+
m |> Range.equals range
70+
->
71+
Some(targetTy, argExpr, path)
72+
| _ -> None
73+
74+
match (range.Start, parseFileResults.ParseTree) ||> ParsedInput.tryPick matchingApp with
75+
| None -> return ValueNone
76+
| Some(targetTy, argExpr, path) ->
77+
// Adding `new` may require additional parentheses: https://github.com/dotnet/fsharp/issues/15622
78+
let needsParens =
79+
let newExpr = SynExpr.New(false, targetTy, argExpr, range)
80+
81+
argExpr
82+
|> SynExpr.shouldBeParenthesizedInContext getSourceLineStr (SyntaxNode.SynExpr newExpr :: path)
83+
84+
let newText =
85+
let targetTyText =
86+
sourceText.ToString(RoslynHelpers.FSharpRangeToTextSpan(sourceText, targetTy.Range))
87+
88+
// Constructor namedArg → new Constructor(namedArg)
89+
// Constructor "literal" → new Constructor "literal"
90+
// Constructor () → new Constructor ()
91+
// Constructor() → new Constructor()
92+
// Constructor → new Constructor
93+
// ····indentedArg ····(indentedArg)
94+
let textBetween =
95+
let range =
96+
Range.mkRange context.Document.FilePath targetTy.Range.End argExpr.Range.Start
97+
98+
if needsParens && range.StartLine = range.EndLine then
99+
""
100+
else
101+
sourceText.ToString(RoslynHelpers.FSharpRangeToTextSpan(sourceText, range))
102+
103+
let argExprText =
104+
let originalArgText =
105+
sourceText.ToString(RoslynHelpers.FSharpRangeToTextSpan(sourceText, argExpr.Range))
106+
107+
if needsParens then
108+
$"(%s{originalArgText})"
109+
else
110+
originalArgText
111+
112+
$"new %s{targetTyText}%s{textBetween}%s{argExprText}"
113+
114+
return
115+
ValueSome
116+
{
117+
Name = CodeFix.AddNewKeyword
118+
Message = title
119+
Changes = [ TextChange(context.Span, newText) ]
120+
}
121+
}

vsintegration/tests/FSharp.Editor.Tests/CodeFixes/AddNewKeywordToDisposableConstructorInvocationTests.fs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,116 @@ let sr = new System.IO.StreamReader "test.txt"
2929
let actual = codeFix |> tryFix code Auto
3030

3131
Assert.Equal(expected, actual)
32+
33+
[<Fact>]
34+
let ``Fixes FS0760 type app`` () =
35+
let code =
36+
"""
37+
let _ = System.Threading.Tasks.Task<int>(fun _ -> 3)
38+
"""
39+
40+
let expected =
41+
Some
42+
{
43+
Message = "Add 'new' keyword"
44+
FixedCode =
45+
"""
46+
let _ = new System.Threading.Tasks.Task<int>(fun _ -> 3)
47+
"""
48+
}
49+
50+
let actual = codeFix |> tryFix code Auto
51+
52+
Assert.Equal(expected, actual)
53+
54+
[<Fact>]
55+
let ``Fixes FS0760 keeps space`` () =
56+
let code =
57+
"""
58+
let stream = System.IO.MemoryStream ()
59+
"""
60+
61+
let expected =
62+
Some
63+
{
64+
Message = "Add 'new' keyword"
65+
FixedCode =
66+
"""
67+
let stream = new System.IO.MemoryStream ()
68+
"""
69+
}
70+
71+
let actual = codeFix |> tryFix code Auto
72+
73+
Assert.Equal(expected, actual)
74+
75+
[<Fact>]
76+
let ``Fixes FS0760 does not add space`` () =
77+
let code =
78+
"""
79+
let stream = System.IO.MemoryStream()
80+
"""
81+
82+
let expected =
83+
Some
84+
{
85+
Message = "Add 'new' keyword"
86+
FixedCode =
87+
"""
88+
let stream = new System.IO.MemoryStream()
89+
"""
90+
}
91+
92+
let actual = codeFix |> tryFix code Auto
93+
94+
Assert.Equal(expected, actual)
95+
96+
[<Fact>]
97+
let ``Fixes FS0760 adds parentheses when needed`` () =
98+
let code =
99+
"""
100+
let path = "test.txt"
101+
let sr = System.IO.StreamReader path
102+
"""
103+
104+
let expected =
105+
Some
106+
{
107+
Message = "Add 'new' keyword"
108+
FixedCode =
109+
"""
110+
let path = "test.txt"
111+
let sr = new System.IO.StreamReader(path)
112+
"""
113+
}
114+
115+
let actual = codeFix |> tryFix code Auto
116+
117+
Assert.Equal(expected, actual)
118+
119+
[<Fact>]
120+
let ``Fixes FS0760 adds parentheses when needed and keeps indentation`` () =
121+
let code =
122+
"""
123+
let path = "test.txt"
124+
let sr =
125+
System.IO.StreamReader
126+
path
127+
"""
128+
129+
let expected =
130+
Some
131+
{
132+
Message = "Add 'new' keyword"
133+
FixedCode =
134+
"""
135+
let path = "test.txt"
136+
let sr =
137+
new System.IO.StreamReader
138+
(path)
139+
"""
140+
}
141+
142+
let actual = codeFix |> tryFix code Auto
143+
144+
Assert.Equal(expected, actual)

0 commit comments

Comments
 (0)