Created
November 29, 2021 04:46
-
-
Save azirbel/51518d919de979197a7c5c25c54a56d6 to your computer and use it in GitHub Desktop.
eslint rule to avoid mixing conditionals and raw JSX text
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Handy tools & articles for developing eslint rules: | |
https://astexplorer.net/ | |
https://www.webiny.com/blog/create-custom-eslint-rules-in-2-minutes-e3d41cb6a9a0 | |
https://blog.maximeheckel.com/posts/how-to-build-first-eslint-rule/ | |
Developing: | |
(make changes) | |
yarn add --dev file:./eslint && yarn eslint . | |
*/ | |
module.exports = { | |
rules: { | |
'no-conditional-literals-in-jsx': { | |
meta: { | |
docs: { | |
description: | |
'Browser auto-translation will break if pieces of text nodes are be rendered conditionally.', | |
}, | |
schema: [], | |
messages: { | |
unexpected: | |
'Conditional expression is a sibling of raw text and must be wrapped in <div> or <span>', | |
}, | |
}, | |
create: function(context) { | |
return { | |
// Imagine evaluating <div>text {conditional && 'string'}</div> | |
JSXExpressionContainer(node) { | |
// We start at the expression {conditional && 'string'} | |
if (node.expression.type !== 'LogicalExpression') return | |
// "text" is one of the siblingTextNodes. | |
const siblingTextNodes = (node.parent.children || []).filter(n => { | |
// In normal code these are 'Literal', but in test code they are 'JSXText' | |
const isText = n.type === 'Literal' || n.type === 'JSXText' | |
// Skip empty text nodes, like " \n " -- these may be JSX artifacts | |
return isText && !!n.value.trim() | |
}) | |
// If we were evaluting | |
// <div>{property} {conditional && 'string'}</div> | |
// Then {property} would be one of the siblingExpressionNodes | |
const siblingExpressionNodes = (node.parent.children || []).filter( | |
n => | |
n.type === 'JSXExpressionContainer' && | |
(n.expression.type === 'Identifier' || | |
n.expression.type === 'MemberExpression') | |
) | |
// Operands of {conditional && 'string'} -- the conditional and the | |
// literal. We want to make sure we have a text literal, otherwise we'd | |
// trigger this rule on the (safe) {conditional && <div>string</div>}. | |
const expressionOperandTypes = [ | |
node.expression.left.type, | |
node.expression.right.type, | |
] | |
if ( | |
siblingTextNodes.concat(siblingExpressionNodes).length > 0 && | |
expressionOperandTypes.includes('Literal') | |
) { | |
context.report({ node, messageId: 'unexpected' }) | |
} | |
}, | |
} | |
}, | |
}, | |
}, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const rules = require('./rules').rules | |
const RuleTester = require('eslint').RuleTester | |
const ruleTester = new RuleTester({ | |
parserOptions: { | |
ecmaVersion: 6, | |
sourceType: 'module', | |
ecmaFeatures: { | |
jsx: true, | |
}, | |
}, | |
}) | |
const errors = [{ messageId: 'unexpected' }] | |
const noConditionalLiterals = rules['no-conditional-literals-in-jsx'] | |
ruleTester.run('no-conditional-literals-in-jsx', noConditionalLiterals, { | |
valid: [ | |
{ | |
code: `<div>{conditional && 'string'}</div>`, | |
errors, | |
}, | |
{ | |
code: `<div>{conditional || 'string'}</div>`, | |
errors, | |
}, | |
{ | |
// The error happens when DOM elements are added or removed; swapping | |
// is ok | |
code: `<div>{conditional ? 'a' : 'b'}</div>`, | |
errors, | |
}, | |
{ | |
// As long as conditionally-rendered stuff is wrapped in div or span, it's fine | |
code: `<div>text {conditional && <div>wrapped is ok</div>}</div>`, | |
errors, | |
}, | |
{ | |
// Logic within an attribute doesn't affect the DOM | |
code: `<Avatar alt={conditional && 'string'} />`, | |
errors, | |
}, | |
{ | |
// JSX auto-adds whitespace when there are newlines. Make sure they don't trigger | |
code: `<div> | |
{conditional && 'string'} | |
</div>`, | |
errors, | |
}, | |
], | |
invalid: [ | |
{ | |
code: `<div>text {conditional && 'string'}</div>`, | |
errors, | |
}, | |
{ | |
code: `<div>text {conditional || 'string'}</div>`, | |
errors, | |
}, | |
{ | |
code: `<div>{conditional && 'string'} text</div>`, | |
errors, | |
}, | |
{ | |
code: `<div>{conditional || 'string'} text</div>`, | |
errors, | |
}, | |
{ | |
// More complicated logic | |
code: `<div>text {(conditional1 && conditional2) || 'string'}</div>`, | |
errors, | |
}, | |
{ | |
// This results in 2 text nodes with no JSX containers -- dangerous | |
code: `<div>{property} {conditional && 'string'}</div>`, | |
errors, | |
}, | |
{ | |
// This results in 2 text nodes with no JSX containers -- dangerous | |
code: `<div>{object.property} {conditional && 'string'}</div>`, | |
errors, | |
}, | |
], | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment