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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * 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; } } }