/** * This pass walks the AST to create a Collection of 'new' nodes and 'goog.require' nodes. It * reconciles these Collections, creating a warning for each discrepancy. * * <p>The rules on when a warning is reported are: * * <ul> * <li>Type is referenced in code -> goog.require is required (missingRequires check fails if it's * not there) * <li>Type is referenced in an @extends or @implements -> goog.require is required * (missingRequires check fails if it's not there) * <li>Type is referenced in other JsDoc (@type etc) -> goog.require is optional (don't warn, * regardless of if it is there) * <li>Type is not referenced at all -> goog.require is forbidden (extraRequires check fails if it * is there) * </ul> */ class CheckRequiresForConstructors implements HotSwapCompilerPass, NodeTraversal.Callback { private final AbstractCompiler compiler; private final CodingConvention codingConvention; public static enum Mode { // Looking at a single file. Only a minimal set of externs are present. SINGLE_FILE, // Used during a normal compilation. The entire program + externs are available. FULL_COMPILE }; private final Mode mode; private final Set<String> providedNames = new HashSet<>(); private final Map<String, Node> requires = new HashMap<>(); // Only used in single-file mode. private final Set<String> closurizedNamespaces = new HashSet<>(); // Adding an entry to usages indicates that the name is used and should be required. private final Map<String, Node> usages = new HashMap<>(); // Adding an entry to weakUsages indicates that the name is used, but in a way which may not // require a goog.require, such as in a @type annotation. If the only usages of a name are // in weakUsages, don't give a missingRequire warning, nor an extraRequire warning. private final Map<String, Node> weakUsages = new HashMap<>(); static final DiagnosticType MISSING_REQUIRE_WARNING = DiagnosticType.disabled("JSC_MISSING_REQUIRE_WARNING", "missing require: ''{0}''"); // Essentially the same as MISSING_REQUIRE_WARNING except that if the user calls foo.bar.baz() // then we don't know whether they should require it as goog.require('foo.bar.baz') or as // goog.require('foo.bar'). So, warn but don't provide a suggested fix. static final DiagnosticType MISSING_REQUIRE_CALL_WARNING = DiagnosticType.disabled( "JSC_MISSING_REQUIRE_CALL_WARNING", "No matching require found for ''{0}''"); static final DiagnosticType EXTRA_REQUIRE_WARNING = DiagnosticType.disabled("JSC_EXTRA_REQUIRE_WARNING", "extra require: ''{0}''"); static final DiagnosticType DUPLICATE_REQUIRE_WARNING = DiagnosticType.disabled("JSC_DUPLICATE_REQUIRE_WARNING", "''{0}'' required more than once."); private static final Set<String> DEFAULT_EXTRA_NAMESPACES = ImmutableSet.of("goog.testing.asserts", "goog.testing.jsunit"); CheckRequiresForConstructors(AbstractCompiler compiler, Mode mode) { this.compiler = compiler; this.mode = mode; this.codingConvention = compiler.getCodingConvention(); } /** * Uses Collections of new and goog.require nodes to create a compiler warning for each new class * name without a corresponding goog.require(). */ @Override public void process(Node externs, Node root) { NodeTraversal.traverseRootsEs6(compiler, this, externs, root); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { NodeTraversal.traverseEs6(compiler, scriptRoot, this); } // Return true if the name is a class name (starts with an uppercase // character). This also matches for all-caps constants, which eliminates // some false positives (e.g. goog.LOCALE.replace()). private static boolean isClassName(String name) { return name != null && name.length() > 1 && Character.isUpperCase(name.charAt(0)); } // Return the shortest prefix of the className that refers to a class, // or null if no part refers to a class. private static String getOutermostClassName(String className) { for (String part : Splitter.on('.').split(className)) { if (isClassName(part)) { return className.substring(0, className.indexOf(part) + part.length()); } } return null; } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return parent == null || !parent.isScript() || !t.getInput().isExtern(); } @Override public void visit(NodeTraversal t, Node n, Node parent) { maybeAddJsDocUsages(t, n); switch (n.getType()) { case Token.ASSIGN: case Token.VAR: case Token.LET: case Token.CONST: maybeAddProvidedName(n); break; case Token.FUNCTION: // Exclude function expressions. if (NodeUtil.isStatement(n)) { maybeAddProvidedName(n); } break; case Token.NAME: if (!NodeUtil.isLValue(n)) { visitQualifiedName(n); } break; case Token.GETPROP: visitQualifiedName(n); break; case Token.CALL: visitCallNode(t, n, parent); break; case Token.SCRIPT: visitScriptNode(t); reset(); break; case Token.NEW: visitNewNode(t, n); break; case Token.CLASS: visitClassNode(t, n); break; case Token.IMPORT: visitImportNode(n); break; } } private void reset() { this.usages.clear(); this.weakUsages.clear(); this.requires.clear(); this.closurizedNamespaces.clear(); this.providedNames.clear(); } private void visitScriptNode(NodeTraversal t) { if (mode == Mode.SINGLE_FILE && requires.isEmpty()) { // Likely a file that isn't using Closure at all. return; } Set<String> namespaces = new HashSet<>(); // For every usage, check that there is a goog.require, and warn if not. for (Map.Entry<String, Node> entry : usages.entrySet()) { String namespace = entry.getKey(); if (namespace.endsWith(".call") || namespace.endsWith(".apply")) { namespace = namespace.substring(0, namespace.lastIndexOf('.')); } if (namespace.startsWith("goog.global.")) { continue; } Node node = entry.getValue(); JSDocInfo info = NodeUtil.getBestJSDocInfo(NodeUtil.getEnclosingStatement(node)); if (info != null && info.getSuppressions().contains("missingRequire")) { continue; } String outermostClassName = getOutermostClassName(namespace); // The parent namespace is also checked as part of the requires so that classes // used by goog.module are still checked properly. This may cause missing requires // to be missed but in practice that should happen rarely. String nonNullClassName = outermostClassName != null ? outermostClassName : namespace; String parentNamespace = null; int separatorIndex = nonNullClassName.lastIndexOf('.'); if (separatorIndex > 0) { parentNamespace = nonNullClassName.substring(0, separatorIndex); } boolean notProvidedByConstructors = !providedNames.contains(namespace) && !providedNames.contains(outermostClassName) && !providedNames.contains(parentNamespace); boolean notProvidedByRequires = !requires.containsKey(namespace) && !requires.containsKey(outermostClassName) && !requires.containsKey(parentNamespace); if (notProvidedByConstructors && notProvidedByRequires && !namespaces.contains(namespace) && !"goog".equals(parentNamespace)) { // TODO(mknichel): If the symbol is not explicitly provided, find the next best // symbol from the provides in the same file. String rootName = Splitter.on('.').split(namespace).iterator().next(); if (mode != Mode.SINGLE_FILE || closurizedNamespaces.contains(rootName)) { if (node.isCall()) { compiler.report(t.makeError(node, MISSING_REQUIRE_CALL_WARNING, namespace)); } else { compiler.report(t.makeError(node, MISSING_REQUIRE_WARNING, namespace)); } namespaces.add(namespace); } } } // For every goog.require, check that there is a usage (in either usages or weakUsages) // and warn if there is not. for (Map.Entry<String, Node> entry : requires.entrySet()) { String require = entry.getKey(); Node call = entry.getValue(); Node parent = call.getParent(); if (parent.isAssign()) { // var baz = goog.require('foo.bar.baz'); // Assume that the var 'baz' is used somewhere, and don't warn. continue; } if (!usages.containsKey(require) && !weakUsages.containsKey(require)) { reportExtraRequireWarning(call, require); } } } private void reportExtraRequireWarning(Node call, String require) { if (DEFAULT_EXTRA_NAMESPACES.contains(require)) { return; } JSDocInfo jsDoc = call.getJSDocInfo(); if (jsDoc != null && jsDoc.getSuppressions().contains("extraRequire")) { // There is a @suppress {extraRequire} on the call node. Even though the compiler generally // doesn't understand @suppress in that position, respect it in this case, // since lots of people put it there to suppress the closure-linter's extraRequire check. return; } compiler.report(JSError.make(call, EXTRA_REQUIRE_WARNING, require)); } private void reportDuplicateRequireWarning(Node call, String require) { compiler.report(JSError.make(call, DUPLICATE_REQUIRE_WARNING, require)); } private void visitRequire(String requiredName, Node node) { if (requires.containsKey(requiredName)) { reportDuplicateRequireWarning(node, requiredName); } else { requires.put(requiredName, node); if (mode == Mode.SINGLE_FILE) { String rootName = Splitter.on('.').split(requiredName).iterator().next(); closurizedNamespaces.add(rootName); } } } private void visitImportNode(Node importNode) { Node defaultImport = importNode.getFirstChild(); if (defaultImport.isName()) { visitRequire(defaultImport.getString(), importNode); } Node namedImports = defaultImport.getNext(); if (namedImports.getType() == Token.IMPORT_SPECS) { for (Node importSpec : namedImports.children()) { visitRequire(importSpec.getLastChild().getString(), importNode); } } } private void visitCallNode(NodeTraversal t, Node call, Node parent) { String required = codingConvention.extractClassNameIfRequire(call, parent); if (required != null) { visitRequire(required, call); return; } String provided = codingConvention.extractClassNameIfProvide(call, parent); if (provided != null) { providedNames.add(provided); return; } if (codingConvention.isClassFactoryCall(call)) { if (parent.isName()) { providedNames.add(parent.getString()); } else if (parent.isAssign()) { providedNames.add(parent.getFirstChild().getQualifiedName()); } } Node callee = call.getFirstChild(); if (callee.isName()) { weakUsages.put(callee.getString(), callee); } else if (callee.isQualifiedName()) { Node root = NodeUtil.getRootOfQualifiedName(callee); if (root.isName()) { Var var = t.getScope().getVar(root.getString()); if (var == null || (!var.isExtern() && !var.isLocal())) { String name = getOutermostClassName(callee.getQualifiedName()); if (name == null) { name = callee.getQualifiedName(); } usages.put(name, call); } } } } private void visitQualifiedName(Node getprop) { // For "foo.bar.baz.qux" add weak usages for "foo.bar.baz.qux", "foo.bar.baz", // "foo.bar", and "foo" because those might all be goog.provide'd in different files, // so it doesn't make sense to require the user to goog.require all of them. for (; getprop != null; getprop = getprop.getFirstChild()) { weakUsages.put(getprop.getQualifiedName(), getprop); } } private void visitNewNode(NodeTraversal t, Node newNode) { Node qNameNode = newNode.getFirstChild(); // Single names are likely external, but if this is running in single-file mode, they // will not be in the externs, so add a weak usage. if (mode == Mode.SINGLE_FILE && qNameNode.isName()) { weakUsages.put(qNameNode.getString(), qNameNode); return; } // If the ctor is something other than a qualified name, ignore it. if (!qNameNode.isQualifiedName()) { return; } // Grab the root ctor namespace. Node root = NodeUtil.getRootOfQualifiedName(qNameNode); // We only consider programmer-defined constructors that are // global variables, or are defined on global variables. if (!root.isName()) { return; } String name = root.getString(); Var var = t.getScope().getVar(name); if (var != null && (var.isExtern() || var.getSourceFile() == newNode.getStaticSourceFile())) { return; } usages.put(qNameNode.getQualifiedName(), newNode); // for "new foo.bar.Baz.Qux" add weak usages for "foo.bar.Baz", "foo.bar", and "foo" // because those might be goog.provide'd from a different file than foo.bar.Baz.Qux, // so it doesn't make sense to require the user to goog.require all of them. for (; qNameNode != null; qNameNode = qNameNode.getFirstChild()) { weakUsages.put(qNameNode.getQualifiedName(), qNameNode); } } private void visitClassNode(NodeTraversal t, Node classNode) { String name = NodeUtil.getName(classNode); if (name != null) { providedNames.add(name); } Node extendClass = classNode.getSecondChild(); // If the superclass is something other than a qualified name, ignore it. if (!extendClass.isQualifiedName()) { return; } // Single names are likely external, but if this is running in single-file mode, they // will not be in the externs, so add a weak usage. if (mode == Mode.SINGLE_FILE && extendClass.isName()) { weakUsages.put(extendClass.getString(), extendClass); return; } Node root = NodeUtil.getRootOfQualifiedName(extendClass); // It should always be a name. Extending this.something or // super.something is unlikely. // We only consider programmer-defined superclasses that are // global variables, or are defined on global variables. if (root.isName()) { String rootName = root.getString(); Var var = t.getScope().getVar(rootName); if (var != null && (var.isLocal() || var.isExtern())) { // "require" not needed for these } else { usages.put(extendClass.getQualifiedName(), extendClass); } } } private void maybeAddProvidedName(Node n) { Node name = n.getFirstChild(); if (name.isQualifiedName()) { providedNames.add(name.getQualifiedName()); } } /** * If this returns true, check for @extends and @implements annotations on this node. Otherwise, * it's probably an alias for an existing class, so skip those annotations. * * @return Whether the given node declares a function. True for the following forms: * <li> * <pre>function foo() {}</pre> * <li> * <pre>var foo = function() {};</pre> * <li> * <pre>foo.bar = function() {};</pre> */ private boolean declaresFunction(Node n) { if (n.isFunction()) { return true; } if (n.isAssign() && n.getLastChild().isFunction()) { return true; } if (NodeUtil.isNameDeclaration(n) && n.getFirstChild().hasChildren() && n.getFirstFirstChild().isFunction()) { return true; } return false; } private void maybeAddJsDocUsages(NodeTraversal t, Node n) { JSDocInfo info = n.getJSDocInfo(); if (info == null) { return; } if (declaresFunction(n)) { for (JSTypeExpression expr : info.getImplementedInterfaces()) { maybeAddUsage(t, n, expr); } if (info.getBaseType() != null) { maybeAddUsage(t, n, info.getBaseType()); } for (JSTypeExpression extendedInterface : info.getExtendedInterfaces()) { maybeAddUsage(t, n, extendedInterface); } } for (Node typeNode : info.getTypeNodes()) { maybeAddWeakUsage(t, n, typeNode); } } /** * Adds a weak usage for the given type expression (unless it references a variable that is * defined in the externs, in which case no goog.require() is needed). When a "weak usage" is * added, it means that a goog.require for that type is optional: No warning is given whether the * require is there or not. */ private void maybeAddWeakUsage(NodeTraversal t, Node n, Node typeNode) { maybeAddUsage(t, n, typeNode, this.weakUsages, Predicates.<Node>alwaysTrue()); } /** * Adds a usage for the given type expression (unless it references a variable that is defined in * the externs, in which case no goog.require() is needed). When a usage is added, it means that * there should be a goog.require for that type. */ private void maybeAddUsage(NodeTraversal t, Node n, final JSTypeExpression expr) { // Just look at the root node, don't traverse. Predicate<Node> pred = new Predicate<Node>() { @Override public boolean apply(Node n) { return n == expr.getRoot(); } }; maybeAddUsage(t, n, expr.getRoot(), this.usages, pred); } private void maybeAddUsage( final NodeTraversal t, final Node n, Node rootTypeNode, final Map<String, Node> usagesMap, Predicate<Node> pred) { Visitor visitor = new Visitor() { @Override public void visit(Node typeNode) { if (typeNode.isString()) { String typeString = typeNode.getString(); if (mode == Mode.SINGLE_FILE && !typeString.contains(".")) { // If using a single-name type, it's probably something like Error, which we // don't have externs for. weakUsages.put(typeString, n); return; } String rootName = Splitter.on('.').split(typeString).iterator().next(); Var var = t.getScope().getVar(rootName); if (var == null || !var.isExtern()) { usagesMap.put(typeString, n); // Regardless of whether we're adding a weak or strong usage here, add weak usages // for the prefixes of the namespace, like we do for GETPROP nodes. Otherwise we get // an extra require warning for cases like: // // goog.require('foo.bar.SomeService'); // // /** @constructor @extends {foo.bar.SomeService.Handler} */ // var MyHandler = function() {}; Node getprop = NodeUtil.newQName(compiler, typeString); getprop.useSourceInfoIfMissingFromForTree(typeNode); visitQualifiedName(getprop); } else { // Even if the root namespace is in externs, add a weak usage because the full // namespace may still be goog.provided. weakUsages.put(typeString, n); } } } }; NodeUtil.visitPreOrder(rootTypeNode, visitor, pred); } }
/** * Ensures string literals matching certain patterns are only used as goog.getCssName parameters. * * @author [email protected] (Martin Kretzschmar) */ @GwtIncompatible("java.util.regex") class CheckMissingGetCssName extends AbstractPostOrderCallback implements CompilerPass { private final AbstractCompiler compiler; private final CheckLevel level; private final Matcher blacklist; static final String GET_CSS_NAME_FUNCTION = "goog.getCssName"; static final String GET_UNIQUE_ID_FUNCTION = ".getUniqueId"; static final DiagnosticType MISSING_GETCSSNAME = DiagnosticType.disabled( "JSC_MISSING_GETCSSNAME", "missing goog.getCssName around literal ''{0}''"); CheckMissingGetCssName(AbstractCompiler compiler, CheckLevel level, String blacklistRegex) { this.compiler = compiler; this.level = level; this.blacklist = Pattern.compile("\\b(?:" + blacklistRegex + ")").matcher(""); } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, root, this); } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isString() && !parent.isGetProp() && !parent.isRegExp()) { String s = n.getString(); for (blacklist.reset(s); blacklist.find(); ) { if (parent.isTemplateLit()) { if (parent.getChildCount() > 1) { // Ignore template string with substitutions continue; } else { n = parent; } } if (insideGetCssNameCall(n)) { continue; } if (insideGetUniqueIdCall(n)) { continue; } if (insideAssignmentToIdConstant(n)) { continue; } compiler.report(t.makeError(n, level, MISSING_GETCSSNAME, blacklist.group())); } } } /** Returns whether the node is an argument of a goog.getCssName call. */ private static boolean insideGetCssNameCall(Node n) { Node parent = n.getParent(); return parent.isCall() && parent.getFirstChild().matchesQualifiedName(GET_CSS_NAME_FUNCTION); } /** * Returns whether the node is an argument of a function that returns a unique id (the last part * of the qualified name matches GET_UNIQUE_ID_FUNCTION). */ private static boolean insideGetUniqueIdCall(Node n) { Node parent = n.getParent(); String name = parent.isCall() ? parent.getFirstChild().getQualifiedName() : null; return name != null && name.endsWith(GET_UNIQUE_ID_FUNCTION); } /** * Returns whether the node is the right hand side of an assignment or initialization of a * variable named *_ID of *_ID_. */ private boolean insideAssignmentToIdConstant(Node n) { Node parent = n.getParent(); if (parent.isAssign()) { String qname = parent.getFirstChild().getQualifiedName(); return qname != null && isIdName(qname); } else if (parent.isName()) { Node grandParent = parent.getParent(); if (grandParent != null && NodeUtil.isNameDeclaration(grandParent)) { String name = parent.getString(); return isIdName(name); } else { return false; } } else { return false; } } private static boolean isIdName(String name) { return name.endsWith("ID") || name.endsWith("ID_"); } }
/** Insures '@constructor X' has a 'goog.provide("X")' . */ class CheckProvides implements HotSwapCompilerPass { private final AbstractCompiler compiler; private final CheckLevel checkLevel; private final CodingConvention codingConvention; static final DiagnosticType MISSING_PROVIDE_WARNING = DiagnosticType.disabled("JSC_MISSING_PROVIDE", "missing goog.provide(''{0}'')"); CheckProvides(AbstractCompiler compiler, CheckLevel checkLevel) { this.compiler = compiler; this.checkLevel = checkLevel; this.codingConvention = compiler.getCodingConvention(); } @Override public void process(Node externs, Node root) { hotSwapScript(root); } @Override public void hotSwapScript(Node scriptRoot) { CheckProvidesCallback callback = new CheckProvidesCallback(codingConvention); new NodeTraversal(compiler, callback).traverse(scriptRoot); } private class CheckProvidesCallback extends AbstractShallowCallback { private final Map<String, Node> provides = Maps.newHashMap(); private final Map<String, Node> ctors = Maps.newHashMap(); private final CodingConvention convention; CheckProvidesCallback(CodingConvention convention) { this.convention = convention; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getType()) { case Token.CALL: String providedClassName = codingConvention.extractClassNameIfProvide(n, parent); if (providedClassName != null) { provides.put(providedClassName, n); } break; case Token.FUNCTION: visitFunctionNode(n, parent); break; case Token.SCRIPT: visitScriptNode(t, n); } } private void visitFunctionNode(Node n, Node parent) { Node name = null; JSDocInfo info = parent.getJSDocInfo(); if (info != null && info.isConstructor()) { name = parent.getFirstChild(); } else { // look to the child, maybe it's a named function info = n.getJSDocInfo(); if (info != null && info.isConstructor()) { name = n.getFirstChild(); } } if (name != null && name.isQualifiedName()) { String qualifiedName = name.getQualifiedName(); if (!this.convention.isPrivate(qualifiedName)) { Visibility visibility = info.getVisibility(); if (!visibility.equals(JSDocInfo.Visibility.PRIVATE)) { ctors.put(qualifiedName, name); } } } } private void visitScriptNode(NodeTraversal t, Node n) { for (String ctorName : ctors.keySet()) { if (!provides.containsKey(ctorName)) { compiler.report( t.makeError(ctors.get(ctorName), checkLevel, MISSING_PROVIDE_WARNING, ctorName)); } } provides.clear(); ctors.clear(); } } }