Skip to content

Commit e33e82f

Browse files
Nokel81ljharb
authored andcommitted
add checking createElement
1 parent ae78cc9 commit e33e82f

File tree

2 files changed

+231
-3
lines changed

2 files changed

+231
-3
lines changed

lib/rules/no-invalid-html-attribute.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,89 @@ function checkAttribute(context, node) {
185185
}
186186
}
187187

188+
function isValidCreateElement(node) {
189+
return node.callee
190+
&& node.callee.type === 'MemberExpression'
191+
&& node.callee.object.name === 'React'
192+
&& node.callee.property.name === 'createElement'
193+
&& node.arguments.length > 0;
194+
}
195+
196+
function checkPropValidValue(context, node, value, attribute) {
197+
const validTags = VALID_VALUES.get(attribute);
198+
199+
if (value.type !== 'Literal') {
200+
return; // cannot check non-literals
201+
}
202+
203+
const validTagSet = validTags.get(value.value);
204+
if (!validTagSet) {
205+
return context.report({
206+
node: value,
207+
message: `${value.raw} is never a valid "${attribute}" attribute value.`
208+
});
209+
}
210+
211+
if (!validTagSet.has(node.arguments[0].value)) {
212+
return context.report({
213+
node: value,
214+
message: `${value.raw} is not a valid value of "${attribute}" for a ${node.arguments[0].raw} element`
215+
});
216+
}
217+
}
218+
219+
/**
220+
*
221+
* @param {*} context
222+
* @param {*} node
223+
* @param {string} attribute
224+
*/
225+
function checkCreateProps(context, node, attribute) {
226+
if (node.arguments[0].type !== 'Literal') {
227+
return; // can only check literals
228+
}
229+
230+
const propsArg = node.arguments[1];
231+
232+
if (!propsArg || propsArg.type !== 'ObjectExpression') {
233+
return; // can't check variables, computed, or shorthands
234+
}
235+
236+
propsArg.properties.filter((prop) => (
237+
prop.key.type !== 'Identifier' // cannot check computed keys
238+
&& prop.key.name !== attribute // ignore not this attribute
239+
)).forEach((prop) => {
240+
if (!COMPONENT_ATTRIBUTE_MAP.get(attribute).has(node.arguments[0].value)) {
241+
const tagNames = Array.from(
242+
COMPONENT_ATTRIBUTE_MAP.get(attribute).values(),
243+
(tagName) => `"<${tagName}>"`
244+
).join(', ');
245+
246+
context.report({
247+
node,
248+
message: `The "${attribute}" attribute only has meaning on the tags: ${tagNames}`
249+
});
250+
} else if (prop.method) {
251+
context.report({
252+
node: prop,
253+
message: `The "${attribute}" attribute cannot be a method.`
254+
});
255+
}
256+
257+
if (prop.shorthand || prop.computed) {
258+
return; // cannot check these
259+
}
260+
261+
if (prop.value.type === 'ArrayExpression') {
262+
prop.value.elements.forEach((value) => {
263+
checkPropValidValue(context, node, value, attribute);
264+
});
265+
} else {
266+
checkPropValidValue(context, node, prop.value, attribute);
267+
}
268+
});
269+
}
270+
188271
module.exports = {
189272
meta: {
190273
fixable: 'code',
@@ -213,6 +296,18 @@ module.exports = {
213296
}
214297

215298
checkAttribute(context, node);
299+
},
300+
301+
CallExpression(node) {
302+
if (!isValidCreateElement(node)) {
303+
return;
304+
}
305+
306+
const attributes = new Set(context.options[0] || DEFAULT_ATTRIBUTES);
307+
308+
for (const attribute of attributes) {
309+
checkCreateProps(context, node, attribute);
310+
}
216311
}
217312
};
218313
}

