/*
* Copyright 2016 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.bugpatterns;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.UnionTypeTree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeScanner;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/** @author cushon@google.com (Liam Miller-Cushon) */
@BugPattern(
name = "ClassNewInstance",
category = JDK,
summary =
"Class.newInstance() bypasses exception checking; prefer"
+ " getDeclaredConstructor().newInstance()",
severity = WARNING
)
public class ClassNewInstance extends BugChecker implements MethodInvocationTreeMatcher {
private static final Matcher<ExpressionTree> NEW_INSTANCE =
instanceMethod().onExactClass(Class.class.getName()).named("newInstance");
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!NEW_INSTANCE.matches(tree, state)) {
return Description.NO_MATCH;
}
SuggestedFix.Builder fix = SuggestedFix.builder();
fix.replace(
state.getEndPosition(ASTHelpers.getReceiver(tree)),
state.getEndPosition(tree),
String.format(".getDeclaredConstructor().newInstance()"));
boolean fixedExceptions = fixExceptions(state, fix);
if (!fixedExceptions) {
fixThrows(state, fix);
}
return describeMatch(tree, fix.build());
}
// if the match occurrs inside the body of a try statement with existing catch clauses
// update or add a catch block to handle the new exceptions
private boolean fixExceptions(final VisitorState state, SuggestedFix.Builder fix) {
TryTree tryTree = null;
OUTER:
for (TreePath path = state.getPath(); path != null; path = path.getParentPath()) {
if (path.getLeaf() instanceof CatchTree) {
// don't add more catch blocks if newInstance() was called in a catch block
return false;
} else if (path.getLeaf() instanceof TryTree
&& !((TryTree) path.getLeaf()).getCatches().isEmpty()) {
tryTree = (TryTree) path.getLeaf();
break;
}
}
if (tryTree == null) {
return false;
}
ImmutableMap.Builder<Type, CatchTree> catches = ImmutableMap.builder();
for (CatchTree c : tryTree.getCatches()) {
catches.put(ASTHelpers.getType(c.getParameter().getType()), c);
}
UnhandledResult<CatchTree> result = unhandled(catches.build(), state);
if (result.unhandled.isEmpty()) {
// no fix needed
return true;
}
{
// if there's an existing multi-catch at the end that handles reflective exceptions,
// replace all of them with ROE and leave any non-reflective exceptions.
// earlier catch blocks are left unchanged.
CatchTree last = Iterables.getLast(tryTree.getCatches());
Tree lastType = last.getParameter().getType();
if (lastType.getKind() == Tree.Kind.UNION_TYPE) {
Type roe = state.getTypeFromString(ReflectiveOperationException.class.getName());
Set<String> exceptions = new LinkedHashSet<>();
boolean foundReflective = false;
for (Tree alternate : ((UnionTypeTree) lastType).getTypeAlternatives()) {
if (ASTHelpers.isSubtype(ASTHelpers.getType(alternate), roe, state)) {
foundReflective = true;
exceptions.add("ReflectiveOperationException");
} else {
exceptions.add(state.getSourceForNode(alternate));
}
}
if (foundReflective) {
fix.replace(lastType, Joiner.on(" | ").join(exceptions));
return true;
}
}
}
// check for duplicated catch blocks that handle reflective exceptions exactly the same way,
// and merge them into a single block that catches ROE
Set<String> uniq = new HashSet<>();
for (CatchTree ct : result.handles.values()) {
uniq.add(state.getSourceForNode(ct.getBlock()));
}
// the catch blocks are all unique, append a new fresh one
if (uniq.size() != 1) {
CatchTree last = Iterables.getLast(tryTree.getCatches());
// borrow the variable name of the previous catch variable, in case the naive 'e' conflicts
// with something in the current scope
String name = last.getParameter().getName().toString();
fix.postfixWith(
last,
String.format(
"catch (ReflectiveOperationException %s) {"
+ " throw new LinkageError(%s.getMessage(), %s); }",
name, name, name));
return true;
}
// if the catch blocks contain calls to newInstance, don't delete any of them to avoid
// overlapping fixes
final AtomicBoolean newInstanceInCatch = new AtomicBoolean(false);
((JCTree) result.handles.values().iterator().next())
.accept(
new TreeScanner() {
@Override
public void visitApply(JCTree.JCMethodInvocation tree) {
if (NEW_INSTANCE.matches(tree, state)) {
newInstanceInCatch.set(true);
}
}
});
if (newInstanceInCatch.get()) {
fix.replace(
Iterables.getLast(result.handles.values()).getParameter().getType(),
"ReflectiveOperationException");
return true;
}
// otherwise, merge the duplicated catch blocks into a single block that
// handles ROE
boolean first = true;
for (CatchTree ct : result.handles.values()) {
if (first) {
fix.replace(ct.getParameter().getType(), "ReflectiveOperationException");
first = false;
} else {
fix.delete(ct);
}
}
return true;
}
// if there wasn't a try/catch to add new catch clauses to, update the enclosing
// method declaration's throws clause to declare the new checked exceptions
private void fixThrows(VisitorState state, SuggestedFix.Builder fix) {
MethodTree methodTree = state.findEnclosing(MethodTree.class);
if (methodTree == null || methodTree.getThrows().isEmpty()) {
return;
}
ImmutableMap.Builder<Type, ExpressionTree> thrown = ImmutableMap.builder();
for (ExpressionTree e : methodTree.getThrows()) {
thrown.put(ASTHelpers.getType(e), e);
}
UnhandledResult<ExpressionTree> result = unhandled(thrown.build(), state);
if (result.unhandled.isEmpty()) {
return;
}
List<String> newThrows = new ArrayList<>();
for (Type handle : result.unhandled) {
newThrows.add(handle.tsym.getSimpleName().toString());
}
Collections.sort(newThrows);
fix.postfixWith(
Iterables.getLast(methodTree.getThrows()), ", " + Joiner.on(", ").join(newThrows));
// the other exceptions are in java.lang
fix.addImport("java.lang.reflect.InvocationTargetException");
}
static class UnhandledResult<T> {
/** Exceptions thrown by {@link Constructor#newInstance} that were unhandled. */
final ImmutableSet<Type> unhandled;
/** Handlers for reflective exceptions (e.g. a throws declaration or catch clause). */
final ImmutableMap<Type, T> handles;
UnhandledResult(ImmutableSet<Type> unhandled, ImmutableMap<Type, T> handles) {
this.unhandled = unhandled;
this.handles = handles;
}
}
/**
* Given a map of handled exception types and the trees of those handlers (i.e. catch clauses or
* method throws clauses), determine which handlers are for reflective exceptions, and whether
* all exceptions thrown by {#link Constructor#newInstance} are handled.
*/
private <T> UnhandledResult<T> unhandled(ImmutableMap<Type, T> handles, VisitorState state) {
LinkedHashSet<Type> toHandle = new LinkedHashSet<>();
for (Class<?> e :
Arrays.asList(
InstantiationException.class,
IllegalAccessException.class,
InvocationTargetException.class,
NoSuchMethodException.class)) {
Type type = state.getTypeFromString(e.getName());
if (type != null) {
toHandle.add(type);
}
}
Type roe = state.getTypeFromString(ReflectiveOperationException.class.getName());
ImmutableMap.Builder<Type, T> newHandles = ImmutableMap.builder();
for (Map.Entry<Type, T> entry : handles.entrySet()) {
Type type = entry.getKey();
if (ASTHelpers.isSubtype(type, roe, state)) {
newHandles.put(type, entry.getValue());
}
for (Type precise :
type.isUnion()
? ((Type.UnionClassType) type).getAlternativeTypes()
: Collections.singleton(type)) {
Iterator<Type> it = toHandle.iterator();
while (it.hasNext()) {
if (ASTHelpers.isSubtype(it.next(), precise, state)) {
it.remove();
}
}
}
}
return new UnhandledResult<>(ImmutableSet.copyOf(toHandle), newHandles.build());
}
}