Skip to content

Commit

Permalink
PolymerPass now generates goog.exportProperty calls instead of augmen…
Browse files Browse the repository at this point in the history
…ting the externs when protecting Polymer element methods from renaming and dead code removal.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=214868628
  • Loading branch information
aomarks authored and brad4d committed Sep 28, 2018
1 parent 117215d commit 9c9d613
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 52 deletions.
91 changes: 69 additions & 22 deletions src/com/google/javascript/jscomp/PolymerClassRewriter.java
Expand Up @@ -31,7 +31,6 @@
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token; import com.google.javascript.rhino.Token;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
Expand Down Expand Up @@ -128,7 +127,7 @@ void rewritePolymerCall(
ImmutableList<MemberDefinition> readOnlyProps = parseReadOnlyProperties(cls, block); ImmutableList<MemberDefinition> readOnlyProps = parseReadOnlyProperties(cls, block);
ImmutableList<MemberDefinition> attributeReflectedProps = ImmutableList<MemberDefinition> attributeReflectedProps =
parseAttributeReflectedProperties(cls); parseAttributeReflectedProperties(cls);
addInterfaceExterns(cls, readOnlyProps, attributeReflectedProps); createExportsAndExterns(cls, readOnlyProps, attributeReflectedProps);
removePropertyDocs(objLit, PolymerClassDefinition.DefinitionType.ObjectLiteral); removePropertyDocs(objLit, PolymerClassDefinition.DefinitionType.ObjectLiteral);


Node statements = block.removeChildren(); Node statements = block.removeChildren();
Expand Down Expand Up @@ -218,7 +217,7 @@ void rewritePolymerClassDeclaration(
ImmutableList<MemberDefinition> readOnlyProps = parseReadOnlyProperties(cls, block); ImmutableList<MemberDefinition> readOnlyProps = parseReadOnlyProperties(cls, block);
ImmutableList<MemberDefinition> attributeReflectedProps = ImmutableList<MemberDefinition> attributeReflectedProps =
parseAttributeReflectedProperties(cls); parseAttributeReflectedProperties(cls);
addInterfaceExterns(cls, readOnlyProps, attributeReflectedProps); createExportsAndExterns(cls, readOnlyProps, attributeReflectedProps);


// If an external interface is required, mark the class as implementing it // If an external interface is required, mark the class as implementing it
if (polymerExportPolicy == PolymerExportPolicy.EXPORT_ALL if (polymerExportPolicy == PolymerExportPolicy.EXPORT_ALL
Expand Down Expand Up @@ -426,19 +425,6 @@ private void appendPropertiesToBlock(
} }
} }


/** Appends all of the given methods to the given block. */
private void appendMethodsToBlock(
final Collection<MemberDefinition> methods, Node block, String basePath) {
for (MemberDefinition method : methods) {
Node propertyNode = IR.exprResult(
NodeUtil.newQName(compiler, basePath + method.name.getString()));
propertyNode.useSourceInfoIfMissingFromForTree(method.name);
JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(method.info);
propertyNode.getFirstChild().setJSDocInfo(info.build());
block.addChildToBack(propertyNode);
}
}

/** Remove all JSDocs from properties of a class definition */ /** Remove all JSDocs from properties of a class definition */
private void removePropertyDocs( private void removePropertyDocs(
final Node objLit, PolymerClassDefinition.DefinitionType defType) { final Node objLit, PolymerClassDefinition.DefinitionType defType) {
Expand Down Expand Up @@ -539,20 +525,33 @@ private Node makeReadOnlySetter(String propName, String qualifiedPath) {


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


return exprResNode; return exprResNode;
} }


/** /**
* Adds an interface for the given ClassDefinition to externs. This allows generated setter * Create exports and externs to protect element properties and methods from renaming and dead
* functions for read-only properties to avoid renaming altogether. * code removal.
* *
* @see https://www.polymer-project.org/0.8/docs/devguide/properties.html#read-only * <p>Since Polymer templates, observers, and computed properties rely on string references to
* element properties and methods, and because we don't have a way to update those references
* reliably, we instead export or extern them.
*
* <p>For properties, we create a new interface called {@code Polymer<ElementName>Interface}, 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}.
*
* <p>For methods, when {@code polymerExportPolicy = EXPORT_ALL}, we instead generate {@code
* goog.exportProperty} calls. This is preferable to using the externs, since it can result in
* better minified code (since compiled code can still use the minified name), avoids unnecessary
* {@code missingOverride} checks, and avoids needing to keep the signatures of the element class
* and the generated interface synchronized (previously the source of a bug).
*/ */
private void addInterfaceExterns( private void createExportsAndExterns(
final PolymerClassDefinition cls, final PolymerClassDefinition cls,
List<MemberDefinition> readOnlyProps, List<MemberDefinition> readOnlyProps,
List<MemberDefinition> attributeReflectedProps) { List<MemberDefinition> attributeReflectedProps) {
Expand Down Expand Up @@ -591,7 +590,14 @@ private void addInterfaceExterns(
for (MemberDefinition method : cls.methods) { for (MemberDefinition method : cls.methods) {
uniqueMethods.put(method.name.getString(), method); uniqueMethods.put(method.name.getString(), method);
} }
appendMethodsToBlock(uniqueMethods.values(), block, interfaceBasePath); for (MemberDefinition method : uniqueMethods.values()) {
addExportPropertyCall(
cls.target.getQualifiedName() + ".prototype",
method.name.getString(),
cls.target.getQualifiedName() + ".prototype." + method.name.getString(),
method.value,
cls.definition);
}


} else if (polymerVersion == 1) { } else if (polymerVersion == 1) {
// For Polymer 1, all declared properties are non-renameable // For Polymer 1, all declared properties are non-renameable
Expand Down Expand Up @@ -634,6 +640,47 @@ private void addInterfaceExterns(
compiler.reportChangeToEnclosingScope(stmts); compiler.reportChangeToEnclosingScope(stmts);
} }


/**
* Insert a call to {@code goog.exportProperty} (or equivalent function depending on current
* coding convention).
*
* <p>Note this function is very similar to {@link GenerateExports#addExportPropertyCall}.
*
* @param object Object whose static property is being exported (e.g. @{code MyClass.prototype}).
* @param publicName Unobfuscated name to export (e.g. @{code "myMethod"}).
* @param symbol Object the name should point to (e.g. @{code MyClass.prototype.myMethod}).
* @param source The context node to use for source info.
* @param insertAfter The call will be inserted as the next sibling of this node (or as close as
* possible).
*/
private void addExportPropertyCall(
String object, String publicName, String symbol, Node source, Node insertAfter) {
CodingConvention convention = compiler.getCodingConvention();

// Generate the `goog.exportProperty(object, publicName, symbol);` call.
String exportPropertyFunction = convention.getExportPropertyFunction();
Node call =
IR.call(
NodeUtil.newQName(
compiler, exportPropertyFunction,
source, exportPropertyFunction),
NodeUtil.newQName(
compiler, object,
source, exportPropertyFunction),
IR.string(publicName),
NodeUtil.newQName(
compiler, symbol,
source, exportPropertyFunction));
Node expression = IR.exprResult(call).useSourceInfoIfMissingFromForTree(source);

// Walk up until we find a statement we can insert after.
while (!NodeUtil.isStatementBlock(insertAfter.getParent())) {
insertAfter = insertAfter.getParent();
}
insertAfter.getParent().addChildAfter(expression, insertAfter);
compiler.reportChangeToEnclosingScope(expression);
}

/** /**
* @return The name of the generated extern interface which the element implements. * @return The name of the generated extern interface which the element implements.
*/ */
Expand Down
33 changes: 21 additions & 12 deletions test/com/google/javascript/jscomp/IntegrationTest.java
Expand Up @@ -55,6 +55,12 @@ public final class IntegrationTest extends IntegrationTestCase {
private static final String CLOSURE_COMPILED = private static final String CLOSURE_COMPILED =
"var COMPILED = true; var goog$exportSymbol = function() {};"; "var COMPILED = true; var goog$exportSymbol = function() {};";


private static final String EXPORT_PROPERTY_DEF =
lines(
"goog.exportProperty = function(object, publicName, symbol) {",
" object[publicName] = symbol;",
"};");

@Test @Test
public void testStaticMemberClass() { public void testStaticMemberClass() {
CompilerOptions options = createCompilerOptions(); CompilerOptions options = createCompilerOptions();
Expand Down Expand Up @@ -1034,18 +1040,20 @@ public void testPolymerExportPolicyExportAll() {
options.polymerExportPolicy = PolymerExportPolicy.EXPORT_ALL; options.polymerExportPolicy = PolymerExportPolicy.EXPORT_ALL;


Compiler compiler = Compiler compiler =
compile(options, compile(
lines( options,
"class FooElement extends PolymerElement {", lines(
" static get properties() {", EXPORT_PROPERTY_DEF,
" return {", "class FooElement extends PolymerElement {",
" longUnusedProperty: String,", " static get properties() {",
" }", " return {",
" }", " longUnusedProperty: String,",
" longUnusedMethod() {", " }",
" return this.longUnusedProperty;", " }",
" }", " longUnusedMethod() {",
"}")); " return this.longUnusedProperty;",
" }",
"}"));
String source = compiler.getCurrentJsSource(); String source = compiler.getCurrentJsSource();


// If we see these identifiers anywhere in the output source, we know that we successfully // If we see these identifiers anywhere in the output source, we know that we successfully
Expand Down Expand Up @@ -1074,6 +1082,7 @@ public void testPolymerPropertyDeclarationsWithConstructor() {
compile( compile(
options, options,
lines( lines(
EXPORT_PROPERTY_DEF,
"class FooElement extends PolymerElement {", "class FooElement extends PolymerElement {",
" constructor() {", " constructor() {",
" super();", " super();",
Expand Down
91 changes: 73 additions & 18 deletions test/com/google/javascript/jscomp/PolymerPassTest.java
Expand Up @@ -121,6 +121,12 @@ public class PolymerPassTest extends CompilerTestCase {
" */", " */",
"$jscomp.reflectObject = function (type, object) { return object; };"); "$jscomp.reflectObject = function (type, object) { return object; };");


private static final String EXPORT_PROPERTY_DEF =
lines(
"goog.exportProperty = function(object, publicName, symbol) {",
" object[publicName] = symbol;",
"};");

private int polymerVersion = 1; private int polymerVersion = 1;
private PolymerExportPolicy polymerExportPolicy = PolymerExportPolicy.LEGACY; private PolymerExportPolicy polymerExportPolicy = PolymerExportPolicy.LEGACY;
private boolean propertyRenamingEnabled = false; private boolean propertyRenamingEnabled = false;
Expand Down Expand Up @@ -3549,18 +3555,42 @@ public void testObjectReflectionAddedToConfigProperties3() {
"XElement.prototype.thingToDo;")); "XElement.prototype.thingToDo;"));
} }


@Test
public void textExportsMethodsFromClassBasedElement() {
polymerExportPolicy = PolymerExportPolicy.EXPORT_ALL;
test(
2,
lines(
EXPORT_PROPERTY_DEF,
"class TestElement extends PolymerElement {",
" method1() {}",
" method2() {}",
"}"),
lines(
EXPORT_PROPERTY_DEF,
"/** @implements {PolymerTestElementInterface} */",
"class TestElement extends PolymerElement {",
" method1() {}",
" method2() {}",
"}",
"goog.exportProperty(TestElement.prototype, \"method2\",",
" TestElement.prototype.method2);",
"goog.exportProperty(TestElement.prototype, \"method1\",",
" TestElement.prototype.method1);"));
}

/** /**
* When --polymer_export_policy=EXPORT_ALL, the PolymerPass will add all methods of an element to * When --polymer_export_policy=EXPORT_ALL, the PolymerPass will export all methods of an element
* that element's generated interface (which is injected into the externs), including methods * including methods inherited from Polymer Behaviors. Ensure that each method is included on the
* inherited from Polymer Behaviors. Ensure that each method is included on the interface only * interface only once, even when it is implemented in multiple places.
* once, even when it is implemented in multiple places.
*/ */
@Test @Test
public void testExportAllOnlyExportsEachMethodOnce() { public void textExportsUniqueMethodsFromLegacyElementAndBehaviors() {
polymerExportPolicy = PolymerExportPolicy.EXPORT_ALL; polymerExportPolicy = PolymerExportPolicy.EXPORT_ALL;

test(
String js = 2,
lines( lines(
EXPORT_PROPERTY_DEF,
"/** @polymerBehavior */", "/** @polymerBehavior */",
"const Behavior1 = {", "const Behavior1 = {",
" onAll: function() {},", " onAll: function() {},",
Expand All @@ -3576,18 +3606,43 @@ public void testExportAllOnlyExportsEachMethodOnce() {
" behaviors: [Behavior1, Behavior2],", " behaviors: [Behavior1, Behavior2],",
" onAll() {},", " onAll() {},",
" onElement: function() {},", " onElement: function() {},",
"});"); "});"),

String newExterns =
lines( lines(
EXTERNS, EXPORT_PROPERTY_DEF,
"/** @interface */ var PolymerTestElementElementInterface=function(){};", "/** @nocollapse @polymerBehavior */",
"PolymerTestElementElementInterface.prototype.onAll;", "const Behavior1 = {",
"PolymerTestElementElementInterface.prototype.onBehavior1;", " /** @suppress {checkTypes,globalThis,visibility} */ onAll: function() {},",
"PolymerTestElementElementInterface.prototype.onBehavior2;", " /** @suppress {checkTypes,globalThis,visibility} */ onBehavior1: function() {}",
"PolymerTestElementElementInterface.prototype.onElement"); "};",

"/** @nocollapse @polymerBehavior */",
testExternChanges(EXTERNS, js, newExterns); "const Behavior2 = {",
" /** @suppress {checkTypes,globalThis,visibility} */ onAll: function() {},",
" /** @suppress {checkTypes,globalThis,visibility} */ onBehavior2: function() {}",
"};",
"/**",
" * @constructor",
" * @extends {PolymerElement}",
" * @implements {PolymerTestElementElementInterface}",
" */",
"var TestElementElement = function() {};",
"/** @suppress {unusedPrivateMembers} */",
"TestElementElement.prototype.onBehavior1 = function() {};",
"/** @suppress {unusedPrivateMembers} */",
"TestElementElement.prototype.onBehavior2 = function() {};",
"Polymer(/** @lends {TestElementElement.prototype} */ {",
" is: \"test-element\",",
" behaviors: [Behavior1, Behavior2],",
" /** @this {TestElementElement} */ onAll() {},",
" /** @this {TestElementElement} */ onElement: function() {}",
"});",
"goog.exportProperty(TestElementElement.prototype, \"onElement\",",
" TestElementElement.prototype.onElement);",
"goog.exportProperty(TestElementElement.prototype, \"onBehavior2\",",
" TestElementElement.prototype.onBehavior2);",
"goog.exportProperty(TestElementElement.prototype, \"onBehavior1\",",
" TestElementElement.prototype.onBehavior1);",
"goog.exportProperty(TestElementElement.prototype, \"onAll\",",
" TestElementElement.prototype.onAll);"));
} }


@Override @Override
Expand Down

0 comments on commit 9c9d613

Please sign in to comment.