Skip to content

Commit

Permalink
add checking createElement
Browse files Browse the repository at this point in the history
  • Loading branch information
Nokel81 authored and ljharb committed Mar 23, 2021
1 parent ae78cc9 commit ce77747
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 0 deletions.
109 changes: 109 additions & 0 deletions lib/rules/no-invalid-html-attribute.js
Expand Up @@ -185,6 +185,103 @@ function checkAttribute(context, node) {
}
}

function isValidCreateElement(node) {
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.object.name === 'React'
&& node.callee.property.name === 'createElement'
&& node.arguments.length > 0;
}

function checkPropValidValue(context, node, value, attribute) {
const validTags = VALID_VALUES.get(attribute);

if (value.type !== 'Literal') {
return; // cannot check non-literals
}

const validTagSet = validTags.get(value.value);
if (!validTagSet) {
return context.report({
node: value,
message: `${value.raw} is never a valid "${attribute}" attribute value.`
})
}

if (!validTagSet.has(node.arguments[0].value)) {
return context.report({
node: value,
message: `${value.raw} is not a valid value of "${attribute}" for a ${node.arguments[0].raw} element`
})
}
}

/**
*
* @param {*} context
* @param {*} node
* @param {string} attribute
*/
function checkCreateProps(context, node, attribute) {
if (node.arguments[0].type !== 'Literal') {
return; // can only check literals
}

const propsArg = node.arguments[1];

if (!propsArg || propsArg.type !== 'ObjectExpression'
) {
return; // can't check variables, computed, or shorthands
}

for (const prop of propsArg.properties) {
if (prop.key.type !== 'Identifier') {
continue; // cannot check computed keys
}

if (prop.key.name !== attribute) {
continue; // ignore not this attribute
}

if (!COMPONENT_ATTRIBUTE_MAP.get(attribute).has(node.arguments[0].value)) {
const tagNames = Array.from(
COMPONENT_ATTRIBUTE_MAP.get(attribute).values(),
(tagName) => `"<${tagName}>"`
).join(', ');

context.report({
node,
message: `The "${attribute}" attribute only has meaning on the tags: ${tagNames}`
});

continue;
}

if (prop.method) {
context.report({
node: prop,
message: `The "${attribute}" attribute cannot be a method.`
});

continue;
}

if (prop.shorthand || prop.computed) {
continue; // cannot check these
}

if (prop.value.type === 'ArrayExpression') {
for (const value of prop.value.elements) {
checkPropValidValue(context, node, value, attribute);
}

continue;
}

checkPropValidValue(context, node, prop.value, attribute);
}
}

module.exports = {
meta: {
fixable: 'code',
Expand Down Expand Up @@ -213,6 +310,18 @@ module.exports = {
}

checkAttribute(context, node);
},

CallExpression(node) {
if (!isValidCreateElement(node)) {
return;
}

const attributes = new Set(context.options[0] || DEFAULT_ATTRIBUTES);

for (const attribute of attributes) {
checkCreateProps(context, node, attribute);
}
}
};
}
Expand Down
144 changes: 144 additions & 0 deletions tests/lib/rules/no-invalid-html-attribute.js
Expand Up @@ -29,62 +29,176 @@ const ruleTester = new RuleTester({parserOptions});
ruleTester.run('no-invalid-html-attribute', rule, {
valid: [
{code: '<a rel="alternate"></a>'},
{code: 'React.createElement("a", { rel: "alternate" })'},
{code: 'React.createElement("a", { rel: ["alternate"] })'},
{code: '<a rel="author"></a>'},
{code: 'React.createElement("a", { rel: "author" })'},
{code: 'React.createElement("a", { rel: ["author"] })'},
{code: '<a rel="bookmark"></a>'},
{code: 'React.createElement("a", { rel: "bookmark" })'},
{code: 'React.createElement("a", { rel: ["bookmark"] })'},
{code: '<a rel="external"></a>'},
{code: 'React.createElement("a", { rel: "external" })'},
{code: 'React.createElement("a", { rel: ["external"] })'},
{code: '<a rel="help"></a>'},
{code: 'React.createElement("a", { rel: "help" })'},
{code: 'React.createElement("a", { rel: ["help"] })'},
{code: '<a rel="license"></a>'},
{code: 'React.createElement("a", { rel: "license" })'},
{code: 'React.createElement("a", { rel: ["license"] })'},
{code: '<a rel="next"></a>'},
{code: 'React.createElement("a", { rel: "next" })'},
{code: 'React.createElement("a", { rel: ["next"] })'},
{code: '<a rel="nofollow"></a>'},
{code: 'React.createElement("a", { rel: "nofollow" })'},
{code: 'React.createElement("a", { rel: ["nofollow"] })'},
{code: '<a rel="noopener"></a>'},
{code: 'React.createElement("a", { rel: "noopener" })'},
{code: 'React.createElement("a", { rel: ["noopener"] })'},
{code: '<a rel="noreferrer"></a>'},
{code: 'React.createElement("a", { rel: "noreferrer" })'},
{code: 'React.createElement("a", { rel: ["noreferrer"] })'},
{code: '<a rel="opener"></a>'},
{code: 'React.createElement("a", { rel: "opener" })'},
{code: 'React.createElement("a", { rel: ["opener"] })'},
{code: '<a rel="prev"></a>'},
{code: 'React.createElement("a", { rel: "prev" })'},
{code: 'React.createElement("a", { rel: ["prev"] })'},
{code: '<a rel="search"></a>'},
{code: 'React.createElement("a", { rel: "search" })'},
{code: 'React.createElement("a", { rel: ["search"] })'},
{code: '<a rel="tag"></a>'},
{code: 'React.createElement("a", { rel: "tag" })'},
{code: 'React.createElement("a", { rel: ["tag"] })'},
{code: '<area rel="alternate"></area>'},
{code: 'React.createElement("area", { rel: "alternate" })'},
{code: 'React.createElement("area", { rel: ["alternate"] })'},
{code: '<area rel="author"></area>'},
{code: 'React.createElement("area", { rel: "author" })'},
{code: 'React.createElement("area", { rel: ["author"] })'},
{code: '<area rel="bookmark"></area>'},
{code: 'React.createElement("area", { rel: "bookmark" })'},
{code: 'React.createElement("area", { rel: ["bookmark"] })'},
{code: '<area rel="external"></area>'},
{code: 'React.createElement("area", { rel: "external" })'},
{code: 'React.createElement("area", { rel: ["external"] })'},
{code: '<area rel="help"></area>'},
{code: 'React.createElement("area", { rel: "help" })'},
{code: 'React.createElement("area", { rel: ["help"] })'},
{code: '<area rel="license"></area>'},
{code: 'React.createElement("area", { rel: "license" })'},
{code: 'React.createElement("area", { rel: ["license"] })'},
{code: '<area rel="next"></area>'},
{code: 'React.createElement("area", { rel: "next" })'},
{code: 'React.createElement("area", { rel: ["next"] })'},
{code: '<area rel="nofollow"></area>'},
{code: 'React.createElement("area", { rel: "nofollow" })'},
{code: 'React.createElement("area", { rel: ["nofollow"] })'},
{code: '<area rel="noopener"></area>'},
{code: 'React.createElement("area", { rel: "noopener" })'},
{code: 'React.createElement("area", { rel: ["noopener"] })'},
{code: '<area rel="noreferrer"></area>'},
{code: 'React.createElement("area", { rel: "noreferrer" })'},
{code: 'React.createElement("area", { rel: ["noreferrer"] })'},
{code: '<area rel="opener"></area>'},
{code: 'React.createElement("area", { rel: "opener" })'},
{code: 'React.createElement("area", { rel: ["opener"] })'},
{code: '<area rel="prev"></area>'},
{code: 'React.createElement("area", { rel: "prev" })'},
{code: 'React.createElement("area", { rel: ["prev"] })'},
{code: '<area rel="search"></area>'},
{code: 'React.createElement("area", { rel: "search" })'},
{code: 'React.createElement("area", { rel: ["search"] })'},
{code: '<area rel="tag"></area>'},
{code: 'React.createElement("area", { rel: "tag" })'},
{code: 'React.createElement("area", { rel: ["tag"] })'},
{code: '<link rel="alternate"></link>'},
{code: 'React.createElement("link", { rel: "alternate" })'},
{code: 'React.createElement("link", { rel: ["alternate"] })'},
{code: '<link rel="author"></link>'},
{code: 'React.createElement("link", { rel: "author" })'},
{code: 'React.createElement("link", { rel: ["author"] })'},
{code: '<link rel="canonical"></link>'},
{code: 'React.createElement("link", { rel: "canonical" })'},
{code: 'React.createElement("link", { rel: ["canonical"] })'},
{code: '<link rel="dns-prefetch"></link>'},
{code: 'React.createElement("link", { rel: "dns-prefetch" })'},
{code: 'React.createElement("link", { rel: ["dns-prefetch"] })'},
{code: '<link rel="help"></link>'},
{code: 'React.createElement("link", { rel: "help" })'},
{code: 'React.createElement("link", { rel: ["help"] })'},
{code: '<link rel="icon"></link>'},
{code: 'React.createElement("link", { rel: "icon" })'},
{code: 'React.createElement("link", { rel: ["icon"] })'},
{code: '<link rel="license"></link>'},
{code: 'React.createElement("link", { rel: "license" })'},
{code: 'React.createElement("link", { rel: ["license"] })'},
{code: '<link rel="manifest"></link>'},
{code: 'React.createElement("link", { rel: "manifest" })'},
{code: 'React.createElement("link", { rel: ["manifest"] })'},
{code: '<link rel="modulepreload"></link>'},
{code: 'React.createElement("link", { rel: "modulepreload" })'},
{code: 'React.createElement("link", { rel: ["modulepreload"] })'},
{code: '<link rel="next"></link>'},
{code: 'React.createElement("link", { rel: "next" })'},
{code: 'React.createElement("link", { rel: ["next"] })'},
{code: '<link rel="pingback"></link>'},
{code: 'React.createElement("link", { rel: "pingback" })'},
{code: 'React.createElement("link", { rel: ["pingback"] })'},
{code: '<link rel="preconnect"></link>'},
{code: 'React.createElement("link", { rel: "preconnect" })'},
{code: 'React.createElement("link", { rel: ["preconnect"] })'},
{code: '<link rel="prefetch"></link>'},
{code: 'React.createElement("link", { rel: "prefetch" })'},
{code: 'React.createElement("link", { rel: ["prefetch"] })'},
{code: '<link rel="preload"></link>'},
{code: 'React.createElement("link", { rel: "preload" })'},
{code: 'React.createElement("link", { rel: ["preload"] })'},
{code: '<link rel="prerender"></link>'},
{code: 'React.createElement("link", { rel: "prerender" })'},
{code: 'React.createElement("link", { rel: ["prerender"] })'},
{code: '<link rel="prev"></link>'},
{code: 'React.createElement("link", { rel: "prev" })'},
{code: 'React.createElement("link", { rel: ["prev"] })'},
{code: '<link rel="search"></link>'},
{code: 'React.createElement("link", { rel: "search" })'},
{code: 'React.createElement("link", { rel: ["search"] })'},
{code: '<link rel="stylesheet"></link>'},
{code: 'React.createElement("link", { rel: "stylesheet" })'},
{code: 'React.createElement("link", { rel: ["stylesheet"] })'},
{code: '<form rel="external"></form>'},
{code: 'React.createElement("form", { rel: "external" })'},
{code: 'React.createElement("form", { rel: ["external"] })'},
{code: '<form rel="help"></form>'},
{code: 'React.createElement("form", { rel: "help" })'},
{code: 'React.createElement("form", { rel: ["help"] })'},
{code: '<form rel="license"></form>'},
{code: 'React.createElement("form", { rel: "license" })'},
{code: 'React.createElement("form", { rel: ["license"] })'},
{code: '<form rel="next"></form>'},
{code: 'React.createElement("form", { rel: "next" })'},
{code: 'React.createElement("form", { rel: ["next"] })'},
{code: '<form rel="nofollow"></form>'},
{code: 'React.createElement("form", { rel: "nofollow" })'},
{code: 'React.createElement("form", { rel: ["nofollow"] })'},
{code: '<form rel="noopener"></form>'},
{code: 'React.createElement("form", { rel: "noopener" })'},
{code: 'React.createElement("form", { rel: ["noopener"] })'},
{code: '<form rel="noreferrer"></form>'},
{code: 'React.createElement("form", { rel: "noreferrer" })'},
{code: 'React.createElement("form", { rel: ["noreferrer"] })'},
{code: '<form rel="opener"></form>'},
{code: 'React.createElement("form", { rel: "opener" })'},
{code: 'React.createElement("form", { rel: ["opener"] })'},
{code: '<form rel="prev"></form>'},
{code: 'React.createElement("form", { rel: "prev" })'},
{code: 'React.createElement("form", { rel: ["prev"] })'},
{code: '<form rel="search"></form>'},
{code: 'React.createElement("form", { rel: "search" })'},
{code: 'React.createElement("form", { rel: ["search"] })'},
{code: '<form rel={callFoo()}></form>'},
{code: 'React.createElement("form", { rel: callFoo() })'},
{code: 'React.createElement("form", { rel: [callFoo()] })'},
{code: '<a rel={{a: "noreferrer"}["a"]}></a>'},
{code: '<a rel={{a: "noreferrer"}["b"]}></a>'}
],
Expand All @@ -96,20 +210,44 @@ ruleTester.run('no-invalid-html-attribute', rule, {
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
}]
},
{
code: 'React.createElement("html", { rel: 1 })',
errors: [{
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
}]
},
{
code: '<Foo rel></Foo>',
output: '<Foo ></Foo>',
errors: [{
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
}]
},
{
code: 'React.createElement("Foo", { rel: true })',
errors: [{
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
}]
},
{
code: '<a rel></a>',
output: '<a ></a>',
errors: [{
message: 'An empty "rel" attribute is meaningless.'
}]
},
{
code: 'React.createElement("a", { rel: 1 })',
errors: [{
message: '1 is never a valid "rel" attribute value.'
}]
},
{
code: 'React.createElement("a", { rel() { return 1; } })',
errors: [{
message: 'The "rel" attribute cannot be a method.'
}]
},
{
code: '<any rel></any>',
output: '<any ></any>',
Expand Down Expand Up @@ -173,6 +311,12 @@ ruleTester.run('no-invalid-html-attribute', rule, {
message: '"foobar" is never a valid "rel" attribute value.'
}]
},
{
code: 'React.createElement("a", { rel: ["noreferrer", "noopener", "foobar" ] })',
errors: [{
message: '"foobar" is never a valid "rel" attribute value.'
}]
},
{
code: '<a rel={"foobar noreferrer noopener"}></a>',
output: '<a rel={" noreferrer noopener"}></a>',
Expand Down

0 comments on commit ce77747

Please sign in to comment.