1/** 2 * @fileoverview Rule to flag use of parseInt without a radix argument 3 * @author James Allardice 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Helpers 16//------------------------------------------------------------------------------ 17 18const MODE_ALWAYS = "always", 19 MODE_AS_NEEDED = "as-needed"; 20 21const validRadixValues = new Set(Array.from({ length: 37 - 2 }, (_, index) => index + 2)); 22 23/** 24 * Checks whether a given variable is shadowed or not. 25 * @param {eslint-scope.Variable} variable A variable to check. 26 * @returns {boolean} `true` if the variable is shadowed. 27 */ 28function isShadowed(variable) { 29 return variable.defs.length >= 1; 30} 31 32/** 33 * Checks whether a given node is a MemberExpression of `parseInt` method or not. 34 * @param {ASTNode} node A node to check. 35 * @returns {boolean} `true` if the node is a MemberExpression of `parseInt` 36 * method. 37 */ 38function isParseIntMethod(node) { 39 return ( 40 node.type === "MemberExpression" && 41 !node.computed && 42 node.property.type === "Identifier" && 43 node.property.name === "parseInt" 44 ); 45} 46 47/** 48 * Checks whether a given node is a valid value of radix or not. 49 * 50 * The following values are invalid. 51 * 52 * - A literal except integers between 2 and 36. 53 * - undefined. 54 * @param {ASTNode} radix A node of radix to check. 55 * @returns {boolean} `true` if the node is valid. 56 */ 57function isValidRadix(radix) { 58 return !( 59 (radix.type === "Literal" && !validRadixValues.has(radix.value)) || 60 (radix.type === "Identifier" && radix.name === "undefined") 61 ); 62} 63 64/** 65 * Checks whether a given node is a default value of radix or not. 66 * @param {ASTNode} radix A node of radix to check. 67 * @returns {boolean} `true` if the node is the literal node of `10`. 68 */ 69function isDefaultRadix(radix) { 70 return radix.type === "Literal" && radix.value === 10; 71} 72 73//------------------------------------------------------------------------------ 74// Rule Definition 75//------------------------------------------------------------------------------ 76 77module.exports = { 78 meta: { 79 type: "suggestion", 80 81 docs: { 82 description: "enforce the consistent use of the radix argument when using `parseInt()`", 83 category: "Best Practices", 84 recommended: false, 85 url: "https://eslint.org/docs/rules/radix" 86 }, 87 88 schema: [ 89 { 90 enum: ["always", "as-needed"] 91 } 92 ], 93 94 messages: { 95 missingParameters: "Missing parameters.", 96 redundantRadix: "Redundant radix parameter.", 97 missingRadix: "Missing radix parameter.", 98 invalidRadix: "Invalid radix parameter, must be an integer between 2 and 36." 99 } 100 }, 101 102 create(context) { 103 const mode = context.options[0] || MODE_ALWAYS; 104 105 /** 106 * Checks the arguments of a given CallExpression node and reports it if it 107 * offends this rule. 108 * @param {ASTNode} node A CallExpression node to check. 109 * @returns {void} 110 */ 111 function checkArguments(node) { 112 const args = node.arguments; 113 114 switch (args.length) { 115 case 0: 116 context.report({ 117 node, 118 messageId: "missingParameters" 119 }); 120 break; 121 122 case 1: 123 if (mode === MODE_ALWAYS) { 124 context.report({ 125 node, 126 messageId: "missingRadix" 127 }); 128 } 129 break; 130 131 default: 132 if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) { 133 context.report({ 134 node, 135 messageId: "redundantRadix" 136 }); 137 } else if (!isValidRadix(args[1])) { 138 context.report({ 139 node, 140 messageId: "invalidRadix" 141 }); 142 } 143 break; 144 } 145 } 146 147 return { 148 "Program:exit"() { 149 const scope = context.getScope(); 150 let variable; 151 152 // Check `parseInt()` 153 variable = astUtils.getVariableByName(scope, "parseInt"); 154 if (variable && !isShadowed(variable)) { 155 variable.references.forEach(reference => { 156 const node = reference.identifier; 157 158 if (astUtils.isCallee(node)) { 159 checkArguments(node.parent); 160 } 161 }); 162 } 163 164 // Check `Number.parseInt()` 165 variable = astUtils.getVariableByName(scope, "Number"); 166 if (variable && !isShadowed(variable)) { 167 variable.references.forEach(reference => { 168 const node = reference.identifier.parent; 169 const maybeCallee = node.parent.type === "ChainExpression" 170 ? node.parent 171 : node; 172 173 if (isParseIntMethod(node) && astUtils.isCallee(maybeCallee)) { 174 checkArguments(maybeCallee.parent); 175 } 176 }); 177 } 178 } 179 }; 180 } 181}; 182