Skip to content
Merged

2.x #113

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
749ab62
Remove legacy project dir
AB-xdev Jul 28, 2026
a607dfc
Abstract and split up Config UI code
AB-xdev Jul 28, 2026
e47eab1
Refactor how locations are handled
AB-xdev Jul 29, 2026
843a5b1
Bump version
AB-xdev Jul 29, 2026
e2dc185
Correctly handle resolved "$PROJECT_DIR$"
AB-xdev Jul 29, 2026
b8e515e
Order BundledLocations last
AB-xdev Jul 29, 2026
52b6c51
Path is automatically templated by IntelliJ
AB-xdev Jul 29, 2026
daf60dd
Disable project configuration when project is not a real project
AB-xdev Jul 29, 2026
e1a85f1
Implement basic support for loading 3rd party rules jars
AB-xdev Jul 29, 2026
158b3de
Move ruleset package
AB-xdev Jul 30, 2026
67a5360
Remove not needed conversion logic
AB-xdev Jul 30, 2026
5bf3b8d
Improve sorting
AB-xdev Jul 30, 2026
62d1123
Fix CS
AB-xdev Jul 30, 2026
3e3b192
Configuration of default project is not supported
AB-xdev Jul 30, 2026
9d61836
Fix doc
AB-xdev Jul 30, 2026
e78c658
3rd party rules v2 rough implementation
AB-xdev Jul 30, 2026
8374747
Fix serialization problem
AB-xdev Jul 31, 2026
f39281b
Fix CS and PMD
AB-xdev Jul 31, 2026
7789279
Add support for using thrid party rules from maven central
AB-xdev Jul 31, 2026
3c83e65
Update docs
AB-xdev Jul 31, 2026
971a2d3
Improve maven import to now also import maven 3rd party locations
AB-xdev Jul 31, 2026
f3fd88e
Cleanup comment
AB-xdev Jul 31, 2026
402b1b9
Link intellij bug report
AB-xdev Aug 3, 2026
48293da
Cleanup
AB-xdev Aug 3, 2026
a2b3c43
Abstract EP resolution code
AB-xdev Aug 3, 2026
73f4419
Cleanup UI
AB-xdev Aug 3, 2026
450921f
More comment cleanup
AB-xdev Aug 3, 2026
77d316f
Update README.md
AB-xdev Aug 3, 2026
51f4fdf
Fine tune settings UI
AB-xdev Aug 3, 2026
d486c23
Adjust readme and docs
AB-xdev Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# 1.2.0
# 2.0.0
* Add support 3rd Party Rules #10
* Allows to use custom PMD rules shipped in JAR files
* JARs can be located from project-relative files, absolute files or maven artifacts
* Maven artifacts
* Are downloaded and loaded automatically if not present
* The download tries to use the configured Maven mirror or can be completely overwritten in the settings
* Make it possible to add (project-wide) exclusions #58
* Add support for importing settings from Maven PMD plugin (best-effort)
* Add shortcuts for jumping to previous/next result/violation
* Add a "scan entire project" button #59
* Only show "Enable automatic build" hint on startup when projects have PMD enabled
* Refactor location store
* Order bundled locations at the end

# 1.1.4
* Prevent rare NPE in module configuration UI #107
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ A plugin for IntelliJ that provides code analysis and highlighting with <a href=
* Real-time analysis and highlighting of currently edited file
* Run bulk analysis of files by right clicking the project menu <br/> <img src="./assets/run-bulk-analysis.avif"> <br/> <img src="./assets/toolwindow-report.avif">
* Currently Java and Kotlin are supported, Language versions are automatically detected
* Include / exclude files with regex support
* Use JARs for custom 3rd party rules <br/> <img src="./assets/custom-rule.avif">
* JARs can automatically be downloaded from Maven Central
* Configuration can be imported from Maven projects (best-effort)

