fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

IsUnionCaseTester throwing an error

Open ncave opened this issue 1 year ago • 13 comments

2 issues with IsUnionCaseTester:

  • Seems to be throwing an error, instead of returning false on non-matching values.
  • Seems to be missing a case where an union case tester method is neither IsProperty nor IsMethod (but it is IsPropertyGetterMethod).

Perhaps it's missing the | V v -> ... case, maybe something like this?

member _.IsUnionCaseTester =
    checkIsResolved()
    match d with
    | P p -> p.IsUnionCaseTester
    | M m -> m.IsUnionCaseTester
    | V v -> v.IsPropertyGetterMethod && v.LogicalName.StartsWith("get_Is") // or something like it
    | E _ | C _ -> false // fixed to return boolean

Related to #16341 Tested on .NET 8.0, with <LangVersion>preview</LangVersion>.

ncave avatar Jun 12 '24 19:06 ncave

Would this be fixed in F# 9 ?

edgarfgp avatar Aug 05 '24 18:08 edgarfgp

We probably should, yes.

vzarytovskii avatar Aug 05 '24 19:08 vzarytovskii

@ncave Could you please share code that repros this?

abonie avatar Aug 08 '24 09:08 abonie

I mean specifically how does this case looks like

  • Seems to be missing a case where an union case tester method is neither IsProperty nor IsMethod (but it is IsPropertyGetterMethod).

abonie avatar Aug 08 '24 14:08 abonie

@abonie Sure, here it is:

type HTMLAttr =
    | Href of string
    | Custom

let a = Custom
let b = a.IsCustom
printfn $"IsCustom = {b}"

Here, when inspecting the AST for the generated union case tester members get_IsCustom and get_IsHref, we can see the following:

  • FSharpMemberOrFunctionOrValue.IsProperty = false
  • FSharpMemberOrFunctionOrValue.IsMethod = false
  • FSharpMemberOrFunctionOrValue.IsFunction = true
  • FSharpMemberOrFunctionOrValue.IsPropertyGetterMethod = true
  • FSharpMemberOrFunctionOrValue.IsUnionCaseTester = throws an exception, not good...

Based on these, see the OP above for a proposed fix in FSharpMemberOrFunctionOrValue.IsUnionCaseTester.

ncave avatar Aug 08 '24 21:08 ncave

@abonie In other words, instead of what it is right now:

member _.IsUnionCaseTester =
    checkIsResolved()
    match d with
    | P p -> p.IsUnionCaseTester
    | M m -> m.IsUnionCaseTester
    | E _ | C _ | V _ -> invalidOp "the value or member is not a property"

should be something like this:

member _.IsUnionCaseTester =
    checkIsResolved()
    match d with
    | P p -> p.IsUnionCaseTester
    | M m -> m.IsUnionCaseTester
    | V v -> v.IsPropertyGetterMethod && v.LogicalName.StartsWith("get_Is") // added missing case
    | E _ | C _ -> false // fixed to return boolean

ncave avatar Aug 10 '24 01:08 ncave

@ncave sorry I got a bit sidetracked last couple of days. I guess I wanted to know what F# code you are inspecting that results in IsProperty = false, IsMethod = false, IsFunction = true and IsPropertyGetterMethod = false. But you're saying you are looking at the AST from the IL that was generated for get_IsCustom, is that correct?

abonie avatar Aug 13 '24 18:08 abonie

@abonie Yes, that is correct, see above for an example F# code, and the AST for the generated union case tester member get_IsCustom.

ncave avatar Aug 13 '24 18:08 ncave

Right @ncave, sorry for being a little clueless here, but I can't figure out how you're grabbing the get_IsCustom symbol that makes this combination of IsMethod = false and IsPropertyGetterMethod = true happen.

Here's what I am trying in our unit tests to capture this, based on the snippet you shared:

[<Fact>]
let ``IsUnionCaseTester test`` () =
    FSharp """
module Lib

type HTMLAttr =
    | Href of string
    | Custom

let a = Custom
let b = a.IsCustom
printfn $"IsCustom = {b}"
let c = a.get_IsCustom
"""
    |> withLangVersionPreview
    |> typecheckResults
    |> fun results ->
        let isCustomSymbolUse = results.GetSymbolUseAtLocation(11, 22, "let c = a.get_IsCustom", [ "get_IsCustom" ]).Value
        match isCustomSymbolUse.Symbol with
        | :? FSharpMemberOrFunctionOrValue as mfv ->
            Assert.True(false, $"""IsProperty = {mfv.IsProperty}
IsMethod = {mfv.IsMethod}
IsFunction = {mfv.IsFunction}
IsPropertyGetterMethod = {mfv.IsPropertyGetterMethod}
IsValue = {mfv.IsValue}""")
        | _ -> failwith "Expected FSharpMemberOrFunctionOrValue"

