UNPKG

orionsoft-react-scripts

Version:

Orionsoft Configuration and scripts for Create React App.

581 lines (532 loc) 17.1 kB
/** * @fileoverview Utility class and functions for React components detection * @author Yannick Croissant */ 'use strict'; var util = require('util'); var doctrine = require('doctrine'); var variableUtil = require('./variable'); var pragmaUtil = require('./pragma'); /** * Components * @class */ function Components() { this._list = {}; this._getId = function(node) { return node && node.range.join(':'); }; } /** * Add a node to the components list, or update it if it's already in the list * * @param {ASTNode} node The AST node being added. * @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes) * @returns {Object} Added component object */ Components.prototype.add = function(node, confidence) { var id = this._getId(node); if (this._list[id]) { if (confidence === 0 || this._list[id].confidence === 0) { this._list[id].confidence = 0; } else { this._list[id].confidence = Math.max(this._list[id].confidence, confidence); } return this._list[id]; } this._list[id] = { node: node, confidence: confidence }; return this._list[id]; }; /** * Find a component in the list using its node * * @param {ASTNode} node The AST node being searched. * @returns {Object} Component object, undefined if the component is not found */ Components.prototype.get = function(node) { var id = this._getId(node); return this._list[id]; }; /** * Update a component in the list * * @param {ASTNode} node The AST node being updated. * @param {Object} props Additional properties to add to the component. */ Components.prototype.set = function(node, props) { while (node && !this._list[this._getId(node)]) { node = node.parent; } if (!node) { return; } var id = this._getId(node); this._list[id] = util._extend(this._list[id], props); }; /** * Return the components list * Components for which we are not confident are not returned * * @returns {Object} Components list */ Components.prototype.list = function() { var list = {}; var usedPropTypes = {}; // Find props used in components for which we are not confident for (var i in this._list) { if (!this._list.hasOwnProperty(i) || this._list[i].confidence >= 2) { continue; } var component = null; var node = null; node = this._list[i].node; while (!component && node.parent) { node = node.parent; // Stop moving up if we reach a decorator if (node.type === 'Decorator') { break; } component = this.get(node); } if (component) { usedPropTypes[this._getId(component.node)] = (this._list[i].usedPropTypes || []).filter(function(propType) { return !propType.node || propType.node.kind !== 'init'; }); } } // Assign used props in not confident components to the parent component for (var j in this._list) { if (!this._list.hasOwnProperty(j) || this._list[j].confidence < 2) { continue; } var id = this._getId(this._list[j].node); list[j] = this._list[j]; if (usedPropTypes[id]) { list[j].usedPropTypes = (list[j].usedPropTypes || []).concat(usedPropTypes[id]); } } return list; }; /** * Return the length of the components list * Components for which we are not confident are not counted * * @returns {Number} Components list length */ Components.prototype.length = function() { var length = 0; for (var i in this._list) { if (!this._list.hasOwnProperty(i) || this._list[i].confidence < 2) { continue; } length++; } return length; }; function componentRule(rule, context) { var createClass = pragmaUtil.getCreateClassFromContext(context); var pragma = pragmaUtil.getFromContext(context); var sourceCode = context.getSourceCode(); var components = new Components(); // Utilities for component detection var utils = { /** * Check if the node is a React ES5 component * * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if the node is a React ES5 component, false if not */ isES5Component: function(node) { if (!node.parent) { return false; } return new RegExp('^(' + pragma + '\\.)?' + createClass + '$').test(sourceCode.getText(node.parent.callee)); }, /** * Check if the node is a React ES6 component * * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if the node is a React ES6 component, false if not */ isES6Component: function(node) { if (utils.isExplicitComponent(node)) { return true; } if (!node.superClass) { return false; } return new RegExp('^(' + pragma + '\\.)?(Pure)?Component$').test(sourceCode.getText(node.superClass)); }, /** * Check if the node is explicitly declared as a descendant of a React Component * * @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression). * @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not */ isExplicitComponent: function(node) { var comment = sourceCode.getJSDocComment(node); if (comment === null) { return false; } var commentAst = doctrine.parse(comment.value, { unwrap: true, tags: ['extends', 'augments'] }); var relevantTags = commentAst.tags.filter(function(tag) { return tag.name === 'React.Component' || tag.name === 'React.PureComponent'; }); return relevantTags.length > 0; }, /** * Checks to see if our component extends React.PureComponent * * @param {ASTNode} node The AST node being checked. * @returns {Boolean} True if node extends React.PureComponent, false if not */ isPureComponent: function (node) { if (node.superClass) { return new RegExp('^(' + pragma + '\\.)?PureComponent$').test(sourceCode.getText(node.superClass)); } return false; }, /** * Check if the node is returning JSX * * @param {ASTNode} ASTnode The AST node being checked * @param {Boolean} strict If true, in a ternary condition the node must return JSX in both cases * @returns {Boolean} True if the node is returning JSX, false if not */ isReturningJSX: function(ASTnode, strict) { var property; var node = ASTnode; switch (node.type) { case 'ReturnStatement': property = 'argument'; break; case 'ArrowFunctionExpression': property = 'body'; break; default: node = utils.findReturnStatement(node); if (!node) { return false; } property = 'argument'; } var returnsConditionalJSXConsequent = node[property] && node[property].type === 'ConditionalExpression' && node[property].consequent.type === 'JSXElement' ; var returnsConditionalJSXAlternate = node[property] && node[property].type === 'ConditionalExpression' && node[property].alternate.type === 'JSXElement' ; var returnsConditionalJSX = strict ? (returnsConditionalJSXConsequent && returnsConditionalJSXAlternate) : (returnsConditionalJSXConsequent || returnsConditionalJSXAlternate); var returnsJSX = node[property] && node[property].type === 'JSXElement' ; var returnsReactCreateElement = node[property] && node[property].callee && node[property].callee.property && node[property].callee.property.name === 'createElement' ; return Boolean( returnsConditionalJSX || returnsJSX || returnsReactCreateElement ); }, /** * Find a return statment in the current node * * @param {ASTNode} ASTnode The AST node being checked */ findReturnStatement: function(node) { if (!node.value || !node.value.body || !node.value.body.body) { return false; } var i = node.value.body.body.length - 1; for (; i >= 0; i--) { if (node.value.body.body[i].type === 'ReturnStatement') { return node.value.body.body[i]; } } return false; }, /** * Get the parent component node from the current scope * * @returns {ASTNode} component node, null if we are not in a component */ getParentComponent: function() { return ( utils.getParentES6Component() || utils.getParentES5Component() || utils.getParentStatelessComponent() ); }, /** * Get the parent ES5 component node from the current scope * * @returns {ASTNode} component node, null if we are not in a component */ getParentES5Component: function() { var scope = context.getScope(); while (scope) { var node = scope.block && scope.block.parent && scope.block.parent.parent; if (node && utils.isES5Component(node)) { return node; } scope = scope.upper; } return null; }, /** * Get the parent ES6 component node from the current scope * * @returns {ASTNode} component node, null if we are not in a component */ getParentES6Component: function() { var scope = context.getScope(); while (scope && scope.type !== 'class') { scope = scope.upper; } var node = scope && scope.block; if (!node || !utils.isES6Component(node)) { return null; } return node; }, /** * Get the parent stateless component node from the current scope * * @returns {ASTNode} component node, null if we are not in a component */ getParentStatelessComponent: function() { var scope = context.getScope(); while (scope) { var node = scope.block; var isClass = node.type === 'ClassExpression'; var isFunction = /Function/.test(node.type); // Functions var isMethod = node.parent && node.parent.type === 'MethodDefinition'; // Classes methods var isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.) // Stop moving up if we reach a class or an argument (like a callback) if (isClass || isArgument) { return null; } // Return the node if it is a function that is not a class method if (isFunction && !isMethod) { return node; } scope = scope.upper; } return null; }, /** * Get the related component from a node * * @param {ASTNode} node The AST node being checked (must be a MemberExpression). * @returns {ASTNode} component node, null if we cannot find the component */ getRelatedComponent: function(node) { var i; var j; var k; var l; var componentName; var componentNode; // Get the component path var componentPath = []; while (node) { if (node.property && node.property.type === 'Identifier') { componentPath.push(node.property.name); } if (node.object && node.object.type === 'Identifier') { componentPath.push(node.object.name); } node = node.object; } componentPath.reverse(); componentName = componentPath.slice(0, componentPath.length - 1).join('.'); // Find the variable in the current scope var variableName = componentPath.shift(); if (!variableName) { return null; } var variableInScope; var variables = variableUtil.variablesInScope(context); for (i = 0, j = variables.length; i < j; i++) { if (variables[i].name === variableName) { variableInScope = variables[i]; break; } } if (!variableInScope) { return null; } // Try to find the component using variable references var refs = variableInScope.references; var refId; for (i = 0, j = refs.length; i < j; i++) { refId = refs[i].identifier; if (refId.parent && refId.parent.type === 'MemberExpression') { refId = refId.parent; } if (sourceCode.getText(refId) !== componentName) { continue; } if (refId.type === 'MemberExpression') { componentNode = refId.parent.right; } else if (refId.parent && refId.parent.type === 'VariableDeclarator') { componentNode = refId.parent.init; } break; } if (componentNode) { // Return the component return components.add(componentNode, 1); } // Try to find the component using variable declarations var defInScope; var defs = variableInScope.defs; for (i = 0, j = defs.length; i < j; i++) { if (defs[i].type === 'ClassName' || defs[i].type === 'FunctionName' || defs[i].type === 'Variable') { defInScope = defs[i]; break; } } if (!defInScope || !defInScope.node) { return null; } componentNode = defInScope.node.init || defInScope.node; // Traverse the node properties to the component declaration for (i = 0, j = componentPath.length; i < j; i++) { if (!componentNode.properties) { continue; } for (k = 0, l = componentNode.properties.length; k < l; k++) { if (componentNode.properties[k].key.name === componentPath[i]) { componentNode = componentNode.properties[k]; break; } } if (!componentNode || !componentNode.value) { return null; } componentNode = componentNode.value; } // Return the component return components.add(componentNode, 1); } }; // Component detection instructions var detectionInstructions = { ClassExpression: function(node) { if (!utils.isES6Component(node)) { return; } components.add(node, 2); }, ClassDeclaration: function(node) { if (!utils.isES6Component(node)) { return; } components.add(node, 2); }, ClassProperty: function(node) { node = utils.getParentComponent(); if (!node) { return; } components.add(node, 2); }, ObjectExpression: function(node) { if (!utils.isES5Component(node)) { return; } components.add(node, 2); }, FunctionExpression: function(node) { var component = utils.getParentComponent(); if ( !component || (component.parent && component.parent.type === 'JSXExpressionContainer') ) { // Ban the node if we cannot find a parent component components.add(node, 0); return; } components.add(component, 1); }, FunctionDeclaration: function(node) { node = utils.getParentComponent(); if (!node) { return; } components.add(node, 1); }, ArrowFunctionExpression: function(node) { var component = utils.getParentComponent(); if ( !component || (component.parent && component.parent.type === 'JSXExpressionContainer') ) { // Ban the node if we cannot find a parent component components.add(node, 0); return; } if (component.expression && utils.isReturningJSX(component)) { components.add(component, 2); } else { components.add(component, 1); } }, ThisExpression: function(node) { var component = utils.getParentComponent(); if (!component || !/Function/.test(component.type) || !node.parent.property) { return; } // Ban functions accessing a property on a ThisExpression components.add(node, 0); }, BlockComment: function(node) { pragma = pragmaUtil.getFromNode(node) || pragma; }, ReturnStatement: function(node) { if (!utils.isReturningJSX(node)) { return; } node = utils.getParentComponent(); if (!node) { var scope = context.getScope(); components.add(scope.block, 1); return; } components.add(node, 2); } }; // Update the provided rule instructions to add the component detection var ruleInstructions = rule(context, components, utils); var updatedRuleInstructions = util._extend({}, ruleInstructions); Object.keys(detectionInstructions).forEach(function(instruction) { updatedRuleInstructions[instruction] = function(node) { detectionInstructions[instruction](node); return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : void 0; }; }); // Return the updated rule instructions return updatedRuleInstructions; } Components.detect = function(rule) { return componentRule.bind(this, rule); }; module.exports = Components;