All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.palantir.baseline.errorprone.IllegalSafeLoggingArgument Maven / Gradle / Ivy

There is a newer version: 6.11.0
Show newest version
/*
 * (c) Copyright 2019 Palantir Technologies 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.palantir.baseline.errorprone;

import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.method.MethodMatchers;
import com.google.errorprone.util.ASTHelpers;
import com.palantir.baseline.errorprone.safety.Safety;
import com.palantir.baseline.errorprone.safety.SafetyAnalysis;
import com.palantir.baseline.errorprone.safety.SafetyAnnotations;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
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.MethodSymbol;
import com.sun.tools.javac.code.Symbol.TypeVariableSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.TypeVar;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Ensures that safe-logging annotated elements are handled correctly by annotated method parameters.
 * Potential future work:
 * 
    *
  • We could check return statements in methods annotated for * safety to require consistency
  • *
  • Enforce propagation of safety annotations from fields and types to types which encapsulate them.
  • *
  • More complex flow analysis to ensure safety information is respected.
  • *
*/ @AutoService(BugChecker.class) @BugPattern( link = "https://github.com/palantir/gradle-baseline#baseline-error-prone-checks", linkType = BugPattern.LinkType.CUSTOM, severity = BugPattern.SeverityLevel.ERROR, summary = "safe-logging annotations must agree between args and method parameters") public final class IllegalSafeLoggingArgument extends BugChecker implements BugChecker.MethodInvocationTreeMatcher, BugChecker.ReturnTreeMatcher, BugChecker.AssignmentTreeMatcher, BugChecker.CompoundAssignmentTreeMatcher, BugChecker.MethodTreeMatcher, BugChecker.VariableTreeMatcher, BugChecker.NewClassTreeMatcher, BugChecker.ClassTreeMatcher { private static final String UNSAFE_ARG = "com.palantir.logsafe.UnsafeArg"; private static final Matcher SAFE_ARG_OF_METHOD_MATCHER = MethodMatchers.staticMethod() .onClass("com.palantir.logsafe.SafeArg") .named("of"); private static Type resolveParameterType(Type input, ExpressionTree tree, VisitorState state) { // Important not to call getReceiver/getReceiverType on a NewClassTree, which throws. if (input instanceof TypeVar && tree instanceof MethodInvocationTree) { TypeVar typeVar = (TypeVar) input; Type receiver = ASTHelpers.getReceiverType(tree); if (receiver == null) { return input; } Symbol symbol = ASTHelpers.getSymbol(tree); // List -> Collection gives us Collection Type boundToMethodOwner = state.getTypes().asSuper(receiver, symbol.owner); List ownerTypeVars = symbol.owner.getTypeParameters(); // Validate that the type parameters match -- it's possible raw types are used, and // no type variables are bound. See IllegalSafeLoggingArgumentTest.testRawTypes. if (ownerTypeVars.size() == boundToMethodOwner.getTypeArguments().size()) { for (int i = 0; i < ownerTypeVars.size(); i++) { TypeVariableSymbol ownerVar = ownerTypeVars.get(i); if (Objects.equals(ownerVar, typeVar.tsym)) { return boundToMethodOwner.getTypeArguments().get(i); } } } } return input; } @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { return matchCtorOrMethodInvocation( tree, tree.getTypeArguments(), tree.getArguments(), ASTHelpers.getSymbol(tree), state); } @Override public Description matchNewClass(NewClassTree tree, VisitorState state) { return matchCtorOrMethodInvocation( tree, tree.getTypeArguments(), tree.getArguments(), ASTHelpers.getSymbol(tree), state); } @SuppressWarnings({"CheckStyle", "ReferenceEquality"}) private Description matchCtorOrMethodInvocation( ExpressionTree tree, List typeArguments, List arguments, MethodSymbol methodSymbol, VisitorState state) { if (methodSymbol == null) { return Description.NO_MATCH; } handleResultTypeArguments(tree, state); handleMethodTypeArguments(tree, typeArguments, methodSymbol, state); if (arguments.isEmpty()) { return Description.NO_MATCH; } List parameters = methodSymbol.getParameters(); for (int i = 0; i < parameters.size(); i++) { VarSymbol parameter = parameters.get(i); Type resolvedParameterType = resolveParameterType(parameter.type, tree, state); Safety parameterSafety = Safety.mergeAssumingUnknownIsSame( SafetyAnnotations.getSafety(parameter, state), SafetyAnnotations.getSafety(resolvedParameterType, state), SafetyAnnotations.getSafety(resolvedParameterType.tsym, state)); // Collect additional safety info from the declared type // Reference equality is okay because 'resolveParameterType' returns the input if the type doesn't need to // be resolved. if (parameter.type != resolvedParameterType) { parameterSafety = Safety.mergeAssumingUnknownIsSame( parameterSafety, SafetyAnnotations.getSafety(parameter.type, state), SafetyAnnotations.getSafety(parameter.type.tsym, state)); } if (parameterSafety.allowsAll()) { // Fast path: all types are accepted, there's no reason to do further analysis. continue; } int limit = methodSymbol.isVarArgs() && i == parameters.size() - 1 ? arguments.size() : i + 1; for (int j = i; j < limit; j++) { ExpressionTree argument = arguments.get(j); Safety argumentSafety = SafetyAnalysis.of(state.withPath(new TreePath(state.getPath(), argument))); if (!parameterSafety.allowsValueWith(argumentSafety)) { // use state.reportMatch to report all failing arguments if multiple are invalid state.reportMatch(buildDescription(argument) .setMessage(String.format( "Dangerous argument value: arg is '%s' but the parameter requires '%s'.", argumentSafety, parameterSafety)) .addFix(getSuggestedFix(tree, state, argumentSafety)) .build()); } } } return Description.NO_MATCH; } private void handleResultTypeArguments(ExpressionTree tree, VisitorState state) { Type type = ASTHelpers.getResultType(tree); if (type != null && !type.getTypeArguments().isEmpty()) { List resultTypeArguments = type.getTypeArguments(); List parameterTypes = type.tsym.getTypeParameters(); if (parameterTypes.size() == resultTypeArguments.size()) { for (int i = 0; i < parameterTypes.size(); i++) { TypeVariableSymbol typeVar = parameterTypes.get(i); Type typeArgumentType = resultTypeArguments.get(i); Safety typeVarSafety = Safety.mergeAssumingUnknownIsSame( SafetyAnnotations.getSafety(typeVar, state), SafetyAnnotations.getSafety(typeVar.type, state), SafetyAnnotations.getSafety(typeVar.type.tsym, state)); Safety typeArgumentSafety = Safety.mergeAssumingUnknownIsSame( SafetyAnnotations.getSafety(typeArgumentType, state), SafetyAnnotations.getSafety(typeArgumentType.tsym, state)); if (!typeVarSafety.allowsAll() && !typeVarSafety.allowsValueWith(typeArgumentSafety)) { // use state.reportMatch to report all failing arguments if multiple are invalid state.reportMatch(buildDescription(tree) .setMessage(String.format( "Dangerous argument value: arg is '%s' but the parameter requires '%s'.", typeArgumentSafety, typeVarSafety)) .build()); } } } } } private void handleMethodTypeArguments( ExpressionTree tree, List typeArguments, MethodSymbol methodSymbol, VisitorState state) { List typeParameters = methodSymbol.getTypeParameters(); if (typeParameters == null || typeParameters.isEmpty() || typeArguments == null || typeArguments.isEmpty() || typeArguments.size() != typeParameters.size()) { return; } for (int i = 0; i < typeParameters.size(); i++) { TypeVariableSymbol parameter = typeParameters.get(i); Tree argument = typeArguments.get(i); Safety required = Safety.mergeAssumingUnknownIsSame( SafetyAnnotations.getSafety(parameter, state), SafetyAnnotations.getSafety(parameter.type, state), SafetyAnnotations.getSafety(parameter.type.tsym, state)); Safety given = SafetyAnnotations.getSafety(argument, state); if (!required.allowsValueWith(given)) { // use state.reportMatch to report all failing arguments if multiple are invalid state.reportMatch(buildDescription(tree) .setMessage(String.format( "Dangerous argument value: arg is '%s' but the parameter requires '%s'.", given, required)) .build()); } } } private static SuggestedFix getSuggestedFix(ExpressionTree tree, VisitorState state, Safety argumentSafety) { if (SAFE_ARG_OF_METHOD_MATCHER.matches(tree, state) && Safety.UNSAFE.allowsValueWith(argumentSafety)) { SuggestedFix.Builder fix = SuggestedFix.builder(); String unsafeQualifiedClassName = SuggestedFixes.qualifyType(state, fix, UNSAFE_ARG); String replacement = String.format("%s.of", unsafeQualifiedClassName); return fix.replace(((MethodInvocationTree) tree).getMethodSelect(), replacement) .build(); } return SuggestedFix.emptyFix(); } @Override public Description matchReturn(ReturnTree tree, VisitorState state) { if (tree.getExpression() == null) { return Description.NO_MATCH; } TreePath path = state.getPath(); while (path != null && path.getLeaf() instanceof StatementTree) { path = path.getParentPath(); } if (path == null || !(path.getLeaf() instanceof MethodTree)) { return Description.NO_MATCH; } MethodTree method = (MethodTree) path.getLeaf(); Safety methodDeclaredSafety = SafetyAnnotations.getSafety(ASTHelpers.getSymbol(method), state); if (methodDeclaredSafety.allowsAll()) { // Fast path, all types are accepted, there's no reason to do further analysis. return Description.NO_MATCH; } Safety returnValueSafety = SafetyAnalysis.of(state.withPath(new TreePath(state.getPath(), tree.getExpression()))); if (methodDeclaredSafety.allowsValueWith(returnValueSafety)) { return Description.NO_MATCH; } return buildDescription(tree) .setMessage(String.format( "Dangerous return value: result is '%s' but the method is annotated '%s'.", returnValueSafety, methodDeclaredSafety)) .build(); } @Override public Description matchAssignment(AssignmentTree tree, VisitorState state) { return handleAssignment(tree, tree.getVariable(), tree.getExpression(), state); } @Override public Description matchCompoundAssignment(CompoundAssignmentTree tree, VisitorState state) { return handleAssignment(tree, tree.getVariable(), tree.getExpression(), state); } private Description handleAssignment( ExpressionTree assignmentTree, ExpressionTree variable, ExpressionTree expression, VisitorState state) { Safety variableDeclaredSafety = SafetyAnnotations.getSafety(variable, state); if (variableDeclaredSafety.allowsAll()) { return Description.NO_MATCH; } Safety assignmentValue = SafetyAnalysis.of(state.withPath(new TreePath(state.getPath(), expression))); if (variableDeclaredSafety.allowsValueWith(assignmentValue)) { return Description.NO_MATCH; } return buildDescription(assignmentTree) .setMessage(String.format( "Dangerous assignment: value is '%s' but the variable is annotated '%s'.", assignmentValue, variableDeclaredSafety)) .build(); } @Override public Description matchMethod(MethodTree tree, VisitorState state) { Tree returnType = tree.getReturnType(); if (returnType == null) { return Description.NO_MATCH; } MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree); // Method annotated safety including all supertypes Safety methodCombinedSafety = SafetyAnnotations.getSafety(methodSymbol, state); Safety methodExplicitSafety = Safety.mergeAssumingUnknownIsSame( SafetyAnnotations.getDirectSafety(methodSymbol, state), SafetyAnnotations.getSafety(methodSymbol.getReturnType(), state)); for (MethodSymbol superMethod : ASTHelpers.findSuperMethods(methodSymbol, state.getTypes())) { Safety superMethodSafety = Safety.mergeAssumingUnknownIsSame( SafetyAnnotations.getDirectSafety(superMethod, state), SafetyAnnotations.getSafety(superMethod.getReturnType(), state)); if (!superMethodSafety.allowsValueWith(methodExplicitSafety)) { return buildDescription(returnType) .setMessage(String.format( "Dangerous method override: supertype %s declares '%s' but the method is annotated " + "'%s'. When this object is cast to the supertype, safety annotations will " + "not be correct, violating Liskov substitution.", superMethod.owner, superMethodSafety, methodExplicitSafety)) .build(); } if (!superMethodSafety.allowsValueWith(methodCombinedSafety)) { return buildDescription(returnType) .setMessage(String.format( "Dangerous method override: supertype %s declares '%s' but the method inherits safety " + "'%s'. When this object is cast to the supertype, safety annotations will " + "not be correct, violating Liskov substitution.", superMethod.owner, superMethodSafety, methodCombinedSafety)) .build(); } } if (methodCombinedSafety.allowsAll()) { return Description.NO_MATCH; } Safety returnTypeSafety = SafetyAnnotations.getSafety(ASTHelpers.getSymbol(returnType), state); if (methodCombinedSafety.allowsValueWith(returnTypeSafety)) { return Description.NO_MATCH; } return buildDescription(returnType) .setMessage(String.format( "Dangerous return type: type is '%s' but the method is annotated '%s'.", returnTypeSafety, methodCombinedSafety)) .build(); } @Override public Description matchVariable(VariableTree tree, VisitorState state) { Safety parameterSafetyAnnotation = SafetyAnnotations.getSafety(ASTHelpers.getSymbol(tree), state); if (parameterSafetyAnnotation.allowsAll()) { return Description.NO_MATCH; } Safety variableTypeSafety = SafetyAnnotations.getSafety(ASTHelpers.getSymbol(tree.getType()), state); if (parameterSafetyAnnotation.allowsValueWith(variableTypeSafety)) { return Description.NO_MATCH; } return buildDescription(tree) .setMessage(String.format( "Dangerous variable: type is '%s' but the variable is annotated '%s'.", variableTypeSafety, parameterSafetyAnnotation)) .build(); } @Override public Description matchClass(ClassTree tree, VisitorState state) { ClassSymbol classSymbol = ASTHelpers.getSymbol(tree); Safety directSafety = SafetyAnnotations.getDirectSafety(classSymbol, state); Safety combinedSafety = SafetyAnnotations.getSafety(classSymbol, state); if (combinedSafety == Safety.UNKNOWN) { return Description.NO_MATCH; } Set superTypes = Stream.concat( Stream.of(classSymbol.getSuperclass()), classSymbol.getInterfaces().stream()) .collect(Collectors.toUnmodifiableSet()); for (Type superType : superTypes) { Safety superTypeSafety = Safety.mergeAssumingUnknownIsSame( SafetyAnnotations.getSafety(superType, state), SafetyAnnotations.getSafety(superType.tsym, state)); if (superTypeSafety.allowsAll()) { continue; } if (!superTypeSafety.allowsValueWith(directSafety)) { return buildDescription(tree) .setMessage(String.format( "Dangerous subtype: supertype %s declares '%s' but the type is annotated " + "'%s'. When this object is cast to the supertype, safety annotations will " + "not be correct, violating Liskov substitution.", superType, superTypeSafety, directSafety)) .build(); } if (!superTypeSafety.allowsValueWith(combinedSafety)) { return buildDescription(tree) .setMessage(String.format( "Dangerous subtype: supertype %s declares '%s' but the type inherits safety " + "'%s'. When this object is cast to the supertype, safety annotations will " + "not be correct, violating Liskov substitution.", superType, superTypeSafety, combinedSafety)) .build(); } } if (directSafety.allowsAll()) { return Description.NO_MATCH; } Safety ancestorSafety = SafetyAnnotations.getTypeSafetyFromAncestors(tree, state); if (directSafety.allowsValueWith(ancestorSafety)) { return Description.NO_MATCH; } return buildDescription(tree) .setMessage(String.format( "Dangerous type: annotated '%s' but ancestors declare '%s'.", directSafety, ancestorSafety)) .build(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy