void-dom-elements-no-children.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * @fileoverview Prevent void elements (e.g. <img />, <br />) from receiving
  3. * children
  4. * @author Joe Lencioni
  5. */
  6. 'use strict';
  7. const has = require('has');
  8. const Components = require('../util/Components');
  9. const docsUrl = require('../util/docsUrl');
  10. // ------------------------------------------------------------------------------
  11. // Helpers
  12. // ------------------------------------------------------------------------------
  13. // Using an object here to avoid array scan. We should switch to Set once
  14. // support is good enough.
  15. const VOID_DOM_ELEMENTS = {
  16. area: true,
  17. base: true,
  18. br: true,
  19. col: true,
  20. embed: true,
  21. hr: true,
  22. img: true,
  23. input: true,
  24. keygen: true,
  25. link: true,
  26. menuitem: true,
  27. meta: true,
  28. param: true,
  29. source: true,
  30. track: true,
  31. wbr: true
  32. };
  33. function isVoidDOMElement(elementName) {
  34. return has(VOID_DOM_ELEMENTS, elementName);
  35. }
  36. function errorMessage(elementName) {
  37. return `Void DOM element <${elementName} /> cannot receive children.`;
  38. }
  39. // ------------------------------------------------------------------------------
  40. // Rule Definition
  41. // ------------------------------------------------------------------------------
  42. module.exports = {
  43. meta: {
  44. docs: {
  45. description: 'Prevent passing of children to void DOM elements (e.g. <br />).',
  46. category: 'Best Practices',
  47. recommended: false,
  48. url: docsUrl('void-dom-elements-no-children')
  49. },
  50. schema: []
  51. },
  52. create: Components.detect((context, components, utils) => ({
  53. JSXElement: function(node) {
  54. const elementName = node.openingElement.name.name;
  55. if (!isVoidDOMElement(elementName)) {
  56. // e.g. <div />
  57. return;
  58. }
  59. if (node.children.length > 0) {
  60. // e.g. <br>Foo</br>
  61. context.report({
  62. node: node,
  63. message: errorMessage(elementName)
  64. });
  65. }
  66. const attributes = node.openingElement.attributes;
  67. const hasChildrenAttributeOrDanger = attributes.some(attribute => {
  68. if (!attribute.name) {
  69. return false;
  70. }
  71. return attribute.name.name === 'children' || attribute.name.name === 'dangerouslySetInnerHTML';
  72. });
  73. if (hasChildrenAttributeOrDanger) {
  74. // e.g. <br children="Foo" />
  75. context.report({
  76. node: node,
  77. message: errorMessage(elementName)
  78. });
  79. }
  80. },
  81. CallExpression: function(node) {
  82. if (node.callee.type !== 'MemberExpression' && node.callee.type !== 'Identifier') {
  83. return;
  84. }
  85. if (!utils.isReactCreateElement(node)) {
  86. return;
  87. }
  88. const args = node.arguments;
  89. if (args.length < 1) {
  90. // React.createElement() should not crash linter
  91. return;
  92. }
  93. const elementName = args[0].value;
  94. if (!isVoidDOMElement(elementName)) {
  95. // e.g. React.createElement('div');
  96. return;
  97. }
  98. if (args.length < 2 || args[1].type !== 'ObjectExpression') {
  99. return;
  100. }
  101. const firstChild = args[2];
  102. if (firstChild) {
  103. // e.g. React.createElement('br', undefined, 'Foo')
  104. context.report({
  105. node: node,
  106. message: errorMessage(elementName)
  107. });
  108. }
  109. const props = args[1].properties;
  110. const hasChildrenPropOrDanger = props.some(prop => {
  111. if (!prop.key) {
  112. return false;
  113. }
  114. return prop.key.name === 'children' || prop.key.name === 'dangerouslySetInnerHTML';
  115. });
  116. if (hasChildrenPropOrDanger) {
  117. // e.g. React.createElement('br', { children: 'Foo' })
  118. context.report({
  119. node: node,
  120. message: errorMessage(elementName)
  121. });
  122. }
  123. }
  124. }))
  125. };