diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/FileFormatter.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/FileFormatter.java index 55de5a7..d7b8e35 100644 --- a/src/main/java/com/funbiscuit/idea/plugin/formatter/FileFormatter.java +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/FileFormatter.java @@ -1,5 +1,6 @@ package com.funbiscuit.idea.plugin.formatter; +import com.funbiscuit.idea.plugin.formatter.report.FileInfo; import com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor; import com.intellij.codeInsight.actions.RearrangeCodeProcessor; import com.intellij.codeInsight.actions.ReformatCodeProcessor; @@ -12,22 +13,16 @@ import java.util.Objects; public class FileFormatter implements FileProcessor { - private static final String PROCESS_RESULT_OK = "OK"; - private static final String PROCESS_RESULT_READ_ONLY = "Skipped, read only"; - - private final FormatStatistics statistics; - - public FileFormatter(FormatStatistics statistics) { - this.statistics = statistics; - } @Override - public String processFile(PsiFile originalFile) { + public void processFile(PsiFile originalFile, FileInfo fileInfo) { + String originalContent = originalFile.getText(); FileDocumentManager documentManager = FileDocumentManager.getInstance(); VirtualFile virtualFile = PsiUtilCore.getVirtualFile(originalFile); var document = documentManager.getDocument(Objects.requireNonNull(virtualFile)); if (!documentManager.requestWriting(Objects.requireNonNull(document), null)) { - return PROCESS_RESULT_READ_ONLY; + fileInfo.addWarning(ProcessStatuses.SKIPPED_READ_ONLY); + return; } AbstractLayoutCodeProcessor processor = new ReformatCodeProcessor(originalFile, false); @@ -35,12 +30,10 @@ public String processFile(PsiFile originalFile) { NonProjectFileWritingAccessProvider.disableChecksDuring(processor::run); FileDocumentManager.getInstance().saveDocument(document); - statistics.fileProcessed(true); - return PROCESS_RESULT_OK; - } - - @Override - public String actionMessage() { - return "Formatting"; + if (originalFile.getText().equals(originalContent)) { + fileInfo.addInfo(ProcessStatuses.FORMATTED_WELL); + } else { + fileInfo.addInfo(ProcessStatuses.FORMATTED); + } } } diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/FileProcessor.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/FileProcessor.java index b47215f..90ed0a8 100644 --- a/src/main/java/com/funbiscuit/idea/plugin/formatter/FileProcessor.java +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/FileProcessor.java @@ -1,9 +1,8 @@ package com.funbiscuit.idea.plugin.formatter; +import com.funbiscuit.idea.plugin.formatter.report.FileInfo; import com.intellij.psi.PsiFile; public interface FileProcessor { - String processFile(PsiFile originalFile); - - String actionMessage(); + void processFile(PsiFile originalFile, FileInfo fileInfo); } diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/FileVerifier.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/FileVerifier.java index d881b55..2e87103 100644 --- a/src/main/java/com/funbiscuit/idea/plugin/formatter/FileVerifier.java +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/FileVerifier.java @@ -1,5 +1,6 @@ package com.funbiscuit.idea.plugin.formatter; +import com.funbiscuit.idea.plugin.formatter.report.FileInfo; import com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor; import com.intellij.codeInsight.actions.RearrangeCodeProcessor; import com.intellij.codeInsight.actions.ReformatCodeProcessor; @@ -16,16 +17,12 @@ import java.util.UUID; public class FileVerifier implements FileProcessor { - private static final String PROCESS_RESULT_DRY_OK = "Formatted well"; - private static final String PROCESS_RESULT_DRY_FAIL = "Needs formatting"; private final Project project; private final PsiDirectory projectPsiDir; - private final FormatStatistics statistics; - public FileVerifier(Project project, FormatStatistics statistics) { + public FileVerifier(Project project) { this.project = project; - this.statistics = statistics; VirtualFile baseDir = ProjectUtil.guessProjectDir(project); if (baseDir == null) { @@ -38,7 +35,7 @@ public FileVerifier(Project project, FormatStatistics statistics) { } @Override - public String processFile(PsiFile originalFile) { + public void processFile(PsiFile originalFile, FileInfo fileInfo) { String originalContent = originalFile.getText(); PsiFile processedFile = createFileCopy(originalFile, originalContent); @@ -48,19 +45,12 @@ public String processFile(PsiFile originalFile) { processor.run(); if (processedFile.getText().equals(originalContent)) { - statistics.fileProcessed(true); - return PROCESS_RESULT_DRY_OK; + fileInfo.addInfo(ProcessStatuses.FORMATTED_WELL); } else { - statistics.fileProcessed(false); - return PROCESS_RESULT_DRY_FAIL; + fileInfo.addError(ProcessStatuses.NEEDS_FORMATTING); } } - @Override - public String actionMessage() { - return "Checking"; - } - private PsiFile createFileCopy(PsiFile originalFile, String content) { return ApplicationManager.getApplication().runWriteAction((Computable) () -> { var psiCopy = PsiFileFactory.getInstance(project).createFileFromText( diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/FormatCommand.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/FormatCommand.java index eabf1ec..9dce92a 100644 --- a/src/main/java/com/funbiscuit/idea/plugin/formatter/FormatCommand.java +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/FormatCommand.java @@ -1,5 +1,6 @@ package com.funbiscuit.idea.plugin.formatter; +import com.funbiscuit.idea.plugin.formatter.report.*; import com.intellij.application.options.CodeStyle; import com.intellij.formatting.commandLine.StdIoMessageOutput; import com.intellij.ide.RecentProjectsManager; @@ -11,7 +12,6 @@ import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; -import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.impl.source.codeStyle.CodeStyleSettingsLoader; import picocli.CommandLine; @@ -22,6 +22,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.UUID; import java.util.concurrent.Callable; @@ -35,9 +37,6 @@ public class FormatCommand implements Callable { private static final StdIoMessageOutput messageOutput = StdIoMessageOutput.INSTANCE; private static final String PROJECT_DIR_PREFIX = "idea.reformat."; private static final String PROJECT_DIR_SUFFIX = ".tmp"; - private static final String PROCESS_RESULT_BINARY_FILE = "Skipped, binary file."; - private static final String PROCESS_RESULT_FAILED_OPEN = "Failed to open."; - private static final String PROCESS_RESULT_FAILED_TO_PROCESS = "Failed to process."; @Option(names = {"-s", "--style"}, required = true, description = "A path to Intellij IDEA code style settings .xml file") private Path style; @@ -52,6 +51,9 @@ public class FormatCommand implements Callable { @Option(names = {"-d", "--dry"}, description = "Perform a dry run: no file modifications, only exit status") private boolean dry; + @Option(names = {"--report"}, description = "Where to save report (by default not saved). Supported extensions: '.html'") + private Path report; + @Parameters(index = "1..*", paramLabel = "", description = "A path to a file or a directory") private List files = List.of(); @@ -62,6 +64,17 @@ public class FormatCommand implements Callable { private Project project; private FileProcessor fileProcessor; + private static Reporter createReporterForFile(Path file) { + if (file == null) { + return null; + } + if (file.toString().endsWith(".html")) { + return new HtmlReporter(); + } else { + return null; + } + } + @Override public Integer call() throws Exception { Predicate fileNamePredicate = masks.stream() @@ -75,74 +88,113 @@ public Integer call() throws Exception { .reduce(Predicate::or) .orElse(p -> true); + var reporter = createReporterForFile(report); + if (report != null && reporter == null) { + messageOutput.info("Given report file extension is not supported\n"); + return CommandLine.ExitCode.SOFTWARE; + } + createTempProject(); loadSettings(style); - FormatStatistics statistics = new FormatStatistics(); if (dry) { - fileProcessor = new FileVerifier(project, statistics); + fileProcessor = new FileVerifier(project); } else { - fileProcessor = new FileFormatter(statistics); + fileProcessor = new FileFormatter(); } + List allFiles = new ArrayList<>(); + messageOutput.info("Counting files...\n"); for (var path : files) { try (var stream = Files.walk(path, recursive ? Integer.MAX_VALUE : 1)) { - stream + List dirFiles = stream .filter(Files::isRegularFile) .filter(p -> fileNamePredicate.test(p.toString())) - .forEach(this::processPath); + .toList(); + allFiles.addAll(dirFiles); + } + } + messageOutput.info("Processing %d files%n".formatted(allFiles.size())); + List fileInfos = new ArrayList<>(); + + var lastPrintTime = System.currentTimeMillis(); + for (int i = 0; i < allFiles.size(); i++) { + Path file = allFiles.get(i); + fileInfos.add(processPath(file)); + + var now = System.currentTimeMillis(); + if (now - lastPrintTime > 1000L) { + lastPrintTime = now; + messageOutput.info("Processed %d/%d files%n".formatted(i + 1, allFiles.size())); } } + messageOutput.info("Processed %d files%n".formatted(allFiles.size())); RecentProjectsManager.getInstance().removePath(projectPath.toString()); - messageOutput.info("Processed: %d%n".formatted(statistics.getProcessed())); - return statistics.allValid() ? CommandLine.ExitCode.OK : CommandLine.ExitCode.SOFTWARE; + var withErrors = fileInfos.stream().anyMatch(FileInfo::hasErrors); + + + List processResults = fileInfos.stream() + .flatMap(info -> info.getProcessLog().stream() + .map(entry -> new ProcessResult(info.getFilename(), entry.level(), entry.message()))) + .sorted(Comparator.comparing(ProcessResult::level).reversed().thenComparing(ProcessResult::filename)) + .toList(); + + if (reporter != null) { + reporter.generate(report, processResults); + } else { + // print report to stdout only when no report file is given + processResults.stream() + .filter(status -> status.level() != Level.INFO) + .forEach(status -> messageOutput.info( + "%s %s: %s%n".formatted(status.filename(), status.level(), status.status()) + )); + } + + return withErrors ? CommandLine.ExitCode.SOFTWARE : CommandLine.ExitCode.OK; } - private void processPath(Path filePath) { - messageOutput.info("%s %s... ".formatted(fileProcessor.actionMessage(), filePath)); - String result; + private FileInfo processPath(Path filePath) { Exception ex = null; + var fileInfo = new FileInfo(filePath.toString()); try { - result = processPathInternal(filePath); + processPathInternal(filePath, fileInfo); } catch (Exception e) { - result = PROCESS_RESULT_FAILED_TO_PROCESS; ex = e; + fileInfo.addWarning(ProcessStatuses.FAILED_TO_PROCESS); } - messageOutput.info("%s%n".formatted(result)); if (ex != null) { ex.printStackTrace(); } + return fileInfo; } - private String processPathInternal(Path filePath) { + private void processPathInternal(Path filePath, FileInfo fileInfo) { PsiManager psiManager = PsiManager.getInstance(project); var virtualFile = VirtualFileManager.getInstance().refreshAndFindFileByNioPath(filePath); if (virtualFile == null) { - return PROCESS_RESULT_FAILED_OPEN; + fileInfo.addWarning(ProcessStatuses.FAILED_TO_OPEN); + return; } virtualFile.refresh(false, false); if (virtualFile.getFileType().isBinary()) { - return PROCESS_RESULT_BINARY_FILE; + fileInfo.addWarning(ProcessStatuses.SKIPPED_BINARY_FILE); + return; } var psiFile = psiManager.findFile(virtualFile); if (psiFile == null) { - return PROCESS_RESULT_FAILED_OPEN; + fileInfo.addWarning(ProcessStatuses.FAILED_TO_OPEN); + return; } - return processPsiFile(psiFile); - } - - - private String processPsiFile(PsiFile originalFile) { - return fileProcessor.processFile(originalFile); + fileProcessor.processFile(psiFile, fileInfo); } private void loadSettings(Path stylePath) throws SchemeImportException { diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/FormatStatistics.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/FormatStatistics.java deleted file mode 100644 index c4c2d22..0000000 --- a/src/main/java/com/funbiscuit/idea/plugin/formatter/FormatStatistics.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.funbiscuit.idea.plugin.formatter; - -import java.util.concurrent.atomic.AtomicInteger; - -public class FormatStatistics { - private final AtomicInteger processed = new AtomicInteger(); - private final AtomicInteger valid = new AtomicInteger(); - - public void fileProcessed(boolean isValid) { - processed.incrementAndGet(); - if (isValid) { - valid.incrementAndGet(); - } - } - - public int getProcessed() { - return processed.get(); - } - - public boolean allValid() { - return valid.get() == processed.get(); - } -} diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/ProcessStatuses.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/ProcessStatuses.java new file mode 100644 index 0000000..a84da98 --- /dev/null +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/ProcessStatuses.java @@ -0,0 +1,18 @@ +package com.funbiscuit.idea.plugin.formatter; + +/** + * @author skokurin + * @since 08.07.2023 + */ +public final class ProcessStatuses { + public static final String SKIPPED_BINARY_FILE = "Skipped, binary file"; + public static final String FAILED_TO_OPEN = "Failed to open"; + public static final String FAILED_TO_PROCESS = "Failed to process"; + public static final String SKIPPED_READ_ONLY = "Skipped, read only"; + public static final String FORMATTED = "Formatted"; + public static final String FORMATTED_WELL = "Formatted well"; + public static final String NEEDS_FORMATTING = "Needs formatting"; + + private ProcessStatuses() { + } +} diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/report/FileInfo.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/FileInfo.java new file mode 100644 index 0000000..48cb03d --- /dev/null +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/FileInfo.java @@ -0,0 +1,38 @@ +package com.funbiscuit.idea.plugin.formatter.report; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class FileInfo { + private final String filename; + private final List processLog = new ArrayList<>(); + + public FileInfo(String filename) { + this.filename = filename; + } + + public void addInfo(String message) { + processLog.add(new LogEntry(Level.INFO, message)); + } + + public void addWarning(String message) { + processLog.add(new LogEntry(Level.WARN, message)); + } + + public void addError(String message) { + processLog.add(new LogEntry(Level.ERROR, message)); + } + + public boolean hasErrors() { + return processLog.stream().anyMatch(p -> p.level() == Level.ERROR); + } + + public String getFilename() { + return filename; + } + + public List getProcessLog() { + return processLog.stream().sorted(Comparator.comparing(LogEntry::level).reversed()).toList(); + } +} diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/report/HtmlReporter.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/HtmlReporter.java new file mode 100644 index 0000000..0664559 --- /dev/null +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/HtmlReporter.java @@ -0,0 +1,91 @@ +package com.funbiscuit.idea.plugin.formatter.report; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; +import java.util.Map; + +public class HtmlReporter implements Reporter { + private static final String ROW_TEMPLATE = """ + + %1$s + %2$s + + """; + private static final String TABLE_BEGIN = """ + + + + + + + + + """; + private static final String TABLE_END = "
FileStatus
"; + private static final String HTML_BEGIN = """ + + + + + Formatter report + + +
+ + """; + private static final String HTML_END = """ +
+
+
+

Date: %s

+
+
+ + + """; + private static final Map LEVEL_CLASSES = Map.of( + Level.INFO, "", + Level.WARN, "has-background-warning", + Level.ERROR, "has-background-danger" + ); + + @Override + public void generate(Path output, List processResults) throws IOException { + long warnings = processResults.stream().filter(processResult -> processResult.level() == Level.WARN).count(); + long errors = processResults.stream().filter(processResult -> processResult.level() == Level.ERROR).count(); + + try (var writer = Files.newBufferedWriter(output)) { + writer.write(HTML_BEGIN.formatted(processResults.size(), warnings, errors)); + writer.write(TABLE_BEGIN); + for (ProcessResult processResult : processResults) { + writer.write(ROW_TEMPLATE.formatted( + processResult.filename(), processResult.status(), LEVEL_CLASSES.get(processResult.level()) + )); + } + writer.write(TABLE_END); + writer.write(HTML_END.formatted(Instant.now())); + } + } +} diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/report/Level.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/Level.java new file mode 100644 index 0000000..54f3f80 --- /dev/null +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/Level.java @@ -0,0 +1,7 @@ +package com.funbiscuit.idea.plugin.formatter.report; + +public enum Level { + INFO, + WARN, + ERROR +} diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/report/LogEntry.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/LogEntry.java new file mode 100644 index 0000000..71436d7 --- /dev/null +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/LogEntry.java @@ -0,0 +1,4 @@ +package com.funbiscuit.idea.plugin.formatter.report; + +public record LogEntry(Level level, String message) { +} diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/report/ProcessResult.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/ProcessResult.java new file mode 100644 index 0000000..973c0f3 --- /dev/null +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/ProcessResult.java @@ -0,0 +1,4 @@ +package com.funbiscuit.idea.plugin.formatter.report; + +public record ProcessResult(String filename, Level level, String status) { +} diff --git a/src/main/java/com/funbiscuit/idea/plugin/formatter/report/Reporter.java b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/Reporter.java new file mode 100644 index 0000000..015945f --- /dev/null +++ b/src/main/java/com/funbiscuit/idea/plugin/formatter/report/Reporter.java @@ -0,0 +1,9 @@ +package com.funbiscuit.idea.plugin.formatter.report; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +public interface Reporter { + void generate(Path output, List processResults) throws IOException; +}