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 com.google.turbine.testing.TestClassPaths.TURBINE_BOOTCLASSPATH; 21 import static java.nio.charset.StandardCharsets.UTF_8; 22 import static java.util.stream.Collectors.toCollection; 23 import static java.util.stream.Collectors.toList; 24 25 import com.google.common.base.Joiner; 26 import com.google.common.base.Splitter; 27 import com.google.common.collect.ImmutableList; 28 import com.google.common.io.MoreFiles; 29 import com.google.common.jimfs.Configuration; 30 import com.google.common.jimfs.Jimfs; 31 import com.google.turbine.binder.Binder; 32 import com.google.turbine.binder.ClassPath; 33 import com.google.turbine.binder.ClassPathBinder; 34 import com.google.turbine.diag.SourceFile; 35 import com.google.turbine.parse.Parser; 36 import com.google.turbine.testing.AsmUtils; 37 import com.google.turbine.tree.Tree; 38 import com.sun.source.util.JavacTask; 39 import com.sun.tools.javac.api.JavacTool; 40 import com.sun.tools.javac.file.JavacFileManager; 41 import com.sun.tools.javac.util.Context; 42 import java.io.BufferedWriter; 43 import java.io.IOException; 44 import java.io.OutputStreamWriter; 45 import java.io.PrintWriter; 46 import java.nio.file.FileSystem; 47 import java.nio.file.FileVisitResult; 48 import java.nio.file.Files; 49 import java.nio.file.Path; 50 import java.nio.file.SimpleFileVisitor; 51 import java.nio.file.attribute.BasicFileAttributes; 52 import java.util.ArrayDeque; 53 import java.util.ArrayList; 54 import java.util.Collection; 55 import java.util.Collections; 56 import java.util.Comparator; 57 import java.util.Deque; 58 import java.util.HashMap; 59 import java.util.HashSet; 60 import java.util.LinkedHashMap; 61 import java.util.List; 62 import java.util.Map; 63 import java.util.Optional; 64 import java.util.Set; 65 import javax.tools.DiagnosticCollector; 66 import javax.tools.JavaFileObject; 67 import javax.tools.StandardLocation; 68 import org.objectweb.asm.ClassReader; 69 import org.objectweb.asm.ClassWriter; 70 import org.objectweb.asm.Opcodes; 71 import org.objectweb.asm.Type; 72 import org.objectweb.asm.signature.SignatureReader; 73 import org.objectweb.asm.signature.SignatureVisitor; 74 import org.objectweb.asm.tree.AnnotationNode; 75 import org.objectweb.asm.tree.ClassNode; 76 import org.objectweb.asm.tree.FieldNode; 77 import org.objectweb.asm.tree.InnerClassNode; 78 import org.objectweb.asm.tree.MethodNode; 79 import org.objectweb.asm.tree.TypeAnnotationNode; 80 81 /** Support for bytecode diffing-integration tests. */ 82 public class IntegrationTestSupport { 83 84 /** 85 * Normalizes order of members, attributes, and constant pool entries, to allow diffing bytecode. 86 */ sortMembers(Map<String, byte[]> in)87 public static Map<String, byte[]> sortMembers(Map<String, byte[]> in) { 88 List<ClassNode> classes = toClassNodes(in); 89 for (ClassNode n : classes) { 90 sortAttributes(n); 91 } 92 return toByteCode(classes); 93 } 94 95 /** 96 * Canonicalizes bytecode produced by javac to match the expected output of turbine. Includes the 97 * same normalization as {@link #sortMembers}, as well as removing everything not produced by the 98 * header compiler (code, debug info, etc.) 99 */ canonicalize(Map<String, byte[]> in)100 public static Map<String, byte[]> canonicalize(Map<String, byte[]> in) { 101 List<ClassNode> classes = toClassNodes(in); 102 103 // drop anonymous classes 104 classes = classes.stream().filter(n -> !isAnonymous(n)).collect(toCollection(ArrayList::new)); 105 106 // collect all inner classes attributes 107 Map<String, InnerClassNode> infos = new HashMap<>(); 108 for (ClassNode n : classes) { 109 for (InnerClassNode innerClassNode : n.innerClasses) { 110 infos.put(innerClassNode.name, innerClassNode); 111 } 112 } 113 114 HashSet<String> all = classes.stream().map(n -> n.name).collect(toCollection(HashSet::new)); 115 for (ClassNode n : classes) { 116 removeImplementation(n); 117 removeUnusedInnerClassAttributes(infos, n); 118 makeEnumsFinal(all, n); 119 sortAttributes(n); 120 undeprecate(n); 121 } 122 123 return toByteCode(classes); 124 } 125 isAnonymous(ClassNode n)126 private static boolean isAnonymous(ClassNode n) { 127 // JVMS 4.7.6: if C is anonymous, the value of the inner_name_index item must be zero 128 return n.innerClasses.stream().anyMatch(i -> i.name.equals(n.name) && i.innerName == null); 129 } 130 131 // ASM sets ACC_DEPRECATED for elements with the Deprecated attribute; 132 // unset it if the @Deprecated annotation is not also present. 133 // This can happen if the @deprecated javadoc tag was present but the 134 // annotation wasn't. undeprecate(ClassNode n)135 private static void undeprecate(ClassNode n) { 136 if (!isDeprecated(n.visibleAnnotations)) { 137 n.access &= ~Opcodes.ACC_DEPRECATED; 138 } 139 n.methods.stream() 140 .filter(m -> !isDeprecated(m.visibleAnnotations)) 141 .forEach(m -> m.access &= ~Opcodes.ACC_DEPRECATED); 142 n.fields.stream() 143 .filter(f -> !isDeprecated(f.visibleAnnotations)) 144 .forEach(f -> f.access &= ~Opcodes.ACC_DEPRECATED); 145 } 146 isDeprecated(List<AnnotationNode> visibleAnnotations)147 private static boolean isDeprecated(List<AnnotationNode> visibleAnnotations) { 148 return visibleAnnotations != null 149 && visibleAnnotations.stream().anyMatch(a -> a.desc.equals("Ljava/lang/Deprecated;")); 150 } 151 makeEnumsFinal(Set<String> all, ClassNode n)152 private static void makeEnumsFinal(Set<String> all, ClassNode n) { 153 n.innerClasses.forEach( 154 x -> { 155 if (all.contains(x.name) && (x.access & Opcodes.ACC_ENUM) == Opcodes.ACC_ENUM) { 156 x.access &= ~Opcodes.ACC_ABSTRACT; 157 x.access |= Opcodes.ACC_FINAL; 158 } 159 }); 160 if ((n.access & Opcodes.ACC_ENUM) == Opcodes.ACC_ENUM) { 161 n.access &= ~Opcodes.ACC_ABSTRACT; 162 n.access |= Opcodes.ACC_FINAL; 163 } 164 } 165 toByteCode(List<ClassNode> classes)166 private static Map<String, byte[]> toByteCode(List<ClassNode> classes) { 167 Map<String, byte[]> out = new LinkedHashMap<>(); 168 for (ClassNode n : classes) { 169 ClassWriter cw = new ClassWriter(0); 170 n.accept(cw); 171 out.put(n.name, cw.toByteArray()); 172 } 173 return out; 174 } 175 toClassNodes(Map<String, byte[]> in)176 private static List<ClassNode> toClassNodes(Map<String, byte[]> in) { 177 List<ClassNode> classes = new ArrayList<>(); 178 for (byte[] f : in.values()) { 179 ClassNode n = new ClassNode(); 180 new ClassReader(f).accept(n, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); 181 182 classes.add(n); 183 } 184 return classes; 185 } 186 187 /** Remove elements that are omitted by turbine, e.g. private and synthetic members. */ removeImplementation(ClassNode n)188 private static void removeImplementation(ClassNode n) { 189 n.innerClasses = 190 n.innerClasses.stream() 191 .filter(x -> (x.access & Opcodes.ACC_SYNTHETIC) == 0 && x.innerName != null) 192 .collect(toList()); 193 194 n.methods = 195 n.methods.stream() 196 .filter(x -> (x.access & (Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PRIVATE)) == 0) 197 .filter(x -> !x.name.equals("<clinit>")) 198 .collect(toList()); 199 200 n.fields = 201 n.fields.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. */ sortAttributes(ClassNode n)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 sortParameterAnnotations(List<AnnotationNode>[] parameters)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 sortTypeAnnotations(List<TypeAnnotationNode> annos)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 sortAnnotations(List<AnnotationNode> annos)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 */ removeUnusedInnerClassAttributes( Map<String, InnerClassNode> infos, ClassNode n)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 addTypesFromParameterAnnotations( Set<String> types, List<AnnotationNode>[] parameterAnnotations)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 addTypesInTypeAnnotations(Set<String> types, List<TypeAnnotationNode> annos)339 private static void addTypesInTypeAnnotations(Set<String> types, List<TypeAnnotationNode> annos) { 340 if (annos == null) { 341 return; 342 } 343 annos.forEach(a -> collectTypesFromAnnotation(types, a)); 344 } 345 addTypesInAnnotations(Set<String> types, List<AnnotationNode> annos)346 private static void addTypesInAnnotations(Set<String> types, List<AnnotationNode> annos) { 347 if (annos == null) { 348 return; 349 } 350 annos.forEach(a -> collectTypesFromAnnotation(types, a)); 351 } 352 collectTypesFromAnnotation(Set<String> types, AnnotationNode a)353 private static void collectTypesFromAnnotation(Set<String> types, AnnotationNode a) { 354 collectTypesFromSignature(types, a.desc); 355 collectTypesFromAnnotationValues(types, a.values); 356 } 357 collectTypesFromAnnotationValues(Set<String> types, List<?> values)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 collectTypesFromAnnotationValue(Set<String> types, Object v)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 */ addInnerChain( Map<String, InnerClassNode> infos, List<InnerClassNode> used, String i)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. */ collectTypesFromSignature(Set<String> classes, String 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.ASM7) { 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 runTurbine(Map<String, String> input, ImmutableList<Path> classpath)427 static Map<String, byte[]> runTurbine(Map<String, String> input, ImmutableList<Path> classpath) 428 throws IOException { 429 return runTurbine( 430 input, classpath, TURBINE_BOOTCLASSPATH, /* moduleVersion= */ Optional.empty()); 431 } 432 runTurbine( Map<String, String> input, ImmutableList<Path> classpath, ClassPath bootClassPath, Optional<String> moduleVersion)433 static Map<String, byte[]> runTurbine( 434 Map<String, String> input, 435 ImmutableList<Path> classpath, 436 ClassPath bootClassPath, 437 Optional<String> moduleVersion) 438 throws IOException { 439 List<Tree.CompUnit> units = 440 input.entrySet().stream() 441 .map(e -> new SourceFile(e.getKey(), e.getValue())) 442 .map(Parser::parse) 443 .collect(toList()); 444 445 Binder.BindingResult bound = 446 Binder.bind(units, ClassPathBinder.bindClasspath(classpath), bootClassPath, moduleVersion); 447 return Lower.lowerAll(bound.units(), bound.modules(), bound.classPathEnv()).bytes(); 448 } 449 runJavac( Map<String, String> sources, Collection<Path> classpath)450 public static Map<String, byte[]> runJavac( 451 Map<String, String> sources, Collection<Path> classpath) throws Exception { 452 return runJavac( 453 sources, classpath, ImmutableList.of("-parameters", "-source", "8", "-target", "8")); 454 } 455 runJavac( Map<String, String> sources, Collection<Path> classpath, ImmutableList<String> options)456 public static Map<String, byte[]> runJavac( 457 Map<String, String> sources, Collection<Path> classpath, ImmutableList<String> options) 458 throws Exception { 459 460 FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); 461 462 Path srcs = fs.getPath("srcs"); 463 Path out = fs.getPath("out"); 464 465 Files.createDirectories(out); 466 467 ArrayList<Path> inputs = new ArrayList<>(); 468 for (Map.Entry<String, String> entry : sources.entrySet()) { 469 Path path = srcs.resolve(entry.getKey()); 470 if (path.getParent() != null) { 471 Files.createDirectories(path.getParent()); 472 } 473 MoreFiles.asCharSink(path, UTF_8).write(entry.getValue()); 474 inputs.add(path); 475 } 476 477 JavacTool compiler = JavacTool.create(); 478 DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>(); 479 JavacFileManager fileManager = new JavacFileManager(new Context(), true, UTF_8); 480 fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, ImmutableList.of(out)); 481 fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath); 482 fileManager.setLocationFromPaths(StandardLocation.locationFor("MODULE_PATH"), classpath); 483 if (inputs.stream().filter(i -> i.getFileName().toString().equals("module-info.java")).count() 484 > 1) { 485 // multi-module mode 486 fileManager.setLocationFromPaths( 487 StandardLocation.locationFor("MODULE_SOURCE_PATH"), ImmutableList.of(srcs)); 488 } 489 490 JavacTask task = 491 compiler.getTask( 492 new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true), 493 fileManager, 494 collector, 495 options, 496 ImmutableList.of(), 497 fileManager.getJavaFileObjectsFromPaths(inputs)); 498 499 assertThat(task.call()).named(collector.getDiagnostics().toString()).isTrue(); 500 501 List<Path> classes = new ArrayList<>(); 502 Files.walkFileTree( 503 out, 504 new SimpleFileVisitor<Path>() { 505 @Override 506 public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) 507 throws IOException { 508 if (path.getFileName().toString().endsWith(".class")) { 509 classes.add(path); 510 } 511 return FileVisitResult.CONTINUE; 512 } 513 }); 514 Map<String, byte[]> result = new LinkedHashMap<>(); 515 for (Path path : classes) { 516 String r = out.relativize(path).toString(); 517 result.put(r.substring(0, r.length() - ".class".length()), Files.readAllBytes(path)); 518 } 519 return result; 520 } 521 522 /** Normalizes and stringifies a collection of class files. */ dump(Map<String, byte[]> compiled)523 public static String dump(Map<String, byte[]> compiled) throws Exception { 524 StringBuilder sb = new StringBuilder(); 525 List<String> keys = new ArrayList<>(compiled.keySet()); 526 Collections.sort(keys); 527 for (String key : keys) { 528 String na = key; 529 if (na.startsWith("/")) { 530 na = na.substring(1); 531 } 532 sb.append(String.format("=== %s ===\n", na)); 533 sb.append(AsmUtils.textify(compiled.get(key))); 534 } 535 return sb.toString(); 536 } 537 538 static class TestInput { 539 540 final Map<String, String> sources; 541 final Map<String, String> classes; 542 TestInput(Map<String, String> sources, Map<String, String> classes)543 public TestInput(Map<String, String> sources, Map<String, String> classes) { 544 this.sources = sources; 545 this.classes = classes; 546 } 547 parse(String text)548 static TestInput parse(String text) { 549 Map<String, String> sources = new LinkedHashMap<>(); 550 Map<String, String> classes = new LinkedHashMap<>(); 551 String className = null; 552 String sourceName = null; 553 List<String> lines = new ArrayList<>(); 554 for (String line : Splitter.on('\n').split(text)) { 555 if (line.startsWith("===")) { 556 if (sourceName != null) { 557 sources.put(sourceName, Joiner.on('\n').join(lines) + "\n"); 558 } 559 if (className != null) { 560 classes.put(className, Joiner.on('\n').join(lines) + "\n"); 561 } 562 lines.clear(); 563 sourceName = line.substring(3, line.length() - 3).trim(); 564 className = null; 565 } else if (line.startsWith("%%%")) { 566 if (className != null) { 567 classes.put(className, Joiner.on('\n').join(lines) + "\n"); 568 } 569 if (sourceName != null) { 570 sources.put(sourceName, Joiner.on('\n').join(lines) + "\n"); 571 } 572 className = line.substring(3, line.length() - 3).trim(); 573 lines.clear(); 574 sourceName = null; 575 } else { 576 lines.add(line); 577 } 578 } 579 if (sourceName != null) { 580 sources.put(sourceName, Joiner.on('\n').join(lines) + "\n"); 581 } 582 if (className != null) { 583 classes.put(className, Joiner.on('\n').join(lines) + "\n"); 584 } 585 lines.clear(); 586 return new TestInput(sources, classes); 587 } 588 } 589 } 590