Skip to content

Commit

Permalink
[New] linkAttribute setting, jsx-no-target-blank: support multipl…
Browse files Browse the repository at this point in the history
…e properties
  • Loading branch information
burtek authored and ljharb committed Jan 8, 2024
1 parent 3730edb commit de35e6f
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* [`jsx-wrap-multilines`]: add `never` option to prohibit wrapping parens on multiline JSX ([#3668][] @reedws)
* [`jsx-filename-extension`]: add `ignoreFilesWithoutCode` option to allow empty files ([#3674][] @burtek)
* [`jsx-boolean-value`]: add `assumeUndefinedIsFalse` option ([#3675][] @developer-bandi)
* `linkAttribute` setting, [`jsx-no-target-blank`]: support multiple properties ([#3673][] @burtek)

### Fixed
* [`jsx-no-leaked-render`]: preserve RHS parens for multiline jsx elements while fixing ([#3623][] @akulsr0)
Expand All @@ -32,6 +33,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange

[#3675]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3675
[#3674]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3674
[#3673]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3673
[#3668]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3668
[#3666]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3666
[#3662]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3662
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ You should also specify settings that will be shared across all the plugin rules
"formComponents": [
// Components used as alternatives to <form> for forms, eg. <Form endpoint={ url } />
"CustomForm",
{"name": "Form", "formAttribute": "endpoint"}
{"name": "SimpleForm", "formAttribute": "endpoint"},
{"name": "Form", "formAttribute": ["registerEndpoint", "loginEndpoint"]}, // allows specifying multiple properties if necessary
],
"linkComponents": [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
"Hyperlink",
{"name": "Link", "linkAttribute": "to"}
{"name": "MyLink", "linkAttribute": "to"},
{"name": "Link", "linkAttribute": ["to", "href"]}, // allows specifying multiple properties if necessary
]
}
}
Expand Down
21 changes: 11 additions & 10 deletions lib/rules/jsx-no-target-blank.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

'use strict';

const includes = require('array-includes');
const docsUrl = require('../util/docsUrl');
const linkComponentsUtil = require('../util/linkComponents');
const report = require('../util/report');
Expand Down Expand Up @@ -48,16 +49,16 @@ function attributeValuePossiblyBlank(attribute) {
return false;
}

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

function hasDynamicLink(node, linkAttribute) {
function hasDynamicLink(node, linkAttributes) {
const dynamicLinkIndex = findLastIndex(node.attributes, (attr) => attr.name
&& attr.name.name === linkAttribute
&& includes(linkAttributes, attr.name.name)
&& attr.value
&& attr.value.type === 'JSXExpressionContainer');
if (dynamicLinkIndex !== -1) {
Expand Down Expand Up @@ -194,9 +195,9 @@ module.exports = {
}
}

const linkAttribute = linkComponents.get(node.name.name);
const hasDangerousLink = hasExternalLink(node, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex)
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttribute));
const linkAttributes = linkComponents.get(node.name.name);
const hasDangerousLink = hasExternalLink(node, linkAttributes, warnOnSpreadAttributes, spreadAttributeIndex)
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttributes));
if (hasDangerousLink && !hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex)) {
const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer';
const relValue = allowReferrer ? 'noopener' : 'noreferrer';
Expand Down Expand Up @@ -265,11 +266,11 @@ module.exports = {
return;
}

const formAttribute = formComponents.get(node.name.name);
const formAttributes = formComponents.get(node.name.name);

if (
hasExternalLink(node, formAttribute)
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, formAttribute))
hasExternalLink(node, formAttributes)
|| (enforceDynamicLinks === 'always' && hasDynamicLink(node, formAttributes))
) {
const messageId = allowReferrer ? 'noTargetBlankWithoutNoopener' : 'noTargetBlankWithoutNoreferrer';
report(context, messages[messageId], messageId, {
Expand Down
8 changes: 4 additions & 4 deletions lib/util/linkComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ function getFormComponents(context) {
);
return new Map(map(iterFrom(formComponents), (value) => {
if (typeof value === 'string') {
return [value, DEFAULT_FORM_ATTRIBUTE];
return [value, [DEFAULT_FORM_ATTRIBUTE]];
}
return [value.name, value.formAttribute];
return [value.name, [].concat(value.formAttribute)];
}));
}

Expand All @@ -37,9 +37,9 @@ function getLinkComponents(context) {
);
return new Map(map(iterFrom(linkComponents), (value) => {
if (typeof value === 'string') {
return [value, DEFAULT_LINK_ATTRIBUTE];
return [value, [DEFAULT_LINK_ATTRIBUTE]];
}
return [value.name, value.linkAttribute];
return [value.name, [].concat(value.linkAttribute)];
}));
}

Expand Down
28 changes: 28 additions & 0 deletions tests/lib/rules/jsx-no-target-blank.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ ruleTester.run('jsx-no-target-blank', rule, {
options: [{ enforceDynamicLinks: 'never' }],
settings: { linkComponents: { name: 'Link', linkAttribute: 'to' } },
},
{
code: '<Link target="_blank" to={ dynamicLink }></Link>',
options: [{ enforceDynamicLinks: 'never' }],
settings: { linkComponents: { name: 'Link', linkAttribute: ['to'] } },
},
{
code: '<a href="foobar" target="_blank" rel="noopener"></a>',
options: [{ allowReferrer: true }],
Expand Down Expand Up @@ -167,6 +172,14 @@ ruleTester.run('jsx-no-target-blank', rule, {
{
code: '<a href={href} target={isExternal ? "_blank" : undefined} rel={isExternal ? "noopener noreferrer" : undefined} />',
},
{
code: '<form action={action} />',
options: [{ forms: true }],
},
{
code: '<form action={action} {...spread} />',
options: [{ forms: true }],
},
]),
invalid: parsers.all([
{
Expand Down Expand Up @@ -407,5 +420,20 @@ ruleTester.run('jsx-no-target-blank', rule, {
options: [{ allowReferrer: true }],
errors: allowReferrerErrors,
},
{
code: '<form action={action} target="_blank" />',
options: [{ allowReferrer: true, forms: true }],
errors: allowReferrerErrors,
},
{
code: '<form action={action} target="_blank" />',
options: [{ forms: true }],
errors: defaultErrors,
},
{
code: '<form action={action} {...spread} />',
options: [{ forms: true, warnOnSpreadAttributes: true }],
errors: defaultErrors,
},
]),
});
47 changes: 43 additions & 4 deletions tests/util/linkComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('linkComponentsFunctions', () => {
it('returns a default map of components', () => {
const context = {};
assert.deepStrictEqual(linkComponentsUtil.getLinkComponents(context), new Map([
['a', 'href'],
['a', ['href']],
]));
});

Expand All @@ -19,16 +19,55 @@ describe('linkComponentsFunctions', () => {
name: 'Link',
linkAttribute: 'to',
},
{
name: 'Link2',
linkAttribute: ['to1', 'to2'],
},
];
const context = {
settings: {
linkComponents,
},
};
assert.deepStrictEqual(linkComponentsUtil.getLinkComponents(context), new Map([
['a', 'href'],
['Hyperlink', 'href'],
['Link', 'to'],
['a', ['href']],
['Hyperlink', ['href']],
['Link', ['to']],
['Link2', ['to1', 'to2']],
]));
});
});

describe('getFormComponents', () => {
it('returns a default map of components', () => {
const context = {};
assert.deepStrictEqual(linkComponentsUtil.getFormComponents(context), new Map([
['form', ['action']],
]));
});

it('returns a map of components', () => {
const formComponents = [
'Form',
{
name: 'MyForm',
formAttribute: 'endpoint',
},
{
name: 'MyForm2',
formAttribute: ['endpoint1', 'endpoint2'],
},
];
const context = {
settings: {
formComponents,
},
};
assert.deepStrictEqual(linkComponentsUtil.getFormComponents(context), new Map([
['form', ['action']],
['Form', ['action']],
['MyForm', ['endpoint']],
['MyForm2', ['endpoint1', 'endpoint2']],
]));
});
});
Expand Down

0 comments on commit de35e6f

Please sign in to comment.