Files
asciidisco.com/build/node_modules/hydrolysis/lib/ast-utils/behavior-finder.js
2023-08-01 13:49:46 +02:00

226 lines
7.7 KiB
JavaScript

/**
* @license
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
// jshint node: true
'use strict';
var estraverse = require('estraverse');
var docs = require('./docs');
var esutil = require('./esutil');
var jsdoc = require('./jsdoc');
var analyzeProperties = require('./analyze-properties');
var astValue = require('./ast-value.js');
var declarationPropertyHandlers = require('./declaration-property-handlers');
module.exports = function behaviorFinder() {
/** @type {!Array<BehaviorDescriptor>} The behaviors we've found. */
var behaviors = [];
var currentBehavior = null;
var propertyHandlers = null;
/**
* merges behavior with preexisting behavior with the same name.
* here to support multiple @polymerBehavior tags referring
* to same behavior. See iron-multi-selectable for example.
*/
function mergeBehavior(newBehavior) {
var isBehaviorImpl = function(b) { // filter out BehaviorImpl
return b.indexOf(newBehavior.is) === -1;
};
for (var i=0; i<behaviors.length; i++) {
if (newBehavior.is !== behaviors[i].is)
continue;
// merge desc, longest desc wins
if (newBehavior.desc) {
if (behaviors[i].desc) {
if (newBehavior.desc.length > behaviors[i].desc.length)
behaviors[i].desc = newBehavior.desc;
}
else {
behaviors[i].desc = newBehavior.desc;
}
}
// merge demos
behaviors[i].demos = (behaviors[i].demos || []).concat(newBehavior.demos || []);
// merge events,
behaviors[i].events = (behaviors[i].events || []).concat(newBehavior.events || []);
// merge properties
behaviors[i].properties = (behaviors[i].properties || []).concat(newBehavior.properties || []);
// merge observers
behaviors[i].observers = (behaviors[i].observers || []).concat(newBehavior.observers || []);
// merge behaviors
behaviors[i].behaviors =
(behaviors[i].behaviors || []).concat(newBehavior.behaviors || [])
.filter(isBehaviorImpl);
return behaviors[i];
}
return newBehavior;
}
/**
* gets the expression representing a behavior from a node.
*/
function behaviorExpression(node) {
switch(node.type) {
case 'ExpressionStatement':
return node.expression.right;
case 'VariableDeclaration':
return node.declarations.length > 0 ? node.declarations[0].init : null;
}
}
/**
* checks whether an expression is a simple array containing only member
* expressions or identifiers.
*/
function isSimpleBehaviorArray(expression) {
if (!expression || expression.type !== 'ArrayExpression') return false;
for (var i=0; i < expression.elements.length; i++) {
if (expression.elements[i].type !== 'MemberExpression' &&
expression.elements[i].type !== 'Identifier') {
return false;
}
}
return true;
}
var templatizer = "Polymer.Templatizer";
var visitors = {
/**
* Look for object declarations with @behavior in the docs.
*/
enterVariableDeclaration: function(node, parent) {
if (node.declarations.length !== 1) return; // Ambiguous.
this._initBehavior(node, function () {
return esutil.objectKeyToString(node.declarations[0].id);
});
},
/**
* Look for object assignments with @polymerBehavior in the docs.
*/
enterAssignmentExpression: function(node, parent) {
this._initBehavior(parent, function () {
return esutil.objectKeyToString(node.left);
});
},
_parseChainedBehaviors: function(node) {
// if current behavior is part of an array, it gets extended by other behaviors
// inside the array. Ex:
// Polymer.IronMultiSelectableBehavior = [ {....}, Polymer.IronSelectableBehavior]
// We add these to behaviors array
var expression = behaviorExpression(node);
var chained = [];
if (expression && expression.type === 'ArrayExpression') {
for (var i=0; i < expression.elements.length; i++) {
if (expression.elements[i].type === 'MemberExpression' ||
expression.elements[i].type === 'Identifier') {
chained.push(astValue.expressionToValue(expression.elements[i]));
}
}
if (chained.length > 0)
currentBehavior.behaviors = chained;
}
},
_initBehavior: function(node, getName) {
var comment = esutil.getAttachedComment(node);
var symbol = getName();
// Quickly filter down to potential candidates.
if (!comment || comment.indexOf('@polymerBehavior') === -1) {
if (symbol !== templatizer) {
return;
}
}
currentBehavior = {
type: 'behavior',
desc: comment,
events: esutil.getEventComments(node).map( function(event) {
return { desc: event};
})
};
propertyHandlers = declarationPropertyHandlers(currentBehavior);
docs.annotateBehavior(currentBehavior);
// Make sure that we actually parsed a behavior tag!
if (!jsdoc.hasTag(currentBehavior.jsdoc, 'polymerBehavior') &&
symbol !== templatizer) {
currentBehavior = null;
propertyHandlers = null;
return;
}
var name = jsdoc.getTag(currentBehavior.jsdoc, 'polymerBehavior', 'name');
currentBehavior.symbol = symbol;
if (!name) {
name = currentBehavior.symbol;
}
if (!name) {
console.warn('Unable to determine name for @polymerBehavior:', comment);
}
currentBehavior.is = name;
this._parseChainedBehaviors(node);
currentBehavior = mergeBehavior(currentBehavior);
propertyHandlers = declarationPropertyHandlers(currentBehavior);
// Some behaviors are just lists of other behaviors. If this is one then
// add it to behaviors right away.
if (isSimpleBehaviorArray(behaviorExpression(node))) {
// TODO(ajo): Add a test to confirm the presence of `properties`
if (!currentBehavior.observers) currentBehavior.observers = [];
if (!currentBehavior.properties) currentBehavior.properties = [];
if (behaviors.indexOf(currentBehavior) === -1)
behaviors.push(currentBehavior);
currentBehavior = null;
propertyHandlers = null;
}
},
/**
* We assume that the object expression after such an assignment is the
* behavior's declaration. Seems to be a decent assumption for now.
*/
enterObjectExpression: function(node, parent) {
if (!currentBehavior || currentBehavior.properties) return;
currentBehavior.properties = currentBehavior.properties || [];
currentBehavior.observers = currentBehavior.observers || [];
for (var i = 0; i < node.properties.length; i++) {
var prop = node.properties[i];
var name = esutil.objectKeyToString(prop.key);
if (!name) {
throw {
message: 'Cant determine name for property key.',
location: node.loc.start
};
}
if (name in propertyHandlers) {
propertyHandlers[name](prop.value);
}
else {
currentBehavior.properties.push(esutil.toPropertyDescriptor(prop));
}
}
behaviors.push(currentBehavior);
currentBehavior = null;
},
};
return {visitors: visitors, behaviors: behaviors};
};