blob: d449ddab772152e8b347f6f229b811867fcc9bd9 [file] [log] [blame]
package org.checkerframework.framework.testchecker.util;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.tree.JCTree;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.tools.JavaFileObject;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.basetype.BaseTypeVisitor;
import org.checkerframework.framework.source.SourceChecker;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TreeUtils;
/**
* A specialized checker for testing purposes. It compares an expression's annotated type to an
* expected type.
*
* <p>The expected type is written in a stylized comment (starting with "///") in the same Java
* source file. The comment appears either on the same line as the expression, or else by itself on
* the line preceding the expression.
*
* <p>The comments are of two forms:
*
* <ul>
* <li>{@code /// <expected type>}: to specify the type of the expression in the expression
* statement
* <li>{@code /// <subtree> -:- <expected type>}: to specify the type of the given subexpression
* within the line.
* </ul>
*
* The specified types are allowed to use simple names (e.g., {@code List<String>}), instead of
* fully qualified names (e.g., {@code java.util.List<java.lang.String>}).
*
* <p>Example:
*
* <pre>
* void test() {
* // Comments in the same line
* Collections.<@NonNull String>emptyList(); /// List<@NonNull String>
* List<@NonNull String> l = Collections.emptyList(); /// Collections.emptyList() -:- List<@NonNull String>
*
* // Comments in the previous lines
* /// List<@NonNull String>
* Collections.<@NonNull String>emptyList();
*
* /// Collections.emptyList() -:- List<@NonNull String>
* List<@NonNull String> l = Collections.emptyList();
* }
* </pre>
*
* The fully qualified name of the custom <i>AnnotatedTypeFactory</i> is specified through an {@code
* -Afactory} command-line argument (e.g. {@code
* -Afactory=checkers.nullness.NullnessAnnotatedTypeFactory}). The factory needs to have a
* constructor of the form {@code <init>(ProcessingEnvironment, CompilationUnitTree)}.
*/
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions({"checker"})
public class FactoryTestChecker extends BaseTypeChecker {
SourceChecker checker;
@Override
public void initChecker() {
super.initChecker();
// Find factory constructor
String checkerClassName = getOption("checker");
try {
if (checkerClassName != null) {
Class<?> checkerClass = Class.forName(checkerClassName);
Constructor<?> constructor = checkerClass.getConstructor();
Object o = constructor.newInstance();
if (o instanceof SourceChecker) {
checker = (SourceChecker) o;
}
}
} catch (Exception e) {
throw new BugInCF("Couldn't load " + checkerClassName + " class.");
}
}
/*
@Override
public AnnotatedTypeFactory createTypeFactory() {
return checker.createTypeFactory();
}*/
@Override
public Properties getMessagesProperties() {
// We don't have any properties
Properties prop = new Properties();
prop.setProperty(
"type.unexpected",
"unexpected type for the given tree%n"
+ "Tree : %s%n"
+ "Found : %s%n"
+ "Expected : %s%n");
return prop;
}
@Override
protected BaseTypeVisitor<?> createSourceVisitor() {
return new ToStringVisitor(this);
}
/** Builds the expected type for the trees from the source file of the tree compilation unit. */
// This method is extremely ugly
private Map<TreeSpec, String> buildExpected(CompilationUnitTree tree) {
Map<TreeSpec, String> expected = new HashMap<>();
try {
JavaFileObject o = tree.getSourceFile();
File sourceFile = new File(o.toUri());
LineNumberReader reader = new LineNumberReader(new FileReader(sourceFile));
String line = reader.readLine();
Pattern prevsubtreePattern = Pattern.compile("\\s*///(.*)-:-(.*)");
Pattern prevfulltreePattern = Pattern.compile("\\s*///(.*)");
Pattern subtreePattern = Pattern.compile("(.*)///(.*)-:-(.*)");
Pattern fulltreePattern = Pattern.compile("(.*)///(.*)");
while (line != null) {
Matcher prevsubtreeMatcher = prevsubtreePattern.matcher(line);
Matcher prevfulltreeMatcher = prevfulltreePattern.matcher(line);
Matcher subtreeMatcher = subtreePattern.matcher(line);
Matcher fulltreeMatcher = fulltreePattern.matcher(line);
if (prevsubtreeMatcher.matches()) {
String treeString = prevsubtreeMatcher.group(1).trim();
if (treeString.endsWith(";")) {
treeString = treeString.substring(0, treeString.length() - 1);
}
TreeSpec treeSpec = new TreeSpec(treeString.trim(), reader.getLineNumber() + 1);
expected.put(treeSpec, canonizeTypeString(prevsubtreeMatcher.group(2)));
} else if (prevfulltreeMatcher.matches()) {
String treeString = reader.readLine().trim();
if (treeString.endsWith(";")) {
treeString = treeString.substring(0, treeString.length() - 1);
}
TreeSpec treeSpec = new TreeSpec(treeString.trim(), reader.getLineNumber());
expected.put(treeSpec, canonizeTypeString(prevfulltreeMatcher.group(1)));
} else if (subtreeMatcher.matches()) {
String treeString = subtreeMatcher.group(2).trim();
if (treeString.endsWith(";")) {
treeString = treeString.substring(0, treeString.length() - 1);
}
TreeSpec treeSpec = new TreeSpec(treeString.trim(), reader.getLineNumber());
expected.put(treeSpec, canonizeTypeString(subtreeMatcher.group(3)));
} else if (fulltreeMatcher.matches()) {
String treeString = fulltreeMatcher.group(1).trim();
if (treeString.endsWith(";")) {
treeString = treeString.substring(0, treeString.length() - 1);
}
TreeSpec treeSpec = new TreeSpec(treeString.trim(), reader.getLineNumber());
expected.put(treeSpec, canonizeTypeString(fulltreeMatcher.group(2)));
}
line = reader.readLine();
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
return expected;
}
/** A method to canonize the tree representation. */
private static String canonizeTreeString(String str) {
String canon = str.trim();
Pattern pattern = Pattern.compile("(@\\S+)\\(\\)");
Matcher matcher = pattern.matcher(canon);
while (matcher.find()) {
canon = matcher.replaceFirst(matcher.group(1));
matcher.reset(canon);
}
return canon.trim();
}
/**
* A method to canonize type string representation. It removes any unnecessary white spaces and
* finds the type simple name instead of the fully qualified name.
*
* @param str the type string representation
* @return a canonical representation of the type
*/
private static String canonizeTypeString(String str) {
String canon = str.trim();
canon = canon.replaceAll("\\s+", " ");
// Remove spaces between [ ]
canon = canon.replaceAll("\\[\\s+", "[");
canon = canon.replaceAll("\\s+\\]", "]");
// Remove spaces between < >
canon = canon.replaceAll("<\\s+", "<");
canon = canon.replaceAll("\\s+>", ">");
// Take simply names!
canon = canon.replaceAll("[^\\<]*\\.(?=\\w)", "");
return canon;
}
/**
* A data structure that encapsulate a string and the line number that string appears in the
* buffer
*/
private static class TreeSpec {
public final String treeString;
public final long lineNumber;
public TreeSpec(String treeString, long lineNumber) {
this.treeString = canonizeTreeString(treeString);
this.lineNumber = lineNumber;
}
@Override
public int hashCode() {
return Objects.hash(treeString, lineNumber);
}
@Override
public boolean equals(Object o) {
if (o instanceof TreeSpec) {
TreeSpec other = (TreeSpec) o;
return treeString.equals(other.treeString) && lineNumber == other.lineNumber;
}
return false;
}
@Override
public String toString() {
return lineNumber + ":" + treeString;
}
}
/**
* A specialized visitor that compares the actual and expected types for the specified trees and
* report an error if they differ
*/
private class ToStringVisitor extends BaseTypeVisitor<GenericAnnotatedTypeFactory<?, ?, ?, ?>> {
Map<TreeSpec, String> expected;
public ToStringVisitor(BaseTypeChecker checker) {
super(checker);
this.expected = buildExpected(root);
}
@Override
public Void scan(Tree tree, Void p) {
if (TreeUtils.isExpressionTree(tree)) {
ExpressionTree expTree = (ExpressionTree) tree;
TreeSpec treeSpec =
new TreeSpec(
expTree.toString().trim(), root.getLineMap().getLineNumber(((JCTree) expTree).pos));
if (expected.containsKey(treeSpec)) {
String actualType = canonizeTypeString(atypeFactory.getAnnotatedType(expTree).toString());
String expectedType = expected.get(treeSpec);
if (!actualType.equals(expectedType)) {
// The key is added above using a setProperty call, which is not supported
// by the CompilerMessagesChecker.
@SuppressWarnings("compilermessages")
@CompilerMessageKey String key = "type.unexpected";
FactoryTestChecker.this.reportError(
tree, key, tree.toString(), actualType, expectedType);
}
}
}
return super.scan(tree, p);
}
}
}