-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Detect and allow failing for unused/undeclared dependencies #62
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the contribution, Ben. I think this is a useful addition to takari-lifecycle and agree with implementation approach. There are few code-polish issues, but nothing major. Let me know if you have any questions.
@@ -273,6 +273,11 @@ public final int compile() throws MojoExecutionException, IOException { | |||
return compile(files); | |||
} | |||
|
|||
@Override | |||
public Set<String> getReferencedClasspathEntries() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't silently ignore unsupported policy value, make it clear to the users that unusedDeclaredDependencies=error
only works with JDT-based compiler backend. See how this is done for transitiveDependencyReference
and privatePackageReference
(or just move unusedDeclaredDependency
to CompilerJdt
)
List<File> processorpath = getProcessorpath(); | ||
|
||
// Find the equivalent artifact equivalent for each referenced entry | ||
for (String entry : referencedEntries) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is N^2 loop and needs optimization. For a project with 3000 dependencies it'll run 9000000 (9 million) iterations. I'd change #getClasspath()
to return Map<File,Artifact>
and then do single for-each referencedEntries to match entries to classpath artifacts.
if (unusedDeclaredDependency == AccessRulesViolation.error) { | ||
context.addPomMessage("The following dependencies are declared but are not used: " + unusedArtifacts, MessageSeverity.ERROR, null); | ||
} else { | ||
log.warn(pom.getAbsolutePath() + ": The following dependencies are declared but are not used: {}", unusedArtifacts); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ignore
means ignore, don't log anything here. actually, don't execute the check at all if unusedDeclaredDependency=ignore
.
NameEnvironmentAnswer suggestedAnswer = null; | ||
Collection<ClasspathEntry> 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.getEntryDescription(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
entry.getEntryDescription()
is for humans to read, introduce new Path entry.getLocation()
to get classpath entry location for further processing.
@@ -102,4 +116,8 @@ public void reset() { | |||
public List<ClasspathEntry> getEntries() { | |||
return entries; | |||
} | |||
|
|||
public Set<String> getReferencedEntries() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return Set<Path>
mojos.compile(moduleA, unused); | ||
fail(); | ||
} catch (MojoExecutionException e) { | ||
assertTrue(e.getLocalizedMessage().contains("The following dependencies are declared but are not used: [test:module-b:jar:1.0:compile]")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't assert the exception, use CompileRule.assertMessages
to assert expected message was created on the pom file.
@@ -0,0 +1,5 @@ | |||
package reactor.moduleb; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove unused test file
@@ -0,0 +1,5 @@ | |||
package reactor.moduleb; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove unused test file
@@ -0,0 +1,8 @@ | |||
package reactor.moduleb; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove unused test file
@@ -0,0 +1,14 @@ | |||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove unused test file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the comments. I've made a new commit to address them. I also added a few questions here.
@@ -117,7 +115,7 @@ public void reset() { | |||
return entries; | |||
} | |||
|
|||
public Set<String> getReferencedEntries() { | |||
public Set<Path> getReferencedEntries() { | |||
return referencedentries; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to use File
here to have parity with File
in AbstractCompiler
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently Classpath
uses Path
, and CompilerJdt
translates between Path
and File
. Beware, however, that under java 9 system libraries will not have corresponding File
s
undeclaredArtifacts.add(artifact); | ||
} | ||
} | ||
} | ||
|
||
// Log or fail the build for any referenced artifacts that are not declared as a direct dependency | ||
// Fail the build for any referenced artifacts that are not declared as a direct dependency | ||
if (!undeclaredArtifacts.isEmpty()) { | ||
if (unusedDeclaredDependency == AccessRulesViolation.error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this again. Would it make sense to have a separate AccessRulesViolation
for "undeclared" vs "unused" artifacts? To fail only on undeclared or unused rather than failing on any combination of the two. Although, I guess technically speaking, "undeclared" is almost the same thing as transitiveDependencyReference
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, I missed it.
checkDependencyReferences
should really be checkUnusedDependencies
and only check for unused dependencies declared in project pom.xml (or inherited from the parent).
Existing transitiveDependencyReference
policy already covers references to transitive/undeclared dependencies. No need duplicate functionality. I'll update review in the code too.
|
||
// Find the equivalent artifact equivalent for each referenced entry | ||
for (File entry : allReferencedEntries) { | ||
Artifact artifact = getClasspath().get(entry); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This unnecessary calculates new classpath map for each referenced entry. At least pull #getClasspath()
out of the loop. Better yet, pass classpath map as a parameter, so the same map is used during compilation and when checking for unused dependencies. (which will also ensure the two classpaths are the same)
|
||
// Fail the build for any referenced artifacts that are not declared as a direct dependency | ||
if (!undeclaredArtifacts.isEmpty()) { | ||
if (unusedDeclaredDependency == AccessRulesViolation.error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unusedDeclaredDependency == AccessRulesViolation.error
is redundant here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, remove this branch altogether. transitiveDependencyReference
policy already provides mechanism to block use of transitive dependencies, and that mechanism also provides more targeted error messages.
private List<File> getClasspath() { | ||
List<File> classpath = new ArrayList<File>(); | ||
private Map<File, Artifact> getClasspath() { | ||
Map<File, Artifact> classpath = new LinkedHashMap<File, Artifact>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use diamond operator.
@@ -121,6 +122,11 @@ public String getEntryDescription() { | |||
return sb.toString(); | |||
} | |||
|
|||
@Override | |||
public Path getLocation() { | |||
return file.toPath(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What branch are you using? #file
has type Path
in the current master.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My fork is 5 commits behind master, so I'll have to rebase the branch.
@@ -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 && compiler.getReferencedClasspathEntries() != null && !compiler.getReferencedClasspathEntries().isEmpty()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check compiler.getReferencedClasspathEntries() != null && !compiler.getReferencedClasspathEntries().isEmpty()
is redundant here.
|
||
// Fail the build for any direct dependencies that are never referenced | ||
if (!unusedArtifacts.isEmpty()) { | ||
if (unusedDeclaredDependency == AccessRulesViolation.error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unusedDeclaredDependency == AccessRulesViolation.error
is redundant here.
@@ -59,6 +61,8 @@ | |||
|
|||
private AccessRulesViolation privatePackageReference; | |||
|
|||
private AccessRulesViolation unusedDeclaredDependency; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think unusedDeclaredDependency
and corresponding getter are redundant.
this.unusedDeclaredDependency = unusedDeclaredDependency; | ||
} | ||
|
||
protected Set<File> getReferencedClasspathEntries() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd define getReferencedClasspathEntries()
as abstract here and throw UnsupportedOperationException
in Javac compiler backend.
@@ -433,18 +448,70 @@ public void execute() throws MojoExecutionException, MojoFailureException { | |||
} | |||
} | |||
|
|||
private void checkDependencyReferences(Set<File> referencedEntries) throws MojoExecutionException, IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be called checkUnusedDependencies
.
|
||
// Fail the build for any referenced artifacts that are not declared as a direct dependency | ||
if (!undeclaredArtifacts.isEmpty()) { | ||
if (unusedDeclaredDependency == AccessRulesViolation.error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, remove this branch altogether. transitiveDependencyReference
policy already provides mechanism to block use of transitive dependencies, and that mechanism also provides more targeted error messages.
a9a865c
to
4e2a9db
Compare
Thanks for the quick review and answers to my questions. Added another commit to address your comments(along with rebasing the branch on the latest master). |
This change fails when running the build under java 9. I haven't looked closely, but suspect I also would like to see an automated test for the following scenario:
|
* refactor checking logic to remove n^2 loop * setting unusedDeclaredDependency to ignore skips check logic * use Path & File over String for referenced entries * Move test to CompileTest * Remove unused test files
* removed undeclared dependency checking * remove redundant checking, calls, and methods
4e2a9db
to
81593db
Compare
I also made a change for |
return path.toFile(); | ||
} catch (UnsupportedOperationException e) { | ||
// Java 9 support | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not feel right, to be honest. I think it will be cleaner to remember classpath dependencies as Set<Path>
and filter referenced dependencies based on membership in the set.
|
||
@Test | ||
public void testReferencedEntries() throws Exception { | ||
final String javacMessage = "Compiler javac does not support unusedDeclaredDependency=error, use compilerId=jdt"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Assume.assumeTrue(CompilerJdt.ID.equals(compilerId));
to only run the test with JDT compiler backend. Validating javac error message pollutes the test and makes it more difficult to understand.
mojos.assertBuildOutputs(parent, "a/target/classes/a/A2.class"); | ||
} catch (UnsupportedOperationException e) { | ||
ErrorMessage.isMatch(e.getMessage(), javacMessage); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for completeness sake this test should also remove a.A1
->b.B
dependency and assert that only a.A1
got recompiled and that unused dependency error was reported.
@@ -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()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1
&& context.isEscalated()
here will result in errors reported during full build "magically" disappear during incremental build, only to reappear during the next full build or when modifying some sources but not others.
We need to find a way to make unusedDeclaredDependency=error
work during incremental build. I think the only way to do this is to track dependencies used by each source, record this information in build context and use it during subsequent incremental builds. I don't know how easy this is to implement.
Do you see any other way to correctly support incremental build?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. I did not consider that scenario, and I agree with what you said. I'll remove this change.
I do not see another way from what you detailed. Since there is a need of persistence of the set of dependencies referenced between builds, I believe to keep that set of dependencies accurate, each source's list of dependencies would need to be tracked.
Along those lines, I'm wondering if something like a Map<Path, Set<Artifact>>
where at the end of each build, any sources that were built would update the map would work (or the reverse with each dependency listing each source that references it where any that are still empty by the end of the build are considered unused).
Here is another incremental build test I'd like to see
Compiler uses observable class features (like members or class modifiers) to determine how what project sources needs to be recompiled after classpath dependency change. Removing redundant dependency does not change set of available types, so nothing is recompiled. Moving classes from one dependency to another will have the same effect. I don't have a good proposal how to solve this. |
This adds tracking on what dependencies are referenced in the jdt compiler.