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 Jun 22, 2021
1 parent ae78cc9 commit e33e82f
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 3 deletions.
95 changes: 95 additions & 0 deletions lib/rules/no-invalid-html-attribute.js
Expand Up @@ -185,6 +185,89 @@ 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
}

propsArg.properties.filter((prop) => (
prop.key.type !== 'Identifier' // cannot check computed keys
&& prop.key.name !== attribute // ignore not this attribute
)).forEach((prop) => {
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}`
});
} else if (prop.method) {
context.report({
node: prop,
message: `The "${attribute}" attribute cannot be a method.`
});
}

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

if (prop.value.type === 'ArrayExpression') {
prop.value.elements.forEach((value) => {
checkPropValidValue(context, node, value, attribute);
});
} else {
checkPropValidValue(context, node, prop.value, attribute);
}
});
}

module.exports = {
meta: {
fixable: 'code',
Expand Down Expand Up @@ -213,6 +296,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
139 changes: 136 additions & 3 deletions tests/lib/rules/no-invalid-html-attribute.js
Expand Up @@ -29,64 +29,180 @@ 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>'}
{code: '<a rel={{a: "noreferrer"}["b"]}></a>'},
{code: '<Foo rel></Foo>'},
{code: 'React.createElement("Foo", { rel: true })'}
],
invalid: [
{
Expand All @@ -97,8 +213,7 @@ ruleTester.run('no-invalid-html-attribute', rule, {
}]
},
{
code: '<Foo rel></Foo>',
output: '<Foo ></Foo>',
code: 'React.createElement("html", { rel: 1 })',
errors: [{
message: 'The "rel" attribute only has meaning on the tags: "<link>", "<a>", "<area>", "<form>"'
}]
Expand All @@ -110,6 +225,18 @@ ruleTester.run('no-invalid-html-attribute', rule, {
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 +300,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 e33e82f

Please sign in to comment.