Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Compiler/Service/SemanticClassification.fs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,9 @@ module TcResolutionsExtensions =
| Item.Types(_, ty :: _), LegitTypeOccurrence, m ->
let ty = stripTyEqns g ty

if isDisposableTy g amap ty then
if isInterfaceTy g ty then
add m SemanticClassificationType.Interface
elif isDisposableTy g amap ty then
add m SemanticClassificationType.DisposableType
else
match tryTcrefOfAppTy g ty with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,97 @@ type Animal =
(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

/// Extract the substring of `source` covered by an FCS range.
/// Lines are 1-based; columns are 0-based half-open [start, end).
let private substringOfRange (source: string) (r: FSharp.Compiler.Text.range) =
let lines = source.Replace("\r\n", "\n").Split('\n')
if r.StartLine < 1 || r.StartLine > lines.Length then "" else
let line = lines.[r.StartLine - 1]
let startCol = min r.StartColumn line.Length
let endCol =
if r.EndLine = r.StartLine then min r.EndColumn line.Length
else line.Length
if endCol <= startCol then "" else line.Substring(startCol, endCol - startCol)

/// (#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) must NOT be
/// classified as Interface - the fix must not regress non-interface disposables.
[<Fact>]
let ``Concrete disposable class not classified as interface`` () =
let source = """
open System.IO
let s = new MemoryStream()
"""
let classifications = getClassifications source
let memStream =
classifications
|> Array.filter (fun c -> substringOfRange source c.Range = "MemoryStream")
Assert.True(memStream.Length > 0, "Expected at least one classification covering MemoryStream")
Assert.True(
memStream
|> Array.forall (fun c -> c.Type <> SemanticClassificationType.Interface),
sprintf "MemoryStream must not be classified as Interface, 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)))
Loading