123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- '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,
- },
- };
|