All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.gwt.client.JsfileParser Maven / Gradle / Ivy

/*
 * Copyright 2015 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.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multiset;
import com.google.common.collect.Ordering;
import com.google.common.collect.TreeMultiset;
import com.google.javascript.jscomp.BasicErrorManager;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.GatherModuleMetadata;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode;
import com.google.javascript.jscomp.gwt.client.Util.JsArray;
import com.google.javascript.jscomp.gwt.client.Util.JsObject;
import com.google.javascript.jscomp.gwt.client.Util.JsRegExp;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.jscomp.parsing.Config;
import com.google.javascript.jscomp.parsing.ParserRunner;
import com.google.javascript.jscomp.parsing.parser.trees.Comment;
import com.google.javascript.rhino.ErrorReporter;
import com.google.javascript.rhino.InputId;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.Nullable;
import jsinterop.annotations.JsFunction;
import jsinterop.annotations.JsMethod;

/**
 * GWT module to parse files for dependency and
 * {@literal @}{@code fileoverview} annotation
 * information.
 */
public class JsfileParser {

  /**
   * All the information parsed out of a single file.
   * Exported as a JSON object:
   * 
 {@code {
   *   "custom_annotations": {?Array<[string, string]>},  @.*
   *   "goog": {?bool},  whether 'goog' is implicitly required
   *   "has_soy_delcalls": {?Array},  @fileoverview @hassoydelcall {.*}
   *   "has_soy_deltemplates": {?Array},  @fileoverview @hassoydeltemplate {.*}
   *   "imported_modules": {?Array},  import ... from .*
   *   "is_config": {?bool},  @fileoverview @config
   *   "is_externs": {?bool},  @fileoverview @externs
   *   "load_flags": {?Array<[string, string]>},
   *   "mod_name": {?Array},  @fileoverview @modName .*, @modName {.*}
   *   "mods": {?Array},  @fileoverview @mods {.*}
   *   "provide_goog": {?bool},  @fileoverview @provideGoog
   *   "provides": {?Array},
   *   "requires": {?Array},  note: look for goog.* for 'goog'
   *   "requires_css": {?Array},  @fileoverview @requirecss {.*}
   *   "testonly": {?bool},  goog.setTestOnly
   *   "type_requires": {?Array},
   *   "visibility: {?Array},  @fileoverview @visibility {.*}
   * }}
* Any trivial values are omitted. */ static final class FileInfo { final ErrorReporter reporter; boolean goog = false; boolean isConfig = false; boolean isExterns = false; boolean provideGoog = false; boolean testonly = false; final Set hasSoyDelcalls = new TreeSet<>(); final Set hasSoyDeltemplates = new TreeSet<>(); // Use a LinkedHashSet as import order matters! final Set importedModules = new LinkedHashSet<>(); final List modName = new ArrayList<>(); final List mods = new ArrayList<>(); // Note: multiple copies doesn't make much sense, but we report // each copy so that calling code can choose how to handle it final Multiset provides = TreeMultiset.create(); final Multiset requires = TreeMultiset.create(); final Multiset typeRequires = TreeMultiset.create(); final Multiset requiresCss = TreeMultiset.create(); final Multiset visibility = TreeMultiset.create(); final Set> customAnnotations = assoc(); final Set> loadFlags = assoc(); FileInfo(ErrorReporter reporter) { this.reporter = reporter; } private void handleGoog() { if (provideGoog) { provides.add("goog"); } else if (goog) { requires.add("goog"); } } /** Exports the file info as a JSON object. */ JsObject full() { handleGoog(); return new SparseObject() .set("custom_annotations", customAnnotations) .set("goog", goog) .set("has_soy_delcalls", hasSoyDelcalls) .set("has_soy_deltemplates", hasSoyDeltemplates) .set("imported_modules", importedModules) .set("is_config", isConfig) .set("is_externs", isExterns) .set("load_flags", loadFlags) .set("modName", modName) .set("mods", mods) .set("provide_goog", provideGoog) .set("provides", provides) .set("requires", requires) .set("requiresCss", requiresCss) .set("testonly", testonly) .set("type_requires", typeRequires) .set("visibility", visibility) .object; } } /** * Exports the {@link #compile} method via JSNI. * *

This will be placed on {@code module.exports.gjd} or the global {@code jscomp.gjd}. */ public native void exportGjd() /*-{ var fn = $entry(@com.google.javascript.jscomp.gwt.client.JsfileParser::gjd(*)); if (typeof module !== 'undefined' && module.exports) { module.exports.gjd = fn; } }-*/; /** Represents a single JSDoc annotation, with an optional argument. */ private static class CommentAnnotation { /** Annotation name, e.g. "@fileoverview" or "@externs". */ final String name; /** * Annotation value: either the bare identifier immediately after the * annotation, or else string in braces. */ final String value; CommentAnnotation(String name, String value) { this.name = name; this.value = value; } /** Returns all the annotations in a given comment string. */ static List parse(String comment) { // TODO(sdh): This is reinventing a large part of JSDocInfoParser. We should // try to consolidate as much as possible. This requires several steps: // 1. Make all the annotations we look for first-class in JSDocInfo // 2. Support custom annotations (may already be done?) // 3. Fix up existing code so that all these annotations are in @fileoverview // 4. Change this code to simply inspect the script's JSDocInfo instead of re-parsing JsRegExp re = new JsRegExp( ANNOTATION_RE, "g"); JsRegExp.Match match; List out = new ArrayList<>(); while ((match = re.exec(comment)) != null) { boolean modName = match.get(OTHER_ANNOTATION_GROUP) == null; String name = modName ? "@modName" : match.get(OTHER_ANNOTATION_GROUP); String value = Strings.nullToEmpty(match.get(modName ? MODNAME_VALUE_GROUP : OTHER_VALUE_GROUP)); out.add(new CommentAnnotation(name, value)); } return out; } private static final String ANNOTATION_RE = Joiner.on("").join( // Don't match "@" in the middle of a word "(?:[^a-zA-Z0-9_$]|^)", "(?:", // Case 1: @modName with a single identifier and no braces "@modName[\\t\\v\\f ]*([^{\\t\\n\\v\\f\\r ][^\\t\\n\\v\\f\\r ]*)", "|", // Case 2: Everything else, with an optional brace-delimited argument "(@[a-zA-Z]+)(?:\\s*\\{\\s*([^}\\t\\n\\v\\f\\r ]+)\\s*\\})?", ")"); private static final int MODNAME_VALUE_GROUP = 1; private static final int OTHER_ANNOTATION_GROUP = 2; private static final int OTHER_VALUE_GROUP = 3; } /** Method exported to JS to parse a file for dependencies and annotations. */ @JsMethod(namespace = "jscomp") public static JsObject gjd(String code, String filename, @Nullable Reporter reporter) { return parse(code, filename, reporter).full(); } /** Internal implementation to produce the {@link FileInfo} object. */ private static FileInfo parse(String code, String filename, @Nullable Reporter reporter) { ErrorReporter errorReporter = new DelegatingReporter(reporter); Compiler compiler = new Compiler( new BasicErrorManager() { @Override public void println(CheckLevel level, JSError error) { if (level == CheckLevel.ERROR) { errorReporter.error( error.description, error.sourceName, error.getLineNumber(), error.getCharno()); } else if (level == CheckLevel.WARNING) { errorReporter.warning( error.description, error.sourceName, error.getLineNumber(), error.getCharno()); } } @Override protected void printSummary() {} }); SourceFile source = SourceFile.fromCode(filename, code); compiler.init( ImmutableList.of(), ImmutableList.of(source), new CompilerOptions()); Config config = ParserRunner.createConfig( // TODO(sdh): ES8 STRICT, with a non-strict fallback - then give warnings. Config.LanguageMode.ECMASCRIPT8, Config.JsDocParsing.INCLUDE_DESCRIPTIONS_NO_WHITESPACE, Config.RunMode.KEEP_GOING, /* extraAnnotationNames */ ImmutableSet.of(), /* parseInlineSourceMaps */ true, Config.StrictMode.SLOPPY); FileInfo info = new FileInfo(errorReporter); ParserRunner.ParseResult parsed = ParserRunner.parse(source, code, config, errorReporter); parsed.ast.setInputId(new InputId(filename)); String version = parsed.features.version(); if (!version.equals("es3")) { info.loadFlags.add(JsArray.of("lang", version)); } for (Comment comment : parsed.comments) { if (comment.type == Comment.Type.JSDOC) { parseComment(comment, info); } } GatherModuleMetadata gatherModuleMetadata = new GatherModuleMetadata( compiler, /* processCommonJsModules= */ false, ResolutionMode.BROWSER); gatherModuleMetadata.process(new Node(Token.ROOT), parsed.ast); compiler.generateReport(); ModuleMetadata module = Iterables.getOnlyElement( compiler.getModuleMetadataMap().getModulesByPath().values()); if (module.isEs6Module()) { info.loadFlags.add(JsArray.of("module", "es6")); } else if (module.isGoogModule()) { info.loadFlags.add(JsArray.of("module", "goog")); } info.goog = module.usesClosure(); // If something doesn't have an external dependency on Closure, then it does not have any // externally required files or symbols to provide. This is needed for bundles that contain // base.js as well as other files. These bundles should look like they do not require or provide // anything at all. if (module.usesClosure()) { info.provides.addAll(module.googNamespaces()); info.requires.addAll(module.requiredGoogNamespaces()); info.typeRequires.addAll(module.requiredTypes()); info.testonly = module.isTestOnly(); } info.importedModules.addAll(module.es6ImportSpecifiers().elementSet()); return info; } /** Mutates {@code info} with information from the given {@code comment}. */ private static void parseComment(Comment comment, FileInfo info) { boolean fileOverview = comment.value.contains("@fileoverview"); for (CommentAnnotation annotation : CommentAnnotation.parse(comment.value)) { switch (annotation.name) { case "@fileoverview": case "@author": case "@see": case "@link": break; case "@mods": if (!annotation.value.isEmpty()) { info.mods.add(annotation.value); } break; case "@visibility": if (!annotation.value.isEmpty()) { info.visibility.add(annotation.value); } break; case "@modName": if (!annotation.value.isEmpty()) { info.modName.add(annotation.value); } break; case "@config": info.isConfig = true; break; case "@provideGoog": info.provideGoog = true; break; case "@requirecss": if (!annotation.value.isEmpty()) { info.requiresCss.add(annotation.value); } break; case "@hassoydeltemplate": if (!annotation.value.isEmpty()) { info.hasSoyDeltemplates.add(annotation.value); } break; case "@hassoydelcall": if (!annotation.value.isEmpty()) { info.hasSoyDelcalls.add(annotation.value); } break; case "@externs": info.isExterns = true; break; case "@enhanceable": case "@pintomodule": info.customAnnotations.add( JsArray.of(annotation.name.substring(1), annotation.value)); break; case "@enhance": if (!annotation.value.isEmpty()) { info.customAnnotations.add( JsArray.of(annotation.name.substring(1), annotation.value)); } break; default: if (fileOverview) { info.customAnnotations.add( JsArray.of(annotation.name.substring(1), annotation.value)); } } } } /** JS function interface for reporting errors. */ @JsFunction public interface Reporter { void report(boolean fatal, String message, String sourceName, int line, int lineOffset); } private static final class DelegatingReporter implements ErrorReporter { final Reporter delegate; DelegatingReporter(Reporter delegate) { this.delegate = delegate != null ? delegate : NULL_REPORTER; } @Override public void warning(String message, String sourceName, int line, int lineOffset) { delegate.report(false, message, sourceName, line, lineOffset); } @Override public void error(String message, String sourceName, int line, int lineOffset) { delegate.report(true, message, sourceName, line, lineOffset); } } private static final Reporter NULL_REPORTER = new Reporter() { @Override public void report( boolean fatal, String message, String sourceName, int line, int lineOffset) {} }; /** Returns an associative multimap. */ private static Set> assoc() { return new TreeSet<>(Ordering.natural().lexicographical().onResultOf(JsArray::asList)); } /** Sparse object helper class: only adds non-trivial values. */ private static class SparseObject { final JsObject object = new JsObject<>(); SparseObject set(String key, Iterable iterable) { JsArray array = JsArray.copyOf(iterable); if (array.getLength() > 0) { object.set(key, array); } return this; } SparseObject set(String key, String value) { if (value != null && !value.isEmpty()) { object.set(key, value); } return this; } SparseObject set(String key, boolean value) { if (value) { object.set(key, value); } return this; } } }