diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index 0055b39f..8834f45e 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -21,6 +21,19 @@ This is a minor release.
### API Changes
+#### Deprecations
+
+The following methods have been deprecated for removal:
+
+* `net.sourceforge.pmd.eclipse.runtime.properties.IProjectProperties#getAuxClasspath()`
+* `net.sourceforge.pmd.eclipse.runtime.properties.impl.ProjectPropertiesImpl#getAuxClasspath()`
+
+Use the new method `getClasspath()` instead. It doesn't use a custom classloader anymore and just returns
+the classpath as a single string (path elements separated by the os specific path separator).
+
+The following class has been deprecated for removal:
+
+* `net.sourceforge.pmd.eclipse.runtime.cmd.JavaProjectClassLoader`
## 30-December-2025: 7.20.0.v20251230-1608-r
diff --git a/net.sourceforge.pmd.eclipse.plugin.test/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/ProjectPropertiesModelTest.java b/net.sourceforge.pmd.eclipse.plugin.test/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/ProjectPropertiesModelTest.java
index 28cb48c7..6a27efc3 100644
--- a/net.sourceforge.pmd.eclipse.plugin.test/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/ProjectPropertiesModelTest.java
+++ b/net.sourceforge.pmd.eclipse.plugin.test/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/ProjectPropertiesModelTest.java
@@ -15,12 +15,15 @@
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.eclipse.core.resources.IFile;
@@ -523,9 +526,11 @@ private void dumpRuleSet(final RuleSet ruleSet) {
*
*
* @throws Exception
+ * @deprecated Since 7.21.0. Tests the deprecated {@link IProjectProperties#getAuxClasspath()} method.
*/
@Test
- public void testProjectClasspath() throws Exception {
+ @Deprecated
+ public void testProjectClasspathClassloader() throws Exception {
IProject otherProject = EclipseUtils.createJavaProject("OtherProject");
additionalProjects.add(otherProject);
IFile sampleLib1 = otherProject.getFile("sample-lib1.jar");
@@ -607,4 +612,95 @@ public void testProjectClasspath() throws Exception {
// no remaining urls
Assert.assertTrue(urls.isEmpty());
}
+
+ /**
+ * Project structure:
+ *
+ * - this.testProject "ProjectPropertiesModelTest": main project, with build path, contains lib/sample-lib3.jar
+ * - otherProject "OtherProject": contains sample-lib1.jar, sample-lib2.jar
+ * - otherProject2 "OtherProject2": ProjectPropertiesModelTest depends on this
+ * - externalProject "ExternalProject": not stored within workspace, contains sample-lib4.jar
+ *
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testProjectClasspath() throws Exception {
+ IProject otherProject = EclipseUtils.createJavaProject("OtherProject");
+ additionalProjects.add(otherProject);
+ IFile sampleLib1 = otherProject.getFile("sample-lib1.jar");
+ sampleLib1.create(IOUtils.toInputStream("", "UTF-8"), false, null);
+ File realSampleLib1 = sampleLib1.getLocation().toFile().getCanonicalFile();
+ IFile sampleLib2 = otherProject.getFile("sample-lib2.jar");
+ sampleLib2.create(IOUtils.toInputStream("", "UTF-8"), false, null);
+ File realSampleLib2 = sampleLib2.getLocation().toFile().getCanonicalFile();
+
+ IFolder libFolder = this.testProject.getFolder("lib");
+ libFolder.create(false, true, null);
+ IFile sampleLib3 = libFolder.getFile("sample-lib3.jar");
+ sampleLib3.create(IOUtils.toInputStream("", "UTF-8"), false, null);
+ File realSampleLib3 = sampleLib3.getLocation().toFile().getCanonicalFile();
+
+ IProject otherProject2 = EclipseUtils.createJavaProject("OtherProject2");
+ additionalProjects.add(otherProject2);
+ // build the project, so that the output folder "bin/" is created
+ otherProject2.build(IncrementalProjectBuilder.FULL_BUILD, null);
+
+ IProject externalProject = ResourcesPlugin.getWorkspace().getRoot().getProject("ExternalProject");
+ additionalProjects.add(externalProject);
+ Assert.assertFalse("Project must not exist yet", externalProject.exists());
+ java.nio.file.Path externalProjectDir = Files.createTempDirectory("pmd-eclipse-plugin");
+ IProjectDescription description = externalProject.getWorkspace().newProjectDescription("ExternalProject");
+ description.setLocation(Path.fromOSString(externalProjectDir.toString()));
+ externalProject.create(description, null);
+ externalProject.open(null);
+ IFile sampleLib4 = externalProject.getFile("sample-lib4.jar");
+ sampleLib4.create(IOUtils.toInputStream("", "UTF-8"), false, null);
+ File realSampleLib4 = sampleLib4.getLocation().toFile().getCanonicalFile();
+
+ // build the project, so that the output folder "bin/" is created
+ this.testProject.build(IncrementalProjectBuilder.FULL_BUILD, null);
+
+ IFile file = this.testProject.getFile(".classpath");
+ String newClasspathContent = "\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + "\n";
+ file.setContents(IOUtils.toInputStream(newClasspathContent, "UTF-8"), 0, null);
+ // refresh, so that changed .classpath file is considered
+ this.testProject.refreshLocal(IResource.DEPTH_INFINITE, null);
+ // rebuild again, so that changed classpath is configured on java project
+ this.testProject.build(IncrementalProjectBuilder.FULL_BUILD, null);
+
+ final IProjectPropertiesManager mgr = PMDPlugin.getDefault().getPropertiesManager();
+ IProjectProperties model = mgr.loadProjectProperties(this.testProject);
+ List classpath = new ArrayList<>(Arrays.asList(model.getClasspath().split(Pattern.quote(File.pathSeparator))));
+
+ Assert.assertEquals("Found these paths: " + classpath, 6, classpath.size());
+
+ // own project's output folder
+ Assert.assertTrue(classpath.remove(
+ new File(this.testProject.getLocation().toFile().getAbsoluteFile(), "bin").toString()));
+ // output folder of other project 2 (project dependency)
+ Assert.assertTrue(classpath.remove(
+ new File(otherProject2.getLocation().toFile().getAbsoluteFile(), "bin").toString()));
+ // sample-lib1.jar stored in OtherProject
+ Assert.assertTrue(classpath.remove(realSampleLib1.toString()));
+ // sample-lib2.jar referenced with absolute path
+ Assert.assertTrue(classpath.remove(realSampleLib2.toString()));
+ // sample-lib3.jar stored in own project folder lib
+ Assert.assertTrue(classpath.remove(realSampleLib3.toString()));
+ // sample-lib4.jar stored in external project folder outside of workspace
+ Assert.assertTrue(classpath.remove(realSampleLib4.toString()));
+
+ // no remaining urls
+ Assert.assertTrue(classpath.isEmpty());
+ }
}
diff --git a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/BaseVisitor.java b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/BaseVisitor.java
index b7d13502..f124d9aa 100644
--- a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/BaseVisitor.java
+++ b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/BaseVisitor.java
@@ -239,7 +239,7 @@ protected final void reviewResource(IResource resource) {
LOG.debug("discovered language: {}", languageVersion);
if (PMDPlugin.getDefault().loadPreferences().isProjectBuildPathEnabled()) {
- configuration().setClassLoader(projectProperties.getAuxClasspath());
+ configuration().prependAuxClasspath(projectProperties.getClasspath());
}
// Avoid warnings about not providing cache for incremental analysis
diff --git a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/JavaProjectClassLoader.java b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/JavaProjectClassLoader.java
index 4cc031c5..2b216249 100644
--- a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/JavaProjectClassLoader.java
+++ b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/JavaProjectClassLoader.java
@@ -24,10 +24,14 @@
import org.slf4j.LoggerFactory;
import net.sourceforge.pmd.eclipse.core.internal.FileModificationUtil;
+import net.sourceforge.pmd.eclipse.runtime.properties.IProjectProperties;
/**
* This is a ClassLoader for the Build Path of an IJavaProject.
+ *
+ * @deprecated Since 7.21.0. This class is not needed anymore. See {@link IProjectProperties#getClasspath()}.
*/
+@Deprecated
public class JavaProjectClassLoader extends URLClassLoader {
private static final Logger LOG = LoggerFactory.getLogger(JavaProjectClassLoader.class);
diff --git a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/internal/JavaProjectClasspath.java b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/internal/JavaProjectClasspath.java
new file mode 100644
index 00000000..c7bfe31b
--- /dev/null
+++ b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/cmd/internal/JavaProjectClasspath.java
@@ -0,0 +1,153 @@
+/*
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+
+package net.sourceforge.pmd.eclipse.runtime.cmd.internal;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.sourceforge.pmd.eclipse.core.internal.FileModificationUtil;
+
+public class JavaProjectClasspath {
+ private static final Logger LOG = LoggerFactory.getLogger(JavaProjectClasspath.class);
+
+ private final IJavaProject javaProject;
+ private final long lastModTimestamp;
+ private final IWorkspace workspace;
+ private Set javaProjects = new HashSet<>();
+ private final List classpath = new ArrayList<>();
+
+ public JavaProjectClasspath(IProject project) {
+ try {
+ if (!project.hasNature(JavaCore.NATURE_ID)) {
+ throw new IllegalArgumentException("The project " + project + " is not a java project");
+ }
+ } catch (CoreException e) {
+ throw new IllegalArgumentException("The project " + project + " is not a java project", e);
+ }
+
+ workspace = project.getWorkspace();
+ javaProject = JavaCore.create(project);
+ lastModTimestamp = getClasspathModificationTimestamp();
+ addPaths(javaProject, false);
+
+ // No longer need these things, drop references
+ javaProjects = null;
+ }
+
+ public boolean isModified() {
+ long newTimestamp = getClasspathModificationTimestamp();
+ return newTimestamp != lastModTimestamp;
+ }
+
+ public List getClasspath() {
+ return classpath;
+ }
+
+ private long getClasspathModificationTimestamp() {
+ IFile classpathFile = javaProject.getProject().getFile(IJavaProject.CLASSPATH_FILE_NAME);
+ return FileModificationUtil.getFileModificationTimestamp(classpathFile.getLocation().toFile());
+ }
+
+ private IProject projectFor(IClasspathEntry classpathEntry) {
+ return workspace.getRoot().getProject(classpathEntry.getPath().toString());
+ }
+
+ private void addPaths(IJavaProject javaProject, boolean exportsOnly) {
+
+ if (javaProjects.contains(javaProject)) {
+ return;
+ }
+
+ javaProjects.add(javaProject);
+
+ try {
+ // Add default output location
+ IPath projectLocation = javaProject.getProject().getLocation();
+ addPath(projectLocation.append(javaProject.getOutputLocation().removeFirstSegments(1)));
+
+ // Add each classpath entry
+ IClasspathEntry[] classpathEntries = javaProject.getResolvedClasspath(true);
+ for (IClasspathEntry classpathEntry : classpathEntries) {
+ if (classpathEntry.isExported() || !exportsOnly) {
+ switch (classpathEntry.getEntryKind()) {
+
+ // Recurse on projects
+ case IClasspathEntry.CPE_PROJECT:
+ IProject project = projectFor(classpathEntry);
+ IJavaProject javaProj = JavaCore.create(project);
+ if (javaProj != null) {
+ addPaths(javaProj, true);
+ }
+ break;
+
+ // Library
+ case IClasspathEntry.CPE_LIBRARY:
+ addPath(classpathEntry);
+ break;
+
+ // Only Source entries with custom output location need to
+ // be added
+ case IClasspathEntry.CPE_SOURCE:
+ IPath outputLocation = classpathEntry.getOutputLocation();
+ if (outputLocation != null) {
+ addPath(projectLocation.append(outputLocation.removeFirstSegments(1)));
+ }
+ break;
+
+ // Variable and Container entries should not be happening,
+ // because we've asked for resolved entries.
+ case IClasspathEntry.CPE_VARIABLE:
+ case IClasspathEntry.CPE_CONTAINER:
+ default:
+ break;
+ }
+ }
+ }
+ } catch (JavaModelException e) {
+ LOG.warn("JavaModelException occurred: {}", e.getMessage(), e);
+ }
+ }
+
+ private void addPath(IClasspathEntry classpathEntry) {
+ addPath(classpathEntry.getPath());
+ }
+
+ private void addPath(IPath path) {
+ File absoluteFile = null;
+ IPath location = workspace.getRoot().getFile(path).getLocation();
+ if (location != null) {
+ // location is only present, if a project exists in the workspace
+ // in other words: only if path referenced something inside an existing project
+ absoluteFile = location.toFile().getAbsoluteFile();
+ }
+
+ if (absoluteFile == null) {
+ // if location couldn't be resolved, then it is already an absolute path
+ absoluteFile = path.toFile().getAbsoluteFile();
+ }
+
+ if (!absoluteFile.exists()) {
+ LOG.warn("auxclasspath: Resolved file {} does not exist", absoluteFile);
+ }
+ LOG.debug("auxclasspath: Adding {}", absoluteFile);
+ classpath.add(absoluteFile.toString());
+ }
+}
diff --git a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/IProjectProperties.java b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/IProjectProperties.java
index 86dd6326..5a3dd4e7 100644
--- a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/IProjectProperties.java
+++ b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/IProjectProperties.java
@@ -182,6 +182,18 @@ public interface IProjectProperties {
* The classloader is not stored to the project properties file.
*
* @return the classpath or null if the project is not a java project
+ * @deprecated Since 7.21.0. Avoid using a classloader directly. Use {@link #getClasspath()} instead.
*/
+ @Deprecated
ClassLoader getAuxClasspath();
+
+ /**
+ * Determines the auxiliary classpath needed for type resolution.
+ * The classpath is cached and used for all PMD executions for the same project.
+ * The classpath is not stored to the project properties file.
+ *
+ * @return the classpath or null if the project is not a java project
+ * @since 7.21.0
+ */
+ String getClasspath();
}
diff --git a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/impl/ProjectPropertiesImpl.java b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/impl/ProjectPropertiesImpl.java
index d36c0806..a89eda73 100644
--- a/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/impl/ProjectPropertiesImpl.java
+++ b/net.sourceforge.pmd.eclipse.plugin/src/main/java/net/sourceforge/pmd/eclipse/runtime/properties/impl/ProjectPropertiesImpl.java
@@ -13,6 +13,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.resources.IFile;
@@ -32,6 +33,7 @@
import net.sourceforge.pmd.eclipse.core.internal.FileModificationUtil;
import net.sourceforge.pmd.eclipse.plugin.PMDPlugin;
import net.sourceforge.pmd.eclipse.runtime.cmd.JavaProjectClassLoader;
+import net.sourceforge.pmd.eclipse.runtime.cmd.internal.JavaProjectClasspath;
import net.sourceforge.pmd.eclipse.runtime.properties.IProjectProperties;
import net.sourceforge.pmd.eclipse.runtime.properties.IProjectPropertiesManager;
import net.sourceforge.pmd.eclipse.runtime.properties.PropertiesException;
@@ -67,6 +69,7 @@ public class ProjectPropertiesImpl implements IProjectProperties {
private Set buildPathExcludePatterns = new HashSet<>();
private Set buildPathIncludePatterns = new HashSet<>();
private JavaProjectClassLoader auxclasspath;
+ private JavaProjectClasspath classpath;
/**
* The default constructor takes a project as an argument
@@ -437,7 +440,11 @@ public Set getBuildPathIncludePatterns() {
return buildPathIncludePatterns;
}
+ /**
+ * @deprecated Since 7.21.0. Avoid using a classloader directly. Use {@link #getClasspath()} instead.
+ */
@Override
+ @Deprecated
public ClassLoader getAuxClasspath() {
try {
if (project != null && project.hasNature(JavaCore.NATURE_ID)) {
@@ -466,4 +473,29 @@ public ClassLoader getAuxClasspath() {
}
return null;
}
+
+ @Override
+ public String getClasspath() {
+ try {
+ if (project != null && project.hasNature(JavaCore.NATURE_ID)) {
+ String projectName = project.getName();
+ if (classpath != null && classpath.isModified()) {
+ PMDPlugin.getDefault().logInformation("Classpath of project " + projectName
+ + " changed - recreating it.");
+ classpath = null;
+ }
+
+ if (classpath == null) {
+ PMDPlugin.getDefault()
+ .logInformation("Creating new classpath for project " + project.getName());
+ classpath = new JavaProjectClasspath(project);
+ }
+ return classpath.getClasspath().stream().collect(Collectors.joining(File.pathSeparator));
+ }
+ } catch (CoreException e) {
+ LOG.error("Error determining classpath", e);
+ PMDPlugin.getDefault().logError("Error determining classpath", e);
+ }
+ return null;
+ }
}