## Usage
1. Install the plugin and open a project
Expand Down
Binary file added assets/custom-rule.avif
Binary file not shown.
Binary file modified assets/project-configuration.avif
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pluginGroup=software.xdev.pmd
pluginName=PMD X
# SemVer format -> https://semver.org
pluginVersion=1.2.0-SNAPSHOT
pluginVersion=2.0.0-SNAPSHOT
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType=IU
platformVersion=2026.2
Expand Down
24 changes: 17 additions & 7 deletions src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
import software.xdev.pmd.config.PluginConfigurationManager;
import software.xdev.pmd.external.org.springframework.util.ConcurrentReferenceHashMap;
import software.xdev.pmd.langversion.ManagedLanguageVersionResolver;
import software.xdev.pmd.model.config.ConfigurationLocation;
import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation;


public class PMDAnalyzer implements Disposable
Expand All @@ -66,8 +66,8 @@ public class PMDAnalyzer implements Disposable
private final Map<Optional<Module>, ReentrantLock> locks = new ConcurrentHashMap<>();
private final Map<Optional<Module>, CacheFile> cacheFiles = new ConcurrentHashMap<>();
// Reuse classloader when path is the same
private final Map<Set<String>, ClassLoader> cachedSdkLibAuxClassLoader =
new ConcurrentReferenceHashMap<>();
private final Map<ClassLoader, Map<Set<String>, ClassLoader>> baseClassLoaderCachedSdkLibAuxClassLoaders =
new ConcurrentReferenceHashMap<>(ConcurrentReferenceHashMap.ReferenceType.WEAK);

