Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
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 static com.google.common.base.Preconditions.checkState;
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.Multiset;
import com.google.common.collect.Ordering;
import com.google.common.collect.TreeMultiset;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.Var;
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.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 java.util.ArrayList;
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<>();
final Set importedModules = new TreeSet<>();
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() /*-{
if (typeof module !== 'undefined' && module.exports) {
module.exports.gjd = jscomp.gjd;
}
}-*/;
/** 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();
compiler.init(
ImmutableList.of(), ImmutableList.of(), 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);
SourceFile source = SourceFile.fromCode(filename, code);
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);
}
}
NodeTraversal.traverse(compiler, parsed.ast, new Traverser(info));
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));
}
}
}
}
/** Traverser that mutates {@code #info} with information from the AST. */
private static class Traverser extends AbstractPostOrderCallback {
final FileInfo info;
Traverser(FileInfo info) {
this.info = info;
}
private boolean isFromGoogImport(Var goog) {
Node nameNode = goog.getNameNode();
// Because other tools are regex based we force importing this file as "import * as goog".
return nameNode != null
&& nameNode.isImportStar()
&& nameNode.getString().equals("goog")
&& nameNode.getParent().getFirstChild().isEmpty()
&& nameNode.getParent().getLastChild().getString().endsWith("/goog.js");
}
@Override
public void visit(NodeTraversal traversal, Node node, Node parent) {
// Look for goog.* calls
if (node.isGetProp() && node.getFirstChild().isName()
&& node.getFirstChild().getString().equals("goog")) {
Var root = traversal.getScope().getVar("goog");
if (root == null || isFromGoogImport(root)) {
info.goog = true;
if (parent.isCall() && parent.getChildCount() < 3) {
Node arg;
switch (node.getLastChild().getString()) {
case "module":
info.loadFlags.add(JsArray.of("module", "goog"));
// fall through
case "provide":
arg = parent.getSecondChild();
if (arg.isString()) {
info.provides.add(arg.getString());
} // TODO(sdh): else warning?
break;
case "require":
arg = parent.getSecondChild();
if (arg.isString()) {
info.requires.add(arg.getString());
} // TODO(sdh): else warning?
break;
case "requireType":
arg = parent.getSecondChild();
if (arg.isString()) {
info.typeRequires.add(arg.getString());
} // TODO(blickly): else warning?
break;
case "setTestOnly":
info.testonly = true;
break;
default:
// Do nothing
}
} else if (parent.isGetProp()
&& parent.matchesQualifiedName("goog.module.declareNamespace")
&& parent.getParent().isCall()) {
Node arg = parent.getParent().getSecondChild();
if (arg.isString()) {
info.provides.add(arg.getString());
} // TODO(johnplaisted): else warning?
}
}
}
// Look for ES6 import statements
if (node.isImport()) {
Node moduleSpecifier = node.getChildAtIndex(2);
// NOTE: previous tool was more forgiving here.
checkState(moduleSpecifier.isString());
info.loadFlags.add(JsArray.of("module", "es6"));
info.importedModules.add(moduleSpecifier.getString());
} else if (node.isExport()) {
info.loadFlags.add(JsArray.of("module", "es6"));
// export from
if (node.hasTwoChildren() && node.getLastChild().isString()) {
info.importedModules.add(node.getLastChild().getString());
}
}
}
}
/** 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;
}
}
}