tests/lib/rules/no-invalid-html-attribute.js

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,64 +29,180 @@ const ruleTester = new RuleTester({parserOptions});
2929
ruleTester.run('no-invalid-html-attribute', rule, {
3030
valid: [
3131
{code: '<a rel="alternate"></a>'},
32+
{code: 'React.createElement("a", { rel: "alternate" })'},
33+
{code: 'React.createElement("a", { rel: ["alternate"] })'},
3234
{code: '<a rel="author"></a>'},
35+
{code: 'React.createElement("a", { rel: "author" })'},
36+
{code: 'React.createElement("a", { rel: ["author"] })'},
3337
{code: '<a rel="bookmark"></a>'},
38+
{code: 'React.createElement("a", { rel: "bookmark" })'},
39+
{code: 'React.createElement("a", { rel: ["bookmark"] })'},
3440
{code: '<a rel="external"></a>'},
41+
{code: 'React.createElement("a", { rel: "external" })'},
42+
{code: 'React.createElement("a", { rel: ["external"] })'},
3543
{code: '<a rel="help"></a>'},
44+
{code: 'React.createElement("a", { rel: "help" })'},
45+
{code: 'React.createElement("a", { rel: ["help"] })'},
3646
{code: '<a rel="license"></a>'},
47+
{code: 'React.createElement("a", { rel: "license" })'},
48+
{code: 'React.createElement("a", { rel: ["license"] })'},
3749
{code: '<a rel="next"></a>'},
50+
{code: 'React.createElement("a", { rel: "next" })'},
51+
{code: 'React.createElement("a", { rel: ["next"] })'},
3852
{code: '<a rel="nofollow"></a>'},
53+
{code: 'React.createElement("a", { rel: "nofollow" })'},
54+
{code: 'React.createElement("a", { rel: ["nofollow"] })'},
3955
{code: '<a rel="noopener"></a>'},
56+
{code: 'React.createElement("a", { rel: "noopener" })'},
57+
{code: 'React.createElement("a", { rel: ["noopener"] })'},
4058
{code: '<a rel="noreferrer"></a>'},
59+
{code: 'React.createElement("a", { rel: "noreferrer" })'},
60+
{code: 'React.createElement("a", { rel: ["noreferrer"] })'},
4161
{code: '<a rel="opener"></a>'},
62+
{code: 'React.createElement("a", { rel: "opener" })'},
63+
{code: 'React.createElement("a", { rel: ["opener"] })'},
4264
{code: '<a rel="prev"></a>'},
65+
{code: 'React.createElement("a", { rel: "prev" })'},
66+
{code: 'React.createElement("a", { rel: ["prev"] })'},
4367
{code: '<a rel="search"></a>'},
68+
{code: 'React.createElement("a", { rel: "search" })'},
69+
{code: 'React.createElement("a", { rel: ["search"] })'},
4470
{code: '<a rel="tag"></a>'},
71+
{code: 'React.createElement("a", { rel: "tag" })'},
72+
{code: 'React.createElement("a", { rel: ["tag"] })'},
4573
{code: '<area rel="alternate"></area>'},
74+
{code: 'React.createElement("area", { rel: "alternate" })'},
75+
{code: 'React.createElement("area", { rel: ["alternate"] })'},
4676
{code: '<area rel="author"></area>'},
77+
{code: 'React.createElement("area", { rel: "author" })'},
78+
{code: 'React.createElement("area", { rel: ["author"] })'},
4779
{code: '<area rel="bookmark"></area>'},
80+
{code: 'React.createElement("area", { rel: "bookmark" })'},
81+
{code: 'React.createElement("area", { rel: ["bookmark"] })'},
4882
{code: '<area rel="external"></area>'},
83+
{code: 'React.createElement("area", { rel: "external" })'},
84+
{code: 'React.createElement("area", { rel: ["external"] })'},
4985
{code: '<area rel="help"></area>'},
86+
{code: 'React.createElement("area", { rel: "help" })'},
87+
{code: 'React.createElement("area", { rel: ["help"] })'},
5088
{code: '<area rel="license"></area>'},
89+
{code: 'React.createElement("area", { rel: "license" })'},
90+
{code: 'React.createElement("area", { rel: ["license"] })'},
5191
{code: '<area rel="next"></area>'},
92+
{code: 'React.createElement("area", { rel: "next" })'},
93+
{code: 'React.createElement("area", { rel: ["next"] })'},
5294
{code: '<area rel="nofollow"></area>'},
95+
{code: 'React.createElement("area", { rel: "nofollow" })'},
96+
{code: 'React.createElement("area", { rel: ["nofollow"] })'},
5397
{code: '<area rel="noopener"></area>'},
98+
{code: 'React.createElement("area", { rel: "noopener" })'},
99+
{code: 'React.createElement("area", { rel: ["noopener"] })'},
54100
{code: '<area rel="noreferrer"></area>'},
101+
{code: 'React.createElement("area", { rel: "noreferrer" })'},
102+
{code: 'React.createElement("area", { rel: ["noreferrer"] })'},
55103
{code: '<area rel="opener"></area>'},
104+
{code: 'React.createElement("area", { rel: "opener" })'},
105+
{code: 'React.createElement("area", { rel: ["opener"] })'},
56106
{code: '<area rel="prev"></area>'},
107+
{code: 'React.createElement("area", { rel: "prev" })'},
108+
{code: 'React.createElement("area", { rel: ["prev"] })'},
57109
{code: '<area rel="search"></area>'},
110+
{code: 'React.createElement("area", { rel: "search" })'},
111+
{code: 'React.createElement("area", { rel: ["search"] })'},
58112
{code: '<area rel="tag"></area>'},
113+
{code: 'React.createElement("area", { rel: "tag" })'},
114+
{code: 'React.createElement("area", { rel: ["tag"] })'},
59115
{code: '<link rel="alternate"></link>'},
116+
{code: 'React.createElement("link", { rel: "alternate" })'},
117+
{code: 'React.createElement("link", { rel: ["alternate"] })'},
60118
{code: '<link rel="author"></link>'},
119+
{code: 'React.createElement("link", { rel: "author" })'},
120+
{code: 'React.createElement("link", { rel: ["author"] })'},
61121
{code: '<link rel="canonical"></link>'},
122+
{code: 'React.createElement("link", { rel: "canonical" })'},
123+
{code: 'React.createElement("link", { rel: ["canonical"] })'},
62124
{code: '<link rel="dns-prefetch"></link>'},
125+
{code: 'React.createElement("link", { rel: "dns-prefetch" })'},
126+
{code: 'React.createElement("link", { rel: ["dns-prefetch"] })'},
63127
{code: '<link rel="help"></link>'},
128+
{code: 'React.createElement("link", { rel: "help" })'},
129+
{code: 'React.createElement("link", { rel: ["help"] })'},
64130
{code: '<link rel="icon"></link>'},
131+
{code: 'React.createElement("link", { rel: "icon" })'},
132+
{code: 'React.createElement("link", { rel: ["icon"] })'},
65133
{code: '<link rel="license"></link>'},
134+
{code: 'React.createElement("link", { rel: "license" })'},
135+
{code: 'React.createElement("link", { rel: ["license"] })'},
66136
{code: '<link rel="manifest"></link>'},
137+
{code: 'React.createElement("link", { rel: "manifest" })'},
138+
{code: 'React.createElement("link", { rel: ["manifest"] })'},
67139
{code: '<link rel="modulepreload"></link>'},
140+
{code: 'React.createElement("link", { rel: "modulepreload" })'},
141+
{code: 'React.createElement("link", { rel: ["modulepreload"] })'},
68142
{code: '<link rel="next"></link>'},
143+
{code: 'React.createElement("link", { rel: "next" })'},
144+
{code: 'React.createElement("link", { rel: ["next"] })'},
69145
{code: '<link rel="pingback"></link>'},
146+
{code: 'React.createElement("link", { rel: "pingback" })'},
147+
{code: 'React.createElement("link", { rel: ["pingback"] })'},
70148
{code: '<link rel="preconnect"></link>'},
149+
{code: 'React.createElement("link", { rel: "preconnect" })'},
150+
{code: 'React.createElement("link", { rel: ["preconnect"] })'},
71151
{code: '<link rel="prefetch"></link>'},
152+
{code: 'React.createElement("link", { rel: "prefetch" })'},
153+
{code: 'React.createElement("link", { rel: ["prefetch"] })'},
72154
{code: '<link rel="preload"></link>'},
155+
{code: 'React.createElement("link", { rel: "preload" })'},
156+
{code: 'React.createElement("link", { rel: ["preload"] })'},
73157
{code: '<link rel="prerender"></link>'},
158+
{code: 'React.createElement("link", { rel: "prerender" })'},
159+
{code: 'React.createElement("link", { rel: ["prerender"] })'},
74160
{code: '<link rel="prev"></link>'},
161+
{code: 'React.createElement("link", { rel: "prev" })'},
162+
{code: 'React.createElement("link", { rel: ["prev"] })'},
75163
{code: '<link rel="search"></link>'},
164+
{code: 'React.createElement("link", { rel: "search" })'},
165+
{code: 'React.createElement("link", { rel: ["search"] })'},
76166
{code: '<link rel="stylesheet"></link>'},
167+
{code: 'React.createElement("link", { rel: "stylesheet" })'},
168+
{code: 'React.createElement("link", { rel: ["stylesheet"] })'},
77169
{code: '<form rel="external"></form>'},
170+
{code: 'React.createElement("form", { rel: "external" })'},
171+
{code: 'React.createElement("form", { rel: ["external"] })'},
78172
{code: '<form rel="help"></form>'},
173+
{code: 'React.createElement("form", { rel: "help" })'},
174+
{code: 'React.createElement("form", { rel: ["help"] })'},
79175
{code: '<form rel="license"></form>'},
176+
{code: 'React.createElement("form", { rel: "license" })'},
177+
{code: 'React.createElement("form", { rel: ["license"] })'},
80178
{code: '<form rel="next"></form>'},
179+
{code: 'React.createElement("form", { rel: "next" })'},
180+
{code: 'React.createElement("form", { rel: ["next"] })'},
81181
{code: '<form rel="nofollow"></form>'},
182+
{code: 'React.createElement("form", { rel: "nofollow" })'},
183+
{code: 'React.createElement("form", { rel: ["nofollow"] })'},
82184
{code: '<form rel="noopener"></form>'},
185+
{code: 'React.createElement("form", { rel: "noopener" })'},
186+
{code: 'React.createElement("form", { rel: ["noopener"] })'},
83187
{code: '<form rel="noreferrer"></form>'},
188+
{code: 'React.createElement("form", { rel: "noreferrer" })'},
189+
{code: 'React.createElement("form", { rel: ["noreferrer"] })'},
84190
{code: '<form rel="opener"></form>'},
191+
{code: 'React.createElement("form", { rel: "opener" })'},
192+
{code: 'React.createElement("form", { rel: ["opener"] })'},
85193
{code: '<form rel="prev"></form>'},
194+
{code: 'React.createElement("form", { rel: "prev" })'},
195+
{code: 'React.createElement("form", { rel: ["prev"] })'},
86196
{code: '<form rel="search"></form>'},
197+
{code: 'React.createElement("form", { rel: "search" })'},
198+
{code: 'React.createElement("form", { rel: ["search"] })'},
87199
{code: '<form rel={callFoo()}></form>'},
200+
{code: 'React.createElement("form", { rel: callFoo() })'},
201+
{code: 'React.createElement("form", { rel: [callFoo()] })'},
88202
{code: '<a rel={{a: "noreferrer"}["a"]}></a>'},
89-
{code: '<a rel={{a: "noreferrer"}["b"]}></a>'}
203+
{code: '<a rel={{a: "noreferrer"}["b"]}></a>'},
204+
{code: '<Foo rel></Foo>'},
205+
{code: 'React.createElement("Foo", { rel: true })'}
90206
],
91207
invalid: [
92208
{
@@ -97,8 +213,7 @@ ruleTester.run('no-invalid-html-attribute', rule, {
97213
}]
98214
},
99215
{
100-
code: '<Foo rel></Foo>',
101-
output: '<Foo ></Foo>',
216+
code: 'React.createElement("html", { rel: 1 })',
102217
errors: [{
103218
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
104219
}]
@@ -110,6 +225,18 @@ ruleTester.run('no-invalid-html-attribute', rule, {
110225
message: 'An empty "rel" attribute is meaningless.'
111226
}]
112227
},
228+
{
229+
code: 'React.createElement("a", { rel: 1 })',
230+
errors: [{
231+
message: '1 is never a valid "rel" attribute value.'
232+
}]
233+
},
234+
{
235+
code: 'React.createElement("a", { rel() { return 1; } })',
236+
errors: [{
237+
message: 'The "rel" attribute cannot be a method.'
238+
}]
239+
},
113240
{
114241
code: '<any rel></any>',
115242
output: '<any ></any>',
@@ -173,6 +300,12 @@ ruleTester.run('no-invalid-html-attribute', rule, {
173300
message: '"foobar" is never a valid "rel" attribute value.'
174301
}]
175302
},
303+
{
304+
code: 'React.createElement("a", { rel: ["noreferrer", "noopener", "foobar" ] })',
305+
errors: [{
306+
message: '"foobar" is never a valid "rel" attribute value.'
307+
}]
308+
},
176309
{
177310
code: '<a rel={"foobar noreferrer noopener"}></a>',
178311
output: '<a rel={" noreferrer noopener"}></a>',

0 commit comments

Comments
 (0)