2

In Delphi, one can write the following code:

type
  TMyEnum = (meFirstValue, meSecondValue);

  TInfo = record
    Name: string;
    Description: string;
  end;

const
  MyEnumDetails: array [TMyEnum] of TInfo =
    (
      (Name: 'First Value'; Description: 'This is the first value'),
      (Name: 'Second Value'; Description: 'This is the second value')
    );

This has the very nice advantage that should a new enum member be added to TMyEnum, then the compiler will complain on the MyEnumDetails constant definition, forcing the developer to resolve the situation before the application reaches the build machine.

As I'm now writing .Net 6 C# code within Visual Studio 2022, I'm trying to replicate the above, and in particular the fact that the compiler will stop with an error if an enum member has been left out.

I can use a Dictionary as mentioned here but this does not emit a warning/error if I forget a member of the enum in the initialization. I know this is not strictly a constant, nor a readonly element, but in my case I'm more interested in the compiler erroring out than the constness of the dictionary.

I also tried enumerating the enum members and use a switch statement to create the instances as needed:

foreach (var myEnum in (MyEnum[])Enum.GetValues(typeof(MyEnum)))
{
    switch (myEnum)
    {
        case MyEnum.FirstValue:
            enumDictionary.Add(myEnum, new Info("First Value", "This is the first value"));
            break;
        }
    }
}

But then again, this does not make the compiler emit any message. I was kind of expecting to get CS8509 or IDE0072/IDE0010 but nothing comes up.

So I tried moving the switch a secondary method that only does this:

private static Info GetInfo(MyEnum myEnum)
{
    switch (kind)
    {
        case MyEnum.FirstValue:
            return new Info("First Value", "This is the first value");
    }
}

And this time I did receive an error, namely the CS0161 one because GetInfo does not return a value on all code paths. But that error won't go if I provide a case for all enum members, only does it disappear if I provide a default case or a return outside the switch. And then, there are no longer any messages if an enum member is not used in the switch statement, just like with the previous attempt.

I checked in the csproj file and could not find anything that would specifically disable the CS8509 or IDE0072/IDE0010 warnings. Looking at the options page, I did not see anything either.

What have I missed here? Is there a place unknown to me that disables the above warning?

Alternatively, do you have any other suggestion as to how I can achieve the goal of having the compiler stopping on a missing enum member?

OBones
  • 310
  • 2
  • 13
  • 1
    Not sure about the base issue, but switch *expressions* (`kind switch { MyEnum.FirstValue => new Info(...); }`) have a very nice CS8509 warning that even lists what kind of cases are missing. – Jeroen Mostert Sep 21 '22 at 09:41
  • I changed it to a switch expression and still no CS8509 warning here either. What's weird though, is that by using the Intellisense shortcut to get the automatic conversion, I see a suggestion to add the missing cases. But still no message in the error list, and no way to make it become an error. – OBones Sep 21 '22 at 09:46
  • If in Visual Studio, perform a clean, then try View -> Output and perform a build to see if you can see the compiler output there. If so, then the error list is on the fritz. Checking the "treat warnings as errors" box should still block the build in that case. If not, using Code Analysis the warning level settings are subsumed into that and verifying the settings becomes more complicated than simply cranking the warning level up to maximum. – Jeroen Mostert Sep 21 '22 at 09:50
  • Alas, nothing comes up in the output window, there must be a global setting somewhere that turns off those warnings, but for the life of me, I can't find it. – OBones Sep 21 '22 at 09:58
  • Check if your project has an `.editorconfig` (the file might not be included in the project/solution, but check if it exists on disk). If not, now might be a good time to create it and give it nice defaults. – Jeroen Mostert Sep 21 '22 at 09:59
  • Ah, thanks for the tip, I have now added that file with two lines inside it, forcing the error level for IDE0010 and CS8509 which covers both cases of missing switch elements. – OBones Sep 21 '22 at 13:00

2 Answers2

1

The following program will give you a CS8509 compile error:

using System;
using System.Collections.Generic;

namespace Demo;

public static class Program
{
    public static void Main()
    {
        initDict();    
    }

    static void initDict()
    {
        foreach (var myEnum in (EnumTest[])Enum.GetValues(typeof(EnumTest)))
        {
            string value = myEnum switch
            {
                EnumTest.Zero => "Case: Zero",
                EnumTest.One  => "Case: One",
                EnumTest.Two  => "Case: Two"
            };

            _enumDictionary.Add(myEnum, value);
        }
    }

    static readonly Dictionary<EnumTest, string> _enumDictionary = new();
}

public enum EnumTest
{
    Zero,
    One,
    Two,
    Three
}

If you comment-out the Three item in enum EnumTest it will compile OK, but with the following warning instead:

Warning CS8524 The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Demo.EnumTest)3' is not covered.

If you don't want that warning, you can suppress it by surrounding the affected code with:

#pragma warning disable CS8524

string value = myEnum switch
{
    EnumTest.Zero => "Case: Zero",
    EnumTest.One  => "Case: One",
    EnumTest.Two  => "Case: Two"
};

#pragma warning restore CS8524

If you do that, you'll still get the error if you add a new item to enum EnumTest without adding it to the switch.

Obviously you have to enable Code Analysis for this to work!

Is that the sort of thing you're looking for?

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • 1
    Thanks, it's definitely what I'm looking for, and it showed me that the CS8509 message only comes up in a switch value expression, not in a regular switch case expression. – OBones Sep 21 '22 at 12:59
0

We have solved this using a helper class in an unit test assembly.

public static class EnumTestHelper
    {
        /// <summary>
        /// Encapsulates enum value checking and fails when unexpected number of enum values.
        /// Should be called on each test which handles enum values to ensure that no place in the codebase is forgotten when a new enum member is introduced.
        /// </summary>
        public static void FailOnNewEnumValue(Type enumType, int expectedValueCount)
        {
            FailOnNewEnumValue(enumType, expectedValueCount, string.Format(CultureInfo.InvariantCulture, "Add unit test for new enum value of type {0}", enumType.FullName));
        }

        /// <summary>
        /// Encapsulates enum value checking and fails when unexpected number of enum values.
        /// Should be called on each test which handles enum values to ensure that no place in the codebase is forgotten when a new enum member is introduced.
        /// </summary>
        public static void FailOnNewEnumValue(Type enumType, int expectedValueCount, string failureMessage)
        {
            Assert.AreEqual(expectedValueCount, Enum.GetValues(enumType).Length, failureMessage);
        }
    }

Then in some unit test that uses the enum, we do:

[Test]
public void TestNoNewEnumValues()
{
     EnumTestHelper.FailOnNewEnumValue(typeof(MyEnum), 5);
}

This does not cause a build time error, but still prevents pushing such an error to prod, because we cannot merge changes that fail an unit test.

PMF
  • 14,535
  • 3
  • 23
  • 49