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)))