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: + * + * + * @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; + } }