From 46c112edfdcb40681a8997ec4f47b413a08fdd14 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 5 Apr 2024 15:09:35 +0100 Subject: Enforce that text is wrapped in , remaining cases (#3421) * Toggle.Button -> Toggle.ButtonWithText * Simplify Prompt.Cancel/Action * Move lines down for better diff * Remove ButtonWithText * Simplify types * Enforce Button/ButtonText nesting * Add suggested wrapper in linter error * Check ancestry too * Also check literals * Rm ts-ignore --- eslint/avoid-unwrapped-text.js | 194 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 189 insertions(+), 5 deletions(-) (limited to 'eslint/avoid-unwrapped-text.js') diff --git a/eslint/avoid-unwrapped-text.js b/eslint/avoid-unwrapped-text.js index 79d099f00..eef31f795 100644 --- a/eslint/avoid-unwrapped-text.js +++ b/eslint/avoid-unwrapped-text.js @@ -33,6 +33,7 @@ exports.create = function create(context) { const options = context.options[0] || {} const impliedTextProps = options.impliedTextProps ?? [] const impliedTextComponents = options.impliedTextComponents ?? [] + const suggestedTextWrappers = options.suggestedTextWrappers ?? {} const textProps = [...impliedTextProps] const textComponents = ['Text', ...impliedTextComponents] @@ -54,13 +55,13 @@ exports.create = function create(context) { return } if (tagName === 'Trans') { - // Skip over it and check above. + // Exit and rely on the traversal for JSXElement (code below). // TODO: Maybe validate that it's present. - parent = parent.parent - continue + return } - let message = 'Wrap this string in .' - if (tagName !== 'View') { + const suggestedWrapper = suggestedTextWrappers[tagName] + let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.` + if (tagName !== 'View' && !suggestedWrapper) { message += ' If <' + tagName + @@ -112,6 +113,189 @@ exports.create = function create(context) { continue } }, + Literal(node) { + if (typeof node.value !== 'string' && typeof node.value !== 'number') { + return + } + let parent = node.parent + while (parent) { + if (parent.type === 'JSXElement') { + const tagName = getTagName(parent) + if (isTextComponent(tagName)) { + // We're good. + return + } + if (tagName === 'Trans') { + // Exit and rely on the traversal for JSXElement (code below). + // TODO: Maybe validate that it's present. + return + } + const suggestedWrapper = suggestedTextWrappers[tagName] + let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.` + if (tagName !== 'View' && !suggestedWrapper) { + message += + ' If <' + + tagName + + '> is guaranteed to render , ' + + 'rename it to <' + + tagName + + 'Text> or add it to impliedTextComponents.' + } + context.report({ + node, + message, + }) + return + } + + if (parent.type === 'BinaryExpression' && parent.operator === '+') { + parent = parent.parent + continue + } + + if ( + parent.type === 'JSXExpressionContainer' || + parent.type === 'LogicalExpression' + ) { + parent = parent.parent + continue + } + + // Be conservative for other types. + return + } + }, + TemplateLiteral(node) { + let parent = node.parent + while (parent) { + if (parent.type === 'JSXElement') { + const tagName = getTagName(parent) + if (isTextComponent(tagName)) { + // We're good. + return + } + if (tagName === 'Trans') { + // Exit and rely on the traversal for JSXElement (code below). + // TODO: Maybe validate that it's present. + return + } + const suggestedWrapper = suggestedTextWrappers[tagName] + let message = `Wrap this string in <${suggestedWrapper ?? 'Text'}>.` + if (tagName !== 'View' && !suggestedWrapper) { + message += + ' If <' + + tagName + + '> is guaranteed to render , ' + + 'rename it to <' + + tagName + + 'Text> or add it to impliedTextComponents.' + } + context.report({ + node, + message, + }) + return + } + + if ( + parent.type === 'CallExpression' && + parent.callee.type === 'Identifier' && + parent.callee.name === '_' + ) { + // This is a user-facing string, keep going up. + parent = parent.parent + continue + } + + if (parent.type === 'BinaryExpression' && parent.operator === '+') { + parent = parent.parent + continue + } + + if ( + parent.type === 'JSXExpressionContainer' || + parent.type === 'LogicalExpression' || + parent.type === 'TaggedTemplateExpression' + ) { + parent = parent.parent + continue + } + + // Be conservative for other types. + return + } + }, + JSXElement(node) { + if (getTagName(node) !== 'Trans') { + return + } + let parent = node.parent + while (parent) { + if (parent.type === 'JSXElement') { + const tagName = getTagName(parent) + if (isTextComponent(tagName)) { + // We're good. + return + } + if (tagName === 'Trans') { + // Exit and rely on the traversal for this JSXElement. + // TODO: Should nested even be allowed? + return + } + const suggestedWrapper = suggestedTextWrappers[tagName] + let message = `Wrap this in <${suggestedWrapper ?? 'Text'}>.` + if (tagName !== 'View' && !suggestedWrapper) { + message += + ' If <' + + tagName + + '> is guaranteed to render , ' + + 'rename it to <' + + tagName + + 'Text> or add it to impliedTextComponents.' + } + context.report({ + node, + message, + }) + return + } + + if ( + parent.type === 'JSXAttribute' && + parent.name.type === 'JSXIdentifier' && + parent.parent.type === 'JSXOpeningElement' && + parent.parent.parent.type === 'JSXElement' + ) { + const tagName = getTagName(parent.parent.parent) + const propName = parent.name.name + if ( + textProps.includes(tagName + ' ' + propName) || + propName === 'text' || + propName.endsWith('Text') + ) { + // We're good. + return + } + const message = + 'Wrap this in .' + + ' If `' + + propName + + '` is guaranteed to be wrapped in , ' + + 'rename it to `' + + propName + + 'Text' + + '` or add it to impliedTextProps.' + context.report({ + node, + message, + }) + return + } + + parent = parent.parent + continue + } + }, ReturnStatement(node) { let fnScope = context.getScope() while (fnScope && fnScope.type !== 'function') { -- cgit 1.4.1