/* * Copyright 2016 Google Inc. All Rights Reserved. * * 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.google.errorprone; import static com.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.io.CharStreams; import com.google.errorprone.apply.DescriptionBasedDiff; import com.google.errorprone.apply.ImportOrganizer; import com.google.errorprone.apply.SourceFile; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.fixes.Fix; import com.google.errorprone.matchers.Description; import com.google.errorprone.scanner.ErrorProneScanner; import com.google.errorprone.scanner.ErrorProneScannerTransformer; import com.google.testing.compile.JavaFileObjects; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.api.JavacTaskImpl; import com.sun.tools.javac.api.JavacTool; import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.util.Context; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; import javax.tools.JavaFileObject; /** * Compare a file transformed as suggested by {@link BugChecker} to an expected source. * * <p>Inputs are a {@link BugChecker} instance, input file and expected file. * * @author kurs@google.com (Jan Kurs) */ public class BugCheckerRefactoringTestHelper { /** Test mode for matching refactored source against expected source. */ public enum TestMode { TEXT_MATCH { @Override void verifyMatch(JavaFileObject refactoredSource, JavaFileObject expectedSource) throws IOException { assertThat(refactoredSource.getCharContent(false).toString()) .isEqualTo(expectedSource.getCharContent(false).toString()); } }, AST_MATCH { @Override void verifyMatch(JavaFileObject refactoredSource, JavaFileObject expectedSource) { assertAbout(javaSource()).that(refactoredSource).parsesAs(expectedSource); } }; abstract void verifyMatch(JavaFileObject refactoredSource, JavaFileObject expectedSource) throws IOException; } /** * For checks that provide multiple possible fixes, chooses the one that will be applied for the * test. */ public interface FixChooser { Fix choose(List<Fix> fixes); } /** Predefined FixChoosers for selecting a fix by its position in the list */ public enum FixChoosers implements FixChooser { FIRST { @Override public Fix choose(List<Fix> fixes) { return fixes.get(0); } }, SECOND { @Override public Fix choose(List<Fix> fixes) { return fixes.get(1); } }, THIRD { @Override public Fix choose(List<Fix> fixes) { return fixes.get(2); } }, FOURTH { @Override public Fix choose(List<Fix> fixes) { return fixes.get(3); } } } private final Map<JavaFileObject, JavaFileObject> sources = new HashMap<>(); private final BugChecker refactoringBugChecker; private final ErrorProneInMemoryFileManager fileManager; private FixChooser fixChooser = FixChoosers.FIRST; private List<String> options = ImmutableList.of(); private boolean allowBreakingChanges = false; private String importOrder = "static-first"; private BugCheckerRefactoringTestHelper(BugChecker refactoringBugChecker, Class<?> clazz) { this.refactoringBugChecker = refactoringBugChecker; this.fileManager = new ErrorProneInMemoryFileManager(clazz); } public static BugCheckerRefactoringTestHelper newInstance( BugChecker refactoringBugChecker, Class<?> clazz) { return new BugCheckerRefactoringTestHelper(refactoringBugChecker, clazz); } public BugCheckerRefactoringTestHelper.ExpectOutput addInput(String inputFilename) { return new ExpectOutput(fileManager.forResource(inputFilename)); } public BugCheckerRefactoringTestHelper.ExpectOutput addInputLines(String path, String... input) { assertThat(fileManager.exists(path)).isFalse(); return new ExpectOutput(fileManager.forSourceLines(path, input)); } public BugCheckerRefactoringTestHelper setFixChooser(FixChooser chooser) { this.fixChooser = chooser; return this; } public BugCheckerRefactoringTestHelper setArgs(String... args) { this.options = ImmutableList.copyOf(args); return this; } /** If set, fixes that produce output that doesn't compile are allowed. Off by default. */ public BugCheckerRefactoringTestHelper allowBreakingChanges() { allowBreakingChanges = true; return this; } public BugCheckerRefactoringTestHelper setImportOrder(String importOrder) { this.importOrder = importOrder; return this; } public void doTest() throws IOException { this.doTest(TestMode.AST_MATCH); } public void doTest(TestMode testMode) throws IOException { for (Map.Entry<JavaFileObject, JavaFileObject> entry : sources.entrySet()) { runTestOnPair(entry.getKey(), entry.getValue(), testMode); } } private BugCheckerRefactoringTestHelper addInputAndOutput( JavaFileObject input, JavaFileObject output) { sources.put(input, output); return this; } private void runTestOnPair(JavaFileObject input, JavaFileObject output, TestMode testMode) throws IOException { Context context = new Context(); JCCompilationUnit tree = doCompile(input, sources.keySet(), context); JavaFileObject transformed = applyDiff(input, context, tree); testMode.verifyMatch(transformed, output); if (!allowBreakingChanges) { doCompile(output, sources.values(), new Context()); } } private JCCompilationUnit doCompile( final JavaFileObject input, Iterable<JavaFileObject> files, Context context) throws IOException { JavacTool tool = JavacTool.create(); DiagnosticCollector<JavaFileObject> diagnosticsCollector = new DiagnosticCollector<>(); context.put(ErrorProneOptions.class, ErrorProneOptions.empty()); JavacTaskImpl task = (JavacTaskImpl) tool.getTask( CharStreams.nullWriter(), fileManager, diagnosticsCollector, options, /*classes=*/ null, files, context); Iterable<? extends CompilationUnitTree> trees = task.parse(); task.analyze(); JCCompilationUnit tree = Iterables.getOnlyElement( Iterables.filter( Iterables.filter(trees, JCCompilationUnit.class), compilationUnit -> compilationUnit.getSourceFile() == input)); Iterable<Diagnostic<? extends JavaFileObject>> errorDiagnostics = Iterables.filter( diagnosticsCollector.getDiagnostics(), d -> d.getKind() == Diagnostic.Kind.ERROR); if (!Iterables.isEmpty(errorDiagnostics)) { fail("compilation failed unexpectedly: " + errorDiagnostics); } return tree; } private JavaFileObject applyDiff( JavaFileObject sourceFileObject, Context context, JCCompilationUnit tree) throws IOException { ImportOrganizer importOrganizer = ImportOrderParser.getImportOrganizer(importOrder); final DescriptionBasedDiff diff = DescriptionBasedDiff.create(tree, importOrganizer); transformer(refactoringBugChecker) .apply( new TreePath(tree), context, new DescriptionListener() { @Override public void onDescribed(Description description) { if (!description.fixes.isEmpty()) { diff.handleFix(fixChooser.choose(description.fixes)); } } }); SourceFile sourceFile = SourceFile.create(sourceFileObject); diff.applyDifferences(sourceFile); JavaFileObject transformed = JavaFileObjects.forSourceString( Iterables.getOnlyElement(Iterables.filter(tree.getTypeDecls(), JCClassDecl.class)) .sym .getQualifiedName() .toString(), sourceFile.getSourceText()); return transformed; } private ErrorProneScannerTransformer transformer(BugChecker bugChecker) { ErrorProneScanner scanner = new ErrorProneScanner(bugChecker); return ErrorProneScannerTransformer.create(scanner); } /** To assert the proper {@code .addInput().addOutput()} chain. */ public class ExpectOutput { private final JavaFileObject input; public ExpectOutput(JavaFileObject input) { this.input = input; } public BugCheckerRefactoringTestHelper addOutputLines(String path, String... output) throws IOException { if (fileManager.exists(path)) { throw new FileAlreadyExistsException(path); } return addInputAndOutput(input, fileManager.forSourceLines(path, output)); } public BugCheckerRefactoringTestHelper addOutput(String outputFilename) { return addInputAndOutput(input, fileManager.forResource(outputFilename)); } public BugCheckerRefactoringTestHelper expectUnchanged() { return addInputAndOutput(input, input); } } }