Skip to content

Commit 12310e2

Browse files
committed
add rule for checking invalid rel values
1 parent aa43237 commit 12310e2

File tree

4 files changed

+669
-0
lines changed

4 files changed

+669
-0
lines changed

docs/rules/jsx-no-invalid-rel.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Prevent usage of invalid `rel` (react/jsx-no-invalid-rel)
2+
3+
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.
4+
5+
## Rule Details
6+
7+
This rule aims to remove invalid `rel` attribute values.
8+
9+
## Rule Options
10+
There are no options.
11+
12+
## When Not To Use It
13+
14+
When you don't want to enforce `rel` value correctness.

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const allRules = {
5454
'jsx-uses-react': require('./lib/rules/jsx-uses-react'),
5555
'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'),
5656
'jsx-wrap-multilines': require('./lib/rules/jsx-wrap-multilines'),
57+
'jsx-no-invalid-rel': require('./lib/rules/jsx-no-invalid-rel'),
5758
'no-access-state-in-setstate': require('./lib/rules/no-access-state-in-setstate'),
5859
'no-adjacent-inline-elements': require('./lib/rules/no-adjacent-inline-elements'),
5960
'no-array-index-key': require('./lib/rules/no-array-index-key'),

lib/rules/jsx-no-invalid-rel.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* @fileoverview Forbid rel attribute to have non-valid value
3+
* @author Sebastian Malton
4+
*/
5+
6+
'use strict';
7+
8+
const docsUrl = require('../util/docsUrl');
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
14+
const standardValues = new Map(Object.entries({
15+
alternate: new Set(['link', 'area', 'a']),
16+
author: new Set(['link', 'area', 'a']),
17+
bookmark: new Set(['area', 'a']),
18+
canonical: new Set(['link']),
19+
'dns-prefetch': new Set(['link']),
20+
external: new Set(['area', 'a', 'form']),
21+
help: new Set(['link', 'area', 'a', 'form']),
22+
icon: new Set(['link']),
23+
license: new Set(['link', 'area', 'a', 'form']),
24+
manifest: new Set(['link']),
25+
modulepreload: new Set(['link']),
26+
next: new Set(['link', 'area', 'a', 'form']),
27+
nofollow: new Set(['area', 'a', 'form']),
28+
noopener: new Set(['area', 'a', 'form']),
29+
noreferrer: new Set(['area', 'a', 'form']),
30+
opener: new Set(['area', 'a', 'form']),
31+
pingback: new Set(['link']),
32+
preconnect: new Set(['link']),
33+
prefetch: new Set(['link']),
34+
preload: new Set(['link']),
35+
prerender: new Set(['link']),
36+
prev: new Set(['link', 'area', 'a', 'form']),
37+
search: new Set(['link', 'area', 'a', 'form']),
38+
stylesheet: new Set(['link']),
39+
tag: new Set(['area', 'a'])
40+
}));
41+
const components = new Set(['link', 'a', 'area', 'form']);
42+
43+
function splitIntoRangedParts(node) {
44+
const res = [];
45+
const regex = /\s*([^\s]+)/g;
46+
const valueRangeStart = node.range[0] + 1; // the plus one is for the initial quote
47+
let match;
48+
49+
// eslint-disable-next-line no-cond-assign
50+
while ((match = regex.exec(node.value)) !== null) {
51+
const start = match.index + valueRangeStart;
52+
const end = start + match[0].length;
53+
res.push({
54+
reportingValue: `"${match[1]}"`,
55+
value: match[1],
56+
range: [start, end]
57+
});
58+
}
59+
60+
return res;
61+
}
62+
63+
function checkLiteralValueNode(context, node, parentNodeName) {
64+
if (typeof node.value !== 'string') {
65+
return context.report({
66+
node,
67+
messageId: 'onlyStrings',
68+
fix(fixer) {
69+
return fixer.remove(node.parent.parent);
70+
}
71+
});
72+
}
73+
74+
if (!node.value.trim()) {
75+
return context.report({
76+
node,
77+
messageId: 'emptyRel',
78+
fix(fixer) {
79+
return fixer.remove(node);
80+
}
81+
});
82+
}
83+
84+
const parts = splitIntoRangedParts(node);
85+
for (const part of parts) {
86+
const allowedTags = standardValues.get(part.value);
87+
if (!allowedTags) {
88+
context.report({
89+
node,
90+
messageId: 'realRelValues',
91+
data: {
92+
value: part.reportingValue
93+
},
94+
fix(fixer) {
95+
return fixer.removeRange(part.range);
96+
}
97+
});
98+
} else if (!allowedTags.has(parentNodeName)) {
99+
context.report({
100+
node,
101+
messageId: 'matchingRelValues',
102+
data: {
103+
value: part.reportingValue,
104+
tag: parentNodeName
105+
},
106+
fix(fixer) {
107+
return fixer.removeRange(part.range);
108+
}
109+
});
110+
}
111+
}
112+
}
113+
114+
module.exports = {
115+
meta: {
116+
fixable: 'code',
117+
docs: {
118+
description: 'Forbid `rel` attribute with an invalid value`',
119+
category: 'Possible Errors',
120+
url: docsUrl('jsx-no-invalid-rel')
121+
},
122+
messages: {
123+
relOnlyOnSpecific: 'The "rel" attribute only has meaning on `<link>`, `<a>`, `<area>`, and `<form>` tags.',
124+
emptyRel: 'An empty "rel" attribute is meaningless.',
125+
onlyStrings: '"rel" attribute only supports strings',
126+
realRelValues: '{{ value }} is never a valid "rel" attribute value.',
127+
matchingRelValues: '"{{ value }}" is not a valid "rel" attribute value for <{{ tag }}>.'
128+
}
129+
},
130+
131+
create(context) {
132+
return {
133+
JSXAttribute(node) {
134+
// ignore attributes that aren't "rel"
135+
if (node.type !== 'JSXIdentifier' && node.name.name !== 'rel') {
136+
return;
137+
}
138+
139+
const parentNodeName = node.parent.name.name;
140+
if (!components.has(parentNodeName)) {
141+
return context.report({
142+
node,
143+
messageId: 'relOnlyOnSpecific',
144+
fix(fixer) {
145+
return fixer.remove(node);
146+
}
147+
});
148+
}
149+
150+
if (!node.value) {
151+
return context.report({
152+
node,
153+
messageId: 'emptyRel',
154+
fix(fixer) {
155+
return fixer.remove(node);
156+
}
157+
});
158+
}
159+
160+
if (node.value.type === 'Literal') {
161+
return checkLiteralValueNode(context, node.value, parentNodeName);
162+
}
163+
164+
if (node.value.expression.type === 'Literal') {
165+
return checkLiteralValueNode(context, node.value.expression, parentNodeName);
166+
}
167+
168+
if (node.value.expression.type === 'ObjectExpression') {
169+
return context.report({
170+
node,
171+
messageId: 'onlyStrings',
172+
fix(fixer) {
173+
return fixer.remove(node);
174+
}
175+
});
176+
}
177+
178+
if (node.value.expression.type === 'Identifier' && node.value.expression.name === 'undefined') {
179+
return context.report({
180+
node,
181+
messageId: 'onlyStrings',
182+
fix(fixer) {
183+
return fixer.remove(node);
184+
}
185+
});
186+
}
187+
}
188+
};
189+
}
190+
};

0 commit comments

Comments
 (0)