dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.94k stars 788 forks source link

[<CLIEvent>] on abstract type member generates accessor methods without `specialname` #5834

Open stakx opened 6 years ago

stakx commented 6 years ago

F# does not mark CLI event accessor methods (add_X, remove_X) with the specialname IL flag if the event member is abstract.

This can be problematic for Reflection-based tools because they might not correctly recognize event accessor methods as such.

Someone stumbled on this a while ago when using the Moq mocking library against a F#-defined interface type. In order to keep up support for F#, Moq had to stop querying method.IsSpecialName, which it used to quickly check whether it was worth trying to find a property or event associated with method (a much more expensive operation). Abandoning that check for F# also meant reduced runtime performance for all other (C#) code.

I'd like to bring back this performance optimization in Moq. Therefore it would be great if F# could correctly mark all event accessors as such in metadata.

Repro steps

Compile the following code as a console application, run it, and observe its output.

open System;
open System.Reflection

type IAbstract1 =
    [<CLIEvent>]
    abstract member Event : IEvent<EventHandler, EventArgs>

type IAbstract2 =
    [<CLIEvent>]
    abstract member Event : IDelegateEvent<Action>

[<AbstractClass>]
type Abstract3() =
    [<CLIEvent>]
    abstract member Event : IDelegateEvent<Action>

type Concrete1() =
    let event = new Event<EventHandler, EventArgs>()
    [<CLIEvent>]
    member this.Event = event.Publish

type Concrete2() =
    [<CLIEvent>]
    member this.Event = { new IDelegateEvent<Action> with
                              member this.AddHandler _ = ()
                              member this.RemoveHandler _ = () }

[<EntryPoint>]
let main argv =

    let isTestType (t: Type) =
        t.Name.Contains("Abstract") || t.Name.Contains("Concrete")

    let printType (t: Type) =
        printfn "%s" t.Name

        t.GetMethods()
        |> Seq.filter (fun m -> m.Name.Contains("Event"))
        |> Seq.iter (fun m -> printfn "* %s  IsSpecialName = %b" m.Name m.IsSpecialName)

        printfn ""

    Assembly.GetExecutingAssembly().GetExportedTypes()
    |> Seq.filter isTestType
    |> Seq.iter printType

    0

Expected behavior

The program should print IsSpecialName = true for all members, i.e. including those of the abstract types IAbstract1, IAbstract2, and Abstract3.

Actual behavior

The program prints IsSpecialName = true only for accessor methods in types Concrete1 and Concrete2. The accessor methods from the abstract types do not have specialname set.

Known workarounds

None known to me. (Am I possibly missing the correct way to define a CLI event in an abstract type?)

Related information

marklam commented 5 years ago

I just hit this problem after updating some old test code to use the new NSubstitute (4.0), where they also added the check for the specialname flag.

I thought at first it was maybe just a case F# not behaving like C#, but according to the ECMA-335 "Common Language Infrastructure (CLI) Partitions I to VI " spec:

CLS Rule 29: The methods that implement an event shall be marked SpecialName in the metadata. (§I.8.11.4)