From e33e82fd9a7beb9a6c5bb11aa9379a17ced8586a Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 23 Mar 2021 10:21:36 -0400 Subject: [PATCH] add checking createElement --- lib/rules/no-invalid-html-attribute.js | 95 +++++++++++++ tests/lib/rules/no-invalid-html-attribute.js | 139 ++++++++++++++++++- 2 files changed, 231 insertions(+), 3 deletions(-) diff --git a/lib/rules/no-invalid-html-attribute.js b/lib/rules/no-invalid-html-attribute.js index 4c1712f952..b36574e703 100644 --- a/lib/rules/no-invalid-html-attribute.js +++ b/lib/rules/no-invalid-html-attribute.js @@ -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', @@ -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); + } } }; } diff --git a/tests/lib/rules/no-invalid-html-attribute.js b/tests/lib/rules/no-invalid-html-attribute.js index a808d13ac6..8d4cf7491c 100644 --- a/tests/lib/rules/no-invalid-html-attribute.js +++ b/tests/lib/rules/no-invalid-html-attribute.js @@ -29,64 +29,180 @@ const ruleTester = new RuleTester({parserOptions}); ruleTester.run('no-invalid-html-attribute', rule, { valid: [ {code: ''}, + {code: 'React.createElement("a", { rel: "alternate" })'}, + {code: 'React.createElement("a", { rel: ["alternate"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "author" })'}, + {code: 'React.createElement("a", { rel: ["author"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "bookmark" })'}, + {code: 'React.createElement("a", { rel: ["bookmark"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "external" })'}, + {code: 'React.createElement("a", { rel: ["external"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "help" })'}, + {code: 'React.createElement("a", { rel: ["help"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "license" })'}, + {code: 'React.createElement("a", { rel: ["license"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "next" })'}, + {code: 'React.createElement("a", { rel: ["next"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "nofollow" })'}, + {code: 'React.createElement("a", { rel: ["nofollow"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "noopener" })'}, + {code: 'React.createElement("a", { rel: ["noopener"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "noreferrer" })'}, + {code: 'React.createElement("a", { rel: ["noreferrer"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "opener" })'}, + {code: 'React.createElement("a", { rel: ["opener"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "prev" })'}, + {code: 'React.createElement("a", { rel: ["prev"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "search" })'}, + {code: 'React.createElement("a", { rel: ["search"] })'}, {code: ''}, + {code: 'React.createElement("a", { rel: "tag" })'}, + {code: 'React.createElement("a", { rel: ["tag"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "alternate" })'}, + {code: 'React.createElement("area", { rel: ["alternate"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "author" })'}, + {code: 'React.createElement("area", { rel: ["author"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "bookmark" })'}, + {code: 'React.createElement("area", { rel: ["bookmark"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "external" })'}, + {code: 'React.createElement("area", { rel: ["external"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "help" })'}, + {code: 'React.createElement("area", { rel: ["help"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "license" })'}, + {code: 'React.createElement("area", { rel: ["license"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "next" })'}, + {code: 'React.createElement("area", { rel: ["next"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "nofollow" })'}, + {code: 'React.createElement("area", { rel: ["nofollow"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "noopener" })'}, + {code: 'React.createElement("area", { rel: ["noopener"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "noreferrer" })'}, + {code: 'React.createElement("area", { rel: ["noreferrer"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "opener" })'}, + {code: 'React.createElement("area", { rel: ["opener"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "prev" })'}, + {code: 'React.createElement("area", { rel: ["prev"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "search" })'}, + {code: 'React.createElement("area", { rel: ["search"] })'}, {code: ''}, + {code: 'React.createElement("area", { rel: "tag" })'}, + {code: 'React.createElement("area", { rel: ["tag"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "alternate" })'}, + {code: 'React.createElement("link", { rel: ["alternate"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "author" })'}, + {code: 'React.createElement("link", { rel: ["author"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "canonical" })'}, + {code: 'React.createElement("link", { rel: ["canonical"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "dns-prefetch" })'}, + {code: 'React.createElement("link", { rel: ["dns-prefetch"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "help" })'}, + {code: 'React.createElement("link", { rel: ["help"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "icon" })'}, + {code: 'React.createElement("link", { rel: ["icon"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "license" })'}, + {code: 'React.createElement("link", { rel: ["license"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "manifest" })'}, + {code: 'React.createElement("link", { rel: ["manifest"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "modulepreload" })'}, + {code: 'React.createElement("link", { rel: ["modulepreload"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "next" })'}, + {code: 'React.createElement("link", { rel: ["next"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "pingback" })'}, + {code: 'React.createElement("link", { rel: ["pingback"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "preconnect" })'}, + {code: 'React.createElement("link", { rel: ["preconnect"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "prefetch" })'}, + {code: 'React.createElement("link", { rel: ["prefetch"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "preload" })'}, + {code: 'React.createElement("link", { rel: ["preload"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "prerender" })'}, + {code: 'React.createElement("link", { rel: ["prerender"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "prev" })'}, + {code: 'React.createElement("link", { rel: ["prev"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "search" })'}, + {code: 'React.createElement("link", { rel: ["search"] })'}, {code: ''}, + {code: 'React.createElement("link", { rel: "stylesheet" })'}, + {code: 'React.createElement("link", { rel: ["stylesheet"] })'}, {code: '
'}, + {code: 'React.createElement("form", { rel: "external" })'}, + {code: 'React.createElement("form", { rel: ["external"] })'}, {code: '
'}, + {code: 'React.createElement("form", { rel: "help" })'}, + {code: 'React.createElement("form", { rel: ["help"] })'}, {code: '
'}, + {code: 'React.createElement("form", { rel: "license" })'}, + {code: 'React.createElement("form", { rel: ["license"] })'}, {code: ''}, + {code: 'React.createElement("form", { rel: "next" })'}, + {code: 'React.createElement("form", { rel: ["next"] })'}, {code: '
'}, + {code: 'React.createElement("form", { rel: "nofollow" })'}, + {code: 'React.createElement("form", { rel: ["nofollow"] })'}, {code: '
'}, + {code: 'React.createElement("form", { rel: "noopener" })'}, + {code: 'React.createElement("form", { rel: ["noopener"] })'}, {code: '
'}, + {code: 'React.createElement("form", { rel: "noreferrer" })'}, + {code: 'React.createElement("form", { rel: ["noreferrer"] })'}, {code: '
'}, + {code: 'React.createElement("form", { rel: "opener" })'}, + {code: 'React.createElement("form", { rel: ["opener"] })'}, {code: ''}, + {code: 'React.createElement("form", { rel: "prev" })'}, + {code: 'React.createElement("form", { rel: ["prev"] })'}, {code: ''}, + {code: 'React.createElement("form", { rel: "search" })'}, + {code: 'React.createElement("form", { rel: ["search"] })'}, {code: '
'}, + {code: 'React.createElement("form", { rel: callFoo() })'}, + {code: 'React.createElement("form", { rel: [callFoo()] })'}, {code: ''}, - {code: ''} + {code: ''}, + {code: ''}, + {code: 'React.createElement("Foo", { rel: true })'} ], invalid: [ { @@ -97,8 +213,7 @@ ruleTester.run('no-invalid-html-attribute', rule, { }] }, { - code: '', - output: '', + code: 'React.createElement("html", { rel: 1 })', errors: [{ message: 'The "rel" attribute only has meaning on the tags: "", "", "", "
"' }] @@ -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: '', output: '', @@ -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: '', output: '',