package org.infernus.idea.checkstyle; import java.io.File; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Future; import com.intellij.openapi.components.ProjectComponent; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleServiceManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.infernus.idea.checkstyle.checker.CheckerFactoryCache; import org.infernus.idea.checkstyle.checker.ConfigurationLocationResult; import org.infernus.idea.checkstyle.checker.Problem; import org.infernus.idea.checkstyle.checker.ScanFiles; import org.infernus.idea.checkstyle.checker.ScannerListener; import org.infernus.idea.checkstyle.checker.UiFeedbackScannerListener; import org.infernus.idea.checkstyle.exception.CheckStylePluginException; import org.infernus.idea.checkstyle.model.ConfigurationLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import static org.infernus.idea.checkstyle.util.Async.executeOnPooledThread; import static org.infernus.idea.checkstyle.util.Async.whenFinished; /** * Main class for the CheckStyle scanning plug-in. */ public final class CheckStylePlugin implements ProjectComponent { /** The plugin ID. Caution: It must be identical to the String set in build.gradle at intellij.pluginName */ public static final String ID_PLUGIN = "CheckStyle-IDEA"; public static final String ID_MODULE_PLUGIN = "CheckStyle-IDEA-Module"; private static final Log LOG = LogFactory.getLog(CheckStylePlugin.class); private final Set<Future<?>> checksInProgress = new HashSet<>(); private final Project project; private final CheckStyleConfiguration configuration; /** * Construct a plug-in instance for the given project. * * @param project the current project. */ public CheckStylePlugin(@NotNull final Project project) { this.project = project; this.configuration = CheckStyleConfiguration.getInstance(project); LOG.info("CheckStyle Plugin loaded with project base dir: \"" + getProjectPath() + "\""); disableCheckStyleLogging(); } private void disableCheckStyleLogging() { try { // This is a nasty hack to get around IDEA's DialogAppender sending any errors to the Event Log, // which would result in CheckStyle parse errors spamming the Event Log. Logger.getLogger("com.puppycrawl.tools.checkstyle.TreeWalker").setLevel(Level.OFF); } catch (Exception e) { LOG.error("Unable to suppress logging from CheckStyle's TreeWalker", e); } } public Project getProject() { return project; } @Nullable private File getProjectPath() { final VirtualFile baseDir = project.getBaseDir(); if (baseDir == null) { return null; } return new File(baseDir.getPath()); } /** * Get the plugin configuration. * * @return the plug-in configuration. */ public CheckStyleConfiguration getConfiguration() { return configuration; } /** * Is a scan in progress? * <p> * This is only expected to be called from the event thread. * * @return true if a scan is in progress. */ public boolean isScanInProgress() { synchronized (checksInProgress) { return !checksInProgress.isEmpty(); } } public void projectOpened() { LOG.debug("Project opened."); } public void projectClosed() { LOG.debug("Project closed; invalidating checkers."); invalidateCheckerCache(); } private void invalidateCheckerCache() { ServiceManager.getService(CheckerFactoryCache.class).invalidate(); } @NotNull public String getComponentName() { return ID_PLUGIN; } public void initComponent() { } public void disposeComponent() { } public static void processErrorAndLog(@NotNull final String action, @NotNull final Throwable e) { LOG.error(action + " failed", e); } private <T> Future<T> checkInProgress(final Future<T> checkFuture) { synchronized (checksInProgress) { if (!checkFuture.isDone()) { checksInProgress.add(checkFuture); } } return checkFuture; } public void stopChecks() { synchronized (checksInProgress) { checksInProgress.forEach(task -> task.cancel(true)); checksInProgress.clear(); } } public <T> void checkComplete(final Future<T> task) { if (task == null) { return; } synchronized (checksInProgress) { checksInProgress.remove(task); } } public void asyncScanFiles(final List<VirtualFile> files, final ConfigurationLocation overrideConfigLocation) { LOG.info("Scanning current file(s)."); if (files == null || files.isEmpty()) { LOG.debug("No files provided."); return; } final ScanFiles checkFiles = new ScanFiles(this, files, overrideConfigLocation); checkFiles.addListener(new UiFeedbackScannerListener(this)); runAsyncCheck(checkFiles); } public Map<PsiFile, List<Problem>> scanFiles(@NotNull final List<VirtualFile> files) { if (files.isEmpty()) { return Collections.emptyMap(); } try { return whenFinished(runAsyncCheck(new ScanFiles(this, files, null))).get(); } catch (final Throwable e) { LOG.error("Error scanning files", e); return Collections.emptyMap(); } } private Future<Map<PsiFile, List<Problem>>> runAsyncCheck(final ScanFiles checker) { final Future<Map<PsiFile, List<Problem>>> checkFilesFuture = checkInProgress(executeOnPooledThread(checker)); checker.addListener(new ScanCompletionTracker(checkFilesFuture)); return checkFilesFuture; } public ConfigurationLocation getConfigurationLocation(@Nullable final Module module, @Nullable final ConfigurationLocation override) { if (override != null) { return override; } if (module != null) { final CheckStyleModuleConfiguration moduleConfiguration = ModuleServiceManager.getService(module, CheckStyleModuleConfiguration.class); if (moduleConfiguration == null) { throw new IllegalStateException("Couldn't get checkstyle module configuration"); } if (moduleConfiguration.isExcluded()) { return null; } return moduleConfiguration.getActiveConfiguration(); } return getConfiguration().getActiveConfiguration(); } private class ScanCompletionTracker implements ScannerListener { private final Future<Map<PsiFile, List<Problem>>> future; ScanCompletionTracker(final Future<Map<PsiFile, List<Problem>>> future) { this.future = future; } @Override public void scanStarting(final List<PsiFile> filesToScan) { } @Override public void filesScanned(final int count) { } @Override public void scanComplete(final ConfigurationLocationResult configurationLocationResult, final Map<PsiFile, List<Problem>> scanResults) { checkComplete(future); } @Override public void errorCaught(final CheckStylePluginException error) { } } }