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