Merge "Generate android_icu4j with a fixed set of public APIs"
diff --git a/tools/srcgen/currysrc/src/main/java/com/google/currysrc/api/process/ast/BodyDeclarationLocators.java b/tools/srcgen/currysrc/src/main/java/com/google/currysrc/api/process/ast/BodyDeclarationLocators.java
index a012482..2d233f2 100644
--- a/tools/srcgen/currysrc/src/main/java/com/google/currysrc/api/process/ast/BodyDeclarationLocators.java
+++ b/tools/srcgen/currysrc/src/main/java/com/google/currysrc/api/process/ast/BodyDeclarationLocators.java
@@ -166,9 +166,35 @@
     if (parametersString.isEmpty()) {
       return Collections.emptyList();
     }
-    return Splitter.on(',').splitToList(parametersString);
+    return splitParameters(parametersString);
   }
 
+  /**
+   * Split parameters by ','. Support simple generics syntax.
+   * TODO: Replace the implementation by JDT parser.
+   */
+  private static List<String> splitParameters(String parametersString) {
+    List<String> result = new ArrayList<>();
+    int genericsLevel = 0;
+    int start = 0;
+    for (int p = 0; p < parametersString.length(); p++) {
+      char c = parametersString.charAt(p);
+      if (genericsLevel == 0 && c == ',') {
+        result.add(parametersString.substring(start, p));
+        start = p + 1;
+      } else if (c == '<') {
+        genericsLevel++;
+      } else if (c == '>') {
+        genericsLevel--;
+      }
+    }
+    if (start < parametersString.length()) {
+      result.add(parametersString.substring(start, parametersString.length()));
+    }
+    return result;
+  }
+
+
   private static List<String> splitInTwo(String string, String separator) {
     List<String> components = Splitter.on(separator).splitToList(string);
     if (components.size() != 2) {
diff --git a/tools/srcgen/generate_android_icu4j.sh b/tools/srcgen/generate_android_icu4j.sh
index 416ab76..d50ec94 100755
--- a/tools/srcgen/generate_android_icu4j.sh
+++ b/tools/srcgen/generate_android_icu4j.sh
@@ -43,6 +43,7 @@
     mkdir -p ${ANDROID_ICU4J_DIR}
 fi
 
+WHITELIST_API_FILE=${ICU_SRCGEN_DIR}/whitelisted-public-api.txt
 CORE_PLATFORM_API_FILE=${ICU_SRCGEN_DIR}/core-platform-api.txt
 
 # Clean out previous generated code / resources.
@@ -55,7 +56,14 @@
 mkdir -p ${DEST_RESOURCE_DIR}
 
 # Generate the source code needed by Android.
-${SRCGEN_TOOL_BINARY} Icu4jTransform ${INPUT_DIRS} ${DEST_SRC_DIR} ${CORE_PLATFORM_API_FILE}
+# Branches used for testing new versions of ICU will have have the ${WHITELIST_API_FILE} file
+# that prevents new (stable) APIs being added to the Android public SDK API. The file should
+# not exist on "normal" release branches and master.
+ICU4J_BASE_COMMAND="${SRCGEN_TOOL_BINARY} Icu4jTransform"
+if [ -e "${WHITELIST_API_FILE}" ]; then
+  ICU4J_BASE_COMMAND+=" --hide-non-whitelisted-api ${WHITELIST_API_FILE}"
+fi
+${ICU4J_BASE_COMMAND} ${INPUT_DIRS} ${DEST_SRC_DIR} ${CORE_PLATFORM_API_FILE}
 
 # Copy / transform the resources needed by the android_icu4j code.
 for INPUT_DIR in ${INPUT_DIRS}; do
diff --git a/tools/srcgen/generate_whitelisted_public_api.sh b/tools/srcgen/generate_whitelisted_public_api.sh
new file mode 100755
index 0000000..8350749
--- /dev/null
+++ b/tools/srcgen/generate_whitelisted_public_api.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+source $(dirname $BASH_SOURCE)/common.sh
+
+SRCGEN_TOOL_BINARY=${ANDROID_HOST_OUT}/bin/android_icu4j_srcgen_binary
+WHITELIST_API_FILE=${ICU_SRCGEN_DIR}/whitelisted-public-api.txt
+
+${SRCGEN_TOOL_BINARY} GeneratePublicApiReport ${ANDROID_ICU4J_DIR}/src/main/java ${WHITELIST_API_FILE}
+
+TEMP_CONTENT=$(cat ${WHITELIST_API_FILE})
+
+# Prepend the license and README in the header
+cat > ${WHITELIST_API_FILE} <<'_EOF'
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+_EOF
+
+echo "$TEMP_CONTENT" >> ${WHITELIST_API_FILE}
diff --git a/tools/srcgen/src/main/java/com/android/icu4j/srcgen/HideNonWhitelistedDeclarations.java b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/HideNonWhitelistedDeclarations.java
new file mode 100644
index 0000000..ca3c14f
--- /dev/null
+++ b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/HideNonWhitelistedDeclarations.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.icu4j.srcgen;
+
+import com.android.icu4j.srcgen.checker.RecordPublicApiRules;
+import com.google.common.collect.Lists;
+import com.google.currysrc.api.process.Context;
+import com.google.currysrc.api.process.JavadocUtils;
+import com.google.currysrc.api.process.Processor;
+import com.google.currysrc.api.process.ast.BodyDeclarationLocator;
+import com.google.currysrc.api.process.ast.BodyDeclarationLocators;
+import com.google.currysrc.api.process.ast.StartPositionComparator;
+
+import org.eclipse.jdt.core.dom.ASTVisitor;
+import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
+import org.eclipse.jdt.core.dom.BodyDeclaration;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
+import org.eclipse.jdt.core.dom.EnumDeclaration;
+import org.eclipse.jdt.core.dom.FieldDeclaration;
+import org.eclipse.jdt.core.dom.MethodDeclaration;
+import org.eclipse.jdt.core.dom.TypeDeclaration;
+import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Adds a @hide javadoc tag to {@link BodyDeclaration}s that are not whitelisted.
+ */
+public class HideNonWhitelistedDeclarations implements Processor {
+  private final List<BodyDeclarationLocator> whitelist;
+  private final String tagComment;
+
+  public HideNonWhitelistedDeclarations(List<BodyDeclarationLocator> whitelist, String tagComment) {
+    this.whitelist = whitelist;
+    this.tagComment = tagComment;
+  }
+
+  @Override
+  public void process(Context context, CompilationUnit cu) {
+    // Ignore this process if the whitelist is not provided
+    if (whitelist == null) {
+      return;
+    }
+    List<BodyDeclaration> matchingNodes = Lists.newArrayList();
+    cu.accept(new ASTVisitor() {
+      @Override public boolean visit(EnumConstantDeclaration node) {
+        return handleMemberDeclarationNode(node);
+      }
+
+      @Override public boolean visit(EnumDeclaration node) {
+        return handleTypeDeclarationNode(node);
+      }
+
+      @Override public boolean visit(FieldDeclaration node) {
+        return handleMemberDeclarationNode(node);
+      }
+
+      @Override public boolean visit(MethodDeclaration node) {
+        return handleMemberDeclarationNode(node);
+      }
+
+      @Override public boolean visit(TypeDeclaration node) {
+        return handleTypeDeclarationNode(node);
+      }
+
+      private boolean handleTypeDeclarationNode(AbstractTypeDeclaration node) {
+        matchIfNotWhitelistedAndNotHidden(node);
+        // Continue processing for nested types / methods.
+        return true;
+      }
+
+      private boolean handleMemberDeclarationNode(BodyDeclaration node) {
+        matchIfNotWhitelistedAndNotHidden(node);
+        // Leaf declaration (i.e. a method, fields, enum constant).
+        return false;
+      }
+
+      private void matchIfNotWhitelistedAndNotHidden(final BodyDeclaration node) {
+        if (node == null) {
+          return;
+        }
+
+        if (!RecordPublicApiRules.isPublicApiEligible(node)) {
+          return;
+        }
+
+        if (BodyDeclarationLocators.matchesAny(whitelist, node)) {
+          return;
+        }
+        matchingNodes.add(node);
+      }
+    });
+
+    // Tackle nodes in reverse order to avoid messing up the ASTNode offsets.
+    Collections.sort(matchingNodes, new StartPositionComparator());
+    ASTRewrite rewrite = context.rewrite();
+    for (BodyDeclaration bodyDeclaration : Lists.reverse(matchingNodes)) {
+      JavadocUtils.addJavadocTag(rewrite, bodyDeclaration, tagComment);
+    }
+  }
+
+  @Override public String toString() {
+    return "HideNonWhitelistedDeclarations{" +
+        "whitelist=" + whitelist +
+        "tagComment=" + tagComment +
+        '}';
+  }
+}
diff --git a/tools/srcgen/src/main/java/com/android/icu4j/srcgen/Icu4jTransform.java b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/Icu4jTransform.java
index 44b7053..9b2f805 100644
--- a/tools/srcgen/src/main/java/com/android/icu4j/srcgen/Icu4jTransform.java
+++ b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/Icu4jTransform.java
@@ -629,8 +629,24 @@
   }
 
   /**
-   * Usage:
-   * java com.android.icu4j.srcgen.Icu4JTransform {source files/directories} {target dir}
+   * Usage: See {@link Icu4jRules#COMMAND_USAGE}
+   *
+   * The option --hide-non-whitelisted-api can be used to explicitly describe the API surface to be
+   * exposed; anything not in the list will be hidden in additional to other rules. This is useful
+   * when upgrading ICU when we haven't yet added new classes/methods to various hard-coded lists
+   * described below.
+   *
+   * This tool hides the following in the knowledge that classes that are not explicitly
+   * hidden are exposed in the public API set:
+   *
+   * 1) Public classes that are not in the PUBLIC_API_CLASSES list.
+   * 2) Types / fields / methods that were deprecated when the class was first exposed in Android,
+   *    listed in INITIAL_DEPRECATED_SET
+   * 3) Types / fields / methods that we explicitly want to hide, listed in DECLARATIONS_TO_HIDE
+   * 4) Types / fields / methods that are flagged with ICU javadoc as draft / provisional or
+   *    internal.
+   * 5) If the --hide-non-whitelisted-api option is provided, types / fields / methods that are not
+   *    in the whitelisted-api-file.
    */
   public static void main(String[] args) throws Exception {
     Map<String, String> options = JavaCore.getOptions();
@@ -648,6 +664,9 @@
   static class Icu4jRules implements RuleSet {
 
     private static final String SOURCE_CODE_HEADER = "/* GENERATED SOURCE. DO NOT MODIFY. */\n";
+    private static final String COMMAND_USAGE = "Usage: " + Icu4jTransform.class.getCanonicalName()
+            + " [--hide-non-whitelisted-api <whitelisted-api-file>]"
+            + " <source-dir>+ <target-dir> <core-platform-api-file>";
 
     private final InputFileGenerator inputFileGenerator;
     private final List<Rule> rules;
@@ -655,16 +674,24 @@
 
     public Icu4jRules(String[] args) throws IOException {
       if (args.length < 3) {
-        throw new IllegalArgumentException(
-                "Usage: " + Icu4jTransform.class.getCanonicalName()
-                        + " <source-dir>+ <target-dir> <core-platform-api-file>");
+        throw new IllegalArgumentException(COMMAND_USAGE);
+      }
+      Path whitelistedApiPath = null;
+      if ("--hide-non-whitelisted-api".equals(args[0])) {
+        whitelistedApiPath = Paths.get(args[1]);
+        if (args.length < 5) {
+          throw new IllegalArgumentException(COMMAND_USAGE);
+        }
+        String[] newArgs = new String[args.length - 2];
+        System.arraycopy(args, 2, newArgs, 0, args.length - 2);
+        args = newArgs;
       }
 
       String[] inputDirNames = new String[args.length - 2];
       System.arraycopy(args, 0, inputDirNames, 0, args.length - 2);
       inputFileGenerator = Icu4jTransformRules.createInputFileGenerator(inputDirNames);
       Path corePlatformApiFile = Paths.get(args[args.length - 1]);
-      rules = createTransformRules(corePlatformApiFile);
+      rules = createTransformRules(corePlatformApiFile, whitelistedApiPath);
       outputSourceFileGenerator =
           Icu4jTransformRules.createOutputFileGenerator(args[args.length - 2]);
     }
@@ -701,7 +728,8 @@
       };
     }
 
-    private static List<Rule> createTransformRules(Path corePlatformApiFile) throws IOException {
+    private static List<Rule> createTransformRules(Path corePlatformApiFile,
+            Path whitelistedApiPath) throws IOException {
       // The rules needed to repackage source code that declares or references com.ibm.icu code
       // so it references android.icu instead.
       Rule[] repackageRules = getRepackagingRules();
@@ -716,20 +744,25 @@
           createOptionalRule(
               new ReplaceTextCommentScanner(ORIGINAL_ICU_PACKAGE, ANDROID_ICU_PACKAGE)),
 
-          // AST change: Hide all ICU public classes except those in the whitelist.
+          // AST change: Hide all ICU public classes except those in the PUBLIC_API_CLASSES
+          // whitelist.
           createHidePublicClassesRule(),
 
-          // AST change: Hide ICU methods that are deprecated and Android does not want to make
-          // public.
+          // AST change: Hide ICU methods that are in INITIAL_DEPRECATED_SET and Android does not
+          // want to make public.
           createHideOriginalDeprecatedClassesRule(),
-          // AST change: Explicitly hide blacklisted methods such as those that get/set static
-          // default values that might lead to confusion or strange interactions between Android's
-          // ICU4J and java.text / java.util classes.
+          // AST change: Explicitly hide blacklisted methods in DECLARATIONS_TO_HIDE such as those
+          // that get/set static default values that might lead to confusion or strange interactions
+          // between Android's ICU4J and java.text / java.util classes.
           createHideBlacklistedDeclarationsRule(),
           // AST change: Explicitly hide any elements that are marked as
           // @draft / @provisional / @internal
           createOptionalRule(new HideDraftProvisionalInternal()),
 
+          // AST change: Hide new non-whitelisted API in Android temporarily
+          // Usually used for avoiding the new API introduced by upstream to show up in Android.
+          createHideNonWhitelistedRule(whitelistedApiPath),
+
           // AST change: Remove JavaDoc tags that Android has no need of:
           // @hide has been added in place of @draft, @provisional and @internal
           // @stable <ICU version> will not mean much on Android.
@@ -785,4 +818,14 @@
               "Only a subset of ICU is exposed in Android"));
     }
   }
