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

com.google.javascript.jscomp.PolymerBehaviorExtractor Maven / Gradle / Ivy

/*
 * Copyright 2016 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;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import com.google.javascript.jscomp.GlobalNamespace.Name;
import com.google.javascript.jscomp.GlobalNamespace.Ref;
import com.google.javascript.jscomp.PolymerPass.MemberDefinition;
import com.google.javascript.jscomp.modules.Binding;
import com.google.javascript.jscomp.modules.Module;
import com.google.javascript.jscomp.modules.ModuleMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Finds the Polymer behavior definitions associated with Polymer element definitions.
 * @see https://www.polymer-project.org/1.0/docs/devguide/behaviors
 */
final class PolymerBehaviorExtractor {

  private static final ImmutableSet BEHAVIOR_NAMES_NOT_TO_COPY = ImmutableSet.of(
        "created", "attached", "detached", "attributeChanged", "configure", "ready",
        "properties", "listeners", "observers", "hostAttributes");

  private static final String GOOG_MODULE_EXPORTS = "exports";

  private final AbstractCompiler compiler;
  private final GlobalNamespace globalNames;
  private final ModuleMetadataMap moduleMetadataMap;
  private final ModuleMap moduleMap;

  private final Table resolveMemoized =
      HashBasedTable.create();
  private final Map globalResolveMemoized = new HashMap<>();

  PolymerBehaviorExtractor(
      AbstractCompiler compiler,
      GlobalNamespace globalNames,
      ModuleMetadataMap moduleMetadataMap,
      ModuleMap moduleMap) {
    this.compiler = compiler;
    this.globalNames = globalNames;
    this.moduleMetadataMap = moduleMetadataMap;
    this.moduleMap = moduleMap;
  }

  /**
   * Extracts all Behaviors from an array literal, recursively. Entries in the array can be object
   * literals or array literals (of other behaviors). Behavior names must be global, fully qualified
   * names.
   *
   * @see https://github.com/Polymer/polymer/blob/0.8-preview/PRIMER.md#behaviors
   * @param moduleMetadata The module in which these behaviors are being resolved, or null if not in
   *     a module.
   * @return A list of all {@code BehaviorDefinitions} in the array.
   */
  ImmutableList extractBehaviors(
      Node behaviorArray, @Nullable ModuleMetadata moduleMetadata) {
    if (behaviorArray == null) {
      return ImmutableList.of();
    }

    if (!behaviorArray.isArrayLit()) {
      compiler.report(
          JSError.make(behaviorArray, PolymerPassErrors.POLYMER_INVALID_BEHAVIOR_ARRAY));
      return ImmutableList.of();
    }

    ImmutableList.Builder behaviors = ImmutableList.builder();
    for (Node behaviorName : behaviorArray.children()) {
      if (behaviorName.isObjectLit()) {
        PolymerPassStaticUtils.switchDollarSignPropsToBrackets(behaviorName, compiler);
        PolymerPassStaticUtils.quoteListenerAndHostAttributeKeys(behaviorName, compiler);
        if (NodeUtil.getFirstPropMatchingKey(behaviorName, "is") != null) {
          compiler.report(JSError.make(behaviorName, PolymerPassErrors.POLYMER_INVALID_BEHAVIOR));
        }
        behaviors.add(
            new BehaviorDefinition(
                PolymerPassStaticUtils.extractProperties(
                    behaviorName,
                    PolymerClassDefinition.DefinitionType.ObjectLiteral,
                    compiler,
                    /* constructor= */ null),
                getBehaviorFunctionsToCopy(behaviorName),
                getNonPropertyMembersToCopy(behaviorName),
                /* isGlobalDeclaration= */ NodeUtil.getEnclosingScopeRoot(behaviorName).isRoot(),
                (FeatureSet) NodeUtil.getEnclosingScript(behaviorName).getProp(Node.FEATURE_SET)));
        continue;
      }

      ResolveBehaviorNameResult resolveResult;
      if (isGoogModuleGetCall(behaviorName)) {
        resolveResult = resolveGoogModuleGet(behaviorName.getSecondChild().getString());
      } else {
        resolveResult =
            resolveBehaviorName(getQualifiedNameThroughCast(behaviorName), moduleMetadata);
      }
      if (resolveResult.equals(FAILED_RESOLVE_RESULT)) {
        compiler.report(JSError.make(behaviorName, PolymerPassErrors.POLYMER_UNQUALIFIED_BEHAVIOR));
        continue;
      }
      Node behaviorValue = resolveResult.node;

      if (behaviorValue.isArrayLit()) {
        // Individual behaviors can also be arrays of behaviors. Parse them recursively.
        behaviors.addAll(extractBehaviors(behaviorValue, resolveResult.moduleMetadata));
      } else if (behaviorValue.isObjectLit()) {
        PolymerPassStaticUtils.switchDollarSignPropsToBrackets(behaviorValue, compiler);
        PolymerPassStaticUtils.quoteListenerAndHostAttributeKeys(behaviorValue, compiler);
        if (NodeUtil.getFirstPropMatchingKey(behaviorValue, "is") != null) {
          compiler.report(JSError.make(behaviorValue, PolymerPassErrors.POLYMER_INVALID_BEHAVIOR));
        }
        behaviors.add(
            new BehaviorDefinition(
                PolymerPassStaticUtils.extractProperties(
                    behaviorValue,
                    PolymerClassDefinition.DefinitionType.ObjectLiteral,
                    compiler,
                    /* constructor= */ null),
                getBehaviorFunctionsToCopy(behaviorValue),
                getNonPropertyMembersToCopy(behaviorValue),
                resolveResult.isGlobalDeclaration,
                (FeatureSet) NodeUtil.getEnclosingScript(behaviorValue).getProp(Node.FEATURE_SET)));
      } else {
        compiler.report(JSError.make(behaviorName, PolymerPassErrors.POLYMER_UNQUALIFIED_BEHAVIOR));
      }
    }

    return behaviors.build();
  }

