From 3d508f5c76c147b8bb32bf9a5dac8f5edcb887a5 Mon Sep 17 00:00:00 2001 From: Petr Date: Mon, 18 Sep 2023 20:46:47 +0200 Subject: [PATCH 1/5] Improving ImplementInterface code fix --- .../CodeFixes/ImplementInterface.fs | 282 +++++++++++++++++ .../ImplementInterfaceCodeFixProvider.fs | 289 ------------------ .../src/FSharp.Editor/Common/Constants.fs | 3 + .../src/FSharp.Editor/FSharp.Editor.fsproj | 2 +- .../CodeFixes/ImplementInterfaceTests.fs | 129 ++++++++ .../FSharp.Editor.Tests.fsproj | 1 + 6 files changed, 416 insertions(+), 290 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs delete mode 100644 vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs create mode 100644 vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ImplementInterfaceTests.fs diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs new file mode 100644 index 00000000000..098b37a93c1 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Composition +open System.Collections.Immutable + +open Microsoft.CodeAnalysis.Formatting +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.CodeFixes + +open FSharp.Compiler +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Diagnostics +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text +open FSharp.Compiler.Tokenization + +open CancellableTasks + +[] +type internal InterfaceState = + { + InterfaceData: InterfaceData + EndPosOfWith: pos option + AppendBracketAt: int option + Tokens: Tokenizer.SavedTokenInfo[] + } + +[] +type internal ImplementInterfaceCodeFixProvider [] () = + inherit CodeFixProvider() + + let queryInterfaceState appendBracketAt (pos: pos) (tokens: Tokenizer.SavedTokenInfo[]) (ast: ParsedInput) = + let line = pos.Line - 1 + + InterfaceStubGenerator.TryFindInterfaceDeclaration pos ast + |> Option.map (fun iface -> + let endPosOfWidth = + tokens + |> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) -> + if t.Tag = FSharpTokenTag.WITH || t.Tag = FSharpTokenTag.OWITH then + Some(Position.fromZ line (t.RightColumn + 1)) + else + None) + + let appendBracketAt = + match iface, appendBracketAt with + | InterfaceData.ObjExpr _, Some _ -> appendBracketAt + | _ -> None + + { + InterfaceData = iface + EndPosOfWith = endPosOfWidth + AppendBracketAt = appendBracketAt + Tokens = tokens + }) + + let getLineIdent (lineStr: string) = + lineStr.Length - lineStr.TrimStart(' ').Length + + let inferStartColumn indentSize state (sourceText: SourceText) = + match InterfaceStubGenerator.GetMemberNameAndRanges state.InterfaceData with + | (_, range) :: _ -> + let lineStr = sourceText.Lines[ range.StartLine - 1 ].ToString() + getLineIdent lineStr + | [] -> + match state.InterfaceData with + | InterfaceData.Interface _ as iface -> + // 'interface ISomething with' is often in a new line, we use the indentation of that line + let lineStr = sourceText.Lines[ iface.Range.StartLine - 1 ].ToString() + getLineIdent lineStr + indentSize + | InterfaceData.ObjExpr _ as iface -> + state.Tokens + |> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) -> + if t.Tag = FSharpTokenTag.NEW then + Some(t.LeftColumn + indentSize) + else + None) + // There is no reference point, we indent the content at the start column of the interface + |> Option.defaultValue iface.Range.StartColumn + + let getChanges (sourceText: SourceText) state displayContext implementedMemberSignatures entity indentSize verboseMode = + let startColumn = inferStartColumn indentSize state sourceText + let objectIdentifier = "this" + let defaultBody = "raise (System.NotImplementedException())" + let typeParams = state.InterfaceData.TypeParameters + + let stub = + let stub = + InterfaceStubGenerator.FormatInterface + startColumn + indentSize + typeParams + objectIdentifier + defaultBody + displayContext + implementedMemberSignatures + entity + verboseMode + + stub.TrimEnd(Environment.NewLine.ToCharArray()) + + let stubChange = + match state.EndPosOfWith with + | Some pos -> + let currentPos = sourceText.Lines[pos.Line - 1].Start + pos.Column + TextChange(TextSpan(currentPos, 0), stub) + | None -> + let range = state.InterfaceData.Range + let currentPos = sourceText.Lines[range.EndLine - 1].Start + range.EndColumn + TextChange(TextSpan(currentPos, 0), " with" + stub) + + match state.AppendBracketAt with + | Some index -> [ stubChange; TextChange(TextSpan(index, 0), " }") ] + | None -> [ stubChange ] + + let getSuggestions + ( + sourceText: SourceText, + results: FSharpCheckFileResults, + state: InterfaceState, + displayContext, + entity, + indentSize + ) = + if InterfaceStubGenerator.HasNoInterfaceMember entity then + CancellableTask.singleton Seq.empty + else + let membersAndRanges = + InterfaceStubGenerator.GetMemberNameAndRanges state.InterfaceData + + let interfaceMembers = InterfaceStubGenerator.GetInterfaceMembers entity + + let hasTypeCheckError = + results.Diagnostics + |> Array.exists (fun e -> e.Severity = FSharpDiagnosticSeverity.Error) + // This comparison is a bit expensive + if hasTypeCheckError && List.length membersAndRanges <> Seq.length interfaceMembers then + + let getMemberByLocation (name, range: range) = + let lineStr = sourceText.Lines[ range.EndLine - 1 ].ToString() + results.GetSymbolUseAtLocation(range.EndLine, range.EndColumn, lineStr, [ name ]) + + cancellableTask { + let! implementedMemberSignatures = + InterfaceStubGenerator.GetImplementedMemberSignatures getMemberByLocation displayContext state.InterfaceData + + let getCodeFix title verboseMode = + let changes = + getChanges sourceText state displayContext implementedMemberSignatures entity indentSize verboseMode + + { + Name = CodeFix.ImplementInterface + Message = title + Changes = changes + } + + return + seq { + getCodeFix (SR.ImplementInterface()) true + getCodeFix (SR.ImplementInterfaceWithoutTypeAnnotation()) false + } + } + + else + CancellableTask.singleton Seq.empty + + override _.FixableDiagnosticIds = ImmutableArray.Create "FS0366" + + override this.RegisterCodeFixesAsync context = context.RegisterFsharpFixes this + + interface IFSharpMultiCodeFixProvider with + member _.GetCodeFixesAsync context = + cancellableTask { + let! cancellationToken = CancellableTask.getCancellationToken() + let! parseResults, checkFileResults = + context.Document.GetFSharpParseAndCheckResultsAsync(nameof ImplementInterfaceCodeFixProvider) + + let! sourceText = context.GetSourceTextAsync() + + let textLine = sourceText.Lines.GetLineFromPosition context.Span.Start + + let! _, _, parsingOptions, _ = context.Document.GetFSharpCompilationOptionsAsync(nameof ImplementInterfaceCodeFixProvider) + + let defines = CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions + let langVersionOpt = Some parsingOptions.LangVersionText + // Notice that context.Span doesn't return reliable ranges to find tokens at exact positions. + // That's why we tokenize the line and try to find the last successive identifier token + let tokens = + Tokenizer.tokenizeLine ( + context.Document.Id, + sourceText, + context.Span.Start, + context.Document.FilePath, + defines, + langVersionOpt, + parsingOptions.StrictIndentation, + cancellationToken + ) + + let startLeftColumn = context.Span.Start - textLine.Start + + let rec tryFindIdentifierToken acc i = + if i >= tokens.Length then + acc + else + match tokens[i] with + | t when t.LeftColumn < startLeftColumn -> + // Skip all the tokens starting before the context + tryFindIdentifierToken acc (i + 1) + | t when t.Tag = FSharpTokenTag.Identifier -> tryFindIdentifierToken (Some t) (i + 1) + | t when t.Tag = FSharpTokenTag.DOT || Option.isNone acc -> tryFindIdentifierToken acc (i + 1) + | _ -> acc + + let token = tryFindIdentifierToken None 0 + match token with + | None -> return Seq.empty + | Some token -> + let fixupPosition = textLine.Start + token.RightColumn + let interfacePos = Position.fromZ textLine.LineNumber token.RightColumn + // We rely on the observation that the lastChar of the context should be '}' if that character is present + let appendBracketAt = + match sourceText[context.Span.End - 1] with + | '}' -> None + | _ -> Some context.Span.End + + let interfaceState = + queryInterfaceState appendBracketAt interfacePos tokens parseResults.ParseTree + + match interfaceState with + | None -> return Seq.empty + | Some interfaceState -> + let symbol = + Tokenizer.getSymbolAtPosition ( + context.Document.Id, + sourceText, + fixupPosition, + context.Document.FilePath, + defines, + SymbolLookupKind.Greedy, + false, + false, + langVersionOpt, + parsingOptions.StrictIndentation, + cancellationToken + ) + + match symbol with + | None -> return Seq.empty + | Some symbol -> + let fcsTextLineNumber = textLine.LineNumber + 1 + let lineContents = textLine.ToString() + let! options = context.Document.GetOptionsAsync(cancellationToken) + + let tabSize = + options.GetOption(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName) + + let symbolUse = + checkFileResults.GetSymbolUseAtLocation( + fcsTextLineNumber, + symbol.Ident.idRange.EndColumn, + lineContents, + symbol.FullIsland + ) + + match symbolUse with + | None -> return Seq.empty + | Some symbolUse -> + match symbolUse.Symbol with + | :? FSharpEntity as entity + // Things get complicated with interface inheritance: https://github.com/dotnet/fsharp/issues/5813 + // With enough enthusiasm this probably can be handled though, + // in that case change the check to `InterfaceStubGenerator.IsInterface entity` + when entity.AllInterfaces.Count = 1 -> + + return! getSuggestions (sourceText, checkFileResults, interfaceState, symbolUse.DisplayContext, entity, tabSize) + | _ -> return Seq.empty + } diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs deleted file mode 100644 index 922543a3b71..00000000000 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. - -namespace Microsoft.VisualStudio.FSharp.Editor - -open System -open System.Composition -open System.Threading -open System.Threading.Tasks -open System.Collections.Immutable - -open Microsoft.CodeAnalysis.Formatting -open Microsoft.CodeAnalysis.Text -open Microsoft.CodeAnalysis.CodeFixes -open Microsoft.CodeAnalysis.CodeActions - -open FSharp.Compiler -open FSharp.Compiler.CodeAnalysis -open FSharp.Compiler.Diagnostics -open FSharp.Compiler.EditorServices -open FSharp.Compiler.Symbols -open FSharp.Compiler.Syntax -open FSharp.Compiler.Text -open FSharp.Compiler.Tokenization -open CancellableTasks - -[] -type internal InterfaceState = - { - InterfaceData: InterfaceData - EndPosOfWith: pos option - AppendBracketAt: int option - Tokens: Tokenizer.SavedTokenInfo[] - } - -[] -type internal ImplementInterfaceCodeFixProvider [] () = - inherit CodeFixProvider() - - let queryInterfaceState appendBracketAt (pos: pos) (tokens: Tokenizer.SavedTokenInfo[]) (ast: ParsedInput) = - asyncMaybe { - let line = pos.Line - 1 - let! iface = InterfaceStubGenerator.TryFindInterfaceDeclaration pos ast - - let endPosOfWidth = - tokens - |> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) -> - if t.Tag = FSharpTokenTag.WITH || t.Tag = FSharpTokenTag.OWITH then - Some(Position.fromZ line (t.RightColumn + 1)) - else - None) - - let appendBracketAt = - match iface, appendBracketAt with - | InterfaceData.ObjExpr _, Some _ -> appendBracketAt - | _ -> None - - return - { - InterfaceData = iface - EndPosOfWith = endPosOfWidth - AppendBracketAt = appendBracketAt - Tokens = tokens - } - } - - let getLineIdent (lineStr: string) = - lineStr.Length - lineStr.TrimStart(' ').Length - - let inferStartColumn indentSize state (sourceText: SourceText) = - match InterfaceStubGenerator.GetMemberNameAndRanges state.InterfaceData with - | (_, range) :: _ -> - let lineStr = sourceText.Lines.[range.StartLine - 1].ToString() - getLineIdent lineStr - | [] -> - match state.InterfaceData with - | InterfaceData.Interface _ as iface -> - // 'interface ISomething with' is often in a new line, we use the indentation of that line - let lineStr = sourceText.Lines.[iface.Range.StartLine - 1].ToString() - getLineIdent lineStr + indentSize - | InterfaceData.ObjExpr _ as iface -> - state.Tokens - |> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) -> - if t.Tag = FSharpTokenTag.NEW then - Some(t.LeftColumn + indentSize) - else - None) - // There is no reference point, we indent the content at the start column of the interface - |> Option.defaultValue iface.Range.StartColumn - - let applyImplementInterface (sourceText: SourceText) state displayContext implementedMemberSignatures entity indentSize verboseMode = - let startColumn = inferStartColumn indentSize state sourceText - let objectIdentifier = "this" - let defaultBody = "raise (System.NotImplementedException())" - let typeParams = state.InterfaceData.TypeParameters - - let stub = - let stub = - InterfaceStubGenerator.FormatInterface - startColumn - indentSize - typeParams - objectIdentifier - defaultBody - displayContext - implementedMemberSignatures - entity - verboseMode - - stub.TrimEnd(Environment.NewLine.ToCharArray()) - - let stubChange = - match state.EndPosOfWith with - | Some pos -> - let currentPos = sourceText.Lines.[pos.Line - 1].Start + pos.Column - TextChange(TextSpan(currentPos, 0), stub) - | None -> - let range = state.InterfaceData.Range - let currentPos = sourceText.Lines.[range.EndLine - 1].Start + range.EndColumn - TextChange(TextSpan(currentPos, 0), " with" + stub) - - match state.AppendBracketAt with - | Some index -> sourceText.WithChanges(stubChange, TextChange(TextSpan(index, 0), " }")) - | None -> sourceText.WithChanges(stubChange) - - let registerSuggestions - ( - context: CodeFixContext, - results: FSharpCheckFileResults, - state: InterfaceState, - displayContext, - entity, - indentSize - ) = - if InterfaceStubGenerator.HasNoInterfaceMember entity then - () - else - let membersAndRanges = - InterfaceStubGenerator.GetMemberNameAndRanges state.InterfaceData - - let interfaceMembers = InterfaceStubGenerator.GetInterfaceMembers entity - - let hasTypeCheckError = - results.Diagnostics - |> Array.exists (fun e -> e.Severity = FSharpDiagnosticSeverity.Error) - // This comparison is a bit expensive - if hasTypeCheckError && List.length membersAndRanges <> Seq.length interfaceMembers then - - let registerCodeFix title verboseMode = - let codeAction = - CodeAction.Create( - title, - (fun (cancellationToken: CancellationToken) -> - async { - let! sourceText = context.Document.GetTextAsync(cancellationToken) |> Async.AwaitTask - - let getMemberByLocation (name, range: range) = - let lineStr = sourceText.Lines.[range.EndLine - 1].ToString() - results.GetSymbolUseAtLocation(range.EndLine, range.EndColumn, lineStr, [ name ]) - - let! implementedMemberSignatures = - InterfaceStubGenerator.GetImplementedMemberSignatures - getMemberByLocation - displayContext - state.InterfaceData - - let newSourceText = - applyImplementInterface - sourceText - state - displayContext - implementedMemberSignatures - entity - indentSize - verboseMode - - return context.Document.WithText(newSourceText) - } - |> RoslynHelpers.StartAsyncAsTask(cancellationToken)), - title - ) - - context.RegisterCodeFix(codeAction, context.Diagnostics) - - registerCodeFix (SR.ImplementInterface()) true - registerCodeFix (SR.ImplementInterfaceWithoutTypeAnnotation()) false - else - () - - override _.FixableDiagnosticIds = ImmutableArray.Create("FS0366") - - override _.RegisterCodeFixesAsync context : Task = - asyncMaybe { - let! ct = Async.CancellationToken |> liftAsync - - let! parseResults, checkFileResults = - context.Document.GetFSharpParseAndCheckResultsAsync(nameof (ImplementInterfaceCodeFixProvider)) - |> CancellableTask.start ct - |> Async.AwaitTask - |> liftAsync - - let cancellationToken = context.CancellationToken - let! sourceText = context.Document.GetTextAsync(cancellationToken) - let textLine = sourceText.Lines.GetLineFromPosition context.Span.Start - - let! _, _, parsingOptions, _ = - context.Document.GetFSharpCompilationOptionsAsync(nameof (ImplementInterfaceCodeFixProvider)) - |> CancellableTask.start ct - |> Async.AwaitTask - |> liftAsync - - let defines = CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions - let langVersionOpt = Some parsingOptions.LangVersionText - // Notice that context.Span doesn't return reliable ranges to find tokens at exact positions. - // That's why we tokenize the line and try to find the last successive identifier token - let tokens = - Tokenizer.tokenizeLine ( - context.Document.Id, - sourceText, - context.Span.Start, - context.Document.FilePath, - defines, - langVersionOpt, - parsingOptions.StrictIndentation, - context.CancellationToken - ) - - let startLeftColumn = context.Span.Start - textLine.Start - - let rec tryFindIdentifierToken acc i = - if i >= tokens.Length then - acc - else - match tokens.[i] with - | t when t.LeftColumn < startLeftColumn -> - // Skip all the tokens starting before the context - tryFindIdentifierToken acc (i + 1) - | t when t.Tag = FSharpTokenTag.Identifier -> tryFindIdentifierToken (Some t) (i + 1) - | t when t.Tag = FSharpTokenTag.DOT || Option.isNone acc -> tryFindIdentifierToken acc (i + 1) - | _ -> acc - - let! token = tryFindIdentifierToken None 0 - let fixupPosition = textLine.Start + token.RightColumn - let interfacePos = Position.fromZ textLine.LineNumber token.RightColumn - // We rely on the observation that the lastChar of the context should be '}' if that character is present - let appendBracketAt = - match sourceText.[context.Span.End - 1] with - | '}' -> None - | _ -> Some context.Span.End - - let! interfaceState = queryInterfaceState appendBracketAt interfacePos tokens parseResults.ParseTree - - let! symbol = - Tokenizer.getSymbolAtPosition ( - context.Document.Id, - sourceText, - fixupPosition, - context.Document.FilePath, - defines, - SymbolLookupKind.Greedy, - false, - false, - langVersionOpt, - parsingOptions.StrictIndentation, - context.CancellationToken - ) - - let fcsTextLineNumber = textLine.LineNumber + 1 - let lineContents = textLine.ToString() - let! options = context.Document.GetOptionsAsync(cancellationToken) - - let tabSize = - options.GetOption(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName) - - let! symbolUse = - checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.Ident.idRange.EndColumn, lineContents, symbol.FullIsland) - - let! entity, displayContext = - match symbolUse.Symbol with - | :? FSharpEntity as entity -> - if InterfaceStubGenerator.IsInterface entity then - Some(entity, symbolUse.DisplayContext) - else - None - | _ -> None - - registerSuggestions (context, checkFileResults, interfaceState, displayContext, entity, tabSize) - } - |> Async.Ignore - |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/Common/Constants.fs b/vsintegration/src/FSharp.Editor/Common/Constants.fs index d59b241b44e..b29bd3c49a8 100644 --- a/vsintegration/src/FSharp.Editor/Common/Constants.fs +++ b/vsintegration/src/FSharp.Editor/Common/Constants.fs @@ -147,6 +147,9 @@ module internal CodeFix = [] let FixIndexerAccess = "FixIndexerAccess" + [] + let ImplementInterface = "ImplementInterface" + [] let RemoveReturnOrYield = "RemoveReturnOrYield" diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index b73a33b690d..4e9cff37dfa 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -130,7 +130,7 @@ - + diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ImplementInterfaceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ImplementInterfaceTests.fs new file mode 100644 index 00000000000..f134fbb0602 --- /dev/null +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ImplementInterfaceTests.fs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module FSharp.Editor.Tests.CodeFixes.ImplementInterfaceTests + +open Microsoft.VisualStudio.FSharp.Editor +open Xunit + +open CodeFixTestFramework + +let private codeFix = ImplementInterfaceCodeFixProvider() + +[] +[] +[] +let ``Fixes FS0366`` optionalWith = + let code = + $""" +type IMyInterface = + abstract member MyMethod: unit -> unit + +type MyType() = + interface IMyInterface{optionalWith} +""" + + let expected = + [ + { + Message = "Implement interface" + FixedCode = + """ +type IMyInterface = + abstract member MyMethod: unit -> unit + +type MyType() = + interface IMyInterface with + member this.MyMethod(): unit = + raise (System.NotImplementedException()) +""" + } + { + Message = "Implement interface without type annotation" + FixedCode = + """ +type IMyInterface = + abstract member MyMethod: unit -> unit + +type MyType() = + interface IMyInterface with + member this.MyMethod() = raise (System.NotImplementedException()) +""" + } + ] + + let actual = codeFix |> multiFix code Auto + + Assert.Equal(expected, actual) + +[] +let ``Fixes FS0366 for partially implemented interfaces``() = + let code = + $""" +type IMyInterface = + abstract member MyMethod1 : unit -> unit + abstract member MyMethod2 : unit -> unit + +type MyType() = + interface IMyInterface with + member this.MyMethod2(): unit = () +""" + + let expected = + [ + { + Message = "Implement interface" + FixedCode = + """ +type IMyInterface = + abstract member MyMethod1 : unit -> unit + abstract member MyMethod2 : unit -> unit + +type MyType() = + interface IMyInterface with + member this.MyMethod1(): unit = + raise (System.NotImplementedException()) + member this.MyMethod2(): unit = () +""" + } + { + Message = "Implement interface without type annotation" + FixedCode = + """ +type IMyInterface = + abstract member MyMethod1 : unit -> unit + abstract member MyMethod2 : unit -> unit + +type MyType() = + interface IMyInterface with + member this.MyMethod1() = raise (System.NotImplementedException()) + member this.MyMethod2(): unit = () +""" + } + ] + + let actual = codeFix |> multiFix code Auto + + Assert.Equal(expected, actual) + +[] +let ``Doesn't handle FS0036 for inherited interfaces``() = + let code = + $""" +type IMyInterface1 = + abstract member MyMethod1 : unit -> unit + +type IMyInterface2 = + inherit IMyInterface1 + abstract member MyMethod2 : unit -> unit + +type MyType () = + interface IMyInterface1 with + member this.MyMethod1 () = () + interface IMyInterface2 with +""" + + let expected = [] + + let actual = codeFix |> multiFix code Auto + + Assert.Equal(expected, actual) diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj index e8d25a75fb9..9831d2e6c0b 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj +++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj @@ -65,6 +65,7 @@ + From 252b751aa15340cc28cd0a61193bdaf5b4502437 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 22 Sep 2023 14:09:08 +0200 Subject: [PATCH 2/5] temp rename back --- ...plementInterface.fs => ImplementInterfaceCodeFixProvider.fs} | 0 vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename vsintegration/src/FSharp.Editor/CodeFixes/{ImplementInterface.fs => ImplementInterfaceCodeFixProvider.fs} (100%) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs similarity index 100% rename from vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs rename to vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 4e9cff37dfa..b73a33b690d 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -130,7 +130,7 @@ - + From 1fd3876895ea85ac2239df82132643a69ec4e979 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 22 Sep 2023 14:27:12 +0200 Subject: [PATCH 3/5] ftms --- .../ImplementInterfaceCodeFixProvider.fs | 21 ++++++++++++++----- .../CodeFixes/ImplementInterfaceTests.fs | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs index 098b37a93c1..632b4f94f1b 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs @@ -176,7 +176,8 @@ type internal ImplementInterfaceCodeFixProvider [] () = interface IFSharpMultiCodeFixProvider with member _.GetCodeFixesAsync context = cancellableTask { - let! cancellationToken = CancellableTask.getCancellationToken() + let! cancellationToken = CancellableTask.getCancellationToken () + let! parseResults, checkFileResults = context.Document.GetFSharpParseAndCheckResultsAsync(nameof ImplementInterfaceCodeFixProvider) @@ -217,6 +218,7 @@ type internal ImplementInterfaceCodeFixProvider [] () = | _ -> acc let token = tryFindIdentifierToken None 0 + match token with | None -> return Seq.empty | Some token -> @@ -271,12 +273,21 @@ type internal ImplementInterfaceCodeFixProvider [] () = | None -> return Seq.empty | Some symbolUse -> match symbolUse.Symbol with - | :? FSharpEntity as entity + | :? FSharpEntity as entity when // Things get complicated with interface inheritance: https://github.com/dotnet/fsharp/issues/5813 // With enough enthusiasm this probably can be handled though, // in that case change the check to `InterfaceStubGenerator.IsInterface entity` - when entity.AllInterfaces.Count = 1 -> - - return! getSuggestions (sourceText, checkFileResults, interfaceState, symbolUse.DisplayContext, entity, tabSize) + entity.AllInterfaces.Count = 1 + -> + + return! + getSuggestions ( + sourceText, + checkFileResults, + interfaceState, + symbolUse.DisplayContext, + entity, + tabSize + ) | _ -> return Seq.empty } diff --git a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ImplementInterfaceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ImplementInterfaceTests.fs index f134fbb0602..77913f049cf 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ImplementInterfaceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/CodeFixes/ImplementInterfaceTests.fs @@ -56,7 +56,7 @@ type MyType() = Assert.Equal(expected, actual) [] -let ``Fixes FS0366 for partially implemented interfaces``() = +let ``Fixes FS0366 for partially implemented interfaces`` () = let code = $""" type IMyInterface = @@ -106,7 +106,7 @@ type MyType() = Assert.Equal(expected, actual) [] -let ``Doesn't handle FS0036 for inherited interfaces``() = +let ``Doesn't handle FS0036 for inherited interfaces`` () = let code = $""" type IMyInterface1 = From 623768f55e04dd43cf4c72bccb146bb94c85106b Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 22 Sep 2023 15:39:30 +0200 Subject: [PATCH 4/5] up up --- .../CodeFixes/ImplementInterfaceCodeFixProvider.fs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs index 632b4f94f1b..cb90d78a18e 100644 --- a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs @@ -30,6 +30,10 @@ type internal InterfaceState = Tokens: Tokenizer.SavedTokenInfo[] } +// state machine not statically compilable +// TODO: rewrite token arithmetics properly here +#nowarn "3511" + [] type internal ImplementInterfaceCodeFixProvider [] () = inherit CodeFixProvider() From 140ab3be9b8cbd0108b7bdac5a0c6a552c21e61a Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 12 Oct 2023 14:20:09 +0200 Subject: [PATCH 5/5] rename --- ...plementInterfaceCodeFixProvider.fs => ImplementInterface.fs} | 0 vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename vsintegration/src/FSharp.Editor/CodeFixes/{ImplementInterfaceCodeFixProvider.fs => ImplementInterface.fs} (100%) diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs similarity index 100% rename from vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterfaceCodeFixProvider.fs rename to vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index b73a33b690d..4e9cff37dfa 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -130,7 +130,7 @@ - +