You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
815 lines
24 KiB
815 lines
24 KiB
/** |
|
* @fileoverview Utility class and functions for React components detection |
|
* @author Yannick Croissant |
|
*/ |
|
|
|
'use strict'; |
|
|
|
const doctrine = require('doctrine'); |
|
const arrayIncludes = require('array-includes'); |
|
|
|
const variableUtil = require('./variable'); |
|
const pragmaUtil = require('./pragma'); |
|
const astUtil = require('./ast'); |
|
const propTypesUtil = require('./propTypes'); |
|
const jsxUtil = require('./jsx'); |
|
const usedPropTypesUtil = require('./usedPropTypes'); |
|
const defaultPropsUtil = require('./defaultProps'); |
|
|
|
function getId(node) { |
|
return node && node.range.join(':'); |
|
} |
|
|
|
function usedPropTypesAreEquivalent(propA, propB) { |
|
if (propA.name === propB.name) { |
|
if (!propA.allNames && !propB.allNames) { |
|
return true; |
|
} |
|
if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) { |
|
return true; |
|
} |
|
return false; |
|
} |
|
return false; |
|
} |
|
|
|
function mergeUsedPropTypes(propsList, newPropsList) { |
|
const propsToAdd = []; |
|
newPropsList.forEach((newProp) => { |
|
const newPropisAlreadyInTheList = propsList.some(prop => usedPropTypesAreEquivalent(prop, newProp)); |
|
if (!newPropisAlreadyInTheList) { |
|
propsToAdd.push(newProp); |
|
} |
|
}); |
|
|
|
return propsList.concat(propsToAdd); |
|
} |
|
|
|
const Lists = new WeakMap(); |
|
|
|
/** |
|
* Components |
|
*/ |
|
class Components { |
|
constructor() { |
|
Lists.set(this, {}); |
|
} |
|
|
|
/** |
|
* 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 |
|
*/ |
|
add(node, confidence) { |
|
const id = getId(node); |
|
const list = Lists.get(this); |
|
if (list[id]) { |
|
if (confidence === 0 || list[id].confidence === 0) { |
|
list[id].confidence = 0; |
|
} else { |
|
list[id].confidence = Math.max(list[id].confidence, confidence); |
|
} |
|
return list[id]; |
|
} |
|
list[id] = { |
|
node, |
|
confidence |
|
}; |
|
return 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 or has confidence value of 0. |
|
*/ |
|
get(node) { |
|
const id = getId(node); |
|
const item = Lists.get(this)[id]; |
|
if (item && item.confidence >= 1) { |
|
return item; |
|
} |
|
return null; |
|
} |
|
|
|
/** |
|
* Update a component in the list |
|
* |
|
* @param {ASTNode} node The AST node being updated. |
|
* @param {Object} props Additional properties to add to the component. |
|
*/ |
|
set(node, props) { |
|
const list = Lists.get(this); |
|
let component = list[getId(node)]; |
|
while (!component) { |
|
node = node.parent; |
|
if (!node) { |
|
return; |
|
} |
|
component = list[getId(node)]; |
|
} |
|
|
|
Object.assign( |
|
component, |
|
props, |
|
{ |
|
usedPropTypes: mergeUsedPropTypes( |
|
component.usedPropTypes || [], |
|
props.usedPropTypes || [] |
|
) |
|
} |
|
); |
|
} |
|
|
|
/** |
|
* Return the components list |
|
* Components for which we are not confident are not returned |
|
* |
|
* @returns {Object} Components list |
|
*/ |
|
list() { |
|
const thisList = Lists.get(this); |
|
const list = {}; |
|
const usedPropTypes = {}; |
|
|
|
// Find props used in components for which we are not confident |
|
Object.keys(thisList).filter(i => thisList[i].confidence < 2).forEach((i) => { |
|
let component = null; |
|
let node = null; |
|
node = thisList[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) { |
|
const newUsedProps = (thisList[i].usedPropTypes || []).filter(propType => !propType.node || propType.node.kind !== 'init'); |
|
|
|
const componentId = getId(component.node); |
|
|
|
usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], newUsedProps); |
|
} |
|
}); |
|
|
|
// Assign used props in not confident components to the parent component |
|
Object.keys(thisList).filter(j => thisList[j].confidence >= 2).forEach((j) => { |
|
const id = getId(thisList[j].node); |
|
list[j] = thisList[j]; |
|
if (usedPropTypes[id]) { |
|
list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], 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 |
|
*/ |
|
length() { |
|
const list = Lists.get(this); |
|
return Object.keys(list).filter(i => list[i].confidence >= 2).length; |
|
} |
|
} |
|
|
|
function componentRule(rule, context) { |
|
const createClass = pragmaUtil.getCreateClassFromContext(context); |
|
const pragma = pragmaUtil.getFromContext(context); |
|
const sourceCode = context.getSourceCode(); |
|
const components = new Components(); |
|
|
|
// Utilities for component detection |
|
const 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(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(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(node) { |
|
let comment; |
|
// Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes. |
|
// Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27 |
|
// eslint-disable-next-line no-warning-comments |
|
// FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented. |
|
try { |
|
comment = sourceCode.getJSDocComment(node); |
|
} catch (e) { |
|
comment = null; |
|
} |
|
|
|
if (comment === null) { |
|
return false; |
|
} |
|
|
|
const commentAst = doctrine.parse(comment.value, { |
|
unwrap: true, |
|
tags: ['extends', 'augments'] |
|
}); |
|
|
|
const relevantTags = commentAst.tags.filter(tag => 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(node) { |
|
if (node.superClass) { |
|
return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass)); |
|
} |
|
return false; |
|
}, |
|
|
|
/** |
|
* Check if variable is destructured from pragma import |
|
* |
|
* @param {string} variable The variable name to check |
|
* @returns {Boolean} True if createElement is destructured from the pragma |
|
*/ |
|
isDestructuredFromPragmaImport(variable) { |
|
const variables = variableUtil.variablesInScope(context); |
|
const variableInScope = variableUtil.getVariable(variables, variable); |
|
if (variableInScope) { |
|
const map = variableInScope.scope.set; |
|
return map.has(pragma); |
|
} |
|
return false; |
|
}, |
|
|
|
/** |
|
* Checks to see if node is called within createElement from pragma |
|
* |
|
* @param {ASTNode} node The AST node being checked. |
|
* @returns {Boolean} True if createElement called from pragma |
|
*/ |
|
isCreateElement(node) { |
|
const calledOnPragma = ( |
|
node && |
|
node.callee && |
|
node.callee.object && |
|
node.callee.object.name === pragma && |
|
node.callee.property && |
|
node.callee.property.name === 'createElement' |
|
); |
|
|
|
const calledDirectly = ( |
|
node && |
|
node.callee && |
|
node.callee.name === 'createElement' |
|
); |
|
|
|
if (this.isDestructuredFromPragmaImport('createElement')) { |
|
return calledDirectly || calledOnPragma; |
|
} |
|
return calledOnPragma; |
|
}, |
|
|
|
/** |
|
* Check if we are in a class constructor |
|
* @return {boolean} true if we are in a class constructor, false if not |
|
*/ |
|
inConstructor() { |
|
let scope = context.getScope(); |
|
while (scope) { |
|
if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { |
|
return true; |
|
} |
|
scope = scope.upper; |
|
} |
|
return false; |
|
}, |
|
|
|
/** |
|
* Determine if the node is MemberExpression of `this.state` |
|
* @param {Object} node The node to process |
|
* @returns {Boolean} |
|
*/ |
|
isStateMemberExpression(node) { |
|
return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state'; |
|
}, |
|
|
|
getReturnPropertyAndNode(ASTnode) { |
|
let property; |
|
let node = ASTnode; |
|
switch (node.type) { |
|
case 'ReturnStatement': |
|
property = 'argument'; |
|
break; |
|
case 'ArrowFunctionExpression': |
|
property = 'body'; |
|
if (node[property] && node[property].type === 'BlockStatement') { |
|
node = utils.findReturnStatement(node); |
|
property = 'argument'; |
|
} |
|
break; |
|
default: |
|
node = utils.findReturnStatement(node); |
|
property = 'argument'; |
|
} |
|
return { |
|
node, |
|
property |
|
}; |
|
}, |
|
|
|
/** |
|
* 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(ASTnode, strict) { |
|
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode); |
|
const node = nodeAndProperty.node; |
|
const property = nodeAndProperty.property; |
|
|
|
if (!node) { |
|
return false; |
|
} |
|
|
|
const returnsConditionalJSXConsequent = node[property] && |
|
node[property].type === 'ConditionalExpression' && |
|
jsxUtil.isJSX(node[property].consequent); |
|
const returnsConditionalJSXAlternate = node[property] && |
|
node[property].type === 'ConditionalExpression' && |
|
jsxUtil.isJSX(node[property].alternate); |
|
const returnsConditionalJSX = strict ? |
|
(returnsConditionalJSXConsequent && returnsConditionalJSXAlternate) : |
|
(returnsConditionalJSXConsequent || returnsConditionalJSXAlternate); |
|
|
|
const returnsJSX = node[property] && |
|
jsxUtil.isJSX(node[property]); |
|
const returnsPragmaCreateElement = this.isCreateElement(node[property]); |
|
|
|
return Boolean( |
|
returnsConditionalJSX || |
|
returnsJSX || |
|
returnsPragmaCreateElement |
|
); |
|
}, |
|
|
|
/** |
|
* Check if the node is returning null |
|
* |
|
* @param {ASTNode} ASTnode The AST node being checked |
|
* @returns {Boolean} True if the node is returning null, false if not |
|
*/ |
|
isReturningNull(ASTnode) { |
|
const nodeAndProperty = utils.getReturnPropertyAndNode(ASTnode); |
|
const property = nodeAndProperty.property; |
|
const node = nodeAndProperty.node; |
|
|
|
if (!node) { |
|
return false; |
|
} |
|
|
|
return node[property] && node[property].value === null; |
|
}, |
|
|
|
/** |
|
* Check if the node is returning JSX or null |
|
* |
|
* @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 or null, false if not |
|
*/ |
|
isReturningJSXOrNull(ASTNode, strict) { |
|
return utils.isReturningJSX(ASTNode, strict) || utils.isReturningNull(ASTNode); |
|
}, |
|
|
|
getPragmaComponentWrapper(node) { |
|
let isPragmaComponentWrapper; |
|
let currentNode = node; |
|
let prevNode; |
|
do { |
|
currentNode = currentNode.parent; |
|
isPragmaComponentWrapper = this.isPragmaComponentWrapper(currentNode); |
|
if (isPragmaComponentWrapper) { |
|
prevNode = currentNode; |
|
} |
|
} while (isPragmaComponentWrapper); |
|
|
|
return prevNode; |
|
}, |
|
|
|
isPragmaComponentWrapper(node) { |
|
if (!node || node.type !== 'CallExpression') { |
|
return false; |
|
} |
|
const propertyNames = ['forwardRef', 'memo']; |
|
const calleeObject = node.callee.object; |
|
if (calleeObject && node.callee.property) { |
|
return arrayIncludes(propertyNames, node.callee.property.name) && calleeObject.name === pragma; |
|
} |
|
return arrayIncludes(propertyNames, node.callee.name) && this.isDestructuredFromPragmaImport(node.callee.name); |
|
}, |
|
|
|
/** |
|
* Find a return statment in the current node |
|
* |
|
* @param {ASTNode} ASTnode The AST node being checked |
|
*/ |
|
findReturnStatement: astUtil.findReturnStatement, |
|
|
|
/** |
|
* Get the parent component node from the current scope |
|
* |
|
* @returns {ASTNode} component node, null if we are not in a component |
|
*/ |
|
getParentComponent() { |
|
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() { |
|
let scope = context.getScope(); |
|
while (scope) { |
|
const 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() { |
|
let scope = context.getScope(); |
|
while (scope && scope.type !== 'class') { |
|
scope = scope.upper; |
|
} |
|
const 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() { |
|
let scope = context.getScope(); |
|
while (scope) { |
|
const node = scope.block; |
|
const isFunction = /Function/.test(node.type); // Functions |
|
const isArrowFunction = astUtil.isArrowFunction(node); |
|
const enclosingScope = isArrowFunction ? utils.getArrowFunctionScope(scope) : scope; |
|
const enclosingScopeParent = enclosingScope && enclosingScope.block.parent; |
|
const isClass = enclosingScope && astUtil.isClass(enclosingScope.block); |
|
const isMethod = enclosingScopeParent && enclosingScopeParent.type === 'MethodDefinition'; // Classes methods |
|
const isArgument = node.parent && node.parent.type === 'CallExpression'; // Arguments (callback, etc.) |
|
// Attribute Expressions inside JSX Elements (<button onClick={() => props.handleClick()}></button>) |
|
const isJSXExpressionContainer = node.parent && node.parent.type === 'JSXExpressionContainer'; |
|
const pragmaComponentWrapper = this.getPragmaComponentWrapper(node); |
|
if (isFunction && pragmaComponentWrapper) { |
|
return pragmaComponentWrapper; |
|
} |
|
// 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 and is not inside a JSX Element |
|
if (isFunction && !isMethod && !isJSXExpressionContainer && utils.isReturningJSXOrNull(node)) { |
|
return node; |
|
} |
|
scope = scope.upper; |
|
} |
|
return null; |
|
}, |
|
|
|
/** |
|
* Get an enclosing scope used to find `this` value by an arrow function |
|
* @param {Scope} scope Current scope |
|
* @returns {Scope} An enclosing scope used by an arrow function |
|
*/ |
|
getArrowFunctionScope(scope) { |
|
scope = scope.upper; |
|
while (scope) { |
|
if (astUtil.isFunction(scope.block) || astUtil.isClass(scope.block)) { |
|
return scope; |
|
} |
|
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(node) { |
|
let i; |
|
let j; |
|
let k; |
|
let l; |
|
let componentNode; |
|
// Get the component path |
|
const 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(); |
|
const componentName = componentPath.slice(0, componentPath.length - 1).join('.'); |
|
|
|
// Find the variable in the current scope |
|
const variableName = componentPath.shift(); |
|
if (!variableName) { |
|
return null; |
|
} |
|
let variableInScope; |
|
const 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 |
|
const refs = variableInScope.references; |
|
refs.some((ref) => { |
|
let refId = ref.identifier; |
|
if (refId.parent && refId.parent.type === 'MemberExpression') { |
|
refId = refId.parent; |
|
} |
|
if (sourceCode.getText(refId) !== componentName) { |
|
return false; |
|
} |
|
if (refId.type === 'MemberExpression') { |
|
componentNode = refId.parent.right; |
|
} else if ( |
|
refId.parent && |
|
refId.parent.type === 'VariableDeclarator' && |
|
refId.parent.init && |
|
refId.parent.init.type !== 'Identifier' |
|
) { |
|
componentNode = refId.parent.init; |
|
} |
|
return true; |
|
}); |
|
|
|
if (componentNode) { |
|
// Return the component |
|
return components.add(componentNode, 1); |
|
} |
|
|
|
// Try to find the component using variable declarations |
|
const defs = variableInScope.defs; |
|
const defInScope = defs.find(def => ( |
|
def.type === 'ClassName' || |
|
def.type === 'FunctionName' || |
|
def.type === 'Variable' |
|
)); |
|
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; // eslint-disable-line no-continue |
|
} |
|
for (k = 0, l = componentNode.properties.length; k < l; k++) { |
|
if (componentNode.properties[k].key && 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 |
|
const detectionInstructions = { |
|
CallExpression(node) { |
|
if (!utils.isPragmaComponentWrapper(node)) { |
|
return; |
|
} |
|
if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) { |
|
components.add(node, 2); |
|
} |
|
}, |
|
|
|
ClassExpression(node) { |
|
if (!utils.isES6Component(node)) { |
|
return; |
|
} |
|
components.add(node, 2); |
|
}, |
|
|
|
ClassDeclaration(node) { |
|
if (!utils.isES6Component(node)) { |
|
return; |
|
} |
|
components.add(node, 2); |
|
}, |
|
|
|
ClassProperty(node) { |
|
node = utils.getParentComponent(); |
|
if (!node) { |
|
return; |
|
} |
|
components.add(node, 2); |
|
}, |
|
|
|
ObjectExpression(node) { |
|
if (!utils.isES5Component(node)) { |
|
return; |
|
} |
|
components.add(node, 2); |
|
}, |
|
|
|
FunctionExpression(node) { |
|
if (node.async) { |
|
components.add(node, 0); |
|
return; |
|
} |
|
const 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(node) { |
|
if (node.async) { |
|
components.add(node, 0); |
|
return; |
|
} |
|
node = utils.getParentComponent(); |
|
if (!node) { |
|
return; |
|
} |
|
components.add(node, 1); |
|
}, |
|
|
|
ArrowFunctionExpression(node) { |
|
if (node.async) { |
|
components.add(node, 0); |
|
return; |
|
} |
|
const 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(node) { |
|
const 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); |
|
}, |
|
|
|
ReturnStatement(node) { |
|
if (!utils.isReturningJSX(node)) { |
|
return; |
|
} |
|
node = utils.getParentComponent(); |
|
if (!node) { |
|
const scope = context.getScope(); |
|
components.add(scope.block, 1); |
|
return; |
|
} |
|
components.add(node, 2); |
|
} |
|
}; |
|
|
|
// Update the provided rule instructions to add the component detection |
|
const ruleInstructions = rule(context, components, utils); |
|
const updatedRuleInstructions = Object.assign({}, ruleInstructions); |
|
const propTypesInstructions = propTypesUtil(context, components, utils); |
|
const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils); |
|
const defaultPropsInstructions = defaultPropsUtil(context, components, utils); |
|
const allKeys = new Set(Object.keys(detectionInstructions).concat( |
|
Object.keys(propTypesInstructions), |
|
Object.keys(usedPropTypesInstructions), |
|
Object.keys(defaultPropsInstructions) |
|
)); |
|
|
|
allKeys.forEach((instruction) => { |
|
updatedRuleInstructions[instruction] = function (node) { |
|
if (instruction in detectionInstructions) { |
|
detectionInstructions[instruction](node); |
|
} |
|
if (instruction in propTypesInstructions) { |
|
propTypesInstructions[instruction](node); |
|
} |
|
if (instruction in usedPropTypesInstructions) { |
|
usedPropTypesInstructions[instruction](node); |
|
} |
|
if (instruction in defaultPropsInstructions) { |
|
defaultPropsInstructions[instruction](node); |
|
} |
|
if (ruleInstructions[instruction]) { |
|
return ruleInstructions[instruction](node); |
|
} |
|
}; |
|
}); |
|
|
|
// Return the updated rule instructions |
|
return updatedRuleInstructions; |
|
} |
|
|
|
module.exports = Object.assign(Components, { |
|
detect(rule) { |
|
return componentRule.bind(this, rule); |
|
} |
|
});
|
|
|