Skip to content

Commit

Permalink
Rewrite prefer-stateless-function rule (fixes #491)
Browse files Browse the repository at this point in the history
  • Loading branch information
yannickcr committed Mar 12, 2016
1 parent 426b228 commit cfca370
Show file tree
Hide file tree
Showing 2 changed files with 361 additions and 30 deletions.
152 changes: 126 additions & 26 deletions lib/rules/prefer-stateless-function.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* @fileoverview Enforce stateless components to be written as a pure function
* @author Yannick Croissant
* @author Alberto Rodríguez
* @copyright 2015 Alberto Rodríguez. All rights reserved.
*/
'use strict';

Expand All @@ -14,19 +16,6 @@ module.exports = Components.detect(function(context, components, utils) {

var sourceCode = context.getSourceCode();

var lifecycleMethods = [
'state',
'getInitialState',
'getChildContext',
'componentWillMount',
'componentDidMount',
'componentWillReceiveProps',
'shouldComponentUpdate',
'componentWillUpdate',
'componentDidUpdate',
'componentWillUnmount'
];

// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
Expand Down Expand Up @@ -64,24 +53,87 @@ module.exports = Components.detect(function(context, components, utils) {
}

/**
* Check if a given AST node have any lifecycle method
* Checks whether the constructor body is a redundant super call.
* @see ESLint no-useless-constructor rule
* @param {Array} body - constructor body content.
* @param {Array} ctorParams - The params to check against super call.
* @returns {boolean} true if the construtor body is redundant
*/
function isRedundantSuperCall(body, ctorParams) {
if (
body.length !== 1 ||
body[0].type !== 'ExpressionStatement' ||
body[0].expression.callee.type !== 'Super'
) {
return false;
}

var superArgs = body[0].expression.arguments;
var firstSuperArg = superArgs[0];
var lastSuperArgIndex = superArgs.length - 1;
var lastSuperArg = superArgs[lastSuperArgIndex];
var isSimpleParameterList = ctorParams.every(function(param) {
return param.type === 'Identifier' || param.type === 'RestElement';
});

/**
* Checks if a super argument is the same with constructor argument
* @param {ASTNode} arg argument node
* @param {number} index argument index
* @returns {boolean} true if the arguments are same, false otherwise
*/
function isSameIdentifier(arg, index) {
return (
arg.type === 'Identifier' &&
arg.name === ctorParams[index].name
);
}

var spreadsArguments =
superArgs.length === 1 &&
firstSuperArg.type === 'SpreadElement' &&
firstSuperArg.argument.name === 'arguments';

var passesParamsAsArgs =
superArgs.length === ctorParams.length &&
superArgs.every(isSameIdentifier) ||
superArgs.length <= ctorParams.length &&
superArgs.slice(0, -1).every(isSameIdentifier) &&
lastSuperArg.type === 'SpreadElement' &&
ctorParams[lastSuperArgIndex].type === 'RestElement' &&
lastSuperArg.argument.name === ctorParams[lastSuperArgIndex].argument.name;

return isSimpleParameterList && (spreadsArguments || passesParamsAsArgs);
}

/**
* Check if a given AST node have any other properties the ones available in stateless components
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if the node has at least one lifecycle method, false if not.
* @returns {Boolean} True if the node has at least one other property, false if not.
*/
function hasLifecycleMethod(node) {
function hasOtherProperties(node) {
var properties = getComponentProperties(node);
return properties.some(function(property) {
return lifecycleMethods.indexOf(getPropertyName(property)) !== -1;
var name = getPropertyName(property);
var isDisplayName = name === 'displayName';
var isPropTypes = name === 'propTypes' || name === 'props' && property.typeAnnotation;
var contextTypes = name === 'contextTypes';
var isUselessConstructor =
property.kind === 'constructor' &&
isRedundantSuperCall(property.value.body.body, property.value.params)
;
var isRender = name === 'render';
return !isDisplayName && !isPropTypes && !contextTypes && !isUselessConstructor && !isRender;
});
}

/**
* Mark a setState as used
* @param {ASTNode} node The AST node being checked.
*/
function markSetStateAsUsed(node) {
function markThisAsUsed(node) {
components.set(node, {
useSetState: true
useThis: true
});
}

Expand All @@ -95,18 +147,48 @@ module.exports = Components.detect(function(context, components, utils) {
});
}

/**
* Mark return as invalid
* @param {ASTNode} node The AST node being checked.
*/
function markReturnAsInvalid(node) {
components.set(node, {
invalidReturn: true
});
}

return {
CallExpression: function(node) {
var callee = node.callee;
if (callee.type !== 'MemberExpression') {
// Mark `this` destructuring as a usage of `this`
VariableDeclarator: function(node) {
// Ignore destructuring on other than `this`
if (!node.id || node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'ThisExpression') {
return;
}
if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'setState') {
// Ignore `props` and `context`
var useThis = node.id.properties.some(function(property) {
var name = getPropertyName(property);
return name !== 'props' && name !== 'context';
});
if (!useThis) {
return;
}
markThisAsUsed(node);
},

// Mark `this` usage
MemberExpression: function(node) {
// Ignore calls to `this.props` and `this.context`
if (
node.object.type !== 'ThisExpression' ||
(node.property.name || node.property.value) === 'props' ||
(node.property.name || node.property.value) === 'context'
) {
return;
}
markSetStateAsUsed(node);
markThisAsUsed(node);
},

// Mark `ref` usage
JSXAttribute: function(node) {
var name = sourceCode.getText(node.name);
if (name !== 'ref') {
Expand All @@ -115,14 +197,32 @@ module.exports = Components.detect(function(context, components, utils) {
markRefAsUsed(node);
},

// Mark `render` that do not return some JSX
ReturnStatement: function(node) {
var blockNode;
var scope = context.getScope();
while (scope) {
blockNode = scope.block && scope.block.parent;
if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) {
break;
}
scope = scope.upper;
}
if (!blockNode || !blockNode.key || blockNode.key.name !== 'render' || utils.isReturningJSX(node)) {
return;
}
markReturnAsInvalid(node);
},

'Program:exit': function() {
var list = components.list();
for (var component in list) {
if (
!list.hasOwnProperty(component) ||
hasLifecycleMethod(list[component].node) ||
list[component].useSetState ||
hasOtherProperties(list[component].node) ||
list[component].useThis ||
list[component].useRef ||
list[component].invalidReturn ||
(!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
) {
continue;
Expand Down
Loading

0 comments on commit cfca370

Please sign in to comment.