fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

No diagnostic when custom attributes ignored on unparenthesized tuple type

Open bbenoist opened this issue 10 years ago • 5 comments

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

bbenoist avatar May 21 '15 13:05 bbenoist

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.

latkin avatar May 21 '15 19:05 latkin

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.

bbenoist avatar May 22 '15 07:05 bbenoist

@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.

latkin avatar May 22 '15 16:05 latkin

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.

edgarfgp avatar Apr 15 '24 15:04 edgarfgp

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

brianrourkeboll avatar Apr 15 '24 15:04 brianrourkeboll