  private static class ResolveBehaviorNameResult {
    final Node node;
    final boolean isGlobalDeclaration;
    final ModuleMetadata moduleMetadata;

    ResolveBehaviorNameResult(
        Node node, boolean isGlobalDeclaration, ModuleMetadata moduleMetadata) {
      this.node = node;
      this.isGlobalDeclaration = isGlobalDeclaration;
      this.moduleMetadata = moduleMetadata;
    }
  }

  private static final ResolveBehaviorNameResult FAILED_RESOLVE_RESULT =
      new ResolveBehaviorNameResult(null, false, null);

  /**
   * Resolve an identifier, which is presumed to refer to a Polymer Behavior declaration, using the
   * global namespace. Recurses to resolve assignment chains of any length.
   *
   * 

This method memoizes {@link #resolveBehaviorNameInternal(String, ModuleMetadata)} * * @param name the name of the identifier, which may be qualified. * @param moduleMetadata the module (ES module or goog.module) this name is resolved in, or null * if not in a module. * @return The behavior declaration node, or {@link #FAILED_RESOLVE_RESULT} if it couldn't be * resolved. */ private ResolveBehaviorNameResult resolveBehaviorName( @Nullable String name, ModuleMetadata moduleMetadata) { if (name == null) { return FAILED_RESOLVE_RESULT; } ResolveBehaviorNameResult memoized = moduleMetadata != null ? resolveMemoized.get(name, moduleMetadata) : globalResolveMemoized.get(name); if (memoized == null) { memoized = checkNotNull(resolveBehaviorNameInternal(name, moduleMetadata)); if (moduleMetadata != null) { resolveMemoized.put(name, moduleMetadata, memoized); } else { globalResolveMemoized.put(name, memoized); } } return memoized; } /** * Implements behavior resolution. Call {@link #resolveBehaviorName(String, ModuleMetadata)}} * instead. */ private ResolveBehaviorNameResult resolveBehaviorNameInternal( String name, ModuleMetadata moduleMetadata) { // Check if this name is a module import/require. ResolveBehaviorNameResult result = getNameIfModuleImport(name, moduleMetadata); if (result != null) { return result; } // Check if this name is possibly from a legacy goog.module ResolveBehaviorNameResult legacyResolve = resolveReferenceToLegacyGoogModule(name); if (legacyResolve != null) { return legacyResolve; } // If not, look it up within the current module. Name moduleLevelName = moduleMetadata != null ? globalNames.getNameFromModule(moduleMetadata, name) : null; Name globalName = moduleLevelName == null ? globalNames.getSlot(name) : moduleLevelName; if (globalName == null) { return FAILED_RESOLVE_RESULT; } // Whether the declaration of this node is in the top-level global scope, as opposed to a module // or an IIFE. boolean isGlobalDeclaration = moduleLevelName == null; // Use any set as a backup declaration, even if it's local. Ref declarationRef = globalName.getDeclaration(); if (declarationRef == null) { for (Ref ref : globalName.getRefs()) { if (ref.isSet()) { isGlobalDeclaration = false; declarationRef = ref; break; } } } if (declarationRef == null) { return FAILED_RESOLVE_RESULT; } Node declarationNode = declarationRef.getNode(); if (declarationNode == null) { return FAILED_RESOLVE_RESULT; } Node rValue = NodeUtil.getRValueOfLValue(declarationNode); if (rValue == null) { return FAILED_RESOLVE_RESULT; } if (rValue.isQualifiedName()) { // Another identifier; recurse. Scope declarationScope = declarationRef.scope.getClosestHoistScope(); Module m = ModuleImportResolver.getModuleFromScopeRoot( compiler.getModuleMap(), compiler, declarationScope.getRootNode()); return resolveBehaviorName( getQualifiedNameThroughCast(rValue), m != null ? m.metadata() : null); } JSDocInfo behaviorInfo = NodeUtil.getBestJSDocInfo(declarationNode); if (behaviorInfo == null || !behaviorInfo.isPolymerBehavior()) { compiler.report( JSError.make(declarationNode, PolymerPassErrors.POLYMER_UNANNOTATED_BEHAVIOR)); } return new ResolveBehaviorNameResult(rValue, isGlobalDeclaration, moduleMetadata); } /** * Handles resolving behaviors if they are references to legacy modules * *

Returns null if the name is not from a legacy module, and resolution should continue * normally. */ private ResolveBehaviorNameResult resolveReferenceToLegacyGoogModule(String name) { int dot = name.length(); while (dot >= 0) { String subNamespace = name.substring(0, dot); ModuleMetadata metadata = moduleMetadataMap.getModulesByGoogNamespace().get(subNamespace); if (metadata == null || !metadata.isLegacyGoogModule()) { dot = name.lastIndexOf('.', dot - 1); continue; } String rest = dot == name.length() ? "" : name.substring(dot); ResolveBehaviorNameResult result = resolveBehaviorName(GOOG_MODULE_EXPORTS + rest, metadata); // TODO(lharker): Remove this check and just fail to resolve once we have moved module // rewriting unconditionally after the PolymerPass. return result.equals(FAILED_RESOLVE_RESULT) ? null : result; } return null; } private static final QualifiedName GOOG_MODULE_GET = QualifiedName.of("goog.module.get"); private static boolean isGoogModuleGetCall(Node callNode) { if (!callNode.isCall()) { return false; } return GOOG_MODULE_GET.matches(callNode.getFirstChild()) && callNode.hasTwoChildren() && callNode.getSecondChild().isString(); } private ResolveBehaviorNameResult resolveGoogModuleGet(String moduleNamespace) { ModuleMetadata closureModule = moduleMetadataMap.getModulesByGoogNamespace().get(moduleNamespace); if (closureModule == null) { // Invalid goog.module.get() call. return FAILED_RESOLVE_RESULT; } else if (closureModule.isGoogProvide()) { return resolveBehaviorName(moduleNamespace, null); } checkState(closureModule.isGoogModule(), closureModule); return resolveBehaviorName(GOOG_MODULE_EXPORTS, closureModule); } /** * Handles resolving behaviors whose root is imported from another module or a provide. * *

Returns null if the given name is not imported or {@link #FAILED_RESOLVE_RESULT} if it is * imported but is not annotated @polymerBehavior. */ private ResolveBehaviorNameResult getNameIfModuleImport(String name, ModuleMetadata metadata) { if (metadata == null || (!metadata.isEs6Module() && !metadata.isGoogModule())) { return null; } Module module = metadata.isGoogModule() ? moduleMap.getClosureModule(metadata.googNamespaces().asList().get(0)) : moduleMap.getModule(metadata.path()); checkNotNull(module, metadata); int dot = name.indexOf('.'); String root = dot == -1 ? name : name.substring(0, dot); Binding b = module.boundNames().get(root); if (b == null || !b.isSomeImport()) { return null; } String rest = dot == -1 ? "" : name.substring(dot); if (b.isModuleNamespace()) { // `import * as x from '';` or `const ns = goog.require('...` return resolveModuleNamespaceBinding(b, rest); } ModuleMetadata importMetadata = b.originatingExport().moduleMetadata(); String originatingName; // The name in the module being imported if (importMetadata.isEs6Module()) { // import {exportName} from './mod'; originatingName = b.originatingExport().localName() + rest; } else if (importMetadata.isGoogModule()) { // `const {exportName: localName} = goog.require('some.module');` originatingName = GOOG_MODULE_EXPORTS + "." + b.originatingExport().exportName() + rest; } else { // `const {exportName: localName} = goog.require('some.provide');` checkState(importMetadata.isGoogProvide(), importMetadata); originatingName = b.closureNamespace() + "." + b.originatingExport().exportName() + rest; } return resolveBehaviorName(originatingName, importMetadata); } /** Resolves a name that imports the 'namespace' of a module or provide. */ private ResolveBehaviorNameResult resolveModuleNamespaceBinding(Binding b, String rest) { if (b.metadata().isGoogModule()) { return resolveBehaviorName(GOOG_MODULE_EXPORTS + rest, b.metadata()); } else if (b.metadata().isGoogProvide()) { return resolveBehaviorName(b.closureNamespace() + rest, b.metadata()); } // ES module import *. checkState(b.metadata().isEs6Module()); if (rest.isEmpty()) { // The namespace imported by `import *` is never a @polymerBehavior. return FAILED_RESOLVE_RESULT; } rest = rest.substring(1); // Remove leading '.'. int dot = rest.indexOf('.'); // Given: // `const internalName = 0; export {internalName as exportName};` // `import * as mod from './x'; use(mod.exportName.Behavior);` // 1. get the internal name `internalName` from` exportName`. // 2. then proceed to resolve `internalName.Behavior` in './x'. String exportedName = dot == -1 ? rest : rest.substring(0, dot); Module originalModule = moduleMap.getModule(b.metadata().path()); Binding exportBinding = originalModule.namespace().get(exportedName); if (exportBinding == null || !exportBinding.isCreatedByEsExport()) { // This is an invalid import, and will cause an error elsewhere. return FAILED_RESOLVE_RESULT; } return resolveBehaviorName( exportBinding.originatingExport().localName() + (dot == -1 ? "" : rest.substring(dot)), b.metadata()); } /** * @return A list of functions from a behavior which should be copied to the element prototype. */ private static ImmutableList getBehaviorFunctionsToCopy(Node behaviorObjLit) { checkState(behaviorObjLit.isObjectLit()); ImmutableList.Builder functionsToCopy = ImmutableList.builder(); for (Node keyNode : behaviorObjLit.children()) { boolean isFunctionDefinition = (keyNode.isStringKey() && keyNode.getFirstChild().isFunction()) || keyNode.isMemberFunctionDef(); if (isFunctionDefinition && !BEHAVIOR_NAMES_NOT_TO_COPY.contains(keyNode.getString())) { functionsToCopy.add(new MemberDefinition(NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.getFirstChild())); } } return functionsToCopy.build(); } /** * Similar to {@link Node#getQualifiedName} but also handles CAST nodes. For example, given a * GETPROP representing "(/** @type {?} *\/ (x)).y.z" returns "x.y.z". Returns null if node is * not a NAME, GETPROP, or CAST. See b/64389806 for Polymer-specific context. */ @Nullable private static String getQualifiedNameThroughCast(Node node) { if (node.isName()) { String name = node.getString(); return name.isEmpty() ? null : name; } else if (node.isGetProp()) { String left = getQualifiedNameThroughCast(node.getFirstChild()); if (left == null) { return null; } String right = node.getLastChild().getString(); return left + "." + right; } else if (node.isCast()) { return getQualifiedNameThroughCast(node.getFirstChild()); } return null; } /** * @return A list of MemberDefinitions in a behavior which are not in the properties block, but * should still be copied to the element prototype. */ private static ImmutableList getNonPropertyMembersToCopy(Node behaviorObjLit) { checkState(behaviorObjLit.isObjectLit()); ImmutableList.Builder membersToCopy = ImmutableList.builder(); for (Node keyNode : behaviorObjLit.children()) { boolean isNonFunctionMember = keyNode.isGetterDef() || (keyNode.isStringKey() && !keyNode.getFirstChild().isFunction()); if (isNonFunctionMember && !BEHAVIOR_NAMES_NOT_TO_COPY.contains(keyNode.getString())) { membersToCopy.add(new MemberDefinition(NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.getFirstChild())); } } return membersToCopy.build(); } /** * Parsed definition of a Polymer Behavior. Includes members which should be copied to elements * which use the behavior. */ static final class BehaviorDefinition { /** * Properties declared in the behavior 'properties' block. */ final List props; /** * Functions intended to be copied to elements which use this Behavior. */ final List functionsToCopy; /** * Other members intended to be copied to elements which use this Behavior. */ final List nonPropertyMembersToCopy; /** * Whether this Behavior is declared in the global scope. */ final boolean isGlobalDeclaration; /** * Language features to carry over to the extraction destination. */ final FeatureSet features; BehaviorDefinition( List props, List functionsToCopy, List nonPropertyMembersToCopy, boolean isGlobalDeclaration, FeatureSet features) { this.props = props; this.functionsToCopy = functionsToCopy; this.nonPropertyMembersToCopy = nonPropertyMembersToCopy; this.isGlobalDeclaration = isGlobalDeclaration; this.features = features; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy