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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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. // ------------------------------------------------------------------------------
  37. // Rule Definition
  38. // ------------------------------------------------------------------------------
  39. module.exports = {
  40. meta: {
  41. docs: {
  42. description: 'Prevent passing of children to void DOM elements (e.g. `<br />`).',
  43. category: 'Best Practices',
  44. recommended: false,
  45. url: docsUrl('void-dom-elements-no-children')
  46. },
  47. messages: {
  48. noChildrenInVoidEl: 'Void DOM element <{{element}} /> cannot receive children.'
  49. },
  50. schema: []
  51. },
  52. create: Components.detect((context, components, utils) => ({
  53. JSXElement(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,
  63. messageId: 'noChildrenInVoidEl',
  64. data: {
  65. element: elementName
  66. }
  67. });
  68. }
  69. const attributes = node.openingElement.attributes;
  70. const hasChildrenAttributeOrDanger = attributes.some((attribute) => {
  71. if (!attribute.name) {
  72. return false;
  73. }
  74. return attribute.name.name === 'children' || attribute.name.name === 'dangerouslySetInnerHTML';
  75. });
  76. if (hasChildrenAttributeOrDanger) {
  77. // e.g. <br children="Foo" />
  78. context.report({
  79. node,
  80. messageId: 'noChildrenInVoidEl',
  81. data: {
  82. element: elementName
  83. }
  84. });
  85. }
  86. },
  87. CallExpression(node) {
  88. if (node.callee.type !== 'MemberExpression' && node.callee.type !== 'Identifier') {
  89. return;
  90. }
  91. if (!utils.isCreateElement(node)) {
  92. return;
  93. }
  94. const args = node.arguments;
  95. if (args.length < 1) {
  96. // React.createElement() should not crash linter
  97. return;
  98. }
  99. const elementName = args[0].value;
  100. if (!isVoidDOMElement(elementName)) {
  101. // e.g. React.createElement('div');
  102. return;
  103. }
  104. if (args.length < 2 || args[1].type !== 'ObjectExpression') {
  105. return;
  106. }
  107. const firstChild = args[2];
  108. if (firstChild) {
  109. // e.g. React.createElement('br', undefined, 'Foo')
  110. context.report({
  111. node,
  112. messageId: 'noChildrenInVoidEl',
  113. data: {
  114. element: elementName
  115. }
  116. });
  117. }
  118. const props = args[1].properties;
  119. const hasChildrenPropOrDanger = props.some((prop) => {
  120. if (!prop.key) {
  121. return false;
  122. }
  123. return prop.key.name === 'children' || prop.key.name === 'dangerouslySetInnerHTML';
  124. });
  125. if (hasChildrenPropOrDanger) {
  126. // e.g. React.createElement('br', { children: 'Foo' })
  127. context.report({
  128. node,
  129. messageId: 'noChildrenInVoidEl',
  130. data: {
  131. element: elementName
  132. }
  133. });
  134. }
  135. }
  136. }))
  137. };