Skip to content

Commit

Permalink
add rule for checking invalid rel values
Browse files Browse the repository at this point in the history
  • Loading branch information
Nokel81 committed Nov 30, 2020
1 parent edbbd79 commit f2ead45
Show file tree
Hide file tree
Showing 4 changed files with 669 additions and 0 deletions.
14 changes: 14 additions & 0 deletions 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.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -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'),
Expand Down
190 changes: 190 additions & 0 deletions 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 `<link>`, `<a>`, `<area>`, and `<form>` 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);
}
});
}
}
};
}
};

0 comments on commit f2ead45

Please sign in to comment.