diff --git a/CHANGELOG.md b/CHANGELOG.md
index a564e9b411..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
@@ -12,6 +17,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
@@ -3436,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/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/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/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;
+};
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'}]
+ }]
+});