Skip to content

Commit

Permalink
Allow multiple attributes for formComponents and linkComponents s…
Browse files Browse the repository at this point in the history
…ettings
  • Loading branch information
burtek committed Jan 8, 2024
1 parent 2c9c85a commit a819211
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 25 deletions.
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": "Form", "formAttributes": ["registerEnpoint", "loginEnpoint"]}, // allows specifying multiple properties if necessary
{"name": "SimpleForm", "formAttribute": "endpoint"}, // deprecated backwards-compatible version
],
"linkComponents": [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
"Hyperlink",
{"name": "Link", "linkAttribute": "to"}
{"name": "Link", "linkAttributes": ["to", "href"]} // allows specifying multiple properties if necessary
{"name": "MyLink", "linkAttribute": "to"} // deprecated backwards-compatible version
]
}
}
Expand Down
9 changes: 4 additions & 5 deletions lib/rules/jsx-no-script-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,13 @@ function shouldVerifyProp(node, config) {

if (!name || !parentName || !config.has(parentName)) return false;

return name === config.get(parentName);
const attributes = config.get(parentName)
return attributes.includes(name);
}

function parseLegacyOption(config, option) {
option.forEach((opt) => {
opt.props.forEach((prop) => {
config.set(opt.name, prop);
});
config.set(opt.name, opt.props);
});
}

Expand Down Expand Up @@ -82,7 +81,7 @@ module.exports = {
if (context.options[0]) {
parseLegacyOption(linkComponents, context.options[0]);
}

return {
JSXAttribute(node) {
if (shouldVerifyProp(node, linkComponents) && hasJavaScriptProtocol(node)) {
Expand Down
20 changes: 10 additions & 10 deletions lib/rules/jsx-no-target-blank.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,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 && linkAttributes.includes(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
&& linkAttributes.includes(attr.name.name)
&& attr.value
&& attr.value.type === 'JSXExpressionContainer');
if (dynamicLinkIndex !== -1) {
Expand Down Expand Up @@ -194,9 +194,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 +265,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, value.formAttributes || [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, value.linkAttributes || [value.linkAttribute]];
}));
}

Expand Down
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',
linkAttributes: ['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',
linkAttribute: 'endpoint',
},
{
name: 'MyForm2',
linkAttributes: ['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 a819211

Please sign in to comment.