Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
669 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
}); | ||
} | ||
} | ||
}; | ||
} | ||
}; |
Oops, something went wrong.