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