1/*
2 * Copyright 2016 Google Inc. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.turbine.lower;
18
19import static com.google.common.truth.Truth.assertThat;
20import static java.nio.charset.StandardCharsets.UTF_8;
21
22import com.google.common.base.Joiner;
23import com.google.common.collect.ImmutableList;
24import com.google.common.collect.ImmutableMap;
25import com.google.common.io.ByteStreams;
26import com.google.turbine.binder.Binder;
27import com.google.turbine.binder.Binder.BindingResult;
28import com.google.turbine.binder.ClassPathBinder;
29import com.google.turbine.binder.bound.SourceTypeBoundClass;
30import com.google.turbine.binder.bytecode.BytecodeBoundClass;
31import com.google.turbine.binder.env.CompoundEnv;
32import com.google.turbine.binder.env.SimpleEnv;
33import com.google.turbine.binder.lookup.TopLevelIndex;
34import com.google.turbine.binder.sym.ClassSymbol;
35import com.google.turbine.binder.sym.FieldSymbol;
36import com.google.turbine.binder.sym.MethodSymbol;
37import com.google.turbine.binder.sym.TyVarSymbol;
38import com.google.turbine.bytecode.AsmUtils;
39import com.google.turbine.bytecode.ByteReader;
40import com.google.turbine.bytecode.ConstantPoolReader;
41import com.google.turbine.model.TurbineConstantTypeKind;
42import com.google.turbine.model.TurbineFlag;
43import com.google.turbine.model.TurbineTyKind;
44import com.google.turbine.parse.Parser;
45import com.google.turbine.type.Type;
46import java.io.IOException;
47import java.io.OutputStream;
48import java.nio.file.Files;
49import java.nio.file.Path;
50import java.nio.file.Paths;
51import java.util.ArrayList;
52import java.util.LinkedHashMap;
53import java.util.List;
54import java.util.Map;
55import java.util.jar.JarEntry;
56import java.util.jar.JarOutputStream;
57import org.junit.Rule;
58import org.junit.Test;
59import org.junit.rules.TemporaryFolder;
60import org.junit.runner.RunWith;
61import org.junit.runners.JUnit4;
62import org.objectweb.asm.AnnotationVisitor;
63import org.objectweb.asm.ClassReader;
64import org.objectweb.asm.ClassVisitor;
65import org.objectweb.asm.ClassWriter;
66import org.objectweb.asm.FieldVisitor;
67import org.objectweb.asm.Opcodes;
68import org.objectweb.asm.TypePath;
69
70@RunWith(JUnit4.class)
71public class LowerTest {
72
73  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
74
75  private static final ImmutableList<Path> BOOTCLASSPATH =
76      ImmutableList.of(Paths.get(System.getProperty("java.home")).resolve("lib/rt.jar"));
77
78  @Test
79  public void hello() throws Exception {
80    CompoundEnv<ClassSymbol, BytecodeBoundClass> classpath =
81        ClassPathBinder.bind(ImmutableList.of(), BOOTCLASSPATH, TopLevelIndex.builder());
82
83    ImmutableList<Type.ClassTy> interfaceTypes =
84        ImmutableList.of(
85            new Type.ClassTy(
86                ImmutableList.of(
87                    new Type.ClassTy.SimpleClassTy(
88                        new ClassSymbol("java/util/List"),
89                        ImmutableList.of(
90                            new Type.TyVar(
91                                new TyVarSymbol(new ClassSymbol("test/Test"), "V"),
92                                ImmutableList.of())),
93                        ImmutableList.of()))));
94    Type.ClassTy xtnds = Type.ClassTy.OBJECT;
95    ImmutableMap<TyVarSymbol, SourceTypeBoundClass.TyVarInfo> tps =
96        ImmutableMap.of(
97            new TyVarSymbol(new ClassSymbol("test/Test"), "V"),
98            new SourceTypeBoundClass.TyVarInfo(
99                new Type.ClassTy(
100                    ImmutableList.of(
101                        new Type.ClassTy.SimpleClassTy(
102                            new ClassSymbol("test/Test$Inner"),
103                            ImmutableList.of(),
104                            ImmutableList.of()))),
105                ImmutableList.of(),
106                ImmutableList.of()));
107    int access = TurbineFlag.ACC_SUPER | TurbineFlag.ACC_PUBLIC;
108    ImmutableList<SourceTypeBoundClass.MethodInfo> methods =
109        ImmutableList.of(
110            new SourceTypeBoundClass.MethodInfo(
111                new MethodSymbol(new ClassSymbol("test/Test"), "f"),
112                ImmutableMap.of(),
113                new Type.PrimTy(TurbineConstantTypeKind.INT, ImmutableList.of()),
114                ImmutableList.of(),
115                ImmutableList.of(),
116                TurbineFlag.ACC_STATIC | TurbineFlag.ACC_PUBLIC,
117                null,
118                null,
119                ImmutableList.of(),
120                null),
121            new SourceTypeBoundClass.MethodInfo(
122                new MethodSymbol(new ClassSymbol("test/Test"), "g"),
123                ImmutableMap.of(
124                    new TyVarSymbol(new MethodSymbol(new ClassSymbol("test/Test"), "g"), "V"),
125                    new SourceTypeBoundClass.TyVarInfo(
126                        null,
127                        ImmutableList.of(
128                            new Type.ClassTy(
129                                ImmutableList.of(
130                                    new Type.ClassTy.SimpleClassTy(
131                                        new ClassSymbol("java/lang/Runnable"),
132                                        ImmutableList.of(),
133                                        ImmutableList.of())))),
134                        ImmutableList.of()),
135                    new TyVarSymbol(new MethodSymbol(new ClassSymbol("test/Test"), "g"), "E"),
136                    new SourceTypeBoundClass.TyVarInfo(
137                        new Type.ClassTy(
138                            ImmutableList.of(
139                                new Type.ClassTy.SimpleClassTy(
140                                    new ClassSymbol("java/lang/Error"),
141                                    ImmutableList.of(),
142                                    ImmutableList.of()))),
143                        ImmutableList.of(),
144                        ImmutableList.of())),
145                Type.VOID,
146                ImmutableList.of(
147                    new SourceTypeBoundClass.ParamInfo(
148                        new Type.PrimTy(TurbineConstantTypeKind.INT, ImmutableList.of()),
149                        "foo",
150                        ImmutableList.of(),
151                        0)),
152                ImmutableList.of(
153                    new Type.TyVar(
154                        new TyVarSymbol(new MethodSymbol(new ClassSymbol("test/Test"), "g"), "E"),
155                        ImmutableList.of())),
156                TurbineFlag.ACC_PUBLIC,
157                null,
158                null,
159                ImmutableList.of(),
160                null));
161    ImmutableList<SourceTypeBoundClass.FieldInfo> fields =
162        ImmutableList.of(
163            new SourceTypeBoundClass.FieldInfo(
164                new FieldSymbol(new ClassSymbol("test/Test"), "theField"),
165                Type.ClassTy.asNonParametricClassTy(new ClassSymbol("test/Test$Inner")),
166                TurbineFlag.ACC_STATIC | TurbineFlag.ACC_FINAL | TurbineFlag.ACC_PUBLIC,
167                ImmutableList.of(),
168                null,
169                null));
170    ClassSymbol owner = null;
171    TurbineTyKind kind = TurbineTyKind.CLASS;
172    ImmutableMap<String, ClassSymbol> children = ImmutableMap.of();
173    ImmutableMap<String, TyVarSymbol> tyParams =
174        ImmutableMap.of("V", new TyVarSymbol(new ClassSymbol("test/Test"), "V"));
175
176    SourceTypeBoundClass c =
177        new SourceTypeBoundClass(
178            interfaceTypes,
179            xtnds,
180            tps,
181            access,
182            methods,
183            fields,
184            owner,
185            kind,
186            children,
187            tyParams,
188            null,
189            null,
190            null,
191            null,
192            ImmutableList.of(),
193            null);
194
195    SourceTypeBoundClass i =
196        new SourceTypeBoundClass(
197            ImmutableList.of(),
198            Type.ClassTy.OBJECT,
199            ImmutableMap.of(),
200            TurbineFlag.ACC_STATIC | TurbineFlag.ACC_PROTECTED,
201            ImmutableList.of(),
202            ImmutableList.of(),
203            new ClassSymbol("test/Test"),
204            TurbineTyKind.CLASS,
205            ImmutableMap.of("Inner", new ClassSymbol("test/Test$Inner")),
206            ImmutableMap.of(),
207            null,
208            null,
209            null,
210            null,
211            ImmutableList.of(),
212            null);
213
214    SimpleEnv.Builder<ClassSymbol, SourceTypeBoundClass> b = SimpleEnv.builder();
215    b.put(new ClassSymbol("test/Test"), c);
216    b.put(new ClassSymbol("test/Test$Inner"), i);
217
218    Map<String, byte[]> bytes =
219        Lower.lowerAll(
220                ImmutableMap.of(
221                    new ClassSymbol("test/Test"), c, new ClassSymbol("test/Test$Inner"), i),
222                classpath)
223            .bytes();
224
225    assertThat(AsmUtils.textify(bytes.get("test/Test")))
226        .isEqualTo(
227            new String(
228                ByteStreams.toByteArray(
229                    LowerTest.class.getResourceAsStream("testdata/golden/outer.txt")),
230                UTF_8));
231    assertThat(AsmUtils.textify(bytes.get("test/Test$Inner")))
232        .isEqualTo(
233            new String(
234                ByteStreams.toByteArray(
235                    LowerTest.class.getResourceAsStream("testdata/golden/inner.txt")),
236                UTF_8));
237  }
238
239  @Test
240  public void innerClassAttributeOrder() throws IOException {
241    BindingResult bound =
242        Binder.bind(
243            ImmutableList.of(
244                Parser.parse(
245                    Joiner.on('\n')
246                        .join(
247                            "class Test {", //
248                            "  class Inner {",
249                            "    class InnerMost {}",
250                            "  }",
251                            "}"))),
252            ImmutableList.of(),
253            BOOTCLASSPATH);
254    Map<String, byte[]> lowered = Lower.lowerAll(bound.units(), bound.classPathEnv()).bytes();
255    List<String> attributes = new ArrayList<>();
256    new ClassReader(lowered.get("Test$Inner$InnerMost"))
257        .accept(
258            new ClassVisitor(Opcodes.ASM5) {
259              @Override
260              public void visitInnerClass(
261                  String name, String outerName, String innerName, int access) {
262                attributes.add(String.format("%s %s %s", name, outerName, innerName));
263              }
264            },
265            0);
266    assertThat(attributes)
267        .containsExactly("Test$Inner Test Inner", "Test$Inner$InnerMost Test$Inner InnerMost")
268        .inOrder();
269  }
270
271  @Test
272  public void wildArrayElement() throws Exception {
273    IntegrationTestSupport.TestInput input =
274        IntegrationTestSupport.TestInput.parse(
275            new String(
276                ByteStreams.toByteArray(
277                    getClass().getResourceAsStream("testdata/canon_array.test")),
278                UTF_8));
279
280    Map<String, byte[]> actual =
281        IntegrationTestSupport.runTurbine(input.sources, ImmutableList.of(), BOOTCLASSPATH);
282
283    ByteReader reader = new ByteReader(actual.get("Test"), 0);
284    assertThat(reader.u4()).isEqualTo(0xcafebabe); // magic
285    assertThat(reader.u2()).isEqualTo(0); // minor
286    assertThat(reader.u2()).isEqualTo(52); // major
287    ConstantPoolReader pool = ConstantPoolReader.readConstantPool(reader);
288    assertThat(reader.u2()).isEqualTo(TurbineFlag.ACC_SUPER); // access
289    assertThat(pool.classInfo(reader.u2())).isEqualTo("Test"); // this
290    assertThat(pool.classInfo(reader.u2())).isEqualTo("java/lang/Object"); // super
291    assertThat(reader.u2()).isEqualTo(0); // interfaces
292    assertThat(reader.u2()).isEqualTo(1); // field count
293    assertThat(reader.u2()).isEqualTo(0); // access
294    assertThat(pool.utf8(reader.u2())).isEqualTo("i"); // name
295    assertThat(pool.utf8(reader.u2())).isEqualTo("LA$I;"); // descriptor
296    int attributesCount = reader.u2();
297    String signature = null;
298    for (int j = 0; j < attributesCount; j++) {
299      String attributeName = pool.utf8(reader.u2());
300      switch (attributeName) {
301        case "Signature":
302          reader.u4(); // length
303          signature = pool.utf8(reader.u2());
304          break;
305        default:
306          reader.skip(reader.u4());
307          break;
308      }
309    }
310    assertThat(signature).isEqualTo("LA<[*>.I;");
311  }
312
313  @Test
314  public void typePath() throws Exception {
315    BindingResult bound =
316        Binder.bind(
317            ImmutableList.of(
318                Parser.parse(
319                    Joiner.on('\n')
320                        .join(
321                            "import java.lang.annotation.ElementType;",
322                            "import java.lang.annotation.Target;",
323                            "import java.util.List;",
324                            "@Target({ElementType.TYPE_USE}) @interface Anno {}",
325                            "class Test {",
326                            "  public @Anno int[][] xs;",
327                            "}"))),
328            ImmutableList.of(),
329            BOOTCLASSPATH);
330    Map<String, byte[]> lowered = Lower.lowerAll(bound.units(), bound.classPathEnv()).bytes();
331    TypePath[] path = new TypePath[1];
332    new ClassReader(lowered.get("Test"))
333        .accept(
334            new ClassVisitor(Opcodes.ASM5) {
335              @Override
336              public FieldVisitor visitField(
337                  int access, String name, String desc, String signature, Object value) {
338                return new FieldVisitor(Opcodes.ASM5) {
339                  @Override
340                  public AnnotationVisitor visitTypeAnnotation(
341                      int typeRef, TypePath typePath, String desc, boolean visible) {
342                    path[0] = typePath;
343                    return null;
344                  };
345                };
346              }
347            },
348            0);
349    assertThat(path[0].getLength()).isEqualTo(2);
350    assertThat(path[0].getStep(0)).isEqualTo(TypePath.ARRAY_ELEMENT);
351    assertThat(path[0].getStepArgument(0)).isEqualTo(0);
352    assertThat(path[0].getStep(1)).isEqualTo(TypePath.ARRAY_ELEMENT);
353    assertThat(path[0].getStepArgument(1)).isEqualTo(0);
354  }
355
356  @Test
357  public void invalidConstants() throws Exception {
358    Path lib = temporaryFolder.newFile("lib.jar").toPath();
359    try (OutputStream os = Files.newOutputStream(lib);
360        JarOutputStream jos = new JarOutputStream(os)) {
361      jos.putNextEntry(new JarEntry("Lib.class"));
362
363      ClassWriter cw = new ClassWriter(0);
364      cw.visit(52, Opcodes.ACC_SUPER, "Lib", null, "java/lang/Object", null);
365      cw.visitField(Opcodes.ACC_FINAL | Opcodes.ACC_STATIC, "ZCONST", "Z", null, Integer.MAX_VALUE);
366      cw.visitField(Opcodes.ACC_FINAL | Opcodes.ACC_STATIC, "SCONST", "S", null, Integer.MAX_VALUE);
367      jos.write(cw.toByteArray());
368    }
369
370    ImmutableMap<String, String> input =
371        ImmutableMap.of(
372            "Test.java",
373            Joiner.on('\n')
374                .join(
375                    "class Test {",
376                    "  static final short SCONST = Lib.SCONST + 0;",
377                    "  static final boolean ZCONST = Lib.ZCONST || false;",
378                    "}"));
379
380    Map<String, byte[]> actual =
381        IntegrationTestSupport.runTurbine(input, ImmutableList.of(lib), BOOTCLASSPATH);
382
383    Map<String, Object> values = new LinkedHashMap<>();
384    new ClassReader(actual.get("Test"))
385        .accept(
386            new ClassVisitor(Opcodes.ASM5) {
387              @Override
388              public FieldVisitor visitField(
389                  int access, String name, String desc, String signature, Object value) {
390                values.put(name, value);
391                return super.visitField(access, name, desc, signature, value);
392              }
393            },
394            0);
395
396    assertThat(values).containsEntry("SCONST", -1);
397    assertThat(values).containsEntry("ZCONST", 1);
398  }
399
400  @Test
401  public void deprecated() throws Exception {
402    BindingResult bound =
403        Binder.bind(
404            ImmutableList.of(Parser.parse("@Deprecated class Test {}")),
405            ImmutableList.of(),
406            BOOTCLASSPATH);
407    Map<String, byte[]> lowered = Lower.lowerAll(bound.units(), bound.classPathEnv()).bytes();
408    int[] acc = {0};
409    new ClassReader(lowered.get("Test"))
410        .accept(
411            new ClassVisitor(Opcodes.ASM5) {
412              @Override
413              public void visit(
414                  int version,
415                  int access,
416                  String name,
417                  String signature,
418                  String superName,
419                  String[] interfaces) {
420                acc[0] = access;
421              }
422            },
423            0);
424    assertThat((acc[0] & Opcodes.ACC_DEPRECATED) == Opcodes.ACC_DEPRECATED).isTrue();
425  }
426
427  @Test
428  public void lazyImports() throws Exception {
429    ImmutableMap<String, String> sources =
430        ImmutableMap.<String, String>builder()
431            .put(
432                "b/B.java",
433                lines(
434                    "package b;", //
435                    "public class B {",
436                    "  public static class A {",
437                    "    public static final int X = 0;",
438                    "  }",
439                    "  public static class C {}",
440                    "}"))
441            .put(
442                "anno/Anno.java",
443                lines(
444                    "package anno;", //
445                    "public @interface Anno {",
446                    "  int value() default 0;",
447                    "}"))
448            .put(
449                "a/A.java",
450                lines(
451                    "package a;", //
452                    "import b.B;",
453                    "import anno.Anno;",
454                    "import static b.B.nosuch.A;",
455                    "@Anno(A.X)",
456                    "public class A extends B {",
457                    "  public A a;",
458                    "  public static final int X = 1;",
459                    "}"))
460            .put(
461                "a/C.java",
462                lines(
463                    "package c;", //
464                    "import static b.B.nosuch.C;",
465                    "class C {",
466                    "  C c;",
467                    "}"))
468            .build();
469
470    ImmutableMap<String, String> noImports;
471    {
472      ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
473      sources.forEach(
474          (k, v) -> builder.put(k, v.replaceAll("import static b\\.B\\.nosuch\\..*;", "")));
475      noImports = builder.build();
476    }
477
478    Map<String, byte[]> expected =
479        IntegrationTestSupport.runJavac(noImports, ImmutableList.of(), BOOTCLASSPATH);
480    Map<String, byte[]> actual =
481        IntegrationTestSupport.runTurbine(sources, ImmutableList.of(), BOOTCLASSPATH);
482    assertThat(IntegrationTestSupport.dump(IntegrationTestSupport.sortMembers(actual)))
483        .isEqualTo(IntegrationTestSupport.dump(IntegrationTestSupport.canonicalize(expected)));
484  }
485
486  static String lines(String... lines) {
487    return Joiner.on("\n").join(lines);
488  }
489}
490