diff --git a/src/Compiler/Checking/CheckDeclarations.fs b/src/Compiler/Checking/CheckDeclarations.fs index 69e2c483ddf..1c0997851e4 100644 --- a/src/Compiler/Checking/CheckDeclarations.fs +++ b/src/Compiler/Checking/CheckDeclarations.fs @@ -178,7 +178,6 @@ module MutRecShapes = let iterTyconsWithEnv f1 env xs = iterWithEnv f1 (fun _env _x -> ()) (fun _env _x -> ()) (fun _env _x -> ()) env xs - /// Indicates a declaration is contained in the given module let ModuleOrNamespaceContainerInfo modref = ContainerInfo(Parent modref, Some(MemberOrValContainerInfo(modref, None, None, NoSafeInitInfo, []))) diff --git a/src/Compiler/SyntaxTree/LexFilter.fs b/src/Compiler/SyntaxTree/LexFilter.fs index 64bc43c6ebc..e592960c6ba 100644 --- a/src/Compiler/SyntaxTree/LexFilter.fs +++ b/src/Compiler/SyntaxTree/LexFilter.fs @@ -1172,6 +1172,11 @@ type LexFilterImpl ( delayToken (pool.UseShiftedLocation(tokenTup, INFIX_AT_HAT_OP "^", 1, 0)) delayToken (pool.UseShiftedLocation(tokenTup, LESS res, 0, -1)) pool.Return tokenTup + + | INFIX_COMPARE_OP ">:" -> + delayToken (pool.UseShiftedLocation(tokenTup, COLON, 1, 0)) + delayToken (pool.UseShiftedLocation(tokenTup, GREATER res, 0, -1)) + pool.Return tokenTup // NOTE: this is "<@" | LQUOTE ("<@ @>", false) -> delayToken (pool.UseShiftedLocation(tokenTup, INFIX_AT_HAT_OP "@", 1, 0)) diff --git a/src/Compiler/lex.fsl b/src/Compiler/lex.fsl index e8edd3086c1..b3ddfad4a13 100644 --- a/src/Compiler/lex.fsl +++ b/src/Compiler/lex.fsl @@ -112,6 +112,10 @@ let checkExprOp (lexbuf:UnicodeLexing.Lexbuf) = deprecatedWithError (FSComp.SR.lexCharNotAllowedInOperatorNames(":")) lexbuf.LexemeRange if lexbuf.LexemeContains '$' then deprecatedWithError (FSComp.SR.lexCharNotAllowedInOperatorNames("$")) lexbuf.LexemeRange + +let checkExprGreaterColonOp (lexbuf:UnicodeLexing.Lexbuf) = + if lexbuf.LexemeContains '$' then + deprecatedWithError (FSComp.SR.lexCharNotAllowedInOperatorNames("$")) lexbuf.LexemeRange let unexpectedChar lexbuf = LEX_FAILURE (FSComp.SR.lexUnexpectedChar(lexeme lexbuf)) @@ -945,7 +949,9 @@ rule token (args: LexArgs) (skip: bool) = parse | ignored_op_char* ('@'|'^') op_char* { checkExprOp lexbuf; INFIX_AT_HAT_OP(lexeme lexbuf) } - | ignored_op_char* ('=' | "!=" | '<' | '>' | '$') op_char* { checkExprOp lexbuf; INFIX_COMPARE_OP(lexeme lexbuf) } + | ignored_op_char* ('=' | "!=" | '<' | '$') op_char* { checkExprOp lexbuf; INFIX_COMPARE_OP(lexeme lexbuf) } + + | ignored_op_char* ('>') op_char* { checkExprGreaterColonOp lexbuf; INFIX_COMPARE_OP(lexeme lexbuf) } | ignored_op_char* ('&') op_char* { checkExprOp lexbuf; INFIX_AMP_OP(lexeme lexbuf) } diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/OperatorNames/BasicOperatorNames.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/OperatorNames/BasicOperatorNames.fs index 678198d625c..fa340382024 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/OperatorNames/BasicOperatorNames.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/OperatorNames/BasicOperatorNames.fs @@ -14,3 +14,5 @@ if !10 <> 3628800 then failwith "Failed: : 1" // Binary let (<<<) x y = x - x * y if 10 <<< 3 <> -20 then failwith "Failed: : 2" + +let (>:) x y = x + x * y diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/OperatorNames/OperatorNames.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/OperatorNames/OperatorNames.fs index e9cd52bafba..89b5d4c4ad0 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/OperatorNames/OperatorNames.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/BasicGrammarElements/OperatorNames/OperatorNames.fs @@ -73,4 +73,4 @@ module OperatorNames = |> asExe |> withOptions ["--warnaserror+"; "--nowarn:3370"; "--nowarn:988"] |> compileExeAndRun - |> shouldSucceed + |> shouldSucceed \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/ClassesTests.fs b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/ClassesTests.fs index a43e7eb2491..41a672132be 100644 --- a/tests/FSharp.Compiler.ComponentTests/ErrorMessages/ClassesTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/ErrorMessages/ClassesTests.fs @@ -686,4 +686,22 @@ type X = app |> withLangVersion80 |> compile + |> shouldSucceed + + [] + let ``No separator between member and type annotation`` () = + FSharp """ + type IFoo<'T> = + abstract member Bar<'T>: string -> unit + """ + |> typecheck + |> shouldSucceed + + [] + let ``Separator between member and type annotation`` () = + FSharp """ + type IFoo<'T> = + abstract member Bar<'T> : string -> unit + """ + |> typecheck |> shouldSucceed \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs new file mode 100644 index 00000000000..cb90d78a18e --- /dev/null +++ b/vsintegration/src/FSharp.Editor/CodeFixes/ImplementInterface.fs @@ -0,0 +1,297 @@ +// 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[] + } + +// state machine not statically compilable +// TODO: rewrite token arithmetics properly here +#nowarn "3511" + +[] +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 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` + 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 8e529cf0319..a58d115ba6f 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 7b9162b1f24..28cf54ccaca 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..77913f049cf --- /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 @@ +