eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
198 lines (166 loc) • 4.68 kB
JavaScript
import {
isCallExpression,
isStringLiteral,
} from './ast/index.js';
import {
needsSemicolon,
isParenthesized,
} from './utils/index.js';
const MESSAGE_ID_ERROR = 'prefer-bigint-literals/error';
const MESSAGE_ID_SUGGESTION = 'prefer-bigint-literals/suggestion';
const messages = {
[MESSAGE_ID_ERROR]: 'Prefer using bigint literal over `BigInt(…)`.',
[MESSAGE_ID_SUGGESTION]: 'Replace with {{replacement}}.',
};
// A bigint literal is `0n` or a digit sequence that does not start with `0`, unlike a decimal literal where `08` is a legacy octal
const isBigIntLiteralText = text => /^(?:0|[1-9]\d*)$/u.test(text);
// `0b1`, `0o7`, `0xf`, but not `0b01`, the digits have to be canonical or the literal is a different number
const canUsePrefixedRaw = (raw, value) => {
for (const {prefix, base} of [
{prefix: '0b', base: 2},
{prefix: '0o', base: 8},
{prefix: '0x', base: 16},
]) {
if (raw.startsWith(prefix)) {
return raw.slice(2) === value.toString(base);
}
}
return false;
};
const canUseNumericLiteralRaw = numericLiteral => {
const raw = numericLiteral.raw.replaceAll('_', '').toLowerCase();
if (raw.includes('.')) {
return false;
}
const {value} = numericLiteral;
// `1e+21` is also how `String()` prints the value, but `1e+21n` is not a bigint literal
return canUsePrefixedRaw(raw, value) || (!raw.includes('e') && raw === String(value));
};
function getReplacement(valueNode) {
if (isStringLiteral(valueNode)) {
const raw = valueNode.raw.slice(1, -1);
let bigint;
try {
bigint = BigInt(raw);
} catch {
return;
}
let text = raw.trim();
if (text.startsWith('+')) {
text = text.slice(1).trim();
}
// A bigint literal can not have leading zeros, `00001n` is a syntax error
if (
!isBigIntLiteralText(text)
&& !canUsePrefixedRaw(text.toLowerCase(), bigint)
) {
text = bigint.toString();
}
return {shouldUseSuggestion: raw.includes('+'), text: `${text}n`, bigint};
}
let shouldUseSuggestion = false;
let isNegated = false;
while (valueNode.type === 'UnaryExpression' && valueNode.prefix) {
if (valueNode.operator === '+') {
shouldUseSuggestion = true;
valueNode = valueNode.argument;
} else if (valueNode.operator === '-') {
isNegated = !isNegated;
valueNode = valueNode.argument;
} else {
return;
}
}
const {value, raw} = valueNode;
// Intentionally `Number.isInteger`, not `Number.isSafeInteger`: large integers beyond the safe range are exactly what bigint literals are for.
// eslint-disable-next-line unicorn/prefer-number-is-safe-integer
if (!Number.isInteger(value)) {
return;
}
// `BigInt()` does not throw for an integer
let bigint = BigInt(value);
let text;
if (canUseNumericLiteralRaw(valueNode)) {
text = `${raw}n`;
} else {
text = `${bigint}n`;
shouldUseSuggestion = true;
}
if (isNegated && bigint !== 0n) {
text = `-${text}`;
bigint = -bigint;
}
return {shouldUseSuggestion, text, bigint};
}
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
context.on('CallExpression', callExpression => {
if (!isCallExpression(callExpression, {
name: 'BigInt',
argumentsLength: 1,
optional: false,
})) {
return;
}
const [valueNode] = callExpression.arguments;
const replacement = getReplacement(valueNode);
if (!replacement) {
return;
}
const problem = {
node: callExpression,
messageId: MESSAGE_ID_ERROR,
};
const {shouldUseSuggestion, text, bigint} = replacement;
/**
@param {import('eslint').Rule.RuleFixer} fixer
*/
const fix = fixer => {
let replacementText = text;
if (!isParenthesized(callExpression, context) && bigint < 0n) {
replacementText = `(${replacementText})`;
const tokenBefore = context.sourceCode.getTokenBefore(callExpression);
if (needsSemicolon(tokenBefore, context, replacementText)) {
replacementText = `;${replacementText}`;
}
}
return fixer.replaceText(callExpression, replacementText);
};
if (shouldUseSuggestion || context.sourceCode.getCommentsInside(callExpression).length > 0) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
data: {
replacement: text.length < 20 ? `\`${text}\`` : 'a bigint literal',
},
fix,
},
];
} else {
problem.fix = fix;
}
return problem;
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `BigInt` literals over the constructor.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
languages: [
'js/js',
],
},
};
export default config;