apicheck: Allow safe addition and removal of final qualifier.
Split the CHANGED_FINAL error into ADDED_FINAL,
ADDED_FINAL_UNINSTANTIABLE and REMOVED_FINAL so that the
API check scripts can more precisely target policy around
changes to the final qualifier.
ADDED_FINAL_UNINSTANTIABLE covers the case of a class
that has become final but that was previously not
instantiable. This change is safe since applications could
not have declared subclasses of their own.
Change-Id: I36a143bdbcffd04379788a55ff996b47398ba4fe
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java
index 927b240..f3993eb 100644
--- a/src/com/google/doclava/ClassInfo.java
+++ b/src/com/google/doclava/ClassInfo.java
@@ -1694,10 +1694,25 @@
+ " changed abstract qualifier");
}
- if (mIsFinal != cl.mIsFinal) {
+ if (!mIsFinal && cl.mIsFinal) {
+ /*
+ * It is safe to make a class final if it did not previously have any public
+ * constructors because it was impossible for an application to create a subclass.
+ */
+ if (mApiCheckConstructors.isEmpty()) {
+ consistent = false;
+ Errors.error(Errors.ADDED_FINAL_UNINSTANTIABLE, cl.position(),
+ "Class " + cl.qualifiedName() + " added final qualifier but "
+ + "was previously uninstantiable and therefore could not be subclassed");
+ } else {
+ consistent = false;
+ Errors.error(Errors.ADDED_FINAL, cl.position(), "Class " + cl.qualifiedName()
+ + " added final qualifier");
+ }
+ } else if (mIsFinal && !cl.mIsFinal) {
consistent = false;
- Errors.error(Errors.CHANGED_FINAL, cl.position(), "Class " + cl.qualifiedName()
- + " changed final qualifier");
+ Errors.error(Errors.REMOVED_FINAL, cl.position(), "Class " + cl.qualifiedName()
+ + " removed final qualifier");
}
if (mIsStatic != cl.mIsStatic) {
diff --git a/src/com/google/doclava/Errors.java b/src/com/google/doclava/Errors.java
index 2fc0f0c..e890aa4 100644
--- a/src/com/google/doclava/Errors.java
+++ b/src/com/google/doclava/Errors.java
@@ -134,7 +134,7 @@
public static Error REMOVED_FIELD = new Error(10, WARNING);
public static Error REMOVED_INTERFACE = new Error(11, WARNING);
public static Error CHANGED_STATIC = new Error(12, WARNING);
- public static Error CHANGED_FINAL = new Error(13, WARNING);
+ public static Error ADDED_FINAL = new Error(13, WARNING);
public static Error CHANGED_TRANSIENT = new Error(14, WARNING);
public static Error CHANGED_VOLATILE = new Error(15, WARNING);
public static Error CHANGED_TYPE = new Error(16, WARNING);
@@ -147,6 +147,8 @@
public static Error CHANGED_CLASS = new Error(23, WARNING);
public static Error CHANGED_DEPRECATED = new Error(24, WARNING);
public static Error CHANGED_SYNCHRONIZED = new Error(25, ERROR);
+ public static Error ADDED_FINAL_UNINSTANTIABLE = new Error(26, WARNING);
+ public static Error REMOVED_FINAL = new Error(27, WARNING);
// Errors in javadoc generation
public static final Error UNRESOLVED_LINK = new Error(101, WARNING);
@@ -175,10 +177,11 @@
UNAVAILABLE_SYMBOL, HIDDEN_SUPERCLASS, DEPRECATED, DEPRECATION_MISMATCH, MISSING_COMMENT,
IO_ERROR, NO_SINCE_DATA, NO_FEDERATION_DATA, PARSE_ERROR, ADDED_PACKAGE, ADDED_CLASS,
ADDED_METHOD, ADDED_FIELD, ADDED_INTERFACE, REMOVED_PACKAGE, REMOVED_CLASS,
- REMOVED_METHOD, REMOVED_FIELD, REMOVED_INTERFACE, CHANGED_STATIC, CHANGED_FINAL,
+ REMOVED_METHOD, REMOVED_FIELD, REMOVED_INTERFACE, CHANGED_STATIC, ADDED_FINAL,
CHANGED_TRANSIENT, CHANGED_VOLATILE, CHANGED_TYPE, CHANGED_VALUE, CHANGED_SUPERCLASS,
CHANGED_SCOPE, CHANGED_ABSTRACT, CHANGED_THROWS, CHANGED_NATIVE, CHANGED_CLASS,
- CHANGED_DEPRECATED, CHANGED_SYNCHRONIZED, BROKEN_SINCE_FILE, INVALID_CONTENT_TYPE};
+ CHANGED_DEPRECATED, CHANGED_SYNCHRONIZED, ADDED_FINAL_UNINSTANTIABLE, REMOVED_FINAL,
+ BROKEN_SINCE_FILE, INVALID_CONTENT_TYPE};
public static boolean setErrorLevel(int code, int level) {
for (Error e : ERRORS) {
diff --git a/src/com/google/doclava/FieldInfo.java b/src/com/google/doclava/FieldInfo.java
index 0200b95..09391a6 100644
--- a/src/com/google/doclava/FieldInfo.java
+++ b/src/com/google/doclava/FieldInfo.java
@@ -438,9 +438,13 @@
consistent = false;
}
- if (mIsFinal != fInfo.mIsFinal) {
- Errors.error(Errors.CHANGED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
- + " has changed 'final' qualifier");
+ if (!mIsFinal && fInfo.mIsFinal) {
+ Errors.error(Errors.ADDED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
+ + " has added 'final' qualifier");
+ consistent = false;
+ } else if (mIsFinal && !fInfo.mIsFinal) {
+ Errors.error(Errors.REMOVED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
+ + " has removed 'final' qualifier");
consistent = false;
}
diff --git a/src/com/google/doclava/MethodInfo.java b/src/com/google/doclava/MethodInfo.java
index db5e0cf..b6a5cc3 100644
--- a/src/com/google/doclava/MethodInfo.java
+++ b/src/com/google/doclava/MethodInfo.java
@@ -624,6 +624,10 @@
return mIsVarargs;
}
+ public boolean isFinalOrBelongsToFinalClass() {
+ return mIsFinal || (containingClass() != null && containingClass().isFinal());
+ }
+
@Override
public String toString() {
return this.name();
@@ -729,17 +733,19 @@
+ " has changed 'native' qualifier");
}
- if (mIsFinal != mInfo.mIsFinal) {
- // Compiler-generated methods vary in their 'final' qual between versions of
+ if (!mIsStatic) {
+ // Compiler-generated methods vary in their 'final' qualifier between versions of
// the compiler, so this check needs to be quite narrow. A change in 'final'
// status of a method is only relevant if (a) the method is not declared 'static'
- // and (b) the method's class is not itself 'final'.
- if (!mIsStatic) {
- if ((containingClass() == null) || (!containingClass().isFinal())) {
- consistent = false;
- Errors.error(Errors.CHANGED_FINAL, mInfo.position(), "Method " + mInfo.qualifiedName()
- + " has changed 'final' qualifier");
- }
+ // and (b) the method is not already inferred to be 'final' by virtue of its class.
+ if (!isFinalOrBelongsToFinalClass() && mInfo.isFinalOrBelongsToFinalClass()) {
+ consistent = false;
+ Errors.error(Errors.ADDED_FINAL, mInfo.position(), "Method " + mInfo.qualifiedName()
+ + " has added 'final' qualifier");
+ } else if (isFinalOrBelongsToFinalClass() && !mInfo.isFinalOrBelongsToFinalClass()) {
+ consistent = false;
+ Errors.error(Errors.REMOVED_FINAL, mInfo.position(), "Method " + mInfo.qualifiedName()
+ + " has removed 'final' qualifier");
}
}