diff --git a/java/com/google/turbine/processing/TurbineElements.java b/java/com/google/turbine/processing/TurbineElements.java index 42b5d491..b5fd7f4f 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 @@ public List getAllAnnotationMirrors(Element element) @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 00000000..69418d51 --- /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 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 inputs; + + HidesTester(Elements elements, ImmutableMap inputs) { + this.elements = elements; + this.inputs = inputs; + } + + boolean test(String k1, String k2) { + return elements.hides(inputs.get(k1), inputs.get(k2)); + } + + public ImmutableSet keys() { + return inputs.keySet(); + } + } + + /** Compiles the test input with turbine. */ + private HidesTester runTurbine() throws IOException { + ImmutableList 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 env = + CompoundEnv.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 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 diagnostics = new DiagnosticCollector<>(); + JavacTask javacTask = + IntegrationTestSupport.runJavacAnalysis( + input.sources, ImmutableList.of(), ImmutableList.of(), diagnostics); + List 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 collectElements(List typeElements) { + Map elements = new HashMap<>(); + for (TypeElement typeElement : typeElements) { + elements.put(key(typeElement), typeElement); + new ElementScanner8() { + @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 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; + } +}