Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some code fixes for type mismatch. #1250

Merged
merged 1 commit into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/ScaffoldCodeFix.fs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ let wireCodeFixInAdaptiveFSharpLspServer codeFixName =
try
let array = findArrayInAdaptiveFSharpLspServer ()

appendItemToArrayOrList $"%s{codeFixName}.fix tryGetParseResultsForFile" AdaptiveServerStatePath array
appendItemToArrayOrList $"%s{codeFixName}.fix tryGetParseAndCheckResultsForFile" AdaptiveServerStatePath array
with ex ->
Trace.traceException ex

Expand Down
117 changes: 117 additions & 0 deletions src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
module FsAutoComplete.CodeFix.ExprTypeMismatch

#nowarn "57"

open FSharp.Compiler.Diagnostics.ExtendedData
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FsToolkit.ErrorHandling
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.CodeFix.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers

let findReturnType (cursor: pos) (tree: ParsedInput) =
let visitor =
{ new SyntaxVisitorBase<range>() with
member _.VisitBinding(path, defaultTraverse, synBinding) =
match synBinding with
| SynBinding(returnInfo = Some(SynBindingReturnInfo(typeName = t)); expr = bodyExpr) when
Range.rangeContainsPos bodyExpr.Range cursor
->
Some t.Range
| _ -> None }

SyntaxTraversal.Traverse(cursor, tree, visitor)

let needParenthesisWhenWrappedInSome (diagnosticRange: range) (tree: ParsedInput) =
let visitor =
{ new SyntaxVisitorBase<bool>() with
member _.VisitExpr(path, traverseSynExpr, defaultTraverse, synExpr) =
if not (Range.equals synExpr.Range diagnosticRange) then
defaultTraverse synExpr
else
match synExpr with
| SynExpr.Const _
| SynExpr.Ident _ -> Some false
| e -> defaultTraverse e }

SyntaxTraversal.Traverse(diagnosticRange.Start, tree, visitor)
|> Option.defaultValue true

let title = "ExprTypeMismatch Codefix"

let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix =
Run.ifDiagnosticByCode (set [ "1" ]) (fun diagnostic (codeActionParams: CodeActionParams) ->
asyncResult {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
let fcsPos = protocolPosToPos diagnostic.Range.Start

let! (parseAndCheckResults: ParseAndCheckResults, _line: string, sourceText: IFSACSourceText) =
getParseResultsForFile fileName fcsPos

let diagnosticWithExtendedData =
parseAndCheckResults.GetCheckResults.Diagnostics
|> Array.tryPick (fun d ->
match d.ExtendedData with
| Some(:? TypeMismatchDiagnosticExtendedData as data) -> Some(d, data)
| _ -> None)

match diagnosticWithExtendedData with
| None -> return []
| Some(diagnostic, extendedData) ->
let updateReturnType =
findReturnType fcsPos parseAndCheckResults.GetParseResults.ParseTree
|> Option.map (fun mReturnType ->
let currentType = sourceText.GetSubTextFromRange mReturnType
let actualType = extendedData.ActualType.Format(extendedData.DisplayContext)

{ SourceDiagnostic = None
Title = $"Update %s{currentType} to %s{actualType}"
File = codeActionParams.TextDocument
Edits =
[| { Range = fcsRangeToLsp mReturnType
NewText = actualType } |]
Kind = FixKind.Fix })
|> Option.toList

let optionFixes =
if diagnostic.Range.StartLine <> diagnostic.Range.EndLine then
[]
elif
extendedData.ExpectedType.BasicQualifiedName = "Microsoft.FSharp.Core.option`1"
|| extendedData.ExpectedType.BasicQualifiedName = "Microsoft.FSharp.Core.voption`1"
then
let currentExpr = sourceText.GetSubTextFromRange diagnostic.Range

let isValueOption =
extendedData.ExpectedType.BasicQualifiedName = "Microsoft.FSharp.Core.voption`1"

let wrapIn = if isValueOption then "ValueSome" else "Some"
let replaceWithNone = if isValueOption then "ValueNone" else "None"

let needsParenthesis =
needParenthesisWhenWrappedInSome diagnostic.Range parseAndCheckResults.GetParseResults.ParseTree
Comment on lines +93 to +94
Copy link
Contributor

@brianrourkeboll brianrourkeboll Mar 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't apply now, but once the next SDK is shipped, I think this can become something like

parseAndCheckResults.GetAST
|> ParsedInput.tryNode diagnostic.Range.Start
|> Option.forall (fun (node, path) ->
    match node with
    | SyntaxNode.SynExpr expr -> SynExpr.shouldBeParenthesizedInContext getSourceLineStr path expr
    | _ -> true)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was hoping to use that one in the next SDK!


let space, openP, closeP =
if not needsParenthesis then " ", "", "" else "", "(", ")"

[ { SourceDiagnostic = None
Title = $"Wrap expression in %s{wrapIn}"
File = codeActionParams.TextDocument
Edits =
[| { Range = fcsRangeToLsp diagnostic.Range
NewText = $"%s{wrapIn}%s{space}%s{openP}%s{currentExpr}%s{closeP}" } |]
Kind = FixKind.Fix }
{ SourceDiagnostic = None
Title = $"Replace expression with %s{replaceWithNone}"
File = codeActionParams.TextDocument
Edits =
[| { Range = fcsRangeToLsp diagnostic.Range
NewText = replaceWithNone } |]
Kind = FixKind.Fix } ]
else
[]

return [ yield! updateReturnType; yield! optionFixes ]
})
6 changes: 6 additions & 0 deletions src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module FsAutoComplete.CodeFix.ExprTypeMismatch

open FsAutoComplete.CodeFix.Types

val title: string
val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix
3 changes: 2 additions & 1 deletion src/FsAutoComplete/LspServers/AdaptiveServerState.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1905,7 +1905,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac
ToInterpolatedString.fix tryGetParseAndCheckResultsForFile getLanguageVersion
AdjustConstant.fix tryGetParseAndCheckResultsForFile
UpdateValueInSignatureFile.fix tryGetParseAndCheckResultsForFile
RemoveUnnecessaryParentheses.fix forceFindSourceText |])
RemoveUnnecessaryParentheses.fix forceFindSourceText
ExprTypeMismatch.fix tryGetParseAndCheckResultsForFile |])

let forgetDocument (uri: DocumentUri) =
async {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
module private FsAutoComplete.Tests.CodeFixTests.ExprTypeMismatchTests

open Expecto
open Helpers
open Utils.ServerTests
open Utils.CursorbasedTests
open FsAutoComplete.CodeFix

let tests state =
serverTestList (nameof ExprTypeMismatch) state defaultConfigDto None (fun server ->
[ testCaseAsync "Update return type"
<| CodeFix.check
server
"let a b : int = $0\"meh\""
Diagnostics.acceptAll
(CodeFix.withTitle "Update int to string")
"let a b : string = \"meh\""

testCaseAsync "Wrap constant in Some"
<| CodeFix.check
server
"let a b : int option = 1$0"
Diagnostics.acceptAll
(CodeFix.withTitle "Wrap expression in Some")
"let a b : int option = Some 1"

testCaseAsync "Wrap expr in Some"
<| CodeFix.check
server
"let a b : bool option = true$0 = false"
Diagnostics.acceptAll
(CodeFix.withTitle "Wrap expression in Some")
"let a b : bool option = Some(true = false)"

testCaseAsync "Wrap single indent in Some"
<| CodeFix.check
server
"let a b : bool option = let x = true in $0x"
Diagnostics.acceptAll
(CodeFix.withTitle "Wrap expression in Some")
"let a b : bool option = let x = true in Some x"

testCaseAsync "Replace expression with None"
<| CodeFix.check
server
"let a b : int option = 1$0"
Diagnostics.acceptAll
(CodeFix.withTitle "Replace expression with None")
"let a b : int option = None"

testCaseAsync "Wrap constant in ValueSome"
<| CodeFix.check
server
"let a b : int voption = 1$0"
Diagnostics.acceptAll
(CodeFix.withTitle "Wrap expression in ValueSome")
"let a b : int voption = ValueSome 1"

testCaseAsync "Wrap expr in ValueSome"
<| CodeFix.check
server
"let a b : bool voption = true$0 = false"
Diagnostics.acceptAll
(CodeFix.withTitle "Wrap expression in ValueSome")
"let a b : bool voption = ValueSome(true = false)"

testCaseAsync "Wrap single indent in ValueSome"
<| CodeFix.check
server
"let a b : bool voption = let x = true in $0x"
Diagnostics.acceptAll
(CodeFix.withTitle "Wrap expression in ValueSome")
"let a b : bool voption = let x = true in ValueSome x"

testCaseAsync "Replace expression with ValueNone"
<| CodeFix.check
server
"let a b : int voption = 1$0"
Diagnostics.acceptAll
(CodeFix.withTitle "Replace expression with ValueNone")
"let a b : int voption = ValueNone" ])
3 changes: 2 additions & 1 deletion test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3454,4 +3454,5 @@ let tests textFactory state =
removeRedundantAttributeSuffixTests state
removePatternArgumentTests state
UpdateValueInSignatureFileTests.tests state
removeUnnecessaryParenthesesTests state ]
removeUnnecessaryParenthesesTests state
ExprTypeMismatchTests.tests state ]
Loading