Skip to content

Conversation

@iamdanfox
Copy link
Contributor

@iamdanfox iamdanfox commented Apr 30, 2018

Usage

For now, I'm suggesting people explicitly opt-in to this plugin. It adds a new task called checkClassUniqueness which runs as a dependency of check. This task should fail if any jars in the specified classpath happen to contain identically named classes (ie same package and class name).

apply plugin: 'com.palantir.baseline-class-uniqueness'

Example output

> Task :checkClassUniqueness FAILED
26 Identically named classes with differing impls found in [javax.servlet.jsp:jsp-api:2.1, javax.el:javax.el-api:3.0.0]: [javax.el.ExpressionFactory, javax.el.BeanELResolver, javax.el.CompositeELResolver$CompositeIterator, javax.el.ELContextListener, javax.el.MethodExpression, javax.el.PropertyNotWritableException, javax.el.FunctionMapper, javax.el.MethodInfo, javax.el.BeanELResolver$BeanProperty, javax.el.ELException, javax.el.MapELResolver, javax.el.ArrayELResolver, javax.el.CompositeELResolver, javax.el.ListELResolver, javax.el.Expression, javax.el.ELUtil, javax.el.BeanELResolver$BeanProperties, javax.el.ELResolver, javax.el.MethodNotFoundException, javax.el.ELUtil$1, javax.el.ValueExpression, javax.el.ELContextEvent, javax.el.ResourceBundleELResolver, javax.el.ELContext, javax.el.PropertyNotFoundException, javax.el.VariableMapper]

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':checkClassUniqueness'.
> 'runtime' contains multiple copies of identically named classes - this may cause different runtime behaviour depending on classpath ordering.
  To resolve this, try excluding one of the following jars:
  
  	(26 classes)  javax.servlet.jsp:jsp-api:2.1 javax.el:javax.el-api:3.0.0  

Followup to #255.

Future work

  • More speed
  • handle project(':foo') dependencies

Note: 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.

@iamdanfox iamdanfox force-pushed the classpath-duplicates-plugin branch from 5a3deec to a1275df Compare April 30, 2018 18:55
@iamdanfox iamdanfox force-pushed the classpath-duplicates-plugin branch from 4bbd47b to cfa7aec Compare April 30, 2018 19:55
@iamdanfox iamdanfox changed the title Baseline classpath duplicates plugin Plugin to validate all classes on the classpath are uniquely named Apr 30, 2018
@iamdanfox iamdanfox requested a review from uschi2000 April 30, 2018 22:23
Copy link
Contributor

@carterkozak carterkozak left a 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.

@iamdanfox
Copy link
Contributor Author

iamdanfox commented May 1, 2018

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 [org.apache.xmlbeans:xmlbeans:2.6.0] appeared in this list:

EDIT, updated sorted table:

'testRuntime' contains multiple copies of identically named classes - this may cause different runtime behaviour depending on classpath ordering.
  To resolve this, try excluding one of the following jars, changing a version or shadowing:
  
        (4 classes)   com.google.code.findbugs:annotations:3.0.0        com.github.stephenc.jcip:jcip-annotations:1.0-1   
        (35 classes)  com.google.code.findbugs:annotations:3.0.0        com.google.code.findbugs:jsr305:3.0.0             
        (1 classes)   com.palantir.tritium:tritium-api:0.9.0            com.palantir.tritium:tritium-core:0.9.0           
        (18 classes)  com.sun.jersey:jersey-server:1.9                  org.glassfish.jersey.core:jersey-server:2.25.1    
        (6 classes)   commons-logging:commons-logging:1.2               org.slf4j:jcl-over-slf4j:1.7.25                   
        (26 classes)  javax.servlet.jsp:jsp-api:2.1                     org.glassfish:javax.el:3.0.0                      
        (29 classes)  log4j:log4j:1.2.17                                org.slf4j:log4j-over-slf4j:1.7.25                 
        (8 classes)   org.apache.xmlbeans:xmlbeans:2.6.0                
        (6 classes)   org.glassfish.hk2.external:javax.inject:2.5.0-b32 javax.inject:javax.inject:1                       
        (35 classes)  org.hamcrest:hamcrest-all:1.3                     org.hamcrest:hamcrest-library:1.3                 
        (45 classes)  org.hamcrest:hamcrest-core:1.3                    org.hamcrest:hamcrest-all:1.3     

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:

  • com.google.code.findbugs:annotations:3.0.0
  • com.google.code.findbugs:jsr305:3.0.0
//
// 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;
}

@iamdanfox iamdanfox force-pushed the classpath-duplicates-plugin branch from cb013a1 to f6b4ebd Compare May 1, 2018 13:52
@gracew
Copy link
Contributor

gracew commented May 1, 2018

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.
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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",
Copy link
Contributor

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.
Copy link
Contributor

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

Copy link
Contributor Author

@iamdanfox iamdanfox May 2, 2018

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<>();
Copy link
Contributor

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.filter(entry -> entry.getValue().size() > 1)
.forEach(entry -> {
// add to the top level map
entry.getValue().forEach(value -> multiMapPut(classToJarsMap, entry.getKey(), value));
Copy link
Contributor

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 {
Copy link
Contributor

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

Copy link
Contributor Author

@iamdanfox iamdanfox May 2, 2018

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");
Copy link
Contributor

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 =/)

Copy link
Contributor Author

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

@gracew
Copy link
Contributor

gracew commented May 1, 2018

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?

@iamdanfox iamdanfox force-pushed the classpath-duplicates-plugin branch from 503f40d to 452a0b6 Compare May 2, 2018 15:57
dependencies {
compile gradleApi()
compile 'net.ltgt.gradle:gradle-errorprone-plugin'
compile 'com.google.guava:guava'
Copy link
Contributor

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'
Copy link
Contributor

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

Copy link
Contributor Author

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()) {
Copy link
Contributor

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

Copy link
Contributor Author

@iamdanfox iamdanfox May 2, 2018

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()
Copy link
Contributor

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

Copy link
Contributor Author

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!

Copy link
Contributor

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()
Copy link
Contributor

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

@iamdanfox iamdanfox merged commit de7737c into develop May 2, 2018
@robert3005 robert3005 deleted the classpath-duplicates-plugin branch August 13, 2018 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants