UNPKG

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