UNPKG

eslint-plugin-unicorn

Version:
227 lines (200 loc) • 5.83 kB
import {hasSideEffect} from '@eslint-community/eslint-utils'; import {fixSpaceAroundKeyword} from './fix/index.js'; import {isCallExpression, isLiteral, isMethodCall} from './ast/index.js'; import { getCallArgumentText, getCallExpressionArgumentsText, getCallExpressionTokens, wouldRemoveComments, } from './utils/index.js'; const ERROR_BITWISE = 'error-bitwise'; const ERROR_BITWISE_NOT = 'error-bitwise-not'; const ERROR_PARSE_INT = 'error-parse-int'; const SUGGESTION_BITWISE = 'suggestion-bitwise'; const SUGGESTION_BITWISE_NOT = 'suggestion-bitwise-not'; const SUGGESTION_PARSE_INT = 'suggestion-parse-int'; const messages = { [ERROR_BITWISE]: 'Use `Math.trunc` instead of `{{operator}} {{value}}`.', [ERROR_BITWISE_NOT]: 'Use `Math.trunc` instead of `~~`.', [ERROR_PARSE_INT]: 'Use `Math.trunc` instead of `{{name}}(String(...), 10)`.', [SUGGESTION_BITWISE]: 'Replace `{{operator}} {{value}}` with `Math.trunc`.', [SUGGESTION_BITWISE_NOT]: 'Replace `~~` with `Math.trunc`.', [SUGGESTION_PARSE_INT]: 'Replace `{{name}}(String(...), 10)` with `Math.trunc`.', }; // Bitwise operators const bitwiseOperators = new Set(['|', '>>', '<<', '^']); const isBitwiseNot = node => node.type === 'UnaryExpression' && node.operator === '~'; const getParseIntName = (node, sourceCode) => { if ( isCallExpression(node, { name: 'parseInt', argumentsLength: 2, optional: false, }) && sourceCode.isGlobalReference(node.callee) ) { return 'parseInt'; } if ( isMethodCall(node, { object: 'Number', method: 'parseInt', argumentsLength: 2, optionalCall: false, optionalMember: false, computed: false, }) && sourceCode.isGlobalReference(node.callee.object) ) { return 'Number.parseInt'; } }; const isStringCall = (node, sourceCode) => isCallExpression(node, { name: 'String', argumentsLength: 1, optional: false, }) && sourceCode.isGlobalReference(node.callee); const hasCommentsOutsideStringArgument = (node, stringCall, context) => { const {sourceCode} = context; const { openingParenthesisToken, closingParenthesisToken, } = getCallExpressionTokens(stringCall, context); const stringArgumentRange = [ sourceCode.getRange(openingParenthesisToken)[1], sourceCode.getRange(closingParenthesisToken)[0], ]; return wouldRemoveComments(context, node, [stringArgumentRange]); }; /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const {sourceCode} = context; const mathTruncFunctionCall = node => `Math.trunc(${getCallArgumentText(node, context)})`; const mathTruncFunctionCallFromStringArgument = node => { const argumentsText = getCallExpressionArgumentsText(context, node); return `Math.trunc(${argumentsText})`; }; context.on('CallExpression', node => { const name = getParseIntName(node, sourceCode); if (!name) { return; } const [stringCall, radix] = node.arguments; if ( !isStringCall(stringCall, sourceCode) || !isLiteral(radix, 10) ) { return; } const problem = { node, messageId: ERROR_PARSE_INT, data: {name}, }; if (!hasCommentsOutsideStringArgument(node, stringCall, context)) { problem.suggest = [ { messageId: SUGGESTION_PARSE_INT, data: {name}, fix: fixer => fixer.replaceText(node, mathTruncFunctionCallFromStringArgument(stringCall)), }, ]; } return problem; }); context.on(['BinaryExpression', 'AssignmentExpression'], node => { const {type, operator, right, left} = node; const isAssignment = type === 'AssignmentExpression'; if ( !isLiteral(right, 0) || !bitwiseOperators.has(isAssignment ? operator.slice(0, -1) : operator) ) { return; } const problem = { node, messageId: ERROR_BITWISE, data: { operator, value: right.raw, }, }; // The expression is rebuilt as the call argument, so a comment in it would be lost if ( (!isAssignment || !hasSideEffect(left, sourceCode)) && sourceCode.getCommentsInside(node).length === 0 ) { const fix = function * (fixer) { const fixed = mathTruncFunctionCall(left); if (isAssignment) { const operatorToken = sourceCode.getTokenAfter(left, token => token.type === 'Punctuator' && token.value === operator); yield fixer.replaceText(operatorToken, '='); yield fixer.replaceText(right, fixed); } else { yield fixSpaceAroundKeyword(fixer, node, context); yield fixer.replaceText(node, fixed); } }; // Every one of these coerces through `ToInt32`, which wraps where `Math.trunc` does not problem.suggest = [ { messageId: SUGGESTION_BITWISE, fix, }, ]; } return problem; }); // Unary Expression Selector: Inner-most 2 bitwise NOT context.on('UnaryExpression', node => { if ( !isBitwiseNot(node) || !isBitwiseNot(node.argument) || isBitwiseNot(node.argument.argument) ) { return; } const problem = { node, messageId: ERROR_BITWISE_NOT, }; // `~~` coerces through `ToInt32` exactly like the other bitwise forms, it wraps where `Math.trunc` does not, so it can only be a suggestion. The whole double negation is replaced, so a comment inside it would be lost. if (sourceCode.getCommentsInside(node).length === 0) { problem.suggest = [ { messageId: SUGGESTION_BITWISE_NOT, * fix(fixer) { yield fixer.replaceText(node, mathTruncFunctionCall(node.argument.argument)); yield fixSpaceAroundKeyword(fixer, node, context); }, }, ]; } return problem; }); }; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'suggestion', docs: { description: 'Prefer `Math.trunc()` for truncating numbers.', recommended: 'unopinionated', }, hasSuggestions: true, messages, languages: [ 'js/js', ], }, }; export default config;