package checkers.util.test;
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.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.tools.JavaFileObject;
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 checkers.source.*;
import checkers.types.AnnotatedTypeFactory;
/**
* A specialized checker for testing purposes. It compares an expression's
* annotated type to an exprected type.
*
* 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 preceeding the
* expression.
*
* The comments are of two forms:
* <ul>
* <li>{@code /// <expected type>}:
* to specify the type of the expression in the expression statement</li>
* <li>{@code /// <subtree> -:- <expected type>}:
* to specify the type of the given subexpression within the line.</li>
* </ul>
*
* The specified types are allowed to use simple names (e.g.,
* <i>List<String></i>), instead of fully qualified names (e.g.,
* <i>java.util.List<java.lang.String></i>).
*
* 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 -Afactory argument (e.g.
* -Afactory=checkers.nullness.NullnessAnnotatedTypeFactory). The factory needs
* to have a constractor of the form
* {@code <init>(ProcessingEnvironment, CompilationUnitTree)}.
*/
/*
* The code here is one of the most ugliest I have ever written. I should revise
* it in the future. - Mahmood
*/
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedOptions( { "checker" } )
public class FactoryTestChecker extends SourceChecker {
SourceChecker checker;
@Override
public synchronized void init(ProcessingEnvironment p) {
super.init(p);
// Find factory constructor
String checkerClassName = env.getOptions().get("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 RuntimeException("Couldn't load " +
checkerClassName + " class.");
}
}
@Override
public AnnotatedTypeFactory createFactory(CompilationUnitTree root) {
return checker.createFactory(root);
}
@Override
public Properties getMessages() {
// We don't have any properties
// '\n' doesn't need to be replaced here
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 SourceVisitor<Void, Void> createSourceVisitor(CompilationUnitTree root) {
return new ToStringVisitor(this, root);
}
/**
* 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<TreeSpec, String>();
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();
}
} 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 unecessary
* 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;
}
public int hashCode() {
return (int) (31 + 3 * treeString.hashCode() + 7 * lineNumber);
}
public boolean equals(Object o) {
if (o instanceof TreeSpec) {
TreeSpec other = (TreeSpec) o;
return treeString.equals(other.treeString)
&& lineNumber == other.lineNumber;
}
return false;
}
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 SourceVisitor<Void, Void> {
Map<TreeSpec, String> expected;
public ToStringVisitor(SourceChecker checker, CompilationUnitTree root) {
super(checker, root);
this.expected = buildExpected(root);
}
@Override
public Void scan(Tree tree, Void p) {
if (tree instanceof ExpressionTree) {
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))
FactoryTestChecker.this.report(Result.failure("type.unexpected",
tree.toString(), actualType, expectedType), tree);
}
}
return super.scan(tree, p);
}
}
}