Repository: google/closure-compiler
Pull request #884 (2015-03-19 20:16:52)

Deprecate the @expose annotation
With `@nocollapse` now supported, `@expose` can be deprecated. The compiler will only warn on the first occurrence of `@expose`.

I currently have this as part of the `nonStandardJsDocs` diagnostic group. I'm not completely convinced that's the correct place for it.

dimvar: Let's give it until next week to make sure the nocollapse change sticks before we merge this one. I can do the review.
ChadKillingsworth: Refactored and squashed commits. Warning is now a parse warning in IRFactory for every occurrence.

Patch:
diff --git a/src/com/google/javascript/jscomp/ClosureRewriteClass.java b/src/com/google/javascript/jscomp/ClosureRewriteClass.java
index b0d69e14d..46fe230e9 100644
--- a/src/com/google/javascript/jscomp/ClosureRewriteClass.java
+++ b/src/com/google/javascript/jscomp/ClosureRewriteClass.java
@@ -475,11 +475,11 @@ class ClosureRewriteClass extends AbstractPostOrderCallback
     // avoid null checks
     JSDocInfo classInfo = (cls.classInfo != null)
         ? cls.classInfo
-        : new JSDocInfoBuilder(true).build(true);
+        : new JSDocInfo(true);
 
     JSDocInfo ctorInfo = (cls.constructor.info != null)
         ? cls.constructor.info
-        : new JSDocInfoBuilder(true).build(true);
+        : new JSDocInfo(true);
 
     Node superNode = cls.superClass;
 
diff --git a/src/com/google/javascript/jscomp/CollapseVariableDeclarations.java b/src/com/google/javascript/jscomp/CollapseVariableDeclarations.java
index 6b39b4259..2c800e2ef 100644
--- a/src/com/google/javascript/jscomp/CollapseVariableDeclarations.java
+++ b/src/com/google/javascript/jscomp/CollapseVariableDeclarations.java
@@ -17,12 +17,10 @@
 package com.google.javascript.jscomp;
 
 import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
 import com.google.javascript.rhino.JSDocInfo;
-import com.google.javascript.rhino.JSDocInfoBuilder;
 import com.google.javascript.rhino.Node;
 import com.google.javascript.rhino.Token;
 
