diff --git a/vsintegration/src/FSharp.Editor/Refactor/AddReturnType.fs b/vsintegration/src/FSharp.Editor/Refactor/AddReturnType.fs index b3c586b445e..e18eb2b33be 100644 --- a/vsintegration/src/FSharp.Editor/Refactor/AddReturnType.fs +++ b/vsintegration/src/FSharp.Editor/Refactor/AddReturnType.fs @@ -76,43 +76,49 @@ type internal AddReturnType [] () = override _.ComputeRefactoringsAsync context = cancellableTask { - let document = context.Document - let position = context.Span.Start - let! sourceText = document.GetTextAsync() - let textLine = sourceText.Lines.GetLineFromPosition position - let textLinePos = sourceText.Lines.GetLinePosition position - let fcsTextLineNumber = Line.fromZ textLinePos.Line - - let! lexerSymbol = - document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, nameof (AddReturnType)) - - let! (parseFileResults, checkFileResults) = document.GetFSharpParseAndCheckResultsAsync(nameof (AddReturnType)) - - let symbolUseOpt = - lexerSymbol - |> Option.bind (fun lexer -> - checkFileResults.GetSymbolUseAtLocation( - fcsTextLineNumber, - lexer.Ident.idRange.EndColumn, - textLine.ToString(), - lexer.FullIsland - )) - - let memberFuncOpt = - symbolUseOpt - |> Option.bind (fun sym -> sym.Symbol |> AddReturnType.ofFSharpMemberOrFunctionOrValue) - - match (symbolUseOpt, memberFuncOpt) with - | (Some symbolUse, Some memberFunc) -> - let isValidMethod = - memberFunc - |> AddReturnType.isValidMethodWithoutTypeAnnotation symbolUse parseFileResults - - match isValidMethod with - | Some(memberFunc, typeRange) -> do AddReturnType.refactor context (memberFunc, typeRange, symbolUse) - | None -> () - | _ -> () - - return () + try + let document = context.Document + let position = context.Span.Start + let! sourceText = document.GetTextAsync() + let textLine = sourceText.Lines.GetLineFromPosition position + let textLinePos = sourceText.Lines.GetLinePosition position + let fcsTextLineNumber = Line.fromZ textLinePos.Line + + let! lexerSymbol = + document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, nameof (AddReturnType)) + + let! (parseFileResults, checkFileResults) = document.GetFSharpParseAndCheckResultsAsync(nameof (AddReturnType)) + + let symbolUseOpt = + lexerSymbol + |> Option.bind (fun lexer -> + checkFileResults.GetSymbolUseAtLocation( + fcsTextLineNumber, + lexer.Ident.idRange.EndColumn, + textLine.ToString(), + lexer.FullIsland + )) + + let memberFuncOpt = + symbolUseOpt + |> Option.bind (fun sym -> sym.Symbol |> AddReturnType.ofFSharpMemberOrFunctionOrValue) + + match (symbolUseOpt, memberFuncOpt) with + | (Some symbolUse, Some memberFunc) -> + let isValidMethod = + memberFunc + |> AddReturnType.isValidMethodWithoutTypeAnnotation symbolUse parseFileResults + + match isValidMethod with + | Some(memberFunc, typeRange) -> do AddReturnType.refactor context (memberFunc, typeRange, symbolUse) + | None -> () + | _ -> () + + return () + with _ -> + // File is not part of the project yet, or project options are not ready. + // This can happen when files are added/copied before the project system updates. + // Just return without offering any refactorings. + return () } |> CancellableTask.startAsTask context.CancellationToken diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddReturnTypeTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddReturnTypeTests.fs index b5d6ef23e41..9ce09af31aa 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddReturnTypeTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/AddReturnTypeTests.fs @@ -5,6 +5,10 @@ open Xunit open FSharp.Editor.Tests.Refactors.RefactorTestFramework open FSharp.Test.ProjectGeneration open FSharp.Editor.Tests.Helpers +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.CodeRefactorings +open Microsoft.CodeAnalysis.CodeActions [] [] @@ -479,3 +483,42 @@ let sum a b : MyType = {Value=a+b} let resultText = newDoc.GetTextAsync() |> GetTaskResult Assert.Equal(expectedCode.Trim(' ', '\r', '\n'), resultText.ToString().Trim(' ', '\r', '\n')) + +[] +let ``Should not throw when file is not properly configured`` () = + let symbolName = "sum" + + let code = + """ +let sum a b = a + b + """ + + use context = TestContext.CreateWithCode code + + let spanStart = code.IndexOf symbolName + + // Create a document that's not in the F# project options by adding it to solution + // but not to the project's source files list. This simulates the race condition + // where a file is copied but project options haven't been refreshed yet. + let project = context.Solution.Projects |> Seq.head + let newDocId = DocumentId.CreateNewId(project.Id) + + let newDoc = + context.Solution.AddDocument(newDocId, "NotInProject.fs", code, filePath = "C:\\NotInProject.fs") + + let documentNotInProject = newDoc.GetDocument(newDocId) + + let refactoringActions = new System.Collections.Generic.List() + + let refactoringContext = + CodeRefactoringContext(documentNotInProject, TextSpan(spanStart, 1), (fun a -> refactoringActions.Add a), context.CancellationToken) + + let refactorProvider = new AddReturnType() + + // This should not throw even though the document is not in the project options + let computeTask = refactorProvider.ComputeRefactoringsAsync refactoringContext + + computeTask.Wait(context.CancellationToken) + + // The test passes if no exception was thrown + Assert.True(true)