Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 50 additions & 4 deletions japicmp/src/main/java/japicmp/compat/CompatibilityChanges.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import japicmp.util.SignatureParser;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

import java.util.*;
Expand Down Expand Up @@ -448,10 +449,14 @@ private void checkIfMethodsHaveChangedIncompatible(JApiClass jApiClass, Map<Stri
private void checkIfReturnTypeChanged(JApiMethod method) {
// section 13.4.15 of "Java Language Specification" SE7 (Method Result Type)
if (method.getReturnType().getChangeStatus() == JApiChangeStatus.MODIFIED) {
JApiCompatibilityChange change = addCompatibilityChange(method, JApiCompatibilityChangeType.METHOD_RETURN_TYPE_CHANGED);
if (method.getAccessModifier().hasChangedToMoreVisible()) {
change.setSourceCompatible(true);
change.setBinaryCompatible(true);
if (isCovariantReturnTypeChange(method)) {
addCompatibilityChange(method, JApiCompatibilityChangeType.METHOD_RETURN_TYPE_COVARIANT_CHANGED);
} else {
JApiCompatibilityChange change = addCompatibilityChange(method, JApiCompatibilityChangeType.METHOD_RETURN_TYPE_CHANGED);
if (method.getAccessModifier().hasChangedToMoreVisible()) {
change.setSourceCompatible(true);
change.setBinaryCompatible(true);
}
}
}
if (method.getChangeStatus() == JApiChangeStatus.MODIFIED ||
Expand All @@ -462,6 +467,47 @@ private void checkIfReturnTypeChanged(JApiMethod method) {
}
}

private boolean isCovariantReturnTypeChange(JApiMethod method) {
if (!method.getOldMethod().isPresent() || !method.getNewMethod().isPresent()) {
return false;
}
// Check for a compiler-generated bridge method with the original return type in the new class
String methodName = method.getName();
String oldReturnType = method.getReturnType().getOldReturnType();
JApiClass jApiClass = method.getjApiClass();
boolean bridgeMethodFound = false;
for (JApiMethod candidate : jApiClass.getMethods()) {
if (candidate == method) {
continue;
}
if (!candidate.getName().equals(methodName)) {
continue;
}
Optional<BridgeModifier> newBridgeModifier = candidate.getBridgeModifier().getNewModifier();
if (!newBridgeModifier.isPresent() || newBridgeModifier.get() != BridgeModifier.BRIDGE) {
continue;
}
if (!candidate.getReturnType().getNewReturnType().equals(oldReturnType)) {
continue;
}
if (candidate.hasSameParameter(method)) {
bridgeMethodFound = true;
break;
}
}
if (!bridgeMethodFound) {
return false;
}
// Verify the new return type is a subtype of the old return type
try {
CtClass oldReturnCtClass = method.getOldMethod().get().getReturnType();
CtClass newReturnCtClass = method.getNewMethod().get().getReturnType();
return newReturnCtClass.subtypeOf(oldReturnCtClass);
} catch (NotFoundException e) {
return true; // trust the bridge method as the primary indicator
}
}

private void checkIfParametersGenericsChanged(JApiBehavior jApiBehavior) {
if (jApiBehavior.getChangeStatus() == JApiChangeStatus.MODIFIED ||
jApiBehavior.getChangeStatus() == JApiChangeStatus.UNCHANGED) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum JApiCompatibilityChangeType {
METHOD_LESS_ACCESSIBLE_THAN_IN_SUPERCLASS(false, false, JApiSemanticVersionLevel.MAJOR),
METHOD_IS_STATIC_AND_OVERRIDES_NOT_STATIC(false, false, JApiSemanticVersionLevel.MAJOR),
METHOD_RETURN_TYPE_CHANGED(false, false, JApiSemanticVersionLevel.MAJOR),
METHOD_RETURN_TYPE_COVARIANT_CHANGED(true, true, JApiSemanticVersionLevel.MINOR),
METHOD_RETURN_TYPE_GENERICS_CHANGED(true, false, JApiSemanticVersionLevel.MINOR),
METHOD_PARAMETER_GENERICS_CHANGED(true, false, JApiSemanticVersionLevel.MINOR),
METHOD_NOW_ABSTRACT(false, false, JApiSemanticVersionLevel.MAJOR),
Expand Down
39 changes: 39 additions & 0 deletions japicmp/src/test/java/japicmp/compat/MethodCompatibilityTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,45 @@ public List<CtClass> createNewClasses(ClassPool classPool) throws Exception {
assertThat(jApiMethod.isSourceCompatible(), is(true));
}

@Test
void testMethodReturnTypeCovariantChange() throws Exception {
JarArchiveComparatorOptions options = new JarArchiveComparatorOptions();
options.setIncludeSynthetic(true);
List<JApiClass> jApiClasses = ClassesHelper.compareClasses(options, new ClassesHelper.ClassesGenerator() {
@Override
public List<CtClass> createOldClasses(ClassPool classPool) throws Exception {
CtClass ctClass = CtClassBuilder.create().name("japicmp.Test").addToClassPool(classPool);
CtMethodBuilder.create().publicAccess().returnType(classPool.get("java.lang.Object")).name("getValue").body("return null;").addToClass(ctClass);
return Collections.singletonList(ctClass);
}

@Override
public List<CtClass> createNewClasses(ClassPool classPool) throws Exception {
CtClass ctClass = CtClassBuilder.create().name("japicmp.Test").addToClassPool(classPool);
// Covariant override — Integer is a subtype of Object
CtMethodBuilder.create().publicAccess().returnType(classPool.get("java.lang.Integer")).name("getValue").body("return Integer.valueOf(0);").addToClass(ctClass);
// Compiler-generated bridge method with original return type
CtMethodBuilder.create().publicAccess().bridgeModifier().syntheticModifier().returnType(classPool.get("java.lang.Object")).name("getValue").body("return null;").addToClass(ctClass);
return Collections.singletonList(ctClass);
}
});
JApiClass jApiClass = getJApiClass(jApiClasses, "japicmp.Test");
assertThat(jApiClass.getChangeStatus(), is(JApiChangeStatus.MODIFIED));
JApiMethod nonBridgeMethod = null;
for (JApiMethod m : jApiClass.getMethods()) {
if (m.getName().equals("getValue") &&
m.getBridgeModifier().getNewModifier().map(bm -> bm == BridgeModifier.NON_BRIDGE).orElse(false)) {
nonBridgeMethod = m;
break;
}
}
assertThat(nonBridgeMethod, is(notNullValue()));
assertThat(nonBridgeMethod.getCompatibilityChanges(), hasItem(new JApiCompatibilityChange(JApiCompatibilityChangeType.METHOD_RETURN_TYPE_COVARIANT_CHANGED)));
assertThat(nonBridgeMethod.getCompatibilityChanges(), not(hasItem(new JApiCompatibilityChange(JApiCompatibilityChangeType.METHOD_RETURN_TYPE_CHANGED))));
assertThat(nonBridgeMethod.isBinaryCompatible(), is(true));
assertThat(jApiClass.isBinaryCompatible(), is(true));
}

@Test
void testMethodNowAbstract() throws Exception {
JarArchiveComparatorOptions options = new JarArchiveComparatorOptions();
Expand Down
5 changes: 5 additions & 0 deletions japicmp/src/test/java/japicmp/util/CtMethodBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public CtMethodBuilder syntheticModifier() {
return this;
}

public CtMethodBuilder bridgeModifier() {
this.modifier = this.modifier | ModifierHelper.ACC_BRIDGE;
return this;
}

public CtMethodBuilder parameters(CtClass[] parameters) {
return (CtMethodBuilder) super.parameters(parameters);
}
Expand Down
1 change: 1 addition & 0 deletions src/site/markdown/MavenPlugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ for each check. This allows you to customize the following verifications:
| METHOD_REMOVED | false | false | MAJOR |
| METHOD_REMOVED_IN_SUPERCLASS | false | false | MAJOR |
| METHOD_RETURN_TYPE_CHANGED | false | false | MAJOR |
| METHOD_RETURN_TYPE_COVARIANT_CHANGED | true | true | MINOR |
| METHOD_RETURN_TYPE_GENERICS_CHANGED | true | false | MINOR |
| METHOD_STATIC_IN_INTERFACE_NO_LONGER_STATIC | false | false | MAJOR |
| SUPERCLASS_ADDED | true | true | MINOR |
Expand Down
Loading