From f2ead45239788815189ee213911ba0635c119d91 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 30 Nov 2020 08:20:29 -0500 Subject: [PATCH] add rule for checking invalid rel values --- docs/rules/jsx-no-invalid-rel.md | 14 + index.js | 1 + lib/rules/jsx-no-invalid-rel.js | 190 +++++++++++ tests/lib/rules/jsx-no-invalid-rel.js | 464 ++++++++++++++++++++++++++ 4 files changed, 669 insertions(+) create mode 100644 docs/rules/jsx-no-invalid-rel.md create mode 100644 lib/rules/jsx-no-invalid-rel.js create mode 100644 tests/lib/rules/jsx-no-invalid-rel.js diff --git a/docs/rules/jsx-no-invalid-rel.md b/docs/rules/jsx-no-invalid-rel.md new file mode 100644 index 0000000000..7bdf973fa6 --- /dev/null +++ b/docs/rules/jsx-no-invalid-rel.md @@ -0,0 +1,14 @@ +# Prevent usage of invalid `rel` (react/jsx-no-invalid-rel) + +The JSX elements: `a`, `area`, `link`, or `form` all have a attribute called `rel`. There is is fixed list of values that have any meaning on these tags (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel)). To help with minimizing confusion while reading code, only the appropriate values should be on each attribute. + +## Rule Details + +This rule aims to remove invalid `rel` attribute values. + +## Rule Options +There are no options. + +## When Not To Use It + +When you don't want to enforce `rel` value correctness. diff --git a/index.js b/index.js index 8edb1177f1..bf4c2443c0 100644 --- a/index.js +++ b/index.js @@ -54,6 +54,7 @@ const allRules = { 'jsx-uses-react': require('./lib/rules/jsx-uses-react'), 'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'), 'jsx-wrap-multilines': require('./lib/rules/jsx-wrap-multilines'), + 'jsx-no-invalid-rel': require('./lib/rules/jsx-no-invalid-rel'), 'no-access-state-in-setstate': require('./lib/rules/no-access-state-in-setstate'), 'no-adjacent-inline-elements': require('./lib/rules/no-adjacent-inline-elements'), 'no-array-index-key': require('./lib/rules/no-array-index-key'), diff --git a/lib/rules/jsx-no-invalid-rel.js b/lib/rules/jsx-no-invalid-rel.js new file mode 100644 index 0000000000..7c203c2228 --- /dev/null +++ b/lib/rules/jsx-no-invalid-rel.js @@ -0,0 +1,190 @@ +/** + * @fileoverview Forbid rel attribute to have non-valid value + * @author Sebastian Malton + */ + +'use strict'; + +const docsUrl = require('../util/docsUrl'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const standardValues = new Map(Object.entries({ + alternate: new Set(['link', 'area', 'a']), + author: new Set(['link', 'area', 'a']), + bookmark: new Set(['area', 'a']), + canonical: new Set(['link']), + 'dns-prefetch': new Set(['link']), + external: new Set(['area', 'a', 'form']), + help: new Set(['link', 'area', 'a', 'form']), + icon: new Set(['link']), + license: new Set(['link', 'area', 'a', 'form']), + manifest: new Set(['link']), + modulepreload: new Set(['link']), + next: new Set(['link', 'area', 'a', 'form']), + nofollow: new Set(['area', 'a', 'form']), + noopener: new Set(['area', 'a', 'form']), + noreferrer: new Set(['area', 'a', 'form']), + opener: new Set(['area', 'a', 'form']), + pingback: new Set(['link']), + preconnect: new Set(['link']), + prefetch: new Set(['link']), + preload: new Set(['link']), + prerender: new Set(['link']), + prev: new Set(['link', 'area', 'a', 'form']), + search: new Set(['link', 'area', 'a', 'form']), + stylesheet: new Set(['link']), + tag: new Set(['area', 'a']) +})); +const components = new Set(['link', 'a', 'area', 'form']); + +function splitIntoRangedParts(node) { + const res = []; + const regex = /\s*([^\s]+)/g; + const valueRangeStart = node.range[0] + 1; // the plus one is for the initial quote + let match; + + // eslint-disable-next-line no-cond-assign + while ((match = regex.exec(node.value)) !== null) { + const start = match.index + valueRangeStart; + const end = start + match[0].length; + res.push({ + reportingValue: `"${match[1]}"`, + value: match[1], + range: [start, end] + }); + } + + return res; +} + +function checkLiteralValueNode(context, node, parentNodeName) { + if (typeof node.value !== 'string') { + return context.report({ + node, + messageId: 'onlyStrings', + fix(fixer) { + return fixer.remove(node.parent.parent); + } + }); + } + + if (!node.value.trim()) { + return context.report({ + node, + messageId: 'emptyRel', + fix(fixer) { + return fixer.remove(node); + } + }); + } + + const parts = splitIntoRangedParts(node); + for (const part of parts) { + const allowedTags = standardValues.get(part.value); + if (!allowedTags) { + context.report({ + node, + messageId: 'realRelValues', + data: { + value: part.reportingValue + }, + fix(fixer) { + return fixer.removeRange(part.range); + } + }); + } else if (!allowedTags.has(parentNodeName)) { + context.report({ + node, + messageId: 'matchingRelValues', + data: { + value: part.reportingValue, + tag: parentNodeName + }, + fix(fixer) { + return fixer.removeRange(part.range); + } + }); + } + } +} + +module.exports = { + meta: { + fixable: 'code', + docs: { + description: 'Forbid `rel` attribute with an invalid value`', + category: 'Possible Errors', + url: docsUrl('jsx-no-invalid-rel') + }, + messages: { + relOnlyOnSpecific: 'The "rel" attribute only has meaning on ``, ``, ``, and `
` tags.', + emptyRel: 'An empty "rel" attribute is meaningless.', + onlyStrings: '"rel" attribute only supports strings', + realRelValues: '{{ value }} is never a valid "rel" attribute value.', + matchingRelValues: '"{{ value }}" is not a valid "rel" attribute value for <{{ tag }}>.' + } + }, + + create(context) { + return { + JSXAttribute(node) { + // ignore attributes that aren't "rel" + if (node.type !== 'JSXIdentifier' && node.name.name !== 'rel') { + return; + } + + const parentNodeName = node.parent.name.name; + if (!components.has(parentNodeName)) { + return context.report({ + node, + messageId: 'relOnlyOnSpecific', + fix(fixer) { + return fixer.remove(node); + } + }); + } + + if (!node.value) { + return context.report({ + node, + messageId: 'emptyRel', + fix(fixer) { + return fixer.remove(node); + } + }); + } + + if (node.value.type === 'Literal') { + return checkLiteralValueNode(context, node.value, parentNodeName); + } + + if (node.value.expression.type === 'Literal') { + return checkLiteralValueNode(context, node.value.expression, parentNodeName); + } + + if (node.value.expression.type === 'ObjectExpression') { + return context.report({ + node, + messageId: 'onlyStrings', + fix(fixer) { + return fixer.remove(node); + } + }); + } + + if (node.value.expression.type === 'Identifier' && node.value.expression.name === 'undefined') { + return context.report({ + node, + messageId: 'onlyStrings', + fix(fixer) { + return fixer.remove(node); + } + }); + } + } + }; + } +}; diff --git a/tests/lib/rules/jsx-no-invalid-rel.js b/tests/lib/rules/jsx-no-invalid-rel.js new file mode 100644 index 0000000000..accf326bb5 --- /dev/null +++ b/tests/lib/rules/jsx-no-invalid-rel.js @@ -0,0 +1,464 @@ +/** + * @fileoverview Forbid target='_blank' attribute + * @author Kevin Miller + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/jsx-no-invalid-rel'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } +}; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions}); + +ruleTester.run('jsx-no-invalid-rel', rule, { + valid: [ + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: ''}, + {code: '
'}, + {code: '
'}, + {code: '
'}, + {code: ''}, + {code: '
'}, + {code: '
'}, + {code: '
'}, + {code: '
'}, + {code: ''}, + {code: ''}, + {code: '
'}, + {code: ''}, + {code: ''} + ], + invalid: [ + { + code: '', + output: '', + errors: [{ + messageId: 'relOnlyOnSpecific' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'onlyStrings' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'onlyStrings' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'onlyStrings' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'onlyStrings' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'onlyStrings' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'realRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'realRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'realRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '
', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + }, + { + code: '', + output: '
', + errors: [{ + messageId: 'matchingRelValues' + }] + } + ] +});