|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- /**
- * @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';
-
- const has = require('has');
- const Components = require('../util/Components');
- const versionUtil = require('../util/version');
- const astUtil = require('../util/ast');
- const docsUrl = require('../util/docsUrl');
-
- // ------------------------------------------------------------------------------
- // Rule Definition
- // ------------------------------------------------------------------------------
-
- module.exports = {
- meta: {
- docs: {
- description: 'Enforce stateless components to be written as a pure function',
- category: 'Stylistic Issues',
- recommended: false,
- url: docsUrl('prefer-stateless-function')
- },
- schema: [{
- type: 'object',
- properties: {
- ignorePureComponents: {
- default: false,
- type: 'boolean'
- }
- },
- additionalProperties: false
- }]
- },
-
- create: Components.detect((context, components, utils) => {
- const configuration = context.options[0] || {};
- const ignorePureComponents = configuration.ignorePureComponents || false;
-
- const sourceCode = context.getSourceCode();
-
- // --------------------------------------------------------------------------
- // Public
- // --------------------------------------------------------------------------
-
- /**
- * Checks whether a given array of statements is a single call of `super`.
- * @see ESLint no-useless-constructor rule
- * @param {ASTNode[]} body - An array of statements to check.
- * @returns {boolean} `true` if the body is a single call of `super`.
- */
- function isSingleSuperCall(body) {
- return (
- body.length === 1 &&
- body[0].type === 'ExpressionStatement' &&
- body[0].expression.type === 'CallExpression' &&
- body[0].expression.callee.type === 'Super'
- );
- }
-
- /**
- * Checks whether a given node is a pattern which doesn't have any side effects.
- * Default parameters and Destructuring parameters can have side effects.
- * @see ESLint no-useless-constructor rule
- * @param {ASTNode} node - A pattern node.
- * @returns {boolean} `true` if the node doesn't have any side effects.
- */
- function isSimple(node) {
- return node.type === 'Identifier' || node.type === 'RestElement';
- }
-
- /**
- * Checks whether a given array of expressions is `...arguments` or not.
- * `super(...arguments)` passes all arguments through.
- * @see ESLint no-useless-constructor rule
- * @param {ASTNode[]} superArgs - An array of expressions to check.
- * @returns {boolean} `true` if the superArgs is `...arguments`.
- */
- function isSpreadArguments(superArgs) {
- return (
- superArgs.length === 1 &&
- superArgs[0].type === 'SpreadElement' &&
- superArgs[0].argument.type === 'Identifier' &&
- superArgs[0].argument.name === 'arguments'
- );
- }
-
- /**
- * Checks whether given 2 nodes are identifiers which have the same name or not.
- * @see ESLint no-useless-constructor rule
- * @param {ASTNode} ctorParam - A node to check.
- * @param {ASTNode} superArg - A node to check.
- * @returns {boolean} `true` if the nodes are identifiers which have the same
- * name.
- */
- function isValidIdentifierPair(ctorParam, superArg) {
- return (
- ctorParam.type === 'Identifier' &&
- superArg.type === 'Identifier' &&
- ctorParam.name === superArg.name
- );
- }
-
- /**
- * Checks whether given 2 nodes are a rest/spread pair which has the same values.
- * @see ESLint no-useless-constructor rule
- * @param {ASTNode} ctorParam - A node to check.
- * @param {ASTNode} superArg - A node to check.
- * @returns {boolean} `true` if the nodes are a rest/spread pair which has the
- * same values.
- */
- function isValidRestSpreadPair(ctorParam, superArg) {
- return (
- ctorParam.type === 'RestElement' &&
- superArg.type === 'SpreadElement' &&
- isValidIdentifierPair(ctorParam.argument, superArg.argument)
- );
- }
-
- /**
- * Checks whether given 2 nodes have the same value or not.
- * @see ESLint no-useless-constructor rule
- * @param {ASTNode} ctorParam - A node to check.
- * @param {ASTNode} superArg - A node to check.
- * @returns {boolean} `true` if the nodes have the same value or not.
- */
- function isValidPair(ctorParam, superArg) {
- return (
- isValidIdentifierPair(ctorParam, superArg) ||
- isValidRestSpreadPair(ctorParam, superArg)
- );
- }
-
- /**
- * Checks whether the parameters of a constructor and the arguments of `super()`
- * have the same values or not.
- * @see ESLint no-useless-constructor rule
- * @param {ASTNode} ctorParams - The parameters of a constructor to check.
- * @param {ASTNode} superArgs - The arguments of `super()` to check.
- * @returns {boolean} `true` if those have the same values.
- */
- function isPassingThrough(ctorParams, superArgs) {
- if (ctorParams.length !== superArgs.length) {
- return false;
- }
-
- for (let i = 0; i < ctorParams.length; ++i) {
- if (!isValidPair(ctorParams[i], superArgs[i])) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * 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) {
- return (
- isSingleSuperCall(body) &&
- ctorParams.every(isSimple) &&
- (
- isSpreadArguments(body[0].expression.arguments) ||
- isPassingThrough(ctorParams, body[0].expression.arguments)
- )
- );
- }
-
- /**
- * 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 other property, false if not.
- */
- function hasOtherProperties(node) {
- const properties = astUtil.getComponentProperties(node);
- return properties.some(property => {
- const name = astUtil.getPropertyName(property);
- const isDisplayName = name === 'displayName';
- const isPropTypes = name === 'propTypes' || name === 'props' && property.typeAnnotation;
- const contextTypes = name === 'contextTypes';
- const defaultProps = name === 'defaultProps';
- const isUselessConstructor =
- property.kind === 'constructor' &&
- isRedundantSuperCall(property.value.body.body, property.value.params)
- ;
- const isRender = name === 'render';
- return !isDisplayName && !isPropTypes && !contextTypes && !defaultProps && !isUselessConstructor && !isRender;
- });
- }
-
- /**
- * Mark component as pure as declared
- * @param {ASTNode} node The AST node being checked.
- */
- const markSCUAsDeclared = function (node) {
- components.set(node, {
- hasSCU: true
- });
- };
-
- /**
- * Mark childContextTypes as declared
- * @param {ASTNode} node The AST node being checked.
- */
- const markChildContextTypesAsDeclared = function (node) {
- components.set(node, {
- hasChildContextTypes: true
- });
- };
-
- /**
- * Mark a setState as used
- * @param {ASTNode} node The AST node being checked.
- */
- function markThisAsUsed(node) {
- components.set(node, {
- useThis: true
- });
- }
-
- /**
- * Mark a props or context as used
- * @param {ASTNode} node The AST node being checked.
- */
- function markPropsOrContextAsUsed(node) {
- components.set(node, {
- usePropsOrContext: true
- });
- }
-
- /**
- * Mark a ref as used
- * @param {ASTNode} node The AST node being checked.
- */
- function markRefAsUsed(node) {
- components.set(node, {
- useRef: true
- });
- }
-
- /**
- * Mark return as invalid
- * @param {ASTNode} node The AST node being checked.
- */
- function markReturnAsInvalid(node) {
- components.set(node, {
- invalidReturn: true
- });
- }
-
- /**
- * Mark a ClassDeclaration as having used decorators
- * @param {ASTNode} node The AST node being checked.
- */
- function markDecoratorsAsUsed(node) {
- components.set(node, {
- useDecorators: true
- });
- }
-
- function visitClass(node) {
- if (ignorePureComponents && utils.isPureComponent(node)) {
- markSCUAsDeclared(node);
- }
-
- if (node.decorators && node.decorators.length) {
- markDecoratorsAsUsed(node);
- }
- }
-
- return {
- ClassDeclaration: visitClass,
- ClassExpression: visitClass,
-
- // 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;
- }
- // Ignore `props` and `context`
- const useThis = node.id.properties.some(property => {
- const name = astUtil.getPropertyName(property);
- return name !== 'props' && name !== 'context';
- });
- if (!useThis) {
- markPropsOrContextAsUsed(node);
- return;
- }
- markThisAsUsed(node);
- },
-
- // Mark `this` usage
- MemberExpression: function(node) {
- if (node.object.type !== 'ThisExpression') {
- if (node.property && node.property.name === 'childContextTypes') {
- const component = utils.getRelatedComponent(node);
- if (!component) {
- return;
- }
- markChildContextTypesAsDeclared(component.node);
- return;
- }
- return;
- // Ignore calls to `this.props` and `this.context`
- } else if (
- (node.property.name || node.property.value) === 'props' ||
- (node.property.name || node.property.value) === 'context'
- ) {
- markPropsOrContextAsUsed(node);
- return;
- }
- markThisAsUsed(node);
- },
-
- // Mark `ref` usage
- JSXAttribute: function(node) {
- const name = sourceCode.getText(node.name);
- if (name !== 'ref') {
- return;
- }
- markRefAsUsed(node);
- },
-
- // Mark `render` that do not return some JSX
- ReturnStatement: function(node) {
- let blockNode;
- let scope = context.getScope();
- while (scope) {
- blockNode = scope.block && scope.block.parent;
- if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) {
- break;
- }
- scope = scope.upper;
- }
- const isRender = blockNode && blockNode.key && blockNode.key.name === 'render';
- const allowNull = versionUtil.testReactVersion(context, '15.0.0'); // Stateless components can return null since React 15
- const isReturningJSX = utils.isReturningJSX(node, !allowNull);
- const isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false);
- if (
- !isRender ||
- (allowNull && (isReturningJSX || isReturningNull)) ||
- (!allowNull && isReturningJSX)
- ) {
- return;
- }
- markReturnAsInvalid(node);
- },
-
- 'Program:exit': function() {
- const list = components.list();
- for (const component in list) {
- if (
- !has(list, component) ||
- hasOtherProperties(list[component].node) ||
- list[component].useThis ||
- list[component].useRef ||
- list[component].invalidReturn ||
- list[component].hasChildContextTypes ||
- list[component].useDecorators ||
- (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
- ) {
- continue;
- }
-
- if (list[component].hasSCU && list[component].usePropsOrContext) {
- continue;
- }
- context.report({
- node: list[component].node,
- message: 'Component should be written as a pure function'
- });
- }
- }
- };
- })
- };
|