Search code examples
c#visual-studiocompiler-warningscompiler-bugobsolete

Inconsistent reporting of obsolete member usages


Consider this class:

public class Number
{
    [System.Obsolete()] public          string ToExactString() => "";
    [System.Obsolete()] public override string ToString() => "";
}

Now, consider this code:

Number number = new();
string s1 = number.ToExactString();
string s2 = number.ToString();

And now, observe what happens:

  • The invocation of ToExactString() is rendered by Visual Studio with a strike-through, and the compiler generates warning CS0612: "'member' is obsolete".
  • The invocation of ToString() is also rendered by Visual Studio with a strike-through, but the compiler generates no warning whatsoever.

As a result:

It does not matter whether my aging eyes notice the strike-through on the invocation of ToExactString(), because the compiler will let me know during build, and I can even have "treat all warnings as errors" enabled for good measure, so I will will never accidentally run code invoking an obsolete method.

However, if I fail to notice the strike-through on the invocation of that ToString(), I will never know.

Note the following:

The difference between the two methods is that ToExactString() is not an override, while ToString() is an override of a non-obsolete method, but this ought to be completely irrelevant and completely inconsequential in this scenario, because I am holding a reference to Number, not a reference to object.

Not that it matters, but to be clear, in all of my C# projects I have CS0809: "Obsolete member overrides non-obsolete member" disabled, since this warning is completely pointless, as it is perfectly fine and even highly desirable in some cases to have an obsolete member which overrides a non-obsolete member, as the case is here with ToString(). The point here is that I want to create an obsolete override, which nobody should directly invoke, even though it may legitimately be invoked by code holding a reference to the base class.

Visual Studio renders all invocations with a strike-through, which goes to show that Visual Studio understands perfectly well which method is being invoked in each case, whereas Roslyn pretends to not understand which method is invoked in the case of ToString(), as if it being an override somehow matters.

If [System.Obsolete()] is replaced with [System.Obsolete("message")] then the warning on the invocation of ToExactString() changes to CS0618: 'member' is obsolete: 'message', but absolutely nothing else changes.

The questions:

  • Is this a bug in the C# compiler?
  • Is there a known fix or workaround?

Solution

  • This is by-design. See this issue on the roslyn GitHub repo, which is about the same problem on PasswordDeriveBytes.GetBytes. GetBytes is marked [Obsolete], but because it overrides DeriveBytes.GetBytes, there is no warning.

    This is because of how C#'s overload resolution/member lookup algorithm works. During member lookup, methods marked with override are not considered (See also this post). From the spec,

    A member lookup of a name N with K type arguments in a type T is processed as follows:

    • First, a set of accessible members named N is determined:

      • If T is a type parameter, [...]
      • Otherwise, the set consists of all accessible (§7.5) members named N in T, including inherited members and the accessible members named N in object. If T is a constructed type, the set of members is obtained by substituting type arguments as described in §15.3.3. Members that include an override modifier are excluded from the set.

    ...

    So even if number in number.ToString() is of type Number, the call does not bind to the overridden implementation at compile time. Of course, your overridden implementation is still called at runtime.

    Therefore, number.ToString() doesn't count as "using the [Obsolete] method" and there is no warning.

    You might be able to produce a warning if you write a roslyn analyzer, though I have never written one myself. I'd suggest that you just do what .NET did with PasswordDeriveBytes. Keep the [Obsolete], add a message to suggest using ToExactString, so that it at least shows up in the documentation.