diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/AbstractCompileMojo.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/AbstractCompileMojo.java index 8378d4d9..616b3890 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/AbstractCompileMojo.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/AbstractCompileMojo.java @@ -16,6 +16,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -50,6 +51,7 @@ import io.takari.incrementalbuild.Incremental; import io.takari.incrementalbuild.Incremental.Configuration; +import io.takari.incrementalbuild.MessageSeverity; import io.takari.incrementalbuild.ResourceMetadata; import io.takari.maven.plugins.compile.javac.CompilerJavacLauncher; import io.takari.maven.plugins.exportpackage.ExportPackageMojo; @@ -222,6 +224,15 @@ public static enum Sourcepath { @Parameter(defaultValue = "ignore") private AccessRulesViolation privatePackageReference; + /** + * Sets "unused dependency declaration" policy violation action + *

+ * If {@code error}, any dependencies declared in project pom.xml file must be referenced. A lack of reference to any declared dependency will result in compilation errors. If {@code ignore} (the + * default) declaring dependencies without referencing them is allowed. + */ + @Parameter(defaultValue = "ignore") + private AccessRulesViolation unusedDeclaredDependency; + /** * Controls compilation sourcepath. If set to {@code disable}, compilation sourcepath will be empty. If set to {@code reactorProjects}, compilation sourcepath will be set to compile source roots (or * test compile source roots) of dependency projects of the same reactor build. The default is {@code reactorProjects} if {@code proc=only}, otherwise the default is {@code disable}. @@ -378,7 +389,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { mkdirs(getOutputDirectory()); } - final List classpath = getClasspath(); + final Map classpathMap = getClasspath(); + final List classpath = new ArrayList<>(classpathMap.keySet()); final List processorpath = getProcessorpath(); Proc proc = getEffectiveProc(classpath, processorpath); @@ -419,6 +431,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { log.info("Compiling {} sources to {}", sources.size(), getOutputDirectory()); int compiled = compiler.compile(); log.info("Compiled {} out of {} sources ({} ms)", compiled, sources.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS)); + if (unusedDeclaredDependency != AccessRulesViolation.ignore && context.isEscalated()) { + checkUnusedDependencies(compiler.getReferencedClasspathEntries(), classpathMap); + } } else { compiler.skipCompile(); log.info("Skipped compilation, all {} classes are up to date", sources.size()); @@ -433,18 +448,53 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } + private void checkUnusedDependencies(Set referencedEntries, Map classpathMap) throws MojoExecutionException, IOException { + Set referencedArtifacts = new LinkedHashSet<>(); + Set unusedArtifacts = new LinkedHashSet<>(); + List processorPath = getProcessorpath(); + + // Include processor path in referenced entries + Set allReferencedEntries = new LinkedHashSet<>(); + allReferencedEntries.addAll(referencedEntries); + if (processorPath != null) { + allReferencedEntries.addAll(processorPath); + } + + // Find the equivalent artifact equivalent for each referenced entry + for (File entry : allReferencedEntries) { + Artifact artifact = classpathMap.get(entry); + if (artifact != null) { + referencedArtifacts.add(artifact); + } + } + + if (directDependencies != null) { + // Check each direct dependency for existence in referenced artifacts + for (Artifact dependency : directDependencies) { + if (!referencedArtifacts.contains(dependency)) { + unusedArtifacts.add(dependency); + } + } + } + + // Fail the build for any direct dependencies that are never referenced + if (!unusedArtifacts.isEmpty()) { + context.addPomMessage("The following dependencies are declared but are not used: " + unusedArtifacts, MessageSeverity.ERROR, null); + } + } + private static Set toFileSet(Set paths) { Set files = new LinkedHashSet<>(); paths.forEach(path -> files.add(new File(path))); return files; } - private List getClasspath() { - List classpath = new ArrayList(); + private Map getClasspath() { + Map classpath = new LinkedHashMap<>(); for (Artifact artifact : getClasspathArtifacts()) { File file = artifact.getFile(); if (file != null) { - classpath.add(file); + classpath.put(file, artifact); } } return classpath; diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/AbstractCompiler.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/AbstractCompiler.java index 07e29d96..3a9b6818 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/AbstractCompiler.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/AbstractCompiler.java @@ -193,6 +193,8 @@ protected boolean isShowWarnings() { public abstract int compile() throws MojoExecutionException, IOException; + protected abstract Set getReferencedClasspathEntries(); + public void skipCompile() { context.markUptodateExecution(); } diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/javac/AbstractCompilerJavac.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/javac/AbstractCompilerJavac.java index acbf1efc..4dbab637 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/javac/AbstractCompilerJavac.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/javac/AbstractCompilerJavac.java @@ -273,6 +273,12 @@ public final int compile() throws MojoExecutionException, IOException { return compile(files); } + @Override + protected Set getReferencedClasspathEntries() { + String msg = String.format("Compiler %s does not support unusedDeclaredDependency=error, use compilerId=%s", getCompilerId(), CompilerJdt.ID); + throw new UnsupportedOperationException(msg); + } + protected abstract int compile(Map> sources) throws MojoExecutionException, IOException; protected abstract String getCompilerId(); diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/AccessRestrictionClasspathEntry.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/AccessRestrictionClasspathEntry.java index 4032b40c..a420fa22 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/AccessRestrictionClasspathEntry.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/AccessRestrictionClasspathEntry.java @@ -1,5 +1,6 @@ package io.takari.maven.plugins.compile.jdt; +import java.nio.file.Path; import java.util.Collection; import org.eclipse.jdt.core.compiler.IProblem; @@ -39,6 +40,11 @@ public String getEntryDescription() { return sb.toString(); } + @Override + public Path getLocation() { + return entry.getLocation(); + } + public static AccessRestrictionClasspathEntry forbidAll(DependencyClasspathEntry entry) { AccessRule accessRule = new AccessRule(null /* pattern */, IProblem.ForbiddenReference, true /* keep looking for accessible type */); AccessRestriction accessRestriction = new AccessRestriction(accessRule, AccessRestriction.COMMAND_LINE, entry.getEntryName()); diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/CompilerJdt.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/CompilerJdt.java index 9094318e..6faf7205 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/CompilerJdt.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/CompilerJdt.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; @@ -144,6 +145,8 @@ public class CompilerJdt extends AbstractCompiler implements ICompilerRequestor private final Map> sources = new LinkedHashMap<>(); + private Set referencedClasspathEntries = new LinkedHashSet<>(); + /** * Set of ICompilationUnit to be compiled. */ @@ -719,6 +722,7 @@ protected synchronized void addCompilationUnit(ICompilationUnit sourceUnit, Comp compiler.options.storeAnnotations = true; } + referencedClasspathEntries = namingEnvironment.getReferencedEntries(); return strategy.compile(namingEnvironment, compiler); } finally { if (fileManager != null) { @@ -987,4 +991,18 @@ public void skipCompile() { strategy.skipCompile(); super.skipCompile(); } + + @Override + public Set getReferencedClasspathEntries() { + return referencedClasspathEntries.stream().map(this::pathToFile).filter(file -> file != null).collect(Collectors.toSet()); + } + + private File pathToFile(Path path) { + try { + return path.toFile(); + } catch (UnsupportedOperationException e) { + // Java 9 support + return null; + } + } } diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/OutputDirectoryClasspathEntry.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/OutputDirectoryClasspathEntry.java index 24b7eba9..c2302c47 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/OutputDirectoryClasspathEntry.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/OutputDirectoryClasspathEntry.java @@ -69,4 +69,9 @@ public String toString() { public String getEntryDescription() { return directory.getAbsolutePath(); } + + @Override + public Path getLocation() { + return directory.toPath(); + } } diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/Classpath.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/Classpath.java index d4eb3ce7..d2539d42 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/Classpath.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/Classpath.java @@ -7,8 +7,11 @@ */ package io.takari.maven.plugins.compile.jdt.classpath; +import java.nio.file.Path; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.compiler.env.INameEnvironment; @@ -26,10 +29,13 @@ public class Classpath implements INameEnvironment { private Multimap packages; + private Set referencedentries; + public Classpath(List entries, List localentries) { this.entries = entries; this.mutableentries = localentries; this.packages = newPackageIndex(entries); + this.referencedentries = new LinkedHashSet<>(); } private static Multimap newPackageIndex(List entries) { @@ -58,14 +64,17 @@ public NameEnvironmentAnswer findType(char[] typeName, char[][] packageName) { } private NameEnvironmentAnswer findType(String packageName, String typeName) { + Path used = null; NameEnvironmentAnswer suggestedAnswer = null; Collection entries = !packageName.isEmpty() ? packages.get(packageName) : this.entries; if (entries != null) { for (ClasspathEntry entry : entries) { NameEnvironmentAnswer answer = entry.findType(packageName, typeName); if (answer != null) { + used = entry.getLocation(); if (!answer.ignoreIfBetter()) { if (answer.isBetter(suggestedAnswer)) { + referencedentries.add(used); return answer; } } else if (answer.isBetter(suggestedAnswer)) { @@ -75,6 +84,9 @@ private NameEnvironmentAnswer findType(String packageName, String typeName) { } } } + if (used != null) { + referencedentries.add(used); + } return suggestedAnswer; } @@ -102,4 +114,8 @@ public void reset() { public List getEntries() { return entries; } + + public Set getReferencedEntries() { + return referencedentries; + } } diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/ClasspathEntry.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/ClasspathEntry.java index 8b70d3a5..e7179ccc 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/ClasspathEntry.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/ClasspathEntry.java @@ -7,6 +7,7 @@ */ package io.takari.maven.plugins.compile.jdt.classpath; +import java.nio.file.Path; import java.util.Collection; import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer; @@ -18,4 +19,6 @@ public interface ClasspathEntry { NameEnvironmentAnswer findType(String packageName, String typeName); String getEntryDescription(); + + Path getLocation(); } diff --git a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/DependencyClasspathEntry.java b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/DependencyClasspathEntry.java index f3708f64..cf537d6c 100644 --- a/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/DependencyClasspathEntry.java +++ b/takari-lifecycle-plugin/src/main/java/io/takari/maven/plugins/compile/jdt/classpath/DependencyClasspathEntry.java @@ -120,6 +120,11 @@ public String getEntryDescription() { return sb.toString(); } + @Override + public Path getLocation() { + return file; + } + @Override public NameEnvironmentAnswer findType(String packageName, String typeName) { return findType(packageName, typeName, getAccessRestriction(packageName)); diff --git a/takari-lifecycle-plugin/src/test/java/io/takari/maven/plugins/compile/CompileTest.java b/takari-lifecycle-plugin/src/test/java/io/takari/maven/plugins/compile/CompileTest.java index cb8cdd30..8890d04e 100644 --- a/takari-lifecycle-plugin/src/test/java/io/takari/maven/plugins/compile/CompileTest.java +++ b/takari-lifecycle-plugin/src/test/java/io/takari/maven/plugins/compile/CompileTest.java @@ -374,4 +374,74 @@ public void testInnerTypeDependency_sourceDependencies() throws Exception { mojos.compile(project, newParameter("dependencySourceTypes", "prefer")); mojos.assertBuildOutputs(new File(basedir, "target/classes"), "innertyperef/InnerTypeRef.class"); } + + @Test + public void testReferencedEntries() throws Exception { + final String javacMessage = "Compiler javac does not support unusedDeclaredDependency=error, use compilerId=jdt"; + File parent = resources.getBasedir("compile/referenced-entries"); + Xpp3Dom unused = new Xpp3Dom("unusedDeclaredDependency"); + unused.setValue("error"); + + mojos.flushClasspathCaches(); + + File b = new File(parent, "b"); + + try { + mojos.compile(b, unused); + } catch (UnsupportedOperationException e) { + ErrorMessage.isMatch(e.getMessage(), javacMessage); + } + + MavenProject a = mojos.readMavenProject(new File(parent, "a")); + addDependency(a, "b", new File(b, "target/classes")); + + try { + mojos.compile(a, unused); + mojos.assertBuildOutputs(parent, "a/target/classes/a/A1.class", "a/target/classes/a/A2.class"); + } catch (UnsupportedOperationException e) { + ErrorMessage.isMatch(e.getMessage(), javacMessage); + } + + cp(new File(parent, "a/src/main/java/a"), "A2.java-method", "A2.java"); + + try { + mojos.compile(a, unused); + // only A2 should have new output + mojos.assertBuildOutputs(parent, "a/target/classes/a/A2.class"); + } catch (UnsupportedOperationException e) { + ErrorMessage.isMatch(e.getMessage(), javacMessage); + } + } + + @Test + public void testReactorUnused() throws Exception { + final String javacMessage = "Compiler javac does not support unusedDeclaredDependency=error, use compilerId=jdt"; + ErrorMessage expected = new ErrorMessage(compilerId); + expected.setSnippets("jdt", "ERROR pom.xml [0:0] The following dependencies are declared but are not used: [test:module-b:jar:1.0:compile]"); + File parent = resources.getBasedir("compile/reactor-unused"); + Xpp3Dom unused = new Xpp3Dom("unusedDeclaredDependency"); + unused.setValue("error"); + + mojos.flushClasspathCaches(); + + File moduleB = new File(parent, "module-b"); + + try { + mojos.compile(moduleB, unused); + } catch (UnsupportedOperationException e) { + ErrorMessage.isMatch(e.getMessage(), javacMessage); + } + + MavenProject moduleA = mojos.readMavenProject(new File(parent, "module-a")); + addDependency(moduleA, "module-b", new File(moduleB, "target/classes")); + + try { + mojos.compile(moduleA, unused); + Assert.fail(); + } catch (MojoExecutionException e) { + mojos.assertMessage(parent, "module-a/pom.xml", expected); + } catch (UnsupportedOperationException e) { + ErrorMessage.isMatch(e.getMessage(), javacMessage); + } + } } diff --git a/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-a/pom.xml b/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-a/pom.xml new file mode 100644 index 00000000..a538f1e8 --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-a/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + reactor + module-a + 1.0.0-SNAPSHOT + jar + + + + reactor + module-b + 1.0.0-SNAPSHOT + + + \ No newline at end of file diff --git a/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-a/src/main/java/reactor/modulea/ModuleA.java b/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-a/src/main/java/reactor/modulea/ModuleA.java new file mode 100644 index 00000000..093664e6 --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-a/src/main/java/reactor/modulea/ModuleA.java @@ -0,0 +1,5 @@ +package reactor.modulea; + +public class ModuleA { + +} diff --git a/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-b/pom.xml b/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-b/pom.xml new file mode 100644 index 00000000..1e20d0fa --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/reactor-unused/module-b/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + + reactor + module-b + 1.0.0-SNAPSHOT + jar + + \ No newline at end of file diff --git a/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/pom.xml b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/pom.xml new file mode 100644 index 00000000..4c7d57da --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + a + a + 1.0.0-SNAPSHOT + jar + + + + b + b + 1.0.0-SNAPSHOT + + + \ No newline at end of file diff --git a/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A1.java b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A1.java new file mode 100644 index 00000000..6ce52967 --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A1.java @@ -0,0 +1,10 @@ +package a; + +import b.B; + +public class A1 { + public A1() { + B b = new B(); + b.foo(); + } +} \ No newline at end of file diff --git a/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A2.java b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A2.java new file mode 100644 index 00000000..22b68c00 --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A2.java @@ -0,0 +1,7 @@ +package a; + +public class A2 { + public A2() { + + } +} \ No newline at end of file diff --git a/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A2.java-method b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A2.java-method new file mode 100644 index 00000000..ee9dc030 --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/a/src/main/java/a/A2.java-method @@ -0,0 +1,11 @@ +package a; + +public class A2 { + public A2() { + + } + + public void bar() { + + } +} \ No newline at end of file diff --git a/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/b/pom.xml b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/b/pom.xml new file mode 100644 index 00000000..5eaef953 --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/b/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + + b + b + 1.0.0-SNAPSHOT + jar + + \ No newline at end of file diff --git a/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/b/src/main/java/b/B.java b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/b/src/main/java/b/B.java new file mode 100644 index 00000000..2735c645 --- /dev/null +++ b/takari-lifecycle-plugin/src/test/projects/compile/referenced-entries/b/src/main/java/b/B.java @@ -0,0 +1,11 @@ +package b; + +public class B { + public B() { + + } + + public void foo() { + + } +} \ No newline at end of file