@@ -166,7 +164,8 @@ class CollapseVariableDeclarations implements CompilerPass {
 
       boolean hasNodesToCollapse = false;
 
-      while (n != null && (n.isVar() || canBeRedeclared(n, t.getScope()))) {
+      while (n != null &&
+          (n.isVar() || canBeRedeclared(n, t.getScope()))) {
 
         if (n.isVar()) {
           blacklistStubVars(t, n);
@@ -248,11 +247,8 @@ class CollapseVariableDeclarations implements CompilerPass {
       }
 
       if (redeclaration) {
-        // TODO(johnlenz): share the JSDocInfo here rather than building
-        // a new one each time.
-        JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
-        builder.recordSuppressions(ImmutableSet.of("duplicate"));
-        JSDocInfo info = builder.build();
+        JSDocInfo info = new JSDocInfo();
+        info.addSuppression("duplicate");
         var.setJSDocInfo(info);
       }
     }
diff --git a/src/com/google/javascript/jscomp/CoverageInstrumentationPass.java b/src/com/google/javascript/jscomp/CoverageInstrumentationPass.java
index 6e219e388..4c4f1c420 100644
--- a/src/com/google/javascript/jscomp/CoverageInstrumentationPass.java
+++ b/src/com/google/javascript/jscomp/CoverageInstrumentationPass.java
@@ -17,10 +17,8 @@
 package com.google.javascript.jscomp;
 
 import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.javascript.rhino.IR;
-import com.google.javascript.rhino.JSDocInfoBuilder;
 import com.google.javascript.rhino.Node;
 
 import java.util.Map;
@@ -103,10 +101,7 @@ class CoverageInstrumentationPass implements CompilerPass {
         IR.or(
             IR.name(name),
             IR.arraylit()));
-
-    JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
-    builder.recordSuppressions(ImmutableSet.of("duplicate"));
-    var.setJSDocInfo(builder.build());
+    var.addSuppression("duplicate");
     return var;
   }
 }
diff --git a/src/com/google/javascript/jscomp/DiagnosticGroups.java b/src/com/google/javascript/jscomp/DiagnosticGroups.java
index f5ac7c13e..660d059cb 100644
--- a/src/com/google/javascript/jscomp/DiagnosticGroups.java
+++ b/src/com/google/javascript/jscomp/DiagnosticGroups.java
@@ -82,19 +82,18 @@ public class DiagnosticGroups {
   // If a group is suppressible on a per-file basis, it should be added
   // to parser/ParserConfig.properties
   static final String DIAGNOSTIC_GROUP_NAMES =
-      "accessControls, ambiguousFunctionDecl, checkEventfulObjectDisposal, " +
-      "checkRegExp, checkStructDictInheritance, checkTypes, checkVars, " +
-      "conformanceViolations, " +
-      "const, constantProperty, deprecated, duplicateMessage, es3, " +
-      "es5Strict, externsValidation, fileoverviewTags, globalThis, " +
-      "inferredConstCheck, " +
-      "internetExplorerChecks, invalidCasts, misplacedTypeAnnotation, " +
-      "missingGetCssName, missingProperties, " +
-      "missingProvide, missingRequire, missingReturn," +
-      "newCheckTypes, nonStandardJsDocs, reportUnknownTypes, suspiciousCode, " +
-      "strictModuleDepCheck, typeInvalidation, " +
-      "undefinedNames, undefinedVars, unknownDefines, uselessCode, " +
-      "useOfGoogBase, visibility";
+      "accessControls, ambiguousFunctionDecl, checkEventfulObjectDisposal, "
+      + "checkRegExp, checkStructDictInheritance, checkTypes, checkVars, "
+      + "conformanceViolations, const, constantProperty, deprecated, "
+      + "deprecatedAnnotations, duplicateMessage, es3, "
+      + "es5Strict, externsValidation, fileoverviewTags, globalThis, "
+      + "inferredConstCheck, internetExplorerChecks, invalidCasts, "
+      + "misplacedTypeAnnotation, missingGetCssName, missingProperties, "
+      + "missingProvide, missingRequire, missingReturn,"
+      + "newCheckTypes, nonStandardJsDocs, reportUnknownTypes, suspiciousCode, "
+      + "strictModuleDepCheck, typeInvalidation, "
+      + "undefinedNames, undefinedVars, unknownDefines, uselessCode, "
+      + "useOfGoogBase, visibility";
 
   public static final DiagnosticGroup GLOBAL_THIS =
       DiagnosticGroups.registerGroup("globalThis",
@@ -408,6 +407,10 @@ public class DiagnosticGroups {
           CheckSuspiciousCode.SUSPICIOUS_IN_OPERATOR,
           CheckSuspiciousCode.SUSPICIOUS_INSTANCEOF_LEFT_OPERAND);
 
+  public static final DiagnosticGroup DEPRECATED_ANNOTATIONS =
+      DiagnosticGroups.registerGroup("deprecatedAnnotations",
+          RhinoErrorReporter.ANNOTATION_DEPRECATED);
+
   // These checks are not intended to be enabled as errors. It is
   // recommended that you think of them as "linter" warnings that
   // provide optional suggestions.
diff --git a/src/com/google/javascript/jscomp/ExternExportsPass.java b/src/com/google/javascript/jscomp/ExternExportsPass.java
index ec67cac2c..b015cb467 100644
--- a/src/com/google/javascript/jscomp/ExternExportsPass.java
+++ b/src/com/google/javascript/jscomp/ExternExportsPass.java
@@ -25,7 +25,6 @@ import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.javascript.rhino.IR;
 import com.google.javascript.rhino.JSDocInfo;
-import com.google.javascript.rhino.JSDocInfoBuilder;
 import com.google.javascript.rhino.Node;
 import com.google.javascript.rhino.Token;
 
@@ -220,12 +219,6 @@ final class ExternExportsPass extends NodeTraversal.AbstractPostOrderCallback
       return externFunction;
     }
 
-    private JSDocInfo buildEmptyJSDoc() {
-      // TODO(johnlenz): share the JSDocInfo here rather than building
-      // a new one each time.
-      return new JSDocInfoBuilder(false).build(true);
-    }
-
     /**
      * Given an object literal to export, create an object lit with all its
      * string properties. We don't care what the values of those properties
@@ -237,7 +230,7 @@ final class ExternExportsPass extends NodeTraversal.AbstractPostOrderCallback
 
       // This is an indirect way of telling the typed code generator
       // "print the type of this"
-      lit.setJSDocInfo(buildEmptyJSDoc());
+      lit.setJSDocInfo(new JSDocInfo());
 
       int index = 1;
       for (Node child = exportedObjectLit.getFirstChild();
diff --git a/src/com/google/javascript/jscomp/RhinoErrorReporter.java b/src/com/google/javascript/jscomp/RhinoErrorReporter.java
index da72d7f2e..72239697f 100644
--- a/src/com/google/javascript/jscomp/RhinoErrorReporter.java
+++ b/src/com/google/javascript/jscomp/RhinoErrorReporter.java
@@ -90,6 +90,9 @@ class RhinoErrorReporter {
           "{0}. Use --language_in=ECMASCRIPT6 or ECMASCRIPT6_STRICT " +
           "to enable ES6 features.");
 
+  static final DiagnosticType ANNOTATION_DEPRECATED =
+      DiagnosticType.warning("JSC_ANNOTATION_DEPRECATED", "{0}");
+
   // A map of Rhino messages to their DiagnosticType.
   private final Map<Pattern, DiagnosticType> typeMap;
 
@@ -162,6 +165,10 @@ class RhinoErrorReporter {
         .put(Pattern.compile("^this language feature is only supported in es6 mode.*"),
             ES6_FEATURE)
 
+        // Deprecated annotations
+        .put(Pattern.compile("^The @[a-z]+ annotation is deprecated\\..*"),
+            ANNOTATION_DEPRECATED)
+
         .build();
   }
 
diff --git a/src/com/google/javascript/jscomp/SymbolTable.java b/src/com/google/javascript/jscomp/SymbolTable.java
index b54a08b6d..d508a4a64 100644
--- a/src/com/google/javascript/jscomp/SymbolTable.java
+++ b/src/com/google/javascript/jscomp/SymbolTable.java
@@ -121,9 +121,9 @@ public final class SymbolTable {
   private final Map<Node, SymbolScope> scopes = Maps.newLinkedHashMap();
 
   /**
-   * All Nodes with JSDocInfo in the program.
+   * All JSDocInfo in the program.
    */
-  private final List<Node> docInfos = Lists.newArrayList();
+  private final List<JSDocInfo> docInfos = Lists.newArrayList();
 
   private SymbolScope globalScope = null;
 
@@ -173,7 +173,7 @@ public final class SymbolTable {
     return slot.scope;
   }
 
-  public Collection<Node> getAllJSDocInfoNodes() {
+  public Collection<JSDocInfo> getAllJSDocInfo() {
     return Collections.unmodifiableList(docInfos);
   }
 
@@ -1532,7 +1532,7 @@ public final class SymbolTable {
 
         // Find references in the JSDocInfo.
         JSDocInfo info = n.getJSDocInfo();
-        docInfos.add(n);
+        docInfos.add(info);
 
         for (Node typeAst : info.getTypeNodes()) {
           SymbolScope scope = scopes.get(t.getScopeRoot());
diff --git a/src/com/google/javascript/jscomp/VarCheck.java b/src/com/google/javascript/jscomp/VarCheck.java
index 3632c04cd..6967bacdb 100644
--- a/src/com/google/javascript/jscomp/VarCheck.java
+++ b/src/com/google/javascript/jscomp/VarCheck.java
@@ -22,7 +22,6 @@ import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
 import com.google.javascript.jscomp.SyntacticScopeCreator.RedeclarationHandler;
 import com.google.javascript.rhino.IR;
 import com.google.javascript.rhino.JSDocInfo;
-import com.google.javascript.rhino.JSDocInfoBuilder;
 import com.google.javascript.rhino.Node;
 import com.google.javascript.rhino.Token;
 
@@ -178,9 +177,7 @@ class VarCheck extends AbstractPostOrderCallback implements
         varsToDeclareInExterns.contains(varName)) {
       createSynthesizedExternVar(varName);
 
-      JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
-      builder.addSuppression("duplicate");
-      n.setJSDocInfo(builder.build());
+      n.addSuppression("duplicate");
     }
 
     // Check that the var has been declared.
diff --git a/src/com/google/javascript/jscomp/parsing/IRFactory.java b/src/com/google/javascript/jscomp/parsing/IRFactory.java
index fb837afcd..100b05a24 100644
--- a/src/com/google/javascript/jscomp/parsing/IRFactory.java
+++ b/src/com/google/javascript/jscomp/parsing/IRFactory.java
@@ -112,7 +112,6 @@ import com.google.javascript.jscomp.parsing.parser.util.SourceRange;
 import com.google.javascript.rhino.ErrorReporter;
 import com.google.javascript.rhino.IR;
 import com.google.javascript.rhino.JSDocInfo;
-import com.google.javascript.rhino.JSDocInfoBuilder;
 import com.google.javascript.rhino.Node;
 import com.google.javascript.rhino.Node.TypeDeclarationNode;
 import com.google.javascript.rhino.StaticSourceFile;
@@ -197,6 +196,9 @@ class IRFactory {
 
   static final String UNDEFINED_LABEL = "undefined label \"%s\"";
 
+  static final String ANNOTATION_DEPRECATED =
+      "The %s annotation is deprecated.%s";
+
   private final String sourceString;
   private final List<Integer> newlines;
   private final StaticSourceFile sourceFile;
@@ -224,7 +226,9 @@ class IRFactory {
 
   // @license text gets appended onto the fileLevelJsDocBuilder as found,
   // and stored in JSDocInfo for placeholder node.
-  JSDocInfoBuilder fileLevelJsDocBuilder;
+  Node rootNodeJsDocHolder = new Node(Token.SCRIPT);
+  Node.FileLevelJsDocBuilder fileLevelJsDocBuilder =
+      rootNodeJsDocHolder.getJsDocBuilderForNode();
   JSDocInfo fileOverviewInfo = null;
 
   // Use a template node for properties set on all nodes to minimize the
@@ -249,8 +253,6 @@ class IRFactory {
     this.currentComment = nextCommentIter.hasNext() ? nextCommentIter.next() : null;
     this.newlines = Lists.newArrayList();
     this.sourceFile = sourceFile;
-    this.fileLevelJsDocBuilder = new JSDocInfoBuilder(
-        config.parseJsDocDocumentation);
 
     // Pre-generate all the newlines in the file.
     for (int charNo = 0; true; charNo++) {
@@ -471,6 +473,24 @@ class IRFactory {
     validateTypeAnnotations(n);
     validateFunctionJsDoc(n);
     validateMsgJsDoc(n);
+    validateDeprecatedJsDoc(n);
+  }
+
+  /**
+   * Checks that deprecated annotations such as @expose are not present
+   */
+  private void validateDeprecatedJsDoc(Node n) {
+    JSDocInfo info = n.getJSDocInfo();
+    if (info == null) {
+      return;
+    }
+    if (info.isExpose()) {
+      errorReporter.warning(
+          String.format(ANNOTATION_DEPRECATED, "@expose",
+              " Use @nocollapse or @export instead."),
+          sourceName,
+          n.getLineno(), n.getCharno());
+    }
   }
 
   /**
@@ -661,7 +681,7 @@ class IRFactory {
     // Only after we've seen all @fileoverview entries, attach the
     // last one to the root node, and copy the found license strings
     // to that node.
-    JSDocInfo rootNodeJsDoc = fileLevelJsDocBuilder.build();
+    JSDocInfo rootNodeJsDoc = rootNodeJsDocHolder.getJSDocInfo();
     if (rootNodeJsDoc != null) {
       attachJSDoc(rootNodeJsDoc, irNode);
     }
@@ -669,9 +689,7 @@ class IRFactory {
     if (fileOverviewInfo != null) {
       if ((irNode.getJSDocInfo() != null) &&
           (irNode.getJSDocInfo().getLicense() != null)) {
-        JSDocInfoBuilder builder = JSDocInfoBuilder.copyFrom(fileOverviewInfo);
-        builder.recordLicense(irNode.getJSDocInfo().getLicense());
-        fileOverviewInfo = builder.build();
+        fileOverviewInfo.setLicense(irNode.getJSDocInfo().getLicense());
       }
       attachJSDoc(fileOverviewInfo, irNode);
     }
diff --git a/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java b/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java
index 192c611a8..e3e190e20 100644
--- a/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java
+++ b/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java
@@ -99,7 +99,7 @@ public final class JsDocInfoParser {
   private static final Set<String> idGeneratorAnnotationKeywords =
       ImmutableSet.of("unique", "consistent", "stable", "mapped");
 
-  private JSDocInfoBuilder fileLevelJsDocBuilder;
+  private Node.FileLevelJsDocBuilder fileLevelJsDocBuilder;
 
   /**
    * Sets the JsDocBuilder for the file-level (root) node of this parse. The
@@ -109,7 +109,7 @@ public final class JsDocInfoParser {
    * @param fileLevelJsDocBuilder
    */
   void setFileLevelJsDocBuilder(
-      JSDocInfoBuilder fileLevelJsDocBuilder) {
+      Node.FileLevelJsDocBuilder fileLevelJsDocBuilder) {
     this.fileLevelJsDocBuilder = fileLevelJsDocBuilder;
   }
 
@@ -504,7 +504,7 @@ public final class JsDocInfoParser {
 
           if (preserve.length() > 0) {
             if (fileLevelJsDocBuilder != null) {
-              fileLevelJsDocBuilder.addLicense(preserve);
+              fileLevelJsDocBuilder.append(preserve);
             }
           }
 
diff --git a/src/com/google/javascript/rhino/JSDocInfo.java b/src/com/google/javascript/rhino/JSDocInfo.java
index 7a2a6fdf0..8e1257020 100644
--- a/src/com/google/javascript/rhino/JSDocInfo.java
+++ b/src/com/google/javascript/rhino/JSDocInfo.java
@@ -122,8 +122,8 @@ public class JSDocInfo implements Serializable {
     String meaning = null;
     String deprecated = null;
     String license = null;
-    ImmutableSet<String> suppressions = null;
-    ImmutableSet<String> modifies = null;
+    Set<String> suppressions = null;
+    Set<String> modifies = null;
     String lendsName = null;
 
     // Bit flags for properties.
@@ -173,8 +173,8 @@ public class JSDocInfo implements Serializable {
       other.meaning = meaning;
       other.deprecated = deprecated;
       other.license = license;
-      other.suppressions = suppressions == null ? null : ImmutableSet.copyOf(suppressions);
-      other.modifies = modifies == null ? null :  ImmutableSet.copyOf(modifies);
+      other.suppressions = suppressions == null ? null : new HashSet<>(suppressions);
+      other.modifies = modifies == null ? null : new HashSet<>(modifies);
       other.lendsName = lendsName;
 
       other.propertyBitField = propertyBitField;
@@ -443,12 +443,12 @@ public class JSDocInfo implements Serializable {
    * Creates a {@link JSDocInfo} object. This object should be created using
    * a {@link JSDocInfoBuilder}.
    */
-  JSDocInfo(boolean includeDocumentation) {
+  public JSDocInfo(boolean includeDocumentation) {
     this.includeDocumentation = includeDocumentation;
   }
 
   // Visible for testing.
-  JSDocInfo() {}
+  public JSDocInfo() {}
 
   @Override
   public JSDocInfo clone() {
@@ -565,7 +565,8 @@ public class JSDocInfo implements Serializable {
     setFlag(value, MASK_NOALIAS);
   }
 
-  void setDeprecated(boolean value) {
+  // Visible for testing.
+  public void setDeprecated(boolean value) {
     setFlag(value, MASK_DEPRECATED);
   }
 
@@ -834,7 +835,8 @@ public class JSDocInfo implements Serializable {
     return (bitset & mask) != 0x00;
   }
 
-  void setVisibility(Visibility visibility) {
+  // Visible for testing.
+  public void setVisibility(Visibility visibility) {
     this.visibility = visibility;
   }
 
@@ -897,17 +899,13 @@ public class JSDocInfo implements Serializable {
   /**
    * Add a suppressed warning.
    */
-  void addSuppression(String suppression) {
+  public void addSuppression(String suppression) {
     lazyInitInfo();
 
     if (info.suppressions == null) {
-      info.suppressions = ImmutableSet.of(suppression);
-    } else {
-      info.suppressions = new ImmutableSet.Builder<String>()
-          .addAll(info.suppressions)
-          .add(suppression)
-          .build();
+      info.suppressions = Sets.newHashSet();
     }
+    info.suppressions.add(suppression);
   }
 
   /**
@@ -921,10 +919,22 @@ public class JSDocInfo implements Serializable {
       return false;
     }
 
-    info.suppressions = ImmutableSet.copyOf(suppressions);
+    info.suppressions = suppressions;
     return true;
   }
 
+  /**
+   * Add modifies values.
+   */
+  void addModifies(String modifies) {
+    lazyInitInfo();
+
+    if (info.modifies == null) {
+      info.modifies = Sets.newHashSet();
+    }
+    info.modifies.add(modifies);
+  }
+
   /**
    * Sets modifies values.
    * @param modifies A list of modifies types.
@@ -936,7 +946,7 @@ public class JSDocInfo implements Serializable {
       return false;
     }
 
-    info.modifies = ImmutableSet.copyOf(modifies);
+    info.modifies = modifies;
     return true;
   }
 
@@ -1568,10 +1578,14 @@ public class JSDocInfo implements Serializable {
     return (info == null) ? null : info.license;
   }
 
-  /**
+  /** License directives can appear in multiple comments, and always
+   * apply to the entire file.  Break protection and allow outsiders to
+   * update the license string so that we can attach the license text even
+   * when the JSDocInfo has been created and tagged with other information.
    * @param license String containing new license text.
    */
-  void setLicense(String license) {
+
+  public void setLicense(String license) {
     lazyInitInfo();
     info.license = license;
   }
diff --git a/src/com/google/javascript/rhino/JSDocInfoBuilder.java b/src/com/google/javascript/rhino/JSDocInfoBuilder.java
index 5ecad2f90..57fee0e39 100644
--- a/src/com/google/javascript/rhino/JSDocInfoBuilder.java
+++ b/src/com/google/javascript/rhino/JSDocInfoBuilder.java
@@ -39,8 +39,6 @@
 
 package com.google.javascript.rhino;
 
-import com.google.common.base.Preconditions;
-
 import com.google.javascript.rhino.JSDocInfo.Visibility;
 
 import java.util.List;
@@ -56,7 +54,7 @@ import javax.annotation.Nullable;
  * object being created.
  *
  */
-public final class JSDocInfoBuilder {
+final public class JSDocInfoBuilder {
   // the current JSDoc which is being populated
   private JSDocInfo currentInfo;
 
@@ -141,65 +139,29 @@ public final class JSDocInfoBuilder {
     return currentInfo.getDescription() != null;
   }
 
-
   /**
    * Builds a {@link JSDocInfo} object based on the populated information and
-   * returns it.
+   * returns it. Once this method is called, the builder can be reused to build
+   * another {@link JSDocInfo} object.
    *
+   * @param associatedNode The source node containing the JSDoc.
    * @return a {@link JSDocInfo} object populated with the values given to this
    *     builder. If no value was populated, this method simply returns
    *     {@code null}
    */
-  public JSDocInfo build() {
-    return build(false);
-  }
-
-
-  /**
-   * Builds a {@link JSDocInfo} object based on the populated information and
-   * returns it.
-   *
-   * @param always Return an default JSDoc object.
-   * @return a {@link JSDocInfo} object populated with the values given to this
-   *     builder. If no value was populated and {@code always} is false, returns
-   *     {@code null}. If {@code always} is true, returns a default JSDocInfo.
-   */
-  public JSDocInfo build(boolean always) {
-    if (populated || always) {
-      Preconditions.checkState(currentInfo != null);
+  public JSDocInfo build(Node associatedNode) {
+    if (populated) {
       JSDocInfo built = currentInfo;
-      currentInfo = null;
+      built.setAssociatedNode(associatedNode);
       populateDefaults(built);
       populated = false;
+      currentInfo = new JSDocInfo(this.parseDocumentation);
       return built;
     } else {
       return null;
     }
   }
 
-  /**
-   * Builds a {@link JSDocInfo} object based on the populated information and
-   * returns it. Once this method is called, the builder can be reused to build
-   * another {@link JSDocInfo} object.
-   *
-   * @param associatedNode The source node containing the JSDoc.
-   * @return a {@link JSDocInfo} object populated with the values given to this
-   *     builder. If no value was populated, this method simply returns
-   *     {@code null}
-   */
-  public JSDocInfo build(Node associatedNode) {
-    JSDocInfo info = build(false);
-    if (info != null) {
-      info.setAssociatedNode(associatedNode);
-    }
-
-    // TODO(johnlenz): let this be null.
-    currentInfo = new JSDocInfo(this.parseDocumentation);
-    populated = false;
-
-    return info;
-  }
-
   /** Generate defaults when certain parameters are not specified. */
   private static void populateDefaults(JSDocInfo info) {
     if (info.getVisibility() == null) {
@@ -256,6 +218,15 @@ public final class JSDocInfoBuilder {
     }
   }
 
+  /**
+   * Adds a name declaration to the current marker.
+   * @deprecated Use #markName(String, StaticSourceFile, int, int)
+   */
+  @Deprecated
+  public void markName(String name,  int lineno, int charno) {
+    markName(name, null, lineno, charno);
+  }
+
   /**
    * Adds a name declaration to the current marker.
    */
@@ -516,11 +487,6 @@ public final class JSDocInfoBuilder {
     }
   }
 
-  public void addSuppression(String suppression) {
-    currentInfo.addSuppression(suppression);
-    populated = true;
-  }
-
   /**
    * Records the list of modifies warnings.
    */
@@ -771,22 +737,6 @@ public final class JSDocInfoBuilder {
     }
   }
 
-  public boolean recordLicense(String license) {
-    currentInfo.setLicense(license);
-    populated = true;
-    return true;
-  }
-
-  public boolean addLicense(String license) {
-    String txt = currentInfo.getLicense();
-    if (txt == null) {
-      txt = "";
-    }
-    currentInfo.setLicense(txt + license);
-    populated = true;
-    return true;
-  }
-
   /**
    * Records that the {@link JSDocInfo} being built should have its
    * {@link JSDocInfo#isHidden()} flag set to {@code true}.
diff --git a/src/com/google/javascript/rhino/Node.java b/src/com/google/javascript/rhino/Node.java
index a6e4c83e7..5f4463ae8 100644
--- a/src/com/google/javascript/rhino/Node.java
+++ b/src/com/google/javascript/rhino/Node.java
@@ -2070,6 +2070,35 @@ public class Node implements Cloneable, Serializable {
     this.typei = type;
   }
 
+  public FileLevelJsDocBuilder getJsDocBuilderForNode() {
+    return new FileLevelJsDocBuilder();
+  }
+
+  /**
+   * An inner class that provides back-door access to the license
+   * property of the JSDocInfo property for this node. This is only
+   * meant to be used for top-level script nodes where the
+   * {@link com.google.javascript.jscomp.parsing.JsDocInfoParser} needs to
+   * be able to append directly to the top-level node, not just the
+   * current node.
+   */
+  public class FileLevelJsDocBuilder {
+    public void append(String fileLevelComment) {
+      JSDocInfo jsDocInfo = getJSDocInfo();
+      if (jsDocInfo == null) {
+        // TODO(user): Is there a way to determine whether to
+        // parse the JsDoc documentation from here?
+        jsDocInfo = new JSDocInfo(false);
+      }
+      String license = jsDocInfo.getLicense();
+      if (license == null) {
+        license = "";
+      }
+      jsDocInfo.setLicense(license + fileLevelComment);
+      setJSDocInfo(jsDocInfo);
+    }
+  }
+
   /**
    * Get the {@link JSDocInfo} attached to this node.
    * @return the information or {@code null} if no JSDoc is attached to this
@@ -2164,6 +2193,17 @@ public class Node implements Cloneable, Serializable {
     return (Set<String>) getProp(DIRECTIVES);
   }
 
+  /**
+   * Adds a warning to be suppressed. This is indistinguishable
+   * from having a {@code @suppress} tag in the code.
+   */
+  public void addSuppression(String warning) {
+    if (getJSDocInfo() == null) {
+      setJSDocInfo(new JSDocInfo(false));
+    }
+    getJSDocInfo().addSuppression(warning);
+  }
+
   /**
    * Sets whether this is an added block that should not be considered
    * a real source block. Eg: In "if (true) x;", the "x;" is put under an added


Test patch:
diff --git a/test/com/google/javascript/jscomp/CheckConformanceTest.java b/test/com/google/javascript/jscomp/CheckConformanceTest.java
index fc26c5745..cbaf82f91 100644
--- a/test/com/google/javascript/jscomp/CheckConformanceTest.java
+++ b/test/com/google/javascript/jscomp/CheckConformanceTest.java
@@ -831,6 +831,7 @@ public final class CheckConformanceTest extends CompilerTestCase {
         "  error_message: 'BanExpose Message'\n" +
         "}";
 
+    setExpectParseWarningsThisTest();
     testSame(
         EXTERNS,
         "/** @expose */ var x;",
diff --git a/test/com/google/javascript/jscomp/IntegrationTest.java b/test/com/google/javascript/jscomp/IntegrationTest.java
index 0928822bd..1bcacb11f 100644
--- a/test/com/google/javascript/jscomp/IntegrationTest.java
+++ b/test/com/google/javascript/jscomp/IntegrationTest.java
@@ -376,22 +376,26 @@ public final class IntegrationTest extends IntegrationTestCase {
     CompilationLevel.ADVANCED_OPTIMIZATIONS
         .setOptionsForCompilationLevel(options);
     test(options,
-         "var x = {eeny: 1, /** @expose */ meeny: 2};" +
-         "/** @constructor */ var Foo = function() {};" +
-         "/** @expose */  Foo.prototype.miny = 3;" +
-         "Foo.prototype.moe = 4;" +
-         "/** @expose */  Foo.prototype.tiger;" +
-         "function moe(a, b) { return a.meeny + b.miny + a.tiger; }" +
-         "window['x'] = x;" +
-         "window['Foo'] = Foo;" +
-         "window['moe'] = moe;",
-         "function a(){}" +
-         "a.prototype.miny=3;" +
-         "window.x={a:1,meeny:2};" +
-         "window.Foo=a;" +
-         "window.moe=function(b,c){" +
-         "  return b.meeny+c.miny+b.tiger" +
-         "}");
+        new String[] {"var x = {eeny: 1, /** @expose */ meeny: 2};" +
+            "/** @constructor */ var Foo = function() {};" +
+            "/** @expose */  Foo.prototype.miny = 3;" +
+            "Foo.prototype.moe = 4;" +
+            "/** @expose */  Foo.prototype.tiger;" +
+            "function moe(a, b) { return a.meeny + b.miny + a.tiger; }" +
+            "window['x'] = x;" +
+            "window['Foo'] = Foo;" +
+            "window['moe'] = moe;"},
+        new String[] {"function a(){}" +
+            "a.prototype.miny=3;" +
+            "window.x={a:1,meeny:2};" +
+            "window.Foo=a;" +
+            "window.moe=function(b,c){" +
+            "  return b.meeny+c.miny+b.tiger" +
+            "}"},
+        new DiagnosticType[]{
+            RhinoErrorReporter.PARSE_ERROR,
+            RhinoErrorReporter.PARSE_ERROR,
+            RhinoErrorReporter.PARSE_ERROR});
   }
 
   public void testCheckSymbolsOff() {
diff --git a/test/com/google/javascript/jscomp/NormalizeTest.java b/test/com/google/javascript/jscomp/NormalizeTest.java
index 8e59febe1..df384edb4 100644
--- a/test/com/google/javascript/jscomp/NormalizeTest.java
+++ b/test/com/google/javascript/jscomp/NormalizeTest.java
@@ -461,11 +461,13 @@ public final class NormalizeTest extends CompilerTestCase {
   }
 
   public void testExposeSimple() {
+    setExpectParseWarningsThisTest();
     test("var x = {}; /** @expose */ x.y = 3; x.y = 5;",
          "var x = {}; x['y'] = 3; x['y'] = 5;");
   }
 
   public void testExposeComplex() {
+    setExpectParseWarningsThisTest();
     test(
         "var x = {/** @expose */ a: 1, b: 2};"
         + "x.a = 3; /** @expose */ x.b = 5;",
diff --git a/test/com/google/javascript/jscomp/RhinoErrorReporterTest.java b/test/com/google/javascript/jscomp/RhinoErrorReporterTest.java
index 82667d286..e9e5a4b73 100644
--- a/test/com/google/javascript/jscomp/RhinoErrorReporterTest.java
+++ b/test/com/google/javascript/jscomp/RhinoErrorReporterTest.java
@@ -100,6 +100,19 @@ public final class RhinoErrorReporterTest extends TestCase {
     assertEquals(10, error.getCharno());
   }
 
+  public void testAnnotationDeprecated() throws Exception {
+    String message =
+        "The @expose annotation is deprecated. Use @nocollapse or @export "
+        + "instead.";
+    JSError error = assertWarning(
+        "/** @expose */ var x = 1;",
+        RhinoErrorReporter.ANNOTATION_DEPRECATED,
+        message);
+
+    assertEquals(1, error.getLineNumber());
+    assertEquals(15, error.getCharno());
+  }
+
   /**
    * Verifies that the compiler emits an error for the given code.
    */
diff --git a/test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java b/test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java
index b1a95823b..fc3c45d0f 100644
--- a/test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java
+++ b/test/com/google/javascript/jscomp/parsing/JsDocInfoParserTest.java
@@ -28,7 +28,6 @@ import com.google.javascript.rhino.InputId;
 import com.google.javascript.rhino.JSDocInfo;
 import com.google.javascript.rhino.JSDocInfo.Marker;
 import com.google.javascript.rhino.JSDocInfo.Visibility;
-import com.google.javascript.rhino.JSDocInfoBuilder;
 import com.google.javascript.rhino.JSTypeExpression;
 import com.google.javascript.rhino.Node;
 import com.google.javascript.rhino.SimpleSourceFile;
@@ -50,12 +49,11 @@ public final class JsDocInfoParserTest extends BaseJSTypeTestCase {
 
   private Set<String> extraAnnotations;
   private Set<String> extraSuppressions;
-  private JSDocInfoBuilder fileLevelJsDocBuilder = null;
+  private Node.FileLevelJsDocBuilder fileLevelJsDocBuilder = null;
 
   @Override
   public void setUp() throws Exception {
     super.setUp();
-    fileLevelJsDocBuilder = null;
     extraAnnotations = new HashSet<>(ParserRunner.createConfig(
         true, LanguageMode.ECMASCRIPT3, false, null)
             .annotationNames.keySet());
@@ -1469,35 +1467,35 @@ public final class JsDocInfoParserTest extends BaseJSTypeTestCase {
   }
 
   public void testParsePreserve() throws Exception {
-    this.fileLevelJsDocBuilder = new JSDocInfoBuilder(false);
+    Node node = new Node(1);
+    this.fileLevelJsDocBuilder = node.getJsDocBuilderForNode();
     String comment = "@preserve Foo\nBar\n\nBaz*/";
     parse(comment);
-    JSDocInfo info = this.fileLevelJsDocBuilder.build(true);
-    assertThat(info.getLicense()).isEqualTo(" Foo\nBar\n\nBaz");
+    assertThat(node.getJSDocInfo().getLicense()).isEqualTo(" Foo\nBar\n\nBaz");
   }
 
   public void testParseLicense() throws Exception {
-    this.fileLevelJsDocBuilder = new JSDocInfoBuilder(false);
+    Node node = new Node(1);
+    this.fileLevelJsDocBuilder = node.getJsDocBuilderForNode();
     String comment = "@license Foo\nBar\n\nBaz*/";
     parse(comment);
-    JSDocInfo info = this.fileLevelJsDocBuilder.build(true);
-    assertThat(info.getLicense()).isEqualTo(" Foo\nBar\n\nBaz");
+    assertThat(node.getJSDocInfo().getLicense()).isEqualTo(" Foo\nBar\n\nBaz");
   }
 
   public void testParseLicenseAscii() throws Exception {
-    this.fileLevelJsDocBuilder = new JSDocInfoBuilder(false);
+    Node node = new Node(1);
+    this.fileLevelJsDocBuilder = node.getJsDocBuilderForNode();
     String comment = "@license Foo\n *   Bar\n\n  Baz*/";
     parse(comment);
-    JSDocInfo info = this.fileLevelJsDocBuilder.build(true);
-    assertThat(info.getLicense()).isEqualTo(" Foo\n   Bar\n\n  Baz");
+    assertThat(node.getJSDocInfo().getLicense()).isEqualTo(" Foo\n   Bar\n\n  Baz");
   }
 
   public void testParseLicenseWithAnnotation() throws Exception {
-    this.fileLevelJsDocBuilder = new JSDocInfoBuilder(false);
+    Node node = new Node(1);
+    this.fileLevelJsDocBuilder = node.getJsDocBuilderForNode();
     String comment = "@license Foo \n * @author Charlie Brown */";
     parse(comment);
-    JSDocInfo info = this.fileLevelJsDocBuilder.build(true);
-    assertThat(info.getLicense()).isEqualTo(" Foo \n @author Charlie Brown ");
+    assertThat(node.getJSDocInfo().getLicense()).isEqualTo(" Foo \n @author Charlie Brown ");
   }
 
   public void testParseDefine1() throws Exception {
diff --git a/test/com/google/javascript/jscomp/parsing/NewParserTest.java b/test/com/google/javascript/jscomp/parsing/NewParserTest.java
index b2c9025bb..46b1644af 100644
--- a/test/com/google/javascript/jscomp/parsing/NewParserTest.java
+++ b/test/com/google/javascript/jscomp/parsing/NewParserTest.java
@@ -65,6 +65,9 @@ public final class NewParserTest extends BaseJSTypeTestCase {
       "for legacy reasons. Removing this from your code is " +
       "safe for all browsers currently in use.";
 
+  private static final String ANNOTATION_DEPRECATED_WARNING =
+      "The %s annotation is deprecated.%s";
+
   private Config.LanguageMode mode;
   private boolean isIdeMode = false;
 
@@ -2563,6 +2566,12 @@ public final class NewParserTest extends BaseJSTypeTestCase {
     assertThat(stop - start).named("runtime").isLessThan(5000L);
   }
 
+  public void testExposeDeprecated() {
+    parseWarning("/** @expose */ var x = 0;",
+        String.format(ANNOTATION_DEPRECATED_WARNING, "@expose",
+            " Use @nocollapse or @export instead."));
+  }
+
   private Node script(Node stmt) {
     Node n = new Node(Token.SCRIPT, stmt);
     n.setIsSyntheticBlock(true);
diff --git a/test/com/google/javascript/rhino/NodeTest.java b/test/com/google/javascript/rhino/NodeTest.java
index b88bb1449..c49bdea4e 100644
--- a/test/com/google/javascript/rhino/NodeTest.java
+++ b/test/com/google/javascript/rhino/NodeTest.java
@@ -187,6 +187,14 @@ public class NodeTest extends TestCase {
     assertEquals(charno, Node.extractCharno(linecharno));
   }
 
+  public void testFileLevelJSDocAppender() {
+    Node n = new Node(1);
+    Node.FileLevelJsDocBuilder builder = n.getJsDocBuilderForNode();
+    builder.append("foo");
+    builder.append("bar");
+    assertEquals("foobar", n.getJSDocInfo().getLicense());
+  }
+
   public void testIsQualifiedName() {
     assertTrue(IR.name("a").isQualifiedName());
     assertTrue(IR.name("$").isQualifiedName());
diff --git a/test/com/google/javascript/rhino/jstype/JSTypeTest.java b/test/com/google/javascript/rhino/jstype/JSTypeTest.java
index 5834f166e..31d303f0f 100644
--- a/test/com/google/javascript/rhino/jstype/JSTypeTest.java
+++ b/test/com/google/javascript/rhino/jstype/JSTypeTest.java
@@ -47,7 +47,6 @@ import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.javascript.rhino.JSDocInfo;
 import com.google.javascript.rhino.JSDocInfo.Visibility;
-import com.google.javascript.rhino.JSDocInfoBuilder;
 import com.google.javascript.rhino.Node;
 import com.google.javascript.rhino.SimpleErrorReporter;
 import com.google.javascript.rhino.Token;
@@ -3181,8 +3180,7 @@ public class JSTypeTest extends BaseJSTypeTestCase {
 
   /** Tests assigning JsDoc on a prototype property. */
   public void testJSDocOnPrototypeProperty() throws Exception {
-    subclassCtor.setPropertyJSDocInfo("prototype",
-        new JSDocInfoBuilder(false).build());
+    subclassCtor.setPropertyJSDocInfo("prototype", new JSDocInfo());
     assertNull(subclassCtor.getOwnPropertyJSDocInfo("prototype"));
   }
 
@@ -5979,9 +5977,8 @@ public class JSTypeTest extends BaseJSTypeTestCase {
   }
 
   public void testGetAndSetJSDocInfoWithNamedType() throws Exception {
-    JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
-    builder.recordDeprecated();
-    JSDocInfo info = builder.build(null);
+    JSDocInfo info = new JSDocInfo();
+    info.setDeprecated(true);
 
     assertNull(namedGoogBar.getOwnPropertyJSDocInfo("X"));
     namedGoogBar.setPropertyJSDocInfo("X", info);
@@ -5995,13 +5992,11 @@ public class JSTypeTest extends BaseJSTypeTestCase {
         registry.createObjectType(null, registry.createAnonymousObjectType(null));
     ObjectType sub = registry.createObjectType(null, sup);
 
-    JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
-    builder.recordDeprecated();
-    JSDocInfo deprecated = builder.build(null);
+    JSDocInfo deprecated = new JSDocInfo();
+    deprecated.setDeprecated(true);
 
-    builder = new JSDocInfoBuilder(false);
-    builder.recordVisibility(Visibility.PRIVATE);
-    JSDocInfo privateInfo = builder.build(null);
+    JSDocInfo privateInfo = new JSDocInfo();
+    privateInfo.setVisibility(Visibility.PRIVATE);
 
     sup.defineProperty("X", NUMBER_TYPE, true, null);
     sup.setPropertyJSDocInfo("X", privateInfo);
@@ -6014,15 +6009,13 @@ public class JSTypeTest extends BaseJSTypeTestCase {
         sup.getOwnPropertyJSDocInfo("X").getVisibility());
     assertTypeEquals(NUMBER_TYPE, sup.getPropertyType("X"));
     assertTrue(sub.getOwnPropertyJSDocInfo("X").isDeprecated());
-    assertEquals(Visibility.INHERITED,
-        sub.getOwnPropertyJSDocInfo("X").getVisibility());
+    assertNull(sub.getOwnPropertyJSDocInfo("X").getVisibility());
     assertTypeEquals(NUMBER_TYPE, sub.getPropertyType("X"));
   }
 
   public void testGetAndSetJSDocInfoWithNoType() throws Exception {
-    JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
-    builder.recordDeprecated();
-    JSDocInfo deprecated = builder.build(null);
+    JSDocInfo deprecated = new JSDocInfo();
+    deprecated.setDeprecated(true);
 
     NO_TYPE.setPropertyJSDocInfo("X", deprecated);
     assertNull(NO_TYPE.getOwnPropertyJSDocInfo("X"));