Skip to content

Commit

Permalink
Fix a StackOverflowError in getLocalAndInheritedMethods, involvin…
Browse files Browse the repository at this point in the history
…g recursive type bounds.

Also optimize override detection by noting that one method cannot override another if they have a different number of arguments.

RELNOTES=`MoreElements.getLocalAndInheritedMethods` no longer gets a stack overflow in certain circumstances involving recursive type bounds.
PiperOrigin-RevId: 542907665
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed Jun 23, 2023
1 parent 39743a8 commit 7d93867
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 19 deletions.
36 changes: 30 additions & 6 deletions common/src/main/java/com/google/auto/common/Overrides.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
Expand All @@ -44,12 +46,12 @@
import org.checkerframework.checker.nullness.qual.Nullable;

/**
* Determines if one method overrides another. This class defines two ways of doing that:
* {@code NativeOverrides} uses the method
* {@link Elements#overrides(ExecutableElement, ExecutableElement, TypeElement)} while
* {@code ExplicitOverrides} reimplements that method in a way that is more consistent between
* compilers, in particular between javac and ecj (the Eclipse compiler).
* Determines if one method overrides another. This class defines two ways of doing that: {@code
* NativeOverrides} uses the method {@link Elements#overrides(ExecutableElement, ExecutableElement,
* TypeElement)} while {@code ExplicitOverrides} reimplements that method in a way that is more
* consistent between compilers, in particular between javac and ecj (the Eclipse compiler).
*
* @see <a href="https://github.com/google/auto/issues/372">AutoValue issue about Eclipse</a>
* @author [email protected] (Éamonn McManus)
*/
abstract class Overrides {
Expand Down Expand Up @@ -101,6 +103,14 @@ public boolean overrides(
// Static methods can't be overridden (though they can be hidden by other static methods).
return false;
}
if (overrider.getParameters().size() != overridden.getParameters().size()) {
// One method can't override another if they have a different number of parameters.
// Varargs `Foo...` appears as `Foo[]` here; there is a separate
// ExecutableElement.isVarArgs() method to tell whether a method is varargs, but that has no
// effect on override logic.
// The check here isn't strictly needed but avoids unnecessary work.
return false;
}
Visibility overriddenVisibility = Visibility.ofElement(overridden);
Visibility overriderVisibility = Visibility.ofElement(overrider);
if (overriddenVisibility.equals(Visibility.PRIVATE)
Expand Down Expand Up @@ -252,6 +262,13 @@ private class TypeSubstVisitor extends SimpleTypeVisitor8<TypeMirror, Void> {
*/
private final Map<TypeParameterElement, TypeMirror> typeBindings = Maps.newLinkedHashMap();

/**
* Type elements that we are currently visiting. This helps us stay out of trouble when
* looking at something like {@code Enum<E extends Enum<E>>}. At the second {@code Enum} we
* will just return raw {@code Enum}.
*/
private final Set<TypeElement> visitingTypes = new LinkedHashSet<>();

@Nullable
ImmutableList<TypeMirror> erasedParameterTypes(ExecutableElement method, TypeElement in) {
if (method.getEnclosingElement().equals(in)) {
Expand Down Expand Up @@ -313,11 +330,18 @@ public TypeMirror visitDeclared(DeclaredType t, Void p) {
if (t.getTypeArguments().isEmpty()) {
return t;
}
TypeElement typeElement = asTypeElement(t);
if (!visitingTypes.add(typeElement)) {
return typeUtils.erasure(t);
}
List<TypeMirror> newArgs = Lists.newArrayList();
for (TypeMirror arg : t.getTypeArguments()) {
newArgs.add(visit(arg));
}
return typeUtils.getDeclaredType(asTypeElement(t), newArgs.toArray(new TypeMirror[0]));
TypeMirror result =
typeUtils.getDeclaredType(asTypeElement(t), newArgs.toArray(new TypeMirror[0]));
visitingTypes.remove(typeElement);
return result;
}

@Override
Expand Down
18 changes: 10 additions & 8 deletions common/src/test/java/com/google/auto/common/MoreElementsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static java.util.Objects.requireNonNull;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

Expand Down Expand Up @@ -366,12 +365,15 @@ public void getLocalAndInheritedMethods_recursiveTypeVariableBound() {
Types types = compilation.getTypes();
TypeElement builderElement =
elements.getTypeElement(FakeProto.Builder.class.getCanonicalName());
// TODO: b/287060583 - this should not trigger infinite recursion
assertThrows(
StackOverflowError.class,
() -> {
Object unused = MoreElements.getLocalAndInheritedMethods(builderElement, types, elements);
});
TypeMirror abstractMessageLiteMirror =
elements.getTypeElement(AbstractMessageLite.class.getCanonicalName()).asType();
ExecutableElement internalMergeFromMethod =
getMethod(FakeProto.Builder.class, "internalMergeFrom", abstractMessageLiteMirror);

ImmutableSet<ExecutableElement> methods =
MoreElements.getLocalAndInheritedMethods(builderElement, types, elements);

assertThat(methods).contains(internalMergeFromMethod);
}

// The classes that follow mimic the proto classes that triggered the bug that
Expand Down Expand Up @@ -486,7 +488,7 @@ private ExecutableElement getMethod(Class<?> c, String methodName, TypeMirror...
for (int i = 0; i < parameterTypes.length; i++) {
TypeMirror expectedType = parameterTypes[i];
TypeMirror actualType = method.getParameters().get(i).asType();
match &= types.isSameType(expectedType, actualType);
match &= types.isSameType(types.erasure(expectedType), types.erasure(actualType));
}
if (match) {
assertThat(found).isNull();
Expand Down
10 changes: 5 additions & 5 deletions common/src/test/java/com/google/auto/common/OverridesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Range;
import com.google.common.io.Files;
import com.google.common.truth.Correspondence;
import com.google.common.truth.Expect;
import com.google.testing.compile.CompilationRule;
import java.io.File;
Expand Down Expand Up @@ -574,11 +575,10 @@ public void methodFromSuperinterfaces() {
}

private void assertTypeListsEqual(@Nullable List<TypeMirror> actual, List<TypeMirror> expected) {
requireNonNull(actual);
assertThat(actual).hasSize(expected.size());
for (int i = 0; i < actual.size(); i++) {
assertThat(typeUtils.isSameType(actual.get(i), expected.get(i))).isTrue();
}
assertThat(actual)
.comparingElementsUsing(Correspondence.from(typeUtils::isSameType, "is same type as"))
.containsExactlyElementsIn(expected)
.inOrder();
}

// TODO(emcmanus): replace this with something from compile-testing when that's available.
Expand Down

0 comments on commit 7d93867

Please sign in to comment.