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

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

The newest version!
/*
 * 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.javascript.jscomp.PolymerBehaviorExtractor.BehaviorDefinition;
import com.google.javascript.jscomp.PolymerPass.MemberDefinition;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Rewrites a given call to Polymer({}) to a set of declarations and assignments which can be
 * understood by the compiler.
 */
final class PolymerClassRewriter {
  private static final String VIRTUAL_FILE = "";
  private final AbstractCompiler compiler;
  private final int polymerVersion;
  private final PolymerExportPolicy polymerExportPolicy;
  private final boolean propertyRenamingEnabled;
  @VisibleForTesting static final String POLYMER_ELEMENT_PROP_CONFIG = "PolymerElementProperties";

  private final Node polymerElementExterns;
  boolean propertySinkExternInjected = false;

  PolymerClassRewriter(
      AbstractCompiler compiler,
      Node polymerElementExterns,
      int polymerVersion,
      PolymerExportPolicy polymerExportPolicy,
      boolean propertyRenamingEnabled) {
    this.compiler = compiler;
    this.polymerElementExterns = polymerElementExterns;
    this.polymerVersion = polymerVersion;
    this.polymerExportPolicy = polymerExportPolicy;
    this.propertyRenamingEnabled = propertyRenamingEnabled;
  }

  /**
   * Rewrites a given call to Polymer({}) to a set of declarations and assignments which can be
   * understood by the compiler.
   *
   * @param exprRoot The root expression of the call to Polymer({}).
   * @param cls The extracted {@link PolymerClassDefinition} for the Polymer element created by this
   *     call.
   */
  void rewritePolymerCall(
      Node exprRoot, final PolymerClassDefinition cls, boolean isInGlobalScope) {
    Node objLit = checkNotNull(cls.descriptor);

    // Add {@code @lends} to the object literal.
    JSDocInfoBuilder objLitDoc = new JSDocInfoBuilder(true);
    JSTypeExpression jsTypeExpression =
        new JSTypeExpression(
            IR.string(cls.target.getQualifiedName() + ".prototype"), exprRoot.getSourceFileName());
    objLitDoc.recordLends(jsTypeExpression);
    objLit.setJSDocInfo(objLitDoc.build());

    addTypesToFunctions(objLit, cls.target.getQualifiedName(), cls.defType);
    PolymerPassStaticUtils.switchDollarSignPropsToBrackets(objLit, compiler);
    PolymerPassStaticUtils.quoteListenerAndHostAttributeKeys(objLit, compiler);

    for (MemberDefinition prop : cls.props) {
      if (prop.value.isObjectLit()) {
        PolymerPassStaticUtils.switchDollarSignPropsToBrackets(prop.value, compiler);
      }
    }

    // For simplicity add everything into a block, before adding it to the AST.
    Node block = IR.block();

    JSDocInfoBuilder constructorDoc = this.getConstructorDoc(cls);

    // Remove the original constructor JS docs from the objlit.
    Node ctorKey = cls.constructor.value.getParent();
    if (ctorKey != null) {
      ctorKey.removeProp(Node.JSDOC_INFO_PROP);
    }

    if (cls.target.isGetProp()) {
      // foo.bar = Polymer({...});
      Node assign = IR.assign(cls.target.cloneTree(), cls.constructor.value.cloneTree());
      NodeUtil.markNewScopesChanged(assign, compiler);
      assign.setJSDocInfo(constructorDoc.build());
      Node exprResult = IR.exprResult(assign);
      exprResult.useSourceInfoIfMissingFromForTree(cls.target);
      block.addChildToBack(exprResult);
    } else {
      // var foo = Polymer({...}); OR Polymer({...});
      Node var = IR.var(cls.target.cloneTree(), cls.constructor.value.cloneTree());
      NodeUtil.markNewScopesChanged(var, compiler);
      var.useSourceInfoIfMissingFromForTree(exprRoot);
      var.setJSDocInfo(constructorDoc.build());
      block.addChildToBack(var);
    }

    appendPropertiesToBlock(cls.props, block, cls.target.getQualifiedName() + ".prototype.");
    appendBehaviorMembersToBlock(cls, block);
    ImmutableList readOnlyProps = parseReadOnlyProperties(cls, block);
    ImmutableList attributeReflectedProps =
        parseAttributeReflectedProperties(cls);
    createExportsAndExterns(cls, readOnlyProps, attributeReflectedProps);
    removePropertyDocs(objLit, PolymerClassDefinition.DefinitionType.ObjectLiteral);

    Node statements = block.removeChildren();
    Node parent = exprRoot.getParent();

    // If the call to Polymer() is not in the global scope and the assignment target
    // is not namespaced (which likely means it's exported to the global scope), put the type
    // declaration into the global scope at the start of the current script.
    //
    // This avoids unknown type warnings which are a result of the compiler's poor understanding of
    // types declared inside IIFEs or any non-global scope. We should revisit this decision as
    // the typechecker's support for non-global types improves.
    if (!isInGlobalScope && !cls.target.isGetProp()) {
      Node scriptNode = NodeUtil.getEnclosingScript(parent);
      scriptNode.addChildrenToFront(statements);
      compiler.reportChangeToChangeScope(scriptNode);
    } else {
      Node beforeRoot = exprRoot.getPrevious();
      if (beforeRoot == null) {
        parent.addChildrenToFront(statements);
      } else {
        parent.addChildrenAfter(statements, beforeRoot);
      }
      compiler.reportChangeToEnclosingScope(parent);
    }
    compiler.reportChangeToEnclosingScope(statements);

    // Since behavior files might contain language features that aren't present in the class file,
    // we might need to update the FeatureSet.
    if (cls.features != null) {
      Node scriptNode = NodeUtil.getEnclosingScript(parent);
      FeatureSet oldFeatures = (FeatureSet) scriptNode.getProp(Node.FEATURE_SET);
      FeatureSet newFeatures = oldFeatures.union(cls.features);
      if (!newFeatures.equals(oldFeatures)) {
        scriptNode.putProp(Node.FEATURE_SET, newFeatures);
        compiler.reportChangeToChangeScope(scriptNode);
      }
    }

    if (NodeUtil.isNameDeclaration(exprRoot)) {
      Node assignExpr = varToAssign(exprRoot);
      parent.replaceChild(exprRoot, assignExpr);
      compiler.reportChangeToEnclosingScope(assignExpr);
    }

    // If property renaming is enabled, wrap the properties object literal
    // in a reflection call so that the properties are renamed consistently
    // with the class members.
    if (polymerVersion > 1 && propertyRenamingEnabled && cls.descriptor != null) {
      Node props = NodeUtil.getFirstPropMatchingKey(cls.descriptor, "properties");
      if (props != null && props.isObjectLit()) {
        addPropertiesConfigObjectReflection(cls, props);
      }
    }
  }

  /**
   * Rewrites a class which extends Polymer.Element to a set of declarations and assignments which
   * can be understood by the compiler.
   *
   * @param clazz The class node
   * @param cls The extracted {@link PolymerClassDefinition} for the Polymer element created by this
   *     call.
   */
  void rewritePolymerClassDeclaration(
      Node clazz, NodeTraversal traversal, final PolymerClassDefinition cls) {

    if (cls.descriptor != null) {
      addTypesToFunctions(cls.descriptor, cls.target.getQualifiedName(), cls.defType);
    }
    PolymerPassStaticUtils.switchDollarSignPropsToBrackets(
        NodeUtil.getClassMembers(clazz), compiler);

    for (MemberDefinition prop : cls.props) {
      if (prop.value.isObjectLit()) {
        PolymerPassStaticUtils.switchDollarSignPropsToBrackets(prop.value, compiler);
      }
    }

    // For simplicity add everything into a block, before adding it to the AST.
    Node block = IR.block();

    // For each Polymer property we found in the "properties" configuration object, append a
    // property declaration to the prototype (e.g. "/** @type {string} */ MyElement.prototype.foo").
    appendPropertiesToBlock(cls.props, block, cls.target.getQualifiedName() + ".prototype.");

    ImmutableList readOnlyProps = parseReadOnlyProperties(cls, block);
    ImmutableList attributeReflectedProps =
        parseAttributeReflectedProperties(cls);
    createExportsAndExterns(cls, readOnlyProps, attributeReflectedProps);

    // If an external interface is required, mark the class as implementing it
    if (polymerExportPolicy == PolymerExportPolicy.EXPORT_ALL
        || !readOnlyProps.isEmpty()
        || !attributeReflectedProps.isEmpty()) {
      Node jsDocInfoNode = NodeUtil.getBestJSDocInfoNode(clazz);
      JSDocInfoBuilder classInfo = JSDocInfoBuilder.maybeCopyFrom(jsDocInfoNode.getJSDocInfo());
      String interfaceName = getInterfaceName(cls);
      JSTypeExpression interfaceType =
          new JSTypeExpression(new Node(Token.BANG, IR.string(interfaceName)), VIRTUAL_FILE);
      classInfo.recordImplementedInterface(interfaceType);
      jsDocInfoNode.setJSDocInfo(classInfo.build());
    }

    Node insertAfterReference = NodeUtil.getEnclosingStatement(clazz);
    if (block.hasChildren()) {
      removePropertyDocs(cls.descriptor, cls.defType);
      Node newInsertAfterReference = block.getLastChild();
      insertAfterReference
          .getParent()
          .addChildrenAfter(block.removeChildren(), insertAfterReference);
      compiler.reportChangeToEnclosingScope(insertAfterReference);
      insertAfterReference = newInsertAfterReference;
    }

    addReturnTypeIfMissing(cls, "is", new JSTypeExpression(IR.string("string"), VIRTUAL_FILE));

    Node type = new Node(Token.BANG);
    Node array = IR.string("Array");
    type.addChildToBack(array);
    Node arrayTemplateType = new Node(Token.BLOCK, IR.string("string"));
    array.addChildToBack(arrayTemplateType);
    addReturnTypeIfMissing(cls, "observers", new JSTypeExpression(type, VIRTUAL_FILE));
    addReturnTypeIfMissing(
        cls,
        "properties",
        new JSTypeExpression(IR.string(POLYMER_ELEMENT_PROP_CONFIG), VIRTUAL_FILE));

    // If property renaming is enabled, wrap the properties object literal
    // in a reflection call so that the properties are renamed consistently
    // with the class members.
    //
    // Also add reflection and sinks for computed properties and complex observers
    // and switch simple observers to direct function references.
    if (propertyRenamingEnabled && cls.descriptor != null) {
      convertSimpleObserverStringsToReferences(cls);
      List propertySinks = new ArrayList<>();
      if (polymerExportPolicy != PolymerExportPolicy.EXPORT_ALL) {
        propertySinks.addAll(addComputedPropertiesReflectionCalls(cls));
        propertySinks.addAll(addComplexObserverReflectionCalls(cls));
      }

      if (propertySinks.size() > 0) {
        if (!propertySinkExternInjected
            && traversal.getScope().getVar(CheckSideEffects.PROTECTOR_FN) == null) {
          CheckSideEffects.addExtern(compiler);
          propertySinkExternInjected = true;
        }

        for (Node propertyRef : propertySinks) {
          Node name = IR.name(CheckSideEffects.PROTECTOR_FN).srcref(propertyRef);
          name.putBooleanProp(Node.IS_CONSTANT_NAME, true);
          Node protectorCall = IR.call(name, propertyRef).srcref(propertyRef);
          protectorCall.putBooleanProp(Node.FREE_CALL, true);
          protectorCall = IR.exprResult(protectorCall).useSourceInfoFrom(propertyRef);
          insertAfterReference.getParent().addChildAfter(protectorCall, insertAfterReference);
          insertAfterReference = protectorCall;
        }

        compiler.reportChangeToEnclosingScope(insertAfterReference);
      }

      addPropertiesConfigObjectReflection(cls, cls.descriptor);
    }
  }

