/*
* Copyright 2012 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.GUAVA;
import static com.google.errorprone.BugPattern.LinkType.NONE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.CompileTimeConstantExpressionMatcher.hasCompileTimeConstantAnnotation;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.Suppressibility;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
import com.google.errorprone.matchers.CompileTimeConstantExpressionMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import java.util.Iterator;
/**
* Detects invocations of methods with a parameter annotated {@code @CompileTimeConstant} such that
* the corresponding actual parameter is not a compile-time constant expression.
*
* <p>This type annotation checker enforces that for all method and constructor invocations, for all
* formal parameters of the invoked method/constructor that are annotated with the {@link
* com.google.errorprone.annotations.CompileTimeConstant} type annotation, the corresponding actual
* parameter is an expression that satisfies one of the following conditions:
*
* <ol>
* <li>The expression is one for which the Java compiler can determine a constant value at compile
* time, or
* <li>the expression consists of the literal {@code null}, or
* <li>the expression consists of a single identifier, where the identifier is a formal method
* parameter that is declared {@code final} and has the {@link
* com.google.errorprone.annotations.CompileTimeConstant} annotation.
* </ol>
*
* @see CompileTimeConstantExpressionMatcher
*/
@BugPattern(
name = "CompileTimeConstant",
summary =
"Non-compile-time constant expression passed to parameter with "
+ "@CompileTimeConstant type annotation.",
explanation =
"A method or constructor with one or more parameters whose declaration is "
+ "annotated with the @CompileTimeConstant type annotation must only be invoked "
+ "with corresponding actual parameters that are computed as compile-time constant "
+ "expressions, such as a literal or static final constant.",
linkType = NONE,
category = GUAVA,
severity = ERROR,
suppressibility = Suppressibility.UNSUPPRESSIBLE
)
public class CompileTimeConstantChecker extends BugChecker
implements MethodInvocationTreeMatcher, NewClassTreeMatcher {
private static final String DID_YOU_MEAN_FINAL_FMT_MESSAGE =
" Did you mean to make '%s' final?";
private final Matcher<ExpressionTree> compileTimeConstExpressionMatcher =
new CompileTimeConstantExpressionMatcher();
/**
* Matches formal parameters with
* {@link com.google.errorprone.annotations.CompileTimeConstant} annotations against
* corresponding actual parameters.
*
* @param state the visitor state
* @param calleeSymbol the method whose formal parameters to consider
* @param actualParams the list of actual parameters
* @return a {@code Description} of the match <i>iff</i> for any of the actual parameters that
* is annotated with {@link com.google.errorprone.annotations.CompileTimeConstant}, the
* corresponding formal parameter is not a compile-time-constant expression in the sense of
* {@link CompileTimeConstantExpressionMatcher}. Otherwise returns {@code Description.NO_MATCH}.
*/
private Description matchArguments(VisitorState state, final Symbol.MethodSymbol calleeSymbol,
Iterator<? extends ExpressionTree> actualParams) {
Symbol.VarSymbol lastFormalParam = null;
for (Symbol.VarSymbol formalParam : calleeSymbol.getParameters()) {
lastFormalParam = formalParam;
// It appears that for some reason, the Tree for implicit Enum constructors
// includes an invocation of super(), but the target symbol has the signature
// Enum(String, int). This resulted in NoSuchElementExceptions.
// It is safe to return no match in this case, since even if this could happen
// in another scenario, a non-existent actual parameter can't possibly
// be a non-constant parameter for a @CompileTimeConstant formal.
if (!actualParams.hasNext()) {
return Description.NO_MATCH;
}
ExpressionTree actualParam = actualParams.next();
if (hasCompileTimeConstantAnnotation(
state, formalParam)) {
if (!compileTimeConstExpressionMatcher.matches(actualParam, state)) {
return handleMatch(actualParam, state);
}
}
}
// If the last formal parameter is a vararg and has the @CompileTimeConstant annotation,
// we need to check the remaining args as well.
if (lastFormalParam == null || (lastFormalParam.flags() & Flags.VARARGS) == 0) {
return Description.NO_MATCH;
}
if (!hasCompileTimeConstantAnnotation(
state, lastFormalParam)) {
return Description.NO_MATCH;
}
while (actualParams.hasNext()) {
ExpressionTree actualParam = actualParams.next();
if (!compileTimeConstExpressionMatcher.matches(actualParam, state)) {
return handleMatch(actualParam, state);
}
}
return Description.NO_MATCH;
}
/**
* If the non-constant variable is annotated with @CompileTimeConstant, it must have been
* non-final. Suggest making it final in the error message.
*/
private Description handleMatch(ExpressionTree actualParam, VisitorState state) {
Symbol sym = ASTHelpers.getSymbol(actualParam);
if (!(sym instanceof VarSymbol)) {
return describeMatch(actualParam);
}
VarSymbol var = (VarSymbol) sym;
if (!hasCompileTimeConstantAnnotation(state, var)) {
return describeMatch(actualParam);
}
return buildDescription(actualParam)
.setMessage(this.message()
+ String.format(DID_YOU_MEAN_FINAL_FMT_MESSAGE, var.getSimpleName()))
.build();
}
@Override
public Description matchNewClass(NewClassTree tree, VisitorState state) {
Symbol.MethodSymbol sym = ASTHelpers.getSymbol(tree);
if (sym == null) {
return Description.NO_MATCH;
}
return matchArguments(state, sym, tree.getArguments().iterator());
}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
Symbol.MethodSymbol sym = ASTHelpers.getSymbol(tree);
if (sym == null) {
return Description.NO_MATCH;
}
return matchArguments(state, sym, tree.getArguments().iterator());
}
}