/* * 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() {} }