  /** Adds return type information to class getters */
  private void addReturnTypeIfMissing(
      PolymerClassDefinition cls, String getterPropName, JSTypeExpression jsType) {
    Node classMembers = NodeUtil.getClassMembers(cls.definition);
    Node getter = NodeUtil.getFirstGetterMatchingKey(classMembers, getterPropName);
    if (getter != null) {
      JSDocInfo info = NodeUtil.getBestJSDocInfo(getter);
      if (info == null || !info.hasReturnType()) {
        JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(info);
        builder.recordReturnType(jsType);
        getter.setJSDocInfo(builder.build());
      }
    }
  }

  /** Wrap the properties config object in an objectReflect call */
  private void addPropertiesConfigObjectReflection(
      PolymerClassDefinition cls, Node propertiesLiteral) {
    checkNotNull(propertiesLiteral);
    checkState(propertiesLiteral.isObjectLit());

    Node parent = propertiesLiteral.getParent();

    Node objReflectCall =
        IR.call(
                NodeUtil.newQName(compiler, "$jscomp.reflectObject"),
                cls.target.cloneTree(),
                propertiesLiteral.detach())
            .useSourceInfoIfMissingFromForTree(propertiesLiteral);
    parent.addChildToFront(objReflectCall);
    compiler.reportChangeToEnclosingScope(parent);
  }

  /** Adds an @this annotation to all functions in the objLit. */
  private void addTypesToFunctions(
      Node objLit, String thisType, PolymerClassDefinition.DefinitionType defType) {
    checkState(objLit.isObjectLit());
    for (Node keyNode : objLit.children()) {
      Node value = keyNode.getLastChild();
      if (value != null && value.isFunction()) {
        JSDocInfoBuilder fnDoc = JSDocInfoBuilder.maybeCopyFrom(keyNode.getJSDocInfo());
        fnDoc.recordThisType(
            new JSTypeExpression(new Node(Token.BANG, IR.string(thisType)), VIRTUAL_FILE));
        keyNode.setJSDocInfo(fnDoc.build());
      }
    }

    // Add @this and @return to default property values.
    for (MemberDefinition property :
        PolymerPassStaticUtils.extractProperties(
            objLit,
            defType,
            compiler,
            /** constructor= */
            null)) {
      if (!property.value.isObjectLit()) {
        continue;
      }

      Node defaultValue = NodeUtil.getFirstPropMatchingKey(property.value, "value");
      if (defaultValue == null || !defaultValue.isFunction()) {
        continue;
      }
      Node defaultValueKey = defaultValue.getParent();
      JSDocInfoBuilder fnDoc = JSDocInfoBuilder.maybeCopyFrom(defaultValueKey.getJSDocInfo());
      fnDoc.recordThisType(
          new JSTypeExpression(new Node(Token.BANG, IR.string(thisType)), VIRTUAL_FILE));
      fnDoc.recordReturnType(PolymerPassStaticUtils.getTypeFromProperty(property, compiler));
      defaultValueKey.setJSDocInfo(fnDoc.build());
    }
  }

  /**
   * Generates the _set* setters for readonly properties and appends them to the given block.
   *
   * @return A List of all readonly properties.
   */
  private ImmutableList parseReadOnlyProperties(
      final PolymerClassDefinition cls, Node block) {
    String qualifiedPath = cls.target.getQualifiedName() + ".prototype.";
    ImmutableList.Builder readOnlyProps = ImmutableList.builder();

    for (MemberDefinition prop : cls.props) {
      // Generate the setter for readOnly properties.
      if (prop.value.isObjectLit()) {
        Node readOnlyValue = NodeUtil.getFirstPropMatchingKey(prop.value, "readOnly");
        if (readOnlyValue != null && readOnlyValue.isTrue()) {
          Node setter = makeReadOnlySetter(prop.name.getString(), qualifiedPath);
          setter.useSourceInfoIfMissingFromForTree(prop.name);
          block.addChildToBack(setter);
          readOnlyProps.add(prop);
        }
      }
    }

    return readOnlyProps.build();
  }

  /**
   * Generates the _set* setters for readonly properties and appends them to the given block.
   *
   * @return A List of all readonly properties.
   */
  private ImmutableList parseAttributeReflectedProperties(
      final PolymerClassDefinition cls) {
    ImmutableList.Builder attrReflectedProps = ImmutableList.builder();

    for (MemberDefinition prop : cls.props) {
      // Generate the setter for readOnly properties.
      if (prop.value.isObjectLit()) {
        Node reflectedValue = NodeUtil.getFirstPropMatchingKey(prop.value, "reflectToAttribute");
        if (reflectedValue != null && reflectedValue.isTrue()) {
          attrReflectedProps.add(prop);
        }
      }
    }

    return attrReflectedProps.build();
  }

  /** @return The proper constructor doc for the Polymer call. */
  private JSDocInfoBuilder getConstructorDoc(final PolymerClassDefinition cls) {
    JSDocInfoBuilder constructorDoc = JSDocInfoBuilder.maybeCopyFrom(cls.constructor.info);
    constructorDoc.recordConstructor();

    JSTypeExpression baseType =
        new JSTypeExpression(
            new Node(Token.BANG, IR.string(PolymerPassStaticUtils.getPolymerElementType(cls))),
            VIRTUAL_FILE);
    constructorDoc.recordBaseType(baseType);

    String interfaceName = getInterfaceName(cls);
    JSTypeExpression interfaceType =
        new JSTypeExpression(new Node(Token.BANG, IR.string(interfaceName)), VIRTUAL_FILE);
    constructorDoc.recordImplementedInterface(interfaceType);

    return constructorDoc;
  }

  /** Appends all of the given properties to the given block. */
  private void appendPropertiesToBlock(List props, Node block, String basePath) {
    for (MemberDefinition prop : props) {
      Node propertyNode =
          IR.exprResult(NodeUtil.newQName(compiler, basePath + prop.name.getString()));

      // If a property string is quoted, make sure the added prototype properties are also quoted
      if (prop.name.isQuotedString()) {
        continue;
      }

      propertyNode.useSourceInfoIfMissingFromForTree(prop.name);
      JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(prop.info);

      JSTypeExpression propType = PolymerPassStaticUtils.getTypeFromProperty(prop, compiler);
      if (propType == null) {
        return;
      }
      info.recordType(propType);
      propertyNode.getFirstChild().setJSDocInfo(info.build());

      block.addChildToBack(propertyNode);
    }
  }

  /** Remove all JSDocs from properties of a class definition */
  private void removePropertyDocs(
      final Node objLit, PolymerClassDefinition.DefinitionType defType) {
    for (MemberDefinition prop :
        PolymerPassStaticUtils.extractProperties(
            objLit,
            defType,
            compiler,
            /** constructor= */
            null)) {
      prop.name.removeProp(Node.JSDOC_INFO_PROP);
    }
  }

  /** Appends all required behavior functions and non-property members to the given block. */
  private void appendBehaviorMembersToBlock(final PolymerClassDefinition cls, Node block) {
    String qualifiedPath = cls.target.getQualifiedName() + ".prototype.";
    Map nameToExprResult = new HashMap<>();
    for (BehaviorDefinition behavior : cls.behaviors) {
      for (MemberDefinition behaviorFunction : behavior.functionsToCopy) {
        String fnName = behaviorFunction.name.getString();
        // Don't copy functions already defined by the element itself.
        if (NodeUtil.getFirstPropMatchingKey(cls.descriptor, fnName) != null) {
          continue;
        }

        // Avoid copying over the same function twice. The last definition always wins.
        if (nameToExprResult.containsKey(fnName)) {
          block.removeChild(nameToExprResult.get(fnName));
        }

        Node fnValue = behaviorFunction.value.cloneTree();
        NodeUtil.markNewScopesChanged(fnValue, compiler);
        Node exprResult =
            IR.exprResult(IR.assign(NodeUtil.newQName(compiler, qualifiedPath + fnName), fnValue));
        exprResult.useSourceInfoIfMissingFromForTree(behaviorFunction.name);
        JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(behaviorFunction.info);
        // Uses of private members that come from behaviors are not recognized correctly,
        // so just suppress that warning.
        info.addSuppression("unusedPrivateMembers");

        // If the function in the behavior is @protected, switch it to @public so that
        // we don't get a visibility warning. This is a bit of a hack but easier than
        // making the type system understand that methods are "inherited" from behaviors.
        if (behaviorFunction.info != null
            && behaviorFunction.info.getVisibility() == Visibility.PROTECTED) {
          info.overwriteVisibility(Visibility.PUBLIC);
        }

        // Behaviors whose declarations are not in the global scope may contain references to
        // symbols which do not exist in the element's scope. Only copy a function stub.
        if (!behavior.isGlobalDeclaration) {
          NodeUtil.getFunctionBody(fnValue).removeChildren();
        }

        exprResult.getFirstChild().setJSDocInfo(info.build());
        block.addChildToBack(exprResult);
        nameToExprResult.put(fnName, exprResult);
      }

      // Copy other members.
      for (MemberDefinition behaviorProp : behavior.nonPropertyMembersToCopy) {
        String propName = behaviorProp.name.getString();
        if (nameToExprResult.containsKey(propName)) {
          block.removeChild(nameToExprResult.get(propName));
        }

        Node exprResult = IR.exprResult(NodeUtil.newQName(compiler, qualifiedPath + propName));
        exprResult.useSourceInfoFromForTree(behaviorProp.name);
        JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(behaviorProp.info);

        if (behaviorProp.name.isGetterDef()) {
          info = new JSDocInfoBuilder(true);
          if (behaviorProp.info != null && behaviorProp.info.getReturnType() != null) {
            info.recordType(behaviorProp.info.getReturnType());
          }
        }

        exprResult.getFirstChild().setJSDocInfo(info.build());
        block.addChildToBack(exprResult);
        nameToExprResult.put(propName, exprResult);
      }
    }
  }

  /**
   * Adds the generated setter for a readonly property.
   *
   * @see https://www.polymer-project.org/0.8/docs/devguide/properties.html#read-only
   */
  private Node makeReadOnlySetter(String propName, String qualifiedPath) {
    String setterName = "_set" + propName.substring(0, 1).toUpperCase() + propName.substring(1);
    Node fnNode = IR.function(IR.name(""), IR.paramList(IR.name(propName)), IR.block());
    compiler.reportChangeToChangeScope(fnNode);
    Node exprResNode =
        IR.exprResult(IR.assign(NodeUtil.newQName(compiler, qualifiedPath + setterName), fnNode));

    JSDocInfoBuilder info = new JSDocInfoBuilder(true);
    // This is overriding a generated function which was added to the interface in
    // {@code createExportsAndExterns}.
    info.recordOverride();
    exprResNode.getFirstChild().setJSDocInfo(info.build());

    return exprResNode;
  }

