Search code examples
c#.netenumsroslynsourcegenerators

Detect nullable enum type in source generator


I'm writing a source generator that processes my model classes and adds some custom serialisation code for them. The model classes can have all sorts of property types. I'm currently interested in the nullable enum properties. I cannot detect them. I see their name and the type as something like "MyEnum?" but I need to know if that's an enum.

For non-nullable enum types, the check is:

isEnum = classProperty.Type is INamedTypeSymbol namedType2 &&
    namedType2.EnumUnderlyingType != null;

Nullables can be detected so:

isNullable = classProperty.Type is INamedTypeSymbol namedType &&
    namedType.NullableAnnotation == NullableAnnotation.Annotated;

But I cannot find any combination of these. Is that possible, and how?

Updates:

The same applies to property types like int? actually. And I'd expect this to be a Nullable<int> or something. In fact I can see that the type is generic (IsGenericType == true) and its first type argument (TypeArguments[0]) is int (in the case of int?). I just can't unhide that Nullable<> part anywhere so I can never be sure if it's really the nullable situation.


Solution

  • All nullable value type are actually of type Nullable<T> and vice versa. E.g. the following two nullable enum properties:

    public MyEnum? MyNullableEnum { get; set; }
    public Nullable<MyEnum> MyNullableEnum2 { get; set; }
    

    actually compile to identical code. Thus it should be sufficient to check ITypeSymbol.IsValueType to determine whether a nullable type is of type Nullable<T> for some T.

    First add the following extension methods:

    public static partial class CodeAnalysisExtensions
    {
        public static bool IsNullable(this ITypeSymbol typeSymbol) =>  
            typeSymbol.NullableAnnotation == NullableAnnotation.Annotated;
    
        public static bool IsNullableValueType(this ITypeSymbol typeSymbol) =>  
            typeSymbol.IsValueType && typeSymbol.IsNullable();
    
        public static bool TryGetNullableValueUnderlyingType(this ITypeSymbol typeSymbol, [NotNullWhen(returnValue: true)] out ITypeSymbol? underlyingType)
        {
            if (typeSymbol is INamedTypeSymbol namedType && typeSymbol.IsNullableValueType() && namedType.IsGenericType)
            {
                var typeParameters = namedType.TypeArguments;
                // Assert the generic is named System.Nullable<T> as expected.
                Debug.Assert(namedType.ConstructUnboundGenericType() is {} genericType && genericType.Name == "Nullable" && genericType.ContainingNamespace.Name == "System" && genericType.TypeArguments.Length == 1); 
                Debug.Assert(typeParameters.Length == 1);
                underlyingType = typeParameters[0];
                // TODO: decide what to return when the underlying type is not declared due to some compilation error.
                // TypeKind.Error indicats a compilation error, specifically a nullable type where the underlying type was not found.
                // I have observed that IsValueType will be true in such cases even though it is actually unknown whether the missing type is a value type
                // I chose to return false but you may prefer something else. 
                return underlyingType.TypeKind == TypeKind.Error ? false : true;
            }
            underlyingType = null;
            return false;
        }
        
        public static bool IsEnum(this ITypeSymbol typeSymbol) => 
            typeSymbol is INamedTypeSymbol namedType && namedType.EnumUnderlyingType != null;
    
        public static bool IsNullableEnumType(this ITypeSymbol typeSymbol) => 
            typeSymbol.TryGetNullableValueUnderlyingType(out var underlyingType) == true && underlyingType.IsEnum();
    }
    

    Assuming that classProperty is of type IPropertySymbol, you can now do:

    var isNullableEnum = classProperty.Type.TryGetNullableValueUnderlyingType(out var underlyingType) && underlyingType.IsEnum();
    

    Notes:

    • In practice I have found that my statement All nullable value type are actually of type Nullable<T> and vice versa is not true in the presence of a compilation error, specifically a nullable property where the underlying type is undefined, e.g.:

      public CompilationError? MyCompilationError { get; set; }
      

      In such cases the ITypeSymbol corresponding to CompilationError? will have IsValueType == true even though this is not in fact known. When this happens, the TypeKind of the underlying type will have the value TypeKind.Error. In my extension method above, I check for this and return false from TryGetNullableValueUnderlyingType(). You may choose to handle this boundary case differently.

    Demo fiddle here.