public PMDAnalyzer(final Project project)
{
Expand Down Expand Up @@ -135,11 +135,14 @@ private PMDAnalysisResult analyzeInternal(
{
final long startMs = System.currentTimeMillis();

final ClassLoader baseClassLoader =
this.project.getService(ProjectScanClasspathManager.class).getClassLoader();

// Load ruleset - if required - async in background
final CompletableFuture<List<RuleSet>> cfLoadRuleSetsAsync =
CompletableFuture.supplyAsync(
() -> configurationLocations.stream()
.map(ConfigurationLocation::getOrRefreshCachedRuleSet)
.map(configLoc -> configLoc.getOrRefreshCachedRuleSet(baseClassLoader))
.filter(Objects::nonNull)
.toList(),
RULESET_LOADER_SERVICE);
Expand Down Expand Up @@ -173,7 +176,7 @@ private PMDAnalysisResult analyzeInternal(
.map(List::of)
.orElseGet(() -> List.of(ModuleManager.getInstance(this.project).getModules()));

pmdConfig.setClassLoader(this.classLoaderFor(modules));
pmdConfig.setClassLoader(this.classLoaderFor(modules, baseClassLoader));

if(pluginConfiguration.showSuppressedWarnings())
{
Expand Down Expand Up @@ -323,17 +326,24 @@ private Map<LanguageVersion, Set<PsiFile>> getHighestLanguageVersionAndFiles(
}

@NotNull
private ClasspathClassLoader classLoaderFor(final List<Module> modules)
private ClasspathClassLoader classLoaderFor(
final List<Module> modules,
final ClassLoader baseClassLoader)
{
final Set<String> fullClassPaths = this.classPathFor(modules, UnaryOperator.identity());
final Set<String> appClassPaths = this.classPathFor(modules, o -> o.withoutSdk().withoutLibraries());
final Set<String> sdkLibClassPaths = fullClassPaths.stream()
.filter(s -> !appClassPaths.contains(s))
.collect(Collectors.toSet());

final Map<Set<String>, ClassLoader> cachedSdkLibAuxClassLoaders =
this.baseClassLoaderCachedSdkLibAuxClassLoaders.computeIfAbsent(
baseClassLoader,
ignored -> new ConcurrentReferenceHashMap<>());

return this.createClasspathClassLoader(
appClassPaths,
this.cachedSdkLibAuxClassLoader.computeIfAbsent(
cachedSdkLibAuxClassLoaders.computeIfAbsent(
sdkLibClassPaths,
paths -> this.createClasspathClassLoader(paths, PMDConfiguration.class.getClassLoader())));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package software.xdev.pmd.analysis;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import java.util.Objects;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;

import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation;


public class ProjectScanClasspathManager implements Disposable
{
private static final ClassLoader DEFAULT_CL = ProjectScanClasspathManager.class.getClassLoader();
private static final Logger LOG = Logger.getInstance(ProjectScanClasspathManager.class);

private List<ThirdPartyCPLocation> cachedKey;
private URLClassLoader classLoader;

public void configure(final List<ThirdPartyCPLocation> locations)
{
if(!Objects.equals(this.cachedKey, locations))
{
this.closeCurrentClassLoader();
if(!locations.isEmpty())
{
this.classLoader = new URLClassLoader(
locations.stream().map(ThirdPartyCPLocation::url).toArray(URL[]::new),
DEFAULT_CL);
}

this.cachedKey = locations;
}
}

public ClassLoader getClassLoader()
{
return this.classLoader != null ? this.classLoader : DEFAULT_CL;
}

private void closeCurrentClassLoader()
{
closeClassLoader(this.classLoader);
this.classLoader = null;
}

@Override
public void dispose()
{
this.closeCurrentClassLoader();
}

private static void closeClassLoader(final URLClassLoader classLoader)
{
if(classLoader != null)
{
try
{
classLoader.close();
}
catch(final IOException e)
{
LOG.warn("Failed to close classloader", e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import com.intellij.openapi.project.Project;

import software.xdev.pmd.config.state.module.ModuleConfigurationState;
import software.xdev.pmd.model.config.ConfigurationLocation;
import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation;


public class ConfigurationLocationSource
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package software.xdev.pmd.config;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedSet;
Expand All @@ -11,7 +12,9 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import software.xdev.pmd.model.config.ConfigurationLocation;
import software.xdev.pmd.config.plugin.PatternContainer;
import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation;
import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation;
import software.xdev.pmd.model.scope.ScanScope;


Expand All @@ -27,6 +30,7 @@ public record PluginConfiguration(
SortedSet<PatternContainer> projectRelativeFileExclusions,
SortedSet<ConfigurationLocation> locations,
SortedSet<String> activeLocationIds,
List<ThirdPartyCPLocation> thirdPartyCPLocations,
boolean importSettingsFromMaven,
Cache cache
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
Expand All @@ -12,9 +13,11 @@

import com.intellij.openapi.project.Project;

import software.xdev.pmd.model.config.ConfigurationLocation;
import software.xdev.pmd.model.config.ConfigurationLocationFactory;
import software.xdev.pmd.model.config.bundled.BundledConfig;
import software.xdev.pmd.config.plugin.PatternContainer;
import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocation;
import software.xdev.pmd.model.config.rulesetlocation.ConfigurationLocationFactory;
import software.xdev.pmd.model.config.rulesetlocation.bundled.BundledConfig;
import software.xdev.pmd.model.config.thirdpartycplocation.ThirdPartyCPLocation;
import software.xdev.pmd.model.scope.ScanScope;


Expand All @@ -27,19 +30,21 @@ public final class PluginConfigurationBuilder
private SortedSet<PatternContainer> projectRelativeFileExclusions;
private SortedSet<ConfigurationLocation> locations;
private SortedSet<String> activeLocationIds;
private List<ThirdPartyCPLocation> thirdPartyCPLocations;
private boolean importSettingsFromMaven;

public PluginConfigurationBuilder(final Project project)
{
this.showSuppressedWarnings = true;
this.useCacheFile = true;
this.scanScope = ScanScope.getDefaultValue();
this.projectRelativeFileExclusions = Collections.emptySortedSet();
this.projectRelativeFileExclusions = null;
this.locations = BundledConfig.getAllBundledConfigs()
.stream()
.map(bc -> configurationLocationFactory(project).create(bc, project))
.collect(Collectors.toCollection(TreeSet::new));
this.activeLocationIds = Collections.emptySortedSet();
this.activeLocationIds = null;
this.thirdPartyCPLocations = null;
this.importSettingsFromMaven = false;
}

Expand All @@ -52,6 +57,7 @@ public PluginConfigurationBuilder(final PluginConfiguration copyFrom)
this.projectRelativeFileExclusions = copyFrom.projectRelativeFileExclusions();
this.locations = copyFrom.locations();
this.activeLocationIds = copyFrom.activeLocationIds();
this.thirdPartyCPLocations = copyFrom.thirdPartyCPLocations();
this.importSettingsFromMaven = copyFrom.importSettingsFromMaven();
}

Expand Down Expand Up @@ -87,6 +93,12 @@ public PluginConfigurationBuilder withUseCacheFile(@Nullable final Boolean useCa
return this;
}

public PluginConfigurationBuilder withScanScope(@NotNull final ScanScope newScanScope)
{
this.scanScope = newScanScope;
return this;
}

public PluginConfigurationBuilder withProjectRelativeFileExclusionsRaw(
final Collection<String> projectRelativeFileExclusions)
{
Expand Down Expand Up @@ -119,9 +131,9 @@ public PluginConfigurationBuilder withLocations(@NotNull final SortedSet<Configu
return this;
}

public PluginConfigurationBuilder withScanScope(@NotNull final ScanScope newScanScope)
public PluginConfigurationBuilder withThirdPartyCPLocations(final List<ThirdPartyCPLocation> thirdPartyCPLocations)
{
this.scanScope = newScanScope;
this.thirdPartyCPLocations = thirdPartyCPLocations;
return this;
}

Expand All @@ -147,6 +159,7 @@ public PluginConfiguration build()
.filter(Objects::nonNull)
.collect(Collectors.toCollection(TreeSet::new))
: new TreeSet<>(),
Collections.unmodifiableList(Objects.requireNonNullElseGet(this.thirdPartyCPLocations, List::of)),
this.importSettingsFromMaven,
new PluginConfiguration.Cache());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package software.xdev.pmd.config;

import java.util.Objects;

import org.jetbrains.annotations.NotNull;

import com.intellij.openapi.project.Project;

import software.xdev.pmd.analysis.ProjectScanClasspathManager;
import software.xdev.pmd.config.state.project.ProjectConfigurationState;
import software.xdev.pmd.config.state.project.ProjectSettingsState;


public class PluginConfigurationManager
{
private final Project project;

private ProjectSettingsState lastProjectSettingsState;
private PluginConfiguration lastPluginConfiguration;

public PluginConfigurationManager(@NotNull final Project project)
{
this.project = project;
Expand All @@ -19,14 +26,38 @@ public PluginConfigurationManager(@NotNull final Project project)
@NotNull
public PluginConfiguration getCurrent()
{
return this.projectConfigurationState()
.populate(new PluginConfigurationBuilder(this.project))
.build();
final ProjectConfigurationState projectConfigurationState = this.projectConfigurationState();
final ProjectSettingsState currentProjectSettingsState = projectConfigurationState.getState();
if(!Objects.equals(this.lastProjectSettingsState, currentProjectSettingsState))
{
this.setLastPluginConfiguration(
currentProjectSettingsState,
projectConfigurationState
.populate(new PluginConfigurationBuilder(this.project))
.build());
}

return this.lastPluginConfiguration;
}

public void setCurrent(@NotNull final PluginConfiguration updatedConfiguration)
{
this.projectConfigurationState().setCurrentConfig(updatedConfiguration);
final ProjectConfigurationState projectConfigurationState = this.projectConfigurationState();

this.setLastPluginConfiguration(projectConfigurationState.getState(), updatedConfiguration);

projectConfigurationState.setCurrentConfig(updatedConfiguration);
}

private void setLastPluginConfiguration(
final ProjectSettingsState projectSettingsState,
@NotNull final PluginConfiguration pluginConfig)
{
this.lastProjectSettingsState = projectSettingsState;
this.lastPluginConfiguration = pluginConfig;

// Update the classpath information
this.project.getService(ProjectScanClasspathManager.class).configure(pluginConfig.thirdPartyCPLocations());
}

private ProjectConfigurationState projectConfigurationState()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package software.xdev.pmd.config;
package software.xdev.pmd.config.plugin;

import java.util.Map;
import java.util.Objects;
Expand Down
Loading