Skip to content

Commit

Permalink
Add StringUnformatArgsCheck
Browse files Browse the repository at this point in the history
  • Loading branch information
fluentfuture committed Nov 25, 2023
1 parent 35d9558 commit b2090fa
Show file tree
Hide file tree
Showing 4 changed files with 544 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,125 @@
package com.google.mu.errorprone;

import com.google.common.collect.ImmutableMap;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Strings;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.mu.util.Substring;
import com.google.mu.util.stream.BiStream;
import static com.google.mu.util.stream.GuavaCollectors.toImmutableMap;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;

/** Abstract class providing convenience to BugChecker implementations. */
/**
* A convenience base class allowing subclasses to use precondition-style checking such as {@code
* checkingOn(tree).require(formatString != null, "message...")}.
*
* <p>The subclass should implement one or multiple of the "mixin" interfaces ({@link
* ConstructorCallCheck}, {@link MethodInvocationCheck}, {@link MemberReferenceCheck}). They adapt
* from the {@link ErrorReport} exception thrown by the `check` abstract methods to the {@link
* com.google.errorprone.matchers.Description} return value expected by Error-Prone.
*/
abstract class AbstractBugChecker extends BugChecker {
private static final Substring.Pattern PLACEHOLDER = Substring.between('{', '}');
private static final ImmutableMap<?, ?> MAP = BiStream.empty().collect(toImmutableMap());
/**
* Mixin interface for checkers that check constructor calls. Subclasses can implement the {@link
* #checkConstructorCall} and throw {@code ErrorReport} to indicate failures.
*/
interface ConstructorCallCheck extends NewClassTreeMatcher {
/** DO NOT override this method. Implement {@link #checkConstructorCall} instead. */
@Override
public default Description matchNewClass(NewClassTree tree, VisitorState state) {
return ErrorReport.checkAndReportError(tree, state, this::checkConstructorCall);
}

void checkConstructorCall(NewClassTree tree, VisitorState state) throws ErrorReport;
}

/**
* Mixin interface for checkers that check member references. Subclasses can implement the {@link
* #checkMemberReference} and throw {@code ErrorReport} to indicate failures.
*/
interface MemberReferenceCheck extends MemberReferenceTreeMatcher {
/** DO NOT override this method. Implement {@link #checkMemberReference} instead. */
@Override
public default Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
return ErrorReport.checkAndReportError(tree, state, this::checkMemberReference);
}

void checkMemberReference(MemberReferenceTree tree, VisitorState state) throws ErrorReport;
}

/**
* Mixin interface for checkers that check method invocations. Subclasses can implement the {@link
* #checkMethodInvocation} and throw {@code ErrorReport} to indicate failures.
*/
interface MethodInvocationCheck extends MethodInvocationTreeMatcher {
/** DO NOT override this method. Implement {@link #checkMethodInvocation} instead. */
@Override
public default Description matchMethodInvocation(
MethodInvocationTree tree, VisitorState state) {
return ErrorReport.checkAndReportError(tree, state, this::checkMethodInvocation);
}

void checkMethodInvocation(MethodInvocationTree tree, VisitorState state) throws ErrorReport;
}

/** Starts checking on {@code node}. Errors will be reported as pertaining to this node. */
final NodeCheck checkingOn(Tree node) {
checkNotNull(node);
return (condition, message, args) -> {
if (condition) {
return checkingOn(node);
}
throw new ErrorReport(buildDescription(node), message, args);
};
}

/** Fluently checking on a tree node. */
interface NodeCheck {
/**
* Checks that {code condition} holds or else reports error using {@code message} with {@code
* args}. If an arg is an instance of {@link Tree}, the source code of that tree node will be
* reported in the error message.
*/
@CanIgnoreReturnValue
NodeCheck require(boolean condition, String message, Object... args) throws ErrorReport;
}

/** An error report of a violation. */
static final class ErrorReport extends Exception {
private final Description.Builder description;
private final Object[] args;

private ErrorReport(Description.Builder description, String message, Object... args) {
super(message, null, false, false);
this.description = description;
this.args = args.clone();
}

private static <T extends Tree> Description checkAndReportError(
T tree, VisitorState state, Checker<? super T> impl) {
try {
impl.check(tree, state);
} catch (ErrorReport report) {
return report.buildDescription(state);
}
return Description.NO_MATCH;
}

private interface Checker<T extends Tree> {
void check(T tree, VisitorState state) throws ErrorReport;
}

private Description buildDescription(VisitorState state) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Tree) {
args[i] = state.getSourceForNode((Tree) args[i]);
}
}
return description.setMessage(Strings.lenientFormat(getMessage(), args)).build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.google.mu.errorprone;


import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.mu.util.Optionals.optionally;
import static com.google.mu.util.Substring.consecutive;
import static com.google.mu.util.Substring.first;
import static com.google.mu.util.Substring.BoundStyle.INCLUSIVE;

import java.util.List;
import java.util.Optional;

import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.google.mu.util.Substring;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;

/** Some common utils for format and unformat checks. */
final class FormatStringUtils {
static final Substring.Pattern PLACEHOLDER_PATTERN =
consecutive(CharMatcher.noneOf("{}")::matches).immediatelyBetween("{", INCLUSIVE, "}", INCLUSIVE);
static final Substring.RepeatingPattern PLACEHOLDER_NAMES_PATTERN =
consecutive(CharMatcher.noneOf("{}")::matches).immediatelyBetween("{", "}").repeatedly();

static ImmutableList<String> placeholderVariableNames(String formatString) {
return PLACEHOLDER_NAMES_PATTERN
.from(formatString)
.map(first('=').toEnd()::removeFrom) // for Cloud resource name syntax
.collect(toImmutableList());
}

static Optional<ExpressionTree> getInlineStringArg(Tree expression, VisitorState state) {
ImmutableList<? extends ExpressionTree> args =
invocationArgs(expression).stream()
.map(ASTHelpers::stripParentheses)
.filter(arg -> isStringType(arg, state))
.collect(toImmutableList());
return optionally(args.size() == 1, () -> args.get(0));
}

static Optional<String> findFormatString(Tree unformatter, VisitorState state) {
if (unformatter instanceof IdentifierTree) {
Symbol symbol = ASTHelpers.getSymbol(unformatter);
if (symbol instanceof VarSymbol) {
Tree def = JavacTrees.instance(state.context).getTree(symbol);
if (def instanceof VariableTree) {
return findFormatString(((VariableTree) def).getInitializer(), state);
}
}
return Optional.empty();
}
return getInlineStringArg(unformatter, state)
.map(tree -> ASTHelpers.constValue(tree, String.class));
}

private static List<? extends ExpressionTree> invocationArgs(Tree tree) {
if (tree instanceof NewClassTree) {
return ((NewClassTree) tree).getArguments();
}
if (tree instanceof MethodInvocationTree) {
return ((MethodInvocationTree) tree).getArguments();
}
return ImmutableList.of();
}

private static boolean isStringType(ExpressionTree arg, VisitorState state) {
return ASTHelpers.isSameType(ASTHelpers.getType(arg), state.getSymtab().stringType, state);
}
}
Loading

0 comments on commit b2090fa

Please sign in to comment.