diff --git a/CHANGELOG.md b/CHANGELOG.md index d82eb0cc13..0aa9588e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange * [`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) 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/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'] }], + }, }, ]), });