Home | History | Annotate | Download | only in lower
      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 
     17 package com.google.turbine.lower;
     18 
     19 import static com.google.common.truth.Truth.assertThat;
     20 import static java.nio.charset.StandardCharsets.UTF_8;
     21 import static java.util.stream.Collectors.toCollection;
     22 import static java.util.stream.Collectors.toList;
     23 
     24 import com.google.common.base.Joiner;
     25 import com.google.common.base.Splitter;
     26 import com.google.common.collect.ImmutableList;
     27 import com.google.common.jimfs.Configuration;
     28 import com.google.common.jimfs.Jimfs;
     29 import com.google.turbine.binder.Binder;
     30 import com.google.turbine.bytecode.AsmUtils;
     31 import com.google.turbine.diag.SourceFile;
     32 import com.google.turbine.parse.Parser;
     33 import com.google.turbine.tree.Tree;
     34 import com.sun.source.util.JavacTask;
     35 import com.sun.tools.javac.api.JavacTool;
     36 import com.sun.tools.javac.file.JavacFileManager;
     37 import com.sun.tools.javac.util.Context;
     38 import java.io.BufferedWriter;
     39 import java.io.IOException;
     40 import java.io.OutputStreamWriter;
     41 import java.io.PrintWriter;
     42 import java.nio.file.FileSystem;
     43 import java.nio.file.FileVisitResult;
     44 import java.nio.file.Files;
     45 import java.nio.file.Path;
     46 import java.nio.file.SimpleFileVisitor;
     47 import java.nio.file.attribute.BasicFileAttributes;
     48 import java.util.ArrayDeque;
     49 import java.util.ArrayList;
     50 import java.util.Collection;
     51 import java.util.Collections;
     52 import java.util.Comparator;
     53 import java.util.Deque;
     54 import java.util.HashMap;
     55 import java.util.HashSet;
     56 import java.util.LinkedHashMap;
     57 import java.util.List;
     58 import java.util.Map;
     59 import java.util.Set;
     60 import javax.tools.DiagnosticCollector;
     61 import javax.tools.JavaFileObject;
     62 import javax.tools.StandardLocation;
     63 import org.objectweb.asm.ClassReader;
     64 import org.objectweb.asm.ClassWriter;
     65 import org.objectweb.asm.Opcodes;
     66 import org.objectweb.asm.Type;
     67 import org.objectweb.asm.signature.SignatureReader;
     68 import org.objectweb.asm.signature.SignatureVisitor;
     69 import org.objectweb.asm.tree.AnnotationNode;
     70 import org.objectweb.asm.tree.ClassNode;
     71 import org.objectweb.asm.tree.FieldNode;
     72 import org.objectweb.asm.tree.InnerClassNode;
     73 import org.objectweb.asm.tree.MethodNode;
     74 import org.objectweb.asm.tree.TypeAnnotationNode;
     75 
     76 /** Support for bytecode diffing-integration tests. */
     77 public class IntegrationTestSupport {
     78 
     79   /**
     80    * Normalizes order of members, attributes, and constant pool entries, to allow diffing bytecode.
     81    */
     82   public static Map<String, byte[]> sortMembers(Map<String, byte[]> in) {
     83     List<ClassNode> classes = toClassNodes(in);
     84     for (ClassNode n : classes) {
     85       sortAttributes(n);
     86     }
     87     return toByteCode(classes);
     88   }
     89 
     90   /**
     91    * Canonicalizes bytecode produced by javac to match the expected output of turbine. Includes the
     92    * same normalization as {@link #sortMembers}, as well as removing everything not produced by the
     93    * header compiler (code, debug info, etc.)
     94    */
     95   public static Map<String, byte[]> canonicalize(Map<String, byte[]> in) {
     96     List<ClassNode> classes = toClassNodes(in);
     97 
     98     // drop anonymous classes
     99     classes = classes.stream().filter(n -> !isAnonymous(n)).collect(toCollection(ArrayList::new));
    100 
    101     // collect all inner classes attributes
    102     Map<String, InnerClassNode> infos = new HashMap<>();
    103     for (ClassNode n : classes) {
    104       for (InnerClassNode innerClassNode : n.innerClasses) {
    105         infos.put(innerClassNode.name, innerClassNode);
    106       }
    107     }
    108 
    109     HashSet<String> all = classes.stream().map(n -> n.name).collect(toCollection(HashSet::new));
    110     for (ClassNode n : classes) {
    111       removeImplementation(n);
    112       removeUnusedInnerClassAttributes(infos, n);
    113       makeEnumsFinal(all, n);
    114       sortAttributes(n);
    115       undeprecate(n);
    116     }
    117 
    118     return toByteCode(classes);
    119   }
    120 
    121   private static boolean isAnonymous(ClassNode n) {
    122     // JVMS 4.7.6: if C is anonymous, the value of the inner_name_index item must be zero
    123     return n.innerClasses.stream().anyMatch(i -> i.name.equals(n.name) && i.innerName == null);
    124   }
    125 
    126   // ASM sets ACC_DEPRECATED for elements with the Deprecated attribute;
    127   // unset it if the @Deprecated annotation is not also present.
    128   // This can happen if the @deprecated javadoc tag was present but the
    129   // annotation wasn't.
    130   private static void undeprecate(ClassNode n) {
    131     if (!isDeprecated(n.visibleAnnotations)) {
    132       n.access &= ~Opcodes.ACC_DEPRECATED;
    133     }
    134     n.methods
    135         .stream()
    136         .filter(m -> !isDeprecated(m.visibleAnnotations))
    137         .forEach(m -> m.access &= ~Opcodes.ACC_DEPRECATED);
    138     n.fields
    139         .stream()
    140         .filter(f -> !isDeprecated(f.visibleAnnotations))
    141         .forEach(f -> f.access &= ~Opcodes.ACC_DEPRECATED);
    142   }
    143 
    144   private static boolean isDeprecated(List<AnnotationNode> visibleAnnotations) {
    145     return visibleAnnotations != null
    146         && visibleAnnotations.stream().anyMatch(a -> a.desc.equals("Ljava/lang/Deprecated;"));
    147   }
    148 
    149   private static void makeEnumsFinal(Set<String> all, ClassNode n) {
    150     n.innerClasses.forEach(
    151         x -> {
    152           if (all.contains(x.name) && (x.access & Opcodes.ACC_ENUM) == Opcodes.ACC_ENUM) {
    153             x.access &= ~Opcodes.ACC_ABSTRACT;
    154             x.access |= Opcodes.ACC_FINAL;
    155           }
    156         });
    157     if ((n.access & Opcodes.ACC_ENUM) == Opcodes.ACC_ENUM) {
    158       n.access &= ~Opcodes.ACC_ABSTRACT;
    159       n.access |= Opcodes.ACC_FINAL;
    160     }
    161   }
    162 
    163   private static Map<String, byte[]> toByteCode(List<ClassNode> classes) {
    164     Map<String, byte[]> out = new LinkedHashMap<>();
    165     for (ClassNode n : classes) {
    166       ClassWriter cw = new ClassWriter(0);
    167       n.accept(cw);
    168       out.put(n.name, cw.toByteArray());
    169     }
    170     return out;
    171   }
    172 
    173   private static List<ClassNode> toClassNodes(Map<String, byte[]> in) {
    174     List<ClassNode> classes = new ArrayList<>();
    175     for (byte[] f : in.values()) {
    176       ClassNode n = new ClassNode();
    177       new ClassReader(f).accept(n, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
    178 
    179       classes.add(n);
    180     }
    181     return classes;
    182   }
    183 
    184   /** Remove elements that are omitted by turbine, e.g. private and synthetic members. */
    185   private static void removeImplementation(ClassNode n) {
    186     n.innerClasses =
    187         n.innerClasses
    188             .stream()
    189             .filter(x -> (x.access & Opcodes.ACC_SYNTHETIC) == 0 && x.innerName != null)
    190             .collect(toList());
    191 
    192     n.methods =
    193         n.methods
    194             .stream()
    195             .filter(x -> (x.access & (Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PRIVATE)) == 0)
    196             .filter(x -> !x.name.equals("<clinit>"))
    197             .collect(toList());
    198 
    199     n.fields =
    200         n.fields
    201             .stream()
    202             .filter(x -> (x.access & (Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PRIVATE)) == 0)
    203             .collect(toList());
    204   }
    205 
    206   /** Apply a standard sort order to attributes. */
    207   private static void sortAttributes(ClassNode n) {
    208 
    209     n.innerClasses.sort(
    210         Comparator.comparing((InnerClassNode x) -> x.name)
    211             .thenComparing(x -> x.outerName)
    212             .thenComparing(x -> x.innerName)
    213             .thenComparing(x -> x.access));
    214 
    215     sortAnnotations(n.visibleAnnotations);
    216     sortAnnotations(n.invisibleAnnotations);
    217     sortTypeAnnotations(n.visibleTypeAnnotations);
    218     sortTypeAnnotations(n.invisibleTypeAnnotations);
    219 
    220     for (MethodNode m : n.methods) {
    221       sortParameterAnnotations(m.visibleParameterAnnotations);
    222       sortParameterAnnotations(m.invisibleParameterAnnotations);
    223 
    224       sortAnnotations(m.visibleAnnotations);
    225       sortAnnotations(m.invisibleAnnotations);
    226       sortTypeAnnotations(m.visibleTypeAnnotations);
    227       sortTypeAnnotations(m.invisibleTypeAnnotations);
    228     }
    229 
    230     for (FieldNode f : n.fields) {
    231       sortAnnotations(f.visibleAnnotations);
    232       sortAnnotations(f.invisibleAnnotations);
    233 
    234       sortAnnotations(f.visibleAnnotations);
    235       sortAnnotations(f.invisibleAnnotations);
    236       sortTypeAnnotations(f.visibleTypeAnnotations);
    237       sortTypeAnnotations(f.invisibleTypeAnnotations);
    238     }
    239   }
    240 
    241   private static void sortParameterAnnotations(List<AnnotationNode>[] parameters) {
    242     if (parameters == null) {
    243       return;
    244     }
    245     for (List<AnnotationNode> annos : parameters) {
    246       sortAnnotations(annos);
    247     }
    248   }
    249 
    250   private static void sortTypeAnnotations(List<TypeAnnotationNode> annos) {
    251     if (annos == null) {
    252       return;
    253     }
    254     annos.sort(
    255         Comparator.comparing((TypeAnnotationNode a) -> a.desc)
    256             .thenComparing(a -> String.valueOf(a.typeRef))
    257             .thenComparing(a -> String.valueOf(a.typePath))
    258             .thenComparing(a -> String.valueOf(a.values)));
    259   }
    260 
    261   private static void sortAnnotations(List<AnnotationNode> annos) {
    262     if (annos == null) {
    263       return;
    264     }
    265     annos.sort(
    266         Comparator.comparing((AnnotationNode a) -> a.desc)
    267             .thenComparing(a -> String.valueOf(a.values)));
    268   }
    269 
    270   /**
    271    * Remove InnerClass attributes that are no longer needed after member pruning. This requires
    272    * visiting all descriptors and signatures in the bytecode to find references to inner classes.
    273    */
    274   private static void removeUnusedInnerClassAttributes(
    275       Map<String, InnerClassNode> infos, ClassNode n) {
    276     Set<String> types = new HashSet<>();
    277     {
    278       types.add(n.name);
    279       collectTypesFromSignature(types, n.signature);
    280       if (n.superName != null) {
    281         types.add(n.superName);
    282       }
    283       types.addAll(n.interfaces);
    284 
    285       addTypesInAnnotations(types, n.visibleAnnotations);
    286       addTypesInAnnotations(types, n.invisibleAnnotations);
    287       addTypesInTypeAnnotations(types, n.visibleTypeAnnotations);
    288       addTypesInTypeAnnotations(types, n.invisibleTypeAnnotations);
    289     }
    290     for (MethodNode m : n.methods) {
    291       collectTypesFromSignature(types, m.desc);
    292       collectTypesFromSignature(types, m.signature);
    293       types.addAll(m.exceptions);
    294 
    295       addTypesInAnnotations(types, m.visibleAnnotations);
    296       addTypesInAnnotations(types, m.invisibleAnnotations);
    297       addTypesInTypeAnnotations(types, m.visibleTypeAnnotations);
    298       addTypesInTypeAnnotations(types, m.invisibleTypeAnnotations);
    299 
    300       addTypesFromParameterAnnotations(types, m.visibleParameterAnnotations);
    301       addTypesFromParameterAnnotations(types, m.invisibleParameterAnnotations);
    302 
    303       collectTypesFromAnnotationValue(types, m.annotationDefault);
    304     }
    305     for (FieldNode f : n.fields) {
    306       collectTypesFromSignature(types, f.desc);
    307       collectTypesFromSignature(types, f.signature);
    308 
    309       addTypesInAnnotations(types, f.visibleAnnotations);
    310       addTypesInAnnotations(types, f.invisibleAnnotations);
    311       addTypesInTypeAnnotations(types, f.visibleTypeAnnotations);
    312       addTypesInTypeAnnotations(types, f.invisibleTypeAnnotations);
    313     }
    314 
    315     List<InnerClassNode> used = new ArrayList<>();
    316     for (InnerClassNode i : n.innerClasses) {
    317       if (i.outerName != null && i.outerName.equals(n.name)) {
    318         // keep InnerClass attributes for any member classes
    319         used.add(i);
    320       } else if (types.contains(i.name)) {
    321         // otherwise, keep InnerClass attributes that were referenced in class or member signatures
    322         addInnerChain(infos, used, i.name);
    323       }
    324     }
    325     addInnerChain(infos, used, n.name);
    326     n.innerClasses = used;
    327   }
    328 
    329   private static void addTypesFromParameterAnnotations(
    330       Set<String> types, List<AnnotationNode>[] parameterAnnotations) {
    331     if (parameterAnnotations == null) {
    332       return;
    333     }
    334     for (List<AnnotationNode> annos : parameterAnnotations) {
    335       addTypesInAnnotations(types, annos);
    336     }
    337   }
    338 
    339   private static void addTypesInTypeAnnotations(Set<String> types, List<TypeAnnotationNode> annos) {
    340     if (annos == null) {
    341       return;
    342     }
    343     annos.stream().forEach(a -> collectTypesFromAnnotation(types, a));
    344   }
    345 
    346   private static void addTypesInAnnotations(Set<String> types, List<AnnotationNode> annos) {
    347     if (annos == null) {
    348       return;
    349     }
    350     annos.stream().forEach(a -> collectTypesFromAnnotation(types, a));
    351   }
    352 
    353   private static void collectTypesFromAnnotation(Set<String> types, AnnotationNode a) {
    354     collectTypesFromSignature(types, a.desc);
    355     collectTypesFromAnnotationValues(types, a.values);
    356   }
    357 
    358   private static void collectTypesFromAnnotationValues(Set<String> types, List<?> values) {
    359     if (values == null) {
    360       return;
    361     }
    362     for (Object v : values) {
    363       collectTypesFromAnnotationValue(types, v);
    364     }
    365   }
    366 
    367   private static void collectTypesFromAnnotationValue(Set<String> types, Object v) {
    368     if (v instanceof List) {
    369       collectTypesFromAnnotationValues(types, (List<?>) v);
    370     } else if (v instanceof Type) {
    371       collectTypesFromSignature(types, ((Type) v).getDescriptor());
    372     } else if (v instanceof AnnotationNode) {
    373       collectTypesFromAnnotation(types, (AnnotationNode) v);
    374     } else if (v instanceof String[]) {
    375       String[] enumValue = (String[]) v;
    376       collectTypesFromSignature(types, enumValue[0]);
    377     }
    378   }
    379 
    380   /**
    381    * For each preserved InnerClass attribute, keep any information about transitive enclosing
    382    * classes of the inner class.
    383    */
    384   private static void addInnerChain(
    385       Map<String, InnerClassNode> infos, List<InnerClassNode> used, String i) {
    386     while (infos.containsKey(i)) {
    387       InnerClassNode info = infos.get(i);
    388       used.add(info);
    389       i = info.outerName;
    390     }
    391   }
    392 
    393   /** Save all class types referenced in a signature. */
    394   private static void collectTypesFromSignature(Set<String> classes, String signature) {
    395     if (signature == null) {
    396       return;
    397     }
    398     // signatures for qualified generic class types are visited as name and type argument pieces,
    399     // so stitch them back together into a binary class name
    400     final Set<String> classes1 = classes;
    401     new SignatureReader(signature)
    402         .accept(
    403             new SignatureVisitor(Opcodes.ASM5) {
    404               private final Set<String> classes = classes1;
    405               // class signatures may contain type arguments that contain class signatures
    406               Deque<List<String>> pieces = new ArrayDeque<>();
    407 
    408               @Override
    409               public void visitInnerClassType(String name) {
    410                 pieces.peek().add(name);
    411               }
    412 
    413               @Override
    414               public void visitClassType(String name) {
    415                 pieces.push(new ArrayList<>());
    416                 pieces.peek().add(name);
    417               }
    418 
    419               @Override
    420               public void visitEnd() {
    421                 classes.add(Joiner.on('$').join(pieces.pop()));
    422                 super.visitEnd();
    423               }
    424             });
    425   }
    426 
    427   static Map<String, byte[]> runTurbine(
    428       Map<String, String> input, ImmutableList<Path> classpath, Collection<Path> bootclasspath)
    429       throws IOException {
    430     List<Tree.CompUnit> units =
    431         input
    432             .entrySet()
    433             .stream()
    434             .map(e -> new SourceFile(e.getKey(), e.getValue()))
    435             .map(Parser::parse)
    436             .collect(toList());
    437 
    438     Binder.BindingResult bound = Binder.bind(units, classpath, bootclasspath);
    439     return Lower.lowerAll(bound.units(), bound.classPathEnv()).bytes();
    440   }
    441 
    442   public static Map<String, byte[]> runJavac(
    443       Map<String, String> sources,
    444       Collection<Path> classpath,
    445       Collection<? extends Path> bootclasspath)
    446       throws Exception {
    447 
    448     FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
    449 
    450     Path srcs = fs.getPath("srcs");
    451     Path out = fs.getPath("out");
    452 
    453     Files.createDirectories(out);
    454 
    455     ArrayList<Path> inputs = new ArrayList<>();
    456     for (Map.Entry<String, String> entry : sources.entrySet()) {
    457       Path path = srcs.resolve(entry.getKey());
    458       if (path.getParent() != null) {
    459         Files.createDirectories(path.getParent());
    460       }
    461       Files.write(path, entry.getValue().getBytes(UTF_8));
    462       inputs.add(path);
    463     }
    464 
    465     JavacTool compiler = JavacTool.create();
    466     DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
    467     JavacFileManager fileManager = new JavacFileManager(new Context(), true, UTF_8);
    468     fileManager.setLocationFromPaths(StandardLocation.PLATFORM_CLASS_PATH, bootclasspath);
    469     fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, ImmutableList.of(out));
    470     fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath);
    471 
    472     JavacTask task =
    473         compiler.getTask(
    474             new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
    475             fileManager,
    476             collector,
    477             ImmutableList.of("-parameters"),
    478             ImmutableList.of(),
    479             fileManager.getJavaFileObjectsFromPaths(inputs));
    480 
    481     assertThat(task.call()).named(collector.getDiagnostics().toString()).isTrue();
    482 
    483     List<Path> classes = new ArrayList<>();
    484     Files.walkFileTree(
    485         out,
    486         new SimpleFileVisitor<Path>() {
    487           @Override
    488           public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
    489               throws IOException {
    490             if (path.getFileName().toString().endsWith(".class")) {
    491               classes.add(path);
    492             }
    493             return FileVisitResult.CONTINUE;
    494           }
    495         });
    496     Map<String, byte[]> result = new LinkedHashMap<>();
    497     for (Path path : classes) {
    498       String r = out.relativize(path).toString();
    499       result.put(r.substring(0, r.length() - ".class".length()), Files.readAllBytes(path));
    500     }
    501     return result;
    502   }
    503 
    504   /** Normalizes and stringifies a collection of class files. */
    505   public static String dump(Map<String, byte[]> compiled) throws Exception {
    506     StringBuilder sb = new StringBuilder();
    507     List<String> keys = new ArrayList<>(compiled.keySet());
    508     Collections.sort(keys);
    509     for (String key : keys) {
    510       String na = key;
    511       if (na.startsWith("/")) {
    512         na = na.substring(1);
    513       }
    514       sb.append(String.format("=== %s ===\n", na));
    515       sb.append(AsmUtils.textify(compiled.get(key)));
    516     }
    517     return sb.toString();
    518   }
    519 
    520   static class TestInput {
    521 
    522     final Map<String, String> sources;
    523     final Map<String, String> classes;
    524 
    525     public TestInput(Map<String, String> sources, Map<String, String> classes) {
    526       this.sources = sources;
    527       this.classes = classes;
    528     }
    529 
    530     static TestInput parse(String text) {
    531       Map<String, String> sources = new LinkedHashMap<>();
    532       Map<String, String> classes = new LinkedHashMap<>();
    533       String className = null;
    534       String sourceName = null;
    535       List<String> lines = new ArrayList<>();
    536       for (String line : Splitter.on('\n').split(text)) {
    537         if (line.startsWith("===")) {
    538           if (sourceName != null) {
    539             sources.put(sourceName, Joiner.on('\n').join(lines) + "\n");
    540           }
    541           if (className != null) {
    542             classes.put(className, Joiner.on('\n').join(lines) + "\n");
    543           }
    544           lines.clear();
    545           sourceName = line.substring(3, line.length() - 3).trim();
    546           className = null;
    547         } else if (line.startsWith("%%%")) {
    548           if (className != null) {
    549             classes.put(className, Joiner.on('\n').join(lines) + "\n");
    550           }
    551           if (sourceName != null) {
    552             sources.put(sourceName, Joiner.on('\n').join(lines) + "\n");
    553           }
    554           className = line.substring(3, line.length() - 3).trim();
    555           lines.clear();
    556           sourceName = null;
    557         } else {
    558           lines.add(line);
    559         }
    560       }
    561       if (sourceName != null) {
    562         sources.put(sourceName, Joiner.on('\n').join(lines) + "\n");
    563       }
    564       if (className != null) {
    565         classes.put(className, Joiner.on('\n').join(lines) + "\n");
    566       }
    567       lines.clear();
    568       return new TestInput(sources, classes);
    569     }
    570   }
    571 }
    572