From 893fee3ab2f882fab499fe6d76d24539e4e2e9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Aaberg?= Date: Tue, 10 Aug 2021 11:22:19 +0200 Subject: [PATCH] [New] `jsx-no-target-blank`: add `forms` option --- README.md | 7 +- docs/rules/jsx-no-target-blank.md | 91 +++++--- lib/rules/jsx-no-target-blank.js | 305 ++++++++++++++++++------- lib/util/linkComponents.js | 19 ++ tests/lib/rules/jsx-no-target-blank.js | 206 +++++++++++++---- 5 files changed, 469 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index b50f34d052..64af152d7c 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,13 @@ You should also specify settings that will be shared across all the plugin rules {"property": "observer", "object": "Mobx"}, {"property": "observer", "object": ""} // sets `object` to whatever value `settings.react.pragma` is set to ], + "formComponents": [ + // Components used as alternatives to
for forms, eg. + "CustomForm", + {"name": "Form", "formAttribute": "endpoint"} + ] "linkComponents": [ - // Components used as alternatives to for linking, eg. + // Components used as alternatives to for linking, eg. "Hyperlink", {"name": "Link", "linkAttribute": "to"} ] diff --git a/docs/rules/jsx-no-target-blank.md b/docs/rules/jsx-no-target-blank.md index 0ca6129ee2..9f9c79248c 100644 --- a/docs/rules/jsx-no-target-blank.md +++ b/docs/rules/jsx-no-target-blank.md @@ -1,23 +1,35 @@ # Prevent usage of unsafe `target='_blank'` (react/jsx-no-target-blank) -When creating a JSX element that has an `a` tag, it is often desired to have the link open in a new tab using the `target='_blank'` attribute. Using this attribute unaccompanied by `rel='noreferrer'`, however, is a severe security vulnerability ([see here for more details](https://html.spec.whatwg.org/multipage/links.html#link-type-noopener)) +When creating a JSX element that has an `a` tag or a `form` tag, it is often desired to have +the link open in a new tab using the `target='_blank'` attribute. Using this +attribute unaccompanied by `rel='noreferrer'`, however, is a severe +security vulnerability ([see here for more details](https://html.spec.whatwg.org/multipage/links.html#link-type-noopener)) This rules requires that you accompany `target='_blank'` attributes with `rel='noreferrer'`. ## Rule Details -This rule aims to prevent user generated links from creating security vulnerabilities by requiring `rel='noreferrer'` for external links, and optionally any dynamically generated links. +This rule aims to prevent user generated link hrefs and form actions from creating security vulnerabilities by requiring `rel='noreferrer'` for external link hrefs and form actions, and optionally any dynamically generated link hrefs and form actions. ## Rule Options + ```json ... -"react/jsx-no-target-blank": [, { "allowReferrer": , "enforceDynamicLinks": }] +"react/jsx-no-target-blank": [, { + "allowReferrer": , + "enforceDynamicLinks": , + "links": , + "forms": , +}] ... ``` -* allow-referrer: optional boolean. If `true` does not require `noreferrer`. Defaults to `false`. -* enabled: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. -* enforceDynamicLinks: optional string, 'always' or 'never' -* warnOnSpreadAttributes: optional boolean. Defaults to `false`. +- `allowReferrer`: optional boolean. If `true` does not require `noreferrer`. Defaults to `false`. +- `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0. +- `enforceDynamicLinks`: optional string, 'always' or 'never' +- `warnOnSpreadAttributes`: optional boolean. Defaults to `false`. +- `enforceDynamicLinks` - enforce: optional string, 'always' or 'never' +- `links` - Prevent usage of unsafe `target='_blank'` inside links, defaults to `true` +- `forms` - Prevent usage of unsafe `target='_blank'` inside forms, defaults to `false` ### `enforceDynamicLinks` @@ -28,19 +40,21 @@ This rule aims to prevent user generated links from creating security vulnerabil Examples of **incorrect** code for this rule, when configured with `{ "enforceDynamicLinks": "always" }`: ```jsx -var Hello = -var Hello = +var Hello = ; +var Hello = ; ``` Examples of **correct** code for this rule: ```jsx -var Hello =

-var Hello = -var Hello = -var Hello = -var Hello = -var Hello = +var Hello =

; +var Hello = ; +var Hello = ( + +); +var Hello = ; +var Hello = ; +var Hello = ; ``` #### never @@ -50,7 +64,7 @@ var Hello = Examples of **correct** code for this rule, when configured with `{ "enforceDynamicLinks": "never" }`: ```jsx -var Hello = +var Hello = ; ``` ### `warnOnSpreadAttributes` @@ -59,11 +73,11 @@ Spread attributes are a handy way of passing programmatically-generated props to ```jsx const unsafeProps = { - href: "http://example.com", - target: "_blank", + href: 'http://example.com', + target: '_blank', }; - +; ``` Defaults to false. If false, this rule will ignore all spread attributes. If true, this rule will treat all spread attributes as if they contain an unsafe combination of props, unless specifically overridden by props _after_ the last spread attribute prop e.g. the following would not be violations: @@ -74,6 +88,20 @@ Defaults to false. If false, this rule will ignore all spread attributes. If tru ``` +### `links` / `forms` + +When option `forms` is set to `true`, the following is considered an error: + +```jsx +var Hello =
; +``` + +When option `links` is set to `true`, the following is considered an error: + +```jsx +var Hello = +``` + ### Custom link components This rule supports the ability to use custom components for links, such as `` 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 `` elements. @@ -81,22 +109,33 @@ This rule supports the ability to use custom components for links, such as ` -var Hello = +var Hello = ; +var Hello = ; ``` Examples of **correct** code for this rule: ```jsx -var Hello = -var Hello = -var Hello = -var Hello = +var Hello = ( + +); +var Hello = ; +var Hello = ; +var Hello = ; ``` +### Custom form components + +This rule supports the ability to use custom components for forms. To enable this, define your custom form components in the global [shared settings](https://github.com/yannickcr/eslint-plugin-react/blob/master/README.md#configuration) under the `formComponents` configuration area. Once configured, this rule will check those components as if they were `
` elements. + ## When To Override It + For links to a trusted host (e.g. internal links to your own site, or links to a another host you control, where you can be certain this security vulnerability does not exist), you may want to keep the HTTP Referer header for analytics purposes. ## When Not To Use It -If you do not have any external links, you can disable this rule. +If you do not have any external links or forms, you can disable this rule. diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js index 9e3f9d75d4..339e8c5106 100644 --- a/lib/rules/jsx-no-target-blank.js +++ b/lib/rules/jsx-no-target-blank.js @@ -28,18 +28,30 @@ function attributeValuePossiblyBlank(attribute) { } const value = attribute.value; if (value.type === 'Literal') { - return typeof value.value === 'string' && value.value.toLowerCase() === '_blank'; + return ( + typeof value.value === 'string' && value.value.toLowerCase() === '_blank' + ); } if (value.type === 'JSXExpressionContainer') { const expr = value.expression; if (expr.type === 'Literal') { - return typeof expr.value === 'string' && expr.value.toLowerCase() === '_blank'; + return ( + typeof expr.value === 'string' && expr.value.toLowerCase() === '_blank' + ); } if (expr.type === 'ConditionalExpression') { - if (expr.alternate.type === 'Literal' && expr.alternate.value && expr.alternate.value.toLowerCase() === '_blank') { + if ( + expr.alternate.type === 'Literal' + && expr.alternate.value + && expr.alternate.value.toLowerCase() === '_blank' + ) { return true; } - if (expr.consequent.type === 'Literal' && expr.consequent.value && expr.consequent.value.toLowerCase() === '_blank') { + if ( + expr.consequent.type === 'Literal' + && expr.consequent.value + && expr.consequent.value.toLowerCase() === '_blank' + ) { return true; } } @@ -47,18 +59,34 @@ function attributeValuePossiblyBlank(attribute) { return false; } -function hasExternalLink(node, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex) { - const linkIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === linkAttribute); - const foundExternalLink = linkIndex !== -1 && ((attr) => attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))( - node.attributes[linkIndex]); - return foundExternalLink || (warnOnSpreadAttributes && linkIndex < spreadAttributeIndex); +function hasExternalLink( + node, + linkAttribute, + warnOnSpreadAttributes, + spreadAttributeIndex +) { + const linkIndex = findLastIndex( + node.attributes, + (attr) => attr.name && attr.name.name === linkAttribute + ); + const foundExternalLink = linkIndex !== -1 + && ((attr) => attr.value.type === 'Literal' && /^(?:\w+:|\/\/)/.test(attr.value.value))( + node.attributes[linkIndex] + ); + return ( + foundExternalLink + || (warnOnSpreadAttributes && linkIndex < spreadAttributeIndex) + ); } function hasDynamicLink(node, linkAttribute) { - const dynamicLinkIndex = findLastIndex(node.attributes, (attr) => attr.name - && attr.name.name === linkAttribute - && attr.value - && attr.value.type === 'JSXExpressionContainer'); + const dynamicLinkIndex = findLastIndex( + node.attributes, + (attr) => attr.name + && attr.name.name === linkAttribute + && attr.value + && attr.value.type === 'JSXExpressionContainer' + ); if (dynamicLinkIndex !== -1) { return true; } @@ -79,9 +107,20 @@ function getStringFromValue(value) { return null; } -function hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex) { - const relIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXAttribute' && attr.name.name === 'rel')); - if (relIndex === -1 || (warnOnSpreadAttributes && relIndex < spreadAttributeIndex)) { +function hasSecureRel( + node, + allowReferrer, + warnOnSpreadAttributes, + spreadAttributeIndex +) { + const relIndex = findLastIndex( + node.attributes, + (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'rel' + ); + if ( + relIndex === -1 + || (warnOnSpreadAttributes && relIndex < spreadAttributeIndex) + ) { return false; } @@ -99,109 +138,203 @@ module.exports = { meta: { fixable: 'code', docs: { - description: 'Forbid `target="_blank"` attribute without `rel="noreferrer"`', + description: + 'Forbid `target="_blank"` attribute without `rel="noreferrer"`', category: 'Best Practices', recommended: true, url: docsUrl('jsx-no-target-blank') }, messages: { - noTargetBlank: 'Using target="_blank" without rel="noreferrer" ' + noTargetBlank: + 'Using target="_blank" without rel="noreferrer" ' + 'is a security risk: see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener' }, - schema: [{ - type: 'object', - properties: { - allowReferrer: { - type: 'boolean' - }, - enforceDynamicLinks: { - enum: ['always', 'never'] + schema: [ + { + type: 'object', + properties: { + allowReferrer: { + type: 'boolean' + }, + enforceDynamicLinks: { + enum: ['always', 'never'] + }, + warnOnSpreadAttributes: { + type: 'boolean' + }, + links: { + type: 'boolean', + default: true + }, + forms: { + type: 'boolean', + default: false + } }, - warnOnSpreadAttributes: { - type: 'boolean' - } - }, - additionalProperties: false - }] + additionalProperties: false + } + ] }, create(context) { - const configuration = context.options[0] || {}; + const configuration = Object.assign( + { + links: true, + forms: false + }, + context.options[0] + ); const allowReferrer = configuration.allowReferrer || false; const warnOnSpreadAttributes = configuration.warnOnSpreadAttributes || false; const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always'; - const components = linkComponentsUtil.getLinkComponents(context); + const linkComponents = linkComponentsUtil.getLinkComponents(context); + const formComponents = linkComponentsUtil.getFormComponents(context); return { JSXOpeningElement(node) { - if (!components.has(node.name.name)) { - return; - } + const targetIndex = findLastIndex( + node.attributes, + (attr) => attr.name && attr.name.name === 'target' + ); + const spreadAttributeIndex = findLastIndex( + node.attributes, + (attr) => attr.type === 'JSXSpreadAttribute' + ); - const targetIndex = findLastIndex(node.attributes, (attr) => attr.name && attr.name.name === 'target'); - const spreadAttributeIndex = findLastIndex(node.attributes, (attr) => (attr.type === 'JSXSpreadAttribute')); + if (linkComponents.has(node.name.name)) { + if (!attributeValuePossiblyBlank(node.attributes[targetIndex])) { + const hasSpread = spreadAttributeIndex >= 0; - if (!attributeValuePossiblyBlank(node.attributes[targetIndex])) { - const hasSpread = spreadAttributeIndex >= 0; - - if (warnOnSpreadAttributes && hasSpread) { - // continue to check below - } else if ((hasSpread && targetIndex < spreadAttributeIndex) || !hasSpread || !warnOnSpreadAttributes) { - return; + if (warnOnSpreadAttributes && hasSpread) { + // continue to check below + } else if ( + (hasSpread && targetIndex < spreadAttributeIndex) + || !hasSpread + || !warnOnSpreadAttributes + ) { + return; + } } - } - const linkAttribute = components.get(node.name.name); - const hasDangerousLink = hasExternalLink(node, linkAttribute, warnOnSpreadAttributes, spreadAttributeIndex) - || (enforceDynamicLinks === 'always' && hasDynamicLink(node, linkAttribute)); - if (hasDangerousLink && !hasSecureRel(node, allowReferrer, warnOnSpreadAttributes, spreadAttributeIndex)) { - context.report({ + const linkAttribute = linkComponents.get(node.name.name); + const hasDangerousLink = hasExternalLink( node, - messageId: 'noTargetBlank', - fix(fixer) { - // eslint 5 uses `node.attributes`; eslint 6+ uses `node.parent.attributes` - const nodeWithAttrs = node.parent.attributes ? node.parent : node; - // eslint 5 does not provide a `name` property on JSXSpreadElements - const relAttribute = nodeWithAttrs.attributes.find((attr) => attr.name && attr.name.name === 'rel'); - - if (targetIndex < spreadAttributeIndex || (spreadAttributeIndex >= 0 && !relAttribute)) { - return null; - } + linkAttribute, + warnOnSpreadAttributes, + spreadAttributeIndex + ) + || (enforceDynamicLinks === 'always' + && hasDynamicLink(node, linkAttribute)); + if ( + hasDangerousLink + && !hasSecureRel( + node, + allowReferrer, + warnOnSpreadAttributes, + spreadAttributeIndex + ) + ) { + context.report({ + node, + messageId: 'noTargetBlank', + fix(fixer) { + // eslint 5 uses `node.attributes`; eslint 6+ uses `node.parent.attributes` + const nodeWithAttrs = node.parent.attributes + ? node.parent + : node; + // eslint 5 does not provide a `name` property on JSXSpreadElements + const relAttribute = nodeWithAttrs.attributes.find( + (attr) => attr.name && attr.name.name === 'rel' + ); - if (!relAttribute) { - return fixer.insertTextAfter(nodeWithAttrs.attributes.slice(-1)[0], ' rel="noreferrer"'); - } + if ( + targetIndex < spreadAttributeIndex + || (spreadAttributeIndex >= 0 && !relAttribute) + ) { + return null; + } - if (!relAttribute.value) { - return fixer.insertTextAfter(relAttribute, '="noreferrer"'); - } + if (!relAttribute) { + return fixer.insertTextAfter( + nodeWithAttrs.attributes.slice(-1)[0], + ' rel="noreferrer"' + ); + } - if (relAttribute.value.type === 'Literal') { - const parts = relAttribute.value.value - .split('noreferrer') - .filter(Boolean); - return fixer.replaceText(relAttribute.value, `"${parts.concat('noreferrer').join(' ')}"`); - } + if (!relAttribute.value) { + return fixer.insertTextAfter(relAttribute, '="noreferrer"'); + } - if (relAttribute.value.type === 'JSXExpressionContainer') { - if (relAttribute.value.expression.type === 'Literal') { - if (typeof relAttribute.value.expression.value === 'string') { - const parts = relAttribute.value.expression.value - .split('noreferrer') - .filter(Boolean); - return fixer.replaceText(relAttribute.value.expression, `"${parts.concat('noreferrer').join(' ')}"`); - } + if (relAttribute.value.type === 'Literal') { + const parts = relAttribute.value.value + .split('noreferrer') + .filter(Boolean); + return fixer.replaceText( + relAttribute.value, + `"${parts.concat('noreferrer').join(' ')}"` + ); + } + + if (relAttribute.value.type === 'JSXExpressionContainer') { + if (relAttribute.value.expression.type === 'Literal') { + if ( + typeof relAttribute.value.expression.value === 'string' + ) { + const parts = relAttribute.value.expression.value + .split('noreferrer') + .filter(Boolean); + return fixer.replaceText( + relAttribute.value.expression, + `"${parts.concat('noreferrer').join(' ')}"` + ); + } - // for undefined, boolean, number, symbol, bigint, and null - return fixer.replaceText(relAttribute.value, '"noreferrer"'); + // for undefined, boolean, number, symbol, bigint, and null + return fixer.replaceText( + relAttribute.value, + '"noreferrer"' + ); + } } + + return null; } + }); + } + } + if (formComponents.has(node.name.name)) { + if (!attributeValuePossiblyBlank(node.attributes[targetIndex])) { + const hasSpread = spreadAttributeIndex >= 0; - return null; + if (warnOnSpreadAttributes && hasSpread) { + // continue to check below + } else if ( + (hasSpread && targetIndex < spreadAttributeIndex) + || !hasSpread + || !warnOnSpreadAttributes + ) { + return; } - }); + } + + if (!configuration.forms || hasSecureRel(node)) { + return; + } + + const formAttribute = formComponents.get(node.name.name); + + if ( + hasExternalLink(node, formAttribute) + || (enforceDynamicLinks === 'always' + && hasDynamicLink(node, formAttribute)) + ) { + context.report({ + node, + messageId: 'noTargetBlank' + }); + } } } }; diff --git a/lib/util/linkComponents.js b/lib/util/linkComponents.js index 066b2ea249..1f5eb6a358 100644 --- a/lib/util/linkComponents.js +++ b/lib/util/linkComponents.js @@ -9,6 +9,24 @@ const DEFAULT_LINK_COMPONENTS = ['a']; const DEFAULT_LINK_ATTRIBUTE = 'href'; +/** TODO: type {(string | { name: string, formAttribute: string })[]} */ +/** @type {any} */ +const DEFAULT_FORM_COMPONENTS = ['form']; +const DEFAULT_FORM_ATTRIBUTE = 'action'; + +function getFormComponents(context) { + const settings = context.settings || {}; + const formComponents = /** @type {typeof DEFAULT_FORM_COMPONENTS} */ ( + DEFAULT_FORM_COMPONENTS.concat(settings.formComponents || []) + ); + return new Map(formComponents.map((value) => { + if (typeof value === 'string') { + return [value, DEFAULT_FORM_ATTRIBUTE]; + } + return [value.name, value.formAttribute]; + })); +} + function getLinkComponents(context) { const settings = context.settings || {}; const linkComponents = /** @type {typeof DEFAULT_LINK_COMPONENTS} */ ( @@ -23,5 +41,6 @@ function getLinkComponents(context) { } module.exports = { + getFormComponents, getLinkComponents }; diff --git a/tests/lib/rules/jsx-no-target-blank.js b/tests/lib/rules/jsx-no-target-blank.js index ee59331399..7c35b4166b 100644 --- a/tests/lib/rules/jsx-no-target-blank.js +++ b/tests/lib/rules/jsx-no-target-blank.js @@ -34,19 +34,35 @@ ruleTester.run('jsx-no-target-blank', rule, { {code: ''}, {code: ''}, {code: ''}, - {code: ''}, + { + code: '' + }, {code: ''}, - {code: ''}, + { + code: '' + }, {code: ''}, - {code: ''}, - {code: ''}, - {code: ''}, + { + code: "" + }, + {code: ""}, + { + code: '' + }, {code: ''}, - {code: ''}, + { + code: '' + }, {code: ''}, - {code: 's'}, - {code: 's'}, - {code: ''}, + { + code: 's' + }, + { + code: 's' + }, + { + code: '' + }, {code: ''}, {code: '

'}, {code: ''}, @@ -63,19 +79,27 @@ ruleTester.run('jsx-no-target-blank', rule, { {code: ''}, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}] + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ] }, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}] + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ] }, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}] + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ] }, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}] + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ] }, { code: '', @@ -86,7 +110,7 @@ ruleTester.run('jsx-no-target-blank', rule, { options: [{enforceDynamicLinks: 'never'}] }, { - code: '', + code: "", options: [{enforceDynamicLinks: 'never'}] }, { @@ -115,52 +139,81 @@ ruleTester.run('jsx-no-target-blank', rule, { }, { code: '' + }, + { + code: '', + options: [{forms: false}] + }, + { + code: '', + options: [{forms: false, links: true}] + }, + { + code: '
', + options: [] + }, + { + code: '
', + options: [{forms: true}] + }, + { + code: '
', + options: [{forms: true, links: false}] } ], invalid: [ { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { @@ -170,17 +223,20 @@ ruleTester.run('jsx-no-target-blank', rule, { }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { @@ -189,22 +245,26 @@ ruleTester.run('jsx-no-target-blank', rule, { }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { @@ -214,17 +274,20 @@ ruleTester.run('jsx-no-target-blank', rule, { }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', errors: defaultErrors }, { code: '', - output: '', + output: + '', options: [{allowReferrer: true}], errors: defaultErrors }, @@ -236,39 +299,51 @@ ruleTester.run('jsx-no-target-blank', rule, { }, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ], errors: defaultErrors }, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ], errors: defaultErrors }, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ], errors: defaultErrors }, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ], errors: defaultErrors }, { code: '', - options: [{enforceDynamicLinks: 'always', warnOnSpreadAttributes: true}], + options: [ + {enforceDynamicLinks: 'always', warnOnSpreadAttributes: true} + ], errors: defaultErrors }, { code: '', - output: '', + output: + '', options: [{enforceDynamicLinks: 'always'}], settings: {linkComponents: ['Link']}, errors: defaultErrors }, { code: '', - output: '', + output: + '', options: [{enforceDynamicLinks: 'always'}], settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}}, errors: defaultErrors @@ -276,16 +351,55 @@ ruleTester.run('jsx-no-target-blank', rule, { { code: '', errors: defaultErrors, - options: [{ - warnOnSpreadAttributes: true - }] + options: [ + { + warnOnSpreadAttributes: true + } + ] }, { code: '', errors: defaultErrors, - options: [{ - warnOnSpreadAttributes: true - }] + options: [ + { + warnOnSpreadAttributes: true + } + ] + }, + { + code: '', + options: [{links: true}], + errors: defaultErrors + }, + { + code: '', + options: [{links: true, forms: true}], + errors: defaultErrors + }, + { + code: '', + options: [{links: true, forms: false}], + errors: defaultErrors + }, + { + code: '
', + options: [{forms: true}], + errors: defaultErrors + }, + { + code: '
', + options: [{forms: true}], + errors: defaultErrors + }, + { + code: '
', + options: [{forms: true}], + errors: defaultErrors + }, + { + code: '
', + options: [{forms: true, links: false}], + errors: defaultErrors } ] });