Skip to content

Commit 8bd8182

Browse files
committed
NegateBooleanExpression code fix
1 parent e505c1f commit 8bd8182

File tree

5 files changed

+357
-2
lines changed

5 files changed

+357
-2
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
module FsAutoComplete.CodeFix.NegateBooleanExpression
2+
3+
open FSharp.Compiler.Symbols
4+
open FSharp.Compiler.Syntax
5+
open FSharp.Compiler.SyntaxTrivia
6+
open FSharp.Compiler.Text
7+
open FsToolkit.ErrorHandling
8+
open Ionide.LanguageServerProtocol.Types
9+
open FsAutoComplete.CodeFix.Types
10+
open FsAutoComplete
11+
open FsAutoComplete.LspHelpers
12+
13+
let title = "Negate boolean expression"
14+
15+
let booleanOperators = set [ "||"; "&&"; "=" ] // TODO: probably some others
16+
17+
[<return: Struct>]
18+
let (|BooleanOperator|_|) =
19+
function
20+
| SynExpr.LongIdent(
21+
longDotId = SynLongIdent(id = [ operatorIdent ]; trivia = [ Some(IdentTrivia.OriginalNotation operatorText) ])) ->
22+
if booleanOperators.Contains operatorText then
23+
ValueSome operatorIdent
24+
else
25+
ValueNone
26+
| _ -> ValueNone
27+
28+
[<return: Struct>]
29+
let (|LastIdentFromSynLongIdent|_|) (SynLongIdent(id = id)) =
30+
match List.tryLast id with
31+
| None -> ValueNone
32+
| Some ident -> ValueSome ident
33+
34+
type FixData =
35+
{ Expr: SynExpr
36+
Path: SyntaxVisitorPath
37+
Ident: Ident
38+
NeedsParensAfterNot: bool }
39+
40+
let mkFix (codeActionParams: CodeActionParams) (sourceText: ISourceText) fixData (returnType: FSharpType) =
41+
if
42+
returnType.IsFunctionType
43+
|| returnType.BasicQualifiedName <> "Microsoft.FSharp.Core.bool"
44+
then
45+
[]
46+
else
47+
let needsOuterParens =
48+
SynExpr.shouldBeParenthesizedInContext sourceText.GetLineString fixData.Path fixData.Expr
49+
50+
let lpr, rpr = if fixData.NeedsParensAfterNot then "(", ")" else "", ""
51+
52+
let newText =
53+
$"not %s{lpr}%s{sourceText.GetSubTextFromRange fixData.Expr.Range}%s{rpr}"
54+
|> (if not needsOuterParens then id else sprintf "(%s)")
55+
56+
57+
[ { SourceDiagnostic = None
58+
Title = title
59+
File = codeActionParams.TextDocument
60+
Edits =
61+
[| { Range = fcsRangeToLsp fixData.Expr.Range
62+
NewText = newText } |]
63+
Kind = FixKind.Fix } ]
64+
65+
let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix =
66+
fun (codeActionParams: CodeActionParams) ->
67+
asyncResult {
68+
// Most code fixes have some general setup.
69+
// We initially want to detect the state of the current code and whether we can propose any text edits to the user.
70+
71+
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
72+
// The converted LSP start position to an FCS start position.
73+
let fcsPos = protocolPosToPos codeActionParams.Range.Start
74+
// The syntax tree and typed tree, current line and sourceText of the current file.
75+
let! (parseAndCheckResults: ParseAndCheckResults, _line: string, sourceText: IFSACSourceText) =
76+
getParseResultsForFile fileName fcsPos
77+
78+
let optFixData =
79+
ParsedInput.tryNode fcsPos parseAndCheckResults.GetParseResults.ParseTree
80+
|> Option.bind (fun (node, path) ->
81+
82+
match node with
83+
| SyntaxNode.SynExpr e ->
84+
match e, path with
85+
// &&
86+
| BooleanOperator operator,
87+
SyntaxNode.SynExpr(SynExpr.App(isInfix = true)) :: SyntaxNode.SynExpr(SynExpr.App(isInfix = false) as e) :: rest ->
88+
Some
89+
{ Expr = e
90+
Ident = operator
91+
Path = rest
92+
NeedsParensAfterNot = true }
93+
94+
// $0X().Y
95+
| SynExpr.Ident _,
96+
SyntaxNode.SynExpr(SynExpr.App _) :: SyntaxNode.SynExpr(SynExpr.DotGet(
97+
longDotId = LastIdentFromSynLongIdent ident) as e) :: rest ->
98+
Some
99+
{ Expr = e
100+
Ident = ident
101+
Path = rest
102+
NeedsParensAfterNot = true }
103+
104+
// X$0()
105+
| SynExpr.Ident ident, SyntaxNode.SynExpr(SynExpr.App _ as e) :: rest ->
106+
Some
107+
{ Expr = e
108+
Ident = ident
109+
Path = rest
110+
NeedsParensAfterNot = true }
111+
112+
// X()$0
113+
| (SynExpr.Const(constant = SynConst.Unit) | SynExpr.Paren _),
114+
SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.Ident ident) as e) :: rest ->
115+
Some
116+
{ Expr = e
117+
Ident = ident
118+
Path = rest
119+
NeedsParensAfterNot = true }
120+
121+
// X().Y$0
122+
| SynExpr.DotGet(longDotId = LastIdentFromSynLongIdent ident), path ->
123+
Some
124+
{ Expr = e
125+
Ident = ident
126+
Path = path
127+
NeedsParensAfterNot = true }
128+
129+
// a.Y
130+
| SynExpr.LongIdent(isOptional = false; longDotId = LastIdentFromSynLongIdent ident), path ->
131+
Some
132+
{ Expr = e
133+
Ident = ident
134+
Path = path
135+
NeedsParensAfterNot = false }
136+
// a
137+
| SynExpr.Ident ident, path ->
138+
Some
139+
{ Expr = e
140+
Ident = ident
141+
Path = path
142+
NeedsParensAfterNot = false }
143+
144+
| _ -> None
145+
| _ -> None)
146+
147+
match optFixData with
148+
| Some fixData ->
149+
let mExpr = fixData.Expr.Range
150+
151+
if mExpr.StartLine <> mExpr.EndLine then
152+
// Only process single line expressions for now
153+
return []
154+
else
155+
match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText fixData.Ident with
156+
| None -> return []
157+
| Some symbolUse ->
158+
match symbolUse.Symbol with
159+
| :? FSharpField as ff -> return mkFix codeActionParams sourceText fixData ff.FieldType
160+
| :? FSharpMemberOrFunctionOrValue as mfv ->
161+
return mkFix codeActionParams sourceText fixData mfv.ReturnParameter.Type
162+
| _ -> return []
163+
| _ -> return []
164+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module FsAutoComplete.CodeFix.NegateBooleanExpression
2+
3+
open FsAutoComplete.CodeFix.Types
4+
5+
val title: string
6+
val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix

src/FsAutoComplete/LspServers/AdaptiveServerState.fs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1924,7 +1924,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac
19241924
RemoveUnnecessaryParentheses.fix forceFindSourceText
19251925
AddTypeAliasToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile
19261926
UpdateTypeAbbreviationInSignatureFile.fix tryGetParseAndCheckResultsForFile
1927-
AddBindingToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile |])
1927+
AddBindingToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile
1928+
NegateBooleanExpression.fix tryGetParseAndCheckResultsForFile |])
19281929

19291930
let forgetDocument (uri: DocumentUri) =
19301931
async {
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
module private FsAutoComplete.Tests.CodeFixTests.NegateBooleanExpressionTests
2+
3+
open Expecto
4+
open Helpers
5+
open Utils.ServerTests
6+
open Utils.CursorbasedTests
7+
open FsAutoComplete.CodeFix
8+
9+
let tests state =
10+
serverTestList
11+
(nameof NegateBooleanExpression)
12+
state
13+
{ defaultConfigDto with
14+
EnableAnalyzers = Some true }
15+
None
16+
(fun server ->
17+
[ let selectCodeFix = CodeFix.withTitle NegateBooleanExpression.title
18+
19+
ftestCaseAsync "negate single identifier"
20+
<| CodeFix.check
21+
server
22+
"let a = false
23+
let b = a$0"
24+
Diagnostics.acceptAll
25+
selectCodeFix
26+
"let a = false
27+
let b = not a"
28+
29+
ftestCaseAsync "negate boolean expression"
30+
<| CodeFix.check
31+
server
32+
"let a = false
33+
let b = a $0|| false"
34+
Diagnostics.acceptAll
35+
selectCodeFix
36+
"let a = false
37+
let b = not (a || false)"
38+
39+
ftestCaseAsync "negate longdotident expression"
40+
<| CodeFix.check
41+
server
42+
"
43+
module A =
44+
let a = false
45+
46+
let b = $0A.a"
47+
Diagnostics.acceptAll
48+
selectCodeFix
49+
"
50+
module A =
51+
let a = false
52+
53+
let b = not A.a"
54+
55+
ftestCaseAsync "negate record field"
56+
<| CodeFix.check
57+
server
58+
"
59+
type X = { Y: bool }
60+
61+
let a = { Y = true }
62+
let b = $0a.Y"
63+
Diagnostics.acceptAll
64+
selectCodeFix
65+
"
66+
type X = { Y: bool }
67+
68+
let a = { Y = true }
69+
let b = not a.Y"
70+
71+
ftestCaseAsync "negate class property"
72+
<| CodeFix.check
73+
server
74+
"
75+
type X() =
76+
member val Y : bool = false
77+
78+
let b = X().Y$0"
79+
Diagnostics.acceptAll
80+
selectCodeFix
81+
"
82+
type X() =
83+
member val Y : bool = false
84+
85+
let b = not (X().Y)"
86+
87+
ftestCaseAsync "negate class property, cursor at start"
88+
<| CodeFix.check
89+
server
90+
"
91+
type X() =
92+
member val Y : bool = false
93+
94+
let b = $0X().Y"
95+
Diagnostics.acceptAll
96+
selectCodeFix
97+
"
98+
type X() =
99+
member val Y : bool = false
100+
101+
let b = not (X().Y)"
102+
103+
ftestCaseAsync "negate unit function call"
104+
<| CodeFix.check
105+
server
106+
"
107+
let a () = false
108+
let b = a$0 ()"
109+
Diagnostics.acceptAll
110+
selectCodeFix
111+
"
112+
let a () = false
113+
let b = not (a ())"
114+
115+
ftestCaseAsync "negate unit function call, cursor at end"
116+
<| CodeFix.check
117+
server
118+
"
119+
let a () = false
120+
let b = a ()$0"
121+
Diagnostics.acceptAll
122+
selectCodeFix
123+
"
124+
let a () = false
125+
let b = not (a ())"
126+
127+
ftestCaseAsync "negate unit function call, cursor at end"
128+
<| CodeFix.check
129+
server
130+
"
131+
let a _ = false
132+
let b = a 4$0"
133+
Diagnostics.acceptAll
134+
selectCodeFix
135+
"
136+
let a _ = false
137+
let b = not (a 4)"
138+
139+
ftestCaseAsync "negate unit function call, cursor at end"
140+
<| CodeFix.check
141+
server
142+
"
143+
let a _ = false
144+
let b = a 4$0"
145+
Diagnostics.acceptAll
146+
selectCodeFix
147+
"
148+
let a _ = false
149+
let b = not (a 4)"
150+
151+
ftestCaseAsync "negate unit member invocation"
152+
<| CodeFix.check
153+
server
154+
"
155+
type X() =
156+
member x.Y () : bool = false
157+
158+
let b = $0X().Y()"
159+
Diagnostics.acceptAll
160+
selectCodeFix
161+
"
162+
type X() =
163+
member x.Y () : bool = false
164+
165+
let b = not (X().Y())"
166+
167+
ftestCaseAsync "negate unit member invocation, cursor at end"
168+
<| CodeFix.check
169+
server
170+
"
171+
type X() =
172+
member x.Y () : bool = false
173+
174+
let b = X().Y()$0"
175+
Diagnostics.acceptAll
176+
selectCodeFix
177+
"
178+
type X() =
179+
member x.Y () : bool = false
180+
181+
let b = not (X().Y())"
182+
183+
])

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3435,4 +3435,5 @@ let tests textFactory state =
34353435
removeUnnecessaryParenthesesTests state
34363436
AddTypeAliasToSignatureFileTests.tests state
34373437
UpdateTypeAbbreviationInSignatureFileTests.tests state
3438-
AddBindingToSignatureFileTests.tests state ]
3438+
AddBindingToSignatureFileTests.tests state
3439+
NegateBooleanExpressionTests.tests state ]

0 commit comments

Comments
 (0)