From 3208b83499260d597975bf221a3e8719b6c6ddf1 Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Fri, 10 Apr 2026 13:22:38 +0200 Subject: [PATCH] Fix #7776: Detect missing implementations for C# 'abstract override' methods The IgnoreOverrides filter in InfoReader.FilterOverrides incorrectly removed abstract override methods (e.g. C# 'abstract override string ToString()') because they were signature-equivalent to the base virtual method. This caused GetClassDispatchSlots to miss these as required dispatch slots. The fix adds an isAbstract check to the FilterOverrides function so that abstract methods from derived types are preserved even when they override an equivalent virtual in a supertype. This ensures the compiler correctly reports FS0365 when a non-abstract F# class inherits from a C# class with abstract override members without providing implementations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Checking/InfoReader.fs | 9 +- .../AbstractMembers/AbstractMembers.fs | 171 ++++++++++++++++++ 3 files changed, 178 insertions(+), 3 deletions(-) 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 d5c2087765e..1799351985e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -3,6 +3,7 @@ * Fix DU case names matching IWSAM member names no longer cause duplicate property entries. (Issue [#14321](https://github.com/dotnet/fsharp/issues/14321), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix DefaultAugmentation(false) duplicate entry in method table. (Issue [#16565](https://github.com/dotnet/fsharp/issues/16565), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) * Fix abstract event accessors now have SpecialName flag. (Issue [#5834](https://github.com/dotnet/fsharp/issues/5834), [PR #19341](https://github.com/dotnet/fsharp/pull/19341)) +* Fix missing "No implementation was given" error when F# class inherits from a C# class with `abstract override` members without providing an implementation. ([Issue #7776](https://github.com/dotnet/fsharp/issues/7776), [PR #19503](https://github.com/dotnet/fsharp/pull/19503)) * Fix CLIEvent properties to be correctly recognized as events: `IsEvent` returns `true` and `XmlDocSig` uses `E:` prefix instead of `P:`. ([Issue #10273](https://github.com/dotnet/fsharp/issues/10273), [PR #18584](https://github.com/dotnet/fsharp/pull/18584)) * Fix extra sequence point at the end of match expressions. ([Issue #12052](https://github.com/dotnet/fsharp/issues/12052), [PR #19278](https://github.com/dotnet/fsharp/pull/19278)) * Fix wrong sequence point range for `return`/`yield`/`return!`/`yield!` inside computation expressions. ([Issue #19248](https://github.com/dotnet/fsharp/issues/19248), [PR #19278](https://github.com/dotnet/fsharp/pull/19278)) diff --git a/src/Compiler/Checking/InfoReader.fs b/src/Compiler/Checking/InfoReader.fs index 0e564c3add8..121b087f5e5 100644 --- a/src/Compiler/Checking/InfoReader.fs +++ b/src/Compiler/Checking/InfoReader.fs @@ -571,7 +571,7 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this = FilterItemsInSuperTypesBasedOnItemsInSubTypes nmf (fun item1 items -> not (items |> List.exists (fun item2 -> equivTest item1 item2))) itemLists /// Filter the overrides of methods or properties, either keeping the overrides or keeping the dispatch slots. - static let FilterOverrides findFlag (isVirt:'a->bool, isNewSlot, isDefiniteOverride, isFinal, equivSigs, nmf:'a->string) items = + static let FilterOverrides findFlag (isVirt:'a->bool, isNewSlot, isDefiniteOverride, isFinal, isAbstract, equivSigs, nmf:'a->string) items = let equivVirts x y = isVirt x && isVirt y && equivSigs x y let filterDefiniteOverrides = List.filter(isDefiniteOverride >> not) @@ -610,9 +610,10 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this = // (a) not virtual // (b) is a new slot or // (c) not equivalent + // (d) is abstract (e.g. C# 'abstract override' re-abstracting a base virtual method) // We keep virtual finals around for error detection later on |> FilterItemsInSubTypesBasedOnItemsInSuperTypes nmf (fun newItem priorItem -> - (isVirt newItem && isFinal newItem) || not (isVirt newItem) || isNewSlot newItem || not (equivVirts newItem priorItem) ) + (isVirt newItem && isFinal newItem) || not (isVirt newItem) || isNewSlot newItem || isAbstract newItem || not (equivVirts newItem priorItem) ) // Remove any abstract slots in supertypes that are (a) hidden by another newslot and (b) implemented // We leave unimplemented ones around to give errors, e.g. for @@ -649,6 +650,7 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this = (fun minfo -> minfo.IsNewSlot), (fun minfo -> minfo.IsDefiniteFSharpOverride), (fun minfo -> minfo.IsFinal), + (fun minfo -> minfo.IsAbstract), MethInfosEquivByNameAndSig EraseNone true g amap m, (fun minfo -> minfo.LogicalName)) @@ -664,7 +666,8 @@ type InfoReader(g: TcGlobals, amap: ImportMap) as this = ((fun (pinfo: PropInfo) -> pinfo.IsVirtualProperty), (fun pinfo -> pinfo.IsNewSlot), (fun pinfo -> pinfo.IsDefiniteFSharpOverride), - (fun _ -> false), + (fun _ -> false), // isFinal + (fun _ -> false), // isAbstract PropsGetterSetterEquiv (PropInfosEquivByNameAndSig EraseNone g amap m), (fun pinfo -> pinfo.PropertyName)) diff --git a/tests/FSharp.Compiler.ComponentTests/Conformance/ObjectOrientedTypeDefinitions/AbstractMembers/AbstractMembers.fs b/tests/FSharp.Compiler.ComponentTests/Conformance/ObjectOrientedTypeDefinitions/AbstractMembers/AbstractMembers.fs index 7ee84f82ef9..656bc654f58 100644 --- a/tests/FSharp.Compiler.ComponentTests/Conformance/ObjectOrientedTypeDefinitions/AbstractMembers/AbstractMembers.fs +++ b/tests/FSharp.Compiler.ComponentTests/Conformance/ObjectOrientedTypeDefinitions/AbstractMembers/AbstractMembers.fs @@ -291,3 +291,174 @@ let x4 = new TestLib.B() |> compile |> shouldFail |> withErrorCode 759 + + // Regression tests for https://github.com/dotnet/fsharp/issues/7776 + + /// C# 'abstract override' re-abstracts a virtual method from a base class. + /// F# classes inheriting from such a class must provide an implementation. + let private csLibWithAbstractOverride = + CSharp """ +namespace CSharpLib +{ + public abstract class AbstractClass + { + public abstract override string ToString(); + } + + public abstract class AbstractClassWithCustomMethod + { + public virtual int GetValue() => 42; + } + + public abstract class ReAbstractCustomMethod : AbstractClassWithCustomMethod + { + public abstract override int GetValue(); + } + + public class BaseWithVirtualProperty + { + public virtual int Value => 42; + } + + public abstract class ReAbstractProperty : BaseWithVirtualProperty + { + public abstract override int Value { get; } + } +} +""" + |> withName "CSharpAbstractOverrideLib" + + // https://github.com/dotnet/fsharp/issues/7776 + [] + let ``Abstract override ToString - missing implementation should error`` () = + FSharp """ +module Test + +open CSharpLib + +type T() = + inherit AbstractClass() +""" + |> asLibrary + |> withReferences [csLibWithAbstractOverride] + |> compile + |> shouldFail + |> withErrorCode 365 + + // https://github.com/dotnet/fsharp/issues/7776 + [] + let ``Abstract override ToString - with implementation should succeed`` () = + FSharp """ +module Test + +open CSharpLib + +type T() = + inherit AbstractClass() + override _.ToString() = "T" +""" + |> asLibrary + |> withReferences [csLibWithAbstractOverride] + |> compile + |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/7776 + [] + let ``Abstract override custom method - missing implementation should error`` () = + FSharp """ +module Test + +open CSharpLib + +type T() = + inherit ReAbstractCustomMethod() +""" + |> asLibrary + |> withReferences [csLibWithAbstractOverride] + |> compile + |> shouldFail + |> withErrorCode 365 + + // https://github.com/dotnet/fsharp/issues/7776 + [] + let ``Abstract override custom method - with implementation should succeed`` () = + FSharp """ +module Test + +open CSharpLib + +type T() = + inherit ReAbstractCustomMethod() + override _.GetValue() = 100 +""" + |> asLibrary + |> withReferences [csLibWithAbstractOverride] + |> compile + |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/7776 + [] + let ``Abstract override - F# abstract subclass should be allowed`` () = + FSharp """ +module Test + +open CSharpLib + +[] +type T() = + inherit AbstractClass() +""" + |> asLibrary + |> withReferences [csLibWithAbstractOverride] + |> compile + |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/7776 + [] + let ``Abstract override ToString - object expression must implement`` () = + FSharp """ +module Test + +open CSharpLib + +let x = { new AbstractClass() with + override _.ToString() = "obj" } +""" + |> asLibrary + |> withReferences [csLibWithAbstractOverride] + |> compile + |> shouldSucceed + + // https://github.com/dotnet/fsharp/issues/7776 + [] + let ``Abstract override property - missing implementation should error`` () = + FSharp """ +module Test + +open CSharpLib + +type T() = + inherit ReAbstractProperty() +""" + |> asLibrary + |> withReferences [csLibWithAbstractOverride] + |> compile + |> shouldFail + |> withErrorCode 365 + + // https://github.com/dotnet/fsharp/issues/7776 + [] + let ``Abstract override property - with implementation should succeed`` () = + FSharp """ +module Test + +open CSharpLib + +type T() = + inherit ReAbstractProperty() + override _.Value = 100 +""" + |> asLibrary + |> withReferences [csLibWithAbstractOverride] + |> compile + |> shouldSucceed