Skip to content

Commit b2e744d

Browse files
burtekljharb
authored andcommitted
[New] linkAttribute setting, jsx-no-target-blank: support multiple properties
1 parent 3730edb commit b2e744d

File tree

5 files changed

+90
-20
lines changed

5 files changed

+90
-20
lines changed

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@ You should also specify settings that will be shared across all the plugin rules
6363
"formComponents": [
6464
// Components used as alternatives to <form> for forms, eg. <Form endpoint={ url } />
6565
"CustomForm",
66-
{"name": "Form", "formAttribute": "endpoint"}
66+
{"name": "SimpleForm", "formAttribute": "endpoint"},
67+
{"name": "Form", "formAttribute": ["registerEndpoint", "loginEndpoint"]}, // allows specifying multiple properties if necessary
6768
],
6869
"linkComponents": [
6970
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
7071
"Hyperlink",
71-
{"name": "Link", "linkAttribute": "to"}
72+
{"name": "MyLink", "linkAttribute": "to"},
73+
{"name": "Link", "linkAttribute": ["to", "href"]}, // allows specifying multiple properties if necessary
7274
]
7375
}
7476
}

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

+11-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
'use strict';
77

8+
const includes = require('array-includes');
89
const docsUrl = require('../util/docsUrl');
910
const linkComponentsUtil = require('../util/linkComponents');
1011
const report = require('../util/report');
@@ -48,16 +49,16 @@ function attributeValuePossiblyBlank(attribute) {
4849
return false;
4950
}
5051

51-
function hasExternalLink(node, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex) {
52-
const linkIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === linkAttribute);
52+
function hasExternalLink(node, linkAttributes, warnOnSpreadAttributes, spreadAttributeIndex) {
53+
const linkIndex = findLastIndex(node.attributes, (attr) => attr.name && includes(linkAttributes, attr.name.name));
5354
const foundExternalLink = linkIndex !== -1 && ((attr) => attr.value && attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))(
5455
node.attributes[linkIndex]);
5556
return foundExternalLink || (warnOnSpreadAttributes && linkIndex < spreadAttributeIndex);
5657
}
5758

58-
function hasDynamicLink(node, linkAttribute) {
59+
function hasDynamicLink(node, linkAttributes) {
5960
const dynamicLinkIndex = findLastIndex(node.attributes, (attr) => attr.name
60-
&& attr.name.name === linkAttribute
61+
&& includes(linkAttributes, attr.name.name)
6162
&& attr.value
6263
&& attr.value.type === 'JSXExpressionContainer');
6364
if (dynamicLinkIndex !== -1) {
@@ -194,9 +195,9 @@ module.exports = {
194195
}
195196
}
196197

197-
const linkAttribute = linkComponents.get(node.name.name);
198-
const hasDangerousLink = hasExternalLink(node, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex)
199-
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttribute));
198+
const linkAttributes = linkComponents.get(node.name.name);
199+
const hasDangerousLink = hasExternalLink(node, linkAttributes, warnOnSpreadAttributes, spreadAttributeIndex)
200+
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttributes));
200201
if (hasDangerousLink && !hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex)) {
201202
const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer';
202203
const relValue = allowReferrer ? 'noopener' : 'noreferrer';
@@ -265,11 +266,11 @@ module.exports = {
265266
return;
266267
}
267268

268-
const formAttribute = formComponents.get(node.name.name);
269+
const formAttributes = formComponents.get(node.name.name);
269270

270271
if (
271-
hasExternalLink(node, formAttribute)
272-
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, formAttribute))
272+
hasExternalLink(node, formAttributes)
273+
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, formAttributes))
273274
) {
274275
const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer';
275276
report(context, messages[messageId], messageId, {

lib/util/linkComponents.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ function getFormComponents(context) {
2424
);
2525
return new Map(map(iterFrom(formComponents), (value) => {
2626
if (typeof value === 'string') {
27-
return [value, DEFAULT_FORM_ATTRIBUTE];
27+
return [value, [DEFAULT_FORM_ATTRIBUTE]];
2828
}
29-
return [value.name, value.formAttribute];
29+
return [value.name, [].concat(value.formAttribute)];
3030
}));
3131
}
3232

@@ -37,9 +37,9 @@ function getLinkComponents(context) {
3737
);
3838
return new Map(map(iterFrom(linkComponents), (value) => {
3939
if (typeof value === 'string') {
40-
return [value, DEFAULT_LINK_ATTRIBUTE];
40+
return [value, [DEFAULT_LINK_ATTRIBUTE]];
4141
}
42-
return [value.name, value.linkAttribute];
42+
return [value.name, [].concat(value.linkAttribute)];
4343
}));
4444
}
4545

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

+28
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ ruleTester.run('jsx-no-target-blank', rule, {
102102
options: [{ enforceDynamicLinks: 'never' }],
103103
settings: { linkComponents: { name: 'Link', linkAttribute: 'to' } },
104104
},
105+
{
106+
code: '<Link target="_blank" to={ dynamicLink }></Link>',
107+
options: [{ enforceDynamicLinks: 'never' }],
108+
settings: { linkComponents: { name: 'Link', linkAttribute: ['to'] } },
109+
},
105110
{
106111
code: '<a href="foobar" target="_blank" rel="noopener"></a>',
107112
options: [{ allowReferrer: true }],
@@ -167,6 +172,14 @@ ruleTester.run('jsx-no-target-blank', rule, {
167172
{
168173
code: '<a href={href} target={isExternal ? "_blank" : undefined} rel={isExternal ? "noopener noreferrer" : undefined} />',
169174
},
175+
{
176+
code: '<form action={action} />',
177+
options: [{ forms: true }],
178+
},
179+
{
180+
code: '<form action={action} {...spread} />',
181+
options: [{ forms: true }],
182+
},
170183
]),
171184
invalid: parsers.all([
172185
{
@@ -407,5 +420,20 @@ ruleTester.run('jsx-no-target-blank', rule, {
407420
options: [{ allowReferrer: true }],
408421
errors: allowReferrerErrors,
409422
},
423+
{
424+
code: '<form action={action} target="_blank" />',
425+
options: [{ allowReferrer: true, forms: true }],
426+
errors: allowReferrerErrors,
427+
},
428+
{
429+
code: '<form action={action} target="_blank" />',
430+
options: [{ forms: true }],
431+
errors: defaultErrors,
432+
},
433+
{
434+
code: '<form action={action} {...spread} />',
435+
options: [{ forms: true, warnOnSpreadAttributes: true }],
436+
errors: defaultErrors,
437+
},
410438
]),
411439
});

tests/util/linkComponents.js

+43-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('linkComponentsFunctions', () => {
88
it('returns a default map of components', () => {
99
const context = {};
1010
assert.deepStrictEqual(linkComponentsUtil.getLinkComponents(context), new Map([
11-
['a', 'href'],
11+
['a', ['href']],
1212
]));
1313
});
1414

@@ -19,16 +19,55 @@ describe('linkComponentsFunctions', () => {
1919
name: 'Link',
2020
linkAttribute: 'to',
2121
},
22+
{
23+
name: 'Link2',
24+
linkAttribute: ['to1', 'to2'],
25+
},
2226
];
2327
const context = {
2428
settings: {
2529
linkComponents,
2630
},
2731
};
2832
assert.deepStrictEqual(linkComponentsUtil.getLinkComponents(context), new Map([
29-
['a', 'href'],
30-
['Hyperlink', 'href'],
31-
['Link', 'to'],
33+
['a', ['href']],
34+
['Hyperlink', ['href']],
35+
['Link', ['to']],
36+
['Link2', ['to1', 'to2']],
37+
]));
38+
});
39+
});
40+
41+
describe('getFormComponents', () => {
42+
it('returns a default map of components', () => {
43+
const context = {};
44+
assert.deepStrictEqual(linkComponentsUtil.getFormComponents(context), new Map([
45+
['form', ['action']],
46+
]));
47+
});
48+
49+
it('returns a map of components', () => {
50+
const formComponents = [
51+
'Form',
52+
{
53+
name: 'MyForm',
54+
formAttribute: 'endpoint',
55+
},
56+
{
57+
name: 'MyForm2',
58+
formAttribute: ['endpoint1', 'endpoint2'],
59+
},
60+
];
61+
const context = {
62+
settings: {
63+
formComponents,
64+
},
65+
};
66+
assert.deepStrictEqual(linkComponentsUtil.getFormComponents(context), new Map([
67+
['form', ['action']],
68+
['Form', ['action']],
69+
['MyForm', ['endpoint']],
70+
['MyForm2', ['endpoint1', 'endpoint2']],
3271
]));
3372
});
3473
});

0 commit comments

Comments
 (0)