diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd7d07507..0aa9588e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ 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) +* [`jsx-no-script-url`]: add `includeFromSettings` option to support `linkAttributes` setting ([#3673][] @burtek) ### Fixed * [`jsx-no-leaked-render`]: preserve RHS parens for multiline jsx elements while fixing ([#3623][] @akulsr0) @@ -32,6 +34,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/docs/rules/jsx-no-script-url.md b/docs/rules/jsx-no-script-url.md index 8d4f7c8cd4..11fffdad16 100644 --- a/docs/rules/jsx-no-script-url.md +++ b/docs/rules/jsx-no-script-url.md @@ -23,8 +23,14 @@ Examples of **correct** code for this rule: ``` +This rule takes the `linkComponents` setting into account. + ## Rule Options +This rule accepts array option (optional) and object option (optional). + +### Array option (default `[]`) + ```json { "react/jsx-no-script-url": [ @@ -45,11 +51,11 @@ Examples of **correct** code for this rule: Allows you to indicate a specific list of properties used by a custom component to be checked. -### name +#### name Component name. -### props +#### props List of properties that should be validated. @@ -60,3 +66,37 @@ Examples of **incorrect** code for this rule, when configured with the above opt ``` + +### Object option + +#### includeFromSettings (default `false`) + +Indicates if the `linkComponents` config in [global shared settings](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/README.md#configuration) should also be taken into account. If enabled, components and properties defined in settings will be added to the list provided in first option (if provided): + +```json +{ + "react/jsx-no-script-url": [ + "error", + [ + { + "name": "Link", + "props": ["to"] + }, + { + "name": "Foo", + "props": ["href", "to"] + } + ], + { "includeFromSettings": true } + ] +} +``` + +If only global settings should be used for this rule, the array option can be omitted: + +```jsonc +{ + // same as ["error", [], { "includeFromSettings": true }] + "react/jsx-no-script-url": ["error", { "includeFromSettings": true }] +} +``` diff --git a/lib/rules/jsx-no-script-url.js b/lib/rules/jsx-no-script-url.js index 425741cf1d..bcf9468a24 100644 --- a/lib/rules/jsx-no-script-url.js +++ b/lib/rules/jsx-no-script-url.js @@ -5,7 +5,9 @@ 'use strict'; +const includes = require('array-includes'); const docsUrl = require('../util/docsUrl'); +const linkComponentsUtil = require('../util/linkComponents'); const report = require('../util/report'); // ------------------------------------------------------------------------------ @@ -21,26 +23,20 @@ function hasJavaScriptProtocol(attr) { && isJavaScriptProtocol.test(attr.value.value); } -function shouldVerifyElement(node, config) { - const name = node.name && node.name.name; - return name === 'a' || config.find((i) => i.name === name); -} - function shouldVerifyProp(node, config) { const name = node.name && node.name.name; const parentName = node.parent.name && node.parent.name.name; - if (parentName === 'a' && name === 'href') { - return true; - } + if (!name || !parentName || !config.has(parentName)) return false; - const el = config.find((i) => i.name === parentName); - if (!el) { - return false; - } + const attributes = config.get(parentName); + return includes(attributes, name); +} - const props = el.props || []; - return node.name && props.indexOf(name) !== -1; +function parseLegacyOption(config, option) { + option.forEach((opt) => { + config.set(opt.name, opt.props); + }); } const messages = { @@ -58,35 +54,84 @@ module.exports = { messages, - schema: [{ - type: 'array', - uniqueItems: true, - items: { - type: 'object', - properties: { - name: { - type: 'string', - }, - props: { - type: 'array', - items: { - type: 'string', + schema: { + anyOf: [ + { + type: 'array', + items: [ + { + type: 'array', uniqueItems: true, + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + props: { + type: 'array', + items: { + type: 'string', + uniqueItems: true, + }, + }, + }, + required: ['name', 'props'], + additionalProperties: false, + }, + }, + { + type: 'object', + properties: { + includeFromSettings: { + type: 'boolean', + }, + }, + additionalItems: false, }, - }, + ], + additionalItems: false, }, - required: ['name', 'props'], - additionalProperties: false, - }, - }], + { + type: 'array', + items: [ + { + type: 'object', + properties: { + includeFromSettings: { + type: 'boolean', + }, + }, + additionalItems: false, + }, + ], + additionalItems: false, + }, + ], + }, }, create(context) { - const config = context.options[0] || []; + const options = context.options; + const hasLegacyOption = Array.isArray(options[0]); + const legacyOptions = hasLegacyOption ? options[0] : []; + // eslint-disable-next-line no-nested-ternary + const objectOption = (hasLegacyOption && options.length > 1) + ? options[1] + : (options.length > 0 + ? options[0] + : { + includeFromSettings: false, + } + ); + const includeFromSettings = objectOption.includeFromSettings; + + const linkComponents = linkComponentsUtil.getLinkComponents(includeFromSettings ? context : {}); + parseLegacyOption(linkComponents, legacyOptions); + return { JSXAttribute(node) { - const parent = node.parent; - if (shouldVerifyElement(parent, config) && shouldVerifyProp(node, config) && hasJavaScriptProtocol(node)) { + if (shouldVerifyProp(node, linkComponents) && hasJavaScriptProtocol(node)) { report(context, messages.noScriptURL, 'noScriptURL', { node, }); 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-script-url.js b/tests/lib/rules/jsx-no-script-url.js index 24cff790a2..4d0374e08e 100644 --- a/tests/lib/rules/jsx-no-script-url.js +++ b/tests/lib/rules/jsx-no-script-url.js @@ -38,8 +38,22 @@ ruleTester.run('jsx-no-script-url', rule, { { code: '' }, { code: '' }, { code: '' }, + { + code: '', + settings: { + linkComponents: [{ name: 'Foo', linkAttribute: ['to', 'href'] }], + }, + }, + { + code: '', + options: [[], { includeFromSettings: false }], + settings: { + linkComponents: [{ name: 'Foo', linkAttribute: ['to', 'href'] }], + }, + }, ]), invalid: parsers.all([ + // defaults { code: '', errors: [{ messageId: 'noScriptURL' }], @@ -52,6 +66,8 @@ ruleTester.run('jsx-no-script-url', rule, { code: '', errors: [{ messageId: 'noScriptURL' }], }, + + // with component passed by options { code: '', errors: [{ messageId: 'noScriptURL' }], @@ -66,6 +82,34 @@ ruleTester.run('jsx-no-script-url', rule, { [{ name: 'Foo', props: ['to', 'href'] }], ], }, + { // make sure it still uses defaults when passed options + code: '', + errors: [{ messageId: 'noScriptURL' }], + options: [ + [{ name: 'Foo', props: ['to', 'href'] }], + ], + }, + + // with components passed by settings + { + code: '', + errors: [{ messageId: 'noScriptURL' }], + options: [ + [{ name: 'Bar', props: ['to', 'href'] }], + { includeFromSettings: true }, + ], + settings: { + linkComponents: [{ name: 'Foo', linkAttribute: 'to' }], + }, + }, + { + code: '', + errors: [{ messageId: 'noScriptURL' }], + options: [{ includeFromSettings: true }], + settings: { + linkComponents: [{ name: 'Foo', linkAttribute: ['to', 'href'] }], + }, + }, { code: `
@@ -78,11 +122,29 @@ ruleTester.run('jsx-no-script-url', rule, { { messageId: 'noScriptURL' }, ], options: [ - [ - { name: 'Foo', props: ['to', 'href'] }, - { name: 'Bar', props: ['link'] }, - ], + [{ name: 'Bar', props: ['link'] }], + { includeFromSettings: true }, + ], + settings: { + linkComponents: [{ name: 'Foo', linkAttribute: ['to', 'href'] }], + }, + }, + { + code: ` +
+ + +
+ `, + errors: [ + { messageId: 'noScriptURL' }, + ], + options: [ + [{ name: 'Bar', props: ['link'] }], ], + settings: { + linkComponents: [{ name: 'Foo', linkAttribute: ['to', 'href'] }], + }, }, ]), }); 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']], ])); }); }); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy