Skip to content

Commit ce77747

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

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed

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

+109
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,103 @@ 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+
) {
234+
return; // can't check variables, computed, or shorthands
235+
}
236+
237+
for (const prop of propsArg.properties) {
238+
if (prop.key.type !== 'Identifier') {
239+
continue; // cannot check computed keys
240+
}
241+
242+
if (prop.key.name !== attribute) {
243+
continue; // ignore not this attribute
244+
}
245+
246+
if (!COMPONENT_ATTRIBUTE_MAP.get(attribute).has(node.arguments[0].value)) {
247+
const tagNames = Array.from(
248+
COMPONENT_ATTRIBUTE_MAP.get(attribute).values(),
249+
(tagName) => `"<${tagName}>"`
250+
).join(', ');
251+
252+
context.report({
253+
node,
254+
message: `The "${attribute}" attribute only has meaning on the tags: ${tagNames}`
255+
});
256+
257+
continue;
258+
}
259+
260+
if (prop.method) {
261+
context.report({
262+
node: prop,
263+
message: `The "${attribute}" attribute cannot be a method.`
264+
});
265+
266+
continue;
267+
}
268+
269+
if (prop.shorthand || prop.computed) {
270+
continue; // cannot check these
271+
}
272+
273+
if (prop.value.type === 'ArrayExpression') {
274+
for (const value of prop.value.elements) {
275+
checkPropValidValue(context, node, value, attribute);
276+
}
277+
278+
continue;
279+
}
280+
281+
checkPropValidValue(context, node, prop.value, attribute);
282+
}
283+
}
284+
188285
module.exports = {
189286
meta: {
190287
fixable: 'code',
@@ -213,6 +310,18 @@ module.exports = {
213310
}
214311

215312
checkAttribute(context, node);
313+
},
314+
315+
CallExpression(node) {
316+
if (!isValidCreateElement(node)) {
317+
return;
318+
}
319+
320+
const attributes = new Set(context.options[0] || DEFAULT_ATTRIBUTES);
321+
322+
for (const attribute of attributes) {
323+
checkCreateProps(context, node, attribute);
324+
}
216325
}
217326
};
218327
}

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

+144
Original file line numberDiff line numberDiff line change
@@ -29,62 +29,176 @@ 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>'},
89203
{code: '<a rel={{a: "noreferrer"}["b"]}></a>'}
90204
],
@@ -96,20 +210,44 @@ ruleTester.run('no-invalid-html-attribute', rule, {
96210
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
97211
}]
98212
},
213+
{
214+
code: 'React.createElement("html", { rel: 1 })',
215+
errors: [{
216+
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
217+
}]
218+
},
99219
{
100220
code: '<Foo rel></Foo>',
101221
output: '<Foo ></Foo>',
102222
errors: [{
103223
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
104224
}]
105225
},
226+
{
227+
code: 'React.createElement("Foo", { rel: true })',
228+
errors: [{
229+
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
230+
}]
231+
},
106232
{
107233
code: '<a rel></a>',
108234
output: '<a ></a>',
109235
errors: [{
110236
message: 'An empty "rel" attribute is meaningless.'
111237
}]
112238
},
239+
{
240+
code: 'React.createElement("a", { rel: 1 })',
241+
errors: [{
242+
message: '1 is never a valid "rel" attribute value.'
243+
}]
244+
},
245+
{
246+
code: 'React.createElement("a", { rel() { return 1; } })',
247+
errors: [{
248+
message: 'The "rel" attribute cannot be a method.'
249+
}]
250+
},
113251
{
114252
code: '<any rel></any>',
115253
output: '<any ></any>',
@@ -173,6 +311,12 @@ ruleTester.run('no-invalid-html-attribute', rule, {
173311
message: '"foobar" is never a valid "rel" attribute value.'
174312
}]
175313
},
314+
{
315+
code: 'React.createElement("a", { rel: ["noreferrer", "noopener", "foobar" ] })',
316+
errors: [{
317+
message: '"foobar" is never a valid "rel" attribute value.'
318+
}]
319+
},
176320
{
177321
code: '<a rel={"foobar noreferrer noopener"}></a>',
178322
output: '<a rel={" noreferrer noopener"}></a>',

0 commit comments

Comments
 (0)