3
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7
* Code distributed by Google as part of the polymer project is also
8
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
12
import * as estraverse from 'estraverse';
13
import * as docs from './docs';
14
import * as esutil from './esutil';
15
import * as jsdoc from './jsdoc';
16
import * as analyzeProperties from './analyze-properties';
17
import * as astValue from './ast-value';
18
import * as estree from 'estree';
19
import {Visitor} from './fluent-traverse';
20
import {declarationPropertyHandlers, PropertyHandlers} from './declaration-property-handlers';
21
import {BehaviorDescriptor, LiteralValue} from './descriptors';
23
interface KeyFunc<T> {
27
function dedupe<T>(array: T[], keyFunc: KeyFunc<T>): T[] {
29
array.forEach((el) => {
30
var key = keyFunc(el);
36
var returned = <Array<T>>[];
37
Object.keys(bucket).forEach((k) => {
38
returned.push(bucket[k]);
43
// TODO(rictic): turn this into a class.
44
export function behaviorFinder() {
45
/** The behaviors we've found. */
46
var behaviors: BehaviorDescriptor[] = [];
48
var currentBehavior: BehaviorDescriptor = null;
49
var propertyHandlers: PropertyHandlers = null;
52
* merges behavior with preexisting behavior with the same name.
53
* here to support multiple @polymerBehavior tags referring
54
* to same behavior. See iron-multi-selectable for example.
56
function mergeBehavior(newBehavior: BehaviorDescriptor): BehaviorDescriptor {
57
var isBehaviorImpl = (b:string) => {
58
// filter out BehaviorImpl
59
return b.indexOf(newBehavior.is) === -1;
61
for (var i=0; i<behaviors.length; i++) {
62
if (newBehavior.is !== behaviors[i].is)
64
// merge desc, longest desc wins
65
if (newBehavior.desc) {
66
if (behaviors[i].desc) {
67
if (newBehavior.desc.length > behaviors[i].desc.length)
68
behaviors[i].desc = newBehavior.desc;
71
behaviors[i].desc = newBehavior.desc;
75
behaviors[i].demos = (behaviors[i].demos || []).concat(newBehavior.demos || []);
77
behaviors[i].events = (behaviors[i].events || []).concat(newBehavior.events || []);
78
behaviors[i].events = dedupe(behaviors[i].events, (e) => {return e.name});
80
behaviors[i].properties = (behaviors[i].properties || []).concat(newBehavior.properties || []);
82
behaviors[i].observers = (behaviors[i].observers || []).concat(newBehavior.observers || []);
84
behaviors[i].behaviors =
85
(behaviors[i].behaviors || []).concat(newBehavior.behaviors || [])
86
.filter(isBehaviorImpl);
93
* gets the expression representing a behavior from a node.
95
function behaviorExpression(node:estree.Node): estree.Node {
97
case 'ExpressionStatement':
98
// need to cast to `any` here because ExpressionStatement is super
99
// super general. this code is suspicious.
100
return (<any>node).expression.right;
101
case 'VariableDeclaration':
102
const n = <estree.VariableDeclaration>node;
103
return n.declarations.length > 0 ? n.declarations[0].init : null;
108
* checks whether an expression is a simple array containing only member
109
* expressions or identifiers.
111
function isSimpleBehaviorArray(expression: estree.Node): boolean {
112
if (!expression || expression.type !== 'ArrayExpression') return false;
113
const arrayExpr = <estree.ArrayExpression>expression;
114
for (var i=0; i < arrayExpr.elements.length; i++) {
115
if (arrayExpr.elements[i].type !== 'MemberExpression' &&
116
arrayExpr.elements[i].type !== 'Identifier') {
123
var templatizer = "Polymer.Templatizer";
125
function _parseChainedBehaviors(node: estree.Node) {
126
// if current behavior is part of an array, it gets extended by other behaviors
127
// inside the array. Ex:
128
// Polymer.IronMultiSelectableBehavior = [ {....}, Polymer.IronSelectableBehavior]
129
// We add these to behaviors array
130
var expression = behaviorExpression(node);
131
var chained:LiteralValue[] = [];
132
if (expression && expression.type === 'ArrayExpression') {
133
const arrExpr = <estree.ArrayExpression>expression;
134
for (var i=0; i < arrExpr.elements.length; i++) {
135
if (arrExpr.elements[i].type === 'MemberExpression' ||
136
arrExpr.elements[i].type === 'Identifier') {
137
chained.push(astValue.expressionToValue(arrExpr.elements[i]));
140
if (chained.length > 0)
141
currentBehavior.behaviors = chained;
145
function _initBehavior(node: estree.Node, getName: ()=>string) {
146
var comment = esutil.getAttachedComment(node);
147
var symbol = getName();
148
// Quickly filter down to potential candidates.
149
if (!comment || comment.indexOf('@polymerBehavior') === -1) {
150
if (symbol !== templatizer) {
159
events: esutil.getEventComments(node).map( function(event) {
160
return { desc: event};
163
propertyHandlers = declarationPropertyHandlers(currentBehavior);
165
docs.annotateBehavior(currentBehavior);
166
// Make sure that we actually parsed a behavior tag!
167
if (!jsdoc.hasTag(currentBehavior.jsdoc, 'polymerBehavior') &&
168
symbol !== templatizer) {
169
currentBehavior = null;
170
propertyHandlers = null;
174
var name = jsdoc.getTag(currentBehavior.jsdoc, 'polymerBehavior', 'name');
175
currentBehavior.symbol = symbol;
177
name = currentBehavior.symbol;
180
console.warn('Unable to determine name for @polymerBehavior:', comment);
182
currentBehavior.is = name;
184
_parseChainedBehaviors(node);
186
currentBehavior = mergeBehavior(currentBehavior);
187
propertyHandlers = declarationPropertyHandlers(currentBehavior);
189
// Some behaviors are just lists of other behaviors. If this is one then
190
// add it to behaviors right away.
191
if (isSimpleBehaviorArray(behaviorExpression(node))) {
192
// TODO(ajo): Add a test to confirm the presence of `properties`
193
if (!currentBehavior.observers) currentBehavior.observers = [];
194
if (!currentBehavior.properties) currentBehavior.properties = [];
195
if (behaviors.indexOf(currentBehavior) === -1)
196
behaviors.push(currentBehavior);
197
currentBehavior = null;
198
propertyHandlers = null;
202
var visitors: Visitor = {
205
* Look for object declarations with @behavior in the docs.
207
enterVariableDeclaration: function(node, parent) {
208
if (node.declarations.length !== 1) return; // Ambiguous.
209
_initBehavior(node, function () {
210
return esutil.objectKeyToString(node.declarations[0].id);
215
* Look for object assignments with @polymerBehavior in the docs.
217
enterAssignmentExpression: function(node, parent) {
218
_initBehavior(parent, function () {
219
return esutil.objectKeyToString(node.left);
224
* We assume that the object expression after such an assignment is the
225
* behavior's declaration. Seems to be a decent assumption for now.
227
enterObjectExpression: function(node, parent) {
228
if (!currentBehavior || currentBehavior.properties) return;
230
currentBehavior.properties = currentBehavior.properties || [];
231
currentBehavior.observers = currentBehavior.observers || [];
232
for (var i = 0; i < node.properties.length; i++) {
233
var prop = node.properties[i];
234
var name = esutil.objectKeyToString(prop.key);
237
message: 'Cant determine name for property key.',
238
location: node.loc.start
241
if (name in propertyHandlers) {
242
propertyHandlers[name](prop.value);
245
currentBehavior.properties.push(esutil.toPropertyDescriptor(prop));
248
behaviors.push(currentBehavior);
249
currentBehavior = null;
254
return {visitors, behaviors};