Closed wmdietl closed 7 months ago
A bit frustratingly, this doesn't fix #163 and #164. At least #163 I had hoped should be fixed by the change to TYPE_VARIABLE_USE
defaults. I'll look some more.
Hmm. The number of expected errors that are not found went to 411. On the other hand, the number of unexpected errors went to 3.
I can't compare against main-eisop
on CI, because for some reason it fails there: https://github.com/jspecify/jspecify-reference-checker/actions/runs/8058639754/job/22094984781#step:6:177
/home/runner/work/jspecify-reference-checker/jspecify-reference-checker/jspecify-reference-checker/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java:591: error: cannot find symbol
private static final Set<String> NULLNESS_ANNOTATIONS =
> Task :compileJava FAILED
^
symbol: class Set
location: class NullSpecVisitor
/home/runner/work/jspecify-reference-checker/jspecify-reference-checker/jspecify-reference-checker/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java:592: error: cannot find symbol
Set.of(
^
symbol: variable Set
location: class NullSpecVisitor
2 errors
What is strange about this is that there is no Set.of
in that file: https://github.com/jspecify/jspecify-reference-checker/blob/main-eisop/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java
Also, this PR builds successfully and doesn't touch NullSpecVisitor.java
. So I'm a bit confused what CI is doing...
So locally, I get in main-eisop
:
tests.NullSpecTest$Lenient > run[/home/wdietl/tmp/jspecify/jspecify-reference-checker-forked/build/conformanceTests/samples] FAILED
java.lang.AssertionError: 1203 out of 1225 expected diagnostics were found.
21 unexpected diagnostics were found:
whereas in this branch, as it is right now:
tests.NullSpecTest$Lenient > run[/home/wdietl/tmp/jspecify/jspecify-reference-checker-forked/build/conformanceTests/samples] FAILED
java.lang.AssertionError: 1146 out of 1225 expected diagnostics were found.
42 unexpected diagnostics were found:
So let me see what's going wrong here.
What is strange about this is that there is no
Set.of
in that file: https://github.com/jspecify/jspecify-reference-checker/blob/main-eisop/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java
I don't know where it's picking the commit from, but the logs show that it's picking 9cfc77d1a477371bcc743b7a3604cd8dde16c2a6, which does at least have the Set.of
call:
Is this the PR that you were seeing the surprise CI behavior for? (Sorry, I should really remember that....)
I just created 3 local clones:
Alongside each, I ran git clone git@github.com:jspecify/jspecify.git -b samples-google-prototype
. Then, in each, I ran ./gradlew jspecifySamplesTest --include-build ../jspecify
.
I think that's all expected.
Here are the details in test results:
Lenient samples
- 524 unexpected diagnostics were found
+ 486 unexpected diagnostics were found
Lenient implementwithnullabletypeargument
+ 1 unexpected diagnostic was found
Lenient wildcardswithdefault
- 3 unexpected diagnostics were found
Strict samples
- 1367 unexpected diagnostics were found
+ 1495 unexpected diagnostics were found
Strict implementwithnullabletypeargument
+ 1 unexpected diagnostic was found
Strict wildcardswithdefault
- 6 unexpected diagnostics were found
+ 4 unexpected diagnostics were found
I notice that this differs from the CI results, apparently because the CI for main-eisop
checks out a different branch of the jspecify
repo:
Does that all seem reasonable to you? (Again, I might not even be asking about the right PR/branch....)
Thanks for looking into this @cpovirk ! Yes, this is the branch we had been discussing earlier.
For main-eisop
and this PR you need to use the samples-google-prototype-eisop
branch of jspecify, because the error marker format changed.
I'll try to reproduce your numbers. Both locally and on CI, for the lenient test, I see:
tests.NullSpecTest$Lenient > run[/home/runner/work/jspecify-reference-checker/jspecify-reference-checker/jspecify-reference-checker/build/conformanceTests/samples] FAILED
java.lang.AssertionError: 816 out of 1227 expected diagnostics were found.
3 unexpected diagnostics were found:
...
411 expected diagnostics were not found:
Your numbers look very different, so I'm not sure what's going on.
Ah, thanks, I should have tried samples-google-prototype-eisop
. With that, I'm able to reproduce the 816/3/411 numbers for the lenient samples
test.
With this PR, I:
systemProperties += ['emit.test.debug': 'true']
to the tasks.withType(Test).configureEach
block in build.gradle
(not sure if this was necessary)javac -processor
line from the output (the one for samples
in lenient modeIt ends like this:
found : Lib<? extends Foo?>
required: Lib<? extends Foo>
ContainmentSuper.java:38:46: compiler.err.proc.messager: (type.argument.type.incompatible) $$ 4 $$ A $$ Check $$ Lib<? super Foo> $$ Lib<? super Foo?> $$ ( 1446, 1462 ) $$ incompatible type argument for type parameter A of Check.
found : Lib<? super Foo>
required: Lib<? super Foo?>
ContainmentSuperVsExtends.java:24:41: compiler.err.proc.messager: (type.argument.type.incompatible) $$ 4 $$ A $$ Check $$ Lib<? super Foo> $$ Lib<? extends Object> $$ ( 885, 901 ) $$ incompatible type argument for type parameter A of Check.
found : Lib<? super Foo>
required: Lib<? extends Object>
ContainmentSuperVsExtends.java:31:26: compiler.err.proc.messager: (type.argument.type.incompatible) $$ 4 $$ A $$ Check $$ Lib<? super Foo> $$ Lib<? extends Object> $$ ( 1136, 1152 ) $$ incompatible type argument for type parameter A of Check.
found : Lib<? super Foo>
required: Lib<? extends Object>
ContainmentSuperVsExtendsSameType.java:23:41: compiler.err.not.within.bounds: ContainmentSuperVsExtendsSameType.Lib<? super java.lang.Number>, A
133 errors
4 warnings
- compiler.note.proc.messager:
=====
The nullness checker is still early in development.
It has many rough edges.
For an introduction, see https://jspecify.dev/docs/user-guide
=====
The files are being processed in alphabetical order, so it seems that something goes wrong before we even get to ContravariantReturns
(which would be the very next file)—and of course many other files.
I wondered if I'd get more information if I went back to the normal, user-facing output, so I removed -Adetailedmsgtext
(and -XDrawDiagnostics
, which I think was irrelevant, oops). The output still ended mysteriously after ContainmentSuperVsExtendsSameType.java
.
Hmm, wait a minute: That's a javac error:
/tmp/tmp.Mrxwdt3AcN/main-eisop/jspecify-reference-checker/build/conformanceTests/samples/ContainmentSuperVsExtendsSameType.java:23: error: type argument Lib<? super Number> is not within bounds of type-variable A
new Check<Lib<? extends Number>, Lib<? super Number>>();
^
where A,F are type-variables:
A extends F declared in class Check
F extends @org.jspecify.annotations.Nullable Object declared in class Check
1 error
So I suspect that https://github.com/jspecify/jspecify/pull/485 broke this.
I guess that the runner used by jspecifySamplesTest
doesn't abort on javac errors.
Thanks for tracking this down, @cpovirk ! I'll look into why javac errors aren't surfaced properly and into fixing the problem introduced in https://github.com/jspecify/jspecify/pull/485.
I opened https://github.com/jspecify/jspecify/pull/498 to fix the compilation error. While doing that I ran into https://github.com/google/error-prone/issues/4343. I ended up disabling error-prone on samples anyways, b/c there are too many warnings for the hand-crafted samples.
With that fixed, the numbers in this PR look much better. I'll still try to first track down why javac errors don't show up in test output - they do show up in stock EISOP.
The mismatch is
-PASS: implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java: no unexpected facts
+FAIL: implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java: no unexpected facts
I'm trying to find what is different in CI.
I kicked off an internal build with the current code in this PR (752ad0978019cdfdbe6675ffa807a1486e55858e). I'll have to sort through the result in more detail on another day, but the main thing that jumps out to me at the moment is a lot of messages along the lines of found: Foo<T> | required: Foo<T>
. It's somewhat like https://github.com/jspecify/jspecify-reference-checker/issues/164 but not resricted to ? super
:
java/com/google/common/collect/HashBiMap.java:707: error: [override.param.invalid] Incompatible parameter type for entry.
Entry<V, K> output(BiEntry<K, V> entry) {
^
found : BiEntry<K, V>
required: BiEntry<K, V>
Consequence: method in
Entry<V, K> output(BiEntry<K, V>)
cannot override method in Itr<Entry<V, K>>
Entry<V, K> output(BiEntry<K, V>)
I kicked off an internal build with the current code in this PR (752ad09). I'll have to sort through the result in more detail on another day, but the main thing that jumps out to me at the moment is a lot of messages along the lines of
found: Foo<T> | required: Foo<T>
. It's somewhat like #164 but not resricted to? super
:
Thanks for re-running your tests!
I think what is happening is that the @NullMarked
is ignored when code comes from an Element
, that is, a different compilation unit or bytecode.
We treat @NullMarked
and @NullUnmarked
as changes to defaults. Defaults are however only applied when we check source code. In stock CF, we write the defaulted types back into the Element
, which then allows us to directly see the annotations in bytecode.
The reference checker intentionally does not store defaults back into bytecode. I think we need to change how we load bytecode and similarly look for @Null(Un)Marked
on enclosing elements.
Similarly, when looking at stub files, stock CF only looks at @AnnotatedFor
. I'm not sure the defaults set by @Null(Un)Marked
have any effect in stubs and the annotated JDK.
Does either of these sound like you had work-arounds for them before?
I don't see how this PR would have made that behavior different, so I think we can still merge this.
Because the @NullMarked
is ignored, the upper bounds for these type variables are UnknownNullness
and therefore the override check fails. But fixing the override check, ala #164, is probably not enough and we need to fix the above handling of defaults.
(The error message is pretty cryptic, as it's the same type twice. The reference checker intentionally doesn't show bounds of type variables. I believe that in the bound we would see unknown nullness. Then we would still see the same type twice, but at least if one remembers that subtyping for unknown nullness is not reflexive, it might make sense.)
However, I'm still trying to figure out why the conformance test is different on CI than on my local machine...
Some notes:
Itr
comes from the same compilation unit as the subclass: Both are in HashBiMap.java
. So I don't think that a difference in defaulting between source files and class files explains it.getDeclAnnotation
says it supports aliases), and that seems to support the idea that we could read defaults from class files (not source-retention @DefaultQualifier
itself but at least class/runtime-retention aliases like @NullMarked
). And when I added println debugging to defaultsAtDirect(Element elt)
, I saw it called for types like java.lang.Object
. (That doesn't mean that it works :) I didn't investigate further. But the checker does seem to be trying.) If I'm right about my first bullet, then this is academic for now, but it might matter for later.import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class HashBiMapAlike<E extends @Nullable Object> {
abstract class Super {
abstract void accept(Entry<E> entry);
}
abstract class Sub extends Super {
@Override
abstract void accept(Entry<E> entry);
}
}
interface Entry<E extends @Nullable Object> {}
$ ./demo HashBiMapAlike.java
HashBiMapAlike.java:12: error: [override.param.invalid] Incompatible parameter type for entry.
abstract void accept(Entry<E> entry);
^
found : Entry<E>
required: Entry<E>
Consequence: method in Sub
void accept(Entry<E>)
cannot override method in Super
void accept(Entry<E>)
1 error
main-eisop
(using my instructions from https://github.com/jspecify/jspecify-reference-checker/pull/165#issuecomment-2013277831), but that still leaves the question of what sort of changes to defaulting or @ParametricNull
might be the cause.tl;dr I think the conformance-test runner needs to sort the input files. Not that that should matter, but I've seen it matter for my original samples
integration, which I have set up to sort files.
Details:
I mentioned this part on an internal doc, but I'll say it here, too: The tests pass for me locally, as well. (That includes if I use JDK17 and if I use the ./gradlew build conformanceTests demoTest --include-build ../jspecify
command line from CI.)
I wonder if we're seeing something similar to https://github.com/jspecify/jspecify-reference-checker/pull/165#issuecomment-2015364311, in which an error from javac itself stops us from checking many files. And maybe our different systems have very slight difference in how javac behaves, either because of different versions or because of environmental perturbations, perhaps the most likely of which would be filesystem iteration order?? Even if we're not seeing a javac error, I have seen cases (unfortunately) in which the order of files matters.
To test that, I just tried a run in which the conformance tests use ImmutableSortedSet
instead of ImmutableList
for their input files. And... that produced the failure locally!
diff (-expected +actual):
@@ -1,4 +1,4 @@
-# 73 pass; 500 fail; 573 total; 12.7% score
+# 72 pass; 501 fail; 573 total; 12.6% score
FAIL: AnnotatedInnerOfNonParameterized.java: no unexpected facts
FAIL: AnnotatedInnerOfParameterized.java: no unexpected facts
FAIL: AnnotatedReceiver.java: no unexpected facts
@@ -532,7 +532,7 @@
FAIL: ignoreAnnotations/ignoreannotations/IgnoreAnnotations.java: no unexpected facts
PASS: implementWithNullableTypeArgument/implementwithnullabletypeargument/MyFunction.java: no unexpected facts
PASS: implementWithNullableTypeArgument/implementwithnullabletypeargument/SuperFunction.java: no unexpected facts
-PASS: implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java: no unexpected facts
+FAIL: implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java: no unexpected facts
PASS: memberSelectNonExpression/memberselectnonexpression/MemberSelectNonExpressions.java: no unexpected facts
PASS: nonPlatformTypeParameter/nonplatformtypeparameter/NonPlatformTypeParameter.java: no unexpected facts
PASS: nullnessUnspecifiedTypeParameter/nullnessunspecifiedtypeparameter/NullnessUnspecifiedTypeParameter.java:35:jspecify_nullness_mismatch
The extra error, incidentally, appears to be this:
OOPS: implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java:25:test:cannot-convert:SuperFunction!<Object!, Object!> to Object apply(Object)!
2024-03-28T14:07:45.7406117Z /home/runner/work/jspecify-reference-checker/jspecify-reference-checker/jspecify-reference-checker/build/conformanceTests/samples/implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java:25: (override.return.invalid) Incompatible return type.
2024-03-28T14:07:45.7409196Z @Nullable Object apply(@Nullable Object f);
2024-03-28T14:07:45.7409908Z ^
2024-03-28T14:07:45.7410397Z found : Object?
2024-03-28T14:07:45.7410929Z required: Object
That looks like a substitution problem. (Sorry, I can't seem to remember whether your existing suspicion had been a substitution problem or not.)
Since there are multiple files involved in that sample, the ordering hypothesis is relatively plausible. And sure enough:
$ ./demo ../jspecify/samples/implementWithNullableTypeArgument/implementwithnullabletypeargument/SuperFunction.java ../jspecify/samples/implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java ../jspecify/samples/implementWithNullableTypeArgument/implementwithnullabletypeargument/MyFunction.java
(no errors)
$ ./demo ../jspecify/samples/implementWithNullableTypeArgument/implementwithnullabletypeargument/MyFunction.java ../jspecify/samples/implementWithNullableTypeArgument/implementwithnullabletypeargument/SuperFunction.java ../jspecify/samples/implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java
../jspecify/samples/implementWithNullableTypeArgument/implementwithnullabletypeargument/User.java:25: error: [override.return.invalid] Incompatible return type.
@Nullable Object apply(@Nullable Object f);
^
found : Object?
required: Object
Consequence: method in NullableObjectFunction
Object? apply(Object?)
cannot override method in SuperFunction<Object, Object>
Object apply(Object)
1 error
Depends on https://github.com/eisop/checker-framework/pull/708.
Something still seems to be missing, failure in
NullUnmarkedUndoesNullMarkedForWildcards.java
is odd.Fixes #159, #161, #163.