diff --git a/build.gradle.kts b/build.gradle.kts index 9591ec2f..68ebce1a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -85,8 +85,10 @@ dependencies { intellijIdeaCommunity("2024.1.7") bundledPlugin("com.intellij.java") + bundledPlugin("org.jetbrains.idea.maven") testFramework(TestFrameworkType.Platform) + testFramework(TestFrameworkType.Plugin.Maven) } implementation("commons-io:commons-io:2.20.0") diff --git a/src/main/java/org/infernus/idea/checkstyle/config/PluginConfiguration.java b/src/main/java/org/infernus/idea/checkstyle/config/PluginConfiguration.java index a294bc0e..3eb010e2 100644 --- a/src/main/java/org/infernus/idea/checkstyle/config/PluginConfiguration.java +++ b/src/main/java/org/infernus/idea/checkstyle/config/PluginConfiguration.java @@ -24,6 +24,7 @@ public class PluginConfiguration { private final List thirdPartyClasspath; private final SortedSet activeLocationIds; private final boolean scanBeforeCheckin; + private final boolean importSettingsFromMaven; PluginConfiguration(@NotNull final String checkstyleVersion, @NotNull final ScanScope scanScope, @@ -33,7 +34,8 @@ public class PluginConfiguration { @NotNull final SortedSet locations, @NotNull final List thirdPartyClasspath, @NotNull final SortedSet activeLocationIds, - final boolean scanBeforeCheckin) { + final boolean scanBeforeCheckin, + final boolean importSettingsFromMaven) { this.checkstyleVersion = checkstyleVersion; this.scanScope = scanScope; this.suppressErrors = suppressErrors; @@ -45,6 +47,7 @@ public class PluginConfiguration { .filter(Objects::nonNull) .collect(Collectors.toCollection(TreeSet::new)); this.scanBeforeCheckin = scanBeforeCheckin; + this.importSettingsFromMaven = importSettingsFromMaven; } @NotNull @@ -103,6 +106,10 @@ public boolean isScanBeforeCheckin() { return scanBeforeCheckin; } + public boolean isImportSettingsFromMaven() { + return importSettingsFromMaven; + } + public boolean hasChangedFrom(final Object other) { return this.equals(other) && locationsAreEqual((PluginConfiguration) other); } @@ -137,13 +144,14 @@ public boolean equals(final Object other) { && Objects.equals(locations, otherDto.locations) && Objects.equals(thirdPartyClasspath, otherDto.thirdPartyClasspath) && Objects.equals(activeLocationIds, otherDto.activeLocationIds) - && Objects.equals(scanBeforeCheckin, otherDto.scanBeforeCheckin); + && Objects.equals(scanBeforeCheckin, otherDto.scanBeforeCheckin) + && Objects.equals(importSettingsFromMaven, otherDto.importSettingsFromMaven); } @Override public int hashCode() { return Objects.hash(checkstyleVersion, scanScope, suppressErrors, copyLibs, scrollToSource, - locations, thirdPartyClasspath, activeLocationIds, scanBeforeCheckin); + locations, thirdPartyClasspath, activeLocationIds, scanBeforeCheckin, importSettingsFromMaven); } } diff --git a/src/main/java/org/infernus/idea/checkstyle/config/PluginConfigurationBuilder.java b/src/main/java/org/infernus/idea/checkstyle/config/PluginConfigurationBuilder.java index 39e5fad0..9b995932 100644 --- a/src/main/java/org/infernus/idea/checkstyle/config/PluginConfigurationBuilder.java +++ b/src/main/java/org/infernus/idea/checkstyle/config/PluginConfigurationBuilder.java @@ -21,6 +21,7 @@ public final class PluginConfigurationBuilder { private List thirdPartyClasspath; private SortedSet activeLocationIds; private boolean scanBeforeCheckin; + private boolean importSettingsFromMaven; private PluginConfigurationBuilder(@NotNull final String checkstyleVersion, @NotNull final ScanScope scanScope, @@ -30,7 +31,8 @@ private PluginConfigurationBuilder(@NotNull final String checkstyleVersion, @NotNull final SortedSet locations, @NotNull final List thirdPartyClasspath, @NotNull final SortedSet activeLocationIds, - final boolean scanBeforeCheckin) { + final boolean scanBeforeCheckin, + final boolean importSettingsFromMaven) { this.checkstyleVersion = checkstyleVersion; this.scanScope = scanScope; this.suppressErrors = suppressErrors; @@ -40,6 +42,7 @@ private PluginConfigurationBuilder(@NotNull final String checkstyleVersion, this.thirdPartyClasspath = thirdPartyClasspath; this.activeLocationIds = activeLocationIds; this.scanBeforeCheckin = scanBeforeCheckin; + this.importSettingsFromMaven = importSettingsFromMaven; } public static PluginConfigurationBuilder defaultConfiguration(@NotNull final Project project) { @@ -60,6 +63,7 @@ public static PluginConfigurationBuilder defaultConfiguration(@NotNull final Pro defaultLocations, Collections.emptyList(), Collections.emptySortedSet(), + false, false); } @@ -73,6 +77,7 @@ public static PluginConfigurationBuilder testInstance(@NotNull final String chec Collections.emptySortedSet(), Collections.emptyList(), Collections.emptySortedSet(), + false, false); } @@ -85,7 +90,8 @@ public static PluginConfigurationBuilder from(@NotNull final PluginConfiguration source.getLocations(), source.getThirdPartyClasspath(), source.getActiveLocationIds(), - source.isScanBeforeCheckin()); + source.isScanBeforeCheckin(), + source.isImportSettingsFromMaven()); } public PluginConfigurationBuilder withCheckstyleVersion(@NotNull final String newCheckstyleVersion) { @@ -133,6 +139,11 @@ public PluginConfigurationBuilder withScanScope(@NotNull final ScanScope newScan return this; } + public PluginConfigurationBuilder withImportSettingsFromMaven(final boolean importSettingsFromMaven) { + this.importSettingsFromMaven = importSettingsFromMaven; + return this; + } + public PluginConfiguration build() { return new PluginConfiguration( checkstyleVersion, @@ -143,7 +154,8 @@ public PluginConfiguration build() { Objects.requireNonNullElseGet(locations, TreeSet::new), Objects.requireNonNullElseGet(thirdPartyClasspath, ArrayList::new), Objects.requireNonNullElseGet(activeLocationIds, TreeSet::new), - scanBeforeCheckin); + scanBeforeCheckin, + importSettingsFromMaven); } private static ConfigurationLocationFactory configurationLocationFactory(final Project project) { diff --git a/src/main/java/org/infernus/idea/checkstyle/config/ProjectConfigurationState.java b/src/main/java/org/infernus/idea/checkstyle/config/ProjectConfigurationState.java index 853e06f3..fa73d360 100644 --- a/src/main/java/org/infernus/idea/checkstyle/config/ProjectConfigurationState.java +++ b/src/main/java/org/infernus/idea/checkstyle/config/ProjectConfigurationState.java @@ -84,6 +84,8 @@ static class ProjectSettings { private boolean scrollToSource; @Tag private boolean scanBeforeCheckin; + @Tag + private boolean importSettingsFromMaven; @XCollection private List thirdPartyClasspath; @XCollection @@ -105,6 +107,7 @@ static ProjectSettings create(@NotNull final PluginConfiguration currentPluginCo projectSettings.copyLibs = currentPluginConfig.isCopyLibs(); projectSettings.scrollToSource = currentPluginConfig.isScrollToSource(); projectSettings.scanBeforeCheckin = currentPluginConfig.isScanBeforeCheckin(); + projectSettings.importSettingsFromMaven = currentPluginConfig.isImportSettingsFromMaven(); projectSettings.thirdPartyClasspath = new ArrayList<>(currentPluginConfig.getThirdPartyClasspath()); projectSettings.activeLocationIds = new ArrayList<>(currentPluginConfig.getActiveLocationIds()); @@ -151,7 +154,8 @@ PluginConfigurationBuilder populate(@NotNull final PluginConfigurationBuilder bu .withScanBeforeCheckin(scanBeforeCheckin) .withThirdPartyClassPath(requireNonNullElseGet(thirdPartyClasspath, ArrayList::new)) .withLocations(deserialiseLocations(project)) - .withActiveLocationIds(new TreeSet<>(requireNonNullElseGet(activeLocationIds, ArrayList::new))); + .withActiveLocationIds(new TreeSet<>(requireNonNullElseGet(activeLocationIds, ArrayList::new))) + .withImportSettingsFromMaven(importSettingsFromMaven); } return new LegacyProjectConfigurationStateDeserialiser(project) diff --git a/src/main/java/org/infernus/idea/checkstyle/maven/MavenCheckstyleConfigurator.java b/src/main/java/org/infernus/idea/checkstyle/maven/MavenCheckstyleConfigurator.java new file mode 100644 index 00000000..051c2ee2 --- /dev/null +++ b/src/main/java/org/infernus/idea/checkstyle/maven/MavenCheckstyleConfigurator.java @@ -0,0 +1,498 @@ +package org.infernus.idea.checkstyle.maven; + +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.TreeSet; +import java.util.stream.StreamSupport; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.BuildersKt; +import org.infernus.idea.checkstyle.CheckstyleProjectService; +import org.infernus.idea.checkstyle.config.PluginConfiguration; +import org.infernus.idea.checkstyle.config.PluginConfigurationBuilder; +import org.infernus.idea.checkstyle.config.PluginConfigurationManager; +import org.infernus.idea.checkstyle.exception.CheckStylePluginException; +import org.infernus.idea.checkstyle.model.ConfigurationLocation; +import org.infernus.idea.checkstyle.model.ConfigurationLocationFactory; +import org.infernus.idea.checkstyle.model.ConfigurationType; +import org.infernus.idea.checkstyle.model.NamedScopeHelper; +import org.infernus.idea.checkstyle.model.ScanScope; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.idea.maven.buildtool.MavenEventHandler; +import org.jetbrains.idea.maven.dom.MavenDomUtil; +import org.jetbrains.idea.maven.dom.MavenPropertyResolver; +import org.jetbrains.idea.maven.importing.MavenAfterImportConfigurator; +import org.jetbrains.idea.maven.importing.MavenWorkspaceConfigurator.MavenProjectWithModules; +import org.jetbrains.idea.maven.model.MavenArtifactInfo; +import org.jetbrains.idea.maven.model.MavenId; +import org.jetbrains.idea.maven.model.MavenPlugin; +import org.jetbrains.idea.maven.project.MavenEmbeddersManager; +import org.jetbrains.idea.maven.project.MavenProject; +import org.jetbrains.idea.maven.server.MavenArtifactEvent; +import org.jetbrains.idea.maven.server.MavenArtifactResolutionRequest; +import org.jetbrains.idea.maven.server.MavenServerConsoleEvent; +import org.jetbrains.idea.maven.server.MavenServerConsoleIndicator; +import org.jetbrains.idea.maven.utils.MavenArtifactUtil; +import org.jetbrains.idea.maven.utils.MavenLog; +import org.jetbrains.idea.maven.utils.MavenProcessCanceledException; + +/** + * Importer to automatically configure the Checkstyle IntelliJ plugin settings based on the + * Checkstyle Maven plugin configuration. + * + *

Only configures project settings at this time and does not modify module settings. + */ +@SuppressWarnings("UnstableApiUsage") +public class MavenCheckstyleConfigurator implements MavenAfterImportConfigurator { + + private static final MavenId CHECKSTYLE_MAVEN_ID = new MavenId("com.puppycrawl.tools", + "checkstyle", null); + private static final MavenId MAVEN_CHECKSTYLE_PLUGIN_MAVEN_ID = new MavenId( + "org.apache.maven.plugins", "maven-checkstyle-plugin", null); + private static final String MAVEN_CONFIG_LOCATION_ID = "maven-config-location"; + + @Override + public void afterImport(@NotNull final MavenAfterImportConfigurator.Context context) { + final var project = context.getProject(); + final var pluginConfigurationManager = project.getService(PluginConfigurationManager.class); + final var currentPluginConfiguration = pluginConfigurationManager.getCurrent(); + + // Require users to opt in to avoid a breaking change. + if (!currentPluginConfiguration.isImportSettingsFromMaven()) { + return; + } + + final var mavenProject = findMavenProject(context); + if (mavenProject == null) { + return; + } + + final var checkstyleMavenPlugin = mavenProject.findPlugin( + MAVEN_CHECKSTYLE_PLUGIN_MAVEN_ID.getGroupId(), + MAVEN_CHECKSTYLE_PLUGIN_MAVEN_ID.getArtifactId()); + if (checkstyleMavenPlugin == null) { + return; + } + + final var checkstyleDependencyMavenId = findCheckstyleMavenId(checkstyleMavenPlugin, + mavenProject, project); + + final var pluginConfigurationBuilder = PluginConfigurationBuilder.from( + currentPluginConfiguration); + if (checkstyleDependencyMavenId != null + && checkstyleDependencyMavenId.getVersion() != null) { + pluginConfigurationBuilder.withCheckstyleVersion( + checkstyleDependencyMavenId.getVersion()); + } + + pluginConfigurationBuilder.withThirdPartyClassPath( + createThirdPartyClasspath(checkstyleMavenPlugin, mavenProject)); + + updatePluginConfigLocationsFromMavenPlugin(checkstyleMavenPlugin, + currentPluginConfiguration, mavenProject, pluginConfigurationBuilder, project); + + updatePluginScanScopeFromMavenPlugin(checkstyleMavenPlugin, pluginConfigurationBuilder); + + final var newPluginConfiguration = pluginConfigurationBuilder.build(); + if (!currentPluginConfiguration.equals(newPluginConfiguration)) { + pluginConfigurationManager.setCurrent(pluginConfigurationBuilder.build(), true); + } + } + + private static ConfigurationLocation createConfigurationLocation(final Project project, + final MavenProject mavenProject, final CheckstyleProjectService checkstyleProjectService, + final String mavenPluginConfigLocation) { + + String configLocation = null; + ConfigurationType configurationType = null; + + configLocation = createConfigLocationPathForLocalFileUrl(mavenPluginConfigLocation); + if (configLocation != null) { + configurationType = ConfigurationType.LOCAL_FILE; + } + + if (configLocation == null) { + configLocation = createConfigLocationPathForLocalAbsoluteFilePath( + mavenPluginConfigLocation); + if (configLocation != null) { + configurationType = ConfigurationType.LOCAL_FILE; + } + } + + if (configLocation == null) { + configLocation = createConfigLocationPathForLocalRelativeFilePath( + mavenPluginConfigLocation, mavenProject.getDirectoryFile()); + if (configLocation != null) { + configurationType = ConfigurationType.PROJECT_RELATIVE; + } + } + + if (configLocation == null) { + configLocation = createConfigLocationPathForHttpUrl(mavenPluginConfigLocation); + if (configLocation != null) { + configurationType = ConfigurationType.HTTP_URL; + } + } + + if (configLocation == null) { + configLocation = createConfigLocationPathForPluginClasspath(mavenPluginConfigLocation, + checkstyleProjectService.underlyingClassLoader()); + if (configLocation != null) { + configurationType = ConfigurationType.PLUGIN_CLASSPATH; + } + } + + if (configurationType == null) { + throw new CheckStylePluginException( + "Unable to identify ConfigurationType for configured location: " + + mavenPluginConfigLocation); + } + + final var configurationLocationFactory = project.getService( + ConfigurationLocationFactory.class); + return configurationLocationFactory.create(project, MAVEN_CONFIG_LOCATION_ID, + configurationType, configLocation, "Maven Config Location", + NamedScopeHelper.getDefaultScope(project)); + } + + @Nullable + private static String createConfigLocationPathForLocalAbsoluteFilePath( + @NotNull final String mavenConfigLocation) { + try { + final Path mavenPluginConfigLocationPath = Path.of(mavenConfigLocation); + if (mavenPluginConfigLocationPath.isAbsolute() && Files.isReadable( + mavenPluginConfigLocationPath)) { + return mavenConfigLocation; + } + } catch (final InvalidPathException ignored) { + } + + return null; + } + + @Nullable + private static String createConfigLocationPathForLocalFileUrl( + @NotNull final String mavenConfigLocation) { + if (mavenConfigLocation.startsWith("file:/")) { + try { + return new File(new URL(mavenConfigLocation).toURI()).getPath(); + } catch (Exception ignored) { + } + } + + return null; + } + + @Nullable + private static String createConfigLocationPathForLocalRelativeFilePath( + @NotNull final String mavenConfigLocation, @NotNull final VirtualFile rootDirectory) { + try { + final Path mavenPluginConfigLocationPath = rootDirectory.toNioPath() + .resolve(Path.of(mavenConfigLocation)); + if (Files.isReadable(mavenPluginConfigLocationPath)) { + return mavenPluginConfigLocationPath.toString(); + } + } catch (final InvalidPathException ignored) { + } + + return null; + } + + @Nullable + private static String createConfigLocationPathForHttpUrl( + @NotNull final String mavenConfigLocation) { + if (isValidHttpUri(mavenConfigLocation)) { + return mavenConfigLocation; + } + return null; + } + + @Nullable + private static String createConfigLocationPathForPluginClasspath( + @NotNull final String mavenConfigLocation, @NotNull final ClassLoader classLoader) { + final var resource = classLoader.getResource(mavenConfigLocation); + if (resource != null) { + return mavenConfigLocation; + } + + return null; + } + + private static List createThirdPartyClasspath(final MavenPlugin checkstyleMavenPlugin, + final MavenProject mavenProject) { + // This does not differentiate between dependencies that are providing rules or anything else. + // It is possible that a dependency might contribute something that modifies the behavior of Checkstyle causing a problem. + // The Maven sync does not currently provide a solution or workaround for this. + // https://github.com/jshiell/checkstyle-idea/pull/671#discussion_r2313823941 + return checkstyleMavenPlugin.getDependencies().stream().filter(dependency -> { + // Ignore anything that doesn't have all the required parts of the MavenId. + // The artifact can't be detected without all of these parts. + if (dependency.getArtifactId() == null || dependency.getGroupId() == null + || dependency.getVersion() == null) { + return false; + } + + // Ignore the checkstyle dependency, we know it isn't a third party jar. + if (CHECKSTYLE_MAVEN_ID.equals(dependency.getGroupId(), dependency.getArtifactId())) { + return false; + } + + return true; + }).map(dependency -> { + final var dependencyRelativePath = Path.of( + dependency.getGroupId().replace(".", File.separator), dependency.getArtifactId(), + dependency.getVersion(), + dependency.getArtifactId() + "-" + dependency.getVersion() + ".jar"); + final var dependencyPath = mavenProject.getLocalRepository().toPath() + .resolve(dependencyRelativePath); + + return dependencyPath.toAbsolutePath().toString(); + }).toList(); + } + + @Nullable + private static VirtualFile getOrDownloadCheckstyleMavenPluginPom( + @NotNull final MavenPlugin checkstyleMavenPlugin, @NotNull final MavenProject mavenProject, + @NotNull final Path pomPath, @NotNull final Project project) { + final var pomVirtualFile = VirtualFileManager.getInstance().findFileByNioPath(pomPath); + // Pom file may already exist from something such as a previous resolution. + if (pomVirtualFile != null) { + return pomVirtualFile; + } + + try { + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, (scope, continuation) -> { + try { + // Download the Checkstyle Maven Plugin pom file. + new MavenEmbeddersManager(project).execute(mavenProject, + MavenEmbeddersManager.FOR_DOWNLOAD, mavenEmbedderWrapper -> { + final var requests = List.of(new MavenArtifactResolutionRequest( + new MavenArtifactInfo(checkstyleMavenPlugin.getMavenId(), "pom", + ""), mavenProject.getRemoteRepositories())); + mavenEmbedderWrapper.resolveArtifacts(requests, null, + new MavenLogEventHandler(), continuation); + }); + } catch (MavenProcessCanceledException e) { + return null; + } + return null; + }); + LocalFileSystem.getInstance() + .refreshIoFiles(List.of(pomPath.toFile()), true, false, null); + } catch (InterruptedException ignored) { + + } + return VirtualFileManager.getInstance().findFileByNioPath(pomPath); + } + + @Nullable + private static MavenId findCheckstyleMavenId(@NotNull final MavenPlugin checkstyleMavenPlugin, + @NotNull final MavenProject mavenProject, @NotNull final Project project) { + return checkstyleMavenPlugin.getDependencies().stream().filter( + dependency -> CHECKSTYLE_MAVEN_ID.equals(dependency.getGroupId(), + dependency.getArtifactId())).findFirst().orElseGet( + () -> findCheckstyleMavenIdInPom(project, mavenProject, checkstyleMavenPlugin)); + } + + @Nullable + private static MavenId findCheckstyleMavenIdInPom(@NotNull final Project project, + @NotNull final MavenProject mavenProject, + @NotNull final MavenPlugin checkstyleMavenPlugin) { + final var pluginPomPath = MavenArtifactUtil.getArtifactFile( + mavenProject.getLocalRepository(), checkstyleMavenPlugin.getMavenId(), "pom"); + final var mavenPluginVirtualFile = getOrDownloadCheckstyleMavenPluginPom( + checkstyleMavenPlugin, mavenProject, pluginPomPath, project); + if (mavenPluginVirtualFile == null) { + return null; + } + + return ReadAction.compute(() -> { + final var mavenDomProjectModel = Objects.requireNonNull( + MavenDomUtil.getMavenDomProjectModel(project, mavenPluginVirtualFile)); + final var checkstyleDependency = mavenDomProjectModel.getDependencies() + .getDependencies().stream().filter( + dependency -> CHECKSTYLE_MAVEN_ID.equals(dependency.getGroupId().getValue(), + dependency.getArtifactId().getValue())).findFirst().orElseThrow( + () -> new CheckStylePluginException( + "Failed to find Checkstyle dependency within the Maven Checkstyle Plugin pom")); + + final var version = MavenPropertyResolver.resolve( + checkstyleDependency.getVersion().getValue(), mavenDomProjectModel); + + return new MavenId(checkstyleDependency.getGroupId().getValue(), + checkstyleDependency.getArtifactId().getValue(), version); + }); + } + + @Nullable + private static MavenProject findMavenProject( + @NotNull final MavenAfterImportConfigurator.Context context) { + // The first MavenProject (sorted alphabetically by MavenId#getKey()) found with a Maven + // Checkstyle Plugin is what will be used to load settings for the project. + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize(context.getMavenProjectsWithModules().iterator(), + Spliterator.ORDERED), false).filter(mavenProjectWithModulesToFilter -> { + final var mavenProject = mavenProjectWithModulesToFilter.getMavenProject(); + final var checkstyleMavenPlugin = mavenProject.findPlugin( + MAVEN_CHECKSTYLE_PLUGIN_MAVEN_ID.getGroupId(), + MAVEN_CHECKSTYLE_PLUGIN_MAVEN_ID.getArtifactId()); + + return checkstyleMavenPlugin != null; + }).sorted(Comparator.comparing(o -> o.getMavenProject().getMavenId().getKey())).findFirst() + .map(MavenProjectWithModules::getMavenProject).orElse(null); + } + + private static boolean getChildElementAsBoolean(@Nullable final Element element, + @NotNull final String childName, final boolean defaultValue) { + if (element == null) { + return defaultValue; + } + + final var child = element.getChild(childName); + if (child == null) { + return defaultValue; + } + + return Boolean.parseBoolean(child.getText()); + } + + @NotNull + private static ScanScope getScanScopeFromMavenConfig( + @Nullable final Element checkstyleMavenPluginConfig) { + // Default values here match the defaults from Maven Checkstyle plugin 3.6.0. + final var includeResources = getChildElementAsBoolean(checkstyleMavenPluginConfig, + "includeResources", true); + final var includeTestResources = getChildElementAsBoolean(checkstyleMavenPluginConfig, + "includeTestResources", true); + final var includeTestSourceDirectory = getChildElementAsBoolean(checkstyleMavenPluginConfig, + "includeTestSourceDirectory", false); + + if (includeResources && includeTestResources && includeTestSourceDirectory) { + return ScanScope.AllSourcesWithTests; + } + + if (includeResources && !includeTestResources && !includeTestSourceDirectory) { + return ScanScope.AllSources; + } + + if (!includeResources && !includeTestResources && includeTestSourceDirectory) { + return ScanScope.JavaOnlyWithTests; + } + + if (!includeResources && !includeTestResources && !includeTestSourceDirectory) { + return ScanScope.JavaOnly; + } + + return ScanScope.getDefaultValue(); + } + + private static boolean isValidHttpUri(@NotNull final String uriString) { + if (!uriString.startsWith("http://") && !uriString.startsWith("https://")) { + return false; + } + try { + new URI(uriString); + return true; + } catch (URISyntaxException ignored) { + return false; + } + } + + private static void updatePluginConfigLocationsFromMavenPlugin( + final MavenPlugin checkstyleMavenPlugin, + final PluginConfiguration currentPluginConfiguration, final MavenProject mavenProject, + final PluginConfigurationBuilder pluginConfigurationBuilder, final Project project) { + final var checkstyleMavenPluginConfiguration = checkstyleMavenPlugin.getConfigurationElement(); + final var configLocations = new TreeSet<>(currentPluginConfiguration.getLocations()); + pluginConfigurationBuilder.withLocations(configLocations); + + final var activeConfigLocationIds = new TreeSet<>( + currentPluginConfiguration.getActiveLocationIds()); + pluginConfigurationBuilder.withActiveLocationIds(activeConfigLocationIds); + + configLocations.removeIf(location -> MAVEN_CONFIG_LOCATION_ID.equals(location.getId())); + activeConfigLocationIds.removeIf(MAVEN_CONFIG_LOCATION_ID::equals); + + if (checkstyleMavenPluginConfiguration == null) { + return; + } + + final var configLocationElement = checkstyleMavenPluginConfiguration.getChild( + "configLocation"); + if (configLocationElement == null || configLocationElement.getText() == null) { + return; + } + + final String mavenPluginConfigLocation = configLocationElement.getText(); + // This must come after the PluginConfigurationBuilder is modified with the new + // Checkstyle version and the new third party classpath. + final var tempConfiguration = pluginConfigurationBuilder.build(); + final var checkstyleProjectService = CheckstyleProjectService.forVersion(project, + tempConfiguration.getCheckstyleVersion(), tempConfiguration.getThirdPartyClasspath()); + final var configurationLocation = createConfigurationLocation(project, mavenProject, + checkstyleProjectService, mavenPluginConfigLocation); + + configLocations.add(configurationLocation); + activeConfigLocationIds.add(configurationLocation.getId()); + } + + private static void updatePluginScanScopeFromMavenPlugin( + final MavenPlugin checkstyleMavenPlugin, + final PluginConfigurationBuilder pluginConfigurationBuilder) { + final var checkstyleMavenPluginConfiguration = checkstyleMavenPlugin.getConfigurationElement(); + final var scanScope = getScanScopeFromMavenConfig(checkstyleMavenPluginConfiguration); + pluginConfigurationBuilder.withScanScope(scanScope); + } + + // Copy of the private IntelliJ implementation. + private static class MavenLogEventHandler implements MavenEventHandler { + + @Override + public void handleConsoleEvents(@NotNull List list) { + for (var e : list) { + var message = e.getMessage(); + switch (e.getLevel()) { + case MavenServerConsoleIndicator.LEVEL_DEBUG -> MavenLog.LOG.debug(message); + case MavenServerConsoleIndicator.LEVEL_INFO -> MavenLog.LOG.info(message); + default -> MavenLog.LOG.warn(message); + } + var throwable = e.getThrowable(); + if (null != throwable) { + MavenLog.LOG.warn(throwable); + } + } + } + + @Override + public void handleDownloadEvents(@NotNull List list) { + for (var e : list) { + final var id = e.getDependencyId(); + switch (e.getArtifactEventType()) { + case DOWNLOAD_STARTED -> + MavenLog.LOG.debug("Download started: %s".formatted(id)); + case DOWNLOAD_COMPLETED -> + MavenLog.LOG.debug("Download completed: %s".formatted(id)); + case DOWNLOAD_FAILED -> MavenLog.LOG.debug( + "Download failed: %s \n%s \n%s".formatted(id, e.getErrorMessage(), + e.getStackTrace())); + } + } + } + } +} diff --git a/src/main/java/org/infernus/idea/checkstyle/ui/CheckStyleConfigPanel.java b/src/main/java/org/infernus/idea/checkstyle/ui/CheckStyleConfigPanel.java index d9308342..1b74b37f 100644 --- a/src/main/java/org/infernus/idea/checkstyle/ui/CheckStyleConfigPanel.java +++ b/src/main/java/org/infernus/idea/checkstyle/ui/CheckStyleConfigPanel.java @@ -55,6 +55,7 @@ public class CheckStyleConfigPanel extends JPanel { private final ComboBox scopeDropdown = new ComboBox<>(ScanScope.values()); private final JCheckBox suppressErrorsCheckbox = new JCheckBox(); private final JCheckBox copyLibsCheckbox = new JCheckBox(); + private final JCheckBox importSettingsFromMavenCheckbox = new JCheckBox(); private final LocationTableModel locationModel = new LocationTableModel(); private final JBTable locationTable = new JBTable(locationModel); @@ -104,6 +105,9 @@ private JPanel buildConfigPanel() { copyLibsCheckbox.setText(CheckStyleBundle.message("config.stabilize-classpath.text")); copyLibsCheckbox.setToolTipText(CheckStyleBundle.message("config.stabilize-classpath.tooltip")); + importSettingsFromMavenCheckbox.setText(CheckStyleBundle.message("config.import-maven-settings.text")); + importSettingsFromMavenCheckbox.setToolTipText(CheckStyleBundle.message("config.import-maven-settings.tooltip")); + final JPanel configFilePanel = new JPanel(new GridBagLayout()); configFilePanel.setOpaque(false); @@ -125,11 +129,14 @@ private JPanel buildConfigPanel() { configFilePanel.add(copyLibsCheckbox, new GridBagConstraints( 2, 1, 2, 1, 1.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, COMPONENT_INSETS, 0, 0)); + configFilePanel.add(importSettingsFromMavenCheckbox, new GridBagConstraints( + 2, 2, 2, 1, 1.0, 0.0, GridBagConstraints.WEST, + GridBagConstraints.HORIZONTAL, COMPONENT_INSETS, 0, 0)); configFilePanel.add(buildRuleFilePanel(), new GridBagConstraints( - 0, 2, 4, 1, 1.0, 1.0, GridBagConstraints.WEST, + 0, 3, 4, 1, 1.0, 1.0, GridBagConstraints.WEST, GridBagConstraints.BOTH, COMPONENT_INSETS, 0, 0)); configFilePanel.add(buildClassPathPanel(), new GridBagConstraints( - 0, 3, 4, 1, 1.0, 1.0, GridBagConstraints.WEST, + 0, 4, 4, 1, 1.0, 1.0, GridBagConstraints.WEST, GridBagConstraints.BOTH, COMPONENT_INSETS, 0, 0)); return configFilePanel; @@ -232,6 +239,7 @@ public void showPluginConfiguration(@NotNull final PluginConfiguration pluginCon scopeDropdown.setSelectedItem(pluginConfig.getScanScope()); suppressErrorsCheckbox.setSelected(pluginConfig.isSuppressErrors()); copyLibsCheckbox.setSelected(pluginConfig.isCopyLibs()); + importSettingsFromMavenCheckbox.setSelected(pluginConfig.isImportSettingsFromMaven()); locationModel.setLocations(new ArrayList<>(pluginConfig.getLocations())); setThirdPartyClasspath(pluginConfig.getThirdPartyClasspath()); locationModel.setActiveLocations(pluginConfig.getActiveLocations()); @@ -252,6 +260,7 @@ public PluginConfiguration getPluginConfiguration() { .withScanScope(scanScope) .withSuppressErrors(suppressErrorsCheckbox.isSelected()) .withCopyLibraries(copyLibsCheckbox.isSelected()) + .withImportSettingsFromMaven(importSettingsFromMavenCheckbox.isSelected()) .withLocations(new TreeSet<>(locationModel.getLocations())) .withThirdPartyClassPath(getThirdPartyClasspath()) .withActiveLocationIds(locationModel.getActiveLocations().stream() diff --git a/src/main/resources/META-INF/checkstyle-idea-maven.xml b/src/main/resources/META-INF/checkstyle-idea-maven.xml new file mode 100644 index 00000000..3deae578 --- /dev/null +++ b/src/main/resources/META-INF/checkstyle-idea-maven.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 285e4ef1..745df40b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -21,6 +21,7 @@ com.intellij.modules.vcs com.intellij.modules.java + org.jetbrains.idea.maven test + test + 1 + test + """.stripIndent(); + + @Test + public void afterImport_importSettingsFromMavenIsDisabled_doesNothing() throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var beforeConfig = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()).withImportSettingsFromMaven(false) + .withCheckstyleVersion("10.26.0").build(); + pluginConfigurationManager.setCurrent(beforeConfig, true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + com.puppycrawl.tools + checkstyle + 10.26.1 + + + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertEquals(beforeConfig, pluginConfigurationManager.getCurrent()); + } + + @Test + public void afterImport_importSettingsFromMavenIsEnabled_updatesVersion() throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true) + .withCheckstyleVersion("10.26.0"); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + com.puppycrawl.tools + checkstyle + 10.26.1 + + + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertEquals("10.26.1", pluginConfigurationManager.getCurrent().getCheckstyleVersion()); + } + + @Test + public void afterImport_importSettingsFromMavenIsEnabled_updatesThirdPartyClassPath() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true) + .withThirdPartyClassPath(List.of("/com/stuff/something.jar")); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + com.puppycrawl.tools + checkstyle + 10.26.1 + + + com.checkstyle.third.party.rules + cool-stuff + 3.2.1 + + + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertOrderedEquals(List.of(MavenUtil.resolveDefaultLocalRepository() + + "/com/checkstyle/third/party/rules/cool-stuff/3.2.1/cool-stuff-3.2.1.jar".replace( + "/", File.separator)), + pluginConfigurationManager.getCurrent().getThirdPartyClasspath()); + } + + @Test + public void afterImport_importSettingsFromMavenIsEnabled_updatesConfigLocations() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true).withLocations(new TreeSet<>()) + .withActiveLocationIds(new TreeSet<>()); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + final var configPath = Files.writeString( + getProjectRoot().toNioPath().resolve("checkstyle.xml"), ""); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + checkstyle.xml + + + + com.puppycrawl.tools + checkstyle + 10.26.1 + + + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + final var configurationLocationFactory = getProject().getService( + ConfigurationLocationFactory.class); + assertOrderedEquals( + List.of(configurationLocationFactory.create(BundledConfig.SUN_CHECKS, getProject()), + configurationLocationFactory.create(BundledConfig.GOOGLE_CHECKS, getProject()), + configurationLocationFactory.create(getProject(), "maven-config-location", + ConfigurationType.PROJECT_RELATIVE, configPath.toString(), + "Maven Config Location", NamedScopeHelper.getDefaultScope(getProject()))), + pluginConfigurationManager.getCurrent().getLocations()); + assertOrderedEquals(List.of("maven-config-location"), + pluginConfigurationManager.getCurrent().getActiveLocationIds()); + } + + @Test + public void afterImport_importSettingsFromMavenIsEnabled_updatesScanScope() throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true) + .withScanScope(ScanScope.Everything); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + true + true + true + + + + com.puppycrawl.tools + checkstyle + 10.26.1 + + + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertEquals(ScanScope.AllSourcesWithTests, + pluginConfigurationManager.getCurrent().getScanScope()); + } + + @Test + public void afterImport_importSettingsFromMavenIsEnabledAndInheritingMavenPluginCheckstyleVersion_updatesVersionWithInheritedValue() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true) + .withCheckstyleVersion("10.26.1"); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertEquals("9.3", pluginConfigurationManager.getCurrent().getCheckstyleVersion()); + } + + @Test + public void updatePluginConfigLocationsFromMavenPlugin_configLocationIsMissingAndMavenConfigExists_removesMavenConfigLocation() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + final var configurationLocationFactory = getProject().getService( + ConfigurationLocationFactory.class); + + final var mavenConfigLocation = configurationLocationFactory.create(getProject(), + "maven-config-location", ConfigurationType.PROJECT_RELATIVE, "checkstyle.xml", + "Maven Config Location", NamedScopeHelper.getDefaultScope(getProject())); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true) + .withLocations(new TreeSet<>(List.of(mavenConfigLocation))) + .withActiveLocationIds(new TreeSet<>(List.of(mavenConfigLocation.getId()))); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertTrue(pluginConfigurationManager.getCurrent().getLocations().stream() + .noneMatch(config -> config.getId().equals(mavenConfigLocation.getId()))); + } + + @Test + public void updatePluginConfigLocationsFromMavenPlugin_configLocationExistsAndMavenConfigAlreadyExists_overwritesWithNewConfig() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + final var configurationLocationFactory = getProject().getService( + ConfigurationLocationFactory.class); + + final var configPath = Files.writeString( + getProjectRoot().toNioPath().resolve("checkstyle.xml"), ""); + final var mavenConfigLocation = configurationLocationFactory.create(getProject(), + "maven-config-location", ConfigurationType.PROJECT_RELATIVE, "checkstyle-existing.xml", + "Maven Config Location", NamedScopeHelper.getDefaultScope(getProject())); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true) + .withLocations(new TreeSet<>(List.of(mavenConfigLocation))) + .withActiveLocationIds(new TreeSet<>(List.of(mavenConfigLocation.getId()))); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + checkstyle.xml + + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertEquals(configPath.toString(), + pluginConfigurationManager.getCurrent().getLocations().stream() + .filter(config -> config.getId().equals(mavenConfigLocation.getId())) + .map(ConfigurationLocation::getLocation).findFirst().get()); + } + + @Test + public void updatePluginConfigLocationsFromMavenPlugin_mavenConfigDoesNotAlreadyExist_addsNewConfig() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var configPath = Files.writeString( + getProjectRoot().toNioPath().resolve("checkstyle.xml"), ""); + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true).withLocations(new TreeSet<>()) + .withActiveLocationIds(new TreeSet<>()); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + checkstyle.xml + + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertEquals(configPath.toString(), + pluginConfigurationManager.getCurrent().getLocations().stream() + .filter(config -> config.getId().equals("maven-config-location")) + .map(ConfigurationLocation::getLocation).findFirst().get()); + } + + @Test + public void updatePluginScanScopeFromMavenPlugin_includeSettingsAreMissingAndScanScopeExists_usesAllSourcesWithTestsScanScope() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true) + .withScanScope(ScanScope.Everything); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertEquals(ScanScope.getDefaultValue(), + pluginConfigurationManager.getCurrent().getScanScope()); + } + + @Test + public void updatePluginScanScopeFromMavenPlugin_includeSettingsExistsAndScanScopeAlreadyExists_usesNewScanScope() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true) + .withScanScope(ScanScope.Everything); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO + """ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + false + false + + + + + """.stripIndent()); + + BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation)); + + assertEquals(ScanScope.JavaOnly, pluginConfigurationManager.getCurrent().getScanScope()); + } + + @Test + public void afterImport_mavenCheckstylePluginNotConfiguredAndSyncEnabled_doesNotThrow() + throws Exception { + final var pluginConfigurationManager = getProject().getService( + PluginConfigurationManager.class); + + final var updatedConfigurationBuilder = PluginConfigurationBuilder.from( + pluginConfigurationManager.getCurrent()); + updatedConfigurationBuilder.withImportSettingsFromMaven(true); + pluginConfigurationManager.setCurrent(updatedConfigurationBuilder.build(), true); + + createProjectPom(PROJECT_INFO); + + assertDoesNotThrow(() -> BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> importProjectAsync(continuation))); + } + + // TODO: Replace this when migrating to JUnit 5. + private void assertDoesNotThrow(Executable executable) { + try { + executable.execute(); + } catch (Exception e) { + fail("Unexpected exception thrown: " + e.getClass().getName() + ": " + e.getMessage()); + } + } + + @FunctionalInterface + interface Executable { + + void execute() throws Exception; + } +}