diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 43601b6e34..314330ea51 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -67,6 +67,7 @@ * Reference assembly MVIDs are now deterministic across compiler invocations. Previously, `--refout` / `true` produced a different MVID every build because the implied signature hash used .NET's randomized `String.GetHashCode()`. ([Issue #19751](https://github.com/dotnet/fsharp/issues/19751), [PR #19801](https://github.com/dotnet/fsharp/pull/19801)) * Parser: recover on unfinished if and binary expressions ([PR #19724](https://github.com/dotnet/fsharp/pull/19724)) +* Fix semantic classification of `IDisposable` and other interface types in type-occurrence positions being incorrectly classified as `DisposableType` instead of `Interface`. ([Issue #16268](https://github.com/dotnet/fsharp/issues/16268), [PR #19809](https://github.com/dotnet/fsharp/pull/19809)) ### Added diff --git a/src/Compiler/Service/SemanticClassification.fs b/src/Compiler/Service/SemanticClassification.fs index da0193e440..8d3b101d4b 100644 --- a/src/Compiler/Service/SemanticClassification.fs +++ b/src/Compiler/Service/SemanticClassification.fs @@ -321,7 +321,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 diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs index f2a2503da1..97517b310b 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs @@ -271,3 +271,85 @@ type MyDelegate = delegate of int -> string && (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. +[] +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. +[] +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. +[] +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. +[] +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)))