300 lines
9.9 KiB
JavaScript
300 lines
9.9 KiB
JavaScript
'use strict';
|
|
|
|
module.exports = angularRule;
|
|
|
|
|
|
/**
|
|
* Method names from an AngularJS module which can be chained.
|
|
*/
|
|
var angularChainableNames = [
|
|
'animation',
|
|
'component',
|
|
'config',
|
|
'constant',
|
|
'controller',
|
|
'directive',
|
|
'factory',
|
|
'filter',
|
|
'provider',
|
|
'run',
|
|
'service',
|
|
'value'
|
|
];
|
|
|
|
|
|
/**
|
|
* An angularRule defines a simplified interface for AngularJS component based rules.
|
|
*
|
|
* A full rule definition containing rules for all supported rules looks like this:
|
|
* ```js
|
|
* module.exports = angularRule(function(context) {
|
|
* return {
|
|
* 'angular?animation': function(configCallee, configFn) {},
|
|
* 'angular?component': function(componentCallee, componentObj) {},
|
|
* 'angular?config': function(configCallee, configFn) {},
|
|
* 'angular?controller': function(controllerCallee, controllerFn) {},
|
|
* 'angular?directive': function(directiveCallee, directiveFn) {},
|
|
* 'angular?factory': function(factoryCallee, factoryFn) {},
|
|
* 'angular?filter': function(filterCallee, filterFn) {},
|
|
* 'angular?inject': function(injectCallee, injectFn) {}, // inject() calls from angular-mocks
|
|
* 'angular?run': function(runCallee, runFn) {},
|
|
* 'angular?service': function(serviceCallee, serviceFn) {},
|
|
* 'angular?provider': function(providerCallee, providerFn, provider$getFn) {}
|
|
* };
|
|
* })
|
|
* ```
|
|
*/
|
|
function angularRule(ruleDefinition) {
|
|
var angularComponents;
|
|
var angularModuleCalls;
|
|
var angularModuleIdentifiers;
|
|
var angularChainables;
|
|
var injectCalls;
|
|
|
|
return wrapper;
|
|
|
|
function reset() {
|
|
angularComponents = [];
|
|
angularModuleCalls = [];
|
|
angularModuleIdentifiers = [];
|
|
angularChainables = [];
|
|
injectCalls = [];
|
|
}
|
|
|
|
/**
|
|
* A wrapper around the rule definition.
|
|
*/
|
|
function wrapper(context) {
|
|
reset();
|
|
var ruleObject = ruleDefinition(context);
|
|
injectCall(ruleObject, context, 'CallExpression:exit', checkCallee);
|
|
injectCall(ruleObject, context, 'Program:exit', callAngularRules);
|
|
return ruleObject;
|
|
}
|
|
|
|
/**
|
|
* Makes sure an extra function gets called after custom defined rule has run.
|
|
*/
|
|
function injectCall(ruleObject, context, propName, toCallAlso) {
|
|
var original = ruleObject[propName];
|
|
ruleObject[propName] = callBoth;
|
|
|
|
function callBoth(node) {
|
|
if (original) {
|
|
original.call(ruleObject, node);
|
|
}
|
|
toCallAlso(ruleObject, context, node);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect expressions from an entire Angular module call chain expression statement and inject calls.
|
|
*
|
|
* This collects the following nodes:
|
|
* ```js
|
|
* angular.module()
|
|
* ^^^^^^
|
|
* .animation('', function() {})
|
|
* ^^^^^^^^^ ^^^^^^^^^^
|
|
* .component('', {})
|
|
* ^^^^^^^^^
|
|
* .config(function() {})
|
|
* ^^^^^^ ^^^^^^^^^^
|
|
* .constant()
|
|
* ^^^^^^^^
|
|
* .controller('', function() {})
|
|
* ^^^^^^^^^^ ^^^^^^^^^^
|
|
* .directive('', function() {})
|
|
* ^^^^^^^^^ ^^^^^^^^^^
|
|
* .factory('', function() {})
|
|
* ^^^^^^^ ^^^^^^^^^^
|
|
* .filter('', function() {})
|
|
* ^^^^^^ ^^^^^^^^^^
|
|
* .provider('', function() {})
|
|
* ^^^^^^^^ ^^^^^^^^^^
|
|
* .run('', function() {})
|
|
* ^^^ ^^^^^^^^^^
|
|
* .service('', function() {})
|
|
* ^^^^^^^ ^^^^^^^^^^
|
|
* .value();
|
|
* ^^^^^
|
|
*
|
|
* inject(function() {})
|
|
* ^^^^^^ ^^^^^^^^^^
|
|
* ```
|
|
*/
|
|
function checkCallee(ruleObject, context, callExpressionNode) {
|
|
var callee = callExpressionNode.callee;
|
|
if (callee.type === 'Identifier') {
|
|
if (callee.name === 'inject') {
|
|
// inject()
|
|
// ^^^^^^
|
|
injectCalls.push({
|
|
callExpression: callExpressionNode,
|
|
fn: findFunctionByNode(callExpressionNode, context.getScope())
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (callee.type === 'MemberExpression') {
|
|
if (callee.object.name === 'angular' && callee.property.name === 'module') {
|
|
// angular.module()
|
|
// ^^^^^^
|
|
angularModuleCalls.push(callExpressionNode);
|
|
} else if (angularChainableNames.indexOf(callee.property.name !== -1) && (angularModuleCalls.indexOf(callee.object) !== -1 || angularChainables.indexOf(callee.object) !== -1)) {
|
|
// angular.module().factory().controller()
|
|
// ^^^^^^^ ^^^^^^^^^^
|
|
angularChainables.push(callExpressionNode);
|
|
angularComponents.push({
|
|
callExpression: callExpressionNode,
|
|
fn: findFunctionByNode(callExpressionNode, context.getScope())
|
|
});
|
|
} else if (callee.object.type === 'Identifier') {
|
|
// var app = angular.module(); app.factory()
|
|
// ^^^^^^^
|
|
var scope = context.getScope();
|
|
var isAngularModule = scope.variables.some(function(variable) {
|
|
if (callee.object.name !== variable.name) {
|
|
return false;
|
|
}
|
|
return variable.identifiers.some(function(id) {
|
|
return angularModuleIdentifiers.indexOf(id) !== -1;
|
|
});
|
|
});
|
|
if (isAngularModule) {
|
|
angularChainables.push(callExpressionNode);
|
|
angularComponents.push({
|
|
callExpression: callExpressionNode,
|
|
fn: findFunctionByNode(callExpressionNode, context.getScope())
|
|
});
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
if (callExpressionNode.parent.type === 'VariableDeclarator') {
|
|
// var app = angular.module()
|
|
// ^^^
|
|
angularModuleIdentifiers.push(callExpressionNode.parent.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the function expression or function declaration by an Angular component callee.
|
|
*/
|
|
function findFunctionByNode(callExpressionNode, scope) {
|
|
var node;
|
|
if (callExpressionNode.callee.type === 'Identifier') {
|
|
node = callExpressionNode.arguments[0];
|
|
} else if (callExpressionNode.callee.property.name === 'run' || callExpressionNode.callee.property.name === 'config') {
|
|
node = callExpressionNode.arguments[0];
|
|
} else {
|
|
node = callExpressionNode.arguments[1];
|
|
}
|
|
if (!node) {
|
|
return;
|
|
}
|
|
if (node.type === 'ArrayExpression') {
|
|
node = node.elements[node.elements.length - 1] || {};
|
|
}
|
|
if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionDeclaration') {
|
|
return node;
|
|
}
|
|
if (node.type !== 'Identifier') {
|
|
return;
|
|
}
|
|
|
|
var func;
|
|
scope.variables.some(function(variable) {
|
|
if (variable.name === node.name) {
|
|
variable.defs.forEach(function(def) {
|
|
if (def.node.type === 'FunctionDeclaration') {
|
|
func = def.node;
|
|
return true;
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
});
|
|
return func;
|
|
}
|
|
|
|
/**
|
|
* Call the Angular specific rules defined by the rule definition.
|
|
*/
|
|
function callAngularRules(ruleObject, context) {
|
|
angularComponents.forEach(function(component) {
|
|
var name = component.callExpression.callee.property.name;
|
|
var fn = ruleObject['angular?' + name];
|
|
if (!fn) {
|
|
return;
|
|
}
|
|
fn.apply(ruleObject, assembleArguments(component, context));
|
|
});
|
|
var injectRule = ruleObject['angular?inject'];
|
|
if (injectRule) {
|
|
injectCalls.forEach(function(node) {
|
|
injectRule.call(ruleObject, node.CallExpression, node.fn);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assemble the arguments for an Angular callee check.
|
|
*/
|
|
function assembleArguments(node) {
|
|
switch (node.callExpression.callee.property.name) {
|
|
case 'animation':
|
|
case 'component':
|
|
case 'config':
|
|
case 'controller':
|
|
case 'directive':
|
|
case 'factory':
|
|
case 'filter':
|
|
case 'run':
|
|
case 'service':
|
|
return [node.callExpression.callee, node.fn];
|
|
case 'provider':
|
|
return assembleProviderArguments(node);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assemble arguments for a provider rule.
|
|
*
|
|
* On top of a regular Angular component rule, the provider rule gets called with the $get function as its 3rd argument.
|
|
*/
|
|
function assembleProviderArguments(node) {
|
|
return [node.callExpression, node.fn, findProviderGet(node.fn)];
|
|
}
|
|
|
|
/**
|
|
* Find the $get function of a provider based on the provider function body.
|
|
*/
|
|
function findProviderGet(providerFn) {
|
|
if (!providerFn) {
|
|
return;
|
|
}
|
|
var getFn;
|
|
providerFn.body.body.some(function(statement) {
|
|
var expression = statement.expression;
|
|
if (!expression || expression.type !== 'AssignmentExpression') {
|
|
return;
|
|
}
|
|
if (expression.left.type === 'MemberExpression' && expression.left.property.name === '$get') {
|
|
getFn = expression.right;
|
|
return true;
|
|
}
|
|
});
|
|
if (!getFn) {
|
|
return;
|
|
}
|
|
if (getFn.type === 'ArrayExpression') {
|
|
return getFn.elements[getFn.elements.length - 1];
|
|
}
|
|
return getFn;
|
|
}
|
|
}
|