-
Notifications
You must be signed in to change notification settings - Fork 135
Plugin to validate all classes on the classpath are uniquely named #267
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
Conversation
5a3deec to
a1275df
Compare
4bbd47b to
cfa7aec
Compare
carterkozak
left a comment
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.
lgtm.
May need to aded a configuration block for allowed duplicates, there are some instances that can be difficult to deduplicate. If we can get away with not providing that functionality, that's likely much better.
|
I think this features actually needs more work - I just ran this on a bigger project and got a pretty intimidating failure. I think the summary needs more information. Also unclear how the singleton set EDIT, updated sorted table: UPDATE 2: I think some of these are actually false positives because the duplicated classes might be identical. For example, the following two jars both contain this guy:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package javax.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.annotation.meta.TypeQualifier;
import javax.annotation.meta.When;
@Documented
@TypeQualifier(
applicableTo = CharSequence.class
)
@Retention(RetentionPolicy.RUNTIME)
public @interface Syntax {
String value();
When when() default When.ALWAYS;
} |
cb013a1 to
f6b4ebd
Compare
|
is there a reason we can't use https://github.com/nebula-plugins/gradle-lint-plugin/wiki/Duplicate-Classes-Rule instead of implementing this ourselves? |
README.md
Outdated
|
|
||
| ### Class Uniqueness Plugin (com.palantir.baseline-class-uniqueness) | ||
|
|
||
| Run `./gradlew checkClassUniqueness` to scan all jars on the `testRuntime` classpath for identically named classes. |
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.
the plugin configures the task with the runtime configuration
| import com.palantir.baseline.tasks.CheckClassUniquenessTask; | ||
| import org.gradle.api.Project; | ||
|
|
||
| @SuppressWarnings("checkstyle:designforextension") // making this 'final' breaks gradle |
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 can make the apply method final and get rid of this suppression
| import org.gradle.api.tasks.OutputFile; | ||
| import org.gradle.api.tasks.TaskAction; | ||
|
|
||
| @SuppressWarnings("checkstyle:designforextension") // making this 'final' breaks gradle |
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.
again, should be able to make the individual methods final. also, maybe include some javadoc mentioning the gradle lint rule and differences in our implementation (e.g. hashing of class files)?
| "'%s' contains multiple copies of identically named classes - " | ||
| + "this may cause different runtime behaviour depending on classpath ordering.\n" | ||
| + "To resolve this, try excluding one of the following jars, " | ||
| + "changing a version or shadowing:\n\n%s", |
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 don't really want to encourage changing a version or shadowing =/
| } | ||
|
|
||
| /** | ||
| * This only exists to convince gradle this task is incremental. |
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 think this comment is strictly necessary -- I feel this is pretty standard/well understood for people who write gradle plugins
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 it's helpful to reassure readers of this code that the file we write really is inconsequential and not magically wired to something elsewhere in the project.
| .getResolvedConfiguration() | ||
| .getResolvedArtifacts(); | ||
|
|
||
| Map<String, Set<ModuleVersionIdentifier>> tempClassToJarMap = new HashMap<>(); |
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.
nit, the other one is called classToJarsMap (plural jars)
| resolvedArtifact.getModuleVersion().getId()); | ||
| } | ||
| } catch (Exception e) { | ||
| log.error("Failed to read JarFile {}", resolvedArtifact, e); |
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.
the resolvedArtifact might not be a jar -- we should probs filter those out first, a la https://github.com/nebula-plugins/gradle-lint-plugin/blob/master/src/main/groovy/com/netflix/nebula/lint/rule/dependency/DependencyService.groovy#L92-L94
| .filter(entry -> entry.getValue().size() > 1) | ||
| .forEach(entry -> { | ||
| // add to the top level map | ||
| entry.getValue().forEach(value -> multiMapPut(classToJarsMap, entry.getKey(), value)); |
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.
not sure why we need to save off this collection if we're not returning it anywhere -- getProblemJars could be implemented with jarsToClasses.keySet(), i believe
| when: | ||
| buildFile << standardBuildFile | ||
| buildFile << """ | ||
| dependencies { |
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 prefer to trim this test case down to a more minimal one
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.
Lots of entries here allows me to verify the sorted table works nicely.
EDIT have thrown away the complicated sorting and split these unit tests.
| then: | ||
| result.getOutput().contains("Identically named classes found in 2 jars ([javax.servlet.jsp:jsp-api:2.1, javax.el:javax.el-api:3.0.0]): [javax.") | ||
| result.getOutput().contains("'runtime' contains multiple copies of identically named classes") | ||
| result.getOutput().contains("(4 classes) com.google.code.findbugs:annotations:3.0.1 net.jcip:jcip-annotations:1.0 com.github.stephenc.jcip:jcip-annotations:1.0-1"); |
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 feel like people would want to know the class names that are duplicated -- thoughts on listing that out? (it would be more verbose =/)
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.
These appear in log lines (at ERROR) level, so they'll appear in the console
|
talked offline, one benefit cited for this approach over the nebula lint rule is that this PR hashes class files, not erroring in cases where identical classes are found on the classpath (this is considered unhygienic but isn't actively harmful). however, after reading through the PR I'm not sure where that hashing of class file content is happening -- ClassUniquenessAnalyzer only seems to handle jar/class names? |
503f40d to
452a0b6
Compare
| dependencies { | ||
| compile gradleApi() | ||
| compile 'net.ltgt.gradle:gradle-errorprone-plugin' | ||
| compile 'com.google.guava:guava' |
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.
let's remove the testCompile dep in line 17
| buildFile << """ | ||
| dependencies { | ||
| compile 'com.palantir.tritium:tritium-api:0.9.0' | ||
| compile 'com.palantir.tritium:tritium-core:0.9.0' |
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 are the duplicate classes here? i'd expect tritium-core to pull in tritium-api and therefore not have any duplicate classes
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.
Just a package-info.java
|
|
||
| dependencies.stream().forEach(resolvedArtifact -> { | ||
| File file = resolvedArtifact.getFile(); | ||
| if (!file.exists()) { |
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.
probably wanna filter out non-jars too to avoid spurious errors in line 92
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.
TBH I'd kinda like to just get an MVP released and get a few repos to pick it up and see what we encounter! The -sources.jar and -javadoc.jar thing doesn't seem to cause a problem because we only consider files ending in .class.
| }); | ||
|
|
||
| // discard all the classes that only come from one jar - these are completely safe! | ||
| classToJars.entrySet().stream() |
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 think we need this collection -- finding which entries of tempClassToHashCodes have value of size > 1 (which we're doing a few lines down) should be sufficient
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.
If we delete classToJars then I don't really see how we can accumulate information about where a class came from and then derive the Map<Set<ModuleVersionIdentifier>, Set<String>> jarsToClasses we need to report problems?
People need to know which are the offending jars!
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.
oh whoops I didn't read this closely enough
| }); | ||
|
|
||
| // discard all the classes that only come from one jar - these are completely safe! | ||
| classToJars.entrySet().stream() |
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.
oh whoops I didn't read this closely enough
Usage
For now, I'm suggesting people explicitly opt-in to this plugin. It adds a new task called
checkClassUniquenesswhich runs as a dependency ofcheck. This task should fail if any jars in the specified classpath happen to contain identically named classes (ie same package and class name).Example output
Followup to #255.
Future work
project(':foo')dependenciesNote: I intentionally didn't include 'classpath' in the name because java10 replaces the concept of a 'classpath' with a 'modulepath' and I think this plugin should be able to cover both.