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

com.google.javascript.jscomp.lint.CheckRequiresSorted 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.

There is a newer version: v20240317
Show newest version
/*
 * 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.lint;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.errorprone.annotations.Immutable;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.jspecify.nullness.Nullable;

/**
 * Checks that Closure import statements (goog.require, goog.requireType, and goog.forwardDeclare)
 * are sorted and deduplicated, exposing the necessary information to produce a suggested fix.
 */
public final class CheckRequiresSorted implements NodeTraversal.Callback {
  public static final DiagnosticType REQUIRES_NOT_SORTED =
      DiagnosticType.warning(
          "JSC_REQUIRES_NOT_SORTED",
          "goog.require() and goog.requireType() statements are not in recommended format."
              + " The correct order is:\n\n{0}\n");

  /** Operation modes. */
  public enum Mode {
    /** Collect information to determine whether a fix is required, but do not report a warning. */
    COLLECT_ONLY,
    /** Additionally report a warning. */
    COLLECT_AND_REPORT
  };

  /** Primitives that may be called in an import statement. */
  enum ImportPrimitive {
    REQUIRE("goog.require"),
    REQUIRE_TYPE("goog.requireType"),
    FORWARD_DECLARE("goog.forwardDeclare");

    private final String name;

    private ImportPrimitive(String name) {
      this.name = name;
    }

    /** Returns the primitive with the given name. */
    static ImportPrimitive fromName(String name) {
      for (ImportPrimitive primitive : values()) {
        if (primitive.name.equals(name)) {
          return primitive;
        }
      }
      throw new IllegalArgumentException("Invalid primitive name " + name);
    }

    static final ImportPrimitive WEAKEST = FORWARD_DECLARE;

