Add support for `Elements#hides` to turbine
Hiding for fields, classes, and methods is defined in
https://docs.oracle.com/javase/specs/jls/se11/html/jls-8.html
https://github.com/bazelbuild/bazel/issues/14142
PiperOrigin-RevId: 405438970
diff --git a/java/com/google/turbine/processing/TurbineElements.java b/java/com/google/turbine/processing/TurbineElements.java
index 42b5d49..b5fd7f4 100644
--- a/java/com/google/turbine/processing/TurbineElements.java
+++ b/java/com/google/turbine/processing/TurbineElements.java
@@ -29,6 +29,7 @@
import com.google.turbine.binder.sym.PackageSymbol;
import com.google.turbine.binder.sym.Symbol;
import com.google.turbine.model.Const;
+import com.google.turbine.model.TurbineFlag;
import com.google.turbine.model.TurbineVisibility;
import com.google.turbine.processing.TurbineElement.TurbineExecutableElement;
import com.google.turbine.processing.TurbineElement.TurbineFieldElement;
@@ -52,6 +53,7 @@
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
+import org.jspecify.nullness.Nullable;
/** An implementation of {@link Elements} backed by turbine's {@link Element}. */
@SuppressWarnings("nullness") // TODO(cushon): Address nullness diagnostics.
@@ -290,7 +292,89 @@
@Override
public boolean hides(Element hider, Element hidden) {
- throw new UnsupportedOperationException();
+ if (!(hider instanceof TurbineElement)) {
+ throw new IllegalArgumentException(hider.toString());
+ }
+ if (!(hidden instanceof TurbineElement)) {
+ throw new IllegalArgumentException(hidden.toString());
+ }
+ return hides((TurbineElement) hider, (TurbineElement) hidden);
+ }
+
+ private boolean hides(TurbineElement hider, TurbineElement hidden) {
+ if (!hider.sym().symKind().equals(hidden.sym().symKind())) {
+ return false;
+ }
+ if (!hider.getSimpleName().equals(hidden.getSimpleName())) {
+ return false;
+ }
+ if (hider.sym().equals(hidden.sym())) {
+ return false;
+ }
+ if (!isVisibleForHiding(hider, hidden)) {
+ return false;
+ }
+ if (hider.sym().symKind().equals(Symbol.Kind.METHOD)) {
+ int access = ((TurbineExecutableElement) hider).info().access();
+ if ((access & TurbineFlag.ACC_STATIC) != TurbineFlag.ACC_STATIC) {
+ return false;
+ }
+ // Static interface methods shouldn't be able to hide static methods in super-interfaces,
+ // but include them anyways for bug-compatibility with javac, see:
+ // https://bugs.openjdk.java.net/browse/JDK-8275746
+ if (!types.isSubsignature(
+ (TurbineExecutableType) hider.asType(), (TurbineExecutableType) hidden.asType())) {
+ return false;
+ }
+ }
+ Element containingHider = containingClass(hider);
+ Element containingHidden = containingClass(hidden);
+ if (containingHider == null || containingHidden == null) {
+ return false;
+ }
+ if (!types.isSubtype(containingHider.asType(), containingHidden.asType())) {
+ return false;
+ }
+ return true;
+ }
+
+ private static @Nullable Element containingClass(TurbineElement element) {
+ Element enclosing = element.getEnclosingElement();
+ if (enclosing == null) {
+ return null;
+ }
+ if (!isClassOrInterface(enclosing.getKind())) {
+ // The immediately enclosing element of a field or method is a class. For classes, annotation
+ // processing only deals with top-level and nested (but not local or anonymous) classes,
+ // so the immediately enclosing element is either an enclosing class or a package symbol.
+ return null;
+ }
+ return enclosing;
+ }
+
+ private static boolean isClassOrInterface(ElementKind kind) {
+ return kind.isClass() || kind.isInterface();
+ }
+
+ private static boolean isVisibleForHiding(TurbineElement hider, TurbineElement hidden) {
+ int access;
+ switch (hidden.sym().symKind()) {
+ case CLASS:
+ access = ((TurbineTypeElement) hidden).info().access();
+ break;
+ case FIELD:
+ access = ((TurbineFieldElement) hidden).info().access();
+ break;
+ case METHOD:
+ access = ((TurbineExecutableElement) hidden).info().access();
+ break;
+ default:
+ return false;
+ }
+ return isVisible(
+ packageSymbol(asSymbol(hider)),
+ packageSymbol(asSymbol(hidden)),
+ TurbineVisibility.fromAccess(access));
}
@Override
diff --git a/javatests/com/google/turbine/processing/TurbineElementsHidesTest.java b/javatests/com/google/turbine/processing/TurbineElementsHidesTest.java
new file mode 100644
index 0000000..69418d5
--- /dev/null
+++ b/javatests/com/google/turbine/processing/TurbineElementsHidesTest.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2019 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.turbine.processing;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.stream;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ObjectArrays;
+import com.google.common.truth.Expect;
+import com.google.turbine.binder.Binder;
+import com.google.turbine.binder.ClassPathBinder;
+import com.google.turbine.binder.bound.TypeBoundClass;
+import com.google.turbine.binder.env.CompoundEnv;
+import com.google.turbine.binder.env.Env;
+import com.google.turbine.binder.env.SimpleEnv;
+import com.google.turbine.binder.sym.ClassSymbol;
+import com.google.turbine.diag.SourceFile;
+import com.google.turbine.lower.IntegrationTestSupport;
+import com.google.turbine.lower.IntegrationTestSupport.TestInput;
+import com.google.turbine.parse.Parser;
+import com.google.turbine.processing.TurbineElement.TurbineTypeElement;
+import com.google.turbine.testing.TestClassPaths;
+import com.google.turbine.tree.Tree.CompUnit;
+import com.sun.source.util.JavacTask;
+import com.sun.source.util.TaskEvent;
+import com.sun.source.util.TaskListener;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementScanner8;
+import javax.lang.model.util.Elements;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaFileObject;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TurbineElementsHidesTest {
+
+ @Rule public final Expect expect = Expect.create();
+
+ @Parameters
+ public static Iterable<TestInput[]> parameters() {
+ // An array of test inputs. Each element is an array of lines of sources to compile.
+ String[][] inputs = {
+ {
+ "=== A.java ===", //
+ "abstract class A {",
+ " int f;",
+ " static int f() { return 1; }",
+ " static int f(int x) { return 1; }",
+ "}",
+ "=== B.java ===",
+ "abstract class B extends A {",
+ " int f;",
+ " int g;",
+ " static int f() { return 1; }",
+ " static int f(int x) { return 1; }",
+ " static int g() { return 1; }",
+ " static int g(int x) { return 1; }",
+ "}",
+ "=== C.java ===",
+ "abstract class C extends B {",
+ " int f;",
+ " int g;",
+ " int h;",
+ " static int f() { return 1; }",
+ " static int g() { return 1; }",
+ " static int h() { return 1; }",
+ " static int f(int x) { return 1; }",
+ " static int g(int x) { return 1; }",
+ " static int h(int x) { return 1; }",
+ "}",
+ },
+ {
+ "=== A.java ===",
+ "class A {",
+ " class I {",
+ " }",
+ "}",
+ "=== B.java ===",
+ "class B extends A {",
+ " class I extends A.I {",
+ " }",
+ "}",
+ "=== C.java ===",
+ "class C extends B {",
+ " class I extends B.I {",
+ " }",
+ "}",
+ },
+ {
+ "=== A.java ===",
+ "class A {",
+ " class I {",
+ " }",
+ "}",
+ "=== B.java ===",
+ "class B extends A {",
+ " interface I {}",
+ "}",
+ "=== C.java ===",
+ "class C extends B {",
+ " @interface I {}",
+ "}",
+ },
+ {
+ // the containing class or interface of Intf.foo is an interface
+ "=== Outer.java ===",
+ "class Outer {",
+ " static class Inner {",
+ " static void foo() {}",
+ " static class Innerer extends Inner {",
+ " interface Intf {",
+ " static void foo() {}",
+ " }",
+ " }",
+ " }",
+ "}",
+ },
+ {
+ // test two top-level classes with the same name
+ "=== one/A.java ===",
+ "package one;",
+ "public class A {",
+ "}",
+ "=== two/A.java ===",
+ "package two;",
+ "public class A {",
+ "}",
+ },
+ };
+ // https://bugs.openjdk.java.net/browse/JDK-8275746
+ if (IntegrationTestSupport.getMajor() >= 11) {
+ inputs =
+ ObjectArrays.concat(
+ inputs,
+ new String[][] {
+ {
+ // interfaces
+ "=== A.java ===",
+ "interface A {",
+ " static void f() {}",
+ " int x = 42;",
+ "}",
+ "=== B.java ===",
+ "interface B extends A {",
+ " static void f() {}",
+ " int x = 42;",
+ "}",
+ }
+ },
+ String[].class);
+ }
+ return stream(inputs)
+ .map(input -> TestInput.parse(Joiner.on('\n').join(input)))
+ .map(x -> new TestInput[] {x})
+ .collect(toImmutableList());
+ }
+
+ private final TestInput input;
+
+ public TurbineElementsHidesTest(TestInput input) {
+ this.input = input;
+ }
+
+ // Compile the test inputs with javac and turbine, and assert that 'hides' returns the same
+ // results under each implementation.
+ @Test
+ public void test() throws Exception {
+ HidesTester javac = runJavac();
+ HidesTester turbine = runTurbine();
+ assertThat(javac.keys()).containsExactlyElementsIn(turbine.keys());
+ for (String k1 : javac.keys()) {
+ for (String k2 : javac.keys()) {
+ expect
+ .withMessage("hides(%s, %s)", k1, k2)
+ .that(javac.test(k1, k2))
+ .isEqualTo(turbine.test(k1, k2));
+ }
+ }
+ }
+
+ static class HidesTester {
+ // The elements for a particular annotation processing implementation
+ final Elements elements;
+ // A collection of Elements to use as test inputs, keyed by unique strings that can be used to
+ // compare them across processing implementations
+ final ImmutableMap<String, Element> inputs;
+
+ HidesTester(Elements elements, ImmutableMap<String, Element> inputs) {
+ this.elements = elements;
+ this.inputs = inputs;
+ }
+
+ boolean test(String k1, String k2) {
+ return elements.hides(inputs.get(k1), inputs.get(k2));
+ }
+
+ public ImmutableSet<String> keys() {
+ return inputs.keySet();
+ }
+ }
+
+ /** Compiles the test input with turbine. */
+ private HidesTester runTurbine() throws IOException {
+ ImmutableList<CompUnit> units =
+ input.sources.entrySet().stream()
+ .map(e -> new SourceFile(e.getKey(), e.getValue()))
+ .map(Parser::parse)
+ .collect(toImmutableList());
+ Binder.BindingResult bound =
+ Binder.bind(
+ units,
+ ClassPathBinder.bindClasspath(ImmutableList.of()),
+ TestClassPaths.TURBINE_BOOTCLASSPATH,
+ Optional.empty());
+ Env<ClassSymbol, TypeBoundClass> env =
+ CompoundEnv.<ClassSymbol, TypeBoundClass>of(bound.classPathEnv())
+ .append(new SimpleEnv<>(bound.units()));
+ ModelFactory factory = new ModelFactory(env, ClassLoader.getSystemClassLoader(), bound.tli());
+ TurbineTypes turbineTypes = new TurbineTypes(factory);
+ TurbineElements elements = new TurbineElements(factory, turbineTypes);
+ ImmutableList<TurbineTypeElement> typeElements =
+ bound.units().keySet().stream().map(factory::typeElement).collect(toImmutableList());
+ return new HidesTester(elements, collectElements(typeElements));
+ }
+
+ /** Compiles the test input with turbine. */
+ private HidesTester runJavac() throws Exception {
+ DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+ JavacTask javacTask =
+ IntegrationTestSupport.runJavacAnalysis(
+ input.sources, ImmutableList.of(), ImmutableList.of(), diagnostics);
+ List<TypeElement> typeElements = new ArrayList<>();
+ javacTask.addTaskListener(
+ new TaskListener() {
+ @Override
+ public void started(TaskEvent e) {
+ if (e.getKind().equals(TaskEvent.Kind.ANALYZE)) {
+ typeElements.add(e.getTypeElement());
+ }
+ }
+ });
+ Elements elements = javacTask.getElements();
+ if (!javacTask.call()) {
+ fail(Joiner.on("\n").join(diagnostics.getDiagnostics()));
+ }
+ return new HidesTester(elements, collectElements(typeElements));
+ }
+
+ /** Scans a test compilation for elements to use as test inputs. */
+ private ImmutableMap<String, Element> collectElements(List<? extends TypeElement> typeElements) {
+ Map<String, Element> elements = new HashMap<>();
+ for (TypeElement typeElement : typeElements) {
+ elements.put(key(typeElement), typeElement);
+ new ElementScanner8<Void, Void>() {
+ @Override
+ public Void scan(Element e, Void unused) {
+ Element p = elements.put(key(e), e);
+ if (p != null && !e.equals(p) && !p.getKind().equals(ElementKind.CONSTRUCTOR)) {
+ throw new AssertionError(key(e) + " " + p + " " + e);
+ }
+ return super.scan(e, unused);
+ }
+ }.visit(typeElement);
+ }
+ return ImmutableMap.copyOf(elements);
+ }
+
+ /** A unique string representation of an element. */
+ private static String key(Element e) {
+ ArrayDeque<Name> names = new ArrayDeque<>();
+ Element curr = e;
+ do {
+ if (curr.getSimpleName().length() > 0) {
+ names.addFirst(curr.getSimpleName());
+ }
+ curr = curr.getEnclosingElement();
+ } while (curr != null);
+ String key = e.getKind() + ":" + Joiner.on('.').join(names);
+ if (e.getKind().equals(ElementKind.METHOD)) {
+ key += ":" + e.asType();
+ }
+ return key;
+ }
+}