/* * 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 java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.StandardOpenOption.APPEND; import static java.nio.file.StandardOpenOption.CREATE; import com.google.auto.value.AutoValue; import com.google.common.collect.HashMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.errorprone.ErrorProneOptions.PatchingOptions; import com.google.errorprone.apply.DescriptionBasedDiff; import com.google.errorprone.apply.FileDestination; import com.google.errorprone.apply.FileSource; import com.google.errorprone.apply.FsFileDestination; import com.google.errorprone.apply.FsFileSource; import com.google.errorprone.apply.ImportOrganizer; import com.google.errorprone.apply.PatchFileDestination; import com.google.errorprone.apply.SourceFile; import com.google.errorprone.matchers.Description; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.util.Log; import java.io.IOError; import java.io.IOException; import java.net.URI; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; /** A container of fixes that have been collected during a single compilation phase. */ class RefactoringCollection implements DescriptionListener.Factory { private static final Logger logger = Logger.getLogger(RefactoringCollection.class.getName()); private final Multimap<URI, DelegatingDescriptionListener> foundSources = HashMultimap.create(); private final Path rootPath; private final FileDestination fileDestination; private final Function<URI, RefactoringResult> postProcess; private final DescriptionListener.Factory descriptionsFactory; private final ImportOrganizer importOrganizer; @AutoValue abstract static class RefactoringResult { abstract String message(); abstract RefactoringResultType type(); private static RefactoringResult create(String message, RefactoringResultType type) { return new AutoValue_RefactoringCollection_RefactoringResult(message, type); } } enum RefactoringResultType { NO_CHANGES, CHANGED, } static RefactoringCollection refactor(PatchingOptions patchingOptions) { Path rootPath = buildRootPath(); FileDestination fileDestination; Function<URI, RefactoringResult> postProcess; if (patchingOptions.inPlace()) { fileDestination = new FsFileDestination(rootPath); postProcess = uri -> RefactoringResult.create( String.format( "Refactoring changes were successfully applied to %s," + " please check the refactored code and recompile.", uri), RefactoringResultType.CHANGED); } else { Path baseDir = rootPath.resolve(patchingOptions.baseDirectory()); Path patchFilePath = baseDir.resolve("error-prone.patch"); PatchFileDestination patchFileDestination = new PatchFileDestination(baseDir, rootPath); postProcess = new Function<URI, RefactoringResult>() { private final AtomicBoolean first = new AtomicBoolean(true); @Override public RefactoringResult apply(URI uri) { try { writePatchFile(first, uri, patchFileDestination, patchFilePath); return RefactoringResult.create( "Changes were written to " + patchFilePath + ". Please inspect the file and apply with: " + "patch -p0 -u -i error-prone.patch", RefactoringResultType.CHANGED); } catch (IOException e) { throw new RuntimeException("Failed to emit patch file!", e); } } }; fileDestination = patchFileDestination; } ImportOrganizer importOrganizer = patchingOptions.importOrganizer(); return new RefactoringCollection(rootPath, fileDestination, postProcess, importOrganizer); } private RefactoringCollection( Path rootPath, FileDestination fileDestination, Function<URI, RefactoringResult> postProcess, ImportOrganizer importOrganizer) { this.rootPath = rootPath; this.fileDestination = fileDestination; this.postProcess = postProcess; this.descriptionsFactory = JavacErrorDescriptionListener.providerForRefactoring(); this.importOrganizer = importOrganizer; } private static Path buildRootPath() { Path root = Iterables.getFirst(FileSystems.getDefault().getRootDirectories(), null); if (root == null) { throw new RuntimeException("Can't find a root filesystem!"); } return root; } @Override public DescriptionListener getDescriptionListener(Log log, JCCompilationUnit compilation) { URI sourceFile = compilation.getSourceFile().toUri(); DelegatingDescriptionListener delegate = new DelegatingDescriptionListener( descriptionsFactory.getDescriptionListener(log, compilation), DescriptionBasedDiff.createIgnoringOverlaps(compilation, importOrganizer)); foundSources.put(sourceFile, delegate); return delegate; } RefactoringResult applyChanges(URI uri) throws Exception { Collection<DelegatingDescriptionListener> listeners = foundSources.removeAll(uri); if (listeners.isEmpty()) { return RefactoringResult.create("", RefactoringResultType.NO_CHANGES); } doApplyProcess(fileDestination, new FsFileSource(rootPath), listeners); return postProcess.apply(uri); } private static void writePatchFile( AtomicBoolean first, URI uri, PatchFileDestination fileDestination, Path patchFilePatch) throws IOException { String patchFile = fileDestination.patchFile(uri); if (patchFile != null) { if (first.compareAndSet(true, false)) { try { Files.deleteIfExists(patchFilePatch); } catch (IOException e) { throw new IOError(e); } } Files.write(patchFilePatch, patchFile.getBytes(UTF_8), APPEND, CREATE); } } private void doApplyProcess( FileDestination fileDestination, FileSource fileSource, Collection<DelegatingDescriptionListener> listeners) { for (DelegatingDescriptionListener listener : listeners) { try { SourceFile file = fileSource.readFile(listener.base.getRelevantFileName()); listener.base.applyDifferences(file); fileDestination.writeFile(file); } catch (IOException e) { logger.log( Level.WARNING, "Failed to apply diff to file " + listener.base.getRelevantFileName(), e); } } } private final class DelegatingDescriptionListener implements DescriptionListener { final DescriptionBasedDiff base; final DescriptionListener listener; DelegatingDescriptionListener(DescriptionListener listener, DescriptionBasedDiff base) { this.listener = listener; this.base = base; } @Override public void onDescribed(Description description) { listener.onDescribed(description); base.onDescribed(description); } } }