    /**
     * Returns the stronger of two primitives.
     *
     * 

`goog.require` is stronger than `goog.requireType`, which is stronger than * `goog.forwardDeclare`. */ static @Nullable ImportPrimitive stronger(ImportPrimitive p1, ImportPrimitive p2) { return p1.ordinal() < p2.ordinal() ? p1 : p2; } @Override public String toString() { return name; } } /** * One of the bindings of a destructuring pattern. * *

{@code exportedName} and {@code localName} are equal in the case where the binding does not * explicitly specify a local name. */ @AutoValue @Immutable abstract static class DestructuringBinding implements Comparable { abstract String exportedName(); abstract String localName(); abstract boolean isShorthandProperty(); static DestructuringBinding of( String exportedName, String localName, boolean isShorthandProperty) { checkArgument(!isShorthandProperty || exportedName.equals(localName)); return new AutoValue_CheckRequiresSorted_DestructuringBinding( exportedName, localName, isShorthandProperty); } /** Compares two bindings according to the style guide sort order. */ @Override public int compareTo(DestructuringBinding other) { return ComparisonChain.start() .compare(this.exportedName(), other.exportedName()) .compare(this.localName(), other.localName()) .result(); } /** * Returns true if the destructuring binding is not canonical. * *

For example: *

  • `{Foo}` is canonical *
  • `{Foo: Bar}` is canonical *
  • `{Foo: Foo}` is not canonical */ private boolean isCanonical() { return !this.exportedName().equals(this.localName()) || this.isShorthandProperty(); } /** * Canonicalizes the destructuring to a shorthand property when applicable. * *

    In practice, `{Foo: Foo}` gets simplified to `{Foo}`. */ public DestructuringBinding canonicalizeShorthandProperties() { return this.isCanonical() ? this : DestructuringBinding.of( this.exportedName(), this.localName(), /* isShorthandProperty= */ true); } } /** * An import statement, which may have been merged from several import statements for the same * namespace in the original code. * *

    An import statement has exactly one of three shapes: * *

      *
    • Standalone: has no LHS, as in `goog.require('namespace')`. *
    • Aliasing: has an LHS with an alias, as in `const alias = goog.require('namespace')`. *
    • Destructuring: has an LHS with a destructuring pattern, as in `const {name: localName} = * goog.require('namespace')`. *
    */ @AutoValue abstract static class ImportStatement implements Comparable { /** Returns the nodes this import statement was merged from, in source order. */ abstract ImmutableList nodes(); /** Returns the import primitive being called. */ abstract ImportPrimitive primitive(); /** Returns the namespace being imported. */ abstract String namespace(); /** Returns the alias for an aliasing import, or null if the import isn't aliasing. */ abstract @Nullable String alias(); /** * Returns the destructures for a destructuring import in source order, or null if the import * isn't destructuring. * *

    If the import is destructuring but the pattern is empty, the value is non-null but empty. */ abstract @Nullable ImmutableList destructures(); /** Creates a new import statement. */ static ImportStatement of( ImmutableList nodes, ImportPrimitive primitive, String namespace, @Nullable String alias, @Nullable ImmutableList destructures) { Preconditions.checkArgument( alias == null || destructures == null, "Import statement cannot be simultaneously aliasing and destructuring"); return new AutoValue_CheckRequiresSorted_ImportStatement( nodes, primitive, namespace, alias, destructures); } /** Returns whether the import is standalone. */ boolean isStandalone() { return !isAliasing() && !isDestructuring(); } /** Returns whether the import is aliasing. */ boolean isAliasing() { return alias() != null; } /** Returns whether the import is destructuring. */ boolean isDestructuring() { return destructures() != null; } /** * Returns an import statement identical to the current one, except for its primitive, which is * upgraded to the given one if stronger. */ ImportStatement upgrade(ImportPrimitive otherPrimitive) { if (ImportPrimitive.stronger(primitive(), otherPrimitive) != primitive()) { return new AutoValue_CheckRequiresSorted_ImportStatement( nodes(), otherPrimitive, namespace(), alias(), destructures()); } return this; } private String formatWithoutDoc() { StringBuilder sb = new StringBuilder(); if (!isStandalone()) { sb.append("const "); } if (alias() != null) { sb.append(alias()); } if (destructures() != null) { sb.append("{"); boolean first = true; for (DestructuringBinding binding : destructures()) { String exportedName = binding.exportedName(); String localName = binding.localName(); if (first) { first = false; } else { sb.append(", "); } sb.append(exportedName); if (!exportedName.equals(localName)) { sb.append(": "); sb.append(localName); } } sb.append("}"); } if (!isStandalone()) { sb.append(" = "); } sb.append(primitive()); sb.append("('"); sb.append(namespace()); sb.append("');"); return sb.toString(); } /** Formats the import statement into code. */ public String format() { StringBuilder sb = new StringBuilder(); for (Node node : nodes()) { String comment = node.getNonJSDocCommentString(); if (!comment.isEmpty()) { sb.append(node.getNonJSDocCommentString()).append("\n"); } JSDocInfo jsDoc = NodeUtil.getBestJSDocInfo(node); if (jsDoc != null) { sb.append(jsDoc.getOriginalCommentString()).append("\n"); } } sb.append(formatWithoutDoc()); return sb.toString(); } /** Compares two import statements according to the style guide sort order. */ @Override public int compareTo(ImportStatement other) { return this.formatWithoutDoc().compareTo(other.formatWithoutDoc()); } } private static final QualifiedName GOOG_REQUIRE = QualifiedName.of("goog.require"); private static final QualifiedName GOOG_REQUIRETYPE = QualifiedName.of("goog.requireType"); private static final QualifiedName GOOG_FORWARDDECLARE = QualifiedName.of("goog.forwardDeclare"); private final Mode mode; // Maps each namespace into the existing import statements for that namespace. // Use an ArrayListMultimap so that values for a key are iterated in a deterministic order. private final Multimap importsByNamespace = ArrayListMultimap.create(); // The import statements in the order they appear. private final List originalImports = new ArrayList<>(); // The import statements in canonical order. private @Nullable Node firstNode = null; private @Nullable Node lastNode = null; private boolean finished = false; private boolean needsFix = false; private @Nullable String replacement = null; public CheckRequiresSorted(Mode mode) { this.mode = mode; } /** Returns the node for the first recognized import statement. */ public Node getFirstNode() { return firstNode; } /** Returns the node for the last recognized import statement. */ public Node getLastNode() { return lastNode; } /** Returns a textual replacement yielding a canonical version of the imports. */ public String getReplacement() { return replacement; } /** * Returns whether the imports need to be fixed, i.e., whether they are *not* already canonical. */ public boolean needsFix() { return needsFix; } @Override public final boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { // Traverse top-level statements until a block of contiguous requires is found. return !finished && (parent == null || parent.isRoot() || parent.isScript() || parent.isModuleBody()); } @Override public final void visit(NodeTraversal t, Node n, Node parent) { if (n.isScript()) { checkCanonical(t); return; } Node callNode = null; if (n.isExprResult()) { callNode = n.getFirstChild(); } else if (NodeUtil.isNameDeclaration(n)) { callNode = n.getFirstChild().getLastChild(); } if (callNode != null && isValidImportCall(callNode)) { ImportStatement stmt = parseImport(callNode); originalImports.add(stmt); importsByNamespace.put(stmt.namespace(), stmt); if (firstNode == null) { firstNode = lastNode = n; } else { lastNode = n; } } else if (!importsByNamespace.isEmpty()) { finished = true; } } private static boolean isValidImportCall(Node n) { return n.isCall() && n.hasTwoChildren() && (GOOG_REQUIRE.matches(n.getFirstChild()) || GOOG_REQUIRETYPE.matches(n.getFirstChild()) || GOOG_FORWARDDECLARE.matches(n.getFirstChild())) && n.getSecondChild().isStringLit(); } private static ImportStatement parseImport(Node callNode) { ImportPrimitive primitive = ImportPrimitive.fromName(callNode.getFirstChild().getQualifiedName()); String namespace = callNode.getSecondChild().getString(); Node parent = callNode.getParent(); if (parent.isExprResult()) { // goog.require('a'); return ImportStatement.of( ImmutableList.of(parent), primitive, namespace, /* alias= */ null, /* destructures= */ null); } Node grandparent = parent.getParent(); if (parent.isName()) { // const a = goog.require('a'); String alias = parent.getString(); return ImportStatement.of( ImmutableList.of(grandparent), primitive, namespace, alias, /* destructures= */ null); } // const {a: b, c} = goog.require('a'); ImmutableList.Builder destructures = ImmutableList.builder(); for (Node name = parent.getFirstFirstChild(); name != null; name = name.getNext()) { String exportedName = name.getString(); String localName = name.getFirstChild().getString(); // {a: a} and {a: b} both yield false // {a} yields true boolean isShorthandProperty = name.isShorthandProperty(); destructures.add(DestructuringBinding.of(exportedName, localName, isShorthandProperty)); } return ImportStatement.of( ImmutableList.of(grandparent), primitive, namespace, /* alias= */ null, destructures.build()); } private void checkCanonical(NodeTraversal t) { @Nullable List canonicalImports = canonicalizeImports(importsByNamespace); if (!originalImports.equals(canonicalImports)) { needsFix = true; replacement = String.join("\n", Iterables.transform(canonicalImports, ImportStatement::format)); if (mode == Mode.COLLECT_AND_REPORT) { t.report(firstNode, REQUIRES_NOT_SORTED, replacement); } } } /** * Canonicalizes a list of import statements by deduplicating and merging imports for the same * namespace, and sorting the result. */ private static List canonicalizeImports( Multimap importsByNamespace) { List canonicalImports = new ArrayList<>(); for (String namespace : importsByNamespace.keySet()) { Collection allImports = importsByNamespace.get(namespace); // Find the strongest primitive across all existing imports. Every emitted import for this // namespace will use this primitive. This makes the logic simpler and cannot change runtime // behavior, but may produce spurious changes when multiple aliasing imports of differing // strength exist (which are already in violation of the style guide). ImportPrimitive strongestPrimitive = allImports.stream() .map(ImportStatement::primitive) .reduce(ImportPrimitive.WEAKEST, ImportPrimitive::stronger); // Emit each aliasing import separately, as deduplicating them would require code references // to be rewritten. boolean hasAliasing = false; for (ImportStatement stmt : Iterables.filter(allImports, ImportStatement::isAliasing)) { canonicalImports.add(stmt.upgrade(strongestPrimitive)); hasAliasing = true; } // Emit a single destructuring import with a non-empty pattern, merged from the existing // destructuring imports. boolean hasDestructuring = false; ImmutableList destructuringNodes = allImports.stream() .filter(ImportStatement::isDestructuring) .flatMap(i -> i.nodes().stream()) .collect(toImmutableList()); ImmutableList destructures = allImports.stream() .filter(ImportStatement::isDestructuring) .flatMap(i -> i.destructures().stream()) .map(DestructuringBinding::canonicalizeShorthandProperties) .distinct() .sorted() .collect(toImmutableList()); if (!destructures.isEmpty()) { canonicalImports.add( ImportStatement.of( destructuringNodes, strongestPrimitive, namespace, /* alias= */ null, destructures)); hasDestructuring = true; } // Emit a standalone import unless an aliasing or destructuring one already exists. if (!hasAliasing && !hasDestructuring) { ImmutableList standaloneNodes = allImports.stream() .filter(ImportStatement::isStandalone) .flatMap(i -> i.nodes().stream()) .collect(toImmutableList()); canonicalImports.add( ImportStatement.of( standaloneNodes, strongestPrimitive, namespace, /* alias= */ null, /* destructures= */ null)); } } // Sorting by natural order yields the correct result due to the implementation of // ImportStatement#compareTo. Collections.sort(canonicalImports); return canonicalImports; } }





  • © 2015 - 2024 Weber Informatics LLC | Privacy Policy