| /* |
| * Copyright 2013-present Facebook, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may |
| * not use this file except in compliance with the License. You may obtain |
| * a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package com.facebook.buck.java.abi; |
| |
| import static com.google.common.base.Charsets.UTF_8; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import com.facebook.buck.testutil.integration.TestDataHelper; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Optional; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.hash.Hasher; |
| import com.google.common.hash.Hashing; |
| import com.google.common.io.Files; |
| |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TemporaryFolder; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.SortedSet; |
| |
| import javax.tools.JavaCompiler; |
| import javax.tools.JavaFileObject; |
| import javax.tools.StandardJavaFileManager; |
| import javax.tools.ToolProvider; |
| |
| public class AbiWriterTest { |
| |
| @Rule public TemporaryFolder temp = new TemporaryFolder(); |
| |
| @Test |
| public void abiKeyForEmptySourcesIsStable() { |
| assertEquals( |
| "The ABI key used when a java_library() has no srcs should be constant across platforms.", |
| AbiWriterProtocol.EMPTY_ABI_KEY, |
| AbiWriter.computeAbiKey(ImmutableSortedSet.<String>of())); |
| } |
| |
| @Test |
| public void willCaptureClassName() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {}")); |
| |
| assertTrue(summary, summary.contains("public class com.facebook.buck.example.A")); |
| } |
| |
| @Test |
| public void willCaptureTypeParametersOnClass() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A<T> {}")); |
| |
| assertTrue(summary, summary.contains("com.facebook.buck.example.A<T>")); |
| } |
| |
| @Test |
| public void classesDefaultToExtendingJavaLangObject() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {}")); |
| |
| assertTrue(summary, summary.contains("extends java.lang.Object")); |
| } |
| |
| @Test |
| public void classesCanImplementManyInterfaces() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "import java.io.Serializable;", |
| "public class A implements Cloneable, Serializable {}")); |
| |
| assertTrue("Ordering of interfaces is alphabetical based on fully qualified name" + summary, |
| summary.contains("implements java.io.Serializable, java.lang.Cloneable")); |
| } |
| |
| @Test |
| public void classesCanImplementInterfacesThatHaveAGenericType() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A implements Comparable<A> {", |
| " public int compareTo(A o) {", |
| " return 1;", |
| " }", |
| "}")); |
| |
| assertTrue(summary, |
| summary.contains("implements java.lang.Comparable<com.facebook.buck.example.A>")); |
| } |
| |
| @Test |
| public void runtimeAnnotationsArePreservedAtTheClassLevel() throws IOException{ |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "@Deprecated", |
| "public class A {}")); |
| |
| assertTrue(summary, summary.contains("@java.lang.Deprecated")); |
| } |
| |
| @Test |
| public void classesCanHaveMethods() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public int doSomething(String foo) {", |
| " return 0;", |
| " }", |
| "}")); |
| |
| assertTrue(summary, summary.contains("public int doSomething(java.lang.String)")); |
| |
| // Make sure we've not output the method body. |
| assertFalse(summary, summary.contains("return")); |
| } |
| |
| @Test |
| public void genericTypesOnClassMethodsAreCorrectlyReported() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A<Y> {", |
| " public <X> X doSomething(Y foo, A bar, String baz) {", |
| " return null;", |
| " }", |
| "}")); |
| |
| assertTrue(summary, summary.contains( |
| "<X> X doSomething(Y, com.facebook.buck.example.A, java.lang.String)")); |
| } |
| |
| @Test |
| public void retainedAnnotationsOnClassMethodsAreKept() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " @Deprecated public void foo() {}", |
| "}")); |
| |
| assertTrue(summary, summary.contains("@java.lang.Deprecated")); |
| } |
| |
| @Test |
| public void interfacesDoNotExtendAnything() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "interface A {}")); |
| |
| assertTrue(summary, summary.contains("interface com.facebook.buck.example.A")); |
| } |
| |
| @Test |
| public void interfacesCanExtendOtherInterfaces() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "interface A extends java.io.Serializable {}")); |
| |
| assertTrue(summary, summary.contains("example.A extends java.io.Serializable")); |
| } |
| |
| @Test |
| public void interfacesRetainClassLevelAnnotations() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "@Deprecated public interface A {}")); |
| |
| assertTrue(summary, summary.contains("@java.lang.Deprecated")); |
| } |
| |
| @Test |
| public void interfacesCanBeTyped() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public interface A<T> {}")); |
| |
| assertEquals("public abstract interface com.facebook.buck.example.A<T>\n", summary); |
| } |
| |
| @Test |
| public void interfaceMethodsAreReported() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public interface A {", |
| " public int doSomething(String foo);", |
| " void sitQuietly();", |
| "}")); |
| |
| assertTrue(summary, summary.contains("public abstract int doSomething(java.lang.String)")); |
| assertTrue(summary, summary.contains("public abstract void sitQuietly()")); |
| } |
| |
| @Test |
| public void interfaceMethodsWithTypeSignaturesAreCorrectlyKept() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public interface A<Y> {", |
| " public <X> X doSomething(Y foo, A bar, String baz);", |
| "}")); |
| |
| assertTrue(summary, summary.contains( |
| "<X> X doSomething(Y, com.facebook.buck.example.A, java.lang.String)")); |
| } |
| |
| @Test |
| public void shouldNotIncludePrivateMethods() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " private void doSomething() {}", |
| "}")); |
| |
| assertFalse(summary, summary.contains("doSomething")); |
| } |
| |
| // Fields |
| @Test |
| public void visibleFieldsAreIncluded() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public int number = 42;", |
| " private String magic = \"Now you see me\";", |
| "}")); |
| |
| assertTrue(summary, summary.contains("public int number")); |
| assertFalse(summary, summary.contains("42")); |
| assertFalse(summary, summary.contains("magic")); |
| } |
| |
| @Test |
| public void runTimeAnnotationsOnFieldsAreRetained() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " @Deprecated", |
| " public int number;", |
| "}")); |
| |
| assertTrue(summary, summary.contains("@java.lang.Deprecated")); |
| } |
| |
| @Test |
| public void genericTypesOnFieldsAreKept() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "import java.util.Set;", |
| "public class A {", |
| " public Set<String> strings;", |
| "}")); |
| |
| assertTrue(summary, summary.contains("Set<java.lang.String> strings")); |
| } |
| |
| @Test |
| public void shouldMaintainWildCardGenerics() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "import java.util.Set;", |
| "public class A {", |
| " public Set<?> question;", |
| "}")); |
| |
| assertTrue(summary, summary.contains("Set<?> question")); |
| } |
| |
| @Test |
| public void shouldComplexGenerics() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "import java.util.Collection;", |
| "import java.util.Set;", |
| "public class A<T> {", |
| " public Set<? extends Collection> extension;", |
| " public Set<? super T> supered;", |
| "}")); |
| |
| assertTrue(summary, summary.contains("Set<? extends java.util.Collection> extension")); |
| assertTrue(summary, summary.contains("Set<? super T> supered")); |
| } |
| |
| @Test |
| public void nestedGenericsAreOkay() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "import java.util.Collection;", |
| "import java.util.Set;", |
| "public class A<T> {", |
| " public Set<Collection<String>> nested;", |
| "}")); |
| |
| assertTrue(summary, |
| summary.contains("java.util.Set<java.util.Collection<java.lang.String>> nested")); |
| } |
| |
| // Constants |
| @Test |
| public void compileTimeConstantExpressionsAreRetainedOnFields() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public final static int NUMBER = 42;", |
| " final static String MAGIC = \"Now you see me\" + 3;", |
| " public static int foo = 37;", |
| " public static final long NOW = System.currentTimeMillis();", |
| "}")); |
| |
| assertTrue(summary, summary.contains("public static final int NUMBER = 42")); |
| assertTrue(summary, summary.contains("static final java.lang.String MAGIC = Now you see me3")); |
| assertFalse(summary, summary.contains("public static int foo = 37")); |
| assertTrue(summary, summary.contains("public static final long NOW\n")); |
| } |
| |
| @Test |
| public void checkThatDefinedInOtherClassesThatCanBeInlinedAreInlined() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public final static int NUMBER = 42;", |
| " final static String MAGIC = \"Now you see me\" + 3;", |
| " public static int foo = 37;", |
| " public static final long NOW = System.currentTimeMillis();", |
| "}"), |
| "B.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class B {", |
| " public final static int KEY = A.NUMBER + 3;", |
| "}" |
| )); |
| |
| assertTrue(summary, summary.contains("KEY = 45")); |
| } |
| |
| // Enums |
| @Test |
| public void enumsAreHandled() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public enum A {", |
| " ONE, TWO, THREE;", |
| " public void doSomething() {} ", |
| "}")); |
| |
| assertTrue(summary, summary.contains("ONE")); |
| assertTrue(summary, summary.contains("TWO")); |
| assertTrue(summary, summary.contains("THREE")); |
| assertTrue(summary, summary.contains("public void doSomething()")); |
| } |
| |
| @Test |
| public void orderingOfEnumConstantsMatters() throws IOException { |
| String first = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public enum A {", |
| " ONE, TWO;", |
| " public void doSomething() {} ", |
| "}")); |
| |
| String second = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public enum A {", |
| " TWO, ONE;", |
| " public void doSomething() {} ", |
| "}")); |
| |
| assertNotEquals(first, second); |
| } |
| |
| @Test |
| public void canHandleNewAnnotationTypes() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "import java.lang.annotation.*;", |
| "import static java.lang.annotation.ElementType.*;", |
| "@Retention(RetentionPolicy.RUNTIME)", |
| "@Target(value={CONSTRUCTOR, FIELD})", |
| "public @interface A {}")); |
| |
| assertTrue(summary, summary.contains("@interface com.facebook.buck.example.A")); |
| assertTrue(summary, summary.contains( |
| "@java.lang.annotation.Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)")); |
| assertTrue(summary, summary.contains( |
| "@java.lang.annotation.Target(value={java.lang.annotation.ElementType.CONSTRUCTOR,")); |
| } |
| |
| |
| @Test |
| public void innerClassesCanBeNested() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public static class B {", |
| " public void foo() {}", |
| " }", |
| "}")); |
| |
| assertTrue(summary, summary.contains( |
| "public static class com.facebook.buck.example.A.B extends java.lang.Object")); |
| } |
| |
| @Test |
| public void innerClassesCanBeDeeplyNested() throws IOException { |
| String summary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public static class B {", |
| " public class C {", |
| " public void foo() {}", |
| " }", |
| " }", |
| "}")); |
| |
| assertTrue(summary, |
| summary.contains("public class com.facebook.buck.example.A.B.C extends java.lang.Object")); |
| } |
| |
| @Test |
| public void abisOfAClassAndAInterfaceWithSharedMethodsAreNotTheSame() throws IOException { |
| String classSummary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public abstract class A {", |
| " public abstract void doSomething();", |
| "}")); |
| |
| String interfaceSummary = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public interface A {", |
| " void doSomething();", |
| "}")); |
| |
| assertNotEquals(classSummary, interfaceSummary); |
| } |
| |
| @Test |
| public void reorderingFieldsDoesNotModifyTheAbi() throws IOException { |
| String original = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public int first;", |
| " public int second;", |
| "}")); |
| |
| String abiCompatible = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public int second;", |
| " public int first;", |
| "}")); |
| |
| assertEquals(original, abiCompatible); |
| } |
| |
| @Test |
| public void reorderingMethodsDoesNotModifyTheAbi() throws IOException { |
| String original = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public void doSomething() {}", |
| " public int aLongRunningComputation() { return 42; }", |
| "}")); |
| |
| String abiCompatible = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public int aLongRunningComputation() { return 2345; }", |
| " public void doSomething() {}", |
| "}")); |
| |
| assertEquals(original, abiCompatible); |
| } |
| |
| @Test |
| public void renamingMethodArgumentsDoesNotModifyTheAbi() throws IOException { |
| String original = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public void doSomething(int count) {}", |
| "}")); |
| |
| String abiCompatible = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public void doSomething(int numberOfTimes) {}", |
| "}")); |
| |
| assertEquals(original, abiCompatible); |
| } |
| |
| @Test |
| public void varargsAreNotTheSameAsArrays() throws IOException { |
| String original = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public void doSomething(String... args) {}", |
| "}")); |
| |
| String amended = compile("A.java", Joiner.on("\n").join( |
| "package com.facebook.buck.example;", |
| "public class A {", |
| " public void doSomething(String[] args) {}", |
| "}")); |
| |
| assertNotEquals(original, amended); |
| } |
| |
| @Test |
| public void generateSampleOutput() throws IOException { |
| File testDataDir = TestDataHelper.getTestDataScenario(this, "compute_abi"); |
| String source = Files.toString( |
| new File(testDataDir, "generateSampleOutput.src"), UTF_8); |
| String original = compile("A.java", source); |
| |
| String expected = Files.toString( |
| new File(testDataDir, "generateSampleOutput.expected"), UTF_8); |
| |
| assertEquals(expected, original); |
| } |
| |
| @Test |
| public void generatedHashMustBeZeroPaddedToFortyCharacters() { |
| String hex = AbiWriter.computeAbiKey(ImmutableSortedSet.of("abcdefghi")); |
| |
| assertEquals('0', hex.charAt(0)); |
| assertEquals(40, hex.length()); |
| } |
| |
| @Test |
| public void shouldIgnoreAnnotatedPackageDeclarations() throws IOException { |
| Optional<String> compiled = getOptionalSummary( |
| "package-info.java", |
| Joiner.on("\n").join( |
| "/** This is a documented pacakge */", |
| "@Deprecated", // It's legit to deprecate an entire package |
| "package com.facebook.buck.example;")); |
| |
| // If we get this far, then we know that everything is just fine. We're just preventing an |
| // exception being thrown, and it's impossible to accidentally have a java class called |
| // "package-info" so we're not expecting this to contribute to the API. |
| assertFalse(compiled.isPresent()); |
| } |
| |
| @Test |
| public void shouldIgnoreUnannotatedPackageDeclarations() throws IOException { |
| Optional<String> compiled = getOptionalSummary( |
| "package-info.java", |
| Joiner.on("\n").join( |
| "/** This is a documented pacakge without an annotation */", |
| "package com.facebook.buck.example;")); |
| |
| assertFalse(compiled.isPresent()); |
| } |
| |
| @Test |
| public void emptySummariesLeadToAnEmptyAbiKeyBeingMade() throws IOException { |
| File outDir = temp.newFolder(); |
| File onlyDocs = new File(outDir, "package-info.java"); |
| SortedSet<String> summaries = generateSummary( |
| outDir, |
| new FileAndSource(onlyDocs, |
| Joiner.on("\n").join( |
| "/** This is a package */", |
| "package com.facebook.buck.example;")), |
| ImmutableSet.<File>of()); |
| |
| assertTrue(summaries.isEmpty()); |
| |
| String computed = AbiWriter.computeAbiKey(summaries); |
| |
| assertEquals(AbiWriterProtocol.EMPTY_ABI_KEY, computed); |
| } |
| |
| @Test |
| public void ensureHashMatchesGuavaEquivalent() { |
| String key = AbiWriter.computeAbiKey(ImmutableSortedSet.<String>of()); |
| String guava = hashWithGuava(ImmutableSortedSet.<String>of()); |
| assertEquals(guava, key); |
| |
| String javaCode = Joiner.on('\n').join( |
| "public class Example", |
| "public void cheese(java.lang.String)"); |
| |
| SortedSet<String> summaries = ImmutableSortedSet.of(javaCode); |
| key = AbiWriter.computeAbiKey(summaries); |
| guava = hashWithGuava(summaries); |
| assertEquals(guava, key); |
| } |
| |
| private String hashWithGuava(SortedSet<String> summaries) { |
| Hasher hasher = Hashing.sha1().newHasher(); |
| for (String summary : summaries) { |
| hasher.putUnencodedChars(summary); |
| } |
| return hasher.hash().toString(); |
| } |
| |
| /** |
| * Files are compiled in the order that they're fed to this method. As each file is compiled its |
| * output directory is added to a classpath that lives for the duration of the method call. As |
| * each file is compiled, the summaries generated by AbiWriter for the last compilation are |
| * stored. |
| * |
| * @return the summary of the last file to be compiled. |
| */ |
| private String compile( |
| String fileName, String source, String... fileNamesAndSources) throws IOException { |
| return getOptionalSummary(fileName, source, fileNamesAndSources).get(); |
| } |
| |
| private Optional<String> getOptionalSummary( |
| String fileName, String source, String... fileNamesAndSources) throws IOException { |
| List<FileAndSource> targets = Lists.newArrayList(); |
| |
| targets.add(new FileAndSource(new File(temp.newFolder(), fileName), source)); |
| for (int i = 0; i < fileNamesAndSources.length; i++) { |
| targets.add(new FileAndSource( |
| new File(temp.newFolder(), fileNamesAndSources[i++]), fileNamesAndSources[i])); |
| } |
| |
| ImmutableSet.Builder<File> classpath = ImmutableSet.builder(); |
| SortedSet<String> lastSummary = null; |
| for (FileAndSource target : targets) { |
| File outputDir = temp.newFolder(); |
| lastSummary = generateSummary(outputDir, target, classpath.build()); |
| classpath.add(outputDir); |
| } |
| if (lastSummary.isEmpty()) { |
| return Optional.absent(); |
| } |
| return Optional.of(Iterables.getOnlyElement(lastSummary)); |
| } |
| |
| |
| private SortedSet<String> generateSummary( |
| File outputDir, |
| FileAndSource target, |
| ImmutableSet<File> classpath) throws IOException{ |
| File file = target.file; |
| Files.write(target.source, file, UTF_8); |
| |
| JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); |
| StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); |
| Iterable<? extends JavaFileObject> sourceObjects = |
| fileManager.getJavaFileObjectsFromFiles(ImmutableSet.of(file)); |
| |
| List<String> args = Lists.newArrayList("-g", "-d", outputDir.getAbsolutePath()); |
| if (!classpath.isEmpty()) { |
| args.add("-classpath"); |
| args.add(Joiner.on(File.pathSeparator).join(classpath)); |
| } |
| |
| AbiWriter processor = new AbiWriter(); |
| |
| JavaCompiler.CompilationTask compilation = |
| compiler.getTask(null, fileManager, null, args, null, sourceObjects); |
| compilation.setProcessors(ImmutableSet.of(processor)); |
| |
| Boolean result = compilation.call(); |
| assertNotNull(result); |
| assertTrue(result); |
| |
| return processor.getSummaries(); |
| } |
| |
| private static class FileAndSource { |
| public File file; |
| public String source; |
| |
| public FileAndSource(File file, String source) { |
| this.file = file; |
| this.source = source; |
| } |
| } |
| } |