diff --git a/docs/rules/no-danger.md b/docs/rules/no-danger.md index 04c664ba88..7c79000138 100644 --- a/docs/rules/no-danger.md +++ b/docs/rules/no-danger.md @@ -18,6 +18,10 @@ var React = require('react'); var Hello =
; ``` +```js +React.createElement('div', { dangerouslySetInnerHTML: { __html: 'Hello World' } }); +``` + Examples of **correct** code for this rule: ```jsx @@ -26,6 +30,10 @@ var React = require('react'); var Hello =
Hello World
; ``` +```js +React.createElement('div', { className: 'bar' }); +``` + ## Rule Options ```js diff --git a/lib/rules/no-danger.js b/lib/rules/no-danger.js index 54dbed49cf..25d62a4a1a 100644 --- a/lib/rules/no-danger.js +++ b/lib/rules/no-danger.js @@ -12,6 +12,9 @@ const minimatch = require('minimatch'); const docsUrl = require('../util/docsUrl'); const jsxUtil = require('../util/jsx'); const report = require('../util/report'); +const variableUtil = require('../util/variable'); +const getText = require('../util/eslint').getText; +const isCreateElement = require('../util/isCreateElement'); // ------------------------------------------------------------------------------ // Constants @@ -75,6 +78,44 @@ module.exports = { const configuration = context.options[0] || {}; const customComponentNames = configuration.customComponentNames || []; + function findSpreadVariable(node, name) { + return variableUtil.getVariableFromContext(context, node, name); + } + + function findDangerousProperty(node, seenProps) { + if (!node) { + return false; + } + if (node.type === 'Identifier') { + const variable = findSpreadVariable(node, node.name); + if (variable && variable.defs.length && variable.defs[0].node.init) { + return findDangerousProperty(variable.defs[0].node.init, seenProps); + } + } + if (!node.properties) { + return false; + } + return node.properties.find((prop) => { + if (prop.type === 'Property') { + const name = prop.key.type === 'Identifier' ? prop.key.name : prop.key.value; + return prop.key && !prop.computed && isDangerous(name); + } + if (prop.type === 'ExperimentalSpreadProperty' || prop.type === 'SpreadElement') { + if (prop.argument && prop.argument.type === 'Identifier') { + const variable = findSpreadVariable(node, prop.argument.name); + if (variable && variable.defs.length && variable.defs[0].node.init) { + if (seenProps.indexOf(prop.argument.name) > -1) { + return false; + } + const newSeenProps = seenProps.concat(prop.argument.name || []); + return findDangerousProperty(variable.defs[0].node.init, newSeenProps); + } + } + } + return false; + }); + } + return { JSXAttribute(node) { const nodeName = node.parent.name; @@ -91,6 +132,40 @@ module.exports = { }); } }, + + CallExpression(node) { + if ( + isCreateElement(context, node) + && node.arguments.length > 0 + ) { + const typeNode = node.arguments[0]; + let isDOMComponent = false; + let elementName = ''; + + if (typeNode.type === 'Literal' && typeof typeNode.value === 'string') { + elementName = typeNode.value; + isDOMComponent = /^[a-z]/.test(elementName); + } else if (typeNode.type === 'Identifier') { + elementName = typeNode.name; + } else if (typeNode.type === 'MemberExpression') { + elementName = getText(context, typeNode); + } + + const enableCheckingCustomComponent = customComponentNames.some((name) => minimatch(elementName, name)); + + if (enableCheckingCustomComponent || isDOMComponent) { + const dangerously = findDangerousProperty(node.arguments[1], []); + if (dangerously) { + report(context, messages.dangerousProp, 'dangerousProp', { + node: dangerously, + data: { + name: dangerously.key.type === 'Identifier' ? dangerously.key.name : dangerously.key.value, + }, + }); + } + } + } + }, }; }, }; diff --git a/tests/lib/rules/no-danger.js b/tests/lib/rules/no-danger.js index 9403a220bf..6cdceb96f4 100644 --- a/tests/lib/rules/no-danger.js +++ b/tests/lib/rules/no-danger.js @@ -52,6 +52,20 @@ ruleTester.run('no-danger', rule, { `, options: [{ customComponentNames: ['MUI*'] }], }, + { + code: 'React.createElement("div", { className: "bar" });', + }, + { + code: 'React.createElement(App, { dangerouslySetInnerHTML: { __html: "" } });', + }, + { + code: ` + function App() { + return React.createElement(Title, { dangerouslySetInnerHTML: { __html: "hello" } }); + } + `, + options: [{ customComponentNames: ['Home'] }], + }, ]), invalid: parsers.all([ { @@ -173,5 +187,72 @@ ruleTester.run('no-danger', rule, { }, ], }, + { + code: 'React.createElement("div", { dangerouslySetInnerHTML: { __html: "" } });', + errors: [ + { + messageId: 'dangerousProp', + data: { name: 'dangerouslySetInnerHTML' }, + }, + ], + }, + { + code: 'React.createElement("div", { "dangerouslySetInnerHTML": { __html: "" } });', + errors: [ + { + messageId: 'dangerousProp', + data: { name: 'dangerouslySetInnerHTML' }, + }, + ], + }, + { + code: 'React.createElement(App, { dangerouslySetInnerHTML: { __html: "" } });', + options: [{ customComponentNames: ['*'] }], + errors: [ + { + messageId: 'dangerousProp', + data: { name: 'dangerouslySetInnerHTML' }, + }, + ], + }, + { + code: ` + function App() { + return React.createElement(Title, { dangerouslySetInnerHTML: { __html: "hello" } }); + } + `, + options: [{ customComponentNames: ['Title'] }], + errors: [ + { + messageId: 'dangerousProp', + data: { name: 'dangerouslySetInnerHTML' }, + }, + ], + }, + { + code: ` + const props = { dangerouslySetInnerHTML: { __html: "" } }; + React.createElement("div", props); + `, + errors: [ + { + messageId: 'dangerousProp', + data: { name: 'dangerouslySetInnerHTML' }, + }, + ], + }, + { + code: ` + const props = { dangerouslySetInnerHTML: { __html: "" } }; + React.createElement(App, props); + `, + options: [{ customComponentNames: ['*'] }], + errors: [ + { + messageId: 'dangerousProp', + data: { name: 'dangerouslySetInnerHTML' }, + }, + ], + }, ]), });