From 73a3c08868f035ccbfb6cf05a08da634bebfa50f Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Tue, 9 Apr 2024 20:19:07 -0400 Subject: [PATCH 01/23] Use EISOP CF sources (#118) Co-authored-by: David P. Baker --- initialize-project | 11 +- settings.gradle | 12 +- .../jspecify/nullness/NullSpecAnalysis.java | 5 +- .../NullSpecAnnotatedTypeFactory.java | 407 ++++++------------ .../jspecify/nullness/NullSpecChecker.java | 6 +- .../jspecify/nullness/NullSpecTransfer.java | 8 +- .../jspecify/nullness/NullSpecVisitor.java | 13 +- src/test/java/tests/ConformanceTest.java | 8 +- src/test/java/tests/NullSpecTest.java | 22 +- tests/ConformanceTest-report.txt | 14 +- tests/ConformanceTestOnSamples-report.txt | 30 +- 11 files changed, 208 insertions(+), 328 deletions(-) diff --git a/initialize-project b/initialize-project index c8783b63..cd1f9304 100755 --- a/initialize-project +++ b/initialize-project @@ -11,7 +11,7 @@ # Set SHALLOW=1 to clone sibling projects at depth 1. # # This script automatically tries to download your fork of -# jspecify/checker-framework, jspecify/jspecify, or jspecify/jdk, if they exist. +# eisop/checker-framework, jspecify/jspecify, or jspecify/jdk, if they exist. # It uses the URL of the origin remote (the default remote created when cloning # a repo) to determine that. # @@ -55,12 +55,17 @@ git_clone() { local forking_org forking_org="$(forking_org)" - if [[ -n "${forking_org}" ]]; then + if [[ -n "${forking_org}" ]] && [[ "${forking_org}" != "https://github.com/jspecify" ]]; then if run "${git[@]}" "${forking_org}/${repo}.git" "../${repo}"; then return fi fi - run "${git[@]}" "https://github.com/jspecify/${repo}.git" "../${repo}" + if [[ "${repo}" == checker-framework ]]; then + forking_org=https://github.com/eisop + else + forking_org=https://github.com/jspecify + fi + run "${git[@]}" "${forking_org}/${repo}.git" "../${repo}" } git_clone jdk --depth 1 --single-branch diff --git a/settings.gradle b/settings.gradle index a440bde8..5734a5f4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,13 +15,13 @@ includeBuild("../checker-framework") dependencyResolutionManagement { versionCatalogs { libs { - version("checkerFramework", "3.21.5-SNAPSHOT") + version("checkerFramework", "3.42.0-eisop2-SNAPSHOT") - library("checkerFramework-checker", "org.checkerframework", "checker").versionRef("checkerFramework") - library("checkerFramework-checker-qual", "org.checkerframework", "checker-qual").versionRef("checkerFramework") - library("checkerFramework-framework", "org.checkerframework", "framework").versionRef("checkerFramework") - library("checkerFramework-framework-test", "org.checkerframework", "framework-test").versionRef("checkerFramework") - library("checkerFramework-javacutil", "org.checkerframework", "javacutil").versionRef("checkerFramework") + library("checkerFramework-checker", "io.github.eisop", "checker").versionRef("checkerFramework") + library("checkerFramework-checker-qual", "io.github.eisop", "checker-qual").versionRef("checkerFramework") + library("checkerFramework-framework", "io.github.eisop", "framework").versionRef("checkerFramework") + library("checkerFramework-framework-test", "io.github.eisop", "framework-test").versionRef("checkerFramework") + library("checkerFramework-javacutil", "io.github.eisop", "javacutil").versionRef("checkerFramework") library("errorProne-core", "com.google.errorprone:error_prone_core:2.18.0") library("errorProne-javac", "com.google.errorprone:javac:9+181-r4173-1") library("guava", "com.google.guava:guava:31.1-jre") diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnalysis.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnalysis.java index 31871566..82476abb 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnalysis.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnalysis.java @@ -14,12 +14,11 @@ package com.google.jspecify.nullness; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeMirror; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.AnnotationMirrorSet; final class NullSpecAnalysis extends CFAbstractAnalysis { NullSpecAnalysis(BaseTypeChecker checker, NullSpecAnnotatedTypeFactory factory) { @@ -37,7 +36,7 @@ public NullSpecStore createCopiedStore(NullSpecStore other) { } @Override - public CFValue createAbstractValue(Set annotations, TypeMirror underlyingType) { + public CFValue createAbstractValue(AnnotationMirrorSet annotations, TypeMirror underlyingType) { return defaultCreateAbstractValue(this, annotations, underlyingType); } } diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index cf7716ab..f09e7cac 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -15,9 +15,7 @@ package com.google.jspecify.nullness; import static com.google.jspecify.nullness.NullSpecAnnotatedTypeFactory.IsDeclaredOrArray.IS_DECLARED_OR_ARRAY; -import static com.google.jspecify.nullness.Util.IMPLEMENTATION_VARIABLE_LOCATIONS; import static com.google.jspecify.nullness.Util.nameMatches; -import static com.sun.source.tree.Tree.Kind.CONDITIONAL_EXPRESSION; import static com.sun.source.tree.Tree.Kind.IDENTIFIER; import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT; import static com.sun.source.tree.Tree.Kind.NOT_EQUAL_TO; @@ -33,15 +31,8 @@ import static javax.lang.model.type.TypeKind.DECLARED; import static javax.lang.model.type.TypeKind.TYPEVAR; import static javax.lang.model.type.TypeKind.WILDCARD; -import static org.checkerframework.framework.qual.TypeUseLocation.CONSTRUCTOR_RESULT; -import static org.checkerframework.framework.qual.TypeUseLocation.EXCEPTION_PARAMETER; -import static org.checkerframework.framework.qual.TypeUseLocation.IMPLICIT_LOWER_BOUND; -import static org.checkerframework.framework.qual.TypeUseLocation.OTHERWISE; -import static org.checkerframework.framework.qual.TypeUseLocation.RECEIVER; import static org.checkerframework.framework.util.AnnotatedTypes.asSuper; -import static org.checkerframework.framework.util.defaults.QualifierDefaults.AdditionalTypeUseLocation.UNBOUNDED_WILDCARD_UPPER_BOUND; import static org.checkerframework.javacutil.AnnotationUtils.areSame; -import static org.checkerframework.javacutil.AnnotationUtils.areSameByName; import static org.checkerframework.javacutil.TreePathUtil.enclosingClass; import static org.checkerframework.javacutil.TreeUtils.elementFromDeclaration; import static org.checkerframework.javacutil.TreeUtils.elementFromUse; @@ -71,7 +62,6 @@ import java.util.Map; import java.util.Set; import java.util.function.Predicate; -import javax.lang.model.AnnotatedConstruct; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -83,6 +73,7 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.framework.qual.TypeUseLocation; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeFormatter; @@ -114,6 +105,7 @@ import org.checkerframework.framework.util.DefaultQualifierKindHierarchy; import org.checkerframework.framework.util.QualifierKindHierarchy; import org.checkerframework.framework.util.defaults.QualifierDefaults; +import org.checkerframework.javacutil.AnnotationBuilder; final class NullSpecAnnotatedTypeFactory extends GenericAnnotatedTypeFactory< @@ -134,6 +126,25 @@ final class NullSpecAnnotatedTypeFactory final AnnotatedDeclaredType javaLangThreadLocal; final AnnotatedDeclaredType javaUtilMap; + private static final TypeUseLocation[] defaultLocationsMinusNull = + new TypeUseLocation[] { + TypeUseLocation.CONSTRUCTOR_RESULT, + TypeUseLocation.EXCEPTION_PARAMETER, + TypeUseLocation.IMPLICIT_LOWER_BOUND, + TypeUseLocation.RECEIVER, + }; + + private static final TypeUseLocation[] defaultLocationsUnionNull = + new TypeUseLocation[] { + TypeUseLocation.LOCAL_VARIABLE, TypeUseLocation.RESOURCE_VARIABLE, + }; + + private static final TypeUseLocation[] defaultLocationsUnspecified = + new TypeUseLocation[] { + // TypeUseLocation.UNBOUNDED_WILDCARD_UPPER_BOUND, TODO + TypeUseLocation.OTHERWISE + }; + /** Constructor that takes all configuration from the provided {@code checker}. */ NullSpecAnnotatedTypeFactory(BaseTypeChecker checker, Util util) { this(checker, util, checker.hasOption("strict"), /*withOtherWorld=*/ null); @@ -171,6 +182,100 @@ private NullSpecAnnotatedTypeFactory( * recognizing annotations by simple class name instead of by fully qualified name. */ + AnnotationMirror nullMarkedDefaultQualMinusNull = + new AnnotationBuilder(processingEnv, DefaultQualifier.class) + .setValue("value", MinusNull.class) + .setValue( + "locations", + new TypeUseLocation[] { + TypeUseLocation.EXCEPTION_PARAMETER, TypeUseLocation.OTHERWISE + }) + .setValue("applyToSubpackages", false) + .build(); + AnnotationMirror nullMarkedDefaultQualUnionNull = + new AnnotationBuilder(processingEnv, DefaultQualifier.class) + .setValue("value", Nullable.class) + .setValue( + "locations", + new TypeUseLocation[] { + TypeUseLocation.LOCAL_VARIABLE, TypeUseLocation.RESOURCE_VARIABLE, + // TypeUseLocation.UNBOUNDED_WILDCARD_UPPER_BOUND TODO + }) + .setValue("applyToSubpackages", false) + .build(); + AnnotationMirror nullMarkedDefaultQual = + new AnnotationBuilder(processingEnv, DefaultQualifier.List.class) + .setValue( + "value", + new AnnotationMirror[] { + nullMarkedDefaultQualMinusNull, nullMarkedDefaultQualUnionNull + }) + .build(); + + /* + * XXX: When adding support for aliases, make sure to support them here. But consider how to + * handle @Inherited aliases (https://github.com/jspecify/jspecify/issues/155). In particular, we + * have already edited getDeclAnnotations to remove its inheritance logic, and we needed to do so + * to work around another problem (though perhaps we could have found alternatives). + */ + addAliasedDeclAnnotation( + "org.jspecify.annotations.NullMarked", + DefaultQualifier.List.class.getCanonicalName(), + nullMarkedDefaultQual); + addAliasedDeclAnnotation( + "org.jspecify.nullness.NullMarked", + DefaultQualifier.List.class.getCanonicalName(), + nullMarkedDefaultQual); + // TODO: does this work as intended? + /* + * We assume that ProtoNonnullApi is like NullMarked in that it guarantees that *all* types + * are non-null, even those that would require type annotations to annotate (e.g., + * type-parameter bounds). This is probably a safe assumption, if only because such types + * might not arise at all in the generated code where ProtoNonnullApi is used. + */ + addAliasedDeclAnnotation( + "com.google.protobuf.Internal.ProtoNonnullApi", + DefaultQualifier.List.class.getCanonicalName(), + nullMarkedDefaultQual); + + AnnotationMirror nullUnmarkedDefaultQualMinusNull = + new AnnotationBuilder(processingEnv, DefaultQualifier.class) + .setValue("value", MinusNull.class) + .setValue("locations", defaultLocationsMinusNull) + .setValue("applyToSubpackages", false) + .build(); + AnnotationMirror nullUnmarkedDefaultQualUnionNull = + new AnnotationBuilder(processingEnv, DefaultQualifier.class) + .setValue("value", Nullable.class) + .setValue("locations", defaultLocationsUnionNull) + .setValue("applyToSubpackages", false) + .build(); + AnnotationMirror nullUnmarkedDefaultQualUnspecified = + new AnnotationBuilder(processingEnv, DefaultQualifier.class) + .setValue("value", NullnessUnspecified.class) + .setValue("locations", defaultLocationsUnspecified) + .setValue("applyToSubpackages", false) + .build(); + AnnotationMirror nullUnmarkedDefaultQual = + new AnnotationBuilder(processingEnv, DefaultQualifier.List.class) + .setValue( + "value", + new AnnotationMirror[] { + nullUnmarkedDefaultQualMinusNull, + nullUnmarkedDefaultQualUnionNull, + nullUnmarkedDefaultQualUnspecified, + }) + .build(); + + addAliasedDeclAnnotation( + "org.jspecify.annotations.NullUnmarked", + DefaultQualifier.List.class.getCanonicalName(), + nullUnmarkedDefaultQual); + addAliasedDeclAnnotation( + "org.jspecify.nullness.NullUnmarked", + DefaultQualifier.List.class.getCanonicalName(), + nullUnmarkedDefaultQual); + this.isLeastConvenientWorld = isLeastConvenientWorld; javaUtilCollection = createType(util.javaUtilCollectionElement); @@ -227,6 +332,26 @@ private NullSpecAnnotatedTypeFactory( } } + @Override + protected void addCheckedCodeDefaults(QualifierDefaults defs) { + // TODO: add false for subpackages once overload is added to CF. Shouldn't really matter. + defs.addCheckedCodeDefaults(minusNull, defaultLocationsMinusNull); + defs.addCheckedCodeDefaults(unionNull, defaultLocationsUnionNull); + defs.addCheckedCodeDefaults(nullnessOperatorUnspecified, defaultLocationsUnspecified); + } + + @Override + protected void addUncheckedStandardDefaults(QualifierDefaults defs) { + // TODO: figure out whether we need different defaults here + /* + defs.addUncheckedCodeDefaults(minusNull, defaultLocationsMinusNull); + defs.addUncheckedCodeDefaults(unionNull, defaultLocationsUnionNull); + defs.addUncheckedCodeDefaults(nullnessOperatorUnspecified, defaultLocationsUnspecified); + */ + // defs.addUncheckedCodeDefaults(nullnessOperatorUnspecified, new TypeUseLocation[] { + // TypeUseLocation.ALL }); + } + @Override protected Set> createSupportedTypeQualifiers() { return new LinkedHashSet<>(asList(Nullable.class, NullnessUnspecified.class, MinusNull.class)); @@ -240,11 +365,11 @@ protected QualifierHierarchy createQualifierHierarchy() { private final class NullSpecQualifierHierarchy extends NoElementQualifierHierarchy { NullSpecQualifierHierarchy( Collection> qualifierClasses, Elements elements) { - super(qualifierClasses, elements); + super(qualifierClasses, elements, NullSpecAnnotatedTypeFactory.this); } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { + public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { if (subAnno == null || superAnno == null) { /* * The stock CF never passes null to this method: It always expose *some* annotation @@ -784,131 +909,16 @@ protected void addCheckedStandardDefaults(QualifierDefaults defs) { */ } - @Override - protected void checkForDefaultQualifierInHierarchy(QualifierDefaults defs) { - /* - * We don't set normal checkedCodeDefaults. This method would report that lack of defaults as a - * problem. That's because CF wants to ensure that every[*] type usage is annotated. - * - * However, we *do* ensure that every[*] type usage is annotated. To do so, we always set a - * default for OTHERWISE on top-level elements. (We do this in populateNewDefaults.) See further - * discussion in addCheckedStandardDefaults. - * - * So, we override this method to not report a problem. - * - * [*] There are a few exceptions that we don't need to get into here. - */ - } - @Override protected QualifierDefaults createQualifierDefaults() { return new NullSpecQualifierDefaults(elements, this); } - private final class NullSpecQualifierDefaults extends QualifierDefaults { + private static final class NullSpecQualifierDefaults extends QualifierDefaults { NullSpecQualifierDefaults(Elements elements, AnnotatedTypeFactory atypeFactory) { super(elements, atypeFactory); } - @Override - protected void populateNewDefaults(Element elt, boolean initialDefaultsAreEmpty) { - /* - * Note: This method does not contain the totality of our defaulting logic. For example, our - * TypeAnnotator has special logic for upper bounds _in the case of `super` wildcards - * specifically_. - * - * Note: Setting a default here affects not only this element but also its descendants in the - * syntax tree. - */ - if (hasNullMarkedOrEquivalent(elt)) { - addElementDefault(elt, unionNull, UNBOUNDED_WILDCARD_UPPER_BOUND); - addElementDefault(elt, minusNull, OTHERWISE); - addDefaultToTopForLocationsRefinedByDataflow(elt); - /* - * (For any TypeUseLocation that we don't set an explicit value for, we inherit any value - * from the enclosing element, which might be a non-null-aware element. That's fine: While - * our non-null-aware setup sets defaults for more locations than just these, it sets those - * locations' defaults to minusNull -- matching the value that we want here.) - */ - } else if (hasNullUnmarked(elt) || initialDefaultsAreEmpty) { - /* - * We need to set defaults appropriate to non-null-aware code. In a normal checker, we would - * expect for such "default defaults" to be set in addCheckedStandardDefaults. But we do - * not, as discussed in our implementation of that method. - */ - - // Here's the big default, the "default default": - addElementDefault(elt, nullnessOperatorUnspecified, OTHERWISE); - /* - * OTHERWISE covers anything that does not have a more specific default inherited from an - * enclosing element (or, of course, a more specific default that we set below in this - * method). If this is a @NullUnmarked element, then it might have a had a @NullMarked - * enclosing element, which would have set a default for UNBOUNDED_WILDCARD_UPPER_BOUND. So - * we make sure to override that here. - */ - addElementDefault(elt, nullnessOperatorUnspecified, UNBOUNDED_WILDCARD_UPPER_BOUND); - - // Some locations are intrinsically non-nullable: - addElementDefault(elt, minusNull, CONSTRUCTOR_RESULT); - addElementDefault(elt, minusNull, RECEIVER); - - // We do want *some* of the CLIMB standard defaults: - addDefaultToTopForLocationsRefinedByDataflow(elt); - addElementDefault(elt, minusNull, IMPLICIT_LOWER_BOUND); - - /* - * But note one difference from the CLIMB defaults: We want the default for implicit upper - * bounds to match the "default default" of nullnessOperatorUnspecified, not to be - * top/unionNull. We accomplished this already simply by not making our - * addCheckedStandardDefaults implementation call its supermethod (which would otherwise - * call addClimbStandardDefaults, which would override the "default default"). - */ - } - } - - private void addDefaultToTopForLocationsRefinedByDataflow(Element elt) { - for (TypeUseLocation location : IMPLEMENTATION_VARIABLE_LOCATIONS) { - /* - * Handling exception parameters correctly is hard, so just treat them as if they're - * restricted to non-null values. Of course the caught exception is already non-null, so all - * this does is forbid users from manually assigning null to an exception parameter. - */ - if (location == EXCEPTION_PARAMETER) { - addElementDefault(elt, minusNull, location); - } else { - addElementDefault(elt, unionNull, location); - } - } - } - - @Override - protected boolean shouldAnnotateOtherwiseNonDefaultableTypeVariable(AnnotationMirror qual) { - /* - * CF usually doesn't apply defaults to type-variable usages. But in non-null-aware code, we - * want our default of nullnessOperatorUnspecified to apply even to type variables. - * - * But there are 2 other things to keep in mind: - * - * - CF *does* apply defaults to type-variable usages *if* they are local variables. That's - * because it will refine their types with dataflow. This CF behavior works fine for us: Since - * we want to apply defaults in strictly more cases, we're happy to accept what CF already - * does for local variables. (We do need to be sure to apply unionNull (our top type) in that - * case, rather than nullnessOperatorUnspecified. We accomplish that in - * addDefaultToTopForLocationsRefinedByDataflow.) - * - * - Non-null-aware code (discussed above) is easy: We apply nullnessOperatorUnspecified to - * everything except local variables. But null-aware code more complex. First, set aside local - * variables, which we handle as discussed above. After that, we need to apply minusNull to - * most types, but we need to *not* apply it to (non-local-variable) type-variable usages. - * (For more on this, see isNullExclusiveUnderEveryParameterization.) This need is weird - * enough that stock CF doesn't appear to support it. Our solution is to introduce this hook - * method into our CF fork and then override it here. Our solution also requires that we set - * up defaulting in a non-standard way, as discussed in addCheckedStandardDefaults and other - * locations. - */ - return areSame(qual, nullnessOperatorUnspecified); - } - @Override public boolean applyConservativeDefaults(Element annotationScope) { /* @@ -921,51 +931,6 @@ public boolean applyConservativeDefaults(Element annotationScope) { } } - @Override - protected void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean iUseFlow) { - super.addComputedTypeAnnotations( - tree, - type, - iUseFlow - /* - * TODO(cpovirk): Eliminate this workaround (which may cause problems of its own). But - * currently, it helps in some of our samples and in Guava, though I am unsure why. The - * problem it works around may well be caused by our failure to keep wildcards' - * annotations in sync with their bounds' annotations (whereas stock CF does). - * - * Note that the `type.getKind() != WILDCARD` check appears to be necessary before the - * CF implementation of capture conversion and unnecessary afterward, while the reverse - * is true of the `isCapturedTypeVariable` check. - */ - && type.getKind() != WILDCARD - && !isCapturedTypeVariable(type.getUnderlyingType()) - /* - * TODO(cpovirk): See if we can remove this workaround after merging the fix for - * https://github.com/typetools/checker-framework/issues/5042. - * - * The workaround becomes necessary after the CF implementation of capture conversion. - * Without it, we see dataflow decide that `b ? null : nullable` has a type of - * _non-null_, as in code like the following: - * - * https://github.com/google/guava/blob/156694066b5198740a820c6eef723fb86c054343/guava/src/com/google/common/base/Throwables.java#L470 - * - * (But I haven't been able to reproduce this in a smaller test.) - * - * Fortunately, I think the workaround is harmless: - * TypeFromExpressionVisitor.visitConditionalExpression calls getAnnotatedType on both - * candidate expressions, and getAnnotatedType applies dataflow. So the ternary should - * end up with the dataflow-correct result by virtue of applying lub to those types. - * - * (I think the only exception would be if someone performed a null check _on an entire - * ternary_ and then expected _another appearance of that same ternary_ to be recognized - * as non-null. That seems implausible.) - * - * (Still, it would be good to look into what's going on here in case it's a sign of a - * deeper problem.) - */ - && tree.getKind() != CONDITIONAL_EXPRESSION); - } - @Override protected TypeAnnotator createTypeAnnotator() { /* @@ -1466,63 +1431,6 @@ protected NullSpecAnalysis createFlowAnalysis() { return new NullSpecAnalysis(checker, this); } - @Override - public void addDefaultAnnotations(AnnotatedTypeMirror type) { - super.addDefaultAnnotations(type); - /* - * TODO(cpovirk): Find a better solution than this. - * - * The problem I'm working around arises during AnnotatedTypes.leastUpperBound on a - * JSpecify-annotated variant of this code: - * https://github.com/google/guava/blob/39aa77fa0e8912d6bfb5cb9a0bc1ed5135747b6f/guava/src/com/google/common/collect/ImmutableMultiset.java#L205 - * - * CF is unable to infer the right type for `LinkedHashMultiset.create(elements)`: It should - * infer `LinkedHashMultiset`, but instead, it infers `LinkedHashMultiset`. As expected, it sets isUninferredTypeArgument. As *not* expected, it gets to - * AtmLubVisitor.lubTypeArgument with type2Wildcard.extendsBound.lowerBound (a null type) - * missing its annotation. - * - * The part of CF responsible for copying annotations, including those on the extends bound, is - * AsSuperVisitor.visitWildcard_Wildcard. Under stock CF, copyPrimaryAnnos(from, typevar) "also - * sets primary annotations _on the bounds_." Under our CF fork, this is not the case, and we - * end up with an unannotated lower bound on the type-variable usage E (which, again, is itself - * a bound of a wildcard). - * - * (Aside: I haven't looked into how the _upper_ bound of the type-variable usage gets an - * annotation set on it. Could it be happening "accidentally," and if so, might it be wrong - * sometimes?) - * - * The result of an unannotated lower bound is a crash in NullSpecQualifierHierarchy.isSubtype, - * which passes null to areSame. - * - * The workaround: If we see a type-variable usage whose lower bound is a null type that lacks - * an annotation, we annotate that bound as non-null. This workaround shouldn't break any - * working code, but it may or may not be universally the right solution to a missing - * annotation. - * - * I am trying to ignore other questions here, such as: - * - * - Would it make more sense to set the lower bound to match the upper bound, as stock CF does? - * I suspect not under our approach, but I haven't thought about it. - * - * - Does trying to pick correct annotations even matter in the context of an uninferred type - * argument? Does the very idea of "correct annotations" lose meaning in that context? - * - * - Should we fix this in AsSuperVisitor instead? Or would it fix itself if we set bounds on - * our type-variable usages and wildcards in the same way that stock CF does? (Following stock - * CF would likely save us from other problems, too.) - * - * - What's up with the _upper_ bound, as discussed in a parenthetical above? - */ - if (type instanceof AnnotatedTypeVariable) { - AnnotatedTypeMirror lowerBound = ((AnnotatedTypeVariable) type).getLowerBound(); - if (lowerBound instanceof AnnotatedNullType - && !lowerBound.isAnnotatedInHierarchy(unionNull)) { - lowerBound.addAnnotation(minusNull); - } - } - } - @Override protected AnnotationFormatter createAnnotationFormatter() { return new DefaultAnnotationFormatter() { @@ -1821,48 +1729,11 @@ protected void addAnnotationsFromDefaultForType(Element element, AnnotatedTypeMi } private void addIfNoAnnotationPresent(AnnotatedTypeMirror type, AnnotationMirror annotation) { - if (!type.isAnnotatedInHierarchy(unionNull)) { + if (!type.hasAnnotationInHierarchy(unionNull)) { type.addAnnotation(annotation); } } - /* - * XXX: When adding support for aliases, make sure to support them here. But consider how to - * handle @Inherited aliases (https://github.com/jspecify/jspecify/issues/155). In particular, we - * have already edited getDeclAnnotations to remove its inheritance logic, and we needed to do so - * to work around another problem (though perhaps we could have found alternatives). - */ - private boolean hasNullMarkedOrEquivalent(Element elt) { - return getDeclAnnotations(elt).stream() - .anyMatch( - am -> - areSameByName(am, "org.jspecify.annotations.NullMarked") - || areSameByName(am, "org.jspecify.nullness.NullMarked")) - /* - * We assume that ProtoNonnullApi is like NullMarked in that it guarantees that *all* types - * are non-null, even those that would require type annotations to annotate (e.g., - * type-parameter bounds). This is probably a safe assumption, if only because such types - * might not arise at all in the generated code where ProtoNonnullApi is used. - */ - || hasAnnotationInCode(elt, "ProtoNonnullApi"); - } - - private boolean hasNullUnmarked(Element elt) { - return getDeclAnnotations(elt).stream() - .anyMatch( - am -> - areSameByName(am, "org.jspecify.annotations.NullUnmarked") - || areSameByName(am, "org.jspecify.nullness.NullUnmarked")); - } - - /** - * Returns whether the given element has an annotation with the given simple name. This method - * does not consider stub files. - */ - private static boolean hasAnnotationInCode(AnnotatedConstruct construct, String name) { - return construct.getAnnotationMirrors().stream().anyMatch(a -> nameMatches(a, name)); - } - @SuppressWarnings("unchecked") // safety guaranteed by API docs private T withMinusNull(T type) { // Remove the annotation from the *root* type, but preserve other annotations. diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java b/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java index f89ea601..027ddc74 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java @@ -23,7 +23,7 @@ import com.sun.source.util.TreePath; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.util.Log; -import java.util.SortedSet; +import java.util.NavigableSet; import java.util.TreeSet; import javax.lang.model.element.TypeElement; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -62,8 +62,8 @@ public final class NullSpecChecker extends BaseTypeChecker { public NullSpecChecker() {} @Override - public SortedSet getSuppressWarningsPrefixes() { - SortedSet prefixes = new TreeSet<>(); + public NavigableSet getSuppressWarningsPrefixes() { + TreeSet prefixes = new TreeSet<>(); prefixes.add("nullness"); return prefixes; } diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecTransfer.java b/src/main/java/com/google/jspecify/nullness/NullSpecTransfer.java index 13f6aefc..ee526c76 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecTransfer.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecTransfer.java @@ -18,7 +18,6 @@ import static com.sun.source.tree.Tree.Kind.NULL_LITERAL; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; -import static java.util.Collections.singleton; import static java.util.Collections.unmodifiableSet; import static java.util.stream.Collectors.toList; import static org.checkerframework.dataflow.expression.JavaExpression.fromNode; @@ -71,6 +70,7 @@ import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.javacutil.AnnotationMirrorSet; final class NullSpecTransfer extends CFAbstractTransfer { private final Util util; @@ -1019,7 +1019,8 @@ private boolean valueIsAtLeastAsSpecificAs(CFValue value, CFValue targetDataflow if (target == null) { return false; } - return atypeFactory.getQualifierHierarchy().greatestLowerBound(existing, target) == existing; + return atypeFactory.getQualifierHierarchy().greatestLowerBoundQualifiersOnly(existing, target) + == existing; } private static boolean isNullLiteral(Node node) { @@ -1052,7 +1053,8 @@ private void setResultValue( * if (clazz.cast(foo) != null) { return class.cast(foo); } */ result.setResultValue( - analysis.createAbstractValue(singleton(qual), result.getResultValue().getUnderlyingType())); + analysis.createAbstractValue( + AnnotationMirrorSet.singleton(qual), result.getResultValue().getUnderlyingType())); } private JavaExpression expressionToStoreFor(Node node) { diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java b/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java index a2d1a671..52000ef3 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java @@ -61,7 +61,6 @@ import com.sun.source.tree.TypeParameterTree; import com.sun.source.tree.VariableTree; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; @@ -76,6 +75,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; final class NullSpecVisitor extends BaseTypeVisitor { private final boolean checkImpl; @@ -101,9 +101,11 @@ private void ensureNonNull(Tree tree, String messageKey) { } } + /* TODO: implement feature to add extra args to return type errors. + * @Override protected String extraArgForReturnTypeError(Tree tree) { - /* + / * We call originStringIfTernary, not originString: * * If the statement is `return foo.bar()`, then the problem is obvious, so we don't want our @@ -116,10 +118,11 @@ protected String extraArgForReturnTypeError(Tree tree) { * the possibly null value (possibly both!). However, this gets tricky: If the branches return * `Foo?` and `Foo*`, then we ideally want to emphasize the `Foo?` branch *but*, at least in * "strict mode," not altogether ignore the `Foo*` branch. - */ + / String origin = originStringIfTernary(tree); return origin.isEmpty() ? "" : (origin + "\n"); } + */ private String originString(Tree tree) { while (tree instanceof ParenthesizedTree) { @@ -628,8 +631,8 @@ private boolean isClassCastAppliedToNonNullableType(MemberReferenceTree tree) { } @Override - protected Set getExceptionParameterLowerBoundAnnotations() { - return new HashSet<>(asList(AnnotationBuilder.fromClass(elements, MinusNull.class))); + protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { + return new AnnotationMirrorSet(asList(AnnotationBuilder.fromClass(elements, MinusNull.class))); } @Override diff --git a/src/test/java/tests/ConformanceTest.java b/src/test/java/tests/ConformanceTest.java index 8e401988..f4da4400 100644 --- a/src/test/java/tests/ConformanceTest.java +++ b/src/test/java/tests/ConformanceTest.java @@ -155,8 +155,8 @@ static final class DetailMessageReportedFact extends ReportedFact { private static final ImmutableSet CANNOT_CONVERT_KEYS = ImmutableSet.of( - "argument", - "assignment", + "argument.type.incompatible", + "assignment.type.incompatible", "atomicreference.must.include.null", "cast.unsafe", "lambda.param", @@ -165,9 +165,9 @@ static final class DetailMessageReportedFact extends ReportedFact { "methodref.return", "override.param", "override.return", - "return", + "return.type.incompatible", "threadlocal.must.include.null", - "type.argument"); + "type.argument.type.incompatible"); private static final ImmutableSet IRRELEVANT_ANNOTATION_KEYS = ImmutableSet.of( diff --git a/src/test/java/tests/NullSpecTest.java b/src/test/java/tests/NullSpecTest.java index fd198187..bb8a8075 100644 --- a/src/test/java/tests/NullSpecTest.java +++ b/src/test/java/tests/NullSpecTest.java @@ -156,20 +156,20 @@ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { || missing.getMessage().contains("jspecify_nullness_mismatch") || missing.getMessage().contains("test:cannot-convert")) { switch (unexpected.messageKey) { - case "argument": - case "assignment": + case "argument.type.incompatible": + case "assignment.type.incompatible": case "atomicreference.must.include.null": case "cast.unsafe": case "dereference": - case "lambda.param": - case "methodref.receiver.bound": - case "methodref.receiver": - case "methodref.return": - case "override.param": - case "override.return": - case "return": + case "lambda.param.type.incompatible": + case "methodref.receiver.bound.invalid": + case "methodref.receiver.invalid": + case "methodref.return.invalid": + case "override.param.invalid": + case "override.return.invalid": + case "return.type.incompatible": case "threadlocal.must.include.null": - case "type.argument": + case "type.argument.type.incompatible": return true; default: return false; @@ -196,7 +196,7 @@ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { * custom `*.annotated` error. This test probably doesn't confirm that second thing * anymore, but I did manually confirm that it is true as of this writing. */ - case "bound": + case "bound.type.incompatible": case "local.variable.annotated": case "type.parameter.annotated": case "wildcard.annotated": diff --git a/tests/ConformanceTest-report.txt b/tests/ConformanceTest-report.txt index 23c8a51e..333bd802 100644 --- a/tests/ConformanceTest-report.txt +++ b/tests/ConformanceTest-report.txt @@ -1,11 +1,11 @@ -# 90 pass; 24 fail; 114 total; 78.9% score -PASS: Basic.java:28:test:expression-type:Object?:nullable -PASS: Basic.java:28:test:sink-type:Object!:return +# 84 pass; 30 fail; 114 total; 73.7% score +FAIL: Basic.java:28:test:expression-type:Object?:nullable +FAIL: Basic.java:28:test:sink-type:Object!:return PASS: Basic.java:28:test:cannot-convert:Object? to Object! -PASS: Basic.java:34:test:expression-type:Object!:nonNull -PASS: Basic.java:34:test:sink-type:Object?:return -PASS: Basic.java:41:test:sink-type:Object?:nullableObject -PASS: Basic.java:43:test:sink-type:String!:testSinkType#nonNullString +FAIL: Basic.java:34:test:expression-type:Object!:nonNull +FAIL: Basic.java:34:test:sink-type:Object?:return +FAIL: Basic.java:41:test:sink-type:Object?:nullableObject +FAIL: Basic.java:43:test:sink-type:String!:testSinkType#nonNullString FAIL: Basic.java:49:test:expression-type:List!:nullableStrings PASS: Basic.java: no unexpected facts PASS: Irrelevant.java:28:test:irrelevant-annotation:Nullable diff --git a/tests/ConformanceTestOnSamples-report.txt b/tests/ConformanceTestOnSamples-report.txt index 34f25846..d1dec5af 100644 --- a/tests/ConformanceTestOnSamples-report.txt +++ b/tests/ConformanceTestOnSamples-report.txt @@ -1,4 +1,4 @@ -# 75 pass; 498 fail; 573 total; 13.1% score +# 67 pass; 506 fail; 573 total; 11.7% score FAIL: AnnotatedInnerOfNonParameterized.java: no unexpected facts FAIL: AnnotatedInnerOfParameterized.java: no unexpected facts FAIL: AnnotatedReceiver.java: no unexpected facts @@ -14,15 +14,15 @@ FAIL: ArraySubtype.java: no unexpected facts PASS: AssignmentAsExpression.java: no unexpected facts PASS: AugmentedInferenceAgreesWithBaseInference.java:35:jspecify_nullness_mismatch PASS: AugmentedInferenceAgreesWithBaseInference.java: no unexpected facts -PASS: BoundedTypeVariableReturn.java:27:jspecify_nullness_mismatch -PASS: BoundedTypeVariableReturn.java:32:jspecify_nullness_mismatch +FAIL: BoundedTypeVariableReturn.java:27:jspecify_nullness_mismatch +FAIL: BoundedTypeVariableReturn.java:32:jspecify_nullness_mismatch PASS: BoundedTypeVariableReturn.java: no unexpected facts FAIL: CaptureAsInferredTypeArgument.java:51:jspecify_nullness_mismatch FAIL: CaptureAsInferredTypeArgument.java:53:jspecify_nullness_mismatch FAIL: CaptureAsInferredTypeArgument.java:61:jspecify_nullness_mismatch FAIL: CaptureAsInferredTypeArgument.java:63:jspecify_nullness_mismatch FAIL: CaptureAsInferredTypeArgument.java: no unexpected facts -PASS: CaptureConversionForSubtyping.java: no unexpected facts +FAIL: CaptureConversionForSubtyping.java: no unexpected facts FAIL: CaptureConvertedToObject.java:71:jspecify_nullness_mismatch FAIL: CaptureConvertedToObject.java: no unexpected facts FAIL: CaptureConvertedToObjectUnionNull.java: no unexpected facts @@ -70,11 +70,11 @@ FAIL: CaptureConvertedUnspecToOther.java: no unexpected facts FAIL: CaptureConvertedUnspecToOtherUnionNull.java: no unexpected facts FAIL: CaptureConvertedUnspecToOtherUnspec.java: no unexpected facts PASS: CastOfCaptureOfNotNullMarkedUnboundedWildcardForObjectBoundedTypeParameter.java: no unexpected facts -FAIL: CastOfCaptureOfUnboundedWildcardForNotNullMarkedObjectBoundedTypeParameter.java: no unexpected facts +PASS: CastOfCaptureOfUnboundedWildcardForNotNullMarkedObjectBoundedTypeParameter.java: no unexpected facts PASS: CastOfCaptureOfUnboundedWildcardForObjectBoundedTypeParameter.java: no unexpected facts FAIL: CastToPrimitive.java:33:jspecify_nullness_mismatch FAIL: CastToPrimitive.java: no unexpected facts -PASS: CastWildcardToTypeVariable.java:23:jspecify_nullness_mismatch +FAIL: CastWildcardToTypeVariable.java:23:jspecify_nullness_mismatch PASS: CastWildcardToTypeVariable.java: no unexpected facts PASS: Catch.java: no unexpected facts PASS: ClassLiteral.java: no unexpected facts @@ -115,11 +115,11 @@ FAIL: ContainmentSuper.java: no unexpected facts FAIL: ContainmentSuperVsExtends.java:24:jspecify_nullness_mismatch FAIL: ContainmentSuperVsExtends.java: no unexpected facts FAIL: ContainmentSuperVsExtendsSameType.java:23:jspecify_nullness_mismatch -PASS: ContainmentSuperVsExtendsSameType.java: no unexpected facts -PASS: ContravariantReturns.java:30:jspecify_nullness_mismatch -PASS: ContravariantReturns.java:34:jspecify_nullness_mismatch -PASS: ContravariantReturns.java:38:jspecify_nullness_mismatch -PASS: ContravariantReturns.java: no unexpected facts +FAIL: ContainmentSuperVsExtendsSameType.java: no unexpected facts +FAIL: ContravariantReturns.java:30:jspecify_nullness_mismatch +FAIL: ContravariantReturns.java:34:jspecify_nullness_mismatch +FAIL: ContravariantReturns.java:38:jspecify_nullness_mismatch +FAIL: ContravariantReturns.java: no unexpected facts PASS: CovariantReturns.java: no unexpected facts FAIL: DereferenceClass.java:33:jspecify_nullness_mismatch FAIL: DereferenceClass.java: no unexpected facts @@ -302,8 +302,8 @@ FAIL: NullLiteralToTypeVariable.java: no unexpected facts FAIL: NullLiteralToTypeVariableUnionNull.java: no unexpected facts FAIL: NullLiteralToTypeVariableUnspec.java: no unexpected facts FAIL: NullMarkedDirectUseOfNotNullMarkedBoundedTypeVariable.java: no unexpected facts -FAIL: NullUnmarkedUndoesNullMarked.java: no unexpected facts -FAIL: NullUnmarkedUndoesNullMarkedForWildcards.java: no unexpected facts +PASS: NullUnmarkedUndoesNullMarked.java: no unexpected facts +PASS: NullUnmarkedUndoesNullMarkedForWildcards.java: no unexpected facts PASS: NullnessDoesNotAffectOverloadSelection.java:23:jspecify_nullness_mismatch PASS: NullnessDoesNotAffectOverloadSelection.java: no unexpected facts PASS: ObjectAsSuperOfTypeVariable.java:35:jspecify_nullness_mismatch @@ -313,7 +313,7 @@ PASS: OutOfBoundsTypeVariable.java: no unexpected facts FAIL: OverrideParameters.java:48:jspecify_nullness_mismatch FAIL: OverrideParameters.java:68:jspecify_nullness_mismatch FAIL: OverrideParameters.java: no unexpected facts -PASS: OverrideParametersThatAreTypeVariables.java: no unexpected facts +FAIL: OverrideParametersThatAreTypeVariables.java: no unexpected facts FAIL: OverrideReturns.java:57:jspecify_nullness_mismatch FAIL: OverrideReturns.java: no unexpected facts PASS: ParameterizedWithTypeVariableArgumentToSelf.java: no unexpected facts @@ -542,7 +542,7 @@ PASS: nullnessUnspecifiedTypeParameter/nullnessunspecifiedtypeparameter/Nullness PASS: nullnessUnspecifiedTypeParameter/nullnessunspecifiedtypeparameter/NullnessUnspecifiedTypeParameter.java:53:jspecify_nullness_mismatch PASS: nullnessUnspecifiedTypeParameter/nullnessunspecifiedtypeparameter/NullnessUnspecifiedTypeParameter.java:57:jspecify_nullness_mismatch PASS: nullnessUnspecifiedTypeParameter/nullnessunspecifiedtypeparameter/NullnessUnspecifiedTypeParameter.java:59:jspecify_nullness_mismatch -PASS: nullnessUnspecifiedTypeParameter/nullnessunspecifiedtypeparameter/NullnessUnspecifiedTypeParameter.java: no unexpected facts +FAIL: nullnessUnspecifiedTypeParameter/nullnessunspecifiedtypeparameter/NullnessUnspecifiedTypeParameter.java: no unexpected facts PASS: packageDefault/packagedefault/Bar.java:23:test:cannot-convert:Object? to Object! PASS: packageDefault/packagedefault/Bar.java: no unexpected facts PASS: packageDefault/packagedefault/package-info.java: no unexpected facts From 644b705d8589a73d1290dbe76341f7b9ec07348a Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Tue, 6 Feb 2024 17:12:34 -0500 Subject: [PATCH 02/23] Document that detail messages should not be filtered out (#146) --- src/test/java/tests/ConformanceTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/tests/ConformanceTest.java b/src/test/java/tests/ConformanceTest.java index f4da4400..edfb67cb 100644 --- a/src/test/java/tests/ConformanceTest.java +++ b/src/test/java/tests/ConformanceTest.java @@ -144,6 +144,8 @@ private static ImmutableSet analyze( return result.getUnexpectedDiagnostics().stream() .map(d -> DetailMessage.parse(d.getMessage(), testDirectory)) .filter(Objects::nonNull) + // Do not filter out messages without details. + // .filter(DetailMessage::hasDetails) .map(DetailMessageReportedFact::new) .collect(toImmutableSet()); } From 70ca5bbb5d9871fbcf68baf41a58d0b7c2b7cfc9 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Wed, 7 Feb 2024 19:24:36 -0500 Subject: [PATCH 03/23] Use standard error format (#154) --- .github/workflows/build.yml | 6 +++--- tests/minimal/Demo.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bbfbbd64..bf9af48a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,11 +30,11 @@ jobs: run: ./gradlew build conformanceTests demoTest --include-build ../jspecify env: SHALLOW: 1 - - name: Check out jspecify/samples-google-prototype + - name: Check out jspecify/samples-google-prototype-eisop if: always() run: | - git fetch --depth=1 origin samples-google-prototype - git checkout samples-google-prototype + git fetch --depth=1 origin samples-google-prototype-eisop + git checkout samples-google-prototype-eisop working-directory: jspecify - name: Run Samples Tests if: always() diff --git a/tests/minimal/Demo.java b/tests/minimal/Demo.java index cce60abe..f29690cf 100644 --- a/tests/minimal/Demo.java +++ b/tests/minimal/Demo.java @@ -18,7 +18,7 @@ @NullMarked class Demo { Object mismatch(@Nullable Object o) { - // jspecify_nullness_mismatch + // :: error: jspecify_nullness_mismatch return o; } } From 570d80ec783d5e2346e811d27a8f80146a5ae1d4 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Thu, 8 Feb 2024 09:33:33 -0500 Subject: [PATCH 04/23] Handle both `https` and `git@` clones (#158) Co-authored-by: Chris Povirk --- initialize-project | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/initialize-project b/initialize-project index cd1f9304..77b89d2b 100755 --- a/initialize-project +++ b/initialize-project @@ -55,12 +55,14 @@ git_clone() { local forking_org forking_org="$(forking_org)" - if [[ -n "${forking_org}" ]] && [[ "${forking_org}" != "https://github.com/jspecify" ]]; then + if [[ -n "${forking_org}" ]] \ + && [[ "${forking_org}" != "https://github.com/jspecify" ]] \ + && [[ "${forking_org}" != "git@github.com:jspecify" ]] ; then if run "${git[@]}" "${forking_org}/${repo}.git" "../${repo}"; then return fi fi - if [[ "${repo}" == checker-framework ]]; then + if [[ "${repo}" == "checker-framework" ]]; then forking_org=https://github.com/eisop else forking_org=https://github.com/jspecify From c8d59332a7917b4fa2dfe542d45e4e9fb0309578 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Tue, 9 Apr 2024 20:25:04 -0400 Subject: [PATCH 05/23] Use the new `TypeInformationPresenter` to output more type information (#134) --- settings.gradle | 2 +- .../ConformanceTypeInformationPresenter.java | 108 ++++++++++++++++++ .../NullSpecAnnotatedTypeFactory.java | 9 ++ .../jspecify/nullness/NullSpecChecker.java | 3 +- src/test/java/tests/NullSpecTest.java | 2 +- tests/ConformanceTest-report.txt | 14 +-- 6 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/google/jspecify/nullness/ConformanceTypeInformationPresenter.java diff --git a/settings.gradle b/settings.gradle index 5734a5f4..30a546bc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,7 +15,7 @@ includeBuild("../checker-framework") dependencyResolutionManagement { versionCatalogs { libs { - version("checkerFramework", "3.42.0-eisop2-SNAPSHOT") + version("checkerFramework", "3.42.0-eisop3-SNAPSHOT") library("checkerFramework-checker", "io.github.eisop", "checker").versionRef("checkerFramework") library("checkerFramework-checker-qual", "io.github.eisop", "checker-qual").versionRef("checkerFramework") diff --git a/src/main/java/com/google/jspecify/nullness/ConformanceTypeInformationPresenter.java b/src/main/java/com/google/jspecify/nullness/ConformanceTypeInformationPresenter.java new file mode 100644 index 00000000..83d4da63 --- /dev/null +++ b/src/main/java/com/google/jspecify/nullness/ConformanceTypeInformationPresenter.java @@ -0,0 +1,108 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.jspecify.nullness; + +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import java.util.List; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeFormatter; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.util.visualize.AbstractTypeInformationPresenter; +import org.checkerframework.framework.util.visualize.TypeOccurrenceKind; +import org.checkerframework.javacutil.TreeUtils; + +/** + * Output "sinkType" and "sourceType" diagnostic warning messages so the conformance tests can look + * for (a subset of) them. + */ +public final class ConformanceTypeInformationPresenter extends AbstractTypeInformationPresenter { + + /** + * Constructs a presenter for the given factory. + * + * @param atypeFactory the AnnotatedTypeFactory for the current analysis + */ + public ConformanceTypeInformationPresenter(AnnotatedTypeFactory atypeFactory) { + super(atypeFactory); + } + + @Override + protected AnnotatedTypeFormatter createTypeFormatter() { + // Use the same type formatter as normal error messages. Look into whether a different format + // would be better here. + return atypeFactory.getAnnotatedTypeFormatter(); + } + + @Override + protected TypeInformationReporter createTypeInformationReporter(ClassTree tree) { + return new ConformanceTypeInformationReporter(tree); + } + + class ConformanceTypeInformationReporter extends TypeInformationReporter { + ConformanceTypeInformationReporter(ClassTree tree) { + super(tree); + } + + @Override + protected void reportTreeType( + Tree tree, AnnotatedTypeMirror type, TypeOccurrenceKind occurrenceKind) { + switch (tree.getKind()) { + case ASSIGNMENT: + AssignmentTree asgn = (AssignmentTree) tree; + AnnotatedTypeMirror varType = + genFactory != null + ? genFactory.getAnnotatedTypeLhs(asgn.getVariable()) + : atypeFactory.getAnnotatedType(asgn.getVariable()); + checker.reportWarning( + asgn.getVariable(), + "sinkType", + typeFormatter.format(varType), + asgn.getVariable().toString()); + break; + case RETURN: + checker.reportWarning(tree, "sinkType", typeFormatter.format(type), "return"); + break; + case METHOD_INVOCATION: + ExecutableElement calledElem = TreeUtils.elementFromUse((MethodInvocationTree) tree); + String methodName = calledElem.getSimpleName().toString(); + AnnotatedExecutableType calledType = (AnnotatedExecutableType) type; + List params = calledType.getParameterTypes(); + MethodInvocationTree mit = (MethodInvocationTree) tree; + List args = mit.getArguments(); + assert params.size() == args.size(); + + for (int i = 0; i < params.size(); ++i) { + String paramName = calledElem.getParameters().get(i).getSimpleName().toString(); + String paramLocation = String.format("%s#%s", methodName, paramName); + checker.reportWarning( + tree, "sinkType", typeFormatter.format(params.get(i)), paramLocation); + } + break; + default: + // Nothing special for other trees. + } + + if (TreeUtils.isExpressionTree(tree)) { + checker.reportWarning(tree, "sourceType", typeFormatter.format(type), tree.toString()); + } + } + } +} diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index f09e7cac..800a55ce 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -122,6 +122,8 @@ final class NullSpecAnnotatedTypeFactory private final AnnotatedDeclaredType javaUtilCollection; + private final ConformanceTypeInformationPresenter conformanceInformationPresenter; + final AnnotatedDeclaredType javaLangClass; final AnnotatedDeclaredType javaLangThreadLocal; final AnnotatedDeclaredType javaUtilMap; @@ -305,6 +307,9 @@ private NullSpecAnnotatedTypeFactory( withMostConvenientWorld = this; } + conformanceInformationPresenter = + checker.hasOption("showTypes") ? new ConformanceTypeInformationPresenter(this) : null; + if (!givenOtherWorld) { /* * Now the withLeastConvenientWorld and withMostConvenientWorld fields of both `this` and @@ -1707,6 +1712,10 @@ public void postProcessClassTree(ClassTree tree) { * type.invalid.conflicting.annos error, which I have described more in * https://github.com/jspecify/jspecify-reference-checker/commit/d16a0231487e239bc94145177de464b5f77c8b19 */ + + if (conformanceInformationPresenter != null) { + conformanceInformationPresenter.process(tree, getPath(tree)); + } } @Override diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java b/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java index 027ddc74..4d490c86 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java @@ -39,9 +39,10 @@ *
  • "strict": Whether the checker should be a sound, strict type system. Does not imply that * implementation code is checked. *
  • "checkImpl": Whether implementation code should be checked. + *
  • "showTypes": Whether to output type information for the conformance test suite. * */ -@SupportedOptions({"strict", "checkImpl"}) +@SupportedOptions({"strict", "checkImpl", "showTypes"}) public final class NullSpecChecker extends BaseTypeChecker { /* * A non-final field is ugly, but we can't create our Util instance in the constructor because the diff --git a/src/test/java/tests/NullSpecTest.java b/src/test/java/tests/NullSpecTest.java index bb8a8075..91707656 100644 --- a/src/test/java/tests/NullSpecTest.java +++ b/src/test/java/tests/NullSpecTest.java @@ -206,7 +206,7 @@ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { } case "jspecify_conflicting_annotations": switch (unexpected.messageKey) { - case "conflicting.annos": + case "type.invalid.conflicting.annos": return true; default: return false; diff --git a/tests/ConformanceTest-report.txt b/tests/ConformanceTest-report.txt index 333bd802..23c8a51e 100644 --- a/tests/ConformanceTest-report.txt +++ b/tests/ConformanceTest-report.txt @@ -1,11 +1,11 @@ -# 84 pass; 30 fail; 114 total; 73.7% score -FAIL: Basic.java:28:test:expression-type:Object?:nullable -FAIL: Basic.java:28:test:sink-type:Object!:return +# 90 pass; 24 fail; 114 total; 78.9% score +PASS: Basic.java:28:test:expression-type:Object?:nullable +PASS: Basic.java:28:test:sink-type:Object!:return PASS: Basic.java:28:test:cannot-convert:Object? to Object! -FAIL: Basic.java:34:test:expression-type:Object!:nonNull -FAIL: Basic.java:34:test:sink-type:Object?:return -FAIL: Basic.java:41:test:sink-type:Object?:nullableObject -FAIL: Basic.java:43:test:sink-type:String!:testSinkType#nonNullString +PASS: Basic.java:34:test:expression-type:Object!:nonNull +PASS: Basic.java:34:test:sink-type:Object?:return +PASS: Basic.java:41:test:sink-type:Object?:nullableObject +PASS: Basic.java:43:test:sink-type:String!:testSinkType#nonNullString FAIL: Basic.java:49:test:expression-type:List!:nullableStrings PASS: Basic.java: no unexpected facts PASS: Irrelevant.java:28:test:irrelevant-annotation:Nullable From 11a29805fab90f850105e7d0f5dc531977af376e Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Mon, 26 Feb 2024 21:14:41 -0500 Subject: [PATCH 06/23] Map `type.invalid.super.wildcard` as an expected error (#166) --- src/test/java/tests/NullSpecTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/tests/NullSpecTest.java b/src/test/java/tests/NullSpecTest.java index 91707656..500e6f4b 100644 --- a/src/test/java/tests/NullSpecTest.java +++ b/src/test/java/tests/NullSpecTest.java @@ -207,6 +207,7 @@ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { case "jspecify_conflicting_annotations": switch (unexpected.messageKey) { case "type.invalid.conflicting.annos": + case "type.invalid.super.wildcard": return true; default: return false; From 76b33c9872c1ccc1b0fc9219ed70b2237b4e6db3 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Thu, 11 Apr 2024 17:38:45 -0400 Subject: [PATCH 07/23] Use the new `TypeUseLocation` default locations (#165) --- .github/workflows/build.yml | 1 + build.gradle | 1 + .../NullSpecAnnotatedTypeFactory.java | 93 +++++++++++++++---- .../jspecify/nullness/ParametricNull.java | 27 ++++++ .../com/google/jspecify/nullness/Util.java | 2 + src/test/java/tests/ConformanceTest.java | 13 +-- tests/ConformanceTestOnSamples-report.txt | 22 ++--- tests/regression/Issue159.java | 26 ++++++ tests/regression/Issue163.java | 31 +++++++ tests/regression/OverrideTest.java | 44 +++++++++ .../UnboundedDefaultsToNullable.java | 26 ++++++ 11 files changed, 251 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/google/jspecify/nullness/ParametricNull.java create mode 100644 tests/regression/Issue159.java create mode 100644 tests/regression/Issue163.java create mode 100644 tests/regression/OverrideTest.java create mode 100644 tests/regression/UnboundedDefaultsToNullable.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf9af48a..013788a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,7 @@ jobs: run: ./gradlew build conformanceTests demoTest --include-build ../jspecify env: SHALLOW: 1 + JSPECIFY_CONFORMANCE_TEST_MODE: details - name: Check out jspecify/samples-google-prototype-eisop if: always() run: | diff --git a/build.gradle b/build.gradle index 4b4dea5c..947bb6f4 100644 --- a/build.gradle +++ b/build.gradle @@ -235,6 +235,7 @@ if (!cfQualJar.toFile().exists()) { spotless { java { + target '**/*.java' googleJavaFormat() formatAnnotations() } diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index 800a55ce..13c71af7 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -115,6 +115,7 @@ final class NullSpecAnnotatedTypeFactory private final AnnotationMirror minusNull; private final AnnotationMirror unionNull; private final AnnotationMirror nullnessOperatorUnspecified; + private final AnnotationMirror parametricNull; private final boolean isLeastConvenientWorld; private final NullSpecAnnotatedTypeFactory withLeastConvenientWorld; @@ -128,6 +129,11 @@ final class NullSpecAnnotatedTypeFactory final AnnotatedDeclaredType javaLangThreadLocal; final AnnotatedDeclaredType javaUtilMap; + // Ensure that all locations that appear in the `defaultLocations` also appear somewhere in the + // `nullMarkedLocations`. + // As defaults are added and removed to the same environment, we need to ensure that all values + // are correctly changed. + private static final TypeUseLocation[] defaultLocationsMinusNull = new TypeUseLocation[] { TypeUseLocation.CONSTRUCTOR_RESULT, @@ -138,14 +144,38 @@ final class NullSpecAnnotatedTypeFactory private static final TypeUseLocation[] defaultLocationsUnionNull = new TypeUseLocation[] { - TypeUseLocation.LOCAL_VARIABLE, TypeUseLocation.RESOURCE_VARIABLE, + TypeUseLocation.IMPLICIT_WILDCARD_UPPER_BOUND_SUPER, + TypeUseLocation.LOCAL_VARIABLE, + TypeUseLocation.RESOURCE_VARIABLE, }; private static final TypeUseLocation[] defaultLocationsUnspecified = new TypeUseLocation[] { - // TypeUseLocation.UNBOUNDED_WILDCARD_UPPER_BOUND, TODO + TypeUseLocation.IMPLICIT_WILDCARD_UPPER_BOUND_NO_SUPER, + TypeUseLocation.TYPE_VARIABLE_USE, + TypeUseLocation.OTHERWISE + }; + + private static final TypeUseLocation[] nullMarkedLocationsMinusNull = + new TypeUseLocation[] { + TypeUseLocation.CONSTRUCTOR_RESULT, + TypeUseLocation.EXCEPTION_PARAMETER, + TypeUseLocation.IMPLICIT_LOWER_BOUND, + TypeUseLocation.RECEIVER, TypeUseLocation.OTHERWISE }; + private static final TypeUseLocation[] nullMarkedLocationsUnionNull = + new TypeUseLocation[] { + TypeUseLocation.LOCAL_VARIABLE, + TypeUseLocation.RESOURCE_VARIABLE, + TypeUseLocation.IMPLICIT_WILDCARD_UPPER_BOUND_NO_SUPER, + TypeUseLocation.IMPLICIT_WILDCARD_UPPER_BOUND_SUPER, + }; + + private static final TypeUseLocation[] nullMarkedLocationsParametric = + new TypeUseLocation[] {TypeUseLocation.TYPE_VARIABLE_USE}; + + private static final TypeUseLocation[] nullMarkedLocationsUnspecified = new TypeUseLocation[] {}; /** Constructor that takes all configuration from the provided {@code checker}. */ NullSpecAnnotatedTypeFactory(BaseTypeChecker checker, Util util) { @@ -170,6 +200,7 @@ private NullSpecAnnotatedTypeFactory( minusNull = util.minusNull; unionNull = util.unionNull; nullnessOperatorUnspecified = util.nullnessOperatorUnspecified; + parametricNull = util.parametricNull; addAliasedTypeAnnotation( "org.jspecify.annotations.NullnessUnspecified", nullnessOperatorUnspecified); @@ -187,22 +218,25 @@ private NullSpecAnnotatedTypeFactory( AnnotationMirror nullMarkedDefaultQualMinusNull = new AnnotationBuilder(processingEnv, DefaultQualifier.class) .setValue("value", MinusNull.class) - .setValue( - "locations", - new TypeUseLocation[] { - TypeUseLocation.EXCEPTION_PARAMETER, TypeUseLocation.OTHERWISE - }) + .setValue("locations", nullMarkedLocationsMinusNull) .setValue("applyToSubpackages", false) .build(); AnnotationMirror nullMarkedDefaultQualUnionNull = new AnnotationBuilder(processingEnv, DefaultQualifier.class) .setValue("value", Nullable.class) - .setValue( - "locations", - new TypeUseLocation[] { - TypeUseLocation.LOCAL_VARIABLE, TypeUseLocation.RESOURCE_VARIABLE, - // TypeUseLocation.UNBOUNDED_WILDCARD_UPPER_BOUND TODO - }) + .setValue("locations", nullMarkedLocationsUnionNull) + .setValue("applyToSubpackages", false) + .build(); + AnnotationMirror nullMarkedDefaultQualParametric = + new AnnotationBuilder(processingEnv, DefaultQualifier.class) + .setValue("value", ParametricNull.class) + .setValue("locations", nullMarkedLocationsParametric) + .setValue("applyToSubpackages", false) + .build(); + AnnotationMirror nullMarkedDefaultQualUnspecified = + new AnnotationBuilder(processingEnv, DefaultQualifier.class) + .setValue("value", NullnessUnspecified.class) + .setValue("locations", nullMarkedLocationsUnspecified) .setValue("applyToSubpackages", false) .build(); AnnotationMirror nullMarkedDefaultQual = @@ -210,7 +244,10 @@ private NullSpecAnnotatedTypeFactory( .setValue( "value", new AnnotationMirror[] { - nullMarkedDefaultQualMinusNull, nullMarkedDefaultQualUnionNull + nullMarkedDefaultQualMinusNull, + nullMarkedDefaultQualUnionNull, + nullMarkedDefaultQualParametric, + nullMarkedDefaultQualUnspecified }) .build(); @@ -359,7 +396,8 @@ protected void addUncheckedStandardDefaults(QualifierDefaults defs) { @Override protected Set> createSupportedTypeQualifiers() { - return new LinkedHashSet<>(asList(Nullable.class, NullnessUnspecified.class, MinusNull.class)); + return new LinkedHashSet<>( + asList(Nullable.class, NullnessUnspecified.class, MinusNull.class, ParametricNull.class)); } @Override @@ -438,10 +476,16 @@ protected Map> createDirectSuper nameToQualifierKind.get(Nullable.class.getCanonicalName()); DefaultQualifierKind nullnessOperatorUnspecified = nameToQualifierKind.get(NullnessUnspecified.class.getCanonicalName()); + DefaultQualifierKind parametricNullKind = + nameToQualifierKind.get(ParametricNull.class.getCanonicalName()); Map> supers = new HashMap<>(); - supers.put(minusNullKind, singleton(nullnessOperatorUnspecified)); + LinkedHashSet superOfMinusNull = new LinkedHashSet<>(); + superOfMinusNull.add(nullnessOperatorUnspecified); + superOfMinusNull.add(parametricNullKind); + supers.put(minusNullKind, superOfMinusNull); supers.put(nullnessOperatorUnspecified, singleton(unionNullKind)); + supers.put(parametricNullKind, singleton(unionNullKind)); supers.put(unionNullKind, emptySet()); return supers; /* @@ -457,6 +501,16 @@ protected Map> createDirectSuper } }; } + + @Override + public AnnotationMirror getParametricQualifier(AnnotationMirror qualifier) { + return parametricNull; + } + + @Override + public boolean isParametricQualifier(AnnotationMirror qualifier) { + return areSame(parametricNull, qualifier); + } } @Override @@ -706,6 +760,9 @@ private List getUpperBounds(AnnotatedTypeMirror t * * My only worry is that I always worry about making calls to getAnnotatedType, as discussed in * various comments in this file (e.g., in NullSpecTreeAnnotator.visitMethodInvocation). + * + * This is likely caused by https://github.com/eisop/checker-framework/issues/737. + * Revisit this once that issue is fixed. */ if (type instanceof AnnotatedTypeVariable && !isCapturedTypeVariable(type.getUnderlyingType())) { @@ -861,8 +918,8 @@ protected AnnotatedTypeMirror substituteTypeVariable( substitute.replaceAnnotation(minusNull); } else if (argument.hasAnnotation(unionNull) || use.hasAnnotation(unionNull)) { substitute.replaceAnnotation(unionNull); - } else if (argument.hasAnnotation(nullnessOperatorUnspecified) - || use.hasAnnotation(nullnessOperatorUnspecified)) { + } else if (argument.hasEffectiveAnnotation(nullnessOperatorUnspecified) + || use.hasEffectiveAnnotation(nullnessOperatorUnspecified)) { substitute.replaceAnnotation(nullnessOperatorUnspecified); } diff --git a/src/main/java/com/google/jspecify/nullness/ParametricNull.java b/src/main/java/com/google/jspecify/nullness/ParametricNull.java new file mode 100644 index 00000000..0e4c65e9 --- /dev/null +++ b/src/main/java/com/google/jspecify/nullness/ParametricNull.java @@ -0,0 +1,27 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.jspecify.nullness; + +import static java.lang.annotation.ElementType.TYPE_USE; + +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.ParametricTypeVariableUseQualifier; + +/** Internal implementation detail; not usable in user code. */ +@Target(TYPE_USE) +@InvisibleQualifier +@ParametricTypeVariableUseQualifier(Nullable.class) +@interface ParametricNull {} diff --git a/src/main/java/com/google/jspecify/nullness/Util.java b/src/main/java/com/google/jspecify/nullness/Util.java index e6b7dc0b..955bceb9 100644 --- a/src/main/java/com/google/jspecify/nullness/Util.java +++ b/src/main/java/com/google/jspecify/nullness/Util.java @@ -51,6 +51,7 @@ final class Util { final AnnotationMirror minusNull; final AnnotationMirror unionNull; final AnnotationMirror nullnessOperatorUnspecified; + final AnnotationMirror parametricNull; final TypeElement javaUtilCollectionElement; final ExecutableElement collectionToArrayNoArgElement; @@ -116,6 +117,7 @@ final class Util { minusNull = AnnotationBuilder.fromClass(e, MinusNull.class); unionNull = AnnotationBuilder.fromClass(e, Nullable.class); nullnessOperatorUnspecified = AnnotationBuilder.fromClass(e, NullnessUnspecified.class); + parametricNull = AnnotationBuilder.fromClass(e, ParametricNull.class); /* * Note that all the above annotations must be on the *classpath*, not just the *processorpath*. * That's because, even if we change fromClass to fromName, AnnotationBuilder ultimately calls diff --git a/src/test/java/tests/ConformanceTest.java b/src/test/java/tests/ConformanceTest.java index edfb67cb..c7e2cb7d 100644 --- a/src/test/java/tests/ConformanceTest.java +++ b/src/test/java/tests/ConformanceTest.java @@ -161,12 +161,13 @@ static final class DetailMessageReportedFact extends ReportedFact { "assignment.type.incompatible", "atomicreference.must.include.null", "cast.unsafe", - "lambda.param", - "methodref.receiver.bound", - "methodref.receiver", - "methodref.return", - "override.param", - "override.return", + "lambda.param.type.incompatible", + "methodref.receiver.bound.invalid", + "methodref.receiver.invalid", + "methodref.return.invalid", + "override.param.invalid", + "override.receiver.invalid", + "override.return.invalid", "return.type.incompatible", "threadlocal.must.include.null", "type.argument.type.incompatible"); diff --git a/tests/ConformanceTestOnSamples-report.txt b/tests/ConformanceTestOnSamples-report.txt index d1dec5af..991f4fef 100644 --- a/tests/ConformanceTestOnSamples-report.txt +++ b/tests/ConformanceTestOnSamples-report.txt @@ -1,4 +1,4 @@ -# 67 pass; 506 fail; 573 total; 11.7% score +# 73 pass; 500 fail; 573 total; 12.7% score FAIL: AnnotatedInnerOfNonParameterized.java: no unexpected facts FAIL: AnnotatedInnerOfParameterized.java: no unexpected facts FAIL: AnnotatedReceiver.java: no unexpected facts @@ -14,15 +14,15 @@ FAIL: ArraySubtype.java: no unexpected facts PASS: AssignmentAsExpression.java: no unexpected facts PASS: AugmentedInferenceAgreesWithBaseInference.java:35:jspecify_nullness_mismatch PASS: AugmentedInferenceAgreesWithBaseInference.java: no unexpected facts -FAIL: BoundedTypeVariableReturn.java:27:jspecify_nullness_mismatch -FAIL: BoundedTypeVariableReturn.java:32:jspecify_nullness_mismatch +PASS: BoundedTypeVariableReturn.java:27:jspecify_nullness_mismatch +PASS: BoundedTypeVariableReturn.java:32:jspecify_nullness_mismatch PASS: BoundedTypeVariableReturn.java: no unexpected facts FAIL: CaptureAsInferredTypeArgument.java:51:jspecify_nullness_mismatch FAIL: CaptureAsInferredTypeArgument.java:53:jspecify_nullness_mismatch FAIL: CaptureAsInferredTypeArgument.java:61:jspecify_nullness_mismatch FAIL: CaptureAsInferredTypeArgument.java:63:jspecify_nullness_mismatch FAIL: CaptureAsInferredTypeArgument.java: no unexpected facts -FAIL: CaptureConversionForSubtyping.java: no unexpected facts +PASS: CaptureConversionForSubtyping.java: no unexpected facts FAIL: CaptureConvertedToObject.java:71:jspecify_nullness_mismatch FAIL: CaptureConvertedToObject.java: no unexpected facts FAIL: CaptureConvertedToObjectUnionNull.java: no unexpected facts @@ -70,11 +70,11 @@ FAIL: CaptureConvertedUnspecToOther.java: no unexpected facts FAIL: CaptureConvertedUnspecToOtherUnionNull.java: no unexpected facts FAIL: CaptureConvertedUnspecToOtherUnspec.java: no unexpected facts PASS: CastOfCaptureOfNotNullMarkedUnboundedWildcardForObjectBoundedTypeParameter.java: no unexpected facts -PASS: CastOfCaptureOfUnboundedWildcardForNotNullMarkedObjectBoundedTypeParameter.java: no unexpected facts +FAIL: CastOfCaptureOfUnboundedWildcardForNotNullMarkedObjectBoundedTypeParameter.java: no unexpected facts PASS: CastOfCaptureOfUnboundedWildcardForObjectBoundedTypeParameter.java: no unexpected facts FAIL: CastToPrimitive.java:33:jspecify_nullness_mismatch FAIL: CastToPrimitive.java: no unexpected facts -FAIL: CastWildcardToTypeVariable.java:23:jspecify_nullness_mismatch +PASS: CastWildcardToTypeVariable.java:23:jspecify_nullness_mismatch PASS: CastWildcardToTypeVariable.java: no unexpected facts PASS: Catch.java: no unexpected facts PASS: ClassLiteral.java: no unexpected facts @@ -116,10 +116,10 @@ FAIL: ContainmentSuperVsExtends.java:24:jspecify_nullness_mismatch FAIL: ContainmentSuperVsExtends.java: no unexpected facts FAIL: ContainmentSuperVsExtendsSameType.java:23:jspecify_nullness_mismatch FAIL: ContainmentSuperVsExtendsSameType.java: no unexpected facts -FAIL: ContravariantReturns.java:30:jspecify_nullness_mismatch -FAIL: ContravariantReturns.java:34:jspecify_nullness_mismatch -FAIL: ContravariantReturns.java:38:jspecify_nullness_mismatch -FAIL: ContravariantReturns.java: no unexpected facts +PASS: ContravariantReturns.java:30:jspecify_nullness_mismatch +PASS: ContravariantReturns.java:34:jspecify_nullness_mismatch +PASS: ContravariantReturns.java:38:jspecify_nullness_mismatch +PASS: ContravariantReturns.java: no unexpected facts PASS: CovariantReturns.java: no unexpected facts FAIL: DereferenceClass.java:33:jspecify_nullness_mismatch FAIL: DereferenceClass.java: no unexpected facts @@ -303,7 +303,7 @@ FAIL: NullLiteralToTypeVariableUnionNull.java: no unexpected facts FAIL: NullLiteralToTypeVariableUnspec.java: no unexpected facts FAIL: NullMarkedDirectUseOfNotNullMarkedBoundedTypeVariable.java: no unexpected facts PASS: NullUnmarkedUndoesNullMarked.java: no unexpected facts -PASS: NullUnmarkedUndoesNullMarkedForWildcards.java: no unexpected facts +FAIL: NullUnmarkedUndoesNullMarkedForWildcards.java: no unexpected facts PASS: NullnessDoesNotAffectOverloadSelection.java:23:jspecify_nullness_mismatch PASS: NullnessDoesNotAffectOverloadSelection.java: no unexpected facts PASS: ObjectAsSuperOfTypeVariable.java:35:jspecify_nullness_mismatch diff --git a/tests/regression/Issue159.java b/tests/regression/Issue159.java new file mode 100644 index 00000000..ab44b010 --- /dev/null +++ b/tests/regression/Issue159.java @@ -0,0 +1,26 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case for Issue 159: +// https://github.com/jspecify/jspecify-reference-checker/issues/159 + +import java.util.ArrayList; +import org.jspecify.annotations.NullMarked; + +@NullMarked +class Issue159 extends ArrayList { + Issue159 foo() { + return new Issue159(); + } +} diff --git a/tests/regression/Issue163.java b/tests/regression/Issue163.java new file mode 100644 index 00000000..36de896e --- /dev/null +++ b/tests/regression/Issue163.java @@ -0,0 +1,31 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case for Issue 163: +// https://github.com/jspecify/jspecify-reference-checker/issues/163 + +import org.jspecify.annotations.NullMarked; + +@NullMarked +class Issue163NullForUnspecVoid { + void x(Issue163Value val, Issue163Visitor vis) { + val.accept(vis, null); + } +} + +interface Issue163Value { +

    void accept(Issue163Visitor

    visitor, P param); +} + +interface Issue163Visitor

    {} diff --git a/tests/regression/OverrideTest.java b/tests/regression/OverrideTest.java new file mode 100644 index 00000000..c34e08cf --- /dev/null +++ b/tests/regression/OverrideTest.java @@ -0,0 +1,44 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case based on comment: +// https://github.com/jspecify/jspecify-reference-checker/pull/165#issuecomment-2030038854 + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class OverrideTest { + abstract class Super { + abstract void accept(MarkedEntry entry); + } + + abstract class Sub extends Super { + @Override + abstract void accept(MarkedEntry entry); + } + + interface MarkedEntry {} + + abstract class SuperUnmarked { + abstract void accept(OverrideTestUnmarkedEntry entry); + } + + abstract class SubUnmarked extends SuperUnmarked { + @Override + abstract void accept(OverrideTestUnmarkedEntry entry); + } +} + +interface OverrideTestUnmarkedEntry {} diff --git a/tests/regression/UnboundedDefaultsToNullable.java b/tests/regression/UnboundedDefaultsToNullable.java new file mode 100644 index 00000000..6457e3a3 --- /dev/null +++ b/tests/regression/UnboundedDefaultsToNullable.java @@ -0,0 +1,26 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case for Issue 161: +// https://github.com/jspecify/jspecify-reference-checker/issues/161 + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class UnboundedDefaultsToNullable { + UnboundedDefaultsToNullable x() { + return this; + } +} From 0c36cdf106782b8b3965c8b5943f86d99778a301 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Wed, 17 Apr 2024 16:11:36 -0400 Subject: [PATCH 08/23] Test fixes (#173) Co-authored-by: David P. Baker --- build.gradle | 4 +- .../ConformanceTypeInformationPresenter.java | 4 -- src/test/java/tests/ConformanceTest.java | 6 +- src/test/java/tests/DetailMessage.java | 63 ++++++------------- src/test/java/tests/NullSpecTest.java | 20 ++++-- 5 files changed, 41 insertions(+), 56 deletions(-) diff --git a/build.gradle b/build.gradle index 947bb6f4..509b0f4e 100644 --- a/build.gradle +++ b/build.gradle @@ -138,8 +138,10 @@ tasks.withType(Test).configureEach { test { include '**/NullSpecTest$Minimal.class' - inputs.files("${rootDir}/tests/minimal") + + include '**/NullSpecTest$Regression.class' + inputs.files("${rootDir}/tests/regression") } tasks.register('jspecifySamplesTest', Test) { diff --git a/src/main/java/com/google/jspecify/nullness/ConformanceTypeInformationPresenter.java b/src/main/java/com/google/jspecify/nullness/ConformanceTypeInformationPresenter.java index 83d4da63..9bffb729 100644 --- a/src/main/java/com/google/jspecify/nullness/ConformanceTypeInformationPresenter.java +++ b/src/main/java/com/google/jspecify/nullness/ConformanceTypeInformationPresenter.java @@ -16,7 +16,6 @@ import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.ClassTree; -import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import java.util.List; @@ -85,9 +84,6 @@ protected void reportTreeType( String methodName = calledElem.getSimpleName().toString(); AnnotatedExecutableType calledType = (AnnotatedExecutableType) type; List params = calledType.getParameterTypes(); - MethodInvocationTree mit = (MethodInvocationTree) tree; - List args = mit.getArguments(); - assert params.size() == args.size(); for (int i = 0; i < params.size(); ++i) { String paramName = calledElem.getParameters().get(i).getSimpleName().toString(); diff --git a/src/test/java/tests/ConformanceTest.java b/src/test/java/tests/ConformanceTest.java index c7e2cb7d..93bd890e 100644 --- a/src/test/java/tests/ConformanceTest.java +++ b/src/test/java/tests/ConformanceTest.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -142,8 +141,7 @@ private static ImmutableSet analyze( TestUtilities.getShouldEmitDebugInfo()); TypecheckResult result = new TypecheckExecutor().runTest(config); return result.getUnexpectedDiagnostics().stream() - .map(d -> DetailMessage.parse(d.getMessage(), testDirectory)) - .filter(Objects::nonNull) + .map(d -> DetailMessage.parse(d, testDirectory)) // Do not filter out messages without details. // .filter(DetailMessage::hasDetails) .map(DetailMessageReportedFact::new) @@ -182,7 +180,7 @@ static final class DetailMessageReportedFact extends ReportedFact { private final DetailMessage detailMessage; DetailMessageReportedFact(DetailMessage detailMessage) { - super(detailMessage.file, detailMessage.lineNumber); + super(detailMessage.getFile(), detailMessage.getLineNumber()); this.detailMessage = detailMessage; } diff --git a/src/test/java/tests/DetailMessage.java b/src/test/java/tests/DetailMessage.java index c7deeedf..cf77b024 100644 --- a/src/test/java/tests/DetailMessage.java +++ b/src/test/java/tests/DetailMessage.java @@ -4,16 +4,13 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Iterables.getLast; import static java.lang.Integer.parseInt; -import static java.util.Arrays.stream; import static java.util.regex.Pattern.DOTALL; -import static java.util.stream.Collectors.joining; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; @@ -30,13 +27,6 @@ */ final class DetailMessage extends TestDiagnostic { - private static final Pattern MESSAGE_PATTERN = - Pattern.compile( - "(?\\S+):(?\\d+): (?" - + stream(DiagnosticKind.values()).map(k -> k.parseString).collect(joining("|")) - + "): (?.*)", - DOTALL); - /** Parser for the output for -Adetailedmsgtext. */ // Implemented here: org.checkerframework.framework.source.SourceChecker#detailedMsgTextPrefix private static final Pattern DETAIL_MESSAGE_PATTERN = @@ -49,12 +39,6 @@ final class DetailMessage extends TestDiagnostic { private static final Pattern OFFSETS_PATTERN = Pattern.compile("(\\( (?-?\\d+), (?-?\\d+) \\))?"); - /** The path to the source file containing the diagnostic. */ - final Path file; - - /** The line number (1-based) of the diagnostic in the {@link #file}. */ - final int lineNumber; - /** The message key for the user-visible text message that is emitted. */ final String messageKey; @@ -71,30 +55,29 @@ final class DetailMessage extends TestDiagnostic { final String readableMessage; /** - * Returns an object parsed from a diagnostic message, or {@code null} if the message doesn't - * match the expected format. + * Returns an object parsed from a diagnostic message. * * @param rootDirectory if not null, a root directory prefix to remove from the file part of the * message */ - static @Nullable DetailMessage parse(String input, @Nullable Path rootDirectory) { - Matcher messageMatcher = MESSAGE_PATTERN.matcher(input); - if (!messageMatcher.matches()) { - return null; - } - - Path file = Paths.get(messageMatcher.group("file")); - if (rootDirectory != null) { + static DetailMessage parse(TestDiagnostic input, @Nullable Path rootDirectory) { + Path file = input.getFile(); + if (rootDirectory != null && file.startsWith(rootDirectory)) { file = rootDirectory.relativize(file); } - int lineNumber = parseInt(messageMatcher.group("lineNumber")); - DiagnosticKind kind = DiagnosticKind.fromParseString(messageMatcher.group("kind")); - String message = messageMatcher.group("message"); - Matcher detailsMatcher = DETAIL_MESSAGE_PATTERN.matcher(message); + Matcher detailsMatcher = DETAIL_MESSAGE_PATTERN.matcher(input.getMessage()); if (!detailsMatcher.matches()) { // Return a message with no key or parts. - return new DetailMessage(file, lineNumber, kind, "", ImmutableList.of(), null, null, message); + return new DetailMessage( + file, + input.getLineNumber(), + input.getKind(), + "", + ImmutableList.of(), + null, + null, + input.getMessage()); } int messagePartCount = parseInt(detailsMatcher.group("messagePartCount")); @@ -112,8 +95,8 @@ final class DetailMessage extends TestDiagnostic { return new DetailMessage( file, - lineNumber, - kind, + input.getLineNumber(), + input.getKind(), detailsMatcher.group("messageKey"), messageArguments, intOrNull(offsetsMatcher.group("start")), @@ -127,16 +110,14 @@ private static Integer intOrNull(String input) { private DetailMessage( Path file, - int lineNumber, + long lineNumber, DiagnosticKind diagnosticKind, String messageKey, ImmutableList messageArguments, Integer offsetStart, Integer offsetEnd, String readableMessage) { - super(file.toString(), lineNumber, diagnosticKind, readableMessage, false, true); - this.file = file; - this.lineNumber = lineNumber; + super(file, lineNumber, diagnosticKind, readableMessage, false); this.messageKey = messageKey; this.messageArguments = messageArguments; this.offsetStart = offsetStart; @@ -144,11 +125,6 @@ private DetailMessage( this.readableMessage = readableMessage; } - /** The last part of the {@link #file}. */ - String getFileName() { - return file.getFileName().toString(); - } - /** * True if this was parsed from an actual {@code -Adetailedmsgtext} message; false if this was * some other diagnostic. @@ -183,7 +159,7 @@ public int hashCode() { @Override public String toString() { - return String.format("%s:%d: (%s) %s", file, lineNumber, messageKey, readableMessage); + return String.format("%s:%d:%s: (%s) %s", file, lineNumber, kind, messageKey, readableMessage); } /** String format for debugging use. */ @@ -191,6 +167,7 @@ String toDetailedString() { return toStringHelper(this) .add("file", file) .add("lineNumber", lineNumber) + .add("kind", kind) .add("messageKey", messageKey) .add("messageArguments", messageArguments) .add("offsetStart", offsetStart) diff --git a/src/test/java/tests/NullSpecTest.java b/src/test/java/tests/NullSpecTest.java index 500e6f4b..f5ca17e9 100644 --- a/src/test/java/tests/NullSpecTest.java +++ b/src/test/java/tests/NullSpecTest.java @@ -41,6 +41,18 @@ public static String[] getTestDirs() { } } + /** A small set of regression tests. */ + public static class Regression extends NullSpecTest { + public Regression(List testFiles) { + super(testFiles, false); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"regression"}; + } + } + /** A test that ignores cases where there is limited nullness information. */ public static class Lenient extends NullSpecTest { public Lenient(List testFiles) { @@ -105,8 +117,8 @@ public TypecheckResult adjustTypecheckResult(TypecheckResult testResult) { for (ListIterator i = unexpected.listIterator(); i.hasNext(); ) { TestDiagnostic diagnostic = i.next(); - DetailMessage detailMessage = DetailMessage.parse(diagnostic.getMessage(), null); - if (detailMessage != null && detailMessage.hasDetails()) { + DetailMessage detailMessage = DetailMessage.parse(diagnostic, null); + if (detailMessage.hasDetails()) { // Replace diagnostics that can be parsed with DetailMessage diagnostics. i.set(detailMessage); } else if (diagnostic.getKind() != DiagnosticKind.Error) { @@ -145,8 +157,8 @@ private boolean corresponds(TestDiagnostic missing, TestDiagnostic unexpected) { */ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { // First, make sure the two diagnostics are on the same file and line. - if (!missing.getFilename().equals(unexpected.getFileName()) - || missing.getLineNumber() != unexpected.lineNumber) { + if (!missing.getFilename().equals(unexpected.getFilename()) + || missing.getLineNumber() != unexpected.getLineNumber()) { return false; } From 68e9072dc7ed2cb84fa17e6a8d6ecbdc11c74915 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Wed, 17 Apr 2024 16:22:11 -0400 Subject: [PATCH 09/23] Ensure root is set in both worlds. (#174) --- .../NullSpecAnnotatedTypeFactory.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index 13c71af7..ab3512bc 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -43,6 +43,7 @@ import com.sun.source.tree.BinaryTree; import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.LambdaExpressionTree; @@ -374,6 +375,27 @@ private NullSpecAnnotatedTypeFactory( } } + /** To ensure setRoot is called on both worlds exactly once. */ + private boolean settingRoot = false; + + /** + * Ensure setRoot is called on both worlds exactly once whenever it is called on one of the + * worlds. + */ + @Override + public void setRoot(@Nullable CompilationUnitTree root) { + if (!settingRoot) { + settingRoot = true; + super.setRoot(root); + if (withLeastConvenientWorld != this) { + withLeastConvenientWorld.setRoot(root); + } else { + withMostConvenientWorld.setRoot(root); + } + settingRoot = false; + } + } + @Override protected void addCheckedCodeDefaults(QualifierDefaults defs) { // TODO: add false for subpackages once overload is added to CF. Shouldn't really matter. From 2d5331467467371e008a88a646e183ebefa4563e Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Mon, 22 Apr 2024 14:40:05 -0400 Subject: [PATCH 10/23] Fix lower bound of type variables and unspecified subtypes. (#175) --- .../NullSpecAnnotatedTypeFactory.java | 16 +++++---- tests/regression/Issue172.java | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 tests/regression/Issue172.java diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index ab3512bc..b54ff53b 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -14,7 +14,7 @@ package com.google.jspecify.nullness; -import static com.google.jspecify.nullness.NullSpecAnnotatedTypeFactory.IsDeclaredOrArray.IS_DECLARED_OR_ARRAY; +import static com.google.jspecify.nullness.NullSpecAnnotatedTypeFactory.IsDeclaredOrArrayOrNull.IS_DECLARED_OR_ARRAY_OR_NULL; import static com.google.jspecify.nullness.Util.nameMatches; import static com.sun.source.tree.Tree.Kind.IDENTIFIER; import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT; @@ -29,6 +29,7 @@ import static javax.lang.model.element.ElementKind.ENUM_CONSTANT; import static javax.lang.model.type.TypeKind.ARRAY; import static javax.lang.model.type.TypeKind.DECLARED; +import static javax.lang.model.type.TypeKind.NULL; import static javax.lang.model.type.TypeKind.TYPEVAR; import static javax.lang.model.type.TypeKind.WILDCARD; import static org.checkerframework.framework.util.AnnotatedTypes.asSuper; @@ -139,7 +140,6 @@ final class NullSpecAnnotatedTypeFactory new TypeUseLocation[] { TypeUseLocation.CONSTRUCTOR_RESULT, TypeUseLocation.EXCEPTION_PARAMETER, - TypeUseLocation.IMPLICIT_LOWER_BOUND, TypeUseLocation.RECEIVER, }; @@ -152,6 +152,10 @@ final class NullSpecAnnotatedTypeFactory private static final TypeUseLocation[] defaultLocationsUnspecified = new TypeUseLocation[] { + // Lower bounds could be MinusNull, but all uses in unmarked code would become unspecified + // anyways. + // Revisit once https://github.com/eisop/checker-framework/issues/741 is fixed. + TypeUseLocation.IMPLICIT_LOWER_BOUND, TypeUseLocation.IMPLICIT_WILDCARD_UPPER_BOUND_NO_SUPER, TypeUseLocation.TYPE_VARIABLE_USE, TypeUseLocation.OTHERWISE @@ -699,7 +703,7 @@ && isNullInclusiveUnderEveryParameterization( } boolean isNullExclusiveUnderEveryParameterization(AnnotatedTypeMirror type) { - return nullnessEstablishingPathExists(type, IS_DECLARED_OR_ARRAY); + return nullnessEstablishingPathExists(type, IS_DECLARED_OR_ARRAY_OR_NULL); } private boolean nullnessEstablishingPathExists( @@ -1852,12 +1856,12 @@ private AnnotatedDeclaredType createType(TypeElement element) { // Avoid lambdas so that our Predicates can have a useful toString() for logging purposes. - enum IsDeclaredOrArray implements Predicate { - IS_DECLARED_OR_ARRAY; + enum IsDeclaredOrArrayOrNull implements Predicate { + IS_DECLARED_OR_ARRAY_OR_NULL; @Override public boolean test(TypeMirror t) { - return t.getKind() == DECLARED || t.getKind() == ARRAY; + return t.getKind() == DECLARED || t.getKind() == ARRAY || t.getKind() == NULL; } } diff --git a/tests/regression/Issue172.java b/tests/regression/Issue172.java new file mode 100644 index 00000000..5d30b311 --- /dev/null +++ b/tests/regression/Issue172.java @@ -0,0 +1,33 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case for Issue 172: +// https://github.com/jspecify/jspecify-reference-checker/issues/172 + +import org.jspecify.annotations.NullMarked; + +class Issue172 { + E e() { + return null; + } +} + +class Issue172UnmarkedUse { + void foo(Issue172 p) {} +} + +@NullMarked +class Issue172MarkedUse { + void foo(Issue172 p) {} +} From 31279fbc9e2ec425812f23c709279773344299e6 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Mon, 22 Apr 2024 14:45:25 -0400 Subject: [PATCH 11/23] Ensure override compatibility with type variables (#171) Co-authored-by: Chris Povirk --- .../NullSpecAnnotatedTypeFactory.java | 16 +++++++++ tests/ConformanceTestOnSamples-report.txt | 4 +-- tests/regression/Issue164.java | 33 +++++++++++++++++++ tests/regression/Issue164More.java | 32 ++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 tests/regression/Issue164.java create mode 100644 tests/regression/Issue164More.java diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index b54ff53b..d37b5339 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -32,6 +32,7 @@ import static javax.lang.model.type.TypeKind.NULL; import static javax.lang.model.type.TypeKind.TYPEVAR; import static javax.lang.model.type.TypeKind.WILDCARD; +import static org.checkerframework.framework.util.AnnotatedTypes.areCorrespondingTypeVariables; import static org.checkerframework.framework.util.AnnotatedTypes.asSuper; import static org.checkerframework.javacutil.AnnotationUtils.areSame; import static org.checkerframework.javacutil.TreePathUtil.enclosingClass; @@ -666,6 +667,21 @@ private boolean isNullnessSubtype(AnnotatedTypeMirror subtype, AnnotatedTypeMirr && isNullnessSubtype(subtype, ((AnnotatedTypeVariable) supertype).getLowerBound())) { return true; } + if (subtype.getKind() == TYPEVAR && supertype.getKind() == TYPEVAR) { + // Work around wonkyness of CF override checks. + AnnotatedTypeVariable subTV = (AnnotatedTypeVariable) subtype; + if (isCapturedTypeVariable(subTV.getUnderlyingType())) { + AnnotatedTypeMirror subTVBnd = subTV.getUpperBound(); + if (subTVBnd instanceof AnnotatedTypeVariable) { + subTV = (AnnotatedTypeVariable) subTVBnd; + } + } + AnnotatedTypeVariable superTV = (AnnotatedTypeVariable) supertype; + if (areCorrespondingTypeVariables(elements, subTV, superTV)) { + return isSubtype(subTV.getUpperBound(), superTV.getUpperBound()) + && isSubtype(superTV.getLowerBound(), subTV.getLowerBound()); + } + } return isNullInclusiveUnderEveryParameterization(supertype) || isNullExclusiveUnderEveryParameterization(subtype) || (nullnessEstablishingPathExists(subtype, supertype) diff --git a/tests/ConformanceTestOnSamples-report.txt b/tests/ConformanceTestOnSamples-report.txt index 991f4fef..9e5f7006 100644 --- a/tests/ConformanceTestOnSamples-report.txt +++ b/tests/ConformanceTestOnSamples-report.txt @@ -1,4 +1,4 @@ -# 73 pass; 500 fail; 573 total; 12.7% score +# 74 pass; 499 fail; 573 total; 12.9% score FAIL: AnnotatedInnerOfNonParameterized.java: no unexpected facts FAIL: AnnotatedInnerOfParameterized.java: no unexpected facts FAIL: AnnotatedReceiver.java: no unexpected facts @@ -313,7 +313,7 @@ PASS: OutOfBoundsTypeVariable.java: no unexpected facts FAIL: OverrideParameters.java:48:jspecify_nullness_mismatch FAIL: OverrideParameters.java:68:jspecify_nullness_mismatch FAIL: OverrideParameters.java: no unexpected facts -FAIL: OverrideParametersThatAreTypeVariables.java: no unexpected facts +PASS: OverrideParametersThatAreTypeVariables.java: no unexpected facts FAIL: OverrideReturns.java:57:jspecify_nullness_mismatch FAIL: OverrideReturns.java: no unexpected facts PASS: ParameterizedWithTypeVariableArgumentToSelf.java: no unexpected facts diff --git a/tests/regression/Issue164.java b/tests/regression/Issue164.java new file mode 100644 index 00000000..10f0d86c --- /dev/null +++ b/tests/regression/Issue164.java @@ -0,0 +1,33 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case for Issue 164: +// https://github.com/jspecify/jspecify-reference-checker/issues/164 + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +interface Issue164SuperWildcardParent { + void x(Issue164Foo foo); +} + +@NullMarked +interface Issue164SuperWildcardOverride extends Issue164SuperWildcardParent { + @Override + void x(Issue164Foo foo); +} + +@NullMarked +interface Issue164Foo {} diff --git a/tests/regression/Issue164More.java b/tests/regression/Issue164More.java new file mode 100644 index 00000000..2a77d332 --- /dev/null +++ b/tests/regression/Issue164More.java @@ -0,0 +1,32 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case for Issue 164: +// https://github.com/jspecify/jspecify-reference-checker/issues/164 + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class Issue164More { + interface Super { + void foo(Lib lib); + } + + interface Sub extends Super { + void foo(Lib lib); + } + + interface Lib {} +} From 5462021584535334f4556ac6917349ba63c359d0 Mon Sep 17 00:00:00 2001 From: Chris Povirk Date: Sat, 29 Jun 2024 21:08:59 -0400 Subject: [PATCH 12/23] Stop recognizing annotations from the old package. (#181) --- build.gradle | 2 +- .../NullSpecAnnotatedTypeFactory.java | 34 ++++++------------- .../jspecify/nullness/NullSpecStore.java | 2 +- .../jspecify/nullness/NullSpecTransfer.java | 4 +-- .../jspecify/nullness/NullSpecVisitor.java | 7 ++-- 5 files changed, 17 insertions(+), 32 deletions(-) diff --git a/build.gradle b/build.gradle index 509b0f4e..06eced91 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id 'java' id 'maven-publish' - id 'com.diffplug.spotless' version '6.14.0' + id 'com.diffplug.spotless' version '6.25.0' id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' id 'net.ltgt.errorprone' version '3.0.1' } diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index d37b5339..118d21cd 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -185,7 +185,7 @@ final class NullSpecAnnotatedTypeFactory /** Constructor that takes all configuration from the provided {@code checker}. */ NullSpecAnnotatedTypeFactory(BaseTypeChecker checker, Util util) { - this(checker, util, checker.hasOption("strict"), /*withOtherWorld=*/ null); + this(checker, util, checker.hasOption("strict"), /* withOtherWorld= */ null); } /** @@ -210,8 +210,6 @@ private NullSpecAnnotatedTypeFactory( addAliasedTypeAnnotation( "org.jspecify.annotations.NullnessUnspecified", nullnessOperatorUnspecified); - addAliasedTypeAnnotation( - "org.jspecify.nullness.NullnessUnspecified", nullnessOperatorUnspecified); // Yes, it's valid to pass declaration annotations to addAliased*Type*Annotation. NULLABLE_ANNOTATIONS.forEach(a -> addAliasedTypeAnnotation(a, unionNull)); @@ -267,10 +265,6 @@ private NullSpecAnnotatedTypeFactory( "org.jspecify.annotations.NullMarked", DefaultQualifier.List.class.getCanonicalName(), nullMarkedDefaultQual); - addAliasedDeclAnnotation( - "org.jspecify.nullness.NullMarked", - DefaultQualifier.List.class.getCanonicalName(), - nullMarkedDefaultQual); // TODO: does this work as intended? /* * We assume that ProtoNonnullApi is like NullMarked in that it guarantees that *all* types @@ -316,10 +310,6 @@ private NullSpecAnnotatedTypeFactory( "org.jspecify.annotations.NullUnmarked", DefaultQualifier.List.class.getCanonicalName(), nullUnmarkedDefaultQual); - addAliasedDeclAnnotation( - "org.jspecify.nullness.NullUnmarked", - DefaultQualifier.List.class.getCanonicalName(), - nullUnmarkedDefaultQual); this.isLeastConvenientWorld = isLeastConvenientWorld; @@ -340,7 +330,7 @@ private NullSpecAnnotatedTypeFactory( if (!givenOtherWorld) { withOtherWorld = new NullSpecAnnotatedTypeFactory( - checker, util, !isLeastConvenientWorld, /*withOtherWorld=*/ this); + checker, util, !isLeastConvenientWorld, /* withOtherWorld= */ this); } if (isLeastConvenientWorld) { withLeastConvenientWorld = this; @@ -494,7 +484,7 @@ public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror su @Override protected QualifierKindHierarchy createQualifierKindHierarchy( Collection> qualifierClasses) { - return new DefaultQualifierKindHierarchy(qualifierClasses, /*bottom=*/ MinusNull.class) { + return new DefaultQualifierKindHierarchy(qualifierClasses, /* bottom= */ MinusNull.class) { @Override protected Map> createDirectSuperMap() { DefaultQualifierKind minusNullKind = @@ -851,13 +841,13 @@ private final class NullSpecEqualityComparer extends StructuralEqualityComparer @Override protected boolean checkOrAreEqual(AnnotatedTypeMirror type1, AnnotatedTypeMirror type2) { - Boolean pastResult = visitHistory.get(type1, type2, /*hierarchy=*/ unionNull); + Boolean pastResult = visitHistory.get(type1, type2, /* hierarchy= */ unionNull); if (pastResult != null) { return pastResult; } boolean result = areEqual(type1, type2); - this.visitHistory.put(type1, type2, /*hierarchy=*/ unionNull, result); + this.visitHistory.put(type1, type2, /* hierarchy= */ unionNull, result); return result; } @@ -947,7 +937,7 @@ private final class NullSpecTypeVariableSubstitutor extends TypeVariableSubstitu @Override protected AnnotatedTypeMirror substituteTypeVariable( AnnotatedTypeMirror argument, AnnotatedTypeVariable use) { - AnnotatedTypeMirror substitute = argument.deepCopy(/*copyAnnotations=*/ true); + AnnotatedTypeMirror substitute = argument.deepCopy(/* copyAnnotations= */ true); /* * The isNullExclusiveUnderEveryParameterization check handles cases like @@ -1541,7 +1531,7 @@ protected AnnotationFormatter createAnnotationFormatter() { @Override public String formatAnnotationString( Collection annos, boolean printInvisible) { - return super.formatAnnotationString(annos, /*printInvisible=*/ false); + return super.formatAnnotationString(annos, /* printInvisible= */ false); } }; } @@ -1554,7 +1544,7 @@ protected AnnotatedTypeFormatter createAnnotatedTypeFormatter() { private final class NullSpecAnnotatedTypeFormatter implements AnnotatedTypeFormatter { @Override public String format(AnnotatedTypeMirror type) { - return format(type, /*printVerbose=*/ false); + return format(type, /* printVerbose= */ false); } @Override @@ -1845,7 +1835,7 @@ private void addIfNoAnnotationPresent(AnnotatedTypeMirror type, AnnotationMirror @SuppressWarnings("unchecked") // safety guaranteed by API docs private T withMinusNull(T type) { // Remove the annotation from the *root* type, but preserve other annotations. - type = (T) type.deepCopy(/*copyAnnotations=*/ true); + type = (T) type.deepCopy(/* copyAnnotations= */ true); /* * TODO(cpovirk): In the case of a type-variable usage, I feel like we should need to *remove* * any existing annotation but then not *add* minusNull. (This is because of the difference @@ -1860,14 +1850,14 @@ private T withMinusNull(T type) { @SuppressWarnings("unchecked") // safety guaranteed by API docs private T withUnionNull(T type) { // Remove the annotation from the *root* type, but preserve other annotations. - type = (T) type.deepCopy(/*copyAnnotations=*/ true); + type = (T) type.deepCopy(/* copyAnnotations= */ true); type.replaceAnnotation(unionNull); return type; } private AnnotatedDeclaredType createType(TypeElement element) { return (AnnotatedDeclaredType) - AnnotatedTypeMirror.createType(element.asType(), this, /*isDeclaration=*/ false); + AnnotatedTypeMirror.createType(element.asType(), this, /* isDeclaration= */ false); } // Avoid lambdas so that our Predicates can have a useful toString() for logging purposes. @@ -1962,7 +1952,6 @@ private enum Present { "org.jetbrains.annotations.Nullable", "org.jmlspecs.annotation.Nullable", "org.jspecify.annotations.Nullable", - "org.jspecify.nullness.Nullable", "org.json.Nullable", "org.netbeans.api.annotations.common.CheckForNull", "org.netbeans.api.annotations.common.NullAllowed", @@ -2009,7 +1998,6 @@ private enum Present { "org.jetbrains.annotations.NotNull", "org.jmlspecs.annotation.NonNull", "org.jspecify.annotations.NonNull", - "org.jspecify.nullness.NonNull", "org.json.NonNull", "org.netbeans.api.annotations.common.NonNull", "org.springframework.lang.NonNull", diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecStore.java b/src/main/java/com/google/jspecify/nullness/NullSpecStore.java index eab4af71..d854406e 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecStore.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecStore.java @@ -30,6 +30,6 @@ final class NullSpecStore extends CFAbstractStore { @Override protected boolean shouldInsert( JavaExpression expr, CFValue value, boolean permitNondeterministic) { - return super.shouldInsert(expr, value, /*permitNondeterministic=*/ true); + return super.shouldInsert(expr, value, /* permitNondeterministic= */ true); } } diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecTransfer.java b/src/main/java/com/google/jspecify/nullness/NullSpecTransfer.java index ee526c76..eda309c3 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecTransfer.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecTransfer.java @@ -412,7 +412,7 @@ private boolean overwriteGetterFromSetter( argument.getUnderlyingType(), getter, setterCall.getReceiver(), - /*arguments=*/ emptyList()); + /* arguments= */ emptyList()); return overwrite(getterCall, argument, store); } @@ -788,7 +788,7 @@ private boolean isReflectiveRead(MethodInvocationNode node) { private AnnotatedTypeMirror typeWithTopLevelAnnotationsOnly( TransferInput input, Node node) { Set annotations = input.getValueOfSubNode(node).getAnnotations(); - AnnotatedTypeMirror type = createType(node.getType(), atypeFactory, /*isDeclaration=*/ false); + AnnotatedTypeMirror type = createType(node.getType(), atypeFactory, /* isDeclaration= */ false); type.addAnnotations(annotations); return type; } diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java b/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java index 52000ef3..9b471742 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java @@ -88,7 +88,7 @@ final class NullSpecVisitor extends BaseTypeVisitor Date: Tue, 23 Jul 2024 20:41:50 -0400 Subject: [PATCH 13/23] Update to gradle 8.9 (#187) --- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +++++-- gradlew.bat | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 34463 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 SXRcJ{65?fGI57uQ5&!`B?F@7P delta 34554 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce..09523c0e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e4..9b42019c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From 39764fba84746f99b88ccbd294ecfa6ec3d88308 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Wed, 24 Jul 2024 20:32:30 -0400 Subject: [PATCH 14/23] Use `DetailedTestDiagnostic` from EISOP instead of `DetailMessage` (#186) --- src/test/java/tests/ConformanceTest.java | 57 ++++---- src/test/java/tests/DetailMessage.java | 178 ----------------------- src/test/java/tests/NullSpecTest.java | 27 +--- 3 files changed, 40 insertions(+), 222 deletions(-) delete mode 100644 src/test/java/tests/DetailMessage.java diff --git a/src/test/java/tests/ConformanceTest.java b/src/test/java/tests/ConformanceTest.java index 93bd890e..2745d05a 100644 --- a/src/test/java/tests/ConformanceTest.java +++ b/src/test/java/tests/ConformanceTest.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -37,7 +38,9 @@ import org.checkerframework.framework.test.TestUtilities; import org.checkerframework.framework.test.TypecheckExecutor; import org.checkerframework.framework.test.TypecheckResult; +import org.checkerframework.framework.test.diagnostics.DetailedTestDiagnostic; import org.checkerframework.framework.test.diagnostics.DiagnosticKind; +import org.checkerframework.framework.test.diagnostics.TestDiagnostic; import org.jspecify.annotations.Nullable; import org.jspecify.conformance.ConformanceTestRunner; import org.jspecify.conformance.ExpectedFact; @@ -141,10 +144,7 @@ private static ImmutableSet analyze( TestUtilities.getShouldEmitDebugInfo()); TypecheckResult result = new TypecheckExecutor().runTest(config); return result.getUnexpectedDiagnostics().stream() - .map(d -> DetailMessage.parse(d, testDirectory)) - // Do not filter out messages without details. - // .filter(DetailMessage::hasDetails) - .map(DetailMessageReportedFact::new) + .map(d -> new DetailMessageReportedFact(testDirectory, d)) .collect(toImmutableSet()); } @@ -177,60 +177,67 @@ static final class DetailMessageReportedFact extends ReportedFact { "type.parameter.annotated", "wildcard.annotated"); - private final DetailMessage detailMessage; + private final TestDiagnostic diagnostic; - DetailMessageReportedFact(DetailMessage detailMessage) { - super(detailMessage.getFile(), detailMessage.getLineNumber()); - this.detailMessage = detailMessage; + DetailMessageReportedFact(@Nullable Path testDirectory, TestDiagnostic diag) { + super( + (testDirectory != null && diag.getFile().startsWith(testDirectory)) + ? testDirectory.relativize(diag.getFile()) + : diag.getFile(), + diag.getLineNumber()); + this.diagnostic = diag; } @Override protected boolean matches(ExpectedFact expectedFact) { if (expectedFact.isNullnessMismatch()) { - return DEREFERENCE.equals(detailMessage.messageKey) - || CANNOT_CONVERT_KEYS.contains(detailMessage.messageKey); + return DEREFERENCE.equals(diagnostic.getMessageKey()) + || CANNOT_CONVERT_KEYS.contains(diagnostic.getMessageKey()); } return super.matches(expectedFact); } @Override protected boolean mustBeExpected() { - return detailMessage.getKind().equals(DiagnosticKind.Error); + return diagnostic.getKind().equals(DiagnosticKind.Error); } @Override protected String getFactText() { - if (CANNOT_CONVERT_KEYS.contains(detailMessage.messageKey)) { - if (detailMessage.messageArguments.size() < 2) { + if (!(diagnostic instanceof DetailedTestDiagnostic)) { + return toString(); + } + List args = ((DetailedTestDiagnostic) diagnostic).getAdditionalTokens(); + if (CANNOT_CONVERT_KEYS.contains(diagnostic.getMessageKey())) { + if (args.size() < 2) { // The arguments must end with sourceType and sinkType. return toString(); } - ImmutableList reversedArguments = detailMessage.messageArguments.reverse(); - String sourceType = fixType(reversedArguments.get(1)); // penultimate - String sinkType = fixType(reversedArguments.get(0)); // last + String sourceType = fixType(args.get(args.size() - 2)); // penultimate + String sinkType = fixType(args.get(args.size() - 1)); // last return cannotConvert(sourceType, sinkType); } - if (IRRELEVANT_ANNOTATION_KEYS.contains(detailMessage.messageKey)) { - if (detailMessage.messageArguments.isEmpty()) { + if (IRRELEVANT_ANNOTATION_KEYS.contains(diagnostic.getMessageKey())) { + if (args.isEmpty()) { // arguments must start with the annotation return toString(); } return irrelevantAnnotation( // Remove the package name (and any enclosing element name); emit just the simple name. - detailMessage.messageArguments.get(0).replaceFirst(".*\\.", "")); + args.get(0).replaceFirst(".*\\.", "")); } - switch (detailMessage.messageKey) { + switch (diagnostic.getMessageKey()) { case "sourceType": { - String expressionType = fixType(detailMessage.messageArguments.get(0)); - String expression = detailMessage.messageArguments.get(1); + String expressionType = fixType(args.get(0)); + String expression = args.get(1); return expressionType(expressionType, expression); } case "sinkType": { - String sinkType = fixType(detailMessage.messageArguments.get(0)); + String sinkType = fixType(args.get(0)); // Remove the simple name of the class and the dot before the method name. - String sink = detailMessage.messageArguments.get(1).replaceFirst("^[^.]+\\.", ""); + String sink = args.get(1).replaceFirst("^[^.]+\\.", ""); return sinkType(sinkType, sink); } } @@ -239,7 +246,7 @@ protected String getFactText() { @Override public String toString() { - return String.format("(%s) %s", detailMessage.messageKey, detailMessage.readableMessage); + return String.format("(%s) %s", diagnostic.getMessageKey(), diagnostic.getMessage()); } /** diff --git a/src/test/java/tests/DetailMessage.java b/src/test/java/tests/DetailMessage.java deleted file mode 100644 index cf77b024..00000000 --- a/src/test/java/tests/DetailMessage.java +++ /dev/null @@ -1,178 +0,0 @@ -package tests; - -import static com.google.common.base.MoreObjects.toStringHelper; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.collect.Iterables.getLast; -import static java.lang.Integer.parseInt; -import static java.util.regex.Pattern.DOTALL; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.checkerframework.framework.test.diagnostics.DiagnosticKind; -import org.checkerframework.framework.test.diagnostics.TestDiagnostic; -import org.jspecify.annotations.Nullable; - -/** - * Information about a reported diagnostic. - * - *

      Checker Framework uses a special format to put parseable information about a diagnostic into - * the message text. This object represents that information directly. - */ -final class DetailMessage extends TestDiagnostic { - - /** Parser for the output for -Adetailedmsgtext. */ - // Implemented here: org.checkerframework.framework.source.SourceChecker#detailedMsgTextPrefix - private static final Pattern DETAIL_MESSAGE_PATTERN = - Pattern.compile( - Joiner.on(" \\$\\$ ") - .join( - "\\((?[^)]+)\\)", "(?\\d+)", "(?.*)"), - DOTALL); - - private static final Pattern OFFSETS_PATTERN = - Pattern.compile("(\\( (?-?\\d+), (?-?\\d+) \\))?"); - - /** The message key for the user-visible text message that is emitted. */ - final String messageKey; - - /** The arguments to the text message format for the message key. */ - final ImmutableList messageArguments; - - /** The offset within the file of the start of the code covered by the diagnostic. */ - final Integer offsetStart; - - /** The offset within the file of the end (exclusive) of the code covered by the diagnostic. */ - final Integer offsetEnd; - - /** The user-visible message emitted for the diagnostic. */ - final String readableMessage; - - /** - * Returns an object parsed from a diagnostic message. - * - * @param rootDirectory if not null, a root directory prefix to remove from the file part of the - * message - */ - static DetailMessage parse(TestDiagnostic input, @Nullable Path rootDirectory) { - Path file = input.getFile(); - if (rootDirectory != null && file.startsWith(rootDirectory)) { - file = rootDirectory.relativize(file); - } - - Matcher detailsMatcher = DETAIL_MESSAGE_PATTERN.matcher(input.getMessage()); - if (!detailsMatcher.matches()) { - // Return a message with no key or parts. - return new DetailMessage( - file, - input.getLineNumber(), - input.getKind(), - "", - ImmutableList.of(), - null, - null, - input.getMessage()); - } - - int messagePartCount = parseInt(detailsMatcher.group("messagePartCount")); - List messageParts = - Splitter.on("$$") - .trimResults() - .limit(messagePartCount + 2) - .splitToList(detailsMatcher.group("messageParts")); - ImmutableList messageArguments = - ImmutableList.copyOf(Iterables.limit(messageParts, messagePartCount)); - String readableMessage = getLast(messageParts); - - Matcher offsetsMatcher = OFFSETS_PATTERN.matcher(messageParts.get(messagePartCount)); - checkArgument(offsetsMatcher.matches(), "unparseable offsets: %s", input); - - return new DetailMessage( - file, - input.getLineNumber(), - input.getKind(), - detailsMatcher.group("messageKey"), - messageArguments, - intOrNull(offsetsMatcher.group("start")), - intOrNull(offsetsMatcher.group("end")), - readableMessage); - } - - private static Integer intOrNull(String input) { - return input == null ? null : parseInt(input); - } - - private DetailMessage( - Path file, - long lineNumber, - DiagnosticKind diagnosticKind, - String messageKey, - ImmutableList messageArguments, - Integer offsetStart, - Integer offsetEnd, - String readableMessage) { - super(file, lineNumber, diagnosticKind, readableMessage, false); - this.messageKey = messageKey; - this.messageArguments = messageArguments; - this.offsetStart = offsetStart; - this.offsetEnd = offsetEnd; - this.readableMessage = readableMessage; - } - - /** - * True if this was parsed from an actual {@code -Adetailedmsgtext} message; false if this was - * some other diagnostic. - */ - boolean hasDetails() { - return !messageKey.equals(""); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DetailMessage that = (DetailMessage) o; - return lineNumber == that.lineNumber - && file.equals(that.file) - && messageKey.equals(that.messageKey) - && messageArguments.equals(that.messageArguments) - && Objects.equals(offsetStart, that.offsetStart) - && Objects.equals(offsetEnd, that.offsetEnd) - && readableMessage.equals(that.readableMessage); - } - - @Override - public int hashCode() { - return Objects.hash( - file, lineNumber, messageKey, messageArguments, offsetStart, offsetEnd, readableMessage); - } - - @Override - public String toString() { - return String.format("%s:%d:%s: (%s) %s", file, lineNumber, kind, messageKey, readableMessage); - } - - /** String format for debugging use. */ - String toDetailedString() { - return toStringHelper(this) - .add("file", file) - .add("lineNumber", lineNumber) - .add("kind", kind) - .add("messageKey", messageKey) - .add("messageArguments", messageArguments) - .add("offsetStart", offsetStart) - .add("offsetEnd", offsetEnd) - .add("readableMessage", readableMessage) - .toString(); - } -} diff --git a/src/test/java/tests/NullSpecTest.java b/src/test/java/tests/NullSpecTest.java index f5ca17e9..7584547e 100644 --- a/src/test/java/tests/NullSpecTest.java +++ b/src/test/java/tests/NullSpecTest.java @@ -22,6 +22,7 @@ import java.util.ListIterator; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.checkerframework.framework.test.TypecheckResult; +import org.checkerframework.framework.test.diagnostics.DetailedTestDiagnostic; import org.checkerframework.framework.test.diagnostics.DiagnosticKind; import org.checkerframework.framework.test.diagnostics.TestDiagnostic; import org.checkerframework.javacutil.BugInCF; @@ -90,8 +91,7 @@ private static String[] getSamplesDirs() { private static String[] checkerOptions(boolean strict) { ImmutableList.Builder options = ImmutableList.builder(); - options.add( - "-AassumePure", "-Adetailedmsgtext", "-AcheckImpl", "-AsuppressWarnings=conditional"); + options.add("-AassumePure", "-AcheckImpl", "-AsuppressWarnings=conditional"); if (strict) { options.add("-Astrict"); } @@ -117,10 +117,8 @@ public TypecheckResult adjustTypecheckResult(TypecheckResult testResult) { for (ListIterator i = unexpected.listIterator(); i.hasNext(); ) { TestDiagnostic diagnostic = i.next(); - DetailMessage detailMessage = DetailMessage.parse(diagnostic, null); - if (detailMessage.hasDetails()) { - // Replace diagnostics that can be parsed with DetailMessage diagnostics. - i.set(detailMessage); + if (diagnostic instanceof DetailedTestDiagnostic) { + // Keep all detailed test diagnostics. } else if (diagnostic.getKind() != DiagnosticKind.Error) { // Remove warnings like explicit.annotation.ignored and deprecation. i.remove(); @@ -147,15 +145,6 @@ public TypecheckResult adjustTypecheckResult(TypecheckResult testResult) { * unexpected}, a reported diagnostic. */ private boolean corresponds(TestDiagnostic missing, TestDiagnostic unexpected) { - return unexpected instanceof DetailMessage - && corresponds(missing, ((DetailMessage) unexpected)); - } - - /** - * Returns {@code true} if {@code missing} is a JSpecify directive that matches {@code - * unexpected}, a reported diagnostic. - */ - private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { // First, make sure the two diagnostics are on the same file and line. if (!missing.getFilename().equals(unexpected.getFilename()) || missing.getLineNumber() != unexpected.getLineNumber()) { @@ -167,7 +156,7 @@ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { || missing.getMessage().contains("jspecify_nullness_not_enough_information") || missing.getMessage().contains("jspecify_nullness_mismatch") || missing.getMessage().contains("test:cannot-convert")) { - switch (unexpected.messageKey) { + switch (unexpected.getMessageKey()) { case "argument.type.incompatible": case "assignment.type.incompatible": case "atomicreference.must.include.null": @@ -190,7 +179,7 @@ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { switch (missing.getMessage()) { case "jspecify_nullness_intrinsically_not_nullable": - switch (unexpected.messageKey) { + switch (unexpected.getMessageKey()) { case "enum.constant.annotated": case "outer.annotated": case "primitive.annotated": @@ -199,7 +188,7 @@ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { return false; } case "jspecify_unrecognized_location": - switch (unexpected.messageKey) { + switch (unexpected.getMessageKey()) { /* * We'd rather avoid this `bound` error (in part because it suggests that the annotation * is having some effect, which we don't want!), but the most important thing is that the @@ -217,7 +206,7 @@ private boolean corresponds(TestDiagnostic missing, DetailMessage unexpected) { return false; } case "jspecify_conflicting_annotations": - switch (unexpected.messageKey) { + switch (unexpected.getMessageKey()) { case "type.invalid.conflicting.annos": case "type.invalid.super.wildcard": return true; From e1069a217d88cbbdc6783cab75a67da519ec6570 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Wed, 24 Jul 2024 20:39:10 -0400 Subject: [PATCH 15/23] Introduce a `NullSpecTypeValidator` to ensure unspecified type variables are ok (#178) --- .../NullSpecAnnotatedTypeFactory.java | 5 +-- .../nullness/NullSpecTypeValidator.java | 45 +++++++++++++++++++ .../jspecify/nullness/NullSpecVisitor.java | 6 +++ src/test/java/tests/NullSpecTest.java | 12 +++++ tests/ConformanceTest-report.txt | 5 ++- tests/regression-strict/Issue177.java | 18 ++++++++ tests/regression/Issue172.java | 4 ++ 7 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/google/jspecify/nullness/NullSpecTypeValidator.java create mode 100644 tests/regression-strict/Issue177.java diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index 17ef8df9..6b15f91d 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -145,6 +145,7 @@ final class NullSpecAnnotatedTypeFactory new TypeUseLocation[] { TypeUseLocation.CONSTRUCTOR_RESULT, TypeUseLocation.EXCEPTION_PARAMETER, + TypeUseLocation.IMPLICIT_LOWER_BOUND, TypeUseLocation.RECEIVER, }; @@ -157,10 +158,6 @@ final class NullSpecAnnotatedTypeFactory private static final TypeUseLocation[] defaultLocationsUnspecified = new TypeUseLocation[] { - // Lower bounds could be MinusNull, but all uses in unmarked code would become unspecified - // anyways. - // Revisit once https://github.com/eisop/checker-framework/issues/741 is fixed. - TypeUseLocation.IMPLICIT_LOWER_BOUND, TypeUseLocation.IMPLICIT_WILDCARD_UPPER_BOUND_NO_SUPER, TypeUseLocation.TYPE_VARIABLE_USE, TypeUseLocation.OTHERWISE diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecTypeValidator.java b/src/main/java/com/google/jspecify/nullness/NullSpecTypeValidator.java new file mode 100644 index 00000000..bb29fc08 --- /dev/null +++ b/src/main/java/com/google/jspecify/nullness/NullSpecTypeValidator.java @@ -0,0 +1,45 @@ +// Copyright 2020 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.jspecify.nullness; + +import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeValidator; +import org.checkerframework.framework.type.AnnotatedTypeMirror; + +final class NullSpecTypeValidator extends BaseTypeValidator { + private final AnnotationMirror nullnessOperatorUnspecified; + + /** Constructor. */ + NullSpecTypeValidator( + BaseTypeChecker checker, + NullSpecVisitor visitor, + NullSpecAnnotatedTypeFactory atypeFactory, + Util util) { + super(checker, visitor, atypeFactory); + + nullnessOperatorUnspecified = util.nullnessOperatorUnspecified; + } + + @Override + public boolean areBoundsValid(AnnotatedTypeMirror upperBound, AnnotatedTypeMirror lowerBound) { + if (upperBound.hasAnnotation(nullnessOperatorUnspecified) + || lowerBound.hasAnnotation(nullnessOperatorUnspecified)) { + return true; + } else { + return super.areBoundsValid(upperBound, lowerBound); + } + } +} diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java b/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java index 9b471742..c6c2f83c 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecVisitor.java @@ -71,6 +71,7 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.common.basetype.TypeValidator; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; @@ -637,4 +638,9 @@ protected NullSpecAnnotatedTypeFactory createTypeFactory() { // Reading util this way is ugly but necessary. See discussion in NullSpecChecker. return new NullSpecAnnotatedTypeFactory(checker, ((NullSpecChecker) checker).util); } + + @Override + protected TypeValidator createTypeValidator() { + return new NullSpecTypeValidator(checker, this, atypeFactory, ((NullSpecChecker) checker).util); + } } diff --git a/src/test/java/tests/NullSpecTest.java b/src/test/java/tests/NullSpecTest.java index 7584547e..3f127d5b 100644 --- a/src/test/java/tests/NullSpecTest.java +++ b/src/test/java/tests/NullSpecTest.java @@ -54,6 +54,18 @@ public static String[] getTestDirs() { } } + /** A small set of strict regression tests. */ + public static class RegressionStrict extends NullSpecTest { + public RegressionStrict(List testFiles) { + super(testFiles, true); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"regression-strict"}; + } + } + /** A test that ignores cases where there is limited nullness information. */ public static class Lenient extends NullSpecTest { public Lenient(List testFiles) { diff --git a/tests/ConformanceTest-report.txt b/tests/ConformanceTest-report.txt index 23c8a51e..c8496eea 100644 --- a/tests/ConformanceTest-report.txt +++ b/tests/ConformanceTest-report.txt @@ -1,4 +1,4 @@ -# 90 pass; 24 fail; 114 total; 78.9% score +# 93 pass; 24 fail; 117 total; 79.5% score PASS: Basic.java:28:test:expression-type:Object?:nullable PASS: Basic.java:28:test:sink-type:Object!:return PASS: Basic.java:28:test:cannot-convert:Object? to Object! @@ -44,6 +44,7 @@ PASS: irrelevantannotations/notnullmarked/Other.java:Nullable local variable arr PASS: irrelevantannotations/notnullmarked/Other.java:NonNull local variable array:test:irrelevant-annotation:NonNull PASS: irrelevantannotations/notnullmarked/Other.java:Nullable exception parameter:test:irrelevant-annotation:Nullable PASS: irrelevantannotations/notnullmarked/Other.java:NonNull exception parameter:test:irrelevant-annotation:NonNull +PASS: irrelevantannotations/notnullmarked/Other.java:Intrinsically NonNull exception parameter cannot be assigned null:test:cannot-convert:null? to RuntimeException! PASS: irrelevantannotations/notnullmarked/Other.java:Nullable try-with-resources:test:irrelevant-annotation:Nullable FAIL: irrelevantannotations/notnullmarked/Other.java:NonNull try-with-resources:test:irrelevant-annotation:NonNull FAIL: irrelevantannotations/notnullmarked/Other.java:Nullable exception type:test:irrelevant-annotation:Nullable @@ -75,6 +76,7 @@ PASS: irrelevantannotations/nullmarked/Other.java:Nullable local variable array: PASS: irrelevantannotations/nullmarked/Other.java:NonNull local variable array:test:irrelevant-annotation:NonNull PASS: irrelevantannotations/nullmarked/Other.java:Nullable exception parameter:test:irrelevant-annotation:Nullable PASS: irrelevantannotations/nullmarked/Other.java:NonNull exception parameter:test:irrelevant-annotation:NonNull +PASS: irrelevantannotations/nullmarked/Other.java:Intrinsically NonNull exception parameter cannot be assigned null:test:cannot-convert:null? to RuntimeException! PASS: irrelevantannotations/nullmarked/Other.java:Nullable try-with-resources:test:irrelevant-annotation:Nullable FAIL: irrelevantannotations/nullmarked/Other.java:NonNull try-with-resources:test:irrelevant-annotation:NonNull FAIL: irrelevantannotations/nullmarked/Other.java:Nullable exception type:test:irrelevant-annotation:Nullable @@ -107,6 +109,7 @@ PASS: irrelevantannotations/nullunmarked/Other.java:Nullable local variable arra PASS: irrelevantannotations/nullunmarked/Other.java:NonNull local variable array:test:irrelevant-annotation:NonNull PASS: irrelevantannotations/nullunmarked/Other.java:Nullable exception parameter:test:irrelevant-annotation:Nullable PASS: irrelevantannotations/nullunmarked/Other.java:NonNull exception parameter:test:irrelevant-annotation:NonNull +PASS: irrelevantannotations/nullunmarked/Other.java:Intrinsically NonNull exception parameter cannot be assigned null:test:cannot-convert:null? to RuntimeException! PASS: irrelevantannotations/nullunmarked/Other.java:Nullable try-with-resources:test:irrelevant-annotation:Nullable FAIL: irrelevantannotations/nullunmarked/Other.java:NonNull try-with-resources:test:irrelevant-annotation:NonNull FAIL: irrelevantannotations/nullunmarked/Other.java:Nullable exception type:test:irrelevant-annotation:Nullable diff --git a/tests/regression-strict/Issue177.java b/tests/regression-strict/Issue177.java new file mode 100644 index 00000000..c47c8a9e --- /dev/null +++ b/tests/regression-strict/Issue177.java @@ -0,0 +1,18 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case for Issue 177: +// https://github.com/jspecify/jspecify-reference-checker/issues/177 + +class Issue177 {} diff --git a/tests/regression/Issue172.java b/tests/regression/Issue172.java index 5d30b311..b782b8df 100644 --- a/tests/regression/Issue172.java +++ b/tests/regression/Issue172.java @@ -21,6 +21,10 @@ class Issue172 { E e() { return null; } + + void p(E p) { + p = null; + } } class Issue172UnmarkedUse { From 36855b80c0f5bb0f096462f21f13e4e1163322aa Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Wed, 24 Jul 2024 20:45:40 -0400 Subject: [PATCH 16/23] Use EISOP release and include jspecify/jdk in jar (#188) --- README.md | 8 ++-- build.gradle | 106 ++++++++++++++++++++++++++++++++++++-------- demo | 18 +++++++- docs/development.md | 15 +++---- initialize-project | 4 -- settings.gradle | 4 +- 6 files changed, 117 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 4ec80456..153763da 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,13 @@ But that's its *only* job. Notably, it is: ## Relationship to Checker Framework and EISOP -The [EISOP project](https://eisop.github.io/) maintains a fork of [Checker Framework](https://checkerframework.org/), and JSpecify conformance is one of its primary goals. +The [EISOP project](https://eisop.github.io/) maintains a fork of the [Checker Framework](https://checkerframework.org/), and JSpecify conformance is one of its goals. -This tool happens to be built on top of another fork of these ([here](https://github.com/jspecify/checker-framework)). However, please view this relationship as **implementation detail** only. Building a reference checker from scratch would simply have been too difficult, so we needed to base it on some existing tool. The choice of which tool was made purely for expediency and is **subject to change**. +This tool is built on top of the [EISOP Checker Framework](https://github.com/eisop/checker-framework). However, please view this relationship as **implementation detail** only. Building a reference checker from scratch would simply have been too difficult, so we needed to base it on some existing tool. The choice of which tool was made purely for expediency and is **subject to change**. ## Usage -Building and running this tool requires building code from several other repositories, but these instructions will take care of that automatically. +Building and running this tool depends on code from several other repositories, but these instructions will take care of that automatically. These instructions might require workarounds or fail outright. Please file an issue if you have any trouble! @@ -31,7 +31,7 @@ These instructions might require workarounds or fail outright. Please file an is Ideally set `JAVA_HOME` to a JDK 11 or JDK 18 installation. -Make sure you have Apache Maven installed and in your PATH, or the Gradle build will fail: +Make sure you have Apache Maven installed and in your PATH, or the `demo` script will fail: ```sh mvn diff --git a/build.gradle b/build.gradle index 06eced91..90687071 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,17 @@ plugins { group = 'org.jspecify.reference' version = '0.0.0-SNAPSHOT' +sourceSets { + main { + resources { + // Minimized jspecify/jdk + srcDirs += [ + "${buildDir}/generated/resources" + ] + } + } +} + repositories { mavenLocal() maven { @@ -35,10 +46,14 @@ nexusPublishing { } ext { - checkerFramework = gradle.includedBuild("checker-framework") + // null if not included with `--include-build path/to/checker-framework` + checkerFramework = gradle.includedBuilds.find { it.name == 'checker-framework' } // null if not included with `--include-build path/to/jspecify` jspecify = gradle.includedBuilds.find { it.name == 'jspecify' } + + // Location of the jspecify/jdk clone, relative to this directory + jspecifyJdkHome = '../jdk' } configurations { @@ -61,8 +76,10 @@ java { dependencies { implementation libs.checkerFramework.checker implementation libs.checkerFramework.checker.qual - implementation libs.checkerFramework.framework - implementation libs.checkerFramework.javacutil + // Eventually, we would want to only depend on `framework` and + // `javacutil` artifacts instead of the entire `checker`. + // implementation libs.checkerFramework.framework + // implementation libs.checkerFramework.javacutil implementation libs.jspecify @@ -78,8 +95,11 @@ dependencies { errorprone libs.errorProne.core } -// Assemble checker-framework when assembling the reference checker. -assemble.dependsOn(checkerFramework.task(":assemble")) +// If built with `--include-build path/to/checker-framework` then +// assemble checker-framework when assembling the reference checker. +if (checkerFramework != null) { + assemble.dependsOn(checkerFramework.task(":assembleForJavac")) +} // If built with `--include-build path/to/jspecify` then // assemble jspecify when assembling the reference checker. @@ -111,6 +131,54 @@ tasks.withType(JavaCompile).configureEach { .collect { "--add-exports=jdk.compiler/com.sun.tools.javac.$it=ALL-UNNAMED" }) } +tasks.register('includeJSpecifyJDK') { + group = 'Build' + shouldRunAfter 'compileJava' + + def srcDir = "${jspecifyJdkHome}/src" + // This directory needs to be stored at the top-level of the resulting .jar file. + // org.checkerframework.framework.stub.AnnotationFileElementTypes will then load + // the JDK classes from here instead of from checker.jar. + def dstDir = "${buildDir}/generated/resources/annotated-jdk/src/" + + inputs.dir file(srcDir) + outputs.dir file(dstDir) + + doLast { + FileTree srcTree = fileTree(dir: srcDir) + NavigableSet specFiles = new TreeSet<>(); + srcTree.visit { FileVisitDetails fvd -> + if (!fvd.file.isDirectory() && fvd.file.name.matches('.*\\.java')) { + fvd.getFile().readLines().any { line -> + if (line.contains('org.jspecify')) { + specFiles.add(fvd.file.absolutePath) + return true; + } + } + } + } + String absoluteSrcDir = file(srcDir).absolutePath + int srcPrefixSize = absoluteSrcDir.size() + copy { + from(srcDir) + into(dstDir) + for (String specFile : specFiles) { + include specFile.substring(srcPrefixSize) + } + } + javaexec { + classpath = sourceSets.main.runtimeClasspath + standardOutput = System.out + errorOutput = System.err + + mainClass = 'org.checkerframework.framework.stub.JavaStubifier' + args dstDir + } + } +} + +processResources.dependsOn(includeJSpecifyJDK) + tasks.withType(Test).configureEach { if (!JavaVersion.current().java9Compatible) { jvmArgs "-Xbootclasspath/p:${configurations.errorproneJavac.asPath}" @@ -219,19 +287,21 @@ tasks.register('demoTest', Exec) { See https://github.com/jspecify/jspecify-reference-checker/issues/81 */ -def cfQualJar = - checkerFramework.projectDir.toPath() - .resolve("checker-qual/build/libs/checker-qual-${libs.versions.checkerFramework.get()}.jar") - -if (!cfQualJar.toFile().exists()) { - mkdir(cfQualJar.parent) - exec { - executable 'jar' - args = [ - 'cf', - cfQualJar, - buildFile.path // Use this build script file! - ] +if (checkerFramework != null) { + def cfQualJar = + checkerFramework.projectDir.toPath() + .resolve("checker-qual/build/libs/checker-qual-${libs.versions.checkerFramework.get()}.jar") + + if (!cfQualJar.toFile().exists()) { + mkdir(cfQualJar.parent) + exec { + executable 'jar' + args = [ + 'cf', + cfQualJar, + buildFile.path // Use this build script file! + ] + } } } diff --git a/demo b/demo index a96b397e..00b47c9f 100755 --- a/demo +++ b/demo @@ -15,6 +15,20 @@ if [ ! -e "${jspecify}" ]; then -DoutputDirectory="$(dirname "${jspecify}")" fi fi + +checkerFrameworkDir="${dir}/../checker-framework/" +checkerFrameworkJar="${checkerFrameworkDir}/checker/dist/checker.jar" +if [ ! -e "${checkerFrameworkJar}" ]; then + cfVersion="3.42.0-eisop4" + checkerFrameworkJar="${dir}/build/checker-${cfVersion}-all.jar" + if [ ! -e "${checkerFrameworkJar}" ]; then + echo "Downloading $(basename "${checkerFrameworkJar}") from Maven central" + mvn -q org.apache.maven.plugins:maven-dependency-plugin:3.6.1:copy \ + -Dartifact="io.github.eisop:checker:${cfVersion}:jar:all" \ + -DoutputDirectory="$(dirname "${checkerFrameworkJar}")" + fi +fi + jspecify_reference_checker="${dir}/build/libs/jspecify-reference-checker-0.0.0-SNAPSHOT.jar" if [ ! -e "${jspecify_reference_checker}" ]; then echo "Assembling jspecify-reference-checker" @@ -25,9 +39,11 @@ ourclasspath="${jspecify}:${jspecify_reference_checker}" export CLASSPATH="${ourclasspath}:$CLASSPATH" -$dir/../checker-framework/checker/bin/javac \ +java -jar "${checkerFrameworkJar}" \ -processorpath "${ourclasspath}" \ -processor com.google.jspecify.nullness.NullSpecChecker \ + -checkerQualJar "${checkerFrameworkJar}" \ + -checkerUtilJar "${checkerFrameworkJar}" \ -AcheckImpl \ -AassumePure \ -AsuppressWarnings=contracts.conditional.postcondition.false.methodref,contracts.conditional.postcondition.false.override,contracts.conditional.postcondition.true.methodref,contracts.conditional.postcondition.true.override,purity.methodref,purity.overriding,type.anno.before.decl.anno,type.anno.before.modifier \ diff --git a/docs/development.md b/docs/development.md index 3999d396..82d66022 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,14 +1,14 @@ # Development -## Codevelopment with Checker Framework fork +## Codevelopment with the EISOP Checker Framework -This project depends on -an [unreleased fork of the Checker Framework][jspecify-checker-framework]. -(The [main-eisop branch] represents ongoing work to depend on a released version -of the [EISOP] fork instead.) +This project depends on the [EISOP Checker Framework][EISOP]. + +To codevelop changes with the EISOP Checker Framework, clone it into the +sibling directory `../checker-framwork` and pass +`--include-build path/to/checker-framework` to Gradle when building +this project. -Because of that dependency, this build clones that unreleased fork into the -sibling directory `../checker-framwork`. _That_ build then clones some other projects into other sibling directories. It expects `../jdk` to contain an annotated JDK, so our build clones [JSpecify's][jspecify-jdk] there. @@ -40,7 +40,6 @@ Gradle properties on the command line. of the conformance test suite. [EISOP]: https://github.com/eisop/checker-framework -[jspecify-checker-framework]: https://github.com/jspecify/checker-framework [jspecify-jdk]: https://github.com/jspecify/jdk [jspecify-jspecify]: https://github.com/jspecify/jspecify [main-eisop branch]: https://github.com/jspecify/jspecify-reference-checker/tree/main-eisop diff --git a/initialize-project b/initialize-project index 77b89d2b..5f04d589 100755 --- a/initialize-project +++ b/initialize-project @@ -19,8 +19,6 @@ # that contains your forks. For example, FORK=git@github.com:myorg means this # script tries to clone the following before falling back to the JSpecify repos: # -# git@github:myorg/checker-framework.git -# git@github:myorg/jspecify.git # git@github:myorg/jdk.git set -eu @@ -71,5 +69,3 @@ git_clone() { } git_clone jdk --depth 1 --single-branch - -git_clone checker-framework diff --git a/settings.gradle b/settings.gradle index 30a546bc..2874741e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,12 +10,10 @@ exec { executable './initialize-project' } -includeBuild("../checker-framework") - dependencyResolutionManagement { versionCatalogs { libs { - version("checkerFramework", "3.42.0-eisop3-SNAPSHOT") + version("checkerFramework", "3.42.0-eisop4") library("checkerFramework-checker", "io.github.eisop", "checker").versionRef("checkerFramework") library("checkerFramework-checker-qual", "io.github.eisop", "checker-qual").versionRef("checkerFramework") From af9dfbc7a018b3b079725d40ebaf87bc4c21e8bd Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Wed, 24 Jul 2024 21:03:54 -0400 Subject: [PATCH 17/23] Make a local release of the reference checker (#189) Co-authored-by: Chris Povirk --- build.gradle | 44 ++++++++++++++++ demo | 2 +- docs/development.md | 2 +- gradle.properties | 2 +- usage-demo/README.md | 21 ++++++++ usage-demo/build.gradle | 70 +++++++++++++++++++++++++ usage-demo/settings.gradle | 2 + usage-demo/src/main/java/demo/Demo.java | 26 +++++++++ 8 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 usage-demo/README.md create mode 100644 usage-demo/build.gradle create mode 100644 usage-demo/settings.gradle create mode 100644 usage-demo/src/main/java/demo/Demo.java diff --git a/build.gradle b/build.gradle index 90687071..2dede818 100644 --- a/build.gradle +++ b/build.gradle @@ -333,3 +333,47 @@ eclipse.classpath { } } } + +publishing { + publications { + jspecifyReferenceChecker(MavenPublication) { + pom { + groupId = 'org.jspecify.reference' + artifactId = 'checker' + version = project.version + name = 'JSpecify Reference Checker' + description = 'The JSpecify Reference Checker' + url = 'http://jspecify.org/' + from components.java + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + scm { + connection = 'scm:git:git@github.com:jspecify/jspecify-reference-checker.git' + developerConnection = 'scm:git:git@github.com:jspecify/jspecify-reference-checker.git' + url = 'https://github.com/jspecify/jspecify-reference-checker' + } + developers { + developer { + id = 'cpovirk' + name = 'Chris Povirk' + email = 'cpovirk@google.com' + } + developer { + id = 'netdpb' + name = 'David P. Baker' + email = 'dpb@google.com' + } + developer { + id = 'wmdietl' + name = 'Werner M. Dietl' + email = 'wdietl@gmail.com' + } + } + } + } + } +} diff --git a/demo b/demo index 00b47c9f..0e2ae5f0 100755 --- a/demo +++ b/demo @@ -6,7 +6,7 @@ dir=$(dirname $0) jspecify="${dir}/../jspecify/build/libs/jspecify-0.0.0-SNAPSHOT.jar" if [ ! -e "${jspecify}" ]; then - version=0.3.0 + version=1.0.0 jspecify="${dir}/build/jspecify-${version}.jar" if [ ! -e "${jspecify}" ]; then echo "Downloading $(basename "${jspecify}") from Maven central" diff --git a/docs/development.md b/docs/development.md index 82d66022..74fb3d2d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -29,7 +29,7 @@ clone the repo (or your fork) somewhere, and pass `--include-build path/to/jspecify` to Gradle when building. The local clone will be used for both the annotations and the conformance test suite. -By default the reference checker depends on version `0.3.0` of the annotations, +By default the reference checker depends on version `1.0.0` of the annotations, and version `0.0.0-SNAPSHOT` of the conformance test suite. In order to depend on a different published version of either artifact, set diff --git a/gradle.properties b/gradle.properties index 412032de..7315fa5f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -org.jspecify\:jspecify=0.3.0 +org.jspecify\:jspecify=1.0.0 org.jspecify.conformance\:conformance-test-framework=0.0.0-SNAPSHOT org.jspecify.conformance\:conformance-tests=0.0.0-SNAPSHOT diff --git a/usage-demo/README.md b/usage-demo/README.md new file mode 100644 index 00000000..3aaf2233 --- /dev/null +++ b/usage-demo/README.md @@ -0,0 +1,21 @@ +# JSpecify Reference Checker Usage Demo + +This is a simple demonstration for how a gradle project can use the JSpecify Reference Checker. + +Until the JSpecify Reference Checker is released to Maven Central, in the parent directory, +one must first run: + +```` +./gradlew PublishToMavenLocal +```` + +to publish the JSpecify Reference Checker to the local Maven repository. + +Then, in the current `usage-demo` directory, one can run: + +```` +../gradlew assemble +```` + +to assemble the demo project and get a set of three expected error messages +(plus one warning from Error Prone). diff --git a/usage-demo/build.gradle b/usage-demo/build.gradle new file mode 100644 index 00000000..3d99f034 --- /dev/null +++ b/usage-demo/build.gradle @@ -0,0 +1,70 @@ +// EISOP Checker Framework and JSpecify Reference Checker example. + +plugins { + id 'java' + id 'com.diffplug.spotless' version '6.25.0' + id 'net.ltgt.errorprone' version '3.1.0' + id 'org.checkerframework' version '0.6.42' apply false +} + +ext { + versions = [ + eisopVersion: '3.42.0-eisop4', + jspecifyVersion: '1.0.0', + jspecifyReferenceCheckerVersion: '0.0.0-SNAPSHOT' + ] +} + +repositories { + mavenLocal() + mavenCentral() +} + +// Project dependencies, e.g. error prone. +dependencies { + errorprone 'com.google.errorprone:error_prone_core:2.23.0' +} + +// Dependency on JSpecify annotations. +dependencies { + implementation "org.jspecify:jspecify:${versions.jspecifyVersion}" +} + + +apply plugin: 'org.checkerframework' + +// Configure EISOP and JSpecify Reference Checker +dependencies { + compileOnly "io.github.eisop:checker-qual:${versions.eisopVersion}" + testCompileOnly "io.github.eisop:checker-qual:${versions.eisopVersion}" + checkerFramework "io.github.eisop:checker:${versions.eisopVersion}" + + compileOnly "org.jspecify.reference:checker:${versions.jspecifyReferenceCheckerVersion}" + checkerFramework "org.jspecify.reference:checker:${versions.jspecifyReferenceCheckerVersion}" +} + +checkerFramework { + checkers = [ + 'com.google.jspecify.nullness.NullSpecChecker', + ] + extraJavacArgs = [ + // Check implementation code, i.e. method bodies. + '-AcheckImpl', + // Output the EISOP version, to ensure configuration is correct. + '-Aversion' + ] +} + +spotless { + java { + target '**/*.java' + googleJavaFormat() + formatAnnotations() + } + groovyGradle { + target '**/*.gradle' + greclipse() + indentWithSpaces(4) + trimTrailingWhitespace() + } +} diff --git a/usage-demo/settings.gradle b/usage-demo/settings.gradle new file mode 100644 index 00000000..a632f3be --- /dev/null +++ b/usage-demo/settings.gradle @@ -0,0 +1,2 @@ +// Project name is read-only in build scripts, and defaults to directory name. +rootProject.name = "jspecify-reference-checker-usage-demo" diff --git a/usage-demo/src/main/java/demo/Demo.java b/usage-demo/src/main/java/demo/Demo.java new file mode 100644 index 00000000..8f57b853 --- /dev/null +++ b/usage-demo/src/main/java/demo/Demo.java @@ -0,0 +1,26 @@ +package demo; + +import java.lang.reflect.Method; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class Demo { + // Error about usage on primitive, also a warning from Error Prone. + void conflict(@Nullable int i) {} + + Object incompatible(@Nullable Object in) { + // Error about incompatible return. + return in; + } + + String deref(@Nullable Object in) { + // Error about dereference of nullable reference. + return in.toString(); + } + + void jdkDemo(Method m) throws Exception { + // Demo to ensure the jspecify/jdk is used. No error expected. eisop/jdk would give an error. + m.invoke(null); + } +} From 89e055f7374a581a34e9fdfce572f85ea82948d0 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Fri, 26 Jul 2024 14:46:25 -0400 Subject: [PATCH 18/23] Use same version --- demo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo b/demo index cd22c802..496f0b5f 100755 --- a/demo +++ b/demo @@ -23,7 +23,7 @@ if [ ! -e "${checkerFrameworkJar}" ]; then checkerFrameworkJar="${dir}/build/checker-${cfVersion}-all.jar" if [ ! -e "${checkerFrameworkJar}" ]; then echo "Downloading $(basename "${checkerFrameworkJar}") from Maven central" - mvn -q org.apache.maven.plugins:maven-dependency-plugin:3.6.1:copy \ + mvn -q org.apache.maven.plugins:maven-dependency-plugin:3.7.1:copy \ -Dartifact="io.github.eisop:checker:${cfVersion}:jar:all" \ -DoutputDirectory="$(dirname "${checkerFrameworkJar}")" fi From 746e34b4dea726637c5e09fa978313a0f3508014 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Fri, 26 Jul 2024 15:39:58 -0400 Subject: [PATCH 19/23] Build a local `eisop/checker-framework` for CI (#196) --- .github/workflows/build.yml | 9 +++++++-- build.gradle | 6 ++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 013788a3..78bc7977 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,11 @@ jobs: with: repository: jspecify/jspecify path: jspecify + - name: Check out eisop/checker-framework + uses: actions/checkout@v4 + with: + repository: eisop/checker-framework + path: checker-framework - name: Set up Java uses: actions/setup-java@v4 with: @@ -27,7 +32,7 @@ jobs: - name: Set up Gradle uses: gradle/gradle-build-action@v3 - name: Build and Test - run: ./gradlew build conformanceTests demoTest --include-build ../jspecify + run: ./gradlew build conformanceTests demoTest --include-build ../jspecify --include-build ../checker-framework env: SHALLOW: 1 JSPECIFY_CONFORMANCE_TEST_MODE: details @@ -39,7 +44,7 @@ jobs: working-directory: jspecify - name: Run Samples Tests if: always() - run: ./gradlew jspecifySamplesTest --include-build ../jspecify + run: ./gradlew jspecifySamplesTest --include-build ../jspecify --include-build ../checker-framework publish-snapshot: name: Publish Conformance Test Framework Snapshot diff --git a/build.gradle b/build.gradle index 2dede818..251909f0 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,8 @@ plugins { id 'com.diffplug.spotless' version '6.25.0' id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' id 'net.ltgt.errorprone' version '3.0.1' + // To show task list as a tree, run: ./gradlew taskTree + id 'com.dorongold.task-tree' version '4.0.0' } // Nexus Publish plugin requires a group/version at the root project. @@ -98,7 +100,7 @@ dependencies { // If built with `--include-build path/to/checker-framework` then // assemble checker-framework when assembling the reference checker. if (checkerFramework != null) { - assemble.dependsOn(checkerFramework.task(":assembleForJavac")) + compileJava.dependsOn(checkerFramework.task(":checker:assembleForJavac")) } // If built with `--include-build path/to/jspecify` then @@ -133,7 +135,7 @@ tasks.withType(JavaCompile).configureEach { tasks.register('includeJSpecifyJDK') { group = 'Build' - shouldRunAfter 'compileJava' + dependsOn 'compileJava' def srcDir = "${jspecifyJdkHome}/src" // This directory needs to be stored at the top-level of the resulting .jar file. From 3c80e506af5fdd2d9f292082af53e84126317bb5 Mon Sep 17 00:00:00 2001 From: Chris Povirk Date: Fri, 26 Jul 2024 15:57:49 -0400 Subject: [PATCH 20/23] Report all errors as [nullness]. (#194) Co-authored-by: Werner Dietl --- .../java/com/google/jspecify/nullness/NullSpecChecker.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java b/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java index 4d490c86..54d6139d 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecChecker.java @@ -69,6 +69,11 @@ public NavigableSet getSuppressWarningsPrefixes() { return prefixes; } + @Override + protected String suppressWarningsString(String messageKey) { + return "nullness"; + } + @Override public void initChecker() { super.initChecker(); From fc545a8089d627d9c910cc504955fa55d72cfea1 Mon Sep 17 00:00:00 2001 From: Chris Povirk Date: Wed, 14 Aug 2024 13:44:43 -0400 Subject: [PATCH 21/23] Fix capture conversion in lenient mode. (#197) ...by always performing capture conversion in _strict_ mode, where there is a better-defined hierarchy (non-null < unspecified < nullable, with no "but also unspecified < non-null if you want). Fixes https://github.com/jspecify/jspecify-reference-checker/issues/193 --- .../NullSpecAnnotatedTypeFactory.java | 8 +++++ tests/regression/Issue197.java | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/regression/Issue197.java diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index 6b15f91d..c13bab0a 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -531,6 +531,14 @@ public boolean isParametricQualifier(AnnotationMirror qualifier) { } } + @Override + public AnnotatedTypeMirror applyCaptureConversion( + AnnotatedTypeMirror type, TypeMirror typeMirror) { + return this == withLeastConvenientWorld + ? super.applyCaptureConversion(type, typeMirror) + : withLeastConvenientWorld.applyCaptureConversion(type, typeMirror); + } + @Override protected TypeHierarchy createTypeHierarchy() { return new NullSpecTypeHierarchy( diff --git a/tests/regression/Issue197.java b/tests/regression/Issue197.java new file mode 100644 index 00000000..eac3ca61 --- /dev/null +++ b/tests/regression/Issue197.java @@ -0,0 +1,33 @@ +// Copyright 2024 The JSpecify Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Test case for Issue 197: +// https://github.com/jspecify/jspecify-reference-checker/issues/197 + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +class Issue197 { + interface Function {} + + interface Super { + void i(Function p); + } + + @NullMarked + interface Sub extends Super { + @Override + void i(Function p); + } +} From eea8585597aedc3dad0cf3f5720cd3f394fd0900 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Fri, 23 Aug 2024 19:33:57 -0400 Subject: [PATCH 22/23] Use the new `NoContractsFromMethod` (#201) --- .../NullSpecAnnotatedTypeFactory.java | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java index c13bab0a..fc8202a4 100644 --- a/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java +++ b/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java @@ -104,12 +104,10 @@ import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.framework.type.visitor.AnnotatedTypeVisitor; import org.checkerframework.framework.util.AnnotationFormatter; -import org.checkerframework.framework.util.Contract.ConditionalPostcondition; -import org.checkerframework.framework.util.Contract.Postcondition; -import org.checkerframework.framework.util.Contract.Precondition; import org.checkerframework.framework.util.ContractsFromMethod; import org.checkerframework.framework.util.DefaultAnnotationFormatter; import org.checkerframework.framework.util.DefaultQualifierKindHierarchy; +import org.checkerframework.framework.util.NoContractsFromMethod; import org.checkerframework.framework.util.QualifierKindHierarchy; import org.checkerframework.framework.util.defaults.QualifierDefaults; import org.checkerframework.javacutil.AnnotationBuilder; @@ -1037,23 +1035,7 @@ public boolean applyConservativeDefaults(Element annotationScope) { // Disable checking of contracts. @Override protected ContractsFromMethod createContractsFromMethod() { - return new ContractsFromMethod(this) { - @Override - public Set getConditionalPostconditions( - ExecutableElement methodElement) { - return emptySet(); - } - - @Override - public Set getPreconditions(ExecutableElement executableElement) { - return emptySet(); - } - - @Override - public Set getPostconditions(ExecutableElement executableElement) { - return emptySet(); - } - }; + return new NoContractsFromMethod(); } @Override From 27d2334f1c94a223a70c0c98d11058dadce38d94 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Thu, 29 Aug 2024 14:55:43 -0400 Subject: [PATCH 23/23] Update to gradle 8.10 (#203) --- gradle/wrapper/gradle-wrapper.jar | Bin 43504 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197d7c4586c843d1d3e9090525f1898cde..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 3990 zcmV;H4{7l5(*nQL0Kr1kzC=_KMxQY0|W5(lc#i zH*M1^P4B}|{x<+fkObwl)u#`$GxKKV&3pg*-y6R6txw)0qU|Clf9Uds3x{_-**c=7 z&*)~RHPM>Rw#Hi1R({;bX|7?J@w}DMF>dQQU2}9yj%iLjJ*KD6IEB2^n#gK7M~}6R zkH+)bc--JU^pV~7W=3{E*4|ZFpDpBa7;wh4_%;?XM-5ZgZNnVJ=vm!%a2CdQb?oTa z70>8rTb~M$5Tp!Se+4_OKWOB1LF+7gv~$$fGC95ToUM(I>vrd$>9|@h=O?eARj0MH zT4zo(M>`LWoYvE>pXvqG=d96D-4?VySz~=tPVNyD$XMshoTX(1ZLB5OU!I2OI{kb) zS8$B8Qm>wLT6diNnyJZC?yp{Kn67S{TCOt-!OonOK7$K)e-13U9GlnQXPAb&SJ0#3 z+vs~+4Qovv(%i8g$I#FCpCG^C4DdyQw3phJ(f#y*pvNDQCRZ~MvW<}fUs~PL=4??j zmhPyg<*I4RbTz|NHFE-DC7lf2=}-sGkE5e!RM%3ohM7_I^IF=?O{m*uUPH(V?gqyc(Rp?-Qu(3bBIL4Fz(v?=_Sh?LbK{nqZMD>#9D_hNhaV$0ef3@9V90|0u#|PUNTO>$F=qRhg1duaE z0`v~X3G{8RVT@kOa-pU+z8{JWyP6GF*u2e8eKr7a2t1fuqQy)@d|Qn(%YLZ62TWtoX@$nL}9?atE#Yw`rd(>cr0gY;dT9~^oL;u)zgHUvxc2I*b&ZkGM-iq=&(?kyO(3}=P! zRp=rErEyMT5UE9GjPHZ#T<`cnD)jyIL!8P{H@IU#`e8cAG5jMK zVyKw7--dAC;?-qEu*rMr$5@y535qZ6p(R#+fLA_)G~!wnT~~)|s`}&fA(s6xXN`9j zP#Fd3GBa#HeS{5&8p?%DKUyN^X9cYUc6vq}D_3xJ&d@=6j(6BZKPl?!k1?!`f3z&a zR4ZF60Mx7oBxLSxGuzA*Dy5n-d2K=+)6VMZh_0KetK|{e;E{8NJJ!)=_E~1uu=A=r zrn&gh)h*SFhsQJo!f+wKMIE;-EOaMSMB@aXRU(UcnJhZW^B^mgs|M9@5WF@s6B0p& zm#CTz)yiQCgURE{%hjxHcJ6G&>G9i`7MyftL!QQd5 z@RflRs?7)99?X`kHNt>W3l7YqscBpi*R2+fsgABor>KVOu(i(`03aytf2UA!&SC9v z!E}whj#^9~=XHMinFZ;6UOJjo=mmNaWkv~nC=qH9$s-8roGeyaW-E~SzZ3Gg>j zZ8}<320rg4=$`M0nxN!w(PtHUjeeU?MvYgWKZ6kkzABK;vMN0|U;X9abJleJA(xy<}5h5P(5 z{RzAFPvMnX2m0yH0Jn2Uo-p`daE|(O`YQiC#jB8;6bVIUf?SY(k$#C0`d6qT`>Xe0+0}Oj0=F&*D;PVe=Z<=0AGI<6$gYLwa#r` zm449x*fU;_+J>Mz!wa;T-wldoBB%&OEMJgtm#oaI60TSYCy7;+$5?q!zi5K`u66Wq zvg)Fx$s`V3Em{=OEY{3lmh_7|08ykS&U9w!kp@Ctuzqe1JFOGz6%i5}Kmm9>^=gih z?kRxqLA<3@e=}G4R_?phW{4DVr?`tPfyZSN@R=^;P;?!2bh~F1I|fB7P=V=9a6XU5 z<#0f>RS0O&rhc&nTRFOW7&QhevP0#>j0eq<1@D5yAlgMl5n&O9X|Vq}%RX}iNyRFF z7sX&u#6?E~bm~N|z&YikXC=I0E*8Z$v7PtWfjy)$e_Ez25fnR1Q=q1`;U!~U>|&YS zaOS8y!^ORmr2L4ik!IYR8@Dcx8MTC=(b4P6iE5CnrbI~7j7DmM8em$!da&D!6Xu)!vKPdLG z9f#)se|6=5yOCe)N6xDhPI!m81*dNe7u985zi%IVfOfJh69+#ag4ELzGne?o`eA`42K4T)h3S+s)5IT97%O>du- z0U54L8m4}rkRQ?QBfJ%DLssy^+a7Ajw;0&`NOTY4o;0-ivm9 zBz1C%nr_hQ)X)^QM6T1?=yeLkuG9Lf50(eH}`tFye;01&(p?8i+6h};VV-2B~qdxeC#=X z(JLlzy&fHkyi9Ksbcs~&r^%lh^2COldLz^H@X!s~mr9Dr6z!j+4?zkD@Ls7F8(t(f z9`U?P$Lmn*Y{K}aR4N&1N=?xtQ1%jqf1~pJyQ4SgBrEtR`j4lQuh7cqP49Em5cO=I zB(He2`iPN5M=Y0}h(IU$37ANTGx&|b-u1BYA*#dE(L-lptoOpo&th~E)_)y-`6kSH z3vvyVrcBwW^_XYReJ=JYd9OBQrzv;f2AQdZH#$Y{Y+Oa33M70XFI((fs;mB4e`<<{ ze4dv2B0V_?Ytsi>>g%qs*}oDGd5d(RNZ*6?7qNbdp7wP4T72=F&r?Ud#kZr8Ze5tB z_oNb7{G+(o2ajL$!69FW@jjPQ2a5C)m!MKKRirC$_VYIuVQCpf9rIms0GRDf)8AH${I`q^~5rjot@#3$2#zT2f`(N^P7Z;6(@EK$q*Jgif00I6*^ZGV+XB5uw*1R-@23yTw&WKD{s1;HTL;dO)%5i#`dc6b7;5@^{KU%N|A-$zsYw4)7LA{3`Zp>1 z-?K9_IE&z)dayUM)wd8K^29m-l$lFhi$zj0l!u~4;VGR6Y!?MAfBC^?QD53hy6VdD z@eUZIui}~L%#SmajaRq1J|#> z4m=o$vZ*34=ZWK2!QMNEcp2Lbc5N1q!lEDq(bz0b;WI9;e>l=CG9^n#ro`w>_0F$Q zfZ={2QyTkfByC&gy;x!r*NyXXbk=a%~~(#K?< zTke0HuF5{Q+~?@!KDXR|g+43$+;ab`^flS%miup_0OUTm=nIc%d5nLP)i308PIjl_YMF6cpQ__6&$n6it8K- z8PIjl_YMF6cpQ_!r)L8IivW`WdK8mBs6PXdjR2DYdK8nCs73=4j{uVadK8oNjwX|E wpAeHLsTu^*Y>Trk?aBtSQ(D-o$(D8Px^?ZI-PUB? z*1fv!{YdHme3Fc8%cR@*@zc5A_nq&2=R47Hp@$-JF4Fz*;SLw5}K^y>s-s;V!}b2i=5=M- zComP?ju>8Fe@=H@rlwe1l`J*6BTTo`9b$zjQ@HxrAhp0D#u?M~TxGC_!?ccCHCjt| zF*PgJf@kJB`|Ml}cmsyrAjO#Kjr^E5p29w+#>$C`Q|54BoDv$fQ9D?3n32P9LPMIzu?LjNqggOH=1@T{9bMn*u8(GI z!;MLTtFPHal^S>VcJdiYqX0VU|Rn@A}C1xOlxCribxes0~+n2 z6qDaIA2$?e`opx3_KW!rAgbpzU)gFdjAKXh|5w``#F0R|c)Y)Du0_Ihhz^S?k^pk% zP>9|pIDx)xHH^_~+aA=^$M!<8K~Hy(71nJGf6`HnjtS=4X4=Hk^O71oNia2V{HUCC zoN3RSBS?mZCLw;l4W4a+D8qc)XJS`pUJ5X-f^1ytxwr`@si$lAE?{4G|o; zO0l>`rr?;~c;{ZEFJ!!3=7=FdGJ?Q^xfNQh4A?i;IJ4}B+A?4olTK(fN++3CRBP97 ze~lG9h%oegkn)lpW-4F8o2`*WW0mZHwHez`ko@>U1_;EC_6ig|Drn@=DMV9YEUSCa zIf$kHei3(u#zm9I!Jf(4t`Vm1lltJ&lVHy(eIXE8sy9sUpmz%I_gA#8x^Zv8%w?r2 z{GdkX1SkzRIr>prRK@rqn9j2wG|rUvf6PJbbin=yy-TAXrguvzN8jL$hUrIXzr^s5 zVM?H4;eM-QeRFr06@ifV(ocvk?_)~N@1c2ien56UjWXid6W%6ievIh)>dk|rIs##^kY67ib8Kw%#-oVFaXG7$ERyA9(NSJUvWiOA5H(!{uOpcW zg&-?iqPhds%3%tFspHDqqr;A!e@B#iPQjHd=c>N1LoOEGRehVoPOdxJ>b6>yc#o#+ zl8s8!(|NMeqjsy@0x{8^j0d00SqRZjp{Kj)&4UHYGxG+z9b-)72I*&J70?+8e?p_@ z=>-(>l6z5vYlP~<2%DU02b!mA{7mS)NS_eLe=t)sm&+Pmk?asOEKlkPQ)EUvvfC=;4M&*|I!w}(@V_)eUKLA_t^%`o z0PM9LV|UKTLnk|?M3u!|f2S0?UqZsEIH9*NJS-8lzu;A6-rr-ot=dg9SASoluZUkFH$7X; zP=?kYX!K?JL-b~<#7wU;b;eS)O;@?h%sPPk{4xEBxb{!sm0AY|f9cNvx6>$3F!*0c z75H=dy8JvTyO8}g1w{$9T$p~5en}AeSLoCF>_RT9YPMpChUjl310o*$QocjbH& zbnwg#gssR#jDVN{uEi3n(PZ%PFZ|6J2 z5_rBf0-u>e4sFe0*Km49ATi7>Kn0f9!uc|rRMR1Dtt6m1LW8^>qFlo}h$@br=Rmpi z;mI&>OF64Be{dVeHI8utrh)v^wsZ0jii%x8UgZ8TC%K~@I(4E};GFW&(;WVov}3%H zH;IhRkfD^(vt^DjZz(MyHLZxv8}qzPc(%itBkBwf_fC~sDBgh<3XAv5cxxfF3<2U! z03Xe&z`is!JDHbe;mNmfkH+_LFE*I2^mdL@7(@9DfAcP6O04V-ko;Rpgp<%Cj5r8Z zd0`sXoIjV$j)--;jA6Zy^D5&5v$o^>e%>Q?9GLm{i~p^lAn!%ZtF$I~>39XVZxk0b zROh^Bk9cE0AJBLozZIEmy7xG(yHWGztvfnr0(2ro1%>zsGMS^EMu+S$r=_;9 zWwZkgf7Q7`H9sLf2Go^Xy6&h~a&%s2_T@_Csf19MntF$aVFiFkvE3_hUg(B@&Xw@YJ zpL$wNYf78=0c@!QU6_a$>CPiXT7QAGDM}7Z(0z#_ZA=fmLUj{2z7@Ypo71UDy8GHr z-&TLKf6a5WCf@Adle3VglBt4>Z>;xF}}-S~B7<(%B;Y z0QR55{z-buw>8ilNM3u6I+D$S%?)(p>=eBx-HpvZj{7c*_?K=d()*7q?93us}1dq%FAFYLsW8ZTQ_XZLh`P2*6(NgS}qGcfGXVWpwsp#Rs}IuKbk*`2}&) zI^Vsk6S&Q4@oYS?dJ`NwMVBs6f57+RxdqVub#PvMu?$=^OJy5xEl0<5SLsSRy%%a0 zi}Y#1-F3m;Ieh#Y12UgW?-R)|eX>ZuF-2cc!1>~NS|XSF-6In>zBoZg+ml!6%fk7U zw0LHcz8VQk(jOJ+Yu)|^|15ufl$KQd_1eUZZzj`aC%umU6F1&D5XVWce_wAe(qCSZ zpX-QF4e{EmEVN9~6%bR5U*UT{eMHfcUo`jw*u?4r2s_$`}U{?NjvEm(u&<>B|%mq$Q3weshxk z76<``8vh{+nX`@9CB6IE&z)I%IFjR^LH{s1p|eppv=x za(g_jLU|xjWMAn-V7th$f({|LG8zzIE0g?cyW;%Dmtv%C+0@xVxPE^ zyZzi9P%JAD6ynwHptuzP`Kox7*9h7XSMonCalv;Md0i9Vb-c*!f0ubfk?&T&T}AHh z4m8Bz{JllKcdNg?D^%a5MFQ;#1z|*}H^qHLzW)L}wp?2tY7RejtSh8<;Zw)QGJYUm z|MbTxyj*McKlStlT9I5XlSWtQGN&-LTr2XyNU+`490rg?LYLMRnz-@oKqT1hpCGqP zyRXt4=_Woj$%n5ee<3zhLF>5>`?m9a#xQH+Jk_+|RM8Vi;2*XbK- zEL6sCpaGPzP>k8f4Kh|##_imt#zJMB;ir|JrMPGW`rityK1vHXMLy18%qmMQAm4WZ zP)i30KR&5vs15)C+8dM66&$k~i|ZT;KR&5vs15)C+8dJ(sAmGPijyIz6_bsqKLSFH zlOd=TljEpH0>h4zA*dCTK&emy#FCRCs1=i^sZ9bFmXjf<6_X39E(XY)00000#N437 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e..9355b415 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME