/*
* Copyright 2017 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;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.tree.JCTree;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.lang.model.element.Name;
/** @author hanuszczak@google.com (Ćukasz Hanuszczak) */
@BugPattern(
name = "UngroupedOverloads",
summary =
"Constructors and methods with the same name should appear sequentially"
+ " with no other code in between",
generateExamplesFromTestCases = false,
category = JDK,
severity = SUGGESTION,
linkType = CUSTOM,
link = "https://google.github.io/styleguide/javaguide.html#s3.4.2.1-overloads-never-split"
)
public class UngroupedOverloads extends BugChecker implements ClassTreeMatcher {
private static final int DEFAULT_METHOD_COUNT_CUTOFF = 100;
private final int methodCountCutoff;
public UngroupedOverloads() {
this(DEFAULT_METHOD_COUNT_CUTOFF);
}
/**
* @param methodCountCutoff the limit on the number of methods in the class for which fixes are no
* longer suggested (but correctness is still ensured).
*/
UngroupedOverloads(int methodCountCutoff) {
this.methodCountCutoff = methodCountCutoff;
}
@Override
public Description matchClass(ClassTree classTree, VisitorState state) {
List<? extends Tree> classMembers = new ArrayList<>(classTree.getMembers());
MethodFixSuggester suggester = new MethodFixSuggester(classTree, classMembers, state);
/*
* Checking the class members for violations requires only in O(n) time whereas providing
* suggested fixes requires O(n^2) time. This is why we have a cut-off limit that depending on
* the number of class members selects feasible strategy.
*/
long methodCount = classMembers.stream().filter((tree) -> tree instanceof MethodTree).count();
if (methodCount >= methodCountCutoff) {
checkMembers(classMembers, suggester);
} else {
orderMembers(classMembers, suggester);
}
return suggester.describeFix();
}
private static void checkMembers(
List<? extends Tree> classMembers, MethodFixSuggester suggester) {
Map<Name, Integer> previousOccurrences = new LinkedHashMap<>();
for (int currentOccurrence = 0; currentOccurrence < classMembers.size(); currentOccurrence++) {
Tree memberTree = classMembers.get(currentOccurrence);
if (!(memberTree instanceof MethodTree)) {
continue;
}
MethodTree methodTree = (MethodTree) memberTree;
Name methodName = methodTree.getName();
/*
* If there is a previous occurrence and it is on a position different than the previous one
* it must be the case that the methods are not ordered properly (so we report a violation).
*/
Integer previousOccurrence = previousOccurrences.get(methodName);
if (previousOccurrence != null && previousOccurrence != currentOccurrence - 1) {
suggester.justReport(currentOccurrence);
}
previousOccurrences.put(methodName, currentOccurrence);
}
}
private static void orderMembers(
List<? extends Tree> classMembers, MethodFixSuggester suggester) {
/*
* The ordering algorithm works by bubbling (the same way as in bubble-sort) methods until they
* reach correct position. Therefore algorithm has a O(n^2) where n is number of methods within
* the class. However, this is just a worst case scenario (and classes usually don't have more
* than 1000 members anyway) that could happen only for really, really weird code.
*
* The problem itself looks like something that could be solved in O(n lg n) time but it would
* probably be much more complicated and the constant would be much greater (so for sane code
* it would be slower).
*/
Map<Name, Integer> previousOccurrences = new LinkedHashMap<>();
for (int currentOccurrence = 0; currentOccurrence < classMembers.size(); currentOccurrence++) {
Tree memberTree = classMembers.get(currentOccurrence);
if (!(memberTree instanceof MethodTree)) {
continue;
}
MethodTree methodTree = (MethodTree) memberTree;
Name methodName = methodTree.getName();
Integer previousOccurrence = previousOccurrences.get(methodName);
if (previousOccurrence != null) {
// If the block is actually moved (i.e. the `for` loop below does at least one iteration).
if (currentOccurrence - 1 > previousOccurrence) {
// We "bubble" the current occurrence until it is placed next to the previous occurrence.
for (int i = currentOccurrence - 1; i > previousOccurrence; i--) {
Tree splitterTree = classMembers.get(i);
Name splitterName = getMemberName(splitterTree);
// Swapping may invalidate `previousOccurrences` so we need to shift it by one manually.
Integer splitterOccurrence = previousOccurrences.get(splitterName);
if (splitterOccurrence != null && splitterOccurrence.equals(i)) {
previousOccurrences.put(splitterName, i + 1);
}
Collections.swap(classMembers, i, i + 1);
}
suggester.moveBlock(currentOccurrence);
}
previousOccurrences.put(methodName, previousOccurrence + 1);
} else {
previousOccurrences.put(methodName, currentOccurrence);
}
}
}
/**
* Returns a name for given {@code memberTree} declaration.
*
* <p>Unfortunately there is no specific {@code MemberTree} class and {@link
* ClassTree#getMembers()} returns a list of {@link Tree} elements. But we know that the only
* valid member declarations are either inner classes, methods or variables and they are all
* named.
*/
private static Name getMemberName(Tree memberTree) {
if (memberTree instanceof ClassTree) {
return ((ClassTree) memberTree).getSimpleName();
}
if (memberTree instanceof MethodTree) {
return ((MethodTree) memberTree).getName();
}
if (memberTree instanceof VariableTree) {
return ((VariableTree) memberTree).getName();
}
throw new AssertionError("expected member tree instead of " + memberTree.getKind());
}
/**
* Returns a more fine-tuned starting position of a {@code current} node in the source code.
*
* <p>Unfortunately, {@link JCTree#getStartPosition()} doesn't account for comments which usually
* are integral part of the code. So, instead we use a heuristic: the AST node actually "starts"
* after the first newline after the end position of the previous AST node.
*
* <p>This assumes the relevant comments are placed either before (i.e. above) the definition or
* after the definition on the same line rather than below the definition.
*
* @param current the node for which we retrieve the position
* @param previous a node before the {@code current} one
* @return more useful starting position of the {@code current} node
*/
private static int getBroadStartPosition(VisitorState state, Tree current, Tree previous) {
int previousEndPosition = getBroadEndPosition(state, previous, current);
int currentStartPosition = ((JCTree) current).getStartPosition();
return Math.min(previousEndPosition, currentStartPosition);
}
/**
* Returns a more fine-tuned ending position of a {@code current} node in the source code.
*
* <p>See {@link #getBroadStartPosition(VisitorState, Tree, Tree)} for more information.
*
* @param current the node for which we retrieve the position
* @param next a node after the {@code current} one (optional)
* @return more useful ending position of the {@code current} node
*/
private static int getBroadEndPosition(VisitorState state, Tree current, @Nullable Tree next) {
CharSequence source = state.getSourceCode();
int currentEndPosition = state.getEndPosition(current);
int nextStartPosition;
if (next != null) {
nextStartPosition = ((JCTree) next).getStartPosition();
} else {
nextStartPosition = source.length();
}
String newline = System.lineSeparator();
int newlinePosition = indexOf(source, newline, currentEndPosition, nextStartPosition);
return (newlinePosition < 0) ? currentEndPosition : newlinePosition;
}
/**
* Looks for the first occurrence of {@code term} in {@code sequence}.
*
* <p>This is analogous to the {@link java.lang.String#indexOf(String, int)} but works with any
* {@link java.lang.CharSequence} and also supports {@code toIndex} parameter.
*
* <p>The algorithm has a O(nm) time complexity but it is more than enough for this use case.
*
* @param sequence the character sequence to perform the search on
* @param term the character sequence to search for
* @param fromIndex the index from which to start the search (inclusive)
* @param toIndex the index to which the search happens (exclusive)
* @return the index of the first occurrence of specified substring or -1 if not found
*/
private static int indexOf(CharSequence sequence, CharSequence term, int fromIndex, int toIndex) {
int termLength = term.length();
for (int index = fromIndex; index + termLength - 1 < toIndex; index++) {
int i = 0;
for (; i < termLength; i++) {
if (sequence.charAt(index + i) != term.charAt(i)) {
break;
}
}
if (i == termLength) {
return index;
}
}
return -1;
}
private interface OverloadViolation {
Name getMethodName();
void buildFix(SuggestedFix.Builder fix, VisitorState state, MethodTree target);
}
private static class JustReport implements OverloadViolation {
private final MethodTree methodTree;
public JustReport(MethodTree methodTree) {
this.methodTree = methodTree;
}
@Override
public Name getMethodName() {
return methodTree.getName();
}
@Override
public void buildFix(SuggestedFix.Builder fix, VisitorState state, MethodTree target) {
// Do nothing, this is just for reporting violations.
}
}
private static class MoveBlock implements OverloadViolation {
private final Name methodName;
private final int startPosition;
private final int endPosition;
public MoveBlock(Name methodName, int startPosition, int endPosition) {
this.methodName = methodName;
this.startPosition = startPosition;
this.endPosition = endPosition;
}
@Override
public Name getMethodName() {
return methodName;
}
@Override
public void buildFix(SuggestedFix.Builder fix, VisitorState state, MethodTree target) {
String methodSource = getMethodSource(state.getSourceCode());
fix.replace(startPosition, endPosition, "");
fix.postfixWith(target, methodSource);
}
public String getMethodSource(CharSequence sourceCode) {
return sourceCode.subSequence(startPosition, endPosition).toString();
}
}
private class MethodFixSuggester {
private final ClassTree classTree;
private final ImmutableList<? extends Tree> classMembers; // Initial, unchanged members.
private final VisitorState state;
private final List<OverloadViolation> violations;
public MethodFixSuggester(
ClassTree classTree, List<? extends Tree> classMembers, VisitorState state) {
this.classTree = classTree;
this.classMembers = ImmutableList.copyOf(classMembers);
this.state = state;
this.violations = new ArrayList<>();
}
public void justReport(int currentOccurrence) {
violations.add(new JustReport((MethodTree) classMembers.get(currentOccurrence)));
}
public void moveBlock(int currentOccurrence) {
MethodTree currentTree = (MethodTree) classMembers.get(currentOccurrence);
Name currentName = currentTree.getName();
Tree previousTree = classMembers.get(currentOccurrence - 1);
Tree nextTree;
if (currentOccurrence + 1 < classMembers.size()) {
nextTree = classMembers.get(currentOccurrence + 1);
} else {
nextTree = null;
}
int startPosition = getBroadStartPosition(state, currentTree, previousTree);
int endPosition = getBroadEndPosition(state, currentTree, nextTree);
violations.add(new MoveBlock(currentName, startPosition, endPosition));
}
public Description describeFix() {
if (violations.isEmpty()) {
return Description.NO_MATCH;
} else {
return buildAdaptedDescription().addFix(buildFix()).build();
}
}
private SuggestedFix buildFix() {
ImmutableMap<Name, MethodTree> lastGroupedOccurrences = getLastGroupedOccurrences();
SuggestedFix.Builder fix = SuggestedFix.builder();
for (OverloadViolation violation : violations) {
Name methodName = violation.getMethodName();
violation.buildFix(fix, state, lastGroupedOccurrences.get(methodName));
}
return fix.build();
}
private Description.Builder buildAdaptedDescription() {
ImmutableSet<Name> methodNames =
violations.stream().map(OverloadViolation::getMethodName).collect(toImmutableSet());
if (methodNames.size() == 1) {
return buildMethodDescription(methodNames.iterator().next());
} else {
return buildClassDescription(methodNames);
}
}
private Description.Builder buildMethodDescription(Name methodName) {
MethodTree methodTree = getFirstOccurrences().get(methodName);
return buildDescription(methodTree).setMessage(getMethodFixMessage());
}
private Description.Builder buildClassDescription(Collection<Name> methodNames) {
return buildDescription(classTree).setMessage(getClassFixMessage(methodNames));
}
/**
* Returns a mapping from a method name to the first (topmost) AST node matching this name.
*
* <p>For a class with methods {@code A}, {@code B} and {@code C} in the following order the
* marked nodes are considered to be "first occurrences":
*
* <pre>
* AABBBABCCAB
* ^ ^ ^
* </pre>
*/
private ImmutableMap<Name, MethodTree> getFirstOccurrences() {
Map<Name, MethodTree> firstOccurrences = new LinkedHashMap<>();
for (Tree memberTree : classMembers) {
if (!(memberTree instanceof MethodTree)) {
continue;
}
MethodTree methodTree = (MethodTree) memberTree;
Name methodName = methodTree.getName();
firstOccurrences.computeIfAbsent(methodName, __ -> methodTree);
}
return ImmutableMap.copyOf(firstOccurrences);
}
/**
* Returns a mapping from a method name to the last grouped AST node matching this name.
*
* <p>For a class with methods {@code A}, {@code B} and {@code C} in the following order the
* marked nodes are considered to be "last grouped occurrences":
*
* <pre>
* AABBBABCCAB
* ^ ^ ^
* </pre>
*/
private ImmutableMap<Name, MethodTree> getLastGroupedOccurrences() {
Map<Name, Integer> lastGroupedOccurrences = new LinkedHashMap<>();
for (int i = 0; i < classMembers.size(); i++) {
Tree memberTree = classMembers.get(i);
if (!(memberTree instanceof MethodTree)) {
continue;
}
MethodTree methodTree = (MethodTree) memberTree;
Name methodName = methodTree.getName();
Integer lastGroupedOccurrence = lastGroupedOccurrences.get(methodName);
if (lastGroupedOccurrence == null || lastGroupedOccurrence + 1 == i) {
lastGroupedOccurrences.put(methodName, i);
}
}
return transformMap(lastGroupedOccurrences, (index) -> (MethodTree) classMembers.get(index));
}
}
/**
* Transforms each value in the given {@code input} map using the {@code mapper} function.
*
* @return a new map with transformed values
*/
private static <K, V1, V2> ImmutableMap<K, V2> transformMap(
Map<K, V1> input, Function<? super V1, ? extends V2> mapper) {
return input
.entrySet()
.stream()
.collect(toImmutableMap(Map.Entry::getKey, entry -> mapper.apply(entry.getValue())));
}
private static String getMethodFixMessage() {
return "Overloaded versions of this method are not grouped together";
}
private static String getClassFixMessage(Collection<Name> methodNames) {
String methods =
methodNames
.stream()
.map(methodName -> String.format("\"%s\"", methodName.toString()))
.sorted()
.collect(Collectors.joining(", "));
return String.format("Overloaded methods (%s) of this class are not grouped together", methods);
}
}