Skip to content

Commit 524c2da

Browse files
Wrap arg in parens when needed when adding new keyword (#1335)
* Add parens when needed when adding `new` keyword * Update comment
1 parent 36e7647 commit 524c2da

File tree

4 files changed

+168
-16
lines changed

4 files changed

+168
-16
lines changed

src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,114 @@ open FsAutoComplete.CodeFix.Types
66
open Ionide.LanguageServerProtocol.Types
77
open FsAutoComplete
88
open FsAutoComplete.LspHelpers
9+
open FSharp.Compiler.Syntax
10+
open FSharp.Compiler.Text
911

1012
let title = "Add 'new'"
1113

1214
/// a codefix that suggests using the 'new' keyword on IDisposables
13-
let fix =
15+
let fix (getParseResultsForFile: GetParseResultsForFile) =
1416
Run.ifDiagnosticByCode (Set.ofList [ "760" ]) (fun diagnostic codeActionParams ->
15-
AsyncResult.retn
16-
[ { SourceDiagnostic = Some diagnostic
17-
File = codeActionParams.TextDocument
18-
Title = title
19-
Edits =
20-
[| { Range =
21-
{ Start = diagnostic.Range.Start
22-
End = diagnostic.Range.Start }
23-
NewText = $"new " } |]
24-
Kind = FixKind.Refactor } ])
17+
asyncResult {
18+
let fileName = codeActionParams.TextDocument.GetFilePath() |> normalizePath
19+
let fcsRange = protocolRangeToRange (string fileName) diagnostic.Range
20+
let fcsPos = protocolPosToPos diagnostic.Range.Start
21+
let! parseResults, _, sourceText = getParseResultsForFile fileName fcsPos
22+
23+
// Constructor arg
24+
// Qualified.Constructor arg
25+
// Constructor<TypeArg> arg
26+
// Qualified.Constructor<TypeArg> arg
27+
let matchingApp path node =
28+
let (|TargetTy|_|) expr =
29+
match expr with
30+
| SynExpr.Ident id -> Some(SynType.LongIdent(SynLongIdent([ id ], [], [])))
31+
| SynExpr.LongIdent(longDotId = longDotId) -> Some(SynType.LongIdent longDotId)
32+
| SynExpr.TypeApp(SynExpr.Ident id, lessRange, typeArgs, commaRanges, greaterRange, _, range) ->
33+
Some(
34+
SynType.App(
35+
SynType.LongIdent(SynLongIdent([ id ], [], [])),
36+
Some lessRange,
37+
typeArgs,
38+
commaRanges,
39+
greaterRange,
40+
false,
41+
range
42+
)
43+
)
44+
| SynExpr.TypeApp(SynExpr.LongIdent(longDotId = longDotId),
45+
lessRange,
46+
typeArgs,
47+
commaRanges,
48+
greaterRange,
49+
_,
50+
range) ->
51+
Some(
52+
SynType.App(
53+
SynType.LongIdent longDotId,
54+
Some lessRange,
55+
typeArgs,
56+
commaRanges,
57+
greaterRange,
58+
false,
59+
range
60+
)
61+
)
62+
| _ -> None
63+
64+
match node with
65+
| SyntaxNode.SynExpr(SynExpr.App(funcExpr = TargetTy targetTy; argExpr = argExpr; range = m)) when
66+
m |> Range.equals fcsRange
67+
->
68+
Some(targetTy, argExpr, path)
69+
| _ -> None
70+
71+
return
72+
(fcsRange.Start, parseResults.GetAST)
73+
||> ParsedInput.tryPick matchingApp
74+
|> Option.toList
75+
|> List.map (fun (targetTy, argExpr, path) ->
76+
// Adding `new` may require additional parentheses: https://github.com/dotnet/fsharp/issues/15622
77+
let needsParens =
78+
let newExpr = SynExpr.New(false, targetTy, argExpr, fcsRange)
79+
80+
argExpr
81+
|> SynExpr.shouldBeParenthesizedInContext sourceText.GetLineString (SyntaxNode.SynExpr newExpr :: path)
82+
83+
let newText =
84+
let targetTyText = sourceText.GetSubTextFromRange targetTy.Range
85+
86+
// Constructor namedArg → new Constructor(namedArg)
87+
// Constructor "literal" → new Constructor "literal"
88+
// Constructor () → new Constructor ()
89+
// Constructor() → new Constructor()
90+
// Constructor → new Constructor
91+
// ····indentedArg ····(indentedArg)
92+
let textBetween =
93+
let range = Range.mkRange (string fileName) targetTy.Range.End argExpr.Range.Start
94+
95+
if needsParens && range.StartLine = range.EndLine then
96+
""
97+
else
98+
sourceText.GetSubTextFromRange range
99+
100+
let argExprText =
101+
let originalArgText = sourceText.GetSubTextFromRange argExpr.Range
102+
103+
if needsParens then
104+
$"(%s{originalArgText})"
105+
else
106+
originalArgText
107+
108+
$"new %s{targetTyText}%s{textBetween}%s{argExprText}"
109+
110+
{ SourceDiagnostic = Some diagnostic
111+
File = codeActionParams.TextDocument
112+
Title = title
113+
Edits =
114+
[| { Range =
115+
{ Start = diagnostic.Range.Start
116+
End = diagnostic.Range.End }
117+
NewText = newText } |]
118+
Kind = FixKind.Refactor })
119+
})

