Skip to content

Commit

Permalink
Fix crash in ajava-based WPI related to captures (typetools#5335)
Browse files Browse the repository at this point in the history
  • Loading branch information
kelloggm authored and wmdietl committed Oct 10, 2022
1 parent 69904b9 commit 8bc9476
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 20 deletions.
46 changes: 46 additions & 0 deletions checker/build.gradle
Expand Up @@ -726,6 +726,52 @@ task ainferIndexAjavaTest(dependsOn: 'shadowJar', group: 'Verification') {
outputs.upToDateWhen { false }
}

task ainferNullnessGenerateAjava(type: Test) {
description 'Internal task. Users should run ainferNullnessAjavaTest instead. This type-checks the ainfer-nullness files with -Ainfer=ajava to generate ajava files.'

dependsOn(compileTestJava)
doFirst {
delete("tests/ainfer-nullness/annotated")
delete("${buildDir}/ainfer-nullness/")
}
outputs.upToDateWhen { false }
include '**/AinferNullnessAjavaTest.class'
testLogging {
// Always run the tests
outputs.upToDateWhen { false }

// Show the found unexpected diagnostics and the expected diagnostics not found.
exceptionFormat "full"
events "passed", "skipped", "failed"
}

doLast {
copyNonannotatedToAnnotatedDirectory("ainfer-nullness")
}
}

task ainferNullnessValidateAjava(type: Test) {
description 'Internal task. Users should run ainferNullnessAjavaTest instead. This re-type-checks the ainfer-nullness files using the ajava files generated by ainferNullnessGenerateAjava'

dependsOn(ainferNullnessGenerateAjava)
outputs.upToDateWhen { false }
include '**/AinferNullnessAjavaValidationTest.class'
testLogging {
// Always run the tests
outputs.upToDateWhen { false }

// Show the found unexpected diagnostics and the expected diagnostics not found.
exceptionFormat "full"
events "passed", "skipped", "failed"
}
}

task ainferNullnessAjavaTest(dependsOn: 'shadowJar', group: 'Verification') {
description 'Run tests for whole-program inference using ajava files and the Nullness Checker'
dependsOn(ainferNullnessValidateAjava)
outputs.upToDateWhen { false }
}

task ainferNullnessGenerateJaifs(type: Test) {
description 'Internal task. Users should run ainferNullnessJaifTest instead. This type-checks the ainfer-nullness files with -Ainfer=jaifs to generate .jaif files'

Expand Down
@@ -0,0 +1,40 @@
package org.checkerframework.checker.test.junit.ainferrunners;

import java.io.File;
import java.util.List;
import org.checkerframework.checker.nullness.NullnessChecker;
import org.checkerframework.framework.test.AinferGeneratePerDirectoryTest;
import org.junit.experimental.categories.Category;
import org.junit.runners.Parameterized.Parameters;

/**
* Tests whole-program inference with the aid of ajava files. This test is the first pass on the
* test data, which generates the ajava files. This specific test suite is designed to elicit
* problems with ajava parsing that only occur when the Nullness Checker is run.
*
* <p>IMPORTANT: The errors captured in the tests located in tests/ainfer-index/ are not relevant.
* The meaning of this test class is to test if the generated ajava files are similar to the
* expected ones. The errors on .java files must be ignored.
*/
@Category(AinferNullnessAjavaTest.class)
public class AinferNullnessAjavaTest extends AinferGeneratePerDirectoryTest {

/**
* @param testFiles the files containing test code, which will be type-checked
*/
public AinferNullnessAjavaTest(List<File> testFiles) {
super(
testFiles,
NullnessChecker.class,
"ainfer-nullness/non-annotated",
"-Anomsgtext",
"-Ainfer=ajava",
// "-Aajava=tests/ainfer-nullness/input-annotation-files/",
"-Awarns");
}

@Parameters
public static String[] getTestDirs() {
return new String[] {"ainfer-nullness/non-annotated"};
}
}
@@ -0,0 +1,37 @@
package org.checkerframework.checker.test.junit.ainferrunners;

import java.io.File;
import java.util.List;
import org.checkerframework.checker.nullness.NullnessChecker;
import org.checkerframework.framework.test.AinferValidatePerDirectoryTest;
import org.junit.experimental.categories.Category;
import org.junit.runners.Parameterized.Parameters;

/**
* Tests whole-program type inference with ajava files. This test is the second pass, which ensures
* that with the ajava files in place, the errors that those annotations remove are no longer
* issued.
*/
@Category(AinferNullnessAjavaTest.class)
public class AinferNullnessAjavaValidationTest extends AinferValidatePerDirectoryTest {

/**
* @param testFiles the files containing test code, which will be type-checked
*/
public AinferNullnessAjavaValidationTest(List<File> testFiles) {
super(
testFiles,
NullnessChecker.class,
"nullness",
"ainfer-nullness/annotated",
AinferNullnessAjavaTest.class,
"-Anomsgtext",
ajavaArgFromFiles(testFiles, "nullness"),
"-Awarns");
}

@Parameters
public static String[] getTestDirs() {
return new String[] {"ainfer-nullness/annotated/"};
}
}
57 changes: 57 additions & 0 deletions checker/tests/ainfer-index/non-annotated/Dataset6Crash.java
@@ -0,0 +1,57 @@
// Test case for a WPI crash caused by mismatches between captured type variables
// and the declared type of a field: in particular, the issue is that base.next()
// the next field actually have slightly different types: base.next()'s type is
// a capture that extends T.

import java.util.Iterator;

public class Dataset6Crash {

public static <T> Iterator<T> limit(final Iterator<? extends T> base, final CountingPredicate<?
super T> filter) {
return new Iterator<T>() {

private T next;

private boolean end;

private int index = 0;

public boolean hasNext() {
return true;
}

public T next() {
fetch();
T r = next;
next = null;
return r;
}

private void fetch() {
if (next == null && !end) {
if (base.hasNext()) {
next = base.next();
if (!filter.apply(index++, next)) {
next = null;
end = true;
}
} else {
end = true;
}
}
}

public void remove() {
throw new UnsupportedOperationException();
}
};
}


private static class CountingPredicate<T> {
public boolean apply(int i, T next) {
return false;
}
}
}
@@ -0,0 +1,6 @@
public class TypeVarAssignment<T, S extends T> {
T t;
void foo(S s) {
t = s;
}
}
32 changes: 32 additions & 0 deletions checker/tests/ainfer-nullness/non-annotated/NullTypeVarTest.java
@@ -0,0 +1,32 @@
// A test that the interaction between type variables and null types
// is handled correctly in WPI, based on the indentString variable
// in
// https://github.com/plume-lib/bcel-util/blob/master/src/main/java/org/plumelib/bcelutil/SimpleLog.java

import java.util.List;
import java.util.ArrayList;

public class NullTypeVarTest {

// :: warning: assignment
private String indentString = null;

private List<String> indentStrings;

private final String INDENT_STR_ONE_LEVEL = " ";

public NullTypeVarTest() {
indentStrings = new ArrayList<String>();
indentStrings.add("");
}

private String getIndentString(int indentLevel) {
if (indentString == null) {
for (int i = indentStrings.size(); i <= indentLevel; i++) {
indentStrings.add(indentStrings.get(i - 1) + INDENT_STR_ONE_LEVEL);
}
indentString = indentStrings.get(indentLevel);
}
return indentString;
}
}
Expand Up @@ -793,26 +793,6 @@ protected void updateAnnotationSet(
return;
}

// If the rhsATM and the lhsATM have different kinds (which can happen e.g. when
// an array type is substituted for a type parameter), do not attempt to update
// the inferred type, because this method is written with the assumption
// that rhsATM and lhsATM are the same kind.
if (rhsATM.getKind() != lhsATM.getKind()) {
// The one difference in kinds situation that this method can account for is the RHS being
// a literal null expression.
if (!(rhsATM instanceof AnnotatedNullType)) {
if (showWpiFailedInferences) {
printFailedInferenceDebugMessage(
"type structure mismatch, so cannot transfer inferred type"
+ "to declared type.\nDeclared type kind: "
+ rhsATM.getKind()
+ "\nInferred type kind: "
+ lhsATM.getKind());
}
return;
}
}

AnnotatedTypeMirror atmFromStorage =
storage.atmFromStorageLocation(rhsATM.getUnderlyingType(), annotationsToUpdate);
updateAtmWithLub(rhsATM, atmFromStorage);
Expand Down Expand Up @@ -858,6 +838,20 @@ private void printFailedInferenceDebugMessage(String reason) {
*/
private void updateAtmWithLub(AnnotatedTypeMirror sourceCodeATM, AnnotatedTypeMirror ajavaATM) {

if (sourceCodeATM.getKind() != ajavaATM.getKind()) {
// Ignore null types: passing them to asSuper causes a crash, as they cannot be
// substituted for type variables. If sourceCodeATM is a null type, only the primary
// annotation will be considered anyway, so there is no danger of recursing into
// typevar bounds.
if (sourceCodeATM.getKind() != TypeKind.NULL) {
// This can happen e.g. when recursing into the bounds of a type variable:
// the bound on sourceCodeATM might be a declared type (such as T), while
// the ajavaATM might be a typevar (such as S extends T), or vice-versa. In
// that case, use asSuper to make the two ATMs fully-compatible.
sourceCodeATM = AnnotatedTypes.asSuper(this.atypeFactory, sourceCodeATM, ajavaATM);
}
}

switch (sourceCodeATM.getKind()) {
case TYPEVAR:
updateAtmWithLub(
Expand Down

0 comments on commit 8bc9476

Please sign in to comment.