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

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

There is a newer version: 9.0.8
Show 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 static com.google.javascript.jscomp.NodeUtil.isBundledGoogModuleCall;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
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.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.Locale;
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";

  static final DiagnosticType IMPLICIT_GLOBAL_CONFLICT =
      DiagnosticType.error(
          "JSC_POLYMER_IMPLICIT_GLOBAL_CONFLICT",
          "Implicit global name for Polymer element conflicts with existing var {0}. Either"
              + " give the element a lhs or rename {0}. (Or move to class-based Polymer 2"
              + " elements)");

  static final DiagnosticType POLYMER_ELEMENT_CONFLICT =
      DiagnosticType.error(
          "JSC_POLYMER_ELEMENT_CONFLICT",
          "Cannot generate correct types for Polymer call due to PolymerElement definition at"
              + " {0}:{2}:{1}.\n"
              + "Rename the local PolymerElement to avoid shadowing the PolymerElement externs.");

  private final Node externsInsertionRef;
  boolean propertySinkExternInjected = false;

  PolymerClassRewriter(
      AbstractCompiler compiler,
      int polymerVersion,
      PolymerExportPolicy polymerExportPolicy,
      boolean propertyRenamingEnabled) {
    this.compiler = compiler;
    this.externsInsertionRef = compiler.getSynthesizedExternsInput().getAstRoot(compiler);
    this.polymerVersion = polymerVersion;
    this.polymerExportPolicy = polymerExportPolicy;
    this.propertyRenamingEnabled = propertyRenamingEnabled;
  }

  static boolean isIIFE(Node n) {
    return n.isCall() && n.getFirstChild().isFunction();
  }

  static boolean isFunctionArgInGoogLoadModule(Node n) {
    if (!n.isFunction()) {
      return false;
    }

    Node parent = n.getParent();
    return parent != null && isBundledGoogModuleCall(parent);
  }

  /**
   * This function accepts declaration code generated for a nonGlobal Polymer call and inserts that
   * into the AST depending on the enclosing scope of the Polymer call.
   *
   * @param enclosingNode The enclosing scope of the Polymer call decided by the rewritePolymerCall
   * @param declarationCode declaration code generated for Polymer call
   */
  private void insertGeneratedDeclarationCodeToGlobalScope(
      Node enclosingNode, Node declarationCode) {
    switch (enclosingNode.getToken()) {
      case MODULE_BODY:
        {
          Node insertionPoint = getNodeForInsertion(enclosingNode.getParent());
          insertionPoint.addChildToFront(declarationCode);
          compiler.reportChangeToChangeScope(NodeUtil.getEnclosingScript(insertionPoint));
        }
        break;
      case SCRIPT:
        {
          enclosingNode.addChildToFront(declarationCode);
          compiler.reportChangeToChangeScope(NodeUtil.getEnclosingScript(enclosingNode));
        }
        break;
      case CALL:
        {
          // This case represents only the Polymer calls which are enclosed inside an IIFE
          checkState(isIIFE(enclosingNode));
          Node enclosingNodeForIIFE =
              NodeUtil.getEnclosingNode(
                  enclosingNode.getParent(), node -> node.isScript() || node.isModuleBody());
          if (enclosingNodeForIIFE.isScript()) {
            enclosingNodeForIIFE.addChildToFront(declarationCode);
            compiler.reportChangeToChangeScope(NodeUtil.getEnclosingScript(enclosingNodeForIIFE));
          } else {
            checkState(enclosingNodeForIIFE.isModuleBody());
            Node insertionPoint = getNodeForInsertion(enclosingNodeForIIFE.getParent());
            insertionPoint.addChildToFront(declarationCode);
            compiler.reportChangeToChangeScope(NodeUtil.getEnclosingScript(insertionPoint));
          }
        }
        break;
      case FUNCTION:
        {
          // This case represents only the Polymer calls that are inside a function which is an arg
          // to goog.loadModule
          checkState(isFunctionArgInGoogLoadModule(enclosingNode));
          Node enclosingScript = NodeUtil.getEnclosingScript(enclosingNode);
          Node insertionPoint = getNodeForInsertion(enclosingScript);
          insertionPoint.addChildToFront(declarationCode);
          compiler.reportChangeToChangeScope(insertionPoint);
        }
        break;
      default:
        throw new RuntimeException("Enclosing node for Polymer is incorrect");
    }
  }

  /**
   * Returns a SCRIPT node in which to insert new global declarations
   *
   * 

New globals are put in the externs if converting a @typeSummary and in source code * otherwise. */ private Node getNodeForInsertion(Node enclosingScript) { if (NodeUtil.isFromTypeSummary(enclosingScript)) { return externsInsertionRef; } else { return compiler.getNodeForCodeInsertion(null); } } /** * This function accepts code generated for a nonGlobal Polymer call and inserts that code into * the AST depending on the enclosing scope of the Polymer call. * * @param enclosingNode The enclosing scope of the Polymer call decided by the rewritePolymerCall * @param statements code generated for Polymer's properties and behavior */ private void insertGeneratedPropsAndBehaviorCode(Node enclosingNode, Node statements) { switch (enclosingNode.getToken()) { case MODULE_BODY: { if (enclosingNode.getParent().getBooleanProp(Node.GOOG_MODULE)) { // The goog.module('ns'); call must remain the first statement in the module. Node insertionPoint = getInsertionPointForGoogModule(enclosingNode); enclosingNode.addChildrenAfter(statements, insertionPoint); } else { enclosingNode.addChildrenToFront(statements); } } break; case SCRIPT: enclosingNode.addChildrenToFront(statements); compiler.reportChangeToChangeScope(NodeUtil.getEnclosingScript(enclosingNode)); break; case CALL: { // This case represents only the Polymer calls which are enclosed inside an IIFE checkState(isIIFE(enclosingNode)); Node functionNode = enclosingNode.getFirstChild(); Node functionBlock = functionNode.getLastChild(); functionBlock.addChildrenToFront(statements); } break; case FUNCTION: // This case represents only the Polymer calls that are inside a function which is an arg // to goog.loadModule checkState(isFunctionArgInGoogLoadModule(enclosingNode)); Node functionBlock = enclosingNode.getLastChild(); Node insertionPoint = getInsertionPointForGoogModule(functionBlock); // Node insertionPoint will be null here if functionBlock does not contain a goog.module() // Missing goog.module inside the loadModule's functionBlock is semantically incorrect // That will cause the compiler to crash in closureRewriteModule pass. if (insertionPoint != null) { functionBlock.addChildrenAfter(statements, insertionPoint); } break; default: break; } } /** * Rewrites a given call to Polymer({}) to a set of declarations and assignments which can be * understood by the compiler. * * @param cls The extracted {@link PolymerClassDefinition} for the Polymer element created by this * call. * @param traversal Nodetraversal used here to identify the scope in which Polymer exists */ void rewritePolymerCall(final PolymerClassDefinition cls, NodeTraversal traversal) { Node callParent = cls.definition.getParent(); // Determine whether we are in a Polymer({}) call at the top level versus in an assignment. Node exprRoot = callParent.isExprResult() ? callParent : callParent.getParent(); checkState(NodeUtil.isStatementParent(exprRoot.getParent()), exprRoot.getParent()); Node objLit = checkNotNull(cls.descriptor); // Add {@code @lends} to the object literal. JSDocInfo.Builder objLitDoc = JSDocInfo.builder().parseDocumentation(); JSTypeExpression jsTypeExpression = new JSTypeExpression( IR.string(cls.target.getQualifiedName() + ".prototype").srcref(exprRoot), 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); } } // The propsAndBehaviorBlock holds code generated for the Polymer's properties and behaviors Node propsAndBehaviorBlock = IR.block(); JSDocInfo.Builder constructorDoc = this.getConstructorDoc(cls); // Remove the original constructor JS docs from the objlit. Node ctorKey = cls.constructor.value.getParent(); if (ctorKey != null) { ctorKey.setJSDocInfo(null); } // Check for a conflicting definition of PolymerElement if (!traversal.inGlobalScope()) { Var polymerElement = traversal.getScope().getVar("PolymerElement"); if (polymerElement != null && !polymerElement.getScope().isGlobal()) { Node nameNode = polymerElement.getNameNode(); compiler.report( JSError.make( cls.constructor.value, POLYMER_ELEMENT_CONFLICT, nameNode.getSourceFileName(), Integer.toString(nameNode.getLineno()), Integer.toString(nameNode.getCharno()))); } } Node declarationCode = generateDeclarationCode(exprRoot, cls, constructorDoc, traversal); String basePath = cls.target.getQualifiedName() + ".prototype."; appendBehaviorPropertiesToBlock(cls, propsAndBehaviorBlock, basePath, /*isExternsBlock*/ false); appendPropertiesToBlock( cls.props, propsAndBehaviorBlock, basePath, /* isExternsBlock= */ false); appendBehaviorMembersToBlock(cls, propsAndBehaviorBlock); ImmutableList readOnlyPropsAll = parseReadOnlyProperties(cls, propsAndBehaviorBlock); ImmutableList attributeReflectedPropsAll = parseAttributeReflectedProperties(cls); createExportsAndExterns(cls, readOnlyPropsAll, attributeReflectedPropsAll); removePropertyDocs(objLit, cls.defType); Node propsAndBehaviorCode = propsAndBehaviorBlock.removeChildren(); Node parent = exprRoot.getParent(); // Put the type declaration in to either the enclosing module scope, if in a module, or the // enclosing script node. Compiler support for local scopes like IIFEs is sometimes lacking but // module scopes are well-supported. If this is not in a module or the global scope it is likely // exported. if (!traversal.inGlobalScope() && cls.hasGeneratedLhs && !cls.target.isGetProp()) { Node enclosingNode = NodeUtil.getEnclosingNode( parent, node -> node.isScript() || node.isModuleBody() || isIIFE(node) || isFunctionArgInGoogLoadModule(node)); // For module, IIFE and goog.LoadModule enclosed Polymer calls, the declaration code and the // code generated from properties and behavior have to be hoisted in different places within // the AST. We want to insert the generated declarations to global scope, and insert the // propsAndbehaviorCode in the same scope. Hence, dealing with them separately. insertGeneratedDeclarationCodeToGlobalScope(enclosingNode, declarationCode); if (propsAndBehaviorCode != null) { insertGeneratedPropsAndBehaviorCode(enclosingNode, propsAndBehaviorCode); } } else { Node beforeRoot = exprRoot.getPrevious(); if (beforeRoot == null) { if (propsAndBehaviorCode != null) { parent.addChildrenToFront(propsAndBehaviorCode); } parent.addChildToFront(declarationCode); } else { if (propsAndBehaviorCode != null) { parent.addChildrenAfter(propsAndBehaviorCode, beforeRoot); } declarationCode.insertAfter(beforeRoot); } compiler.reportChangeToEnclosingScope(parent); } if (propsAndBehaviorCode != null) { compiler.reportChangeToEnclosingScope(propsAndBehaviorCode); } // 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); exprRoot.replaceWith(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(); appendBehaviorPropertiesToBlock( cls, block, cls.target.getQualifiedName() + ".prototype.", /*isExternsBlock*/ false); // 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.", /* isExternsBlock= */ false); 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); JSDocInfo.Builder classInfo = JSDocInfo.Builder.maybeCopyFrom(jsDocInfoNode.getJSDocInfo()); String interfaceName = cls.getInterfaceName(compiler.getUniqueNameIdSupplier()); JSTypeExpression interfaceType = new JSTypeExpression( new Node(Token.BANG, IR.string(interfaceName)).srcrefTree(jsDocInfoNode), 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.isEmpty()) { 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).srcref(propertyRef); protectorCall.insertAfter(insertAfterReference); insertAfterReference = protectorCall; } compiler.reportChangeToEnclosingScope(insertAfterReference); } addPropertiesConfigObjectReflection(cls, cls.descriptor); } } /** Adds return type information to class getters */ private static 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()) { JSDocInfo.Builder builder = JSDocInfo.Builder.maybeCopyFrom(info); builder.recordReturnType(jsType); jsType.getRoot().srcrefTreeIfMissing(getter); 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()) .srcrefTreeIfMissing(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.getFirstChild(); keyNode != null; keyNode = keyNode.getNext()) { Node value = keyNode.getLastChild(); if (value != null && value.isFunction()) { JSDocInfo.Builder fnDoc = JSDocInfo.Builder.maybeCopyFrom(keyNode.getJSDocInfo()); fnDoc.recordThisType( new JSTypeExpression( new Node(Token.BANG, IR.string(thisType)).srcrefTree(keyNode), 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(); JSDocInfo.Builder fnDoc = JSDocInfo.Builder.maybeCopyFrom(defaultValueKey.getJSDocInfo()); fnDoc.recordThisType( new JSTypeExpression( new Node(Token.BANG, IR.string(thisType)).srcrefTree(defaultValueKey), 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, qualifiedPath); setter.srcrefTreeIfMissing(prop.name); block.addChildToBack(setter); readOnlyProps.add(prop); } } } if (cls.behaviorProps != null) { for (Map.Entry itr : cls.behaviorProps.entrySet()) { MemberDefinition prop = itr.getKey(); // 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, qualifiedPath); setter.srcrefTreeIfMissing(prop.name); block.addChildToBack(setter); readOnlyProps.add(prop); } } } } return readOnlyProps.build(); } private static 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); } } } if (cls.behaviorProps != null) { for (Map.Entry itr : cls.behaviorProps.entrySet()) { MemberDefinition prop = itr.getKey(); // 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 JSDocInfo.Builder getConstructorDoc(final PolymerClassDefinition cls) { JSDocInfo.Builder constructorDoc = JSDocInfo.Builder.maybeCopyFrom(cls.constructor.info); constructorDoc.recordConstructor(); JSTypeExpression baseType = new JSTypeExpression( new Node(Token.BANG, IR.string(PolymerPassStaticUtils.getPolymerElementType(cls))) .srcrefTree(cls.definition), VIRTUAL_FILE); constructorDoc.recordBaseType(baseType); String interfaceName = cls.getInterfaceName(compiler.getUniqueNameIdSupplier()); JSTypeExpression interfaceType = new JSTypeExpression( new Node(Token.BANG, IR.string(interfaceName)).srcrefTree(cls.definition), VIRTUAL_FILE); constructorDoc.recordImplementedInterface(interfaceType); return constructorDoc; } /* Appends var declaration code created from the Polymer call to the given block */ private Node generateDeclarationCode( Node exprRoot, final PolymerClassDefinition cls, JSDocInfo.Builder constructorDoc, NodeTraversal traversal) { 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.srcrefTreeIfMissing(cls.target); return exprResult; } else { // var foo = Polymer({...}); OR Polymer({...}); Node var = IR.var(cls.target.cloneTree(), cls.constructor.value.cloneTree()); NodeUtil.markNewScopesChanged(var, compiler); var.srcrefTreeIfMissing(exprRoot); var.setJSDocInfo(constructorDoc.build()); String name = cls.target.getString(); Var existingVar = traversal.getScope().getSlot(name); if (existingVar != null && cls.hasGeneratedLhs) { compiler.report(JSError.make(cls.constructor.value, IMPLICIT_GLOBAL_CONFLICT, name)); } return var; } } /** * Replaces JSDoc types in externs with unknown type. For a JSDoc like @type {{ propertyName : * string }}, collects all such "propertyName"s, and generates extern vars with an attached * {{propertyName: ?}} JsDoc. This is to prevent renaming of vars in source code with the same * names as propertyNames. */ private JSDocInfo replaceJSDocAndAddNewVars( MemberDefinition prop, JSTypeExpression propType, Node block) { JSDocInfo.Builder infoBuilder = JSDocInfo.Builder.maybeCopyFrom(prop.info); infoBuilder.recordType(propType); JSDocInfo origInfo = infoBuilder.build(); ImmutableSet propertyNames = propType.getRecordPropertyNames(); createVarsInExternsBlock(block, propertyNames, propType, prop); JSTypeExpression unknown = new JSTypeExpression(new Node(Token.QMARK), propType.getSourceName()); JSDocInfo.Builder newInfoBuilder = JSDocInfo.Builder.maybeCopyFromWithNewType(origInfo, unknown); return newInfoBuilder.build(); } /** Returns a node from a property's definition in the Polymer element or behavior */ private Node getPropertyNode(MemberDefinition prop, String basePath) { // If a property string is quoted, make sure the added prototype properties are also quoted if (prop.name.isQuotedString()) { return null; } Node propertyNode = IR.exprResult(NodeUtil.newQName(compiler, basePath + prop.name.getString())); propertyNode.srcrefTreeIfMissing(prop.name); return propertyNode; } /** * Iterates through all the behaviors of this polymer call, and appends the properties of each * behavior to the given block. */ private void appendBehaviorPropertiesToBlock( PolymerClassDefinition cls, Node block, String basePath, boolean isExternsBlock) { if (cls.behaviors == null || cls.behaviors.isEmpty() || cls.behaviorProps == null) { return; } for (Map.Entry itr : cls.behaviorProps.entrySet()) { BehaviorDefinition behavior = itr.getValue(); MemberDefinition prop = itr.getKey(); Node propertyNode = getPropertyNode(prop, basePath); if (propertyNode == null) { continue; } JSTypeExpression propType = PolymerPassStaticUtils.getTypeFromProperty(prop, compiler); if (propType == null) { continue; } JSDocInfo info = null; if (isExternsBlock) { info = replaceJSDocAndAddNewVars(prop, propType, block); } else { JSDocInfo.Builder infoBuilder = getJSDocInfoBuilderForBehavior(behavior, prop); infoBuilder.recordType(propType); info = infoBuilder.build(); } propertyNode.getFirstChild().setJSDocInfo(info); block.addChildToBack(propertyNode); } } /** Appends all of the given properties to the given block. */ private void appendPropertiesToBlock( List props, Node block, String basePath, boolean isExternsBlock) { for (MemberDefinition prop : props) { Node propertyNode = getPropertyNode(prop, basePath); if (propertyNode == null) { continue; } JSTypeExpression propType = PolymerPassStaticUtils.getTypeFromProperty(prop, compiler); if (propType == null) { continue; } JSDocInfo info = null; if (isExternsBlock) { info = replaceJSDocAndAddNewVars(prop, propType, block); } else { JSDocInfo.Builder infoBuilder = JSDocInfo.Builder.maybeCopyFrom(prop.info); infoBuilder.recordType(propType); info = infoBuilder.build(); } propertyNode.getFirstChild().setJSDocInfo(info); block.addChildToBack(propertyNode); } } /** * For a JSDoc like @type {{ propertyName : string }}, collects all such "propertyName"s, and * generates extern vars with an attached {{propertyName: ?}} JsDoc. This is to prevent renaming * of vars in source code with the same names as propertyNames. * *

If we do not preserve the property names, then 540 targets get broken on TGP testing. This * indicates that those targets probably accidentally relied on properties not being renamed, and * we did not find it important to clean up all those targets' JS source. */ private void createVarsInExternsBlock( Node block, ImmutableSet propertyNames, JSTypeExpression propType, MemberDefinition prop) { for (String propName : propertyNames) { String varName = "PolymerDummyVar" + compiler.getUniqueNameIdSupplier().get(); Node n = Node.newString(Token.NAME, varName); Node var = new Node(Token.VAR); var.addChildToBack(n); // Forming @type {{ propertyName : ? }} JSTypeExpression newType = createNewTypeExpressionForExtern(propName, propType.getSourceName(), prop); JSDocInfo.Builder oldInfoBuilder = JSDocInfo.Builder.maybeCopyFrom(prop.info); JSDocInfo info = oldInfoBuilder.build(); JSDocInfo.Builder newInfo = JSDocInfo.Builder.copyFromWithNewType(info, newType); var.setJSDocInfo(newInfo.build()); block.addChildToBack(var); } } /** Creates a new type expression for JSDoc like @type {{ propertyName : ? }} */ private static JSTypeExpression createNewTypeExpressionForExtern( String propName, String sourceName, MemberDefinition prop) { Node leftCurly = new Node(Token.LC); Node leftBracket = new Node(Token.LB); Node colon = new Node(Token.COLON); Node propertyName = Node.newString(propName); propertyName.setToken(Token.STRING_KEY); Node unknown = new Node(Token.QMARK); colon.addChildToBack(propertyName); colon.addChildToBack(unknown); leftBracket.addChildToBack(colon); leftCurly.addChildToBack(leftBracket); leftCurly.srcrefTreeIfMissing(prop.name); return new JSTypeExpression(leftCurly, sourceName); } /** 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.setJSDocInfo(null); } } // TODO(rishipal): Consider passing behavior's module instead of behavior definition and moving // this into a common, re-usable place private Map accumulateModuleLocalVars(BehaviorDefinition behavior) { Map moduleLocalNames = new LinkedHashMap<>(); List orderedNames = new ArrayList<>(); SyntacticScopeCreator scopeCreator = new SyntacticScopeCreator(compiler); Scope globalScope = Scope.createGlobalScope(behavior.behaviorModule.getParent()); NodeUtil.getAllVarsDeclaredInModule( behavior.behaviorModule, moduleLocalNames, orderedNames, compiler, scopeCreator, globalScope); return moduleLocalNames; } private JSDocInfo.Builder getJSDocInfoBuilderForBehavior( BehaviorDefinition behavior, MemberDefinition behaviorFunctionOrProp) { JSDocInfo.Builder info; if (!behavior.isGlobalDeclaration && behaviorFunctionOrProp.info != null && behaviorFunctionOrProp.info.containsTypeDeclaration()) { if (behavior.behaviorModule != null) { Map moduleLocalNames = accumulateModuleLocalVars(behavior); // Replace module local names in @type, @return and @param with unknown type info = JSDocInfo.Builder.maybeCopyFromAndReplaceNames( behaviorFunctionOrProp.info, moduleLocalNames.keySet()); } else { info = JSDocInfo.Builder.maybeCopyFrom(behaviorFunctionOrProp.info); } } else { info = JSDocInfo.Builder.maybeCopyFrom(behaviorFunctionOrProp.info); } return info; } /** 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)) { nameToExprResult.get(fnName).detach(); } Node fnValue = behaviorFunction.value.cloneTree(); NodeUtil.markNewScopesChanged(fnValue, compiler); Node exprResult = IR.exprResult(IR.assign(NodeUtil.newQName(compiler, qualifiedPath + fnName), fnValue)); exprResult.srcrefTreeIfMissing(behaviorFunction.name); JSDocInfo.Builder info = getJSDocInfoBuilderForBehavior(behavior, behaviorFunction); // Uses of private members that come from behaviors are not recognized correctly, // so just suppress that warning. info.recordSuppression("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) { Node body = NodeUtil.getFunctionBody(fnValue); if (fnValue.isArrowFunction() && !NodeUtil.getFunctionBody(fnValue).isBlock()) { // replace `() => ` with `() => undefined` body.replaceWith(NodeUtil.newUndefinedNode(body)); } else { body.removeChildren(); } // Remove any non-named parameters, which may reference locals. int paramIndex = 0; for (Node param = NodeUtil.getFunctionParameters(fnValue).getFirstChild(); param != null; ) { final Node next = param.getNext(); makeParamSafe(param, paramIndex++); param = next; } } 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)) { nameToExprResult.get(propName).detach(); } Node exprResult = IR.exprResult(NodeUtil.newQName(compiler, qualifiedPath + propName)); exprResult.srcrefTree(behaviorProp.name); JSDocInfo.Builder info = getJSDocInfoBuilderForBehavior(behavior, behaviorProp); if (behaviorProp.name.isGetterDef()) { info = JSDocInfo.builder().parseDocumentation(); 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); } } } /** Removes any potential local names referenced within a formal parameter */ private static void makeParamSafe(Node param, int index) { if (param.isRest()) { // The lhs may be a destructuring pattern. param = param.getOnlyChild(); } else if (param.isDefaultValue()) { // Replace default value with void 0, then look at the lhs Node value = param.getSecondChild(); value.replaceWith(NodeUtil.newUndefinedNode(param)); param = param.getFirstChild(); } if (param.isDestructuringPattern()) { param.replaceWith(IR.name("param$polymer$" + index).srcref(param)); } } /** * 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(MemberDefinition prop, String qualifiedPath) { String propName = prop.name.getString(); String setterName = "_set" + propName.substring(0, 1).toUpperCase(Locale.ROOT) + 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)); JSDocInfo.Builder info = JSDocInfo.builder().parseDocumentation(); // This is overriding a generated function which was added to the interface in // {@code createExportsAndExterns}. info.recordOverride(); JSTypeExpression propType = PolymerPassStaticUtils.getTypeFromProperty(prop, compiler); info.recordParameter(propName, propType); 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 = cls.getInterfaceName(compiler.getUniqueNameIdSupplier()); Node fnNode = NodeUtil.emptyFunction(); compiler.reportChangeToChangeScope(fnNode); Node varNode = IR.var(NodeUtil.newQName(compiler, interfaceName), fnNode); JSDocInfo.Builder info = JSDocInfo.builder().parseDocumentation(); info.recordInterface(); varNode.setJSDocInfo(info.build()); block.addChildToBack(varNode); String interfaceBasePath = interfaceName + ".prototype."; if (polymerExportPolicy == PolymerExportPolicy.EXPORT_ALL) { appendBehaviorPropertiesToBlock(cls, block, interfaceBasePath, /*isExternsBlock*/ true); appendPropertiesToBlock(cls.props, block, interfaceBasePath, /* isExternsBlock= */ true); // 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 appendBehaviorPropertiesToBlock(cls, block, interfaceBasePath, /*isExternsBlock*/ true); appendPropertiesToBlock(cls.props, block, interfaceBasePath, /* isExternsBlock= */ true); } 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); } // Readonly properties and attributeReflected properties for Polymer Element and its behaviors // are stored together appendPropertiesToBlock( interfaceProperties, block, interfaceBasePath, /* isExternsBlock= */ true); } for (MemberDefinition prop : readOnlyProps) { // Add all _set* functions to avoid renaming. String propName = prop.name.getString(); String setterName = "_set" + propName.substring(0, 1).toUpperCase(Locale.ROOT) + propName.substring(1); Node setterExprNode = IR.exprResult(NodeUtil.newQName(compiler, interfaceBasePath + setterName)); JSDocInfo.Builder setterInfo = JSDocInfo.builder().parseDocumentation(); JSTypeExpression propType = PolymerPassStaticUtils.getTypeFromProperty(prop, compiler); JSTypeExpression unknown = new JSTypeExpression(new Node(Token.QMARK), propType.getSourceName()); setterInfo.recordParameter(propName, unknown); setterExprNode.getFirstChild().setJSDocInfo(setterInfo.build()); block.addChildToBack(setterExprNode); } block.srcrefTreeIfMissing(externsInsertionRef); Node stmts = block.removeChildren(); externsInsertionRef.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()); JSDocInfo.Builder info = JSDocInfo.builder().parseDocumentation(); 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).srcrefTreeIfMissing(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(); } expression.insertAfter(insertAfter); compiler.reportChangeToEnclosingScope(expression); } /** 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).srcrefTreeIfMissing(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.isStringLit()) { Node observerDirectReference = IR.getprop(cls.target.cloneTree(), "prototype", observer.getString()) .srcref(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.isStringLit()) { 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()).getFirstChild(); child != null; child = child.getNext()) { if (child.isReturn()) { if (child.hasChildren() && child.getFirstChild().isArrayLit()) { complexObservers = child.getFirstChild(); break; } } } if (complexObservers != null) { for (Node complexObserver = complexObservers.getFirstChild(); complexObserver != null; ) { final Node next = complexObserver.getNext(); if (complexObserver.isStringLit()) { propertySinkStatements.addAll( replaceMethodStringWithReflectedCalls(cls.target, complexObserver)); } complexObserver = next; } } } 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.isStringLit()); 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. JSDocInfo.Builder classTypeDoc = JSDocInfo.builder(); JSTypeExpression classType = new JSTypeExpression( new Node(Token.BANG, IR.string(className.getQualifiedName())) .srcrefTree(methodSignature), 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).srcrefTree(methodSignature)); Node reflectedMethodName = IR.call( IR.getprop(IR.name("$jscomp"), "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(), "prototype", paramParts.get(i)) .srcrefTree(methodSignature)); } Node reflectedParamPart = IR.call( IR.getprop(IR.name("$jscomp"), "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.isEmpty()) { reflectedSignature = IR.add(reflectedSignature, IR.string("()")); } else { reflectedSignature = IR.add(reflectedSignature, IR.string(")")); } } else { reflectedSignature = IR.add(reflectedSignature, IR.string("()")); } methodSignature.replaceWith(reflectedSignature.srcrefTree(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; } private static Node getInsertionPointForGoogModule(Node moduleBody) { Node insertionPoint = moduleBody.getFirstChild(); // goog.module('ns'); Node next = insertionPoint.getNext(); while (isGoogRequireExpr(next) || NodeUtil.isGoogModuleDeclareLegacyNamespaceCall(next) || NodeUtil.isGoogSetTestOnlyCall(next)) { insertionPoint = next; next = next.getNext(); } return insertionPoint; } private static boolean isGoogRequireExpr(Node statement) { if (NodeUtil.isExprCall(statement) && ModuleImportResolver.isGoogModuleDependencyCall(statement.getOnlyChild())) { // `goog.require('a.b.c');` return true; } if (!NodeUtil.isNameDeclaration(statement)) { return false; } Node rhs = statement.getFirstChild().isName() // `const c = goog.require('a.b.c');` ? statement.getFirstFirstChild() // `const {D} = goog.require('a.b.c');` : statement.getFirstChild().getSecondChild(); return ModuleImportResolver.isGoogModuleDependencyCall(rhs); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy