From 9790f59d772272d5b7c8a8b86fee232414ac4aa9 Mon Sep 17 00:00:00 2001 From: Nick Rayburn <52075362+nrayburn-tech@users.noreply.github.com> Date: Thu, 24 Jul 2025 01:38:10 -0500 Subject: [PATCH] Add the ability to sync Maven Checkstyle plugin settings with the IDE settings When the Maven sync action is triggered within the IDE, the project settings will be synced. This currently requires users to opt-in with a Checkstyle IntelliJ plugin setting. --- build.gradle.kts | 2 + .../config/PluginConfiguration.java | 14 +- .../config/PluginConfigurationBuilder.java | 18 +- .../config/ProjectConfigurationState.java | 6 +- .../maven/MavenCheckstyleConfigurator.java | 498 ++++++++++++++++++ .../checkstyle/ui/CheckStyleConfigPanel.java | 13 +- .../META-INF/checkstyle-idea-maven.xml | 6 + src/main/resources/META-INF/plugin.xml | 1 + .../checkstyle/CheckStyleBundle.properties | 3 + .../MavenCheckstyleConfiguratorTest.java | 488 +++++++++++++++++ 10 files changed, 1040 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/infernus/idea/checkstyle/maven/MavenCheckstyleConfigurator.java create mode 100644 src/main/resources/META-INF/checkstyle-idea-maven.xml create mode 100644 src/test/java/org/infernus/idea/checkstyle/maven/MavenCheckstyleConfiguratorTest.java 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; + } +}