Search code examples
c#.netnullablecode-contractsverification

Code Contracts - Static Checker doesn't get nullable types?


I think I found a bug in the static contract checking tool. Is there a way besides tagging the whole thing with [ContractVerification(false)] ?

class Employee
{
    public int? HierarchyLevel { get; private set; }

    public Employee(int? level)
    {
        Contract.Requires<ArgumentException>(
            (!level.HasValue) 
            || 
            level >= 0 && level <= 10);
        Contract.Ensures(
            (!HierarchyLevel.HasValue) 
            || 
            (HierarchyLevel.Value >= 0 && HierarchyLevel.Value <= 10));
        this.HierarchyLevel = level; //unproven

        // this doesnt work either
        //if (!level.HasValue) {
        //    this.HierarchyLevel = new Nullable<int>();
        //} else {
        //    this.HierarchyLevel = new Nullable<int>(level.Value);
        //}

        // can't even make the static checker shut up with that:
        //Contract.Assume(
        //    (!HierarchyLevel.HasValue) 
        //    || 
        //    (HierarchyLevel.Value >= 0 && HierarchyLevel.Value <= 10));
    }
}

Surprisingly, The version below works. But I don't want to start writing unidiomatic code or introduce my own version of Nullable just to please the contract verification tool.

class Employee2
{
    public int HierarchyLevel { get; private set; }
    public bool HasLevel { get; private set; }
    public Employee2(int level, bool hasLevel)
    {
        Contract.Requires<ArgumentException>(!hasLevel || level >= 0 && level <=10);
        if (hasLevel) {
            HasLevel = true;
            HierarchyLevel = level;
        } else {
            HasLevel = false;
            HierarchyLevel = -1;
        }
    }
    [ContractInvariantMethod]
    private void ObjectInvariant()
    {
        Contract.Invariant(
            (!HasLevel) || 
            (HierarchyLevel >= 0 && HierarchyLevel <= 10));
    }
}

Solution

  • First of all, you have misplaced some braces in your code (but this does not solve your problem):

    Contract.Requires<ArgumentException>(!level.HasValue
        || (level.Value >= 0 && level.Value <= 10));
    Contract.Ensures(!HierarchyLevel.HasValue
        || (HierarchyLevel.Value >= 0 && HierarchyLevel.Value <= 10));
    

    I think you may be right: the static checker cannot prove everything yet, and also depends on the Base Class Library. People in this thread seem to be talking about how the static checker cannot properly understand nullable types due to an error in the contract implementation of nullable types in general.

    Of course, you can also write the following, which solves your problem:

    Contract.Requires<ArgumentException>(!level.HasValue
        || (level.Value >= 0 && level.Value <= 10);
    Contract.Ensures(HierarchyLevel == level);
    

    And here is another solution to your problem. Put the conditions into a separate method, and mark the method with the PureAttribute (indicating that the method has no side-effects). Then, apply the method on both the incoming argument and the ensured value, like this:

    [Pure]
    public static bool IsInRange(int? value)
    {
        return !value.HasValue
            || (value >= 0 && value <= 10);
    }
    
    public Employee(int? level)
    {
        Contract.Requires<ArgumentException>(IsInRange(level));
        Contract.Ensures(IsInRange(HierarchyLevel));
        this.HierarchyLevel = level;
    }