123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029 |
- /**
- * @fileoverview Utility class and functions for React components detection
- * @author Yannick Croissant
- */
- 'use strict';
- const doctrine = require('doctrine');
- const arrayIncludes = require('array-includes');
- const values = require('object.values');
- 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');
- const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
- 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);
- }
- function isReturnsConditionalJSX(node, property, strict) {
- 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);
- return strict
- ? (returnsConditionalJSXConsequent && returnsConditionalJSXAlternate)
- : (returnsConditionalJSXConsequent || returnsConditionalJSXAlternate);
- }
- function isReturnsLogicalJSX(node, property, strict) {
- const returnsLogicalJSXLeft = node[property]
- && node[property].type === 'LogicalExpression'
- && jsxUtil.isJSX(node[property].left);
- const returnsLogicalJSXRight = node[property]
- && node[property].type === 'LogicalExpression'
- && jsxUtil.isJSX(node[property].right);
- return strict
- ? (returnsLogicalJSXLeft && returnsLogicalJSXRight)
- : (returnsLogicalJSXLeft || returnsLogicalJSXRight);
- }
- function isReturnsSequentialJSX(node, property) {
- return node[property]
- && node[property].type === 'SequenceExpression'
- && jsxUtil.isJSX(node[property].expressions[node[property].expressions.length - 1]);
- }
- 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 getWrapperFunctions(context, pragma) {
- const componentWrapperFunctions = context.settings.componentWrapperFunctions || [];
- // eslint-disable-next-line arrow-body-style
- return componentWrapperFunctions.map((wrapperFunction) => {
- return typeof wrapperFunction === 'string'
- ? {property: wrapperFunction}
- : Object.assign({}, wrapperFunction, {
- object: wrapperFunction.object === '<pragma>' ? pragma : wrapperFunction.object
- });
- }).concat([
- {property: 'forwardRef', object: pragma},
- {property: 'memo', object: pragma}
- ]);
- }
- function componentRule(rule, context) {
- const createClass = pragmaUtil.getCreateClassFromContext(context);
- const pragma = pragmaUtil.getFromContext(context);
- const sourceCode = context.getSourceCode();
- const components = new Components();
- const wrapperFunctions = getWrapperFunctions(context, pragma);
- // 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 latestDef = variableUtil.getLatestVariableDefinition(variableInScope);
- if (latestDef) {
- // check if latest definition is a variable declaration: 'variable = value'
- if (latestDef.node.type === 'VariableDeclarator' && latestDef.node.init) {
- // check for: 'variable = pragma.variable'
- if (
- latestDef.node.init.type === 'MemberExpression'
- && latestDef.node.init.object.type === 'Identifier'
- && latestDef.node.init.object.name === pragma
- ) {
- return true;
- }
- // check for: '{variable} = pragma'
- if (
- latestDef.node.init.type === 'Identifier'
- && latestDef.node.init.name === pragma
- ) {
- return true;
- }
- // "require('react')"
- let requireExpression = null;
- // get "require('react')" from: "{variable} = require('react')"
- if (latestDef.node.init.type === 'CallExpression') {
- requireExpression = latestDef.node.init;
- }
- // get "require('react')" from: "variable = require('react').variable"
- if (
- !requireExpression
- && latestDef.node.init.type === 'MemberExpression'
- && latestDef.node.init.object.type === 'CallExpression'
- ) {
- requireExpression = latestDef.node.init.object;
- }
- // check proper require.
- if (
- requireExpression
- && requireExpression.callee
- && requireExpression.callee.name === 'require'
- && requireExpression.arguments[0]
- && requireExpression.arguments[0].value === pragma.toLocaleLowerCase()
- ) {
- return true;
- }
- return false;
- }
- // latest definition is an import declaration: import {<variable>} from 'react'
- if (
- latestDef.parent
- && latestDef.parent.type === 'ImportDeclaration'
- && latestDef.parent.source.value === pragma.toLocaleLowerCase()
- ) {
- return true;
- }
- }
- }
- 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) {
- // match `React.createElement()`
- if (
- node
- && node.callee
- && node.callee.object
- && node.callee.object.name === pragma
- && node.callee.property
- && node.callee.property.name === 'createElement'
- ) {
- return true;
- }
- // match `createElement()`
- if (
- node
- && node.callee
- && node.callee.name === 'createElement'
- && this.isDestructuredFromPragmaImport('createElement')
- ) {
- return true;
- }
- return false;
- },
- /**
- * 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 returnsConditionalJSX = isReturnsConditionalJSX(node, property, strict);
- const returnsLogicalJSX = isReturnsLogicalJSX(node, property, strict);
- const returnsSequentialJSX = isReturnsSequentialJSX(node, property);
- const returnsJSX = node[property] && jsxUtil.isJSX(node[property]);
- const returnsPragmaCreateElement = this.isCreateElement(node[property]);
- return !!(
- returnsConditionalJSX
- || returnsLogicalJSX
- || returnsSequentialJSX
- || 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;
- },
- getComponentNameFromJSXElement(node) {
- if (node.type !== 'JSXElement') {
- return null;
- }
- if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
- return node.openingElement.name.name;
- }
- return null;
- },
- /**
- * Getting the first JSX element's name.
- * @param {object} node
- * @returns {string | null}
- */
- getNameOfWrappedComponent(node) {
- if (node.length < 1) {
- return null;
- }
- const body = node[0].body;
- if (!body) {
- return null;
- }
- if (body.type === 'JSXElement') {
- return this.getComponentNameFromJSXElement(body);
- }
- if (body.type === 'BlockStatement') {
- const jsxElement = body.body.find((item) => item.type === 'ReturnStatement');
- return jsxElement
- && jsxElement.argument
- && this.getComponentNameFromJSXElement(jsxElement.argument);
- }
- return null;
- },
- /**
- * Get the list of names of components created till now
- * @returns {string | boolean}
- */
- getDetectedComponents() {
- const list = components.list();
- return values(list).filter((val) => {
- if (val.node.type === 'ClassDeclaration') {
- return true;
- }
- if (
- val.node.type === 'ArrowFunctionExpression'
- && val.node.parent
- && val.node.parent.type === 'VariableDeclarator'
- && val.node.parent.id
- ) {
- return true;
- }
- return false;
- }).map((val) => {
- if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
- return val.node.id && val.node.id.name;
- });
- },
- /**
- * It will check wheater memo/forwardRef is wrapping existing component or
- * creating a new one.
- * @param {object} node
- * @returns {boolean}
- */
- nodeWrapsComponent(node) {
- const childComponent = this.getNameOfWrappedComponent(node.arguments);
- const componentList = this.getDetectedComponents();
- return !!childComponent && arrayIncludes(componentList, childComponent);
- },
- isPragmaComponentWrapper(node) {
- if (!node || node.type !== 'CallExpression') {
- return false;
- }
- return wrapperFunctions.some((wrapperFunction) => {
- if (node.callee.type === 'MemberExpression') {
- return wrapperFunction.object
- && wrapperFunction.object === node.callee.object.name
- && wrapperFunction.property === node.callee.property.name
- && !this.nodeWrapsComponent(node);
- }
- return wrapperFunction.property === node.callee.name
- && (!wrapperFunction.object
- // Functions coming from the current pragma need special handling
- || (wrapperFunction.object === pragma && 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;
- },
- /**
- * @param {ASTNode} node
- * @returns {boolean}
- */
- isInAllowedPositionForComponent(node) {
- switch (node.parent.type) {
- case 'VariableDeclarator':
- case 'AssignmentExpression':
- case 'Property':
- case 'ReturnStatement':
- case 'ExportDefaultDeclaration':
- case 'ArrowFunctionExpression': {
- return true;
- }
- case 'SequenceExpression': {
- return utils.isInAllowedPositionForComponent(node.parent)
- && node === node.parent.expressions[node.parent.expressions.length - 1];
- }
- default:
- return false;
- }
- },
- /**
- * Get node if node is a stateless component, or node.parent in cases like
- * `React.memo` or `React.forwardRef`. Otherwise returns `undefined`.
- * @param {ASTNode} node
- * @returns {ASTNode | undefined}
- */
- getStatelessComponent(node) {
- if (
- node.type === 'FunctionDeclaration'
- && (!node.id || isFirstLetterCapitalized(node.id.name))
- && utils.isReturningJSXOrNull(node)
- ) {
- return node;
- }
- if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
- if (node.parent.type === 'VariableDeclarator' && utils.isReturningJSXOrNull(node)) {
- if (isFirstLetterCapitalized(node.parent.id.name)) {
- return node;
- }
- return undefined;
- }
- if (utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node)) {
- if (!node.id || isFirstLetterCapitalized(node.id.name)) {
- return node;
- }
- return undefined;
- }
- // Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
- const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
- if (pragmaComponentWrapper) {
- return pragmaComponentWrapper;
- }
- }
- return undefined;
- },
- /**
- * 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 statelessComponent = utils.getStatelessComponent(node);
- if (statelessComponent) {
- return statelessComponent;
- }
- 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] = (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);
- }
- });
|