(ignore the fact that this is not a good test, using this for exploration purposes atm) This for example gives me IsMethod = true and IsPropertyGetterMethod = true. If I instead change the one line to grab a different symbol

let isCustomSymbolUse = results.GetSymbolUseAtLocation(11, 5, "let c = a.get_IsCustom", [ "c" ]).Value

then I get both IsMethod and IsPropertyGetterMethod = false

Trying to understand what's going on before deciding if the ad hoc bugfix is appropriate

abonie avatar Aug 14 '24 12:08 abonie

@abonie Perhaps try inspecting the IsCustom symbol at this line let b = a.IsCustom.

ncave avatar Aug 14 '24 17:08 ncave

I've started with that (it's IsMethod = false, IsPropertyGetterMethod = false). Is it possible for you to share the code that runs into IsMethod = false, IsPropertyGetterMethod = true? I mean both code that's being inspected and the code that does inspecting

abonie avatar Aug 15 '24 14:08 abonie

@abonie Yes, while it is true that when using the symbol look up (GetSymbolUseAtLocation), you get the correct values, I would still argue that the correct behavior for FSharpMemberOrFunctionOrValue.IsUnionCaseTester is to return false when not matching the input, instead of throwing an exception. It's a boolean property, it shouldn't throw.

e.g. this code prints the correct values (but please see below for more):

    let isCustomSymbolUse =
        typeCheckResults.GetSymbolUseAtLocation(8, 18, inputLines.[7], [ "IsCustom" ])
    match isCustomSymbolUse with
    | Some v ->
        match v.Symbol with
        | :? FSharpMemberOrFunctionOrValue as mfv ->
            printfn $"""
--- when using GetSymbolUseAtLocation ---
{mfv.CompiledName}:
    IsProperty = {mfv.IsProperty}
    IsUnionCaseTester = {mfv.IsUnionCaseTester}
    IsMethod = {mfv.IsMethod}
    IsFunction = {mfv.IsFunction}
    IsPropertyGetterMethod = {mfv.IsPropertyGetterMethod}
    IsValue = {mfv.IsValue}"""
        | _ -> printfn "Expected FSharpMemberOrFunctionOrValue"
    | _ -> printfn "Expected Symbol"

will print the following:

--- when using GetSymbolUseAtLocation ---
get_IsCustom:
    IsProperty = True
    IsUnionCaseTester = True
    IsMethod = False
    IsFunction = False
    IsPropertyGetterMethod = False
    IsValue = False

But, if you enumerate the declarations in the AST, you can see a different story. e.g. this code prints the incorrect values (which is what this PR is about):

    for impl_file in projectResults.AssemblyContents.ImplementationFiles do
        for file_decl in impl_file.Declarations do
            match file_decl with
            | FSharpImplementationFileDeclaration.Entity (ent, ent_decls) ->
                for ent_decl in ent_decls do
                    match ent_decl with
                    | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue (mfv, args, body) ->
                        if mfv.CompiledName.StartsWith("get_Is") then
                            printfn $"""
--- when enumerating declarations ---
{mfv.CompiledName}:
    IsProperty = {mfv.IsProperty}
    IsMethod = {mfv.IsMethod}
    IsFunction = {mfv.IsFunction}
    IsPropertyGetterMethod = {mfv.IsPropertyGetterMethod}
    IsValue = {mfv.IsValue}"""
                    | _ -> ()
            | _ -> ()

will print this:

--- when enumerating declarations ---
get_IsHref:
    IsProperty = False
    IsMethod = False
    IsFunction = True
    IsPropertyGetterMethod = True
    IsValue = False

--- when enumerating declarations ---
get_IsCustom:
    IsProperty = False
    IsMethod = False
    IsFunction = True
    IsPropertyGetterMethod = True
    IsValue = False

And if you try to use mfv.IsUnionCaseTester, it will throw an exception.

ncave avatar Aug 15 '24 21:08 ncave

Thanks @ncave, I can reproduce this now. I suspect this is a bug in these properties (and not in the IsUnionCaseTester) - it doesn't make sense that it would be not a method but a property getter method. I am looking into it.

I would still argue that the correct behavior for FSharpMemberOrFunctionOrValue.IsUnionCaseTester is to return false when not matching the input, instead of throwing an exception. It's a boolean property, it shouldn't throw.

I agree with this. It seems to me there are two separate issues here: throwing instead of returning false and the weird "not a method" corner case.

abonie avatar Aug 16 '24 13:08 abonie