UNPKG

@skyscanner/eslint-plugin-rules

Version:

ESLint plugin containing rules used at Skyscanner

195 lines (163 loc) 6.49 kB
/* # What is this? Builds on eslint-plugin-react/forbid-component-props and extends to allow and Allow List to be provided as a regex. Aside from providing an `allowedForRegex` option the implementation remains the same. A `disallowedForRegex` is not provided as no use case currently exists, so we avoid the extra complexity. https://github.com/jsx-eslint/eslint-plugin-react/blob/9f4b2b96d92bf61ae61e8fc88c413331efe6f0da/lib/rules/forbid-component-props.js#L2 An issue has been raised with eslint-plugin-react to ask for this feature, if provided we should switch to: - https://github.com/jsx-eslint/eslint-plugin-react/issues/3686 # Why do we need a custom rule? We use this linting specifically for linting on className usage. This has been seen to cause specificity problems when working in a code-split app. Our allowlist for the medium to long term will include Bpk* components, and backpack-component-icon Icons. The former is a static list, which could be maintained in an .eslintrc with minimum toil. However, when used with the `withDefaultProps` HOC that Backpack provide the names become more dynamic and more toil to maintain. Additionally, and significantly, Icons are also much higher volume, and have dynamic names. Maintaining a list of components and managing contributors confusion is higher toil than maintaining this custom rule. */ // ------------------------------------------------------------------------------ // Constants // ------------------------------------------------------------------------------ const DEFAULTS = ['className', 'style']; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ const messages = { propIsForbidden: 'Prop "{{prop}}" is forbidden on Components', }; // ------------------------------------------------------------------------------ // Utils // ------------------------------------------------------------------------------ function getMessageData(messageId, message) { return messageId ? { messageId } : { message }; } function report(context, message, messageId, data) { context.report(Object.assign(getMessageData(messageId, message), data)); } module.exports = { meta: { docs: { description: 'Disallow certain props on components', category: 'Best Practices', recommended: false, url: 'https://github.com/Skyscanner/eslint-plugin-rules#forbid-component-props', }, messages, schema: [ { type: 'object', properties: { forbid: { type: 'array', items: { anyOf: [ { type: 'string' }, { type: 'object', properties: { propName: { type: 'string' }, allowedFor: { type: 'array', uniqueItems: true, items: { type: 'string' }, }, allowedForRegex: { type: 'string' }, message: { type: 'string' }, }, additionalProperties: false, }, { type: 'object', properties: { propName: { type: 'string' }, disallowedFor: { type: 'array', uniqueItems: true, minItems: 1, items: { type: 'string' }, }, message: { type: 'string' }, }, required: ['disallowedFor'], additionalProperties: false, }, ], }, }, }, }, ], }, create(context) { const configuration = context.options[0] || {}; const forbid = new Map( (configuration.forbid || DEFAULTS).map((value) => { const propName = typeof value === 'string' ? value : value.propName; const options = { allowList: typeof value === 'string' ? [] : value.allowedFor || [], disallowList: typeof value === 'string' ? [] : value.disallowedFor || [], message: typeof value === 'string' ? null : value.message, // New feature: Support Allow List regex input. allowRegex: typeof value !== 'string' && value.allowedForRegex ? new RegExp(value.allowedForRegex) : null, }; return [propName, options]; }), ); function isForbidden(prop, tagName) { const options = forbid.get(prop); if (!options) { return false; } if (typeof tagName === 'undefined') { return true; } // Disallow List takes precedence over Allow List // tagName is forbidden if it is in the Disallow List if (options.disallowList.length > 0) { return options.disallowList.indexOf(tagName) !== -1; } const isInAllowList = options.allowList.indexOf(tagName) !== -1; // tagName is forbidden if it is not in the Allow List // Exit early here to avoid cases of needlessly running the regex if (isInAllowList) { return false; } return !options.allowRegex || !options.allowRegex.test(tagName); } return { JSXAttribute(node) { debugger; const parentName = node.parent.name; // Extract a component name when using a "namespace", e.g. `<AntdLayout.Content />`. const tag = parentName.name || `${parentName.object.name}.${parentName.property.name}`; const componentName = parentName.name || parentName.property.name; if ( componentName && typeof componentName[0] === 'string' && componentName[0] !== componentName[0].toUpperCase() ) { // This is a DOM node, not a Component, so exit. return; } const prop = node.name.name; if (!isForbidden(prop, tag)) { return; } const customMessage = forbid.get(prop).message; report( context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', { node, data: { prop, }, }, ); }, }; }, };