no-danger-with-children.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. /**
  2. * @fileoverview Report when a DOM element is using both children and dangerouslySetInnerHTML
  3. * @author David Petersen
  4. */
  5. 'use strict';
  6. const variableUtil = require('../util/variable');
  7. const jsxUtil = require('../util/jsx');
  8. const docsUrl = require('../util/docsUrl');
  9. const report = require('../util/report');
  10. // ------------------------------------------------------------------------------
  11. // Rule Definition
  12. // ------------------------------------------------------------------------------
  13. const messages = {
  14. dangerWithChildren: 'Only set one of `children` or `props.dangerouslySetInnerHTML`',
  15. };
  16. module.exports = {
  17. meta: {
  18. docs: {
  19. description: 'Disallow when a DOM element is using both children and dangerouslySetInnerHTML',
  20. category: 'Possible Errors',
  21. recommended: true,
  22. url: docsUrl('no-danger-with-children'),
  23. },
  24. messages,
  25. schema: [], // no options
  26. },
  27. create(context) {
  28. function findSpreadVariable(name) {
  29. return variableUtil.variablesInScope(context).find((item) => item.name === name);
  30. }
  31. /**
  32. * Takes a ObjectExpression and returns the value of the prop if it has it
  33. * @param {object} node - ObjectExpression node
  34. * @param {string} propName - name of the prop to look for
  35. * @param {string[]} seenProps
  36. * @returns {object | boolean}
  37. */
  38. function findObjectProp(node, propName, seenProps) {
  39. if (!node.properties) {
  40. return false;
  41. }
  42. return node.properties.find((prop) => {
  43. if (prop.type === 'Property') {
  44. return prop.key.name === propName;
  45. }
  46. if (prop.type === 'ExperimentalSpreadProperty' || prop.type === 'SpreadElement') {
  47. const variable = findSpreadVariable(prop.argument.name);
  48. if (variable && variable.defs.length && variable.defs[0].node.init) {
  49. if (seenProps.indexOf(prop.argument.name) > -1) {
  50. return false;
  51. }
  52. const newSeenProps = seenProps.concat(prop.argument.name || []);
  53. return findObjectProp(variable.defs[0].node.init, propName, newSeenProps);
  54. }
  55. }
  56. return false;
  57. });
  58. }
  59. /**
  60. * Takes a JSXElement and returns the value of the prop if it has it
  61. * @param {object} node - JSXElement node
  62. * @param {string} propName - name of the prop to look for
  63. * @returns {object | boolean}
  64. */
  65. function findJsxProp(node, propName) {
  66. const attributes = node.openingElement.attributes;
  67. return attributes.find((attribute) => {
  68. if (attribute.type === 'JSXSpreadAttribute') {
  69. const variable = findSpreadVariable(attribute.argument.name);
  70. if (variable && variable.defs.length && variable.defs[0].node.init) {
  71. return findObjectProp(variable.defs[0].node.init, propName, []);
  72. }
  73. }
  74. return attribute.name && attribute.name.name === propName;
  75. });
  76. }
  77. /**
  78. * Checks to see if a node is a line break
  79. * @param {ASTNode} node The AST node being checked
  80. * @returns {Boolean} True if node is a line break, false if not
  81. */
  82. function isLineBreak(node) {
  83. const isLiteral = node.type === 'Literal' || node.type === 'JSXText';
  84. const isMultiline = node.loc.start.line !== node.loc.end.line;
  85. const isWhiteSpaces = jsxUtil.isWhiteSpaces(node.value);
  86. return isLiteral && isMultiline && isWhiteSpaces;
  87. }
  88. return {
  89. JSXElement(node) {
  90. let hasChildren = false;
  91. if (node.children.length && !isLineBreak(node.children[0])) {
  92. hasChildren = true;
  93. } else if (findJsxProp(node, 'children')) {
  94. hasChildren = true;
  95. }
  96. if (
  97. node.openingElement.attributes
  98. && hasChildren
  99. && findJsxProp(node, 'dangerouslySetInnerHTML')
  100. ) {
  101. report(context, messages.dangerWithChildren, 'dangerWithChildren', {
  102. node,
  103. });
  104. }
  105. },
  106. CallExpression(node) {
  107. if (
  108. node.callee
  109. && node.callee.type === 'MemberExpression'
  110. && node.callee.property.name === 'createElement'
  111. && node.arguments.length > 1
  112. ) {
  113. let hasChildren = false;
  114. let props = node.arguments[1];
  115. if (props.type === 'Identifier') {
  116. const variable = variableUtil.variablesInScope(context).find((item) => item.name === props.name);
  117. if (variable && variable.defs.length && variable.defs[0].node.init) {
  118. props = variable.defs[0].node.init;
  119. }
  120. }
  121. const dangerously = findObjectProp(props, 'dangerouslySetInnerHTML', []);
  122. if (node.arguments.length === 2) {
  123. if (findObjectProp(props, 'children', [])) {
  124. hasChildren = true;
  125. }
  126. } else {
  127. hasChildren = true;
  128. }
  129. if (dangerously && hasChildren) {
  130. report(context, messages.dangerWithChildren, 'dangerWithChildren', {
  131. node,
  132. });
  133. }
  134. }
  135. },
  136. };
  137. },
  138. };