src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fsi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ open FsAutoComplete.CodeFix.Types
44
open Ionide.LanguageServerProtocol.Types
55

66
val title: string
7+
78
/// a codefix that suggests using the 'new' keyword on IDisposables
8-
val fix: (CodeActionParams -> Async<Result<Fix list, string>>)
9+
val fix: getParseResultsForFile: GetParseResultsForFile -> (CodeActionParams -> Async<Result<Fix list, string>>)

src/FsAutoComplete/LspServers/AdaptiveServerState.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2130,7 +2130,7 @@ type AdaptiveState
21302130
Run.ifEnabled
21312131
(fun _ -> config.UnusedDeclarationsAnalyzer)
21322132
(RenameUnusedValue.fix tryGetParseAndCheckResultsForFile)
2133-
AddNewKeywordToDisposableConstructorInvocation.fix
2133+
AddNewKeywordToDisposableConstructorInvocation.fix tryGetParseAndCheckResultsForFile
21342134
Run.ifEnabled
21352135
(fun _ -> config.UnionCaseStubGeneration)
21362136
(GenerateUnionCases.fix

test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,63 @@ let private addNewKeywordToDisposableConstructorInvocationTests state =
122122
let _ = System.$0String('.', 3)
123123
"""
124124
Diagnostics.acceptAll
125-
selectCodeFix ])
125+
selectCodeFix
126+
127+
testCaseAsync "adds parentheses when needed"
128+
<| CodeFix.check
129+
server
130+
"""
131+
let path = "test.txt"
132+
let _ = System.IO.StreamReader path$0
133+
"""
134+
(Diagnostics.expectCode "760")
135+
selectCodeFix
136+
"""
137+
let path = "test.txt"
138+
let _ = new System.IO.StreamReader(path)
139+
"""
140+
141+
testCaseAsync "keeps space"
142+
<| CodeFix.check
143+
server
144+
"""
145+
let stream = System.IO.MemoryStream ()$0
146+
"""
147+
(Diagnostics.expectCode "760")
148+
selectCodeFix
149+
"""
150+
let stream = new System.IO.MemoryStream ()
151+
"""
152+
153+
testCaseAsync "does not add space"
154+
<| CodeFix.check
155+
server
156+
"""
157+
let stream = System.IO.MemoryStream()$0
158+
"""
159+
(Diagnostics.expectCode "760")
160+
selectCodeFix
161+
"""
162+
let stream = new System.IO.MemoryStream()
163+
"""
164+
165+
testCaseAsync "adds parentheses when needed and keeps indentation"
166+
<| CodeFix.check
167+
server
168+
"""
169+
let path = "test.txt"
170+
let sr =
171+
System.IO.StreamReader
172+
path$0
173+
"""
174+
(Diagnostics.expectCode "760")
175+
selectCodeFix
176+
"""
177+
let path = "test.txt"
178+
let sr =
179+
new System.IO.StreamReader
180+
(path)
181+
""" ])
126182

127183
let private addTypeToIndeterminateValueTests state =
128184
serverTestList (nameof AddTypeToIndeterminateValue) state defaultConfigDto None (fun server ->
@@ -3550,8 +3606,8 @@ let private removeUnnecessaryParenthesesTests state =
35503606
])
35513607

35523608
let tests textFactory state =
3553-
testSequenced <|
3554-
testList
3609+
testSequenced
3610+
<| testList
35553611
"CodeFix-tests"
35563612
[ HelpersTests.tests textFactory
35573613
AddExplicitTypeAnnotationTests.tests state

0 commit comments

Comments
 (0)