No Is Check On This

ID

csharp.no_is_check_on_this

Severity

critical

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

CSharp

Tags

api-design, code_smell, inheritance

Description

Reports code that asks which of its own subtypes this happens to be: an is test against a pattern that names a runtime type — this is Circle, this is Circle c, this is not Circle, this is Circle or Square, this is Circle { Radius: > 10 } c — and a switch statement or switch expression whose governing expression is this. Code guarded that way is subclass-specific behaviour that has been written in the base type.

Rationale

When a type asks which of its own descendants it happens to be, the branch it takes is knowledge that belongs to the descendant. Two costs follow. Every new subclass forces an edit to the base type, so the class that should have been closed to modification never is. And a subclass added by someone who never reads this method quietly falls into the fallback branch, with no compiler diagnostic and no test failure to reveal it.

Declaring a virtual member and overriding it per subclass moves the decision to the type system: the dispatch is automatic and an unimplemented case is visible at the declaration site.

The spelling does not change the argument, so every modern form of the same test is reported: the negated pattern, the or / and combinators, the recursive pattern that adds property constraints to a type, and both switch forms. What matters is whether the pattern tests a runtime type somewhere inside it — this is not null does not, and is left alone, as are constant patterns, a property pattern with no type, the as conversion and exact-type checks written with GetType().

Type tests on any other operand are perfectly ordinary and are not reported — narrowing a parameter, a field or a collection element is how is is meant to be used.

public abstract class Shape
{
    public double Area()
    {
        if (this is Circle c)              // FLAW — Circle-specific maths in the base type
        {
            return 3.14159 * c.Radius * c.Radius;
        }
        return 0;
    }

    public bool IsFlat() => this is not Circle;      // FLAW — the same test, negated

    public string Kind() => this switch              // FLAW — the same test, as a switch
    {
        Circle => "circle",
        _ => "shape"
    };

    public bool Exists() => this is not null;        // OK — no runtime type is tested

    public string Name(object other)
    {
        if (other is Circle)               // OK — narrowing a value from outside
        {
            return "circle";
        }
        return "shape";
    }
}

public class Circle : Shape
{
    public double Radius;
}

Remediation

Declare a virtual (or abstract) member on the base type and move each branch body — or each switch arm — into the matching subclass override. Where the branch is a one-off and inheritance is not available, consider passing the behaviour in rather than inspecting the runtime type.