Skip to content

Commit

Permalink
Recursively resolve Polymer Behavior assignments in the Closure Polym…
Browse files Browse the repository at this point in the history
…erPass.

We now resolve chains of behavior identifiers recursively, which enables patterns like `const foo = <behavior definition>; const bar = foo;`. Previously only a flat assignment was supported.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=204864284
  • Loading branch information
aomarks authored and tjgq committed Jul 18, 2018
1 parent 80c8949 commit 95ebaa2
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 35 deletions.
110 changes: 75 additions & 35 deletions src/com/google/javascript/jscomp/PolymerBehaviorExtractor.java
Expand Up @@ -26,6 +26,7 @@
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import java.util.List;
import javax.annotation.Nullable;

/**
* Helpers to extract behaviors from Polymer element declarations.
Expand Down Expand Up @@ -81,44 +82,14 @@ ImmutableList<BehaviorDefinition> extractBehaviors(Node behaviorArray) {
continue;
}

Name behaviorGlobalName = globalNames.getSlot(behaviorName.getQualifiedName());
boolean isGlobalDeclaration = true;
if (behaviorGlobalName == null) {
ResolveBehaviorNameResult resolveResult = resolveBehaviorName(behaviorName);
if (resolveResult == null) {
compiler.report(JSError.make(behaviorName, PolymerPassErrors.POLYMER_UNQUALIFIED_BEHAVIOR));
continue;
}
Node behaviorValue = resolveResult.node;

Ref behaviorDeclaration = behaviorGlobalName.getDeclaration();

// Use any set as a backup declaration, even if it's local.
if (behaviorDeclaration == null) {
List<Ref> behaviorRefs = behaviorGlobalName.getRefs();
for (Ref ref : behaviorRefs) {
if (ref.isSet()) {
isGlobalDeclaration = false;
behaviorDeclaration = ref;
break;
}
}
}

if (behaviorDeclaration == null) {
compiler.report(JSError.make(behaviorName, PolymerPassErrors.POLYMER_UNQUALIFIED_BEHAVIOR));
continue;
}

Node behaviorDeclarationNode = behaviorDeclaration.getNode();
JSDocInfo behaviorInfo = NodeUtil.getBestJSDocInfo(behaviorDeclarationNode);
if (behaviorInfo == null || !behaviorInfo.isPolymerBehavior()) {
compiler.report(
JSError.make(behaviorDeclarationNode, PolymerPassErrors.POLYMER_UNANNOTATED_BEHAVIOR));
}

Node behaviorValue = NodeUtil.getRValueOfLValue(behaviorDeclarationNode);

if (behaviorValue == null) {
compiler.report(JSError.make(behaviorName, PolymerPassErrors.POLYMER_UNQUALIFIED_BEHAVIOR));
} else if (behaviorValue.isArrayLit()) {
if (behaviorValue.isArrayLit()) {
// Individual behaviors can also be arrays of behaviors. Parse them recursively.
behaviors.addAll(extractBehaviors(behaviorValue));
} else if (behaviorValue.isObjectLit()) {
Expand All @@ -133,7 +104,7 @@ ImmutableList<BehaviorDefinition> extractBehaviors(Node behaviorArray) {
behaviorValue, PolymerClassDefinition.DefinitionType.ObjectLiteral, compiler),
getBehaviorFunctionsToCopy(behaviorValue),
getNonPropertyMembersToCopy(behaviorValue),
isGlobalDeclaration,
resolveResult.isGlobalDeclaration,
(FeatureSet) NodeUtil.getEnclosingScript(behaviorValue).getProp(Node.FEATURE_SET)));
} else {
compiler.report(JSError.make(behaviorName, PolymerPassErrors.POLYMER_UNQUALIFIED_BEHAVIOR));
Expand All @@ -143,6 +114,75 @@ ImmutableList<BehaviorDefinition> extractBehaviors(Node behaviorArray) {
return behaviors.build();
}

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

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

/**
* Resolve an identifier, which is presumed to refer to a Polymer Behavior declaration, using the
* global namespace. Recurses to resolve assignment chains of any length.
*
* @param nameNode The NAME or GETPROP node containing the identifier.
* @return The behavior declaration node, or null if it couldn't be resolved.
*/
@Nullable
private ResolveBehaviorNameResult resolveBehaviorName(Node nameNode) {
String name = nameNode.getQualifiedName();
if (name == null) {
return null;
}
Name globalName = globalNames.getSlot(name);
if (globalName == null) {
return null;
}

boolean isGlobalDeclaration = true;

// Use any set as a backup declaration, even if it's local.
Ref declarationRef = globalName.getDeclaration();
if (declarationRef == null) {
List<Ref> behaviorRefs = globalName.getRefs();
for (Ref ref : behaviorRefs) {
if (ref.isSet()) {
isGlobalDeclaration = false;
declarationRef = ref;
break;
}
}
}
if (declarationRef == null) {
return null;
}

Node declarationNode = declarationRef.getNode();
if (declarationNode == null) {
return null;
}
Node rValue = NodeUtil.getRValueOfLValue(declarationNode);
if (rValue == null) {
return null;
}

if (rValue.isQualifiedName()) {
// Another identifier; recurse.
return resolveBehaviorName(rValue);
}

JSDocInfo behaviorInfo = NodeUtil.getBestJSDocInfo(declarationNode);
if (behaviorInfo == null || !behaviorInfo.isPolymerBehavior()) {
compiler.report(
JSError.make(declarationNode, PolymerPassErrors.POLYMER_UNANNOTATED_BEHAVIOR));
}

return new ResolveBehaviorNameResult(rValue, isGlobalDeclaration);
}

/**
* @return A list of functions from a behavior which should be copied to the element prototype.
*/
Expand Down
41 changes: 41 additions & 0 deletions test/com/google/javascript/jscomp/PolymerPassTest.java
Expand Up @@ -1738,6 +1738,47 @@ public void testSimpleBehavior() {
});
}

/** Check that we can resolve behaviors through a chain of identifiers. */
public void testIndirectBehaviorAssignment() {
test(
srcs(
lines(
"/** @polymerBehavior */",
"var MyBehavior = {",
" properties: {",
" behaviorProperty: Boolean",
" }",
"};",
"var BehaviorAlias1 = MyBehavior;",
"var BehaviorAlias2 = BehaviorAlias1;",
"var MyElement = Polymer({",
" is: 'my-element',",
" behaviors: [ BehaviorAlias2 ]",
"});")),
expected(
lines(
"/** @polymerBehavior @nocollapse */",
"var MyBehavior = {",
" properties: {",
" behaviorProperty: Boolean",
" }",
"};",
"var BehaviorAlias1 = MyBehavior;",
"var BehaviorAlias2 = BehaviorAlias1;",
"/**",
" * @constructor",
" * @extends {PolymerElement}",
" * @implements {PolymerMyElementInterface}",
" */",
"var MyElement = function(){};",
"/** @type {boolean} */",
"MyElement.prototype.behaviorProperty;",
"MyElement = Polymer(/** @lends {MyElement.prototype} */ {",
" is: 'my-element',",
" behaviors: [ BehaviorAlias2 ]",
"});")));
}

private static class DoSomethingFunFinder implements Visitor {
boolean found = false;

Expand Down

0 comments on commit 95ebaa2

Please sign in to comment.