Skip to content

Commit 77170fb

Browse files
authored
Merge pull request #2116 from gbakernet/allow_custom_components_in_jsx_no_blank
[New] add `linkComponents` setting [New] `jsx-no-target-blank`: add support for `linkComponents` setting
2 parents 16121d4 + a7cb596 commit 77170fb

File tree

9 files changed

+128
-17
lines changed

9 files changed

+128
-17
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ You should also specify settings that will be shared across all the plugin rules
4848
"forbidExtraProps",
4949
{"property": "freeze", "object": "Object"},
5050
{"property": "myFavoriteWrapper"}
51+
],
52+
"linkComponents": [
53+
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
54+
"Hyperlink",
55+
{"name": "Link", "linkAttribute": "to"}
5156
]
5257
}
5358
}

docs/rules/jsx-no-target-blank.md

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,59 @@ This rule aims to prevent user generated links from creating security vulnerabil
2121
* enabled: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
2222
* enforce: optional string, 'always' or 'never'
2323

24-
### always (default)
24+
### `enforceDynamicLinks`
25+
26+
#### always
27+
2528
`{"enforceDynamicLinks": "always"}` enforces the rule if the href is a dynamic link (default)
2629

2730
When {"enforceDynamicLinks": "always"} is set, the following patterns are considered errors:
2831

2932
```jsx
3033
var Hello = <a target='_blank' href="http://example.com/"></a>
31-
var Hello = <a target='_blank' href={ dynamicLink }></a>
34+
var Hello = <a target='_blank' href={dynamicLink}></a>
3235
```
3336

3437
The following patterns are **not** considered errors:
3538

3639
```jsx
37-
var Hello = <p target='_blank'></p>
38-
var Hello = <a target='_blank' rel='noopener noreferrer' href="http://example.com"></a>
39-
var Hello = <a target='_blank' href="relative/path/in/the/host"></a>
40-
var Hello = <a target='_blank' href="/absolute/path/in/the/host"></a>
40+
var Hello = <p target="_blank"></p>
41+
var Hello = <a target="_blank" rel="noopener noreferrer" href="http://example.com"></a>
42+
var Hello = <a target="_blank" href="relative/path/in/the/host"></a>
43+
var Hello = <a target="_blank" href="/absolute/path/in/the/host"></a>
4144
var Hello = <a></a>
4245
```
4346

44-
### never
47+
#### never
4548

4649
`{"enforceDynamicLinks": "never"}` does not enforce the rule if the href is a dynamic link
4750

4851
When {"enforceDynamicLinks": "never"} is set, the following patterns are **not** considered errors:
4952

5053
```jsx
51-
var Hello = <a target='_blank' href={ dynamicLink }></a>
54+
var Hello = <a target='_blank' href={dynamicLink}></a>
55+
```
56+
57+
### Custom link components
58+
59+
This rule supports the ability to use custom components for links, such as `<Link />` which is popular in libraries like `react-router`, `next.js` and `gatsby`. To enable this, define your custom link components in the global [shared settings](https://github.com/yannickcr/eslint-plugin-react/blob/master/README.md#configuration) under the `linkComponents` configuration area. Once configured, this rule will check those components as if they were `<a />` elements.
60+
61+
The following patterns are considered errors:
62+
63+
```jsx
64+
var Hello = <Link target="_blank" to="http://example.com/"></Link>
65+
var Hello = <Link target="_blank" to={dynamicLink}></Link>
66+
```
67+
68+
The following patterns are **not** considered errors:
69+
70+
```jsx
71+
var Hello = <Link target="_blank" rel="noopener noreferrer" to="http://example.com"></Link>
72+
var Hello = <Link target="_blank" to="relative/path/in/the/host"></Link>
73+
var Hello = <Link target="_blank" to="/absolute/path/in/the/host"></Link>
74+
var Hello = <Link />
5275
```
5376

5477
## When Not To Use It
5578

56-
If you do not have any external links, you can disable this rule
79+
If you do not have any external links, you can disable this rule

lib/rules/jsx-no-target-blank.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
'use strict';
66

77
const docsUrl = require('../util/docsUrl');
8+
const linkComponentsUtil = require('../util/linkComponents');
89

910
// ------------------------------------------------------------------------------
1011
// Rule Definition
@@ -18,16 +19,16 @@ function isTargetBlank(attr) {
1819
attr.value.value.toLowerCase() === '_blank';
1920
}
2021

21-
function hasExternalLink(element) {
22+
function hasExternalLink(element, linkAttribute) {
2223
return element.attributes.some(attr => attr.name &&
23-
attr.name.name === 'href' &&
24+
attr.name.name === linkAttribute &&
2425
attr.value.type === 'Literal' &&
2526
/^(?:\w+:|\/\/)/.test(attr.value.value));
2627
}
2728

28-
function hasDynamicLink(element) {
29+
function hasDynamicLink(element, linkAttribute) {
2930
return element.attributes.some(attr => attr.name &&
30-
attr.name.name === 'href' &&
31+
attr.name.name === linkAttribute &&
3132
attr.value.type === 'JSXExpressionContainer');
3233
}
3334

@@ -63,14 +64,17 @@ module.exports = {
6364
create: function(context) {
6465
const configuration = context.options[0] || {};
6566
const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always';
67+
const components = linkComponentsUtil.getLinkComponents(context);
6668

6769
return {
6870
JSXAttribute: function(node) {
69-
if (node.parent.name.name !== 'a' || !isTargetBlank(node) || hasSecureRel(node.parent)) {
71+
if (!components.has(node.parent.name.name) || !isTargetBlank(node) || hasSecureRel(node.parent)) {
7072
return;
7173
}
7274

73-
if (hasExternalLink(node.parent) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent))) {
75+
const linkAttribute = components.get(node.parent.name.name);
76+
77+
if (hasExternalLink(node.parent, linkAttribute) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent, linkAttribute))) {
7478
context.report(node, 'Using target="_blank" without rel="noopener noreferrer" ' +
7579
'is a security risk: see https://mathiasbynens.github.io/rel-noopener');
7680
}

lib/util/linkComponents.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @fileoverview Utility functions for propWrapperFunctions setting
3+
*/
4+
'use strict';
5+
6+
const DEFAULT_LINK_COMPONENTS = ['a'];
7+
const DEFAULT_LINK_ATTRIBUTE = 'href';
8+
9+
function getLinkComponents(context) {
10+
const settings = context.settings || {};
11+
return new Map(DEFAULT_LINK_COMPONENTS.concat(settings.linkComponents || []).map(value => {
12+
if (typeof value === 'string') {
13+
return [value, DEFAULT_LINK_ATTRIBUTE];
14+
}
15+
return [value.name, value.linkAttribute];
16+
}));
17+
}
18+
19+
module.exports = {
20+
getLinkComponents: getLinkComponents
21+
};

tests/lib/rules/jsx-no-target-blank.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ ruleTester.run('jsx-no-target-blank', rule, {
4747
{
4848
code: '<a target="_blank" href={ dynamicLink }></a>',
4949
options: [{enforceDynamicLinks: 'never'}]
50+
},
51+
{
52+
code: '<Link target="_blank" href={ dynamicLink }></Link>',
53+
options: [{enforceDynamicLinks: 'never'}],
54+
settings: {linkComponents: ['Link']}
55+
},
56+
{
57+
code: '<Link target="_blank" to={ dynamicLink }></Link>',
58+
options: [{enforceDynamicLinks: 'never'}],
59+
settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}}
5060
}
5161
],
5262
invalid: [{
@@ -83,5 +93,15 @@ ruleTester.run('jsx-no-target-blank', rule, {
8393
code: '<a target="_blank" href={ dynamicLink }></a>',
8494
options: [{enforceDynamicLinks: 'always'}],
8595
errors: defaultErrors
96+
}, {
97+
code: '<Link target="_blank" href={ dynamicLink }></Link>',
98+
options: [{enforceDynamicLinks: 'always'}],
99+
settings: {linkComponents: ['Link']},
100+
errors: defaultErrors
101+
}, {
102+
code: '<Link target="_blank" to={ dynamicLink }></Link>',
103+
options: [{enforceDynamicLinks: 'always'}],
104+
settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}},
105+
errors: defaultErrors
86106
}]
87107
});

tests/util/.eslintrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"env": {
3+
"mocha": true
4+
}
5+
}

tests/util/linkComponents.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const linkComponentsUtil = require('../../lib/util/linkComponents');
5+
6+
describe('linkComponentsFunctions', () => {
7+
describe('getLinkComponents', () => {
8+
it('returns a default map of components', () => {
9+
const context = {};
10+
assert.deepStrictEqual(linkComponentsUtil.getLinkComponents(context), new Map([
11+
['a', 'href']
12+
]));
13+
});
14+
15+
it('returns a map of components', () => {
16+
const linkComponents = [
17+
'Hyperlink',
18+
{
19+
name: 'Link',
20+
linkAttribute: 'to'
21+
}
22+
];
23+
const context = {
24+
settings: {
25+
linkComponents: linkComponents
26+
}
27+
};
28+
assert.deepStrictEqual(linkComponentsUtil.getLinkComponents(context), new Map([
29+
['a', 'href'],
30+
['Hyperlink', 'href'],
31+
['Link', 'to']
32+
]));
33+
});
34+
});
35+
});

tests/util/propWrapper.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-env mocha */
21
'use strict';
32

43
const assert = require('assert');

tests/util/version.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-env mocha */
21
'use strict';
32

43
const path = require('path');

0 commit comments

Comments
 (0)