+
+  private static Rule createHideNonWhitelistedRule(Path whitelistedApiPath) {
+    List<BodyDeclarationLocator> bodyDeclarationLocators = null;
+    if (whitelistedApiPath != null) {
+      bodyDeclarationLocators = BodyDeclarationLocators.readBodyDeclarationLocators(
+              whitelistedApiPath);
+    }
+    return createOptionalRule(new HideNonWhitelistedDeclarations(bodyDeclarationLocators,
+            "@hide Hide new API in Android temporarily"));
+  }
 }
diff --git a/tools/srcgen/src/main/java/com/android/icu4j/srcgen/Main.java b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/Main.java
index 67b3d60..5e13381 100644
--- a/tools/srcgen/src/main/java/com/android/icu4j/srcgen/Main.java
+++ b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/Main.java
@@ -15,6 +15,8 @@
  */
 package com.android.icu4j.srcgen;
 
+import com.android.icu4j.srcgen.checker.GeneratePublicApiReport;
+
 public class Main {
   public static void main(String[] args) throws Exception {
     if (args.length < 1) {
@@ -33,6 +35,9 @@
       case "Icu4jBasicTransform":
         Icu4jBasicTransform.main(inputArgs);
         break;
+      case "GeneratePublicApiReport":
+        GeneratePublicApiReport.main(inputArgs);
+        break;
       default:
         throw new IllegalArgumentException("Input class name is not valid: " + className);
     }
diff --git a/tools/srcgen/src/main/java/com/android/icu4j/srcgen/checker/GeneratePublicApiReport.java b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/checker/GeneratePublicApiReport.java
new file mode 100644
index 0000000..4cb8d2f
--- /dev/null
+++ b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/checker/GeneratePublicApiReport.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.icu4j.srcgen.checker;
+
+import com.android.icu4j.srcgen.Icu4jTransformRules;
+import com.google.currysrc.Main;
+import com.google.currysrc.api.input.InputFileGenerator;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.util.List;
+
+public class GeneratePublicApiReport {
+  private static final boolean DEBUG = false;
+
+  private GeneratePublicApiReport() {
+  }
+
+  /**
+   * Usage:
+   * java com.android.icu4j.srcgen.checker.GeneratePublicApiReport
+   *   {android_icu4j src main directories} {report output file path}
+   */
+  public static void main(String[] args) throws Exception {
+    if (args.length < 2) {
+      throw new IllegalArgumentException("At least 2 argument required.");
+    }
+
+    Main main = new Main(DEBUG);
+
+    // We assume we only need to look at ICU4J code for this for both passes.
+    String[] inputDirs = new String[args.length - 1];
+    System.arraycopy(args, 0, inputDirs, 0, inputDirs.length);
+    InputFileGenerator inputFileGenerator = Icu4jTransformRules.createInputFileGenerator(
+            inputDirs);
+
+    System.out.println("Establishing Android public ICU4J API");
+    RecordPublicApiRules recordPublicApiRulesRules = new RecordPublicApiRules(
+            inputFileGenerator);
+    main.execute(recordPublicApiRulesRules);
+    List<String> publicMemberLocatorStrings = recordPublicApiRulesRules.publicMembers();
+    File outputReportFile = new File(args[args.length - 1]);
+    try (PrintStream report = new PrintStream(outputReportFile)) {
+      for (String publicMemberLocatorString : publicMemberLocatorStrings) {
+        report.println(publicMemberLocatorString);
+      }
+    }
+
+    System.out.println("Report file: " + outputReportFile);
+  }
+}
diff --git a/tools/srcgen/src/main/java/com/android/icu4j/srcgen/checker/RecordPublicApiRules.java b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/checker/RecordPublicApiRules.java
index 42c34f6..6d961bc 100644
--- a/tools/srcgen/src/main/java/com/android/icu4j/srcgen/checker/RecordPublicApiRules.java
+++ b/tools/srcgen/src/main/java/com/android/icu4j/srcgen/checker/RecordPublicApiRules.java
@@ -52,7 +52,7 @@
  * Rules that operate over a set of files and record the public API (according to Android's rules
  * for @hide).
  */
-class RecordPublicApiRules implements RuleSet {
+public class RecordPublicApiRules implements RuleSet {
 
   private final InputFileGenerator inputFileGenerator;
 
@@ -132,79 +132,91 @@
     }
 
     private void handleDeclarationNode(BodyDeclaration node) {
-      if (isExplicitlyHidden(node)) {
+      if (!isPublicApiEligible(node)) {
         return;
       }
-
-      AbstractTypeDeclaration typeDeclaration = TypeLocator.findTypeDeclarationNode(node);
-      if (typeDeclaration == null) {
-        // Not unusual: methods / fields defined on anonymous types are like this. The parent
-        // is a constructor expression, not a declaration.
-        return;
-      }
-
-      boolean isNonTypeDeclaration = typeDeclaration != node;
-      if (isNonTypeDeclaration) {
-        if (isExplicitlyHidden(node) || !isMemberPublicApiEligible(typeDeclaration, node)) {
-          return;
-        }
-      }
-      while (typeDeclaration != null) {
-        if (isExplicitlyHidden(typeDeclaration) || !isTypePublicApiEligible(typeDeclaration)) {
-          return;
-        }
-        typeDeclaration = TypeLocator.findEnclosingTypeDeclaration(typeDeclaration);
-      }
       // The node is appropriately public and is not hidden.
       publicMembers.addAll(BodyDeclarationLocators.toLocatorStringForms(node));
     }
 
-    private boolean isTypePublicApiEligible(AbstractTypeDeclaration typeDeclaration) {
-      int typeModifiers = typeDeclaration.getModifiers();
-      return ((typeModifiers & Modifier.PUBLIC) != 0);
-    }
-
-    public boolean isMemberPublicApiEligible(AbstractTypeDeclaration type, BodyDeclaration node) {
-      if (node.getNodeType() == ASTNode.ENUM_DECLARATION
-          || node.getNodeType() == ASTNode.TYPE_DECLARATION) {
-        throw new AssertionError("Unsupported node type: " + node);
-      }
-
-      if (type instanceof TypeDeclaration) {
-        TypeDeclaration typeDeclaration = (TypeDeclaration) type;
-        // All methods are public on interfaces. Not sure if true on Java 8.
-        if (typeDeclaration.isInterface()) {
-          return true;
-        }
-      }
-      int memberModifiers = node.getModifiers();
-      return ((memberModifiers & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0);
-    }
-
-    private boolean isExplicitlyHidden(BodyDeclaration node) {
-      Javadoc javadoc = node.getJavadoc();
-      if (javadoc == null) {
-        return false;
-      }
-      final Boolean[] isHidden = new Boolean[] { false };
-      javadoc.accept(new ASTVisitor(true /* visitDocNodes */) {
-        @Override public boolean visit(TagElement node) {
-          String tagName = node.getTagName();
-          if (tagName == null) {
-            return true;
-          }
-          if (tagName.equals("@hide")) {
-            isHidden[0] = true;
-            return false;
-          }
-          return true;
-        }
-      });
-      return isHidden[0];
-    }
-
     public List<String> publicMembers() {
       return publicMembers;
     }
   }
+
+  /**
+   * Determine if a declaration can be public API by looking at its and ancestor type's modifier and
+   * {@code @hide} javadoc tag.
+   */
+  public static boolean isPublicApiEligible(BodyDeclaration node) {
+    if (isExplicitlyHidden(node)) {
+      return false;
+    }
+
+    AbstractTypeDeclaration typeDeclaration = TypeLocator.findTypeDeclarationNode(node);
+    if (typeDeclaration == null) {
+      // Not unusual: methods / fields defined on anonymous types are like this. The parent
+      // is a constructor expression, not a declaration.
+      return false;
+    }
+
+    boolean isNonTypeDeclaration = typeDeclaration != node;
+    if (isNonTypeDeclaration) {
+      if (isExplicitlyHidden(node) || !isMemberPublicApiEligible(typeDeclaration, node)) {
+        return false;
+      }
+    }
+    while (typeDeclaration != null) {
+      if (isExplicitlyHidden(typeDeclaration) ||
+              !RecordPublicApiRules.isTypePublicApiEligible(typeDeclaration)) {
+        return false;
+      }
+      typeDeclaration = TypeLocator.findEnclosingTypeDeclaration(typeDeclaration);
+    }
+    return true;
+  }
+
+  private static boolean isExplicitlyHidden(BodyDeclaration node) {
+    // Don't visit AST or use ASTVisitor here as the caller is visiting the node.
+    Javadoc javadoc = node.getJavadoc();
+    if (javadoc == null) {
+      return false;
+    }
+    for (TagElement tagElement : (List<TagElement>) javadoc.tags()) {
+      String tagName = tagElement.getTagName();
+      if (tagName == null) {
+        continue;
+      }
+      if ("@hide".equals(tagName.toLowerCase())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean isTypePublicApiEligible(AbstractTypeDeclaration typeDeclaration) {
+    int typeModifiers = typeDeclaration.getModifiers();
+    return ((typeModifiers & Modifier.PUBLIC) != 0);
+  }
+
+  private static boolean isMemberPublicApiEligible(AbstractTypeDeclaration type, BodyDeclaration node) {
+    if (node.getNodeType() == ASTNode.ENUM_DECLARATION
+            || node.getNodeType() == ASTNode.TYPE_DECLARATION) {
+      throw new AssertionError("Unsupported node type: " + node);
+    }
+
+    if (type instanceof TypeDeclaration) {
+      TypeDeclaration typeDeclaration = (TypeDeclaration) type;
+      // All methods are public on interfaces. Not sure if true on Java 8.
+      if (typeDeclaration.isInterface()) {
+        return true;
+      }
+    }
+    // Enum constant doesn't have visiblity
+    if (node.getNodeType() == ASTNode.ENUM_CONSTANT_DECLARATION) {
+      return true;
+    }
+    int memberModifiers = node.getModifiers();
+    return ((memberModifiers & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0);
+  }
 }