diff options
-rw-r--r-- | build.gradle | 32 | ||||
-rw-r--r-- | src/com/google/doclava/ClassInfo.java | 22 | ||||
-rw-r--r-- | src/com/google/doclava/Errors.java | 3 | ||||
-rw-r--r-- | src/com/google/doclava/PackageInfo.java | 107 | ||||
-rw-r--r-- | src/com/google/doclava/Stubs.java | 4 | ||||
-rw-r--r-- | src/com/google/doclava/apicheck/ApiCheck.java | 47 | ||||
-rw-r--r-- | src/com/google/doclava/apicheck/ApiInfo.java | 47 |
7 files changed, 235 insertions, 27 deletions
diff --git a/build.gradle b/build.gradle index 79d856e..382e10d 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ sourceSets { String findToolsJar() { new ByteArrayOutputStream().withStream { os -> project.exec { - executable "$rootDir/build/core/find-jdk-tools-jar.sh" + executable "../../build/core/find-jdk-tools-jar.sh" standardOutput = os } @@ -22,9 +22,27 @@ String findToolsJar() { } } -dependencies { - compile files(findToolsJar()) - compile project(path: ':antlr', configuration: 'antlrRuntime') - compile project(':jsilver') - compile project(':tagsoup') -}
\ No newline at end of file + +if (project.hasProperty("usePrebuilts") && project.usePrebuilts == "true") { + repositories { + maven { url '../../prebuilts/tools/common/m2/repository' } + } + + dependencies { + compile files(findToolsJar()) + compile files('../../prebuilts/misc/common/antlr/antlr-3.4-complete.jar') + compile 'com.google.jsilver:jsilver:1.0.0' + // TODO add tagsoup to prebuils to fully support building using prebuilts + compile project(':tagsoup') + // required by jsilver + compile 'com.google.guava:guava:15.0' + //compile project(path: ':junit', configuration: 'target') + } +} else { + dependencies { + compile files(findToolsJar()) + compile project(path: ':antlr', configuration: 'antlrRuntime') + compile project(':jsilver') + compile project(':tagsoup') + } +} diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java index 616cf23..8f0199e 100644 --- a/src/com/google/doclava/ClassInfo.java +++ b/src/com/google/doclava/ClassInfo.java @@ -2045,7 +2045,12 @@ public class ClassInfo extends DocInfo implements ContainerInfo, Comparable, Sco } public boolean isConsistent(ClassInfo cl) { + return isConsistent(cl, null, null); + } + + public boolean isConsistent(ClassInfo cl, List<MethodInfo> newCtors, List<MethodInfo> newMethods) { boolean consistent = true; + boolean diffMode = (newCtors != null) && (newMethods != null); if (isInterface() != cl.isInterface()) { Errors.error(Errors.CHANGED_CLASS, cl.position(), "Class " + cl.qualifiedName() @@ -2092,15 +2097,24 @@ public class ClassInfo extends DocInfo implements ContainerInfo, Comparable, Sco /* * Similarly to the above, do not fail if this "new" method is really an override of an * existing superclass method. + * But we should fail if this is overriding an abstract method, because method's + * abstractness affects how users use it. See also Stubs.methodIsOverride(). */ MethodInfo mi = ClassInfo.overriddenMethod(mInfo, this); - if (mi == null) { + if (mi == null || + mi.isAbstract() != mInfo.isAbstract()) { Errors.error(Errors.ADDED_METHOD, mInfo.position(), "Added public method " + mInfo.prettyQualifiedSignature()); + if (diffMode) { + newMethods.add(mInfo); + } consistent = false; } } } + if (diffMode) { + Collections.sort(newMethods, MethodInfo.comparator); + } for (MethodInfo mInfo : mApiCheckConstructors.values()) { if (cl.mApiCheckConstructors.containsKey(mInfo.getHashableName())) { @@ -2117,9 +2131,15 @@ public class ClassInfo extends DocInfo implements ContainerInfo, Comparable, Sco if (!mApiCheckConstructors.containsKey(mInfo.getHashableName())) { Errors.error(Errors.ADDED_METHOD, mInfo.position(), "Added public constructor " + mInfo.prettyQualifiedSignature()); + if (diffMode) { + newCtors.add(mInfo); + } consistent = false; } } + if (diffMode) { + Collections.sort(newCtors, MethodInfo.comparator); + } for (FieldInfo mInfo : mApiCheckFields.values()) { if (cl.mApiCheckFields.containsKey(mInfo.name())) { diff --git a/src/com/google/doclava/Errors.java b/src/com/google/doclava/Errors.java index 84df16b..156de66 100644 --- a/src/com/google/doclava/Errors.java +++ b/src/com/google/doclava/Errors.java @@ -172,6 +172,7 @@ public class Errors { public static final Error INVALID_CONTENT_TYPE = new Error(119, ERROR); public static final Error INVALID_SAMPLE_INDEX = new Error(120, ERROR); public static final Error HIDDEN_TYPE_PARAMETER = new Error(121, HIDDEN); + public static final Error PRIVATE_SUPERCLASS = new Error(122, ERROR); public static final Error[] ERRORS = {UNRESOLVED_LINK, BAD_INCLUDE_TAG, UNKNOWN_TAG, UNKNOWN_PARAM_TAG_NAME, @@ -183,7 +184,7 @@ public class Errors { CHANGED_TRANSIENT, CHANGED_VOLATILE, CHANGED_TYPE, CHANGED_VALUE, CHANGED_SUPERCLASS, CHANGED_SCOPE, CHANGED_ABSTRACT, CHANGED_THROWS, CHANGED_NATIVE, CHANGED_CLASS, CHANGED_DEPRECATED, CHANGED_SYNCHRONIZED, ADDED_FINAL_UNINSTANTIABLE, REMOVED_FINAL, - BROKEN_SINCE_FILE, INVALID_CONTENT_TYPE, HIDDEN_TYPE_PARAMETER}; + BROKEN_SINCE_FILE, INVALID_CONTENT_TYPE, HIDDEN_TYPE_PARAMETER, PRIVATE_SUPERCLASS}; public static boolean setErrorLevel(int code, int level) { for (Error e : ERRORS) { diff --git a/src/com/google/doclava/PackageInfo.java b/src/com/google/doclava/PackageInfo.java index c0f10da..46b5b8f 100644 --- a/src/com/google/doclava/PackageInfo.java +++ b/src/com/google/doclava/PackageInfo.java @@ -18,8 +18,8 @@ package com.google.doclava; import com.google.doclava.apicheck.ApiInfo; import com.google.clearsilver.jsilver.data.Data; - import com.sun.javadoc.*; + import java.util.*; public class PackageInfo extends DocInfo implements ContainerInfo { @@ -394,9 +394,89 @@ public class PackageInfo extends DocInfo implements ContainerInfo { return mClasses; } - public boolean isConsistent(PackageInfo pInfo, Collection<String> ignoredClasses) { + public boolean isConsistent(PackageInfo pInfo) { + return isConsistent(pInfo, null); + } + + /** + * Creates the delta class by copying class signatures from original, but use provided list of + * constructors and methods. + */ + private ClassInfo createDeltaClass(ClassInfo original, + ArrayList<MethodInfo> constructors, ArrayList<MethodInfo> methods) { + ArrayList<FieldInfo> emptyFields = new ArrayList<>(); + ArrayList<ClassInfo> emptyClasses = new ArrayList<>(); + ArrayList<TypeInfo> emptyTypes = new ArrayList<>(); + ArrayList<MethodInfo> emptyMethods = new ArrayList<>(); + ClassInfo ret = new ClassInfo(null, original.getRawCommentText(), original.position(), + original.isPublic(), original.isProtected(), original.isPackagePrivate(), + original.isPrivate(), original.isStatic(), original.isInterface(), + original.isAbstract(), original.isOrdinaryClass(), + original.isException(), original.isError(), original.isEnum(), original.isAnnotation(), + original.isFinal(), original.isIncluded(), original.name(), original.qualifiedName(), + original.qualifiedTypeName(), original.isPrimitive()); + ArrayList<ClassInfo> interfaces = original.interfaces(); + // avoid providing null to init method, replace with empty array list when needed + if (interfaces == null) { + interfaces = emptyClasses; + } + ArrayList<TypeInfo> interfaceTypes = original.interfaceTypes(); + if (interfaceTypes == null) { + interfaceTypes = emptyTypes; + } + ArrayList<ClassInfo> innerClasses = original.innerClasses(); + if (innerClasses == null) { + innerClasses = emptyClasses; + } + ArrayList<MethodInfo> annotationElements = original.annotationElements(); + if (annotationElements == null) { + annotationElements = emptyMethods; + } + ArrayList<AnnotationInstanceInfo> annotations = original.annotations(); + if (annotations == null) { + annotations = new ArrayList<>(); + } + ret.init(original.type(), interfaces, interfaceTypes, innerClasses, + constructors, methods, annotationElements, + emptyFields /* fields */, emptyFields /* enum */, + original.containingPackage(), original.containingClass(), original.superclass(), + original.superclassType(), annotations); + return ret; + } + + /** + * Check if packages are consistent, also record class deltas. + * <p> + * <ul>class deltas are: + * <li>brand new classes that are not present in current package + * <li>stripped existing classes stripped where only newly added methods are kept + * @param pInfo + * @param clsInfoDiff + * @return + */ + public boolean isConsistent(PackageInfo pInfo, List<ClassInfo> clsInfoDiff) { + return isConsistent(pInfo, clsInfoDiff, null); + } + + /** + * Check if packages are consistent, also record class deltas. + * <p> + * <ul>class deltas are: + * <li>brand new classes that are not present in current package + * <li>stripped existing classes stripped where only newly added methods are kept + * @param pInfo + * @param clsInfoDiff + * @param ignoredClasses + * @return + */ + public boolean isConsistent(PackageInfo pInfo, List<ClassInfo> clsInfoDiff, + Collection<String> ignoredClasses) { boolean consistent = true; + boolean diffMode = clsInfoDiff != null; for (ClassInfo cInfo : mClasses.values()) { + ArrayList<MethodInfo> newClsApis = null; + ArrayList<MethodInfo> newClsCtors = null; + // TODO: Add support for matching inner classes (e.g, something like // example.Type.* should match example.Type.InnerType) if (ignoredClasses != null && ignoredClasses.contains(cInfo.qualifiedName())) { @@ -404,9 +484,19 @@ public class PackageInfo extends DocInfo implements ContainerInfo { continue; } if (pInfo.mClasses.containsKey(cInfo.name())) { - if (!cInfo.isConsistent(pInfo.mClasses.get(cInfo.name()))) { + if (diffMode) { + newClsApis = new ArrayList<>(); + newClsCtors = new ArrayList<>(); + } + if (!cInfo.isConsistent(pInfo.mClasses.get(cInfo.name()), newClsCtors, newClsApis)) { consistent = false; } + // if we are in diff mode, add class to list if there's new ctor or new apis + if (diffMode && !(newClsCtors.isEmpty() && newClsApis.isEmpty())) { + // generate a "delta" class with only added methods and constructors, but no fields etc + ClassInfo deltaClsInfo = createDeltaClass(cInfo, newClsCtors, newClsApis); + clsInfoDiff.add(deltaClsInfo); + } } else { Errors.error(Errors.REMOVED_CLASS, cInfo.position(), "Removed public class " + cInfo.qualifiedName()); @@ -422,12 +512,15 @@ public class PackageInfo extends DocInfo implements ContainerInfo { Errors.error(Errors.ADDED_CLASS, cInfo.position(), "Added class " + cInfo.name() + " to package " + pInfo.name()); consistent = false; + // brand new class, add everything as is + if (diffMode) { + clsInfoDiff.add(cInfo); + } } } + if (diffMode) { + Collections.sort(clsInfoDiff, ClassInfo.comparator); + } return consistent; } - - public boolean isConsistent(PackageInfo pInfo) { - return isConsistent(pInfo, null); - } } diff --git a/src/com/google/doclava/Stubs.java b/src/com/google/doclava/Stubs.java index 647c921..5ae1dd0 100644 --- a/src/com/google/doclava/Stubs.java +++ b/src/com/google/doclava/Stubs.java @@ -337,6 +337,10 @@ public class Stubs { + " stripped of unavailable superclass " + supr.qualifiedName()); } else { cantStripThis(supr, notStrippable, "6:" + cl.realSuperclass().name() + cl.qualifiedName()); + if (supr.isPrivate()) { + Errors.error(Errors.PRIVATE_SUPERCLASS, cl.position(), "Public class " + + cl.qualifiedName() + " extends private class " + supr.qualifiedName()); + } } } } diff --git a/src/com/google/doclava/apicheck/ApiCheck.java b/src/com/google/doclava/apicheck/ApiCheck.java index 9698c89..47a8b5e 100644 --- a/src/com/google/doclava/apicheck/ApiCheck.java +++ b/src/com/google/doclava/apicheck/ApiCheck.java @@ -23,11 +23,13 @@ import java.io.InputStream; import java.io.PrintStream; import java.net.URL; import java.util.ArrayList; +import java.util.List; import java.util.HashSet; import java.util.Set; import java.util.Stack; import com.google.doclava.Errors; +import com.google.doclava.PackageInfo; import com.google.doclava.Errors.ErrorMessage; import com.google.doclava.Stubs; @@ -64,6 +66,10 @@ public class ApiCheck { System.exit(convertToApi(originalArgs[1], originalArgs[2])); } else if (originalArgs.length == 3 && "-convert2xml".equals(originalArgs[0])) { System.exit(convertToXml(originalArgs[1], originalArgs[2])); + } else if (originalArgs.length == 4 && "-new_api".equals(originalArgs[0])) { + // command syntax: -new_api oldapi.txt newapi.txt diff.xml + // TODO: Support reading in other options for new_api, such as ignored classes/packages. + System.exit(newApi(originalArgs[1], originalArgs[2], originalArgs[3])); } else { ApiCheck acheck = new ApiCheck(); Report report = acheck.checkApi(originalArgs); @@ -138,11 +144,11 @@ public class ApiCheck { // only run the consistency check if we haven't had XML parse errors if (!Errors.hadError) { - oldApi.isConsistent(newApi, ignoredPackages, ignoredClasses); + oldApi.isConsistent(newApi, null, ignoredPackages, ignoredClasses); } if (!Errors.hadError) { - oldRemovedApi.isConsistent(newRemovedApi, ignoredPackages, ignoredClasses); + oldRemovedApi.isConsistent(newRemovedApi, null, ignoredPackages, ignoredClasses); } return new Report(Errors.hadError ? 1 : 0, Errors.getErrors()); @@ -287,4 +293,41 @@ public class ApiCheck { return 0; } + /** + * Generates a "diff": where new API is trimmed down by removing existing methods found in old API + * @param origApiPath path to old API text file + * @param newApiPath path to new API text file + * @param outputPath output XML path for the generated diff + * @return + */ + static int newApi(String origApiPath, String newApiPath, String outputPath) { + ApiInfo origApi, newApi; + try { + origApi = parseApi(origApiPath); + } catch (ApiParseException e) { + e.printStackTrace(); + System.err.println("Error parsing API: " + origApiPath); + return 1; + } + try { + newApi = parseApi(newApiPath); + } catch (ApiParseException e) { + e.printStackTrace(); + System.err.println("Error parsing API: " + newApiPath); + return 1; + } + List<PackageInfo> pkgInfoDiff = new ArrayList<>(); + if (!origApi.isConsistent(newApi, pkgInfoDiff)) { + PrintStream apiWriter = null; + try { + apiWriter = new PrintStream(outputPath); + } catch (FileNotFoundException ex) { + System.err.println("can't open file: " + outputPath); + } + Stubs.writeXml(apiWriter, pkgInfoDiff); + } else { + System.err.println("No API change detected, not generating diff."); + } + return 0; + } } diff --git a/src/com/google/doclava/apicheck/ApiInfo.java b/src/com/google/doclava/apicheck/ApiInfo.java index 2752f3a..fa51e8b 100644 --- a/src/com/google/doclava/apicheck/ApiInfo.java +++ b/src/com/google/doclava/apicheck/ApiInfo.java @@ -19,9 +19,12 @@ package com.google.doclava.apicheck; import com.google.doclava.ClassInfo; import com.google.doclava.Errors; import com.google.doclava.PackageInfo; + import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public class ApiInfo { @@ -59,16 +62,31 @@ public class ApiInfo { /** * Checks to see if this api is consistent with a newer version. + */ + public boolean isConsistent(ApiInfo otherApi) { + return isConsistent(otherApi, null); + } + + public boolean isConsistent(ApiInfo otherApi, List<PackageInfo> pkgInfoDiff) { + return isConsistent(otherApi, pkgInfoDiff, null, null); + } + + /** + * Checks to see if this api is consistent with a newer version. * * @param otherApi the other api to test consistency against + * @param pkgInfoDiff * @param ignoredPackages packages to skip consistency checks (will match by exact name) * @param ignoredClasses classes to skip consistency checks (will match by exact fully qualified * name) */ - public boolean isConsistent(ApiInfo otherApi, + public boolean isConsistent(ApiInfo otherApi, List<PackageInfo> pkgInfoDiff, Collection<String> ignoredPackages, Collection<String> ignoredClasses) { boolean consistent = true; + boolean diffMode = pkgInfoDiff != null; for (PackageInfo pInfo : mPackages.values()) { + List<ClassInfo> newClsApis = null; + // TODO: Add support for matching subpackages (e.g, something like // test.example.* should match test.example.subpackage, and // test.example.** should match the above AND test.example.subpackage.more) @@ -77,9 +95,21 @@ public class ApiInfo { continue; } if (otherApi.getPackages().containsKey(pInfo.name())) { - if (!pInfo.isConsistent(otherApi.getPackages().get(pInfo.name()), ignoredClasses)) { + if (diffMode) { + newClsApis = new ArrayList<>(); + } + if (!pInfo.isConsistent(otherApi.getPackages().get(pInfo.name()), newClsApis, ignoredClasses)) { consistent = false; } + if (diffMode && !newClsApis.isEmpty()) { + PackageInfo info = new PackageInfo(pInfo.name(), pInfo.position()); + for (ClassInfo cInfo : newClsApis) { + if (ignoredClasses == null || !ignoredClasses.contains(cInfo.qualifiedName())) { + info.addClass(cInfo); + } + } + pkgInfoDiff.add(info); + } } else { Errors.error(Errors.REMOVED_PACKAGE, pInfo.position(), "Removed package " + pInfo.name()); consistent = false; @@ -93,18 +123,17 @@ public class ApiInfo { if (!mPackages.containsKey(pInfo.name())) { Errors.error(Errors.ADDED_PACKAGE, pInfo.position(), "Added package " + pInfo.name()); consistent = false; + if (diffMode) { + pkgInfoDiff.add(pInfo); + } } } + if (diffMode) { + Collections.sort(pkgInfoDiff, PackageInfo.comparator); + } return consistent; } - /** - * Checks to see if this api is consistent with a newer version. - */ - public boolean isConsistent(ApiInfo otherApi) { - return isConsistent(otherApi, null, null); - } - public HashMap<String, PackageInfo> getPackages() { return mPackages; } |