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.9k stars 783 forks source link

No diagnostic when custom attributes ignored on unparenthesized tuple type #462

Open bbenoist opened 9 years ago

bbenoist commented 9 years ago

Hello,

After hours struggling to retrieve the values of some custom attributes assigned to an F# method return type annotation, I have found an unexpected behavior and would like to know why it did not worked as I thought.

Suppose that you have an F# method which returns a tuple value:

type HelloTupleWithoutParentheses =
    static member Format name : string * string =
            ("Hello " + name + "!", "Goodbye " + name + "!")

Then, you code a custom attribute containing a string describing each tuple field:

[<AttributeUsage(AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)>]
type ReturnParameterDescriptionAttribute(info:string) =
    inherit Attribute()
    member x.Info = info

If you apply it to the Format method:

type HelloTupleWithoutParentheses =
    static member Format name :
        [<ReturnParameterDescription("A string containing 'Hello <name>!'.")>]
        [<ReturnParameterDescription("A string containing 'Goodbye <name>!'.")>]
        string * string =
            ("Hello " + name + "!", "Goodbye " + name + "!")

The following reflection will not return anything:

let methodInfo = typeof<HelloTupleWithoutParentheses>.GetMethod("Format")
methodInfo.ReturnParameter.GetCustomAttributes(typeof<ReturnParameterDescriptionAttribute>, false)

Whereas it works correctly when the tuple type annotation is surrounded by parentheses:

type HelloTupleWithParentheses =
    static member Format name :
        [<ReturnParameterDescription("A string containing 'Hello <name>!'.")>]
        [<ReturnParameterDescription("A string containing 'Goodbye <name>!'.")>]
        (string * string) =
            ("Hello " + name + "!", "Goodbye " + name + "!")

Does anyone knows why the first attempt does not returns the custom attributes? Am I missing something? Is it a parser bug?

Full example of my problem (try it in your web browser):

open System

[<AttributeUsage(AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)>]
type ReturnParameterDescriptionAttribute(info:string) =
    inherit Attribute()
    member x.Info = info

type HelloSimple =
    static member Format name :
        [<ReturnParameterDescription("A string containing 'Hello <name>!'.")>]
        string =
            "Hello " + name + "!"

type HelloTupleWithParentheses =
    static member Format name :
        [<ReturnParameterDescription("A string containing 'Hello <name>!'.")>]
        [<ReturnParameterDescription("A string containing 'Goodbye <name>!'.")>]
        (string * string) =
            ("Hello " + name + "!", "Goodbye " + name + "!")

type HelloTupleWithoutParentheses =
    static member Format name :
        [<ReturnParameterDescription("A string containing 'Hello <name>!'.")>]
        [<ReturnParameterDescription("A string containing 'Goodbye <name>!'.")>]
        string * string =
            ("Hello " + name + "!", "Goodbye " + name + "!")

// Print the descriptions for each implementation.
[| typeof<HelloSimple>; typeof<HelloTupleWithParentheses>; typeof<HelloTupleWithoutParentheses> |]
|> Array.map (fun t ->
        printfn "--- %s ---" t.Name
        t.GetMethod("Format").ReturnParameter.GetCustomAttributes(typeof<ReturnParameterDescriptionAttribute>, false)
        |> Array.map (fun attr -> (attr :?> ReturnParameterDescriptionAttribute).Info |> printfn "%s")
    )
latkin commented 9 years ago

Bringing over some context from original issue at fsharp/fsharp:

@latkin said: This looks like a parser bug. Maybe the parens are required, but if so you should get an error telling you something's wrong. As it stands your attributes are just silently ignored - they don't appear anywhere in the generated IL.

@bbenoist said: The generated IL displayed by dotnetfiddle does not seems to include the attributes in any of the cases presented here. I also tested with this C# code and got the same results. Where is it supposed to be?

When you add parens, the attributes do appear. If you open your assembly with ILDASM you will see the attributes created in the method if you use parens:

.method public static class [mscorlib]System.Tuple`2<string,string> 
        Format(string name) cil managed
{
  .param [0]
  .custom instance void Test.ReturnParameterDescriptionAttribute::.ctor(string) = ( 01 00 24 41 20 73 74 72 69 6E 67 20 63 6F 6E 74   // ..$A string cont
                                                                                    61 69 6E 69 6E 67 20 27 48 65 6C 6C 6F 20 3C 6E   // aining 'Hello <n
                                                                                    61 6D 65 3E 21 27 2E 00 00 )                      // ame>!'...
  .custom instance void Test.ReturnParameterDescriptionAttribute::.ctor(string) = ( 01 00 26 41 20 73 74 72 69 6E 67 20 63 6F 6E 74   // ..&A string cont
                                                                                    61 69 6E 69 6E 67 20 27 47 6F 6F 64 62 79 65 20   // aining 'Goodbye 
                                                                                    3C 6E 61 6D 65 3E 21 27 2E 00 00 )                // <name>!'...

See http://stackoverflow.com/questions/30085515/getting-attribute-data-for-return-value-of-net-method for info on how to grab them via reflection.

bbenoist commented 9 years ago

When you add parens, the attributes do appear. If you open your assembly with ILDASM you will see the attributes created in the method if you use parens.

OK, I will not trust the dotnetfiddle IL anymore :disappointed:

See http://stackoverflow.com/questions/30085515/getting-attribute-data-for-return-value-of-net-method for info on how to grab them via reflection.

Thanks for pointing to an example including the MemberInfo.GetCustomAttributesData method. I was not yet aware of the differences with the MemberInfo.GetCustomAttributes method.

latkin commented 9 years ago

@bbenoist I typically use ILSpy, which seems to have the same problem. It's smart enough to show the attributes in the "C# View", yet doesn't show them in the "IL View". ILDasm is very clunky, but at least it's accurate.

edgarfgp commented 6 months ago
type HelloTupleWithoutParentheses =
    static member Format name :
        [<ReturnParameterDescription("A string containing 'Hello <name>!'.")>]
        [<ReturnParameterDescription("A string containing 'Goodbye <name>!'.")>]
        string * string =
            ("Hello " + name + "!", "Goodbye " + name + "!")

@brianrourkeboll Was wondering if wrapping the return type in extra parentheses during type checking when is a struct similar to your approach in 17017 would work here.

brianrourkeboll commented 6 months ago

I wonder if it's a parsing thing here. Without parentheses around the type, the attributes aren't even colorized as attributes:

image

It looks like bare tuple types get special handling, and it doesn't look like the example code ends up in the same place it would if the tuple type were parenthesized:

https://github.com/dotnet/fsharp/blob/a9037f44722b74046b00cb3424cfc64d57927efc/src/Compiler/pars.fsy#L5730-L5731

https://github.com/dotnet/fsharp/blob/a9037f44722b74046b00cb3424cfc64d57927efc/src/Compiler/pars.fsy#L5747-L5748

https://github.com/dotnet/fsharp/blob/a9037f44722b74046b00cb3424cfc64d57927efc/src/Compiler/pars.fsy#L5773-L5774

https://github.com/dotnet/fsharp/blob/a9037f44722b74046b00cb3424cfc64d57927efc/src/Compiler/pars.fsy#L5795

https://github.com/dotnet/fsharp/blob/a9037f44722b74046b00cb3424cfc64d57927efc/src/Compiler/pars.fsy#L5822

https://github.com/dotnet/fsharp/blob/a9037f44722b74046b00cb3424cfc64d57927efc/src/Compiler/pars.fsy#L6024-L6056