-
Notifications
You must be signed in to change notification settings - Fork 864
Expand file tree
/
Copy pathSemanticClassificationRegressions.fs
More file actions
359 lines (316 loc) · 13.7 KB
/
Copy pathSemanticClassificationRegressions.fs
File metadata and controls
359 lines (316 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
module FSharpChecker.SemanticClassificationRegressions
open Xunit
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
open FSharp.Test.ProjectGeneration
open FSharp.Test.ProjectGeneration.Helpers
#nowarn "57"
/// Get semantic classification items for a single-file source using the transparent compiler.
let getClassifications (source: string) =
let fileName, snapshot, checker = singleFileChecker source
let results = checker.ParseAndCheckFileInProject(fileName, snapshot) |> Async.RunSynchronously
let checkResults = getTypeCheckResult results
checkResults.GetSemanticClassification(None, RelatedSymbolUseKind.All)
/// Extract the source substring covered by a classification item's range (single-line ranges).
let private substringOfRange (source: string) (r: Range) =
let lines = source.Replace("\r\n", "\n").Split('\n')
let line = lines[r.StartLine - 1]
line.Substring(r.StartColumn, r.EndColumn - r.StartColumn)
/// (#15290 regression) Copy-and-update record fields must not be classified as type names.
/// Before the fix, Item.Types was registered with mWholeExpr and ItemOccurrence.Use, producing
/// a wide type classification that overshadowed the correct RecordField classification.
[<Fact>]
let ``Copy-and-update field should not be classified as type name`` () =
let source =
"""
module Test
type MyRecord = { ValidationErrors: string list; Name: string }
let x: MyRecord = { ValidationErrors = []; Name = "" }
let updated = { x with ValidationErrors = [] }
"""
let items = getClassifications source
// Line 6 contains "{ x with ValidationErrors = [] }"
// "ValidationErrors" starts around column 23 (after "let updated = { x with ")
// It should be RecordField, NOT ReferenceType/ValueType.
let fieldLine = 6
let fieldItems =
items
|> Array.filter (fun item ->
item.Range.StartLine = fieldLine
&& item.Type = SemanticClassificationType.RecordField)
Assert.True(fieldItems.Length > 0, "Expected RecordField classification on the copy-and-update line")
// No type classification should cover the field name on that line with a visible range
let typeItemsCoveringField =
items
|> Array.filter (fun item ->
item.Range.StartLine <= fieldLine
&& item.Range.EndLine >= fieldLine
&& item.Range.Start <> item.Range.End
&& (item.Type = SemanticClassificationType.ReferenceType
|| item.Type = SemanticClassificationType.ValueType
|| item.Type = SemanticClassificationType.Type))
Assert.True(
typeItemsCoveringField.Length = 0,
sprintf
"No type classification should cover the copy-and-update line, but found: %A"
(typeItemsCoveringField |> Array.map (fun i -> i.Range, i.Type))
)
/// (#16621) Helper: assert UnionCase classifications on expected lines.
/// Each entry is (line, expectedCount, maxRangeWidth).
/// maxRangeWidth guards against dot-coloring regressions (range including "x." prefix).
let expectUnionCaseClassifications source (expectations: (int * int * int) list) =
let items = getClassifications source
for (line, expectedCount, maxWidth) in expectations do
let found =
items
|> Array.filter (fun item ->
item.Type = SemanticClassificationType.UnionCase
&& item.Range.StartLine = line)
Assert.True(
found.Length = expectedCount,
sprintf "Line %d: expected %d UnionCase classification(s), got %d. Items on that line: %A" line expectedCount found.Length
(items
|> Array.filter (fun i -> i.Range.StartLine = line)
|> Array.map (fun i -> i.Range.StartColumn, i.Range.EndColumn, i.Type))
)
for item in found do
let width = item.Range.EndColumn - item.Range.StartColumn
Assert.True(
width <= maxWidth,
sprintf "Line %d: UnionCase range is too wide (%d columns, max %d): %A" line width maxWidth item.Range
)
/// (#16621 regression) Union case tester classification must not include the dot.
[<Fact>]
let ``Union case tester classification range should not include dot`` () =
let source =
"""
module Test
type Shape = Circle | Square | HyperbolicCaseWithLongName
let s = Circle
let r1 = s.IsCircle
let r2 = s.IsHyperbolicCaseWithLongName
"""
// line, count, maxWidth
expectUnionCaseClassifications source [ (6, 1, 8); (7, 1, 30) ]
/// (#16621) Union case tester classification across scenarios: chaining, RequireQualifiedAccess,
/// multiple testers on one line, and self-referential members.
[<Fact>]
let ``Union case tester classification across scenarios`` () =
let source =
"""
module Test
type Shape = Circle | Square
let s = Circle
let chained = s.IsCircle.ToString()
let both = s.IsCircle && s.IsSquare
[<RequireQualifiedAccess>]
type Token = Ident of string | Keyword
let t = Token.Keyword
let rqa = t.IsIdent
type Animal =
| Cat
| Dog
member this.IsFeline = this.IsCat
"""
// line, count, maxWidth
expectUnionCaseClassifications source
[ (6, 1, 8) // s.IsCircle.ToString() — chained
(7, 2, 8) // s.IsCircle && s.IsSquare — two on same line
(12, 1, 7) // t.IsIdent — RequireQualifiedAccess
(17, 1, 5) ] // this.IsCat — self-referential member
/// (#18009 regression) Static method on a generic type with a *qualified* type argument
/// must still classify the type name as a type.
[<Fact>]
let ``Static method on generic type should classify type name as type`` () =
let source =
"""
module Test
type MyType<'T> =
static member S = 1
let _ = MyType<int>.S
let _ = MyType<System.Int32>.S
"""
let items = getClassifications source
let isMyTypeRefOnLine line (item: SemanticClassificationItem) =
item.Type = SemanticClassificationType.ReferenceType
&& item.Range.StartLine = line
&& item.Range.StartColumn = 8
&& item.Range.EndColumn = 14
let unqualified = items |> Array.filter (isMyTypeRefOnLine 7)
Assert.True(
unqualified.Length = 1,
sprintf
"Expected exactly one ReferenceType classification for MyType on line 7, got: %A"
(items |> Array.filter (fun i -> i.Range.StartLine = 7)
|> Array.map (fun i -> i.Range.StartColumn, i.Range.EndColumn, i.Type))
)
let qualified = items |> Array.filter (isMyTypeRefOnLine 8)
Assert.True(
qualified.Length = 1,
sprintf
"Expected exactly one ReferenceType classification for MyType on line 8, got: %A"
(items |> Array.filter (fun i -> i.Range.StartLine = 8)
|> Array.map (fun i -> i.Range.StartColumn, i.Range.EndColumn, i.Type))
)
/// (#18009 follow-up) Accepting ItemOccurrence.InvalidUse in LegitTypeOccurrence must
/// not cause unresolved identifiers to be classified as types.
[<Fact>]
let ``Undeclared identifier in expression position is not classified as a type`` () =
let source =
"""
module Test
let _ = NotDeclaredAnywhere.S
"""
let items = getClassifications source
let badSpans =
items
|> Array.filter (fun item ->
item.Range.StartLine = 4
&& item.Range.StartColumn = 8
&& item.Range.EndColumn = 27
&& (item.Type = SemanticClassificationType.ReferenceType
|| item.Type = SemanticClassificationType.ValueType
|| item.Type = SemanticClassificationType.Type))
Assert.True(
badSpans.Length = 0,
sprintf
"Undeclared identifier should not be classified as a type, but found: %A"
(badSpans |> Array.map (fun i -> i.Range.StartColumn, i.Range.EndColumn, i.Type))
)
/// (#16982) Delegate `Invoke` synthesized in a delegate declaration must not be classified as Method.
[<Fact>]
let ``Delegate Invoke in declaration not classified as method`` () =
let source = """
type MyDelegate = delegate of int -> string
"""
let classifications = getClassifications source
let invokeMethods =
classifications
|> Array.filter (fun c ->
c.Type = SemanticClassificationType.Method
&& substringOfRange source c.Range = "Invoke")
Assert.Empty(invokeMethods)
/// (#16982) Negative: at a real call site, `Invoke` must still classify as Method.
[<Fact>]
let ``Delegate Invoke at call site classified as method`` () =
let source = """
type MyDelegate = delegate of int -> string
let d = MyDelegate(fun i -> string i)
let result = d.Invoke(42)
"""
let classifications = getClassifications source
let invokeCallSite =
classifications
|> Array.filter (fun c ->
c.Type = SemanticClassificationType.Method && c.Range.StartLine = 4)
Assert.NotEmpty(invokeCallSite)
/// (#16982) Generic delegate variant.
[<Fact>]
let ``Generic delegate Invoke not classified as method in decl`` () =
let source = """
type MyGenDelegate<'T> = delegate of 'T -> 'T
"""
let classifications = getClassifications source
let invokeMethods =
classifications
|> Array.filter (fun c ->
c.Type = SemanticClassificationType.Method
&& substringOfRange source c.Range = "Invoke")
Assert.Empty(invokeMethods)
/// (#16982) The synthesized async-pattern members `BeginInvoke`/`EndInvoke` must also be suppressed in the declaration.
[<Fact>]
let ``BeginInvoke EndInvoke also not classified in decl`` () =
let source = """
type MyDelegate = delegate of int -> string
"""
let classifications = getClassifications source
let asyncInvokeMethods =
classifications
|> Array.filter (fun c ->
c.Type = SemanticClassificationType.Method
&& (let text = substringOfRange source c.Range
text = "BeginInvoke" || text = "EndInvoke"))
Assert.Empty(asyncInvokeMethods)
/// (#16268) IDisposable appearing in `interface IDisposable with` should be
/// classified as Interface, not DisposableType.
[<Fact>]
let ``IDisposable in interface impl classified as interface`` () =
let source = """
open System
type MyClass() =
interface IDisposable with
member _.Dispose() = ()
"""
let classifications = getClassifications source
let idisposableOnLine4 =
classifications
|> Array.filter (fun c ->
c.Range.StartLine = 4 && substringOfRange source c.Range = "IDisposable")
Assert.True(idisposableOnLine4.Length > 0, "Expected at least one classification covering IDisposable on line 4")
Assert.True(
idisposableOnLine4
|> Array.forall (fun c -> c.Type = SemanticClassificationType.Interface),
sprintf "Expected IDisposable to be classified as Interface, got: %A"
(idisposableOnLine4 |> Array.map (fun c -> c.Type)))
/// (#16268) Negative: a concrete disposable class (MemoryStream) in a type-occurrence
/// position must still be classified as DisposableType - the reorder of isInterfaceTy
/// before isDisposableTy must not regress non-interface disposables.
/// A type annotation is used (not `new MemoryStream()`, which is Item.CtorGroup) so the
/// occurrence actually flows through the modified Item.Types/LegitTypeOccurrence arm.
[<Fact>]
let ``Concrete disposable class classified as disposable type`` () =
let source = """
open System.IO
let length (s: MemoryStream) = s.Length
"""
let classifications = getClassifications source
let memStream =
classifications
|> Array.filter (fun c ->
substringOfRange source c.Range = "MemoryStream" && c.Range.StartLine = 3)
Assert.True(memStream.Length > 0, "Expected at least one classification covering MemoryStream")
Assert.True(
memStream
|> Array.forall (fun c -> c.Type = SemanticClassificationType.DisposableType),
sprintf "MemoryStream type occurrence must be classified as DisposableType, got: %A"
(memStream |> Array.map (fun c -> c.Type)))
/// (#16268) IDisposable used as a type constraint should be Interface.
[<Fact>]
let ``IDisposable as type constraint classified as interface`` () =
let source = """
open System
let dispose (x: #IDisposable) = x.Dispose()
"""
let classifications = getClassifications source
let idisposable =
classifications
|> Array.filter (fun c -> substringOfRange source c.Range = "IDisposable")
Assert.True(idisposable.Length > 0, "Expected at least one classification covering IDisposable")
Assert.True(
idisposable
|> Array.forall (fun c -> c.Type = SemanticClassificationType.Interface),
sprintf "Expected IDisposable type-constraint occurrence to be Interface, got: %A"
(idisposable |> Array.map (fun c -> c.Type)))
/// (#16268) Non-IDisposable interface in `interface ... with` position stays Interface.
/// This guards against accidentally narrowing the fix to IDisposable only.
[<Fact>]
let ``Non-IDisposable interface classified as interface`` () =
let source = """
type IMyInterface =
abstract member DoStuff: unit -> unit
type MyClass() =
interface IMyInterface with
member _.DoStuff() = ()
"""
let classifications = getClassifications source
let iface =
classifications
|> Array.filter (fun c ->
substringOfRange source c.Range = "IMyInterface" && c.Range.StartLine = 5)
Assert.True(iface.Length > 0, "Expected at least one IMyInterface classification on line 5")
Assert.True(
iface
|> Array.forall (fun c -> c.Type = SemanticClassificationType.Interface),
sprintf "Expected IMyInterface on line 5 to be Interface, got: %A"
(iface |> Array.map (fun c -> c.Type)))