/* * Copyright 2011 Google Inc. All Rights Reserved. * * 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.errorprone; import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.errorprone.BugPattern.SeverityLevel; import com.google.errorprone.dataflow.nullnesspropagation.NullnessAnalysis; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ErrorProneToken; import com.google.errorprone.util.ErrorProneTokens; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.CompletionFailure; import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.ArrayType; import com.sun.tools.javac.code.Type.ClassType; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.main.JavaCompiler; import com.sun.tools.javac.parser.Tokens.Token; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.Name; import com.sun.tools.javac.util.Names; import com.sun.tools.javac.util.Options; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; /** * @author alexeagle@google.com (Alex Eagle) */ public class VisitorState { private final DescriptionListener descriptionListener; public final Context context; private final TreePath path; private final Map<String, SeverityLevel> severityMap; private final ErrorProneOptions errorProneOptions; private final LoadingCache<String, Optional<Type>> typeCache; // The default no-op implementation of DescriptionListener. We use this instead of null so callers // of getDescriptionListener() don't have to do null-checking. private static final DescriptionListener NULL_LISTENER = new DescriptionListener() { @Override public void onDescribed(Description description) {} }; public VisitorState(Context context) { this(context, NULL_LISTENER); } public VisitorState(Context context, DescriptionListener listener) { this( context, listener, Collections.<String, SeverityLevel>emptyMap(), ErrorProneOptions.empty()); } public VisitorState( Context context, DescriptionListener listener, Map<String, SeverityLevel> severityMap, ErrorProneOptions errorProneOptions) { this(context, null, listener, severityMap, errorProneOptions, null); } private VisitorState( Context context, TreePath path, DescriptionListener descriptionListener, Map<String, SeverityLevel> severityMap, ErrorProneOptions errorProneOptions, LoadingCache<String, Optional<Type>> typeCache) { this.context = context; this.path = path; this.descriptionListener = descriptionListener; this.severityMap = severityMap; this.errorProneOptions = errorProneOptions; if (typeCache != null) { this.typeCache = typeCache; } else { this.typeCache = CacheBuilder.newBuilder() .concurrencyLevel(1) // resolving symbols in javac is not is not thread-safe .build( new CacheLoader<String, Optional<Type>>() { @Override public Optional<Type> load(String key) throws Exception { return Optional.fromNullable(getTypeFromStringInternal(key)); } }); } } public VisitorState withPath(TreePath path) { return new VisitorState( context, path, descriptionListener, severityMap, errorProneOptions, typeCache); } public TreePath getPath() { return path; } public TreeMaker getTreeMaker() { return TreeMaker.instance(context); } public Types getTypes() { return Types.instance(context); } public Symtab getSymtab() { return Symtab.instance(context); } public NullnessAnalysis getNullnessAnalysis() { return NullnessAnalysis.instance(context); } public ErrorProneOptions errorProneOptions() { return errorProneOptions; } public void reportMatch(Description description) { // TODO(cushon): creating Descriptions with the default severity and updating them here isn't // ideal (we could forget to do the update), so consider removing severity from Description. // Instead, there could be another method on the listener that took a description and a // (separate) SeverityLevel. Adding the method to the interface would require updating the // existing implementations, though. Wait for default methods? SeverityLevel override = severityMap.get(description.checkName); if (override != null) { description = description.applySeverityOverride(override); } descriptionListener.onDescribed(description); } public Name getName(String nameStr) { return Names.instance(context).fromString(nameStr); } /** * Given the binary name of a class, returns the {@link Type}. * * <p>If this method returns null, the compiler doesn't have access to this type, which means that * if you are comparing other types to this for equality or the subtype relation, your result * would always be false even if it could create the type. Thus it might be best to bail out early * in your matcher if this method returns null on your type of interest. * * @param typeStr the JLS 13.1 binary name of the class, e.g. {@code "java.util.Map$Entry"} * @return the {@link Type}, or null if it cannot be found */ public Type getTypeFromString(String typeStr) { try { return typeCache.get(typeStr).orNull(); } catch (ExecutionException e) { return null; } } private Type getTypeFromStringInternal(String typeStr) { validateTypeStr(typeStr); if (isPrimitiveType(typeStr)) { return getPrimitiveType(typeStr); } if (isVoidType(typeStr)) { return getVoidType(); } Name typeName = getName(typeStr); try { ClassSymbol typeSymbol = getSymtab().getClass(getSymtab().java_base, typeName); if (typeSymbol == null) { JavaCompiler compiler = JavaCompiler.instance(context); Symbol sym = compiler.resolveIdent(getSymtab().java_base, typeStr); if (!(sym instanceof ClassSymbol)) { return null; } typeSymbol = (ClassSymbol) sym; } Type type = typeSymbol.asType(); // Throws CompletionFailure if the source/class file for this type is not available. // This is hacky but the best way I can think of to handle this case. type.complete(); if (type.isErroneous()) { return null; } return type; } catch (CompletionFailure failure) { return null; } } /** * @param symStr the string representation of a symbol * @return the Symbol object, or null if it cannot be found */ public Symbol getSymbolFromString(String symStr) { try { Name symName = getName(symStr); Symbol result = getSymtab().getClass(getSymtab().java_base, symName); if (result != null) { // Force a completion failure if the type is not available. result.complete(); } return result; } catch (CompletionFailure failure) { return null; } } /** * Build an instance of a Type. */ public Type getType(Type baseType, boolean isArray, List<Type> typeParams) { boolean isGeneric = typeParams != null && !typeParams.isEmpty(); if (!isArray && !isGeneric) { // Simple type. return baseType; } else if (isArray && !isGeneric) { // Array type, not generic. ClassSymbol arraySymbol = getSymtab().arrayClass; return new ArrayType(baseType, arraySymbol); } else if (!isArray && isGeneric) { // Generic type, not array. com.sun.tools.javac.util.List<Type> typeParamsCopy = com.sun.tools.javac.util.List.from(typeParams); return new ClassType(Type.noType, typeParamsCopy, baseType.tsym); } else { throw new IllegalArgumentException("Unsupported arguments to getType"); } } /** Build an Array Type from another Type */ public Type arrayTypeForType(Type baseType) { return new ArrayType(baseType, getSymtab().arrayClass); } /** * Returns the {@link TreePath} to the nearest tree node of one of the given types. To instead * retrieve the element directly, use {@link #findEnclosing(Class...)}. * * @return the path, or {@code null} if there is no match */ @SafeVarargs public final TreePath findPathToEnclosing(Class<? extends Tree>... classes) { TreePath enclosingPath = getPath(); while (enclosingPath != null) { for (Class<? extends Tree> clazz : classes) { if (clazz.isInstance(enclosingPath.getLeaf())) { return enclosingPath; } } enclosingPath = enclosingPath.getParentPath(); } return null; } /** * Find the first enclosing tree node of one of the given types. * * @return the node, or {@code null} if there is no match */ @SuppressWarnings("unchecked") // findPathToEnclosing guarantees that the type is from |classes| @SafeVarargs public final <T extends Tree> T findEnclosing(Class<? extends T>... classes) { TreePath pathToEnclosing = findPathToEnclosing(classes); return (pathToEnclosing == null) ? null : (T) pathToEnclosing.getLeaf(); } /** * Gets the current source file. * * @return the source file as a sequence of characters, or null if it is not available */ public CharSequence getSourceCode() { try { return getPath().getCompilationUnit().getSourceFile().getCharContent(false); } catch (IOException e) { return null; } } /** * Gets the original source code that represents the given node. * * <p>Note that this may be different from what is returned by calling .toString() on the node. * This returns exactly what is in the source code, whereas .toString() pretty-prints the node * from its AST representation. * * @return the source code that represents the node. */ public String getSourceForNode(Tree tree) { JCTree node = (JCTree) tree; int start = node.getStartPosition(); int end = getEndPosition(node); if (end < 0) { return null; } return getSourceCode().subSequence(start, end).toString(); } /** * Returns the list of {@link Token}s for the given {@link JCTree}. * * <p>This is moderately expensive (the source of the node has to be re-lexed), so it should only * be used if a fix is already going to be emitted. */ public java.util.List<ErrorProneToken> getTokensForNode(Tree tree) { return ErrorProneTokens.getTokens(getSourceForNode(tree), context); } /** Returns the end position of the node, or -1 if it is not available. */ public int getEndPosition(Tree node) { JCCompilationUnit compilationUnit = (JCCompilationUnit) getPath().getCompilationUnit(); if (compilationUnit.endPositions == null) { return -1; } return ((JCTree) node).getEndPosition(compilationUnit.endPositions); } /** * Validates a type string, ensuring it is not generic and not an array type. */ private static void validateTypeStr(String typeStr) { if (typeStr.contains("[") || typeStr.contains("]")) { throw new IllegalArgumentException("Cannot convert array types, please build them using " + "getType()"); } if (typeStr.contains("<") || typeStr.contains(">")) { throw new IllegalArgumentException( "Cannot convert generic types, please build them using getType()"); } } /** * Given a string that represents a primitive type (e.g., "int"), return the corresponding Type. */ private Type getPrimitiveType(String typeStr) { if (typeStr.equals("byte")) { return getSymtab().byteType; } else if (typeStr.equals("short")) { return getSymtab().shortType; } else if (typeStr.equals("int")) { return getSymtab().intType; } else if (typeStr.equals("long")) { return getSymtab().longType; } else if (typeStr.equals("float")) { return getSymtab().floatType; } else if (typeStr.equals("double")) { return getSymtab().doubleType; } else if (typeStr.equals("boolean")) { return getSymtab().booleanType; } else if (typeStr.equals("char")) { return getSymtab().charType; } else { throw new IllegalStateException("Type string " + typeStr + " expected to be primitive"); } } private Type getVoidType() { return getSymtab().voidType; } private static boolean isPrimitiveType(String typeStr) { return typeStr.equals("byte") || typeStr.equals("short") || typeStr.equals("int") || typeStr.equals("long") || typeStr.equals("float") || typeStr.equals("double") || typeStr.equals("boolean") || typeStr.equals("char"); } private static boolean isVoidType(String typeStr) { return typeStr.equals("void"); } /** Returns true if the compilation is targeting Android. */ public boolean isAndroidCompatible() { return Options.instance(context).getBoolean("androidCompatible"); } }