diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd7d07507..d82eb0cc13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) @@ -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 diff --git a/README.md b/README.md index 0cda1109c3..9c67ff12b5 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,14 @@ You should also specify settings that will be shared across all the plugin rules "formComponents": [ // Components used as alternatives to
for forms, eg. "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 for linking, eg. "Hyperlink", - {"name": "Link", "linkAttribute": "to"} + {"name": "MyLink", "linkAttribute": "to"}, + {"name": "Link", "linkAttribute": ["to", "href"]}, // allows specifying multiple properties if necessary ] } } diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js index 9b77e37f4c..795de8a70f 100644 --- a/lib/rules/jsx-no-target-blank.js +++ b/lib/rules/jsx-no-target-blank.js @@ -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'); @@ -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) { @@ -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'; @@ -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, { diff --git a/lib/util/linkComponents.js b/lib/util/linkComponents.js index 46d9e0e27c..181ed377b8 100644 --- a/lib/util/linkComponents.js +++ b/lib/util/linkComponents.js @@ -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)]; })); } @@ -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)]; })); } diff --git a/tests/lib/rules/jsx-no-target-blank.js b/tests/lib/rules/jsx-no-target-blank.js index f5ab6b8793..591a1a42ee 100644 --- a/tests/lib/rules/jsx-no-target-blank.js +++ b/tests/lib/rules/jsx-no-target-blank.js @@ -102,6 +102,11 @@ ruleTester.run('jsx-no-target-blank', rule, { options: [{ enforceDynamicLinks: 'never' }], settings: { linkComponents: { name: 'Link', linkAttribute: 'to' } }, }, + { + code: '', + options: [{ enforceDynamicLinks: 'never' }], + settings: { linkComponents: { name: 'Link', linkAttribute: ['to'] } }, + }, { code: '', options: [{ allowReferrer: true }], @@ -167,6 +172,14 @@ ruleTester.run('jsx-no-target-blank', rule, { { code: '', }, + { + code: '', + options: [{ forms: true }], + }, + { + code: '', + options: [{ forms: true }], + }, ]), invalid: parsers.all([ { @@ -407,5 +420,20 @@ ruleTester.run('jsx-no-target-blank', rule, { options: [{ allowReferrer: true }], errors: allowReferrerErrors, }, + { + code: '', + options: [{ allowReferrer: true, forms: true }], + errors: allowReferrerErrors, + }, + { + code: '', + options: [{ forms: true }], + errors: defaultErrors, + }, + { + code: '', + options: [{ forms: true, warnOnSpreadAttributes: true }], + errors: defaultErrors, + }, ]), }); diff --git a/tests/util/linkComponents.js b/tests/util/linkComponents.js index 09394def48..741df2610e 100644 --- a/tests/util/linkComponents.js +++ b/tests/util/linkComponents.js @@ -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']], ])); }); @@ -19,6 +19,10 @@ describe('linkComponentsFunctions', () => { name: 'Link', linkAttribute: 'to', }, + { + name: 'Link2', + linkAttribute: ['to1', 'to2'], + }, ]; const context = { settings: { @@ -26,9 +30,44 @@ describe('linkComponentsFunctions', () => { }, }; 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']], ])); }); });