-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
main-eisop: Fix new incorrect errors involving wildcards #200
Comments
I'd hoped that removing the "wonkyness" workaround (discussed at that link) would help. It does not. |
I had also said:
I am pretty sure that I am thinking of this: https://github.com/jspecify/checker-framework/blob/b84882d9b2f77c99081118cc7ad095dfcc3484f5/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeValidator.java#L561-L570 Here's the corresponding section in eisop: https://github.com/eisop/checker-framework/blob/d30ebee6d096acfbc03bf6eec8672770d046cd77/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeValidator.java#L587 If I recall correctly, the Checker Framework used to have a relatively understandable-to-me justification for requiring that type arguments be in bounds because it didn't implement capture conversion (which would otherwise bring them "effectively" into bounds). But when it implemented capture conversion, that justification became less clear to me. Yet... possibly that's when the check I short-circuited was added?? Maybe the check is addressing some subtlety like what happens if you, say, declare a type parameter like in Maybe eisop could provide a way to disable that check, or maybe I can figure out whether it should be succeeding yet isn't. If anything, I would have hoped that the fixes for intersection types would have helped us here.... |
I got to wondering if the errors have existed on I tried some things. (Spoiler: I didn't figure it out.) Reverting the defaulting change from https://github.com/jspecify/jspecify-reference-checker/pull/178/files only makes the results for the samples worse. (It looks like it may roughly (but seemingly not exactly) break the tests that I'd marked as fixed in jspecify/jspecify#584, but I'm just guessing based on numbers of failures, not on any specific details of the failures.) Additionally changing the The next thing to try might have been to undo eisop/checker-framework#746. But that looks potentially invasive, so I'm not going to go down that road: All I was looking for was any potential obvious lead on what was wrong. Without such a lead, it makes sense to just fire up the debugger. |
Here's one set of unexpected diagnostics from strict mode:
Those correspond to these lines in the samples. At first glance, I'd say:
It may be that this mess in our checker-framework fork was previously saving us in this situation. Until I looked at it just now, I had been thinking that its main purpose was to handle a strange case with unspecified nullness, which I think we'll no longer need after the discussion earlier this year on jspecify/jspecify#248. But that is probably only part of the story, unless "the weird way we do substitution and type variables" is referring exclusively to that. (I think it's probably more general.) I could try copying and pasting that in to see what happens. But again, I think I'll probably fire up the debugger first, assuming that I still have time left today to do so :) |
<?>
for a non-nullable type parameter
Here's a more modern, trimmed down file for the first error: import org.jspecify.annotations.NullMarked;
class CaptureConvertedToObject {
@NullMarked
Object x6(UnspecBounded<? extends Lib> x) {
return x.get();
}
interface UnspecBounded<T> {
T get();
}
interface Lib {}
} And the output:
I'm now debugging with |
One thing that I notice is that the type of expression has
I would have guessed that that was what #178 would accomplish in its change to I could imagine that this could explain many of the wildcard errors (though probably not the specific one I'm looking at now, as shown in the previous post). (@wmdietl for any thoughts) |
In After Something then goes wrong inside Inside that method:
I'll keep stepping through things, but that seems like a large enough dump of information for its own post :) |
So the step where something goes wrong is (Note that for these calls, we have a We end up with OK, So I do I think that the earlier |
Just as my comment in the "special code" linked above suggests that an |
Perhaps |
Wait, it looks like we don't have a call for the |
My override of Maybe I can handle it in But maybe all this would be too general, given that |
But wait, earlier I was just saying that I'd "preemptively apply the parametric qualifier," and I was talking about doing it only for receiver types in |
I notice that I have a small patch in place: --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java
+++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java
@@ -1097,6 +1097,14 @@ final class NullSpecAnnotatedTypeFactory
return super.visitDeclared(type, p);
}
+ @Override
+ public Void visitExecutable(AnnotatedExecutableType method, Void aVoid) {
+ if (method.getReceiverType() != null) {
+ method.getReceiverType().getTypeArguments().forEach(a -> a.addAnnotation(parametricNull));
+ }
+ return super.visitExecutable(method, aVoid);
+ }
+
@Override
public Void visitPrimitive(AnnotatedPrimitiveType type, Void p) {
type.replaceAnnotation(minusNull); I notice that that also applies I still don't really understand the Checker Framework model for type-variable usages, and I can't remember what I understood of That may not even turn out to be relevant to the problem. I'll have to keep tracing through to see what happens. |
Somehow things still do go wrong inside
We enter none of the So... So maybe the line we care about is: copy.setReturnType(visit(original.getReturnType(), originalToCopy)); Yes, that one actually appears to be trouble. OK. We're back in Sigh, wait: I may have over-minimized/-modernized my example: Originally, So I haven't really been testing what I wanted recently. Still, I know that something was broken with the original code and original example. So now I get to retest to see if actually testing my fix with the proper sample input works.... |
So now we're here: import org.jspecify.annotations.NullMarked;
class CaptureConvertedToObject {
@NullMarked
Object x6(UnspecBounded<? extends Lib> x) {
return x.get();
}
interface UnspecBounded<Z> {
@NullMarked
Z get();
}
interface Lib {}
} That passes. But if I remove my new Was there something about |
Somehow it's broken only with an explicit That's good news in the sense that |
Well, it's also broken with an explicit import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullUnmarked;
class CaptureConvertedToObject {
@NullMarked
Object x6(UnspecBounded<? extends Lib> x) {
return x.get();
}
@NullUnmarked
interface UnspecBounded<Z> {
@NullMarked
Z get();
}
interface Lib {}
} |
It's broken there because It turns out that This sounds a little familiar to me, or maybe I'm confusing it with some other feature about defaulting that seemed off to me. |
OK, I have a fix that I'll be sending to eisop/checker-framework: --- a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java
+++ b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java
@@ -1,5 +1,7 @@
package org.checkerframework.framework.util.defaults;
+import static java.util.stream.Collectors.toCollection;
+
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
@@ -11,6 +13,8 @@ import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
+import java.util.EnumSet;
+import java.util.stream.Collectors;
import org.checkerframework.checker.interning.qual.FindDistinct;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.AnnotatedFor;
@@ -716,8 +720,14 @@ public class QualifierDefaults {
if (qualifiers == null || qualifiers.isEmpty()) {
qualifiers = parentDefaults;
} else {
- // TODO(cpovirk): What should happen with conflicts?
- qualifiers.addAll(parentDefaults);
+ // TODO(cpovirk): Consider converting DefaultSet to a Map to avoid duplicates that way.
+ Set<TypeUseLocation> setByDirect =
+ qualifiers.stream()
+ .map(d -> d.location)
+ .collect(toCollection(() -> EnumSet.noneOf(TypeUseLocation.class)));
+ parentDefaults.stream()
+ .filter(d -> !setByDirect.contains(d.location))
+ .forEach(qualifiers::add);
}
/* TODO: it would seem more efficient to also cache null/empty as the result. [edit: Note to self: Cite eisop/checker-framework#741, even though that is a bit different.] That fixes the specific problem I've been working on. I also ran the samples:
That last problem would be explained by the other wildcard problem that I've referred to above. So I think we're still moving in the right direction. |
At that point (as always, as best I can recall), my Other loose ends (in addition to that patch):
I thought maybe I had one more, but I'd be happy if there are "only" 3 :) [edit: Ah, it was from back before I figured out why I was seeing different behavior with different combinations of And that is all on top of:
|
(We could also consider making the reference checker blow up if it ever detects a lower bound (other than the |
The other thing that I keep trying to remind myself is that we care much more about problems in lenient mode than in strict mode. (Now, it's possible that investigating problems in strict mode will lead to more general fixes, as it seems to have done here.) The good news is that lenient mode has fewer problems. (It's possible that that's more a natural result of its leniency rather than a sign of anything particularly great we did. But the recent fixes for problems we found in Google could also have helped lenient mode disproportionately (certainly in the case of #197) .) |
Oh, and I'd lost track of the fact that the problem still exists after my defaulting fix if I go back to using import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.NullnessUnspecified;
@NullMarked
class CaptureConvertedToObject {
Object x6(UnspecBounded<? extends Lib> x) {
return x.get();
}
interface UnspecBounded<Z extends @NullnessUnspecified Object> {
Z get();
}
interface Lib {}
} Again, (My |
Oh, and on the broader wildcard-capture front, I wanted to note: JSpecify represents intersection types differently than the Checker Framework does. That might not be true in a significant way for intersection types produced by capture conversion (as opposed to those produced by |
The problem didn't go away when I commented out this block. I suspect that the relevant work happens inside [edit: And even if I have time for checker work next week, I may focus on the changes to unspecified nullness and substitution instead.] |
See jspecify/jspecify#583 (review).
The text was updated successfully, but these errors were encountered: