• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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