panacekcz / checker-framework

Pluggable type-checking for Java
http://checkerframework.org/
Other
0 stars 0 forks source link

Test CheckAgainstNegativeOne fails when string index annotations enabled #4

Closed panacekcz closed 6 years ago

panacekcz commented 7 years ago

When index annotations on String are enabled, the test CheckAgainstNegativeOne from the index suite fails.

class CheckAgainstNegativeOne {

    public static String replaceString(String target, String oldStr, String newStr) {
        if (oldStr.equals("")) {
            throw new IllegalArgumentException();
        }

        StringBuffer result = new StringBuffer();
        int lastend = 0; // invariant: 0<=lastend && lastend<=target.length()
        int pos; // invariant: -1<=pos && pos+oldStr.length()<=target.length()
        while ((pos = target.indexOf(oldStr, lastend)) != -1) {
            result.append(target.substring(lastend, pos));
            result.append(newStr);
            lastend = pos + oldStr.length();
        }
        result.append(target.substring(lastend));
        return result.toString();
    }
}

The test would pass if:

panacekcz commented 7 years ago

A simpler solution that does not require changes to the subtyping relation:

However, this postcondition is not sound, because it states, that pos+str.length()-1<target.length(). This may not hold when the string is not found. For example when str=="01234" and target=="", then target.indexOf(str)+str.length()-1 is -1+5-1 = 3 which is not lower than 0.

panacekcz commented 7 years ago

Version using both offsets is in the bounded-strings-indexof branch (https://github.com/panacekcz/checker-framework/commit/684557233c8074d857a152475b4dd6de5e451207)

panacekcz commented 7 years ago

The core of this issue is to support this pattern in the upper bound checker:

int i = receiver.indexOf(argument);
if(i != -1){
 @LTEqLengthOf("receiver") int j = i + argument.length();
}

This code should not cause any warnings. However, if the check against -1 is missing, the upper bound checker should report a warning, because if argument is longer than receiver, j might end up being greater than the length of receiver.

A sound upper-bound annotation for the return value of indexOf would be @LTLengthOf(value={"this", "this"}, offset={"-1", "min(#1.length()-1,this.length())"}), which correctly handles the case where the argument is longer than the receiver. However, this will not help solving this issue, because it does not express the fact that if i!=-1, then argument is not longer than receiver. Unless the checker knows this information, it cannot refine the offset min(#1.length()-1,this.length()) to the required #1.length()-1.

This leads me to conclusion that some kind of new annotation is necessary. Possible solutions:

Variant 1: Extending the upper bound hierarchy

The idea is to take LTLengthOf and make a relaxed version LTLengthOfOrNegativeOne which additionally allows -1. indexOf would return LTLengthOfOrNegativeOne(value={"this", "this"}, offset={"-1", "#1.length()-1"}) This annotation would be in most cases treated the same as the corresponding LTLengthOf annotation ignoring all value-offset pairs with non-zero offsets. The only difference would be one of those:

Variant 2: A new special annotation

Add an annotation specifically for this purpose, such as @IndexOf(value="argument", in="receiver"). The upper bound checker would query this annotation and infer upper bound annoations as appropriate. This has the advantage of having a clear meaning and not complicating the upper bound hierarchy.

kelloggm commented 7 years ago

I'll leave my thoughts on each of the variants you suggested here.

Variant 1: The meaning of your proposed annotation (as you define it and as I would understand from its name) is actually not different from the meaning of LTLengthOf: LTLengthOf can definitely mean -1. An variable i with type @LTLengthOf("a") has only one invariant: i < a.length (or a.length() for Strings). Trivially, substituting -1 for i makes that invariant true. It's clear to me that there's some additional information being carried by your proposed annotation - the knowledge that checking against -1 allows us to conclude that the calls to indexOf are safe. I think this information is separate from the Upper Bound hierarchy, so I do not favor this option.

Variant 2: This seems like a more reasonable plan. I do not think that putting it in the Search Index hierarchy is the right move, though - that will cause headaches down the road when we encounter a case where indexOf and binarySearch are used together. Further, though the concepts are similar they're not identical. I wouldn't want to work through what should be a subtype of what, and I imagine you wouldn't either. If we were to take this option, I'd prefer variant 2c: create a specialized, single-purpose checker, like we did for binarySearch.

However, I'm not convinced that this is worth pursuing. We implemented the Search Index Checker because binary search is used in a lot of array-using code. I know that indexOf is used a lot in conjunction with Strings, but I wonder how common this particular case really is. Binary search is an easy and compelling use case to explain for its own specialized checker, and this really isn't. I'm actually comfortable just leaving this as a known false positive and not addressing it, unless it's more common in real code than I currently believe.

panacekcz commented 7 years ago

The difference between LTLengthOfOrNegativeOne and LTLengthOf is only for annotations with some non-zero offset. Otherwise, they are exactly the same. An annotation with a non-zero offset such as LTLengthOf(value="a", offset="b.length+1") may disallow the -1 value. In that case, LTLengthOfOrNegativeOne simply adds it back in. Variant 1 is quite general, but it would complicate the upper bound hierarchy, making it harder to understand and possibly harder to extend in other directions. I would probably prefer variant 2a. Anyway, I will leave this as it is now. This issue is linked from the CheckAgainstNegativeOne test.

kelloggm commented 7 years ago

I think that's a good plan.

panacekcz commented 6 years ago

Fixed by typetools#1461.