diff --git a/src/com/google/javascript/jscomp/gwt/client/Es6ClassConverterJsDocHelper.java b/src/com/google/javascript/jscomp/gwt/client/Es6ClassConverterJsDocHelper.java new file mode 100644 index 00000000000..9c302762de7 --- /dev/null +++ b/src/com/google/javascript/jscomp/gwt/client/Es6ClassConverterJsDocHelper.java @@ -0,0 +1,124 @@ +/* + * Copyright 2019 The Closure Compiler Authors. + * + * 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.google.javascript.jscomp.gwt.client; + +import com.google.common.base.Strings; +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.regexp.shared.RegExp; +import com.google.javascript.jscomp.JSDocInfoPrinter; +import com.google.javascript.jscomp.parsing.Config; +import com.google.javascript.jscomp.parsing.Config.JsDocParsing; +import com.google.javascript.jscomp.parsing.Config.LanguageMode; +import com.google.javascript.jscomp.parsing.JsDocInfoParser; +import com.google.javascript.jscomp.parsing.JsDocTokenStream; +import com.google.javascript.rhino.ErrorReporter; +import com.google.javascript.rhino.JSDocInfo; +import jsinterop.annotations.JsMethod; + +/** + * Utilities to parse JS doc and assist in upgrade ES5 to ES6 classes where JS Doc must be split + * into class and constructor comments. + */ +final class Es6ClassConverterJsDocHelper implements EntryPoint { + + private Es6ClassConverterJsDocHelper() {} + + @Override + public void onModuleLoad() { + exportCode(); + } + + private native void exportCode() /*-{ + var getClassJsDoc = $entry(@com.google.javascript.jscomp.gwt.client.Es6ClassConverterJsDocHelper::getClassJsDoc(*)); + var getConstructorJsDoc = $entry(@com.google.javascript.jscomp.gwt.client.Es6ClassConverterJsDocHelper::getConstructorJsDoc(*)); + if (typeof module !== 'undefined' && module.exports) { + module.exports.getClassJsDoc = getClassJsDoc; + module.exports.getConstructorJsDoc = getConstructorJsDoc; + } + }-*/; + + /** + * Gets JS Doc that should be retained on a class. Used to upgrade ES5 to ES6 classes and separate + * class from constructor comments. + */ + @JsMethod(name = "getClassJsDoc", namespace = "jscomp") + public static String getClassJsDoc(String jsDoc) { + if (Strings.isNullOrEmpty(jsDoc)) { + return null; + } + Config config = + Config.builder() + .setLanguageMode(LanguageMode.ECMASCRIPT3) + .setStrictMode(Config.StrictMode.SLOPPY) + .setJsDocParsingMode(JsDocParsing.INCLUDE_DESCRIPTIONS_WITH_WHITESPACE) + .build(); + JsDocInfoParser parser = + new JsDocInfoParser( + // Stream expects us to remove the leading /** + new JsDocTokenStream(jsDoc.substring(3)), + jsDoc, + 0, + null, + config, + ErrorReporter.NULL_INSTANCE); + parser.parse(); + JSDocInfo parsed = parser.retrieveAndResetParsedJSDocInfo(); + JSDocInfo classComments = parsed.cloneClassDoc(); + JSDocInfoPrinter printer = + new JSDocInfoPrinter(/* useOriginalName= */ true, /* printDesc= */ true); + String comment = printer.print(classComments); + // Don't return empty comments, return null instead. + if (comment == null || RegExp.compile("\\s*/\\*\\*\\s*\\*/\\s*").test(comment)) { + return null; + } + return comment.trim(); + } + + /** + * Gets JS Doc that should be moved to a constructor. Used to upgrade ES5 to ES6 classes and + * separate class from constructor comments. + */ + @JsMethod(name = "getConstructorJsDoc", namespace = "jscomp") + public static String getConstructorJsDoc(String jsDoc) { + if (Strings.isNullOrEmpty(jsDoc)) { + return null; + } + Config config = + Config.builder() + .setLanguageMode(LanguageMode.ECMASCRIPT3) + .setStrictMode(Config.StrictMode.SLOPPY) + .setJsDocParsingMode(JsDocParsing.INCLUDE_DESCRIPTIONS_WITH_WHITESPACE) + .build(); + JsDocInfoParser parser = + new JsDocInfoParser( + // Stream expects us to remove the leading /** + new JsDocTokenStream(jsDoc.substring(3)), + jsDoc, + 0, + null, + config, + ErrorReporter.NULL_INSTANCE); + parser.parse(); + JSDocInfo parsed = parser.retrieveAndResetParsedJSDocInfo(); + JSDocInfo params = parsed.cloneConstructorDoc(); + if (parsed.getParameterNames().isEmpty() && parsed.getSuppressions().isEmpty()) { + return null; + } + JSDocInfoPrinter printer = + new JSDocInfoPrinter(/* useOriginalName= */ true, /* printDesc= */ true); + return printer.print(params).trim(); + } +} diff --git a/src/com/google/javascript/jscomp/parsing/Config.java b/src/com/google/javascript/jscomp/parsing/Config.java index 6aa4abbc578..b3bb78dc962 100644 --- a/src/com/google/javascript/jscomp/parsing/Config.java +++ b/src/com/google/javascript/jscomp/parsing/Config.java @@ -119,34 +119,34 @@ public enum RunMode { } /** Language level to accept. */ - abstract LanguageMode languageMode(); + public abstract LanguageMode languageMode(); /** Whether to assume input is strict mode compliant. */ - abstract StrictMode strictMode(); + public abstract StrictMode strictMode(); /** How to parse the descriptions of JsDoc comments. */ - abstract JsDocParsing jsDocParsingMode(); + public abstract JsDocParsing jsDocParsingMode(); /** Whether to keep going after encountering a parse error. */ - abstract RunMode runMode(); + public abstract RunMode runMode(); /** Recognized JSDoc annotations, mapped from their name to their internal representation. */ - abstract ImmutableMap annotations(); + public abstract ImmutableMap annotations(); /** Set of recognized names in a {@code @suppress} tag. */ - abstract ImmutableSet suppressionNames(); + public abstract ImmutableSet suppressionNames(); /** Set of recognized names in a {@code @closurePrimitive} tag. */ abstract ImmutableSet closurePrimitiveNames(); /** Whether to parse inline source maps (//# sourceMappingURL=data:...). */ - abstract boolean parseInlineSourceMaps(); + public abstract boolean parseInlineSourceMaps(); final ImmutableSet annotationNames() { return annotations().keySet(); } - static Builder builder() { + public static Builder builder() { return new AutoValue_Config.Builder() .setLanguageMode(LanguageMode.TYPESCRIPT) .setStrictMode(StrictMode.STRICT) @@ -158,19 +158,20 @@ static Builder builder() { .setParseInlineSourceMaps(false); } + /** Builder for a Config. */ @AutoValue.Builder - abstract static class Builder { - abstract Builder setLanguageMode(LanguageMode mode); + public abstract static class Builder { + public abstract Builder setLanguageMode(LanguageMode mode); - abstract Builder setStrictMode(StrictMode mode); + public abstract Builder setStrictMode(StrictMode mode); - abstract Builder setJsDocParsingMode(JsDocParsing mode); + public abstract Builder setJsDocParsingMode(JsDocParsing mode); - abstract Builder setRunMode(RunMode mode); + public abstract Builder setRunMode(RunMode mode); - abstract Builder setParseInlineSourceMaps(boolean parseInlineSourceMaps); + public abstract Builder setParseInlineSourceMaps(boolean parseInlineSourceMaps); - abstract Builder setSuppressionNames(Iterable names); + public abstract Builder setSuppressionNames(Iterable names); abstract Builder setClosurePrimitiveNames(Iterable names); @@ -178,10 +179,10 @@ final Builder setExtraAnnotationNames(Iterable names) { return setAnnotations(buildAnnotations(names)); } - abstract Config build(); + public abstract Config build(); // The following is intended to be used internally only (but isn't private due to AutoValue). - abstract Builder setAnnotations(ImmutableMap names); + public abstract Builder setAnnotations(ImmutableMap names); } /** Create the annotation names from the user-specified annotation whitelist. */ diff --git a/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java b/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java index 04648efae32..d6b1b65fea8 100644 --- a/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java +++ b/src/com/google/javascript/jscomp/parsing/JsDocInfoParser.java @@ -160,13 +160,13 @@ private enum State { NEXT_IS_ANNOTATION } - - JsDocInfoParser(JsDocTokenStream stream, - String comment, - int commentPosition, - Node templateNode, - Config config, - ErrorReporter errorReporter) { + public JsDocInfoParser( + JsDocTokenStream stream, + String comment, + int commentPosition, + Node templateNode, + Config config, + ErrorReporter errorReporter) { this.stream = stream; boolean parseDocumentation = config.jsDocParsingMode().shouldParseDescriptions(); @@ -261,14 +261,12 @@ private static JsDocInfoParser getParser(String toParse) { } /** - * Parses a {@link JSDocInfo} object. This parsing method reads all tokens - * returned by the {@link JsDocTokenStream#getJsDocToken()} method until the - * {@link JsDocToken#EOC} is returned. + * Parses a {@link JSDocInfo} object. This parsing method reads all tokens returned by the {@link + * JsDocTokenStream#getJsDocToken()} method until the {@link JsDocToken#EOC} is returned. * - * @return {@code true} if JSDoc information was correctly parsed, - * {@code false} otherwise + * @return {@code true} if JSDoc information was correctly parsed, {@code false} otherwise */ - boolean parse() { + public boolean parse() { state = State.SEARCHING_ANNOTATION; skipEOLs(); @@ -2763,7 +2761,7 @@ private boolean hasParsedFileOverviewDocInfo() { return jsdocBuilder.isPopulatedWithFileOverview(); } - JSDocInfo retrieveAndResetParsedJSDocInfo() { + public JSDocInfo retrieveAndResetParsedJSDocInfo() { return jsdocBuilder.build(); } diff --git a/src/com/google/javascript/jscomp/parsing/JsDocTokenStream.java b/src/com/google/javascript/jscomp/parsing/JsDocTokenStream.java index ec6b248b53d..cbf7afca71c 100644 --- a/src/com/google/javascript/jscomp/parsing/JsDocTokenStream.java +++ b/src/com/google/javascript/jscomp/parsing/JsDocTokenStream.java @@ -23,10 +23,10 @@ /** * This class implements the scanner for JsDoc strings. * - * It is heavily based on Rhino's TokenStream. + *

It is heavily based on Rhino's TokenStream. * */ -class JsDocTokenStream { +public class JsDocTokenStream { /* * For chars - because we need something out-of-range * to check. (And checking EOF by exception is annoying.) @@ -34,7 +34,7 @@ class JsDocTokenStream { */ private static final int EOF_CHAR = -1; - JsDocTokenStream(String sourceString) { + public JsDocTokenStream(String sourceString) { this(sourceString, 0); } diff --git a/src/com/google/javascript/rhino/JSDocInfo.java b/src/com/google/javascript/rhino/JSDocInfo.java index 6528eae38b7..50291be225c 100644 --- a/src/com/google/javascript/rhino/JSDocInfo.java +++ b/src/com/google/javascript/rhino/JSDocInfo.java @@ -156,7 +156,7 @@ public String toString() { .toString(); } - @SuppressWarnings("MissingOverride") // Adding @Override breaks the GWT compilation. + @SuppressWarnings("MissingOverride") // Adding @Override breaks the GWT compilation. protected LazilyInitializedInfo clone() { return clone(false); } @@ -188,6 +188,20 @@ protected LazilyInitializedInfo clone(boolean cloneTypeNodes) { return other; } + protected LazilyInitializedInfo cloneClassDoc() { + LazilyInitializedInfo other = clone(/* cloneTypeNodes= */ false); + other.parameters = null; + other.suppressions = null; + return other; + } + + protected LazilyInitializedInfo cloneConstructorDoc() { + LazilyInitializedInfo other = new LazilyInitializedInfo(); + other.parameters = cloneTypeMap(parameters, /* cloneTypeExpressionNodes= */ false); + other.suppressions = suppressions == null ? null : ImmutableSet.copyOf(suppressions); + return other; + } + protected ArrayList cloneTypeList( ArrayList list, boolean cloneTypeExpressionNodes) { ArrayList newlist = null; @@ -248,6 +262,31 @@ private static final class LazilyInitializedDocumentation implements Serializabl private List authors; private List sees; + + LazilyInitializedDocumentation cloneConstructorDoc() { + LazilyInitializedDocumentation other = new LazilyInitializedDocumentation(); + if (parameters != null) { + other.parameters = new LinkedHashMap<>(parameters); + } + return other; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("sourceComment", sourceComment) + .add("markers", markers) + .add("parameters", parameters) + .add("throwsDescriptions", throwsDescriptions) + .add("blockDescription", blockDescription) + .add("fileOverview", fileOverview) + .add("returnDescription", returnDescription) + .add("version", version) + .add("authors", authors) + .add("sees", sees) + .omitNullValues() + .toString(); + } } /** @@ -561,6 +600,40 @@ public JSDocInfo clone(boolean cloneTypeNodes) { return other; } + /** + * This is used to get all nodes + the description, excluding the param nodes. Used to help in an + * ES5 to ES6 class converter only. + */ + public JSDocInfo cloneClassDoc() { + JSDocInfo other = new JSDocInfo(); + other.info = this.info == null ? null : this.info.cloneClassDoc(); + other.documentation = this.documentation; + other.visibility = this.visibility; + other.bitset = this.bitset; + other.type = cloneType(this.type, false); + other.thisType = cloneType(this.thisType, false); + other.includeDocumentation = this.includeDocumentation; + other.originalCommentPosition = this.originalCommentPosition; + other.setConstructor(false); + other.setStruct(false); + if (!isInterface() && other.info != null) { + other.info.baseType = null; + } + return other; + } + + /** + * This is used to get only the parameter nodes. Used to help in an ES5 to ES6 converter class + * only. + */ + public JSDocInfo cloneConstructorDoc() { + JSDocInfo other = new JSDocInfo(); + other.info = this.info == null ? null : this.info.cloneConstructorDoc(); + other.documentation = + this.documentation == null ? null : this.documentation.cloneConstructorDoc(); + return other; + } + private static JSTypeExpression cloneType(JSTypeExpression expr, boolean cloneTypeNodes) { if (expr != null) { return cloneTypeNodes ? expr.copy() : expr; @@ -666,7 +739,11 @@ void setUnrestricted() { } void setStruct() { - setFlag(true, MASK_STRUCT); + setStruct(true); + } + + void setStruct(boolean value) { + setFlag(value, MASK_STRUCT); } void setDict() {