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 two new codefixes #784

Merged
merged 2 commits into from
May 15, 2021
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
16 changes: 16 additions & 0 deletions src/FsAutoComplete/CodeFixes/AddMissingInstanceMember.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module FsAutoComplete.CodeFix.AddMissingInstanceMember

open FsToolkit.ErrorHandling
open FsAutoComplete.CodeFix.Types
open LanguageServerProtocol.Types

let fix =
Run.ifDiagnosticByCode (Set.ofList [ "673" ]) (fun diagnostic codeActionParams -> asyncResult {
return [{
Title = "Add missing instance member parameter"
File = codeActionParams.TextDocument
Kind = FixKind.Fix
SourceDiagnostic = Some diagnostic
Edits = [| { Range = { Start = diagnostic.Range.Start; End = diagnostic.Range.Start }; NewText = "x." } |]
}]
})
53 changes: 53 additions & 0 deletions src/FsAutoComplete/CodeFixes/ChangeTypeOfNameToNameOf.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/// a codefix that replaces typeof<'t>.Name with nameof('t)
module FsAutoComplete.CodeFix.ChangeTypeOfNameToNameOf

open FsToolkit.ErrorHandling
open FsAutoComplete.CodeFix.Types
open LanguageServerProtocol.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers
open FSharp.Compiler.SourceCodeServices
open FSharp.Compiler.SyntaxTree

type FSharpParseFileResults with
member this.TryRangeOfTypeofWithNameAndTypeExpr pos =
this.ParseTree
|> Option.bind (fun pt ->
AstTraversal.Traverse(pos, pt , { new AstTraversal.AstVisitorBase<_>() with
member _.VisitExpr(_path, _, defaultTraverse, expr) =
match expr with
| SynExpr.DotGet(expr, _, _, range) ->
match expr with
| SynExpr.TypeApp(SynExpr.Ident(ident), _, typeArgs, _, _, _, _) ->
let onlyOneTypeArg =
match typeArgs with
| [] -> false
| [_] -> true
| _ -> false
if ident.idText = "typeof" && onlyOneTypeArg then
Some {| NamedIdentRange = typeArgs.Head.Range; FullExpressionRange = range |}
else
defaultTraverse expr
| _ -> defaultTraverse expr
| _ -> defaultTraverse expr })
)

let fix (getParseResultsForFile: GetParseResultsForFile): CodeFix =
fun codeActionParams ->
asyncResult {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
let pos = protocolPosToPos codeActionParams.Range.Start

let! (tyRes, line, sourceText) = getParseResultsForFile fileName pos
let! results = tyRes.GetParseResults.TryRangeOfTypeofWithNameAndTypeExpr(pos) |> Result.ofOption (fun _ -> "no typeof expr found")
let! typeName = sourceText.GetText results.NamedIdentRange
let replacement = $"nameof({typeName})"

return [{
Edits = [| { Range = fcsRangeToLsp results.FullExpressionRange; NewText = replacement } |]
File = codeActionParams.TextDocument
Title = "Use 'nameof'"
SourceDiagnostic = None
Kind = FixKind.Refactor }]
}
|> AsyncResult.foldResult id (fun _ -> [])
2 changes: 2 additions & 0 deletions src/FsAutoComplete/FsAutoComplete.Lsp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,8 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS
ReplaceBangWithValueFunction.fix tryGetParseResultsForFile getLineText
RemoveUnusedBinding.fix tryGetParseResultsForFile
AddTypeToIndeterminateValue.fix tryGetParseResultsForFile tryGetProjectOptions
ChangeTypeOfNameToNameOf.fix tryGetParseResultsForFile
AddMissingInstanceMember.fix
|]
|> Array.map (fun fixer -> async {
let! fixes = fixer p
Expand Down
67 changes: 67 additions & 0 deletions test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,76 @@ let outerBindingRecursiveTests state =
})
]

let nameofInsteadOfTypeofNameTests state =
let server =
async {
let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "NameofInsteadOfTypeofName")
let! (server, events) = serverInitialize path defaultConfigDto state
do! waitForWorkspaceFinishedParsing events
let path = Path.Combine(path, "Script.fsx")
let tdop : DidOpenTextDocumentParams = { TextDocument = loadDocument path }
do! server.TextDocumentDidOpen tdop
let! diagnostics = waitForParseResultsForFile "Script.fsx" events |> AsyncResult.bimap id (fun _ -> failtest "Should not have had errors")
return (server, path)
}
|> Async.Cache

testList "use nameof instead of typeof.Name" [
testCaseAsync "can suggest fix" (async {
let! server, file = server
let! response = server.TextDocumentCodeAction { CodeActionParams.TextDocument = { Uri = Path.FilePathToUri file }
Range = { Start = { Line = 0; Character = 8 }; End = { Line = 0; Character = 8 }}
Context = { Diagnostics = [| |] } }
match response with
| Ok (Some (TextDocumentCodeActionResult.CodeActions [| { Title = "Use 'nameof'"
Kind = Some "refactor"
Edit = { DocumentChanges = Some [| { Edits = [| { NewText = "nameof(Async<string>)" } |] } |] } } |] )) -> ()
| Ok other -> failtestf $"Should have generated nameof, but instead generated %A{other}"
| Error reason -> failtestf $"Should have succeeded, but failed with %A{reason}"
})
]

let missingInstanceMemberTests state =
let server =
async {
let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "MissingInstanceMember")
let! (server, events) = serverInitialize path defaultConfigDto state
do! waitForWorkspaceFinishedParsing events
let path = Path.Combine(path, "Script.fsx")
let tdop : DidOpenTextDocumentParams = { TextDocument = loadDocument path }
do! server.TextDocumentDidOpen tdop
let! diagnostics = waitForParseResultsForFile "Script.fsx" events |> AsyncResult.bimap (fun _ -> failtest "Should have had errors") (fun e -> e)
return (server, path, diagnostics)
}
|> Async.Cache

testList "missing instance member" [
testCaseAsync "can add this member prefix" (async {
let! server, file, diagnostics = server
let expectedDiagnostic = diagnostics.[0]
Expect.equal expectedDiagnostic.Code (Some "673") "Should have a missing self identifier error"
let! response = server.TextDocumentCodeAction { CodeActionParams.TextDocument = { Uri = Path.FilePathToUri file }
Range = expectedDiagnostic.Range
Context = { Diagnostics = [| expectedDiagnostic |] } }
match response with
| Ok(
Some (
TextDocumentCodeActionResult.CodeActions [| { Title = "Add missing instance member parameter"
Kind = Some "quickfix"
Edit = {
DocumentChanges = Some [|
{ Edits = [|
{ NewText = "x." } |] } |] } } |] )) -> ()
| Ok other -> failtestf $"Should have generated an instance member, but instead generated %A{other}"
| Error reason -> failtestf $"Should have succeeded, but failed with %A{reason}"
})
]

let tests state = testList "codefix tests" [
abstractClassGenerationTests state
generateMatchTests state
missingFunKeywordTests state
outerBindingRecursiveTests state
nameofInsteadOfTypeofNameTests state
missingInstanceMemberTests state
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type C () =
member Foo() = ()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let x = typeof<Async<string>>.Name