'use strict'; const {isParenthesized, isNotSemicolonToken} = require('@eslint-community/eslint-utils'); const needsSemicolon = require('./utils/needs-semicolon.js'); const {removeSpacesAfter} = require('./fix/index.js'); const {matches} = require('./selectors/index.js'); const MESSAGE_ID = 'no-lonely-if'; const messages = { [MESSAGE_ID]: 'Unexpected `if` as the only statement in a `if` block without `else`.', }; const ifStatementWithoutAlternate = 'IfStatement:not([alternate])'; const selector = matches([ // `if (a) { if (b) {} }` [ ifStatementWithoutAlternate, ' > ', 'BlockStatement.consequent', '[body.length=1]', ' > ', `${ifStatementWithoutAlternate}.body`, ].join(''), // `if (a) if (b) {}` `${ifStatementWithoutAlternate} > ${ifStatementWithoutAlternate}.consequent`, ]); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table // Lower precedence than `&&` const needParenthesis = node => ( (node.type === 'LogicalExpression' && (node.operator === '||' || node.operator === '??')) || node.type === 'ConditionalExpression' || node.type === 'AssignmentExpression' || node.type === 'YieldExpression' || node.type === 'SequenceExpression' ); function getIfStatementTokens(node, sourceCode) { const tokens = {}; tokens.ifToken = sourceCode.getFirstToken(node); tokens.openingParenthesisToken = sourceCode.getFirstToken(node, 1); const {consequent} = node; tokens.closingParenthesisToken = sourceCode.getTokenBefore(consequent); if (consequent.type === 'BlockStatement') { tokens.openingBraceToken = sourceCode.getFirstToken(consequent); tokens.closingBraceToken = sourceCode.getLastToken(consequent); } return tokens; } function fix(innerIfStatement, sourceCode) { return function * (fixer) { const outerIfStatement = ( innerIfStatement.parent.type === 'BlockStatement' ? innerIfStatement.parent : innerIfStatement ).parent; const outer = { ...outerIfStatement, ...getIfStatementTokens(outerIfStatement, sourceCode), }; const inner = { ...innerIfStatement, ...getIfStatementTokens(innerIfStatement, sourceCode), }; // Remove inner `if` token yield fixer.remove(inner.ifToken); yield removeSpacesAfter(inner.ifToken, sourceCode, fixer); // Remove outer `{}` if (outer.openingBraceToken) { yield fixer.remove(outer.openingBraceToken); yield removeSpacesAfter(outer.openingBraceToken, sourceCode, fixer); yield fixer.remove(outer.closingBraceToken); const tokenBefore = sourceCode.getTokenBefore(outer.closingBraceToken, {includeComments: true}); yield removeSpacesAfter(tokenBefore, sourceCode, fixer); } // Add new `()` yield fixer.insertTextBefore(outer.openingParenthesisToken, '('); yield fixer.insertTextAfter( inner.closingParenthesisToken, `)${inner.consequent.type === 'EmptyStatement' ? '' : ' '}`, ); // Add ` && ` yield fixer.insertTextAfter(outer.closingParenthesisToken, ' && '); // Remove `()` if `test` don't need it for (const {test, openingParenthesisToken, closingParenthesisToken} of [outer, inner]) { if ( isParenthesized(test, sourceCode) || !needParenthesis(test) ) { yield fixer.remove(openingParenthesisToken); yield fixer.remove(closingParenthesisToken); } yield removeSpacesAfter(closingParenthesisToken, sourceCode, fixer); } // If the `if` statement has no block, and is not followed by a semicolon, // make sure that fixing the issue would not change semantics due to ASI. // Similar logic https://github.com/eslint/eslint/blob/2124e1b5dad30a905dc26bde9da472bf622d3f50/lib/rules/no-lonely-if.js#L61-L77 if (inner.consequent.type !== 'BlockStatement') { const lastToken = sourceCode.getLastToken(inner.consequent); if (isNotSemicolonToken(lastToken)) { const nextToken = sourceCode.getTokenAfter(outer); if (needsSemicolon(lastToken, sourceCode, nextToken.value)) { yield fixer.insertTextBefore(nextToken, ';'); } } } }; } /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const sourceCode = context.getSourceCode(); return { [selector](node) { return { node, messageId: MESSAGE_ID, fix: fix(node, sourceCode), }; }, }; }; /** @type {import('eslint').Rule.RuleModule} */ module.exports = { create, meta: { type: 'suggestion', docs: { description: 'Disallow `if` statements as the only statement in `if` blocks without `else`.', }, fixable: 'code', messages, }, };