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
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
* Reference assembly MVIDs are now deterministic across compiler invocations. Previously, `--refout` / `<ProduceReferenceAssembly>true</ProduceReferenceAssembly>` 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

Expand Down
4 changes: 3 additions & 1 deletion src/Compiler/Service/SemanticClassification.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
[<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