  /**
   * Create exports and externs to protect element properties and methods from renaming and dead
   * code removal.
   *
   * 

Since Polymer templates, observers, and computed properties rely on string references to * element properties and methods, and because we don't yet have a way to update those references * reliably, we instead export or extern them. * *

For properties, we create a new interface called {@code PolymerInterface}, add * all element properties to it, mark that the element class {@code @implements} this interface, * and add the interface to the Closure externs. The specific set of properties we add to this * interface is determined by the value of {@code polymerExportPolicy}. * *

For methods, when {@code polymerExportPolicy = EXPORT_ALL}, we instead append to {@code * Object.prototype} in the externs using {@code @export} annotations. This approach is a * compromise, with the following alternatives considered: * *

Alternative 1: Add methods to our generated {@code PolymerInterface} in the * externs. Pro: More optimal than {@code Object.prototype} when type-aware optimizations are * enabled. Con 1: When a class {@code @implements} an interface, and when {@code * report_missing_override} is enabled, any method on the class that is also in the interface must * have an {@code @override} annotation, which means we generate a spurious warning for all * methods. Con 2: An unresolved bug was encountered (b/115942961) relating to a mismatch between * the signatures of the class and the generated interface. * *

Alternative 2: Generate goog.exportProperty calls, which causes aliases on the prototype * from original to optimized names to be set. Pro: Compiled code can still use the optimized * name. Con: In practice, for Polymer applications, we see a net increase in bundle size due to * the high number of new {@code Foo.prototype.originalName = Foo.prototype.z} expressions. * *

Alternative 3: Append directly to the {@code Object.prototype} externs, instead of using * {@code @export} annotations for the {@link GenerateExports} pass. Pro: Doesn't depend on the * {@code generate_exports} and {@code export_local_property_definitions} flags. Con: The * PolymerPass runs in the type checking phase, so modifying {@code Object.prototype} here causes * unwanted type checking effects, such as allowing the method to be called on any object, and * generating incorrect warnings when {@code report_missing_override} is enabled. */ private void createExportsAndExterns( final PolymerClassDefinition cls, List readOnlyProps, List attributeReflectedProps) { Node block = IR.block(); String interfaceName = getInterfaceName(cls); Node fnNode = NodeUtil.emptyFunction(); compiler.reportChangeToChangeScope(fnNode); Node varNode = IR.var(NodeUtil.newQName(compiler, interfaceName), fnNode); JSDocInfoBuilder info = new JSDocInfoBuilder(true); info.recordInterface(); varNode.setJSDocInfo(info.build()); block.addChildToBack(varNode); String interfaceBasePath = interfaceName + ".prototype."; if (polymerExportPolicy == PolymerExportPolicy.EXPORT_ALL) { // Properties from behaviors were added to our element definition earlier. appendPropertiesToBlock(cls.props, block, interfaceBasePath); // Methods from behaviors were not already added to our element definition, so we need to // export those in addition to methods defined directly on the element. Note it's possible // and valid for two behaviors, or a behavior and an element, to implement the same method, // so we de-dupe by name. We're not checking that the signatures are compatible in the way // that normal class inheritance would, but that's not easy to do since these aren't classes. // Class mixins replace Polymer behaviors and are supported directly by Closure, so new code // should use those instead. LinkedHashMap uniqueMethods = new LinkedHashMap<>(); if (cls.behaviors != null) { for (BehaviorDefinition behavior : cls.behaviors) { for (MemberDefinition method : behavior.functionsToCopy) { uniqueMethods.put(method.name.getString(), method); } } } for (MemberDefinition method : cls.methods) { uniqueMethods.put(method.name.getString(), method); } for (MemberDefinition method : uniqueMethods.values()) { addMethodToObjectExternsUsingExportAnnotation(cls, method); } } else if (polymerVersion == 1) { // For Polymer 1, all declared properties are non-renameable appendPropertiesToBlock(cls.props, block, interfaceBasePath); } else { // For Polymer 2, only read-only properties and reflectToAttribute properties are // non-renameable. Other properties follow the ALL_UNQUOTED renaming rules. List interfaceProperties = new ArrayList<>(); interfaceProperties.addAll(readOnlyProps); if (attributeReflectedProps != null) { interfaceProperties.addAll(attributeReflectedProps); } appendPropertiesToBlock(interfaceProperties, block, interfaceBasePath); } for (MemberDefinition prop : readOnlyProps) { // Add all _set* functions to avoid renaming. String propName = prop.name.getString(); String setterName = "_set" + propName.substring(0, 1).toUpperCase() + propName.substring(1); Node setterExprNode = IR.exprResult(NodeUtil.newQName(compiler, interfaceBasePath + setterName)); JSDocInfoBuilder setterInfo = new JSDocInfoBuilder(true); JSTypeExpression propType = PolymerPassStaticUtils.getTypeFromProperty(prop, compiler); setterInfo.recordParameter(propName, propType); setterExprNode.getFirstChild().setJSDocInfo(setterInfo.build()); block.addChildToBack(setterExprNode); } block.useSourceInfoIfMissingFromForTree(polymerElementExterns); Node scopeRoot = polymerElementExterns; if (!scopeRoot.isScript()) { scopeRoot = scopeRoot.getParent(); } Node stmts = block.removeChildren(); scopeRoot.addChildrenToBack(stmts); compiler.reportChangeToEnclosingScope(stmts); } /** * Add a method to {@code Object.prototype} in the externs by inserting a {@code GETPROP} * expression with an {@code @export} annotation into the program. * *

This relies on the {@code --generate_exports} and {@code export_local_property_definitions} * flags to enable the {@link GenerateExports} pass, which will add properties exported in this * way to {@code Object.prototype} in the externs, thus preventing renaming and dead code removal. * Note that {@link GenerateExports} runs after type checking, so extending {@code * Object.prototype} does not cause unwanted type checking effects. */ private void addMethodToObjectExternsUsingExportAnnotation( PolymerClassDefinition cls, MemberDefinition method) { Node getprop = NodeUtil.newQName( compiler, cls.target.getQualifiedName() + ".prototype." + method.name.getString()); JSDocInfoBuilder info = new JSDocInfoBuilder( /** parseDocumentation */ true); if (method.info != null) { // We need to preserve visibility, but other JSDoc doesn't matter (and can cause // re-declaration errors). info.recordVisibility(method.info.getVisibility()); } info.recordExport(); getprop.setJSDocInfo(info.build()); Node expression = IR.exprResult(getprop).useSourceInfoIfMissingFromForTree(method.name); // Walk up until we find a statement we can insert after. Node insertAfter = cls.definition; while (!NodeUtil.isStatementBlock(insertAfter.getParent())) { insertAfter = insertAfter.getParent(); } insertAfter.getParent().addChildAfter(expression, insertAfter); compiler.reportChangeToEnclosingScope(expression); } /** Returns the name of the generated extern interface which the element implements. */ private static String getInterfaceName(final PolymerClassDefinition cls) { return "Polymer" + cls.target.getQualifiedName().replace('.', '_') + "Interface"; } /** Returns an assign replacing the equivalent var or let declaration. */ private static Node varToAssign(Node var) { Node assign = IR.assign(var.getFirstChild().cloneNode(), var.getFirstChild().removeFirstChild()); return IR.exprResult(assign).useSourceInfoIfMissingFromForTree(var); } /** * Converts property observer strings to direct function references. * *

From: observer: '_observerName' To: * observer: ClassName.prototype._observerName */ private void convertSimpleObserverStringsToReferences(final PolymerClassDefinition cls) { for (MemberDefinition prop : cls.props) { if (prop.value.isObjectLit()) { Node observer = NodeUtil.getFirstPropMatchingKey(prop.value, "observer"); if (observer != null && observer.isString()) { Node observerDirectReference = IR.getprop(cls.target.cloneTree(), "prototype", observer.getString()) .useSourceInfoFrom(observer); observer.replaceWith(observerDirectReference); compiler.reportChangeToEnclosingScope(observerDirectReference); } } } } /** * For any property in the Polymer property configuration object with a `computed` key, parse the * method call and path arguments and replace them with property reflection calls. * *

Returns a list of property sink statements to guard against dead code elimination since the * compiler may not see these methods as being used. */ private List addComputedPropertiesReflectionCalls(final PolymerClassDefinition cls) { List propertySinkStatements = new ArrayList<>(); for (MemberDefinition prop : cls.props) { if (prop.value.isObjectLit()) { Node computed = NodeUtil.getFirstPropMatchingKey(prop.value, "computed"); if (computed != null && computed.isString()) { propertySinkStatements.addAll( replaceMethodStringWithReflectedCalls(cls.target, computed)); } } } return propertySinkStatements; } /** * For any strings returned in the array from the Polymer static observers property, parse the * method call and path arguments and replace them with property reflection calls. * *

Returns a list of property sink statements to guard against dead code elimination since the * compiler may not see these methods as being used. */ private List addComplexObserverReflectionCalls(final PolymerClassDefinition cls) { List propertySinkStatements = new ArrayList<>(); Node classMembers = NodeUtil.getClassMembers(cls.definition); Node getter = NodeUtil.getFirstGetterMatchingKey(classMembers, "observers"); if (getter != null) { Node complexObservers = null; for (Node child : NodeUtil.getFunctionBody(getter.getFirstChild()).children()) { if (child.isReturn()) { if (child.hasChildren() && child.getFirstChild().isArrayLit()) { complexObservers = child.getFirstChild(); break; } } } if (complexObservers != null) { for (Node complexObserver : complexObservers.children()) { if (complexObserver.isString()) { propertySinkStatements.addAll( replaceMethodStringWithReflectedCalls(cls.target, complexObserver)); } } } } return propertySinkStatements; } /** * Given a Polymer method call string such as: "methodName(path.item, other.property.path)" * parses the string into a method name and arguments and builds up a new string of * property reflection calls so that the properties can be renamed consistently. * *

Returns a list of property sink statements to guard against dead code elimination. */ private List replaceMethodStringWithReflectedCalls(Node className, Node methodSignature) { checkArgument(methodSignature.isString()); List propertySinkStatements = new ArrayList<>(); String methodSignatureString = methodSignature.getString().trim(); int openParenIndex = methodSignatureString.indexOf('('); if (methodSignatureString.charAt(methodSignatureString.length() - 1) != ')' || openParenIndex < 1) { compiler.report(JSError.make(methodSignature, PolymerPassErrors.POLYMER_UNPARSABLE_STRING)); return propertySinkStatements; } // Reflect property calls require an instance of a type. Since we don't have one, // just cast an object literal to be that type. While not generally safe, it is // safe for property reflection. JSDocInfoBuilder classTypeDoc = new JSDocInfoBuilder(false); JSTypeExpression classType = new JSTypeExpression( new Node(Token.BANG, IR.string(className.getQualifiedName())), className.getSourceFileName()); classTypeDoc.recordType(classType); Node classTypeExpression = IR.cast(IR.objectlit(), classTypeDoc.build()); // Add reflect and property sinks for the method name which will be a property on the class String methodName = methodSignatureString.substring(0, openParenIndex).trim(); propertySinkStatements.add( IR.getprop(className.cloneTree(), "prototype", methodName) .useSourceInfoFromForTree(methodSignature)); Node reflectedMethodName = IR.call( IR.getprop(IR.name("$jscomp"), IR.string("reflectProperty")), IR.string(methodName), classTypeExpression.cloneTree()); Node reflectedSignature = reflectedMethodName; // Process any parameters in the method call String nextParamDelimeter = "("; if (openParenIndex < methodSignatureString.length() - 2) { String methodParamsString = methodSignatureString .substring(openParenIndex + 1, methodSignatureString.length() - 1) .trim(); List methodParams = parseMethodParams(methodParamsString, methodSignature); // Add property reflection for each parameter for (String methodParam : methodParams) { Node reflectedTypeReference = classTypeExpression; if (methodParam.length() == 0) { continue; } if (isParamLiteral(methodParam)) { Node term = IR.string(methodParam); reflectedSignature = IR.add(IR.add(reflectedSignature, IR.string(nextParamDelimeter)), term); } else { // Arguments in conmplex observer or computed property strings are property paths. // We need to rename each path segment. List paramParts = Splitter.on('.').splitToList(methodParam); String nextPropertyTermDelimiter = nextParamDelimeter; for (int i = 0; i < paramParts.size(); i++) { // Polymer property paths have two special terms recognized when they are the last // path reference: // - * - any sub-property change // - splices - Adds or removes of array items // These terms are not renamable and are left as is if (i > 0 && i == paramParts.size() - 1 && (paramParts.get(i).equals("*") || paramParts.get(i).equals("splices"))) { reflectedSignature = IR.add( reflectedSignature, IR.string(nextPropertyTermDelimiter + paramParts.get(i))); } else { if (i == 0) { // The root of the parameter will be a property reference on the class // Create both a property sink and a reflection call propertySinkStatements.add( IR.getprop( className.cloneTree(), IR.string("prototype"), IR.string(paramParts.get(i))) .useSourceInfoFromForTree(methodSignature)); } Node reflectedParamPart = IR.call( IR.getprop(IR.name("$jscomp"), IR.string("reflectProperty")), IR.string(paramParts.get(i)), reflectedTypeReference.cloneTree()); reflectedSignature = IR.add( IR.add(reflectedSignature, IR.string(nextPropertyTermDelimiter)), reflectedParamPart); reflectedTypeReference = IR.getprop(reflectedTypeReference.cloneTree(), paramParts.get(i)); } nextPropertyTermDelimiter = "."; } } nextParamDelimeter = ","; } if (methodParams.size() == 0) { reflectedSignature = IR.add(reflectedSignature, IR.string("()")); } else { reflectedSignature = IR.add(reflectedSignature, IR.string(")")); } } else { reflectedSignature = IR.add(reflectedSignature, IR.string("()")); } methodSignature.replaceWith(reflectedSignature.useSourceInfoFromForTree(methodSignature)); compiler.reportChangeToEnclosingScope(reflectedSignature); return propertySinkStatements; } /** * Parses the parameters string from a complex observer or computed property into distinct * parameters. Since a parameter can be a quoted string literal, we can't just split on commas. */ private List parseMethodParams(String methodParameters, Node methodSignature) { List parsedParameters = new ArrayList<>(); char nextDelimeter = ','; String currentTerm = ""; for (int i = 0; i < methodParameters.length(); i++) { if (methodParameters.charAt(i) == nextDelimeter) { if (nextDelimeter == ',') { parsedParameters.add(currentTerm.trim()); currentTerm = ""; } else { currentTerm += nextDelimeter; nextDelimeter = ','; } } else { currentTerm += methodParameters.charAt(i); if (methodParameters.charAt(i) == '"' || methodParameters.charAt(i) == '\'') { nextDelimeter = methodParameters.charAt(i); } } } if (nextDelimeter != ',') { compiler.report(JSError.make(methodSignature, PolymerPassErrors.POLYMER_UNPARSABLE_STRING)); return parsedParameters; } if (currentTerm.length() > 0) { parsedParameters.add(currentTerm.trim()); } return parsedParameters; } /** Determine if the method parameter a quoted string or numeric literal recognized by Polymer. */ private static boolean isParamLiteral(String param) { try { Double.parseDouble(param); return true; } catch (NumberFormatException e) { // Check to see if the parameter is a literal - either a quoted string or // numeric literal if (param.length() > 1 && (param.charAt(0) == '"' || param.charAt(0) == '\'') && param.charAt(0) == param.charAt(param.length() - 1)) { return true; } } return false; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy