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 43601b6e34c..314330ea51f 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 da0193e4405..8d3b101d4b8 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 f2a2503da1f..8ce3775869e 100644
--- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs
+++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/SemanticClassificationRegressions.fs
@@ -271,3 +271,89 @@ 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) 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.
+[]
+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.
+[]
+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)))