com.google.errorprone.BugCheckerRefactoringTestHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.google.errorprone.BugCheckerRefactoringTestHelper.java

Source

/*
 * 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.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 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 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 {
        final DescriptionBasedDiff diff = DescriptionBasedDiff.create(tree);
        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);
        }
    }
}