Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2020 The Error Prone Authors.
*
* 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.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static com.sun.source.tree.Tree.Kind.STRING_LITERAL;
import static java.util.Comparator.naturalOrder;
import static java.util.stream.Collectors.groupingBy;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.fixes.SuggestedFixes.AdditionPosition;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.TypeSymbol;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.util.Name;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
@BugPattern(
name = "MemoizeConstantVisitorStateLookups",
summary =
"Anytime you need to look up a constant value from VisitorState, improve performance by"
+ " creating a cache for it with VisitorState.memoize",
severity = SeverityLevel.WARNING)
public class MemoizeConstantVisitorStateLookups extends BugChecker
implements CompilationUnitTreeMatcher {
private static final Matcher CONSTANT_LOOKUP =
instanceMethod()
.onExactClass(VisitorState.class.getName())
.namedAnyOf("getName", "getTypeFromString", "getSymbolFromString")
.withParameters(String.class.getName());
private static final Matcher MEMOIZE_CALL =
staticMethod().onClass(VisitorState.class.getName()).named("memoize");
@Override
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
tree.getTypeDecls().stream()
.filter(t -> t instanceof ClassTree)
.forEach(t -> state.reportMatch(fixLookupsInClass((ClassTree) t, state)));
return NO_MATCH;
}
private Description fixLookupsInClass(ClassTree tree, VisitorState state) {
ImmutableList lookups = findConstantLookups(tree, state);
if (lookups.isEmpty()) {
return NO_MATCH;
}
Map>> groupedCallSites =
lookups.stream()
.collect(
groupingBy(
callSite -> callSite.argumentValue, groupingBy(callSite -> callSite.method)));
// addMembers can only be called once per class, so we emit just one fix for all occurrences.
SuggestedFix.Builder fix = SuggestedFix.builder();
ImmutableSortedSet.Builder membersToAdd =
new ImmutableSortedSet.Builder<>(naturalOrder());
for (Map.Entry>> lookup : groupedCallSites.entrySet()) {
String argument = lookup.getKey();
Map> usages = lookup.getValue();
if (usages.size() == 1) {
// The common case: we have state.foo(argument), and never state.bar(argument), so we can
// name the constant after just argument.
Map.Entry> useSites = usages.entrySet().iterator().next();
Name methodName = useSites.getKey();
List instances = useSites.getValue();
memoizeSupplier(
state, fix, membersToAdd::add, argument, methodName, instances, (name, type) -> name);
} else {
// Sadly we have both state.foo(argument) and also state.bar(argument), so we need two
// constants based on argument and must disambiguate their names.
for (Map.Entry> usage : usages.entrySet()) {
Name methodName = usage.getKey();
List instances = usage.getValue();
memoizeSupplier(
state,
fix,
membersToAdd::add,
argument,
methodName,
instances,
(name, type) -> name + "_" + type);
}
}
}
SuggestedFixes.addMembers(tree, state, AdditionPosition.LAST, membersToAdd.build())
.ifPresent(fix::merge);
return describeMatch(tree, fix.build());
}
private static final class CallSite {
/** The method on VisitorState being called. */
final Name method;
/** The compile-time constant value being passed to that method. */
final String argumentValue;
/** The actual expression with that value: a string literal, or a constant with such a value. */
final ExpressionTree argumentExpression;
/** The entire invocation of the VisitorState method. */
final MethodInvocationTree entireTree;
CallSite(
Name method,
String argumentValue,
ExpressionTree argumentExpression,
MethodInvocationTree entireTree) {
this.method = method;
this.argumentValue = argumentValue;
this.argumentExpression = argumentExpression;
this.entireTree = entireTree;
}
}
private static ImmutableList findConstantLookups(ClassTree tree, VisitorState state) {
ImmutableList.Builder result = ImmutableList.builder();
new TreeScanner() {
@Override
public Void visitMethodInvocation(MethodInvocationTree tree, Void unused) {
if (CONSTANT_LOOKUP.matches(tree, state)) {
handleConstantLookup(tree);
} else if (MEMOIZE_CALL.matches(tree, state)) {
// Don't descend into calls to memoize, because they're already properly memoized!
return null;
}
return super.visitMethodInvocation(tree, null);
}
/** Adds to result if the call uses a compile-time constant argument. */
private void handleConstantLookup(MethodInvocationTree tree) {
ExpressionTree argumentExpr = tree.getArguments().get(0);
String argumentValue = ASTHelpers.constValue(argumentExpr, String.class);
if (argumentValue != null) {
ExpressionTree methodSelect = tree.getMethodSelect();
if (methodSelect instanceof JCFieldAccess) {
JCFieldAccess fieldAccess = (JCFieldAccess) methodSelect;
Name method = fieldAccess.name;
result.add(new CallSite(method, argumentValue, argumentExpr, tree));
} else {
// Just give up on calls we can't understand - maybe from inside VisitorState itself?
}
}
}
}.scan(tree, null);
return result.build();
}
/**
* Adds to {@code fix} the changes necessary to memoize all the callsites in {@code instances},
* and offers {@code memberConsumer} a new constant to which the fixed callsites will refer.
*/
private static void memoizeSupplier(
VisitorState state,
SuggestedFix.Builder fix,
Consumer memberConsumer,
String argument,
Name methodName,
List instances,
BiFunction namingStrategy) {
CallSite prototype = bestCallsite(instances);
MethodSymbol sym = ASTHelpers.getSymbol(prototype.entireTree);
TypeSymbol returnType = sym.getReturnType().tsym;
String returnTypeName = returnType.getSimpleName().toString();
String newConstantPrefix = Ascii.toUpperCase(argument).replaceAll("\\W", "_");
String newConstantName =
namingStrategy.apply(newConstantPrefix, Ascii.toUpperCase(returnTypeName));
memberConsumer.accept(
String.format(
"private static final %s<%s> %s = %s.memoize(state -> state.%s(%s));",
SuggestedFixes.qualifyType(state, fix, Supplier.class.getCanonicalName()),
SuggestedFixes.qualifyType(state, fix, returnType.getQualifiedName().toString()),
newConstantName,
SuggestedFixes.qualifyType(state, fix, VisitorState.class.getCanonicalName()),
methodName,
state.getSourceForNode(prototype.argumentExpression)));
for (CallSite instance : instances) {
ExpressionTree visitorStateExpr = ASTHelpers.getReceiver(instance.entireTree);
fix.replace(
instance.entireTree,
String.format("%s.get(%s)", newConstantName, state.getSourceForNode(visitorStateExpr)));
}
}
/**
* Chooses one call site to use for understanding all related callsites. This is used to determine
* what type is being looked up and what source text to use as the argument to the VisitorState
* method. The idea is that if someone writes both getName("foo") and getName(FOO), we want to
* only define one constant for them. In case there are two callsites with different arguments, we
* prefer defined constants rather than x-raying through to a string literal, i.e., getName(FOO).
*/
private static CallSite bestCallsite(List instances) {
return instances.stream()
.max(
Comparator.comparingInt(
callsite -> callsite.argumentExpression.getKind() == STRING_LITERAL ? 0 : 1))
.orElseThrow(
() -> // Impossible, since we got here by groupingBy
new IllegalArgumentException("No callsites"));
}
}