/** * @fileoverview Prevent using string literals in React component definition * @author Caleb Morris * @author David Buchan-Swanson */ 'use strict'; const iterFrom = require('es-iterator-helpers/Iterator.from'); const map = require('es-iterator-helpers/Iterator.prototype.map'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); const getText = require('../util/eslint').getText; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ function trimIfString(val) { return typeof val === 'string' ? val.trim() : val; } const messages = { invalidPropValue: 'Invalid prop value: "{{text}}"', noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"', noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"', literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"', }; /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { description: 'Disallow usage of string literals in JSX', category: 'Stylistic Issues', recommended: false, url: docsUrl('jsx-no-literals'), }, messages, schema: [{ type: 'object', properties: { noStrings: { type: 'boolean', }, allowedStrings: { type: 'array', uniqueItems: true, items: { type: 'string', }, }, ignoreProps: { type: 'boolean', }, noAttributeStrings: { type: 'boolean', }, }, additionalProperties: false, }], }, create(context) { const defaults = { noStrings: false, allowedStrings: [], ignoreProps: false, noAttributeStrings: false, }; const config = Object.assign({}, defaults, context.options[0] || {}); config.allowedStrings = new Set(map(iterFrom(config.allowedStrings), trimIfString)); function defaultMessageId() { const ancestorIsJSXElement = arguments.length >= 1 && arguments[0]; if (config.noAttributeStrings && !ancestorIsJSXElement) { return 'noStringsInAttributes'; } if (config.noStrings) { return 'noStringsInJSX'; } return 'literalNotInJSXExpression'; } function getParentIgnoringBinaryExpressions(node) { let current = node; while (current.parent.type === 'BinaryExpression') { current = current.parent; } return current.parent; } function getValidation(node) { const values = [trimIfString(node.raw), trimIfString(node.value)]; if (values.some((value) => config.allowedStrings.has(value))) { return false; } const parent = getParentIgnoringBinaryExpressions(node); function isParentNodeStandard() { if (!/^[\s]+$/.test(node.value) && typeof node.value === 'string' && parent.type.includes('JSX')) { if (config.noAttributeStrings) { return parent.type === 'JSXAttribute' || parent.type === 'JSXElement'; } if (!config.noAttributeStrings) { return parent.type !== 'JSXAttribute'; } } return false; } const standard = isParentNodeStandard(); if (config.noStrings) { return standard; } return standard && parent.type !== 'JSXExpressionContainer'; } function getParentAndGrandParentType(node) { const parent = getParentIgnoringBinaryExpressions(node); const parentType = parent.type; const grandParentType = parent.parent.type; return { parent, parentType, grandParentType, grandParent: parent.parent, }; } function hasJSXElementParentOrGrandParent(node) { const parents = getParentAndGrandParentType(node); const parentType = parents.parentType; const grandParentType = parents.grandParentType; return parentType === 'JSXFragment' || parentType === 'JSXElement' || grandParentType === 'JSXElement'; } function reportLiteralNode(node, messageId) { const ancestorIsJSXElement = hasJSXElementParentOrGrandParent(node); messageId = messageId || defaultMessageId(ancestorIsJSXElement); report(context, messages[messageId], messageId, { node, data: { text: getText(context, node).trim(), }, }); } // -------------------------------------------------------------------------- // Public // -------------------------------------------------------------------------- return { Literal(node) { if (getValidation(node) && (hasJSXElementParentOrGrandParent(node) || !config.ignoreProps)) { reportLiteralNode(node); } }, JSXAttribute(node) { const isNodeValueString = node && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string' && !config.allowedStrings.has(node.value.value); if (config.noStrings && !config.ignoreProps && isNodeValueString) { const messageId = 'invalidPropValue'; reportLiteralNode(node, messageId); } }, JSXText(node) { if (getValidation(node)) { reportLiteralNode(node); } }, TemplateLiteral(node) { const parents = getParentAndGrandParentType(node); const parentType = parents.parentType; const grandParentType = parents.grandParentType; const isParentJSXExpressionCont = parentType === 'JSXExpressionContainer'; const isParentJSXElement = parentType === 'JSXElement' || grandParentType === 'JSXElement'; if (isParentJSXExpressionCont && config.noStrings && (isParentJSXElement || !config.ignoreProps)) { reportLiteralNode(node); } }, }; }, };