1/** 2 * @fileoverview Rule to disallow `parseInt()` in favor of binary, octal, and hexadecimal literals 3 * @author Annie Zhang, Henry Zhu 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Helpers 16//------------------------------------------------------------------------------ 17 18const radixMap = new Map([ 19 [2, { system: "binary", literalPrefix: "0b" }], 20 [8, { system: "octal", literalPrefix: "0o" }], 21 [16, { system: "hexadecimal", literalPrefix: "0x" }] 22]); 23 24/** 25 * Checks to see if a CallExpression's callee node is `parseInt` or 26 * `Number.parseInt`. 27 * @param {ASTNode} calleeNode The callee node to evaluate. 28 * @returns {boolean} True if the callee is `parseInt` or `Number.parseInt`, 29 * false otherwise. 30 */ 31function isParseInt(calleeNode) { 32 return ( 33 astUtils.isSpecificId(calleeNode, "parseInt") || 34 astUtils.isSpecificMemberAccess(calleeNode, "Number", "parseInt") 35 ); 36} 37 38//------------------------------------------------------------------------------ 39// Rule Definition 40//------------------------------------------------------------------------------ 41 42module.exports = { 43 meta: { 44 type: "suggestion", 45 46 docs: { 47 description: "disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals", 48 category: "ECMAScript 6", 49 recommended: false, 50 url: "https://eslint.org/docs/rules/prefer-numeric-literals" 51 }, 52 53 schema: [], 54 55 messages: { 56 useLiteral: "Use {{system}} literals instead of {{functionName}}()." 57 }, 58 59 fixable: "code" 60 }, 61 62 create(context) { 63 const sourceCode = context.getSourceCode(); 64 65 //---------------------------------------------------------------------- 66 // Public 67 //---------------------------------------------------------------------- 68 69 return { 70 71 "CallExpression[arguments.length=2]"(node) { 72 const [strNode, radixNode] = node.arguments, 73 str = astUtils.getStaticStringValue(strNode), 74 radix = radixNode.value; 75 76 if ( 77 str !== null && 78 astUtils.isStringLiteral(strNode) && 79 radixNode.type === "Literal" && 80 typeof radix === "number" && 81 radixMap.has(radix) && 82 isParseInt(node.callee) 83 ) { 84 85 const { system, literalPrefix } = radixMap.get(radix); 86 87 context.report({ 88 node, 89 messageId: "useLiteral", 90 data: { 91 system, 92 functionName: sourceCode.getText(node.callee) 93 }, 94 fix(fixer) { 95 if (sourceCode.getCommentsInside(node).length) { 96 return null; 97 } 98 99 const replacement = `${literalPrefix}${str}`; 100 101 if (+replacement !== parseInt(str, radix)) { 102 103 /* 104 * If the newly-produced literal would be invalid, (e.g. 0b1234), 105 * or it would yield an incorrect parseInt result for some other reason, don't make a fix. 106 * 107 * If `str` had numeric separators, `+replacement` will evaluate to `NaN` because unary `+` 108 * per the specification doesn't support numeric separators. Thus, the above condition will be `true` 109 * (`NaN !== anything` is always `true`) regardless of the `parseInt(str, radix)` value. 110 * Consequently, no autofixes will be made. This is correct behavior because `parseInt` also 111 * doesn't support numeric separators, but it does parse part of the string before the first `_`, 112 * so the autofix would be invalid: 113 * 114 * parseInt("1_1", 2) // === 1 115 * 0b1_1 // === 3 116 */ 117 return null; 118 } 119 120 const tokenBefore = sourceCode.getTokenBefore(node), 121 tokenAfter = sourceCode.getTokenAfter(node); 122 let prefix = "", 123 suffix = ""; 124 125 if ( 126 tokenBefore && 127 tokenBefore.range[1] === node.range[0] && 128 !astUtils.canTokensBeAdjacent(tokenBefore, replacement) 129 ) { 130 prefix = " "; 131 } 132 133 if ( 134 tokenAfter && 135 node.range[1] === tokenAfter.range[0] && 136 !astUtils.canTokensBeAdjacent(replacement, tokenAfter) 137 ) { 138 suffix = " "; 139 } 140 141 return fixer.replaceText(node, `${prefix}${replacement}${suffix}`); 142 } 143 }); 144 } 145 } 146 }; 147 } 148}; 149