Skip to content

Commit 4061c38

Browse files
authored
Improving ImplementInterface code fix (#16019)
1 parent 2550537 commit 4061c38

File tree

6 files changed

+431
-290
lines changed

6 files changed

+431
-290
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
namespace Microsoft.VisualStudio.FSharp.Editor
4+
5+
open System
6+
open System.Composition
7+
open System.Collections.Immutable
8+
9+
open Microsoft.CodeAnalysis.Formatting
10+
open Microsoft.CodeAnalysis.Text
11+
open Microsoft.CodeAnalysis.CodeFixes
12+
13+
open FSharp.Compiler
14+
open FSharp.Compiler.CodeAnalysis
15+
open FSharp.Compiler.Diagnostics
16+
open FSharp.Compiler.EditorServices
17+
open FSharp.Compiler.Symbols
18+
open FSharp.Compiler.Syntax
19+
open FSharp.Compiler.Text
20+
open FSharp.Compiler.Tokenization
21+
22+
open CancellableTasks
23+
24+
[<NoEquality; NoComparison>]
25+
type internal InterfaceState =
26+
{
27+
InterfaceData: InterfaceData
28+
EndPosOfWith: pos option
29+
AppendBracketAt: int option
30+
Tokens: Tokenizer.SavedTokenInfo[]
31+
}
32+
33+
// state machine not statically compilable
34+
// TODO: rewrite token arithmetics properly here
35+
#nowarn "3511"
36+
37+
[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.ImplementInterface); Shared>]
38+
type internal ImplementInterfaceCodeFixProvider [<ImportingConstructor>] () =
39+
inherit CodeFixProvider()
40+
41+
let queryInterfaceState appendBracketAt (pos: pos) (tokens: Tokenizer.SavedTokenInfo[]) (ast: ParsedInput) =
42+
let line = pos.Line - 1
43+
44+
InterfaceStubGenerator.TryFindInterfaceDeclaration pos ast
45+
|> Option.map (fun iface ->
46+
let endPosOfWidth =
47+
tokens
48+
|> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) ->
49+
if t.Tag = FSharpTokenTag.WITH || t.Tag = FSharpTokenTag.OWITH then
50+
Some(Position.fromZ line (t.RightColumn + 1))
51+
else
52+
None)
53+
54+
let appendBracketAt =
55+
match iface, appendBracketAt with
56+
| InterfaceData.ObjExpr _, Some _ -> appendBracketAt
57+
| _ -> None
58+
59+
{
60+
InterfaceData = iface
61+
EndPosOfWith = endPosOfWidth
62+
AppendBracketAt = appendBracketAt
63+
Tokens = tokens
64+
})
65+
66+
let getLineIdent (lineStr: string) =
67+
lineStr.Length - lineStr.TrimStart(' ').Length
68+
69+
let inferStartColumn indentSize state (sourceText: SourceText) =
70+
match InterfaceStubGenerator.GetMemberNameAndRanges state.InterfaceData with
71+
| (_, range) :: _ ->
72+
let lineStr = sourceText.Lines[ range.StartLine - 1 ].ToString()
73+
getLineIdent lineStr
74+
| [] ->
75+
match state.InterfaceData with
76+
| InterfaceData.Interface _ as iface ->
77+
// 'interface ISomething with' is often in a new line, we use the indentation of that line
78+
let lineStr = sourceText.Lines[ iface.Range.StartLine - 1 ].ToString()
79+
getLineIdent lineStr + indentSize
80+
| InterfaceData.ObjExpr _ as iface ->
81+
state.Tokens
82+
|> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) ->
83+
if t.Tag = FSharpTokenTag.NEW then
84+
Some(t.LeftColumn + indentSize)
85+
else
86+
None)
87+
// There is no reference point, we indent the content at the start column of the interface
88+
|> Option.defaultValue iface.Range.StartColumn
89+
90+
let getChanges (sourceText: SourceText) state displayContext implementedMemberSignatures entity indentSize verboseMode =
91+
let startColumn = inferStartColumn indentSize state sourceText
92+
let objectIdentifier = "this"
93+
let defaultBody = "raise (System.NotImplementedException())"
94+
let typeParams = state.InterfaceData.TypeParameters
95+
96+
let stub =
97+
let stub =
98+
InterfaceStubGenerator.FormatInterface
99+
startColumn
100+
indentSize
101+
typeParams
102+
objectIdentifier
103+
defaultBody
104+
displayContext
105+
implementedMemberSignatures
106+
entity
107+
verboseMode
108+
109+
stub.TrimEnd(Environment.NewLine.ToCharArray())
110+
111+
let stubChange =
112+
match state.EndPosOfWith with
113+
| Some pos ->
114+
let currentPos = sourceText.Lines[pos.Line - 1].Start + pos.Column
115+
TextChange(TextSpan(currentPos, 0), stub)
116+
| None ->
117+
let range = state.InterfaceData.Range
118+
let currentPos = sourceText.Lines[range.EndLine - 1].Start + range.EndColumn
119+
TextChange(TextSpan(currentPos, 0), " with" + stub)
120+
121+
match state.AppendBracketAt with
122+
| Some index -> [ stubChange; TextChange(TextSpan(index, 0), " }") ]
123+
| None -> [ stubChange ]
124+
125+
let getSuggestions
126+
(
127+
sourceText: SourceText,
128+
results: FSharpCheckFileResults,
129+
state: InterfaceState,
130+
displayContext,
131+
entity,
132+
indentSize
133+
) =
134+
if InterfaceStubGenerator.HasNoInterfaceMember entity then
135+
CancellableTask.singleton Seq.empty
136+
else
137+
let membersAndRanges =
138+
InterfaceStubGenerator.GetMemberNameAndRanges state.InterfaceData
139+
140+
let interfaceMembers = InterfaceStubGenerator.GetInterfaceMembers entity
141+
142+
let hasTypeCheckError =
143+
results.Diagnostics
144+
|> Array.exists (fun e -> e.Severity = FSharpDiagnosticSeverity.Error)
145+
// This comparison is a bit expensive
146+
if hasTypeCheckError && List.length membersAndRanges <> Seq.length interfaceMembers then
147+
148+
let getMemberByLocation (name, range: range) =
149+
let lineStr = sourceText.Lines[ range.EndLine - 1 ].ToString()
150+
results.GetSymbolUseAtLocation(range.EndLine, range.EndColumn, lineStr, [ name ])
151+
152+
cancellableTask {
153+
let! implementedMemberSignatures =
154+
InterfaceStubGenerator.GetImplementedMemberSignatures getMemberByLocation displayContext state.InterfaceData
155+
156+
let getCodeFix title verboseMode =
157+
let changes =
158+
getChanges sourceText state displayContext implementedMemberSignatures entity indentSize verboseMode
159+
160+
{
161+
Name = CodeFix.ImplementInterface
162+
Message = title
163+
Changes = changes
164+
}
165+
166+
return
167+
seq {
168+
getCodeFix (SR.ImplementInterface()) true
169+
getCodeFix (SR.ImplementInterfaceWithoutTypeAnnotation()) false
170+
}
171+
}
172+
173+
else
174+
CancellableTask.singleton Seq.empty
175+
176+
override _.FixableDiagnosticIds = ImmutableArray.Create "FS0366"
177+
178+
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFixes this
179+
180+
interface IFSharpMultiCodeFixProvider with
181+
member _.GetCodeFixesAsync context =
182+
cancellableTask {
183+
let! cancellationToken = CancellableTask.getCancellationToken ()
184+
185+
let! parseResults, checkFileResults =
186+
context.Document.GetFSharpParseAndCheckResultsAsync(nameof ImplementInterfaceCodeFixProvider)
187+
188+
let! sourceText = context.GetSourceTextAsync()
189+
190+
let textLine = sourceText.Lines.GetLineFromPosition context.Span.Start
191+
192+
let! _, _, parsingOptions, _ = context.Document.GetFSharpCompilationOptionsAsync(nameof ImplementInterfaceCodeFixProvider)
193+
194+
let defines = CompilerEnvironment.GetConditionalDefinesForEditing parsingOptions
195+
let langVersionOpt = Some parsingOptions.LangVersionText
196+
// Notice that context.Span doesn't return reliable ranges to find tokens at exact positions.
197+
// That's why we tokenize the line and try to find the last successive identifier token
198+
let tokens =
199+
Tokenizer.tokenizeLine (
200+
context.Document.Id,
201+
sourceText,
202+
context.Span.Start,
203+
context.Document.FilePath,
204+
defines,
205+
langVersionOpt,
206+
parsingOptions.StrictIndentation,
207+
cancellationToken
208+
)
209+
210+
let startLeftColumn = context.Span.Start - textLine.Start
211+
212+
let rec tryFindIdentifierToken acc i =
213+
if i >= tokens.Length then
214+
acc
215+
else
216+
match tokens[i] with
217+
| t when t.LeftColumn < startLeftColumn ->
218+
// Skip all the tokens starting before the context
219+
tryFindIdentifierToken acc (i + 1)
220+
| t when t.Tag = FSharpTokenTag.Identifier -> tryFindIdentifierToken (Some t) (i + 1)
221+
| t when t.Tag = FSharpTokenTag.DOT || Option.isNone acc -> tryFindIdentifierToken acc (i + 1)
222+
| _ -> acc
223+
224+
let token = tryFindIdentifierToken None 0
225+
226+
match token with
227+
| None -> return Seq.empty
228+
| Some token ->
229+
let fixupPosition = textLine.Start + token.RightColumn
230+
let interfacePos = Position.fromZ textLine.LineNumber token.RightColumn
231+
// We rely on the observation that the lastChar of the context should be '}' if that character is present
232+
let appendBracketAt =
233+
match sourceText[context.Span.End - 1] with
234+
| '}' -> None
235+
| _ -> Some context.Span.End
236+
237+
let interfaceState =
238+
queryInterfaceState appendBracketAt interfacePos tokens parseResults.ParseTree
239+
240+
match interfaceState with
241+
| None -> return Seq.empty
242+
| Some interfaceState ->
243+
let symbol =
244+
Tokenizer.getSymbolAtPosition (
245+
context.Document.Id,
246+
sourceText,
247+
fixupPosition,
248+
context.Document.FilePath,
249+
defines,
250+
SymbolLookupKind.Greedy,
251+
false,
252+
false,
253+
langVersionOpt,
254+
parsingOptions.StrictIndentation,
255+
cancellationToken
256+
)
257+
258+
match symbol with
259+
| None -> return Seq.empty
260+
| Some symbol ->
261+
let fcsTextLineNumber = textLine.LineNumber + 1
262+
let lineContents = textLine.ToString()
263+
let! options = context.Document.GetOptionsAsync(cancellationToken)
264+
265+
let tabSize =
266+
options.GetOption(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName)
267+
268+
let symbolUse =
269+
checkFileResults.GetSymbolUseAtLocation(
270+
fcsTextLineNumber,
271+
symbol.Ident.idRange.EndColumn,
272+
lineContents,
273+
symbol.FullIsland
274+
)
275+
276+
match symbolUse with
277+
| None -> return Seq.empty
278+
| Some symbolUse ->
279+
match symbolUse.Symbol with
280+
| :? FSharpEntity as entity when
281+
// Things get complicated with interface inheritance: https://github.com/dotnet/fsharp/issues/5813
282+
// With enough enthusiasm this probably can be handled though,
283+
// in that case change the check to `InterfaceStubGenerator.IsInterface entity`
284+
entity.AllInterfaces.Count = 1
285+
->
286+
287+
return!
288+
getSuggestions (
289+
sourceText,
290+
checkFileResults,
291+
interfaceState,
292+
symbolUse.DisplayContext,
293+
entity,
294+
tabSize
295+
)
296+
| _ -> return Seq.empty
297+
}

0 commit comments

Comments
 (0)