/*
* 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.formatstring;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.FormatMethod;
import com.google.errorprone.annotations.FormatString;
import com.google.errorprone.bugpatterns.formatstring.FormatStringValidation.ValidationResult;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import java.util.List;
import javax.annotation.Nullable;
import javax.lang.model.element.ElementKind;
/**
* Format string validation utility that fails on more cases than {@link FormatStringValidation} to
* enforce strict format string checking.
*/
public class StrictFormatStringValidation {
@Nullable
public static ValidationResult validate(
ExpressionTree formatStringTree, List<? extends ExpressionTree> args, VisitorState state) {
String formatStringValue = ASTHelpers.constValue(formatStringTree, String.class);
// If formatString has a constant value, then it couldn't have been an @FormatString parameter,
// so don't bother with annotations and just check if the parameters match the format string.
if (formatStringValue != null) {
return FormatStringValidation.validate(
ImmutableList.<ExpressionTree>builder().add(formatStringTree).addAll(args).build(),
state);
}
// The format string is not a compile time constant. Check if it is an @FormatString method
// parameter or is in an @FormatMethod method.
Symbol formatStringSymbol = ASTHelpers.getSymbol(formatStringTree);
if (!(formatStringSymbol instanceof VarSymbol)) {
return ValidationResult.create(
null,
String.format(
"Format strings must be either a literal or a variable. Other expressions"
+ " are not valid.\n"
+ "Invalid format string: %s",
formatStringTree));
}
if ((formatStringSymbol.flags() & (Flags.FINAL | Flags.EFFECTIVELY_FINAL)) == 0) {
return ValidationResult.create(
null, "All variables passed as @FormatString must be final or effectively final");
}
if (formatStringSymbol.getKind() == ElementKind.PARAMETER) {
return validateFormatStringParamter(formatStringTree, formatStringSymbol, args, state);
} else {
// The format string is final but not a method parameter or compile time constant. Ensure that
// it is only assigned to compile time constant values and ensure that any possible assignment
// works with the format arguments.
return validateFormatStringVariable(formatStringTree, formatStringSymbol, args, state);
}
}
/** Helps {@code validate()} validate a format string that is declared as a method parameter. */
private static ValidationResult validateFormatStringParamter(
ExpressionTree formatStringTree,
Symbol formatStringSymbol,
List<? extends ExpressionTree> args,
VisitorState state) {
if (!isFormatStringParameter(formatStringSymbol, state)) {
return ValidationResult.create(
null,
String.format(
"Format strings must be compile time constant or parameters annotated "
+ "@FormatString: %s",
formatStringTree));
}
List<VarSymbol> ownerParams = ((MethodSymbol) formatStringSymbol.owner).getParameters();
int ownerFormatStringIndex = ownerParams.indexOf(formatStringSymbol);
ImmutableList.Builder<Type> ownerFormatArgTypesBuilder = ImmutableList.builder();
for (VarSymbol paramSymbol :
ownerParams.subList(ownerFormatStringIndex + 1, ownerParams.size())) {
ownerFormatArgTypesBuilder.add(paramSymbol.type);
}
ImmutableList<Type> ownerFormatArgTypes = ownerFormatArgTypesBuilder.build();
Types types = state.getTypes();
ImmutableList.Builder<Type> calleeFormatArgTypesBuilder = ImmutableList.builder();
for (ExpressionTree formatArgExpression : args) {
calleeFormatArgTypesBuilder.add(types.erasure(((JCExpression) formatArgExpression).type));
}
ImmutableList<Type> calleeFormatArgTypes = calleeFormatArgTypesBuilder.build();
if (ownerFormatArgTypes.size() != calleeFormatArgTypes.size()) {
return ValidationResult.create(
null,
String.format(
"The number of format arguments passed "
+ "with an @FormatString must match the number of format arguments in the "
+ "@FormatMethod header where the format string was declared.\n\t"
+ "Format args passed: %d\n\tFormat args expected: %d",
calleeFormatArgTypes.size(), ownerFormatArgTypes.size()));
} else {
for (int i = 0; i < calleeFormatArgTypes.size(); i++) {
if (!ASTHelpers.isSameType(
ownerFormatArgTypes.get(i), calleeFormatArgTypes.get(i), state)) {
return ValidationResult.create(
null,
String.format(
"The format argument types passed "
+ "with an @FormatString must match the types of the format arguments in "
+ "the @FormatMethod header where the format string was declared.\n\t"
+ "Format arg types passed: %s\n\tFormat arg types expected: %s",
calleeFormatArgTypes.toArray(), ownerFormatArgTypes.toArray()));
}
}
}
// Format string usage was valid.
return null;
}
/**
* Helps {@code validate()} validate a format string that is a variable, but not a parameter. This
* method assumes that the format string variable has already been asserted to be final or
* effectively final.
*/
private static ValidationResult validateFormatStringVariable(
ExpressionTree formatStringTree,
final Symbol formatStringSymbol,
final List<? extends ExpressionTree> args,
final VisitorState state) {
if (formatStringSymbol.getKind() != ElementKind.LOCAL_VARIABLE) {
return ValidationResult.create(
null,
String.format(
"Variables used as format strings that are not local variables must be compile time"
+ " consant.\n%s is not a local variable and is not compile time constant.",
formatStringTree));
}
// Find the Tree for the block in which the variable is defined. If it is not defined in this
// class (though it may have been in a super class). We require compile time constant values in
// that case.
Symbol owner = formatStringSymbol.owner;
TreePath path = TreePath.getPath(state.getPath(), formatStringTree);
while (path != null && ASTHelpers.getSymbol(path.getLeaf()) != owner) {
path = path.getParentPath();
}
// A local variable must be declared in a parent tree to be accessed. This case should be
// impossible.
if (path == null) {
throw new IllegalStateException(
String.format(
"Could not find the Tree where local variable %s is declared. "
+ "This should be impossible.",
formatStringTree));
}
// Scan down from the scope where the variable was declared
ValidationResult result =
path.getLeaf()
.accept(
new TreeScanner<ValidationResult, Void>() {
@Override
public ValidationResult visitVariable(VariableTree node, Void unused) {
if (ASTHelpers.getSymbol(node) == formatStringSymbol) {
if (node.getInitializer() == null) {
return ValidationResult.create(
null,
String.format(
"Variables used as format strings must be initialized when they are"
+ " declared.\nInvalid declaration: %s",
node));
}
return validateStringFromAssignment(node, node.getInitializer(), args, state);
}
return super.visitVariable(node, unused);
}
@Override
public ValidationResult reduce(ValidationResult r1, ValidationResult r2) {
if (r1 == null && r2 == null) {
return null;
}
return MoreObjects.firstNonNull(r1, r2);
}
},
null);
return result;
}
private static ValidationResult validateStringFromAssignment(
Tree formatStringAssignment,
ExpressionTree formatStringRhs,
List<? extends ExpressionTree> args,
VisitorState state) {
String value = ASTHelpers.constValue(formatStringRhs, String.class);
if (value == null) {
return ValidationResult.create(
null,
String.format(
"Local format string variables must only be assigned to compile time constant values."
+ " Invalid format string assignment: %s",
formatStringAssignment));
} else {
return FormatStringValidation.validate(
ImmutableList.<ExpressionTree>builder().add(formatStringRhs).addAll(args).build(), state);
}
}
/**
* Returns whether an input {@link Symbol} is a format string in a {@link FormatMethod}. This is
* true if the {@link Symbol} is a {@link String} parameter in a {@link FormatMethod} and is
* either:
*
* <ol>
* <li>Annotated with {@link FormatString}
* <li>The first {@link String} parameter in the method with no other parameters annotated {@link
* FormatString}.
* </ol>
*/
private static boolean isFormatStringParameter(Symbol formatString, VisitorState state) {
Type stringType = state.getSymtab().stringType;
// The input symbol must be a String and a parameter of a @FormatMethod to be a @FormatString.
if (!ASTHelpers.isSameType(formatString.type, stringType, state)
|| !(formatString.owner instanceof MethodSymbol)
|| !ASTHelpers.hasAnnotation(formatString.owner, FormatMethod.class, state)) {
return false;
}
// If the format string is annotated @FormatString in a @FormatMethod, it is a format string.
if (ASTHelpers.hasAnnotation(formatString, FormatString.class, state)) {
return true;
}
// Check if format string is the first string with no @FormatString params in the @FormatMethod.
MethodSymbol owner = (MethodSymbol) formatString.owner;
boolean formatStringFound = false;
for (Symbol param : owner.getParameters()) {
if (param == formatString) {
formatStringFound = true;
}
if (ASTHelpers.isSameType(param.type, stringType, state)) {
// If this is a String parameter before the input Symbol, then the input symbol can't be the
// format string since it wasn't annotated @FormatString.
if (!formatStringFound) {
return false;
} else if (ASTHelpers.hasAnnotation(param, FormatString.class, state)) {
return false;
}
}
}
return true;
}
private StrictFormatStringValidation() {}
}