Skip to content

Commit

Permalink
[Refactor] create/extract isCreateElement and `isDestructuredFromPr…
Browse files Browse the repository at this point in the history
…agmaImport` utils

This should improve detection in the following rules:
 - `button-has-type`
 - `forbid-elements`
 - `no-adjacent-inline-elements`
 - `no-children-prop`
 - `style-prop-object`
  • Loading branch information
ljharb committed Sep 19, 2021
1 parent 76fdffe commit f25a8ec
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 121 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel

### Changed
* [readme] Update broken link for configuration files ([#3071] @prateek3255)
* [Refactor] create/extract `isCreateElement` and `isDestructuredFromPragmaImport` utils (@ljharb)

[7.25.3]: https://github.com/yannickcr/eslint-plugin-react/compare/v7.25.2...v7.25.3
[#3076]: https://github.com/yannickcr/eslint-plugin-react/pull/3076
Expand Down
18 changes: 2 additions & 16 deletions lib/rules/button-has-type.js
Expand Up @@ -8,21 +8,7 @@
const getProp = require('jsx-ast-utils/getProp');
const getLiteralPropValue = require('jsx-ast-utils/getLiteralPropValue');
const docsUrl = require('../util/docsUrl');
const pragmaUtil = require('../util/pragma');

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

function isCreateElement(node, context) {
const pragma = pragmaUtil.getFromContext(context);
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
&& node.callee.object
&& node.callee.object.name === pragma
&& node.arguments.length > 0;
}
const isCreateElement = require('../util/isCreateElement');

// ------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -150,7 +136,7 @@ module.exports = {
checkValue(node, propValue);
},
CallExpression(node) {
if (!isCreateElement(node, context)) {
if (!isCreateElement(node, context) || node.arguments.length < 1) {
return;
}

Expand Down
11 changes: 2 additions & 9 deletions lib/rules/forbid-elements.js
Expand Up @@ -7,6 +7,7 @@

const has = require('object.hasown/polyfill')();
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');

// ------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -65,14 +66,6 @@ module.exports = {
}
});

function isValidCreateElement(node) {
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.object.name === 'React'
&& node.callee.property.name === 'createElement'
&& node.arguments.length > 0;
}

function reportIfForbidden(element, node) {
if (has(indexedForbidConfigs, element)) {
const message = indexedForbidConfigs[element].message;
Expand All @@ -94,7 +87,7 @@ module.exports = {
},

CallExpression(node) {
if (!isValidCreateElement(node)) {
if (!isCreateElement(node, context)) {
return;
}

Expand Down
3 changes: 2 additions & 1 deletion lib/rules/no-adjacent-inline-elements.js
Expand Up @@ -6,6 +6,7 @@
'use strict';

const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');

// ------------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -108,7 +109,7 @@ module.exports = {
validate(node, node.children);
},
CallExpression(node) {
if (!node.callee || node.callee.type !== 'MemberExpression' || node.callee.property.name !== 'createElement') {
if (!isCreateElement(node, context)) {
return;
}
if (node.arguments.length < 2 || !node.arguments[2]) {
Expand Down
10 changes: 5 additions & 5 deletions lib/rules/no-children-prop.js
Expand Up @@ -6,6 +6,7 @@
'use strict';

const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');

// ------------------------------------------------------------------------------
// Helpers
Expand All @@ -14,13 +15,12 @@ const docsUrl = require('../util/docsUrl');
/**
* Checks if the node is a createElement call with a props literal.
* @param {ASTNode} node - The AST node being checked.
* @param {Context} context - The AST node being checked.
* @returns {Boolean} - True if node is a createElement call with a props
* object literal, False if not.
*/
function isCreateElementWithProps(node) {
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
function isCreateElementWithProps(node, context) {
return isCreateElement(node, context)
&& node.arguments.length > 1
&& node.arguments[1].type === 'ObjectExpression';
}
Expand Down Expand Up @@ -80,7 +80,7 @@ module.exports = {
});
},
CallExpression(node) {
if (!isCreateElementWithProps(node)) {
if (!isCreateElementWithProps(node, context)) {
return;
}

Expand Down
5 changes: 2 additions & 3 deletions lib/rules/style-prop-object.js
Expand Up @@ -7,6 +7,7 @@

const variableUtil = require('../util/variable');
const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');

// ------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -74,9 +75,7 @@ module.exports = {
return {
CallExpression(node) {
if (
node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
isCreateElement(node, context)
&& node.arguments.length > 1
) {
if (node.arguments[0].name) {
Expand Down
91 changes: 4 additions & 87 deletions lib/util/Components.js
Expand Up @@ -17,6 +17,8 @@ const jsxUtil = require('./jsx');
const usedPropTypesUtil = require('./usedPropTypes');
const defaultPropsUtil = require('./defaultProps');
const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
const isCreateElement = require('./isCreateElement');
const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport');

function getId(node) {
return node && node.range.join(':');
Expand Down Expand Up @@ -287,70 +289,7 @@ function componentRule(rule, context) {
* @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;
return isDestructuredFromPragmaImport(variable, context);
},

/**
Expand All @@ -360,29 +299,7 @@ function componentRule(rule, context) {
* @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;
return isCreateElement(node, context);
},

/**
Expand Down
34 changes: 34 additions & 0 deletions lib/util/isCreateElement.js
@@ -0,0 +1,34 @@
'use strict';

const pragmaUtil = require('./pragma');
const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport');

/**
* Checks if the node is a createElement call
* @param {ASTNode} node - The AST node being checked.
* @param {Context} context - The AST node being checked.
* @returns {Boolean} - True if node is a createElement call object literal, False if not.
*/
module.exports = function isCreateElement(node, context) {
const pragma = pragmaUtil.getFromContext(context);
if (
node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'createElement'
&& node.callee.object
&& node.callee.object.name === pragma
) {
return true;
}

if (
node
&& node.callee
&& node.callee.name === 'createElement'
&& isDestructuredFromPragmaImport('createElement', context)
) {
return true;
}

return false;
};
79 changes: 79 additions & 0 deletions lib/util/isDestructuredFromPragmaImport.js
@@ -0,0 +1,79 @@
'use strict';

const pragmaUtil = require('./pragma');
const variableUtil = require('./variable');

/**
* Check if variable is destructured from pragma import
*
* @param {string} variable The variable name to check
* @param {Context} context eslint context
* @returns {Boolean} True if createElement is destructured from the pragma
*/
module.exports = function isDestructuredFromPragmaImport(variable, context) {
const pragma = pragmaUtil.getFromContext(context);
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;
};

0 comments on commit f25a8ec

Please sign in to comment.