From f25a8ec65e1158f3b48441791b520786ae859b94 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 19 Sep 2021 14:24:27 -0700 Subject: [PATCH 1/2] [Refactor] create/extract `isCreateElement` and `isDestructuredFromPragmaImport` utils This should improve detection in the following rules: - `button-has-type` - `forbid-elements` - `no-adjacent-inline-elements` - `no-children-prop` - `style-prop-object` --- CHANGELOG.md | 1 + lib/rules/button-has-type.js | 18 +---- lib/rules/forbid-elements.js | 11 +-- lib/rules/no-adjacent-inline-elements.js | 3 +- lib/rules/no-children-prop.js | 10 +-- lib/rules/style-prop-object.js | 5 +- lib/util/Components.js | 91 +--------------------- lib/util/isCreateElement.js | 34 ++++++++ lib/util/isDestructuredFromPragmaImport.js | 79 +++++++++++++++++++ 9 files changed, 131 insertions(+), 121 deletions(-) create mode 100644 lib/util/isCreateElement.js create mode 100644 lib/util/isDestructuredFromPragmaImport.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a564e9b411..47b4fabe41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/rules/button-has-type.js b/lib/rules/button-has-type.js index ddab18bcb4..565a112681 100644 --- a/lib/rules/button-has-type.js +++ b/lib/rules/button-has-type.js @@ -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 @@ -150,7 +136,7 @@ module.exports = { checkValue(node, propValue); }, CallExpression(node) { - if (!isCreateElement(node, context)) { + if (!isCreateElement(node, context) || node.arguments.length < 1) { return; } diff --git a/lib/rules/forbid-elements.js b/lib/rules/forbid-elements.js index 8fd966214d..a648bce1f9 100644 --- a/lib/rules/forbid-elements.js +++ b/lib/rules/forbid-elements.js @@ -7,6 +7,7 @@ const has = require('object.hasown/polyfill')(); const docsUrl = require('../util/docsUrl'); +const isCreateElement = require('../util/isCreateElement'); // ------------------------------------------------------------------------------ // Rule Definition @@ -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; @@ -94,7 +87,7 @@ module.exports = { }, CallExpression(node) { - if (!isValidCreateElement(node)) { + if (!isCreateElement(node, context)) { return; } diff --git a/lib/rules/no-adjacent-inline-elements.js b/lib/rules/no-adjacent-inline-elements.js index 9e02929bba..deda66e3c2 100644 --- a/lib/rules/no-adjacent-inline-elements.js +++ b/lib/rules/no-adjacent-inline-elements.js @@ -6,6 +6,7 @@ 'use strict'; const docsUrl = require('../util/docsUrl'); +const isCreateElement = require('../util/isCreateElement'); // ------------------------------------------------------------------------------ // Helpers @@ -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]) { diff --git a/lib/rules/no-children-prop.js b/lib/rules/no-children-prop.js index 8ebb19f7ce..9b753f2996 100644 --- a/lib/rules/no-children-prop.js +++ b/lib/rules/no-children-prop.js @@ -6,6 +6,7 @@ 'use strict'; const docsUrl = require('../util/docsUrl'); +const isCreateElement = require('../util/isCreateElement'); // ------------------------------------------------------------------------------ // Helpers @@ -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'; } @@ -80,7 +80,7 @@ module.exports = { }); }, CallExpression(node) { - if (!isCreateElementWithProps(node)) { + if (!isCreateElementWithProps(node, context)) { return; } diff --git a/lib/rules/style-prop-object.js b/lib/rules/style-prop-object.js index 7053755519..33c7d0bd20 100644 --- a/lib/rules/style-prop-object.js +++ b/lib/rules/style-prop-object.js @@ -7,6 +7,7 @@ const variableUtil = require('../util/variable'); const docsUrl = require('../util/docsUrl'); +const isCreateElement = require('../util/isCreateElement'); // ------------------------------------------------------------------------------ // Rule Definition @@ -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) { diff --git a/lib/util/Components.js b/lib/util/Components.js index c0e62b46bf..f4c15997d0 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -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(':'); @@ -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 {} from 'react' - if ( - latestDef.parent - && latestDef.parent.type === 'ImportDeclaration' - && latestDef.parent.source.value === pragma.toLocaleLowerCase() - ) { - return true; - } - } - } - return false; + return isDestructuredFromPragmaImport(variable, context); }, /** @@ -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); }, /** diff --git a/lib/util/isCreateElement.js b/lib/util/isCreateElement.js new file mode 100644 index 0000000000..48ab7a566f --- /dev/null +++ b/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; +}; diff --git a/lib/util/isDestructuredFromPragmaImport.js b/lib/util/isDestructuredFromPragmaImport.js new file mode 100644 index 0000000000..6f8deb08bc --- /dev/null +++ b/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 {} from 'react' + if ( + latestDef.parent + && latestDef.parent.type === 'ImportDeclaration' + && latestDef.parent.source.value === pragma.toLocaleLowerCase() + ) { + return true; + } + } + } + return false; +}; From 53a0d8491411e54da55898ec57296439d3d650a0 Mon Sep 17 00:00:00 2001 From: Yacine Hmito Date: Sun, 10 May 2020 01:19:59 +0200 Subject: [PATCH 2/2] [New] add `no-namespace` rule Co-authored-by: Yacine Hmito Co-authored-by: Jordan Harband --- CHANGELOG.md | 6 ++ README.md | 1 + docs/rules/no-namespace.md | 29 ++++++++ index.js | 1 + lib/rules/no-namespace.js | 49 ++++++++++++ tests/lib/rules/no-namespace.js | 128 ++++++++++++++++++++++++++++++++ 6 files changed, 214 insertions(+) create mode 100644 docs/rules/no-namespace.md create mode 100644 lib/rules/no-namespace.js create mode 100644 tests/lib/rules/no-namespace.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 47b4fabe41..4e9fc569dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +### Added +* add [`no-namespace`] rule ([#2640] @yacinehmito @ljharb) + +[#2640]: https://github.com/yannickcr/eslint-plugin-react/pull/2640 + ## [7.25.3] - 2021.09.19 ### Fixed @@ -3437,6 +3442,7 @@ If you're still not using React 15 you can keep the old behavior by setting the [`no-find-dom-node`]: docs/rules/no-find-dom-node.md [`no-is-mounted`]: docs/rules/no-is-mounted.md [`no-multi-comp`]: docs/rules/no-multi-comp.md +[`no-namespace`]: docs/rules/no-namespace.md [`no-redundant-should-component-update`]: docs/rules/no-redundant-should-component-update.md [`no-render-return-value`]: docs/rules/no-render-return-value.md [`no-set-state`]: docs/rules/no-set-state.md diff --git a/README.md b/README.md index 193920c819..6c5af3db27 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ Enable the rules that you would like to use. | ✔ | | [react/no-find-dom-node](docs/rules/no-find-dom-node.md) | Prevent usage of findDOMNode | | ✔ | | [react/no-is-mounted](docs/rules/no-is-mounted.md) | Prevent usage of isMounted | | | | [react/no-multi-comp](docs/rules/no-multi-comp.md) | Prevent multiple component definition per file | +| | | [react/no-namespace](docs/rules/no-namespace.md) | Enforce that namespaces are not used in React elements | | | | [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md) | Flag shouldComponentUpdate when extending PureComponent | | ✔ | | [react/no-render-return-value](docs/rules/no-render-return-value.md) | Prevent usage of the return value of React.render | | | | [react/no-set-state](docs/rules/no-set-state.md) | Prevent usage of setState | diff --git a/docs/rules/no-namespace.md b/docs/rules/no-namespace.md new file mode 100644 index 0000000000..7ba843e6cc --- /dev/null +++ b/docs/rules/no-namespace.md @@ -0,0 +1,29 @@ +# Enforce that namespaces are not used in React elements (react/no-namespace) + +Enforces the absence of a namespace in React elements, such as with `svg:circle`, as they are not supported in React. + +## Rule Details + +The following patterns are considered warnings: + +```jsx + +``` + +```jsx + +``` + +The following patterns are **not** considered warnings: + +```jsx + +``` + +```jsx + +``` + +## When not to use + +If you are not using React. diff --git a/index.js b/index.js index 15fa8f0465..198af7b37f 100644 --- a/index.js +++ b/index.js @@ -67,6 +67,7 @@ const allRules = { 'no-find-dom-node': require('./lib/rules/no-find-dom-node'), 'no-is-mounted': require('./lib/rules/no-is-mounted'), 'no-multi-comp': require('./lib/rules/no-multi-comp'), + 'no-namespace': require('./lib/rules/no-namespace'), 'no-set-state': require('./lib/rules/no-set-state'), 'no-string-refs': require('./lib/rules/no-string-refs'), 'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'), diff --git a/lib/rules/no-namespace.js b/lib/rules/no-namespace.js new file mode 100644 index 0000000000..8694e589c8 --- /dev/null +++ b/lib/rules/no-namespace.js @@ -0,0 +1,49 @@ +/** + * @fileoverview Enforce that namespaces are not used in React elements + * @author Yacine Hmito + */ + +'use strict'; + +const elementType = require('jsx-ast-utils/elementType'); +const docsUrl = require('../util/docsUrl'); +const isCreateElement = require('../util/isCreateElement'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Enforce that namespaces are not used in React elements', + category: 'Possible Errors', + recommended: false, + url: docsUrl('no-namespace') + }, + + schema: [{ + type: 'object', + additionalProperties: false + }] + }, + + create(context) { + return { + CallExpression(node) { + if (isCreateElement(node, context) && node.arguments.length > 0 && node.arguments[0].type === 'Literal') { + const name = node.arguments[0].value; + if (name.indexOf(':') === -1) return undefined; + const message = `React component ${name} must not be in a namespace as React does not support them`; + context.report({node, message}); + } + }, + JSXOpeningElement(node) { + const name = elementType(node); + if (name.indexOf(':') === -1) return undefined; + const message = `React component ${name} must not be in a namespace as React does not support them`; + context.report({node, message}); + } + }; + } +}; diff --git a/tests/lib/rules/no-namespace.js b/tests/lib/rules/no-namespace.js new file mode 100644 index 0000000000..b7c8c434b7 --- /dev/null +++ b/tests/lib/rules/no-namespace.js @@ -0,0 +1,128 @@ +/** + * @fileoverview Tests for jsx-no-namespace + * @author Yacine Hmito + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/no-namespace'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } +}; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions}); +ruleTester.run('jsx-no-namespace', rule, { + valid: [{ + code: '' + }, { + code: 'React.createElement("testcomponent")' + }, { + code: '' + }, { + code: 'React.createElement("testComponent")' + }, { + code: '' + }, { + code: 'React.createElement("test_component")' + }, { + code: '' + }, { + code: 'React.createElement("TestComponent")' + }, { + code: '' + }, { + code: 'React.createElement("object.testcomponent")' + }, { + code: '' + }, { + code: 'React.createElement("object.testComponent")' + }, { + code: '' + }, { + code: 'React.createElement("object.test_component")' + }, { + code: '' + }, { + code: 'React.createElement("object.TestComponent")' + }, { + code: '' + }, { + code: 'React.createElement("Object.testcomponent")' + }, { + code: '' + }, { + code: 'React.createElement("Object.testComponent")' + }, { + code: '' + }, { + code: 'React.createElement("Object.test_component")' + }, { + code: '' + }, { + code: 'React.createElement("Object.TestComponent")' + }], + + invalid: [{ + code: '', + errors: [{message: 'React component ns:testcomponent must not be in a namespace as React does not support them'}] + }, { + code: 'React.createElement("ns:testcomponent")', + errors: [{message: 'React component ns:testcomponent must not be in a namespace as React does not support them'}] + }, { + code: '', + errors: [{message: 'React component ns:testComponent must not be in a namespace as React does not support them'}] + }, { + code: 'React.createElement("ns:testComponent")', + errors: [{message: 'React component ns:testComponent must not be in a namespace as React does not support them'}] + }, { + code: '', + errors: [{message: 'React component ns:test_component must not be in a namespace as React does not support them'}] + }, { + code: 'React.createElement("ns:test_component")', + errors: [{message: 'React component ns:test_component must not be in a namespace as React does not support them'}] + }, { + code: '', + errors: [{message: 'React component ns:TestComponent must not be in a namespace as React does not support them'}] + }, { + code: 'React.createElement("ns:TestComponent")', + errors: [{message: 'React component ns:TestComponent must not be in a namespace as React does not support them'}] + }, { + code: '', + errors: [{message: 'React component Ns:testcomponent must not be in a namespace as React does not support them'}] + }, { + code: 'React.createElement("Ns:testcomponent")', + errors: [{message: 'React component Ns:testcomponent must not be in a namespace as React does not support them'}] + }, { + code: '', + errors: [{message: 'React component Ns:testComponent must not be in a namespace as React does not support them'}] + }, { + code: 'React.createElement("Ns:testComponent")', + errors: [{message: 'React component Ns:testComponent must not be in a namespace as React does not support them'}] + }, { + code: '', + errors: [{message: 'React component Ns:test_component must not be in a namespace as React does not support them'}] + }, { + code: 'React.createElement("Ns:test_component")', + errors: [{message: 'React component Ns:test_component must not be in a namespace as React does not support them'}] + }, { + code: '', + errors: [{message: 'React component Ns:TestComponent must not be in a namespace as React does not support them'}] + }, { + code: 'React.createElement("Ns:TestComponent")', + errors: [{message: 'React component Ns:TestComponent must not be in a namespace as React does not support them'}] + }] +});