Skip to content

Commit bbf0e06

Browse files
Add parens when needed when adding new keyword
1 parent 36e7647 commit bbf0e06

File tree

4 files changed

+166
-16
lines changed

4 files changed

+166
-16
lines changed

src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,112 @@ 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+
let matchingApp path node =
26+
let (|TargetTy|_|) expr =
27+
match expr with
28+
| SynExpr.Ident id -> Some(SynType.LongIdent(SynLongIdent([ id ], [], [])))
29+
| SynExpr.LongIdent(longDotId = longDotId) -> Some(SynType.LongIdent longDotId)
30+
| SynExpr.TypeApp(SynExpr.Ident id, lessRange, typeArgs, commaRanges, greaterRange, _, range) ->
31+
Some(
32+
SynType.App(
33+
SynType.LongIdent(SynLongIdent([ id ], [], [])),
34+
Some lessRange,
35+
typeArgs,
36+
commaRanges,
37+
greaterRange,
38+
false,
39+
range
40+
)
41+
)
42+
| SynExpr.TypeApp(SynExpr.LongIdent(longDotId = longDotId),
43+
lessRange,
44+
typeArgs,
45+
commaRanges,
46+
greaterRange,
47+
_,
48+
range) ->
49+
Some(
50+
SynType.App(
51+
SynType.LongIdent longDotId,
52+
Some lessRange,
53+
typeArgs,
54+
commaRanges,
55+
greaterRange,
56+
false,
57+
range
58+
)
59+
)
60+
| _ -> None
61+
62+
match node with
63+
| SyntaxNode.SynExpr(SynExpr.App(funcExpr = TargetTy targetTy; argExpr = argExpr; range = m)) when
64+
m |> Range.equals fcsRange
65+
->
66+
Some(targetTy, argExpr, path)
67+
| _ -> None
68+
69+
return
70+
(fcsRange.Start, parseResults.GetAST)
71+
||> ParsedInput.tryPick matchingApp
72+
|> Option.toList
73+
|> List.map (fun (targetTy, argExpr, path) ->
74+
// Adding `new` may require additional parentheses: https://github.com/dotnet/fsharp/issues/15622
75+
let needsParens =
76+
let newExpr = SynExpr.New(false, targetTy, argExpr, fcsRange)
77+
78+
argExpr
79+
|> SynExpr.shouldBeParenthesizedInContext sourceText.GetLineString (SyntaxNode.SynExpr newExpr :: path)
80+
81+
let newText =
82+
let targetTyText = sourceText.GetSubTextFromRange targetTy.Range
83+
84+
// Constructor namedArg → new Constructor(namedArg)
85+
// Constructor "literal" → new Constructor "literal"
86+
// Constructor () → new Constructor ()
87+
// Constructor() → new Constructor()
88+
// Constructor → new Constructor
89+
// ····indentedArg ····(indentedArg)
90+
let textBetween =
91+
let range = Range.mkRange (string fileName) targetTy.Range.End argExpr.Range.Start
92+
93+
if needsParens && range.StartLine = range.EndLine then
94+
""
95+
else
96+
sourceText.GetSubTextFromRange range
97+
98+
let argExprText =
99+
let originalArgText = sourceText.GetSubTextFromRange argExpr.Range
100+
101+
if needsParens then
102+
$"(%s{originalArgText})"
103+
else
104+
originalArgText
105+
106+
$"new %s{targetTyText}%s{textBetween}%s{argExprText}"
107+
108+
{ SourceDiagnostic = Some diagnostic
109+
File = codeActionParams.TextDocument
110+
Title = title
111+
Edits =
112+
[| { Range =
113+
{ Start = diagnostic.Range.Start
114+
End = diagnostic.Range.End }
115+
NewText = newText } |]
116+
Kind = FixKind.Refactor })
117+
})

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)