/* * Copyright 2016 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.binder; import static com.google.common.truth.Truth.assertThat; import static com.google.turbine.testing.TestClassPaths.TURBINE_BOOTCLASSPATH; import static java.util.Objects.requireNonNull; import static org.junit.Assert.assertThrows; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.turbine.binder.bound.SourceTypeBoundClass; import com.google.turbine.binder.sym.ClassSymbol; import com.google.turbine.diag.TurbineError; import com.google.turbine.lower.IntegrationTestSupport; import com.google.turbine.model.TurbineElementType; import com.google.turbine.model.TurbineFlag; import com.google.turbine.parse.Parser; import com.google.turbine.tree.Tree; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Optional; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class BinderTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void hello() throws Exception { ImmutableList units = ImmutableList.of( parseLines( "package a;", // "public class A {", " public class Inner1 extends b.B {", " }", " public class Inner2 extends A.Inner1 {", " }", "}"), parseLines( "package b;", // "import a.A;", "public class B extends A {", "}")); ImmutableMap bound = Binder.bind( units, ClassPathBinder.bindClasspath(ImmutableList.of()), TURBINE_BOOTCLASSPATH, /* moduleVersion=*/ Optional.empty()) .units(); assertThat(bound.keySet()) .containsExactly( new ClassSymbol("a/A"), new ClassSymbol("a/A$Inner1"), new ClassSymbol("a/A$Inner2"), new ClassSymbol("b/B")); SourceTypeBoundClass a = getBoundClass(bound, "a/A"); assertThat(a.superclass()).isEqualTo(new ClassSymbol("java/lang/Object")); assertThat(a.interfaces()).isEmpty(); assertThat(getBoundClass(bound, "a/A$Inner1").superclass()).isEqualTo(new ClassSymbol("b/B")); assertThat(getBoundClass(bound, "a/A$Inner2").superclass()) .isEqualTo(new ClassSymbol("a/A$Inner1")); SourceTypeBoundClass b = getBoundClass(bound, "b/B"); assertThat(b.superclass()).isEqualTo(new ClassSymbol("a/A")); } @Test public void interfaces() throws Exception { ImmutableList units = ImmutableList.of( parseLines( "package com.i;", // "public interface I {", " public class IInner {", " }", "}"), parseLines( "package b;", // "class B implements com.i.I {", " class BInner extends IInner {}", "}")); ImmutableMap bound = Binder.bind( units, ClassPathBinder.bindClasspath(ImmutableList.of()), TURBINE_BOOTCLASSPATH, /* moduleVersion=*/ Optional.empty()) .units(); assertThat(bound.keySet()) .containsExactly( new ClassSymbol("com/i/I"), new ClassSymbol("com/i/I$IInner"), new ClassSymbol("b/B"), new ClassSymbol("b/B$BInner")); assertThat(getBoundClass(bound, "b/B").interfaces()) .containsExactly(new ClassSymbol("com/i/I")); assertThat(getBoundClass(bound, "b/B$BInner").superclass()) .isEqualTo(new ClassSymbol("com/i/I$IInner")); assertThat(getBoundClass(bound, "b/B$BInner").interfaces()).isEmpty(); } @Test public void imports() throws Exception { ImmutableList units = ImmutableList.of( parseLines( "package com.test;", // "public class Test {", " public static class Inner {}", "}"), parseLines( "package other;", // "import com.test.Test.Inner;", "import no.such.Class;", // imports are resolved lazily on-demand "public class Foo extends Inner {", "}")); ImmutableMap bound = Binder.bind( units, ClassPathBinder.bindClasspath(ImmutableList.of()), TURBINE_BOOTCLASSPATH, /* moduleVersion=*/ Optional.empty()) .units(); assertThat(getBoundClass(bound, "other/Foo").superclass()) .isEqualTo(new ClassSymbol("com/test/Test$Inner")); } @Test public void cycle() throws Exception { ImmutableList units = ImmutableList.of( parseLines( "package a;", // "import b.B;", "public class A extends B.Inner {", " class Inner {}", "}"), parseLines( "package b;", // "import a.A;", "public class B extends A.Inner {", " class Inner {}", "}")); TurbineError e = assertThrows( TurbineError.class, () -> Binder.bind( units, ClassPathBinder.bindClasspath(ImmutableList.of()), TURBINE_BOOTCLASSPATH, /* moduleVersion=*/ Optional.empty())); assertThat(e).hasMessageThat().contains("cycle in class hierarchy: a.A -> b.B -> a.A"); } @Test public void annotationDeclaration() throws Exception { ImmutableList units = ImmutableList.of( parseLines( "package com.test;", // "public @interface Annotation {", "}")); ImmutableMap bound = Binder.bind( units, ClassPathBinder.bindClasspath(ImmutableList.of()), TURBINE_BOOTCLASSPATH, /* moduleVersion=*/ Optional.empty()) .units(); SourceTypeBoundClass a = getBoundClass(bound, "com/test/Annotation"); assertThat(a.access()) .isEqualTo( TurbineFlag.ACC_PUBLIC | TurbineFlag.ACC_INTERFACE | TurbineFlag.ACC_ABSTRACT | TurbineFlag.ACC_ANNOTATION); assertThat(a.superclass()).isEqualTo(new ClassSymbol("java/lang/Object")); assertThat(a.interfaces()).containsExactly(new ClassSymbol("java/lang/annotation/Annotation")); } @Test public void helloBytecode() throws Exception { ImmutableList units = ImmutableList.of( parseLines( "package a;", // "import java.util.Map.Entry;", "public class A implements Entry {", "}")); ImmutableMap bound = Binder.bind( units, ClassPathBinder.bindClasspath(ImmutableList.of()), TURBINE_BOOTCLASSPATH, /* moduleVersion=*/ Optional.empty()) .units(); SourceTypeBoundClass a = getBoundClass(bound, "a/A"); assertThat(a.interfaces()).containsExactly(new ClassSymbol("java/util/Map$Entry")); } @Test public void incompleteClasspath() throws Exception { Map lib = IntegrationTestSupport.runJavac( ImmutableMap.of( "A.java", "class A {}", "B.java", "class B extends A {}"), ImmutableList.of()); // create a jar containing only B Path libJar = temporaryFolder.newFile("lib.jar").toPath(); try (OutputStream os = Files.newOutputStream(libJar); JarOutputStream jos = new JarOutputStream(os)) { jos.putNextEntry(new JarEntry("B.class")); jos.write(requireNonNull(lib.get("B"))); } ImmutableList units = ImmutableList.of( parseLines( "import java.lang.annotation.Target;", "import java.lang.annotation.ElementType;", "public class C implements B {", " @Target(ElementType.TYPE_USE)", " @interface A {};", "}")); ImmutableMap bound = Binder.bind( units, ClassPathBinder.bindClasspath(ImmutableList.of(libJar)), TURBINE_BOOTCLASSPATH, /* moduleVersion=*/ Optional.empty()) .units(); SourceTypeBoundClass a = getBoundClass(bound, "C$A"); assertThat(a.annotationMetadata().target()).containsExactly(TurbineElementType.TYPE_USE); } private Tree.CompUnit parseLines(String... lines) { return Parser.parse(Joiner.on('\n').join(lines)); } private static SourceTypeBoundClass getBoundClass( Map bound, String name) { // requireNonNull is safe as long as we call this method with classes that exist in our sources. return requireNonNull(bound.get(new ClassSymbol(name))); } }