|
1 | 1 | 'use strict'; |
2 | | -const {getParenthesizedText} = require('./utils/parentheses.js'); |
| 2 | +const {getStaticValue} = require('eslint-utils'); |
| 3 | +const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js'); |
3 | 4 | const {methodCallSelector} = require('./selectors/index.js'); |
4 | 5 | const isNumber = require('./utils/is-number.js'); |
| 6 | +const {replaceArgument} = require('./fix/index.js'); |
5 | 7 |
|
6 | 8 | const MESSAGE_ID_SUBSTR = 'substr'; |
7 | 9 | const MESSAGE_ID_SUBSTRING = 'substring'; |
@@ -37,123 +39,139 @@ const isLengthProperty = node => ( |
37 | 39 | && node.property.name === 'length' |
38 | 40 | ); |
39 | 41 |
|
40 | | -function getFixArguments(node, context) { |
| 42 | +function * fixSubstrArguments({node, fixer, context, abort}) { |
41 | 43 | const argumentNodes = node.arguments; |
| 44 | + const [firstArgument, secondArgument] = argumentNodes; |
42 | 45 |
|
43 | | - if (argumentNodes.length === 0) { |
44 | | - return []; |
| 46 | + if (!secondArgument) { |
| 47 | + return; |
45 | 48 | } |
46 | 49 |
|
| 50 | + const scope = context.getScope(); |
47 | 51 | const sourceCode = context.getSourceCode(); |
48 | | - const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined; |
49 | | - const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined; |
50 | | - |
51 | | - const method = node.callee.property.name; |
52 | | - |
53 | | - if (method === 'substr') { |
54 | | - switch (argumentNodes.length) { |
55 | | - case 1: { |
56 | | - return [firstArgument]; |
57 | | - } |
58 | | - |
59 | | - case 2: { |
60 | | - if (firstArgument === '0') { |
61 | | - const sliceCallArguments = [firstArgument]; |
62 | | - if (isLiteralNumber(secondArgument) || isLengthProperty(argumentNodes[1])) { |
63 | | - sliceCallArguments.push(secondArgument); |
64 | | - } else if (typeof getNumericValue(argumentNodes[1]) === 'number') { |
65 | | - sliceCallArguments.push(Math.max(0, getNumericValue(argumentNodes[1]))); |
66 | | - } else { |
67 | | - sliceCallArguments.push(`Math.max(0, ${secondArgument})`); |
68 | | - } |
69 | | - |
70 | | - return sliceCallArguments; |
71 | | - } |
72 | | - |
73 | | - if (argumentNodes.every(node => isLiteralNumber(node))) { |
74 | | - return [ |
75 | | - firstArgument, |
76 | | - argumentNodes[0].value + argumentNodes[1].value, |
77 | | - ]; |
78 | | - } |
| 52 | + const firstArgumentStaticResult = getStaticValue(firstArgument, scope); |
| 53 | + const secondArgumentRange = getParenthesizedRange(secondArgument, sourceCode); |
| 54 | + const replaceSecondArgument = text => replaceArgument(fixer, secondArgument, text, sourceCode); |
79 | 55 |
|
80 | | - if (argumentNodes.every(node => isNumber(node, context.getScope()))) { |
81 | | - return [firstArgument, firstArgument + ' + ' + secondArgument]; |
82 | | - } |
| 56 | + if (firstArgumentStaticResult && firstArgumentStaticResult.value === 0) { |
| 57 | + if (isLiteralNumber(secondArgument) || isLengthProperty(secondArgument)) { |
| 58 | + return; |
| 59 | + } |
83 | 60 |
|
84 | | - break; |
85 | | - } |
86 | | - // No default |
| 61 | + if (typeof getNumericValue(secondArgument) === 'number') { |
| 62 | + yield replaceSecondArgument(Math.max(0, getNumericValue(secondArgument))); |
| 63 | + return; |
87 | 64 | } |
88 | | - } else if (method === 'substring') { |
89 | | - const firstNumber = argumentNodes[0] ? getNumericValue(argumentNodes[0]) : undefined; |
90 | | - switch (argumentNodes.length) { |
91 | | - case 1: { |
92 | | - if (firstNumber !== undefined) { |
93 | | - return [Math.max(0, firstNumber)]; |
94 | | - } |
95 | 65 |
|
96 | | - if (isLengthProperty(argumentNodes[0])) { |
97 | | - return [firstArgument]; |
98 | | - } |
| 66 | + yield fixer.insertTextBeforeRange(secondArgumentRange, 'Math.max(0, '); |
| 67 | + yield fixer.insertTextAfterRange(secondArgumentRange, ')'); |
| 68 | + return; |
| 69 | + } |
99 | 70 |
|
100 | | - return [`Math.max(0, ${firstArgument})`]; |
101 | | - } |
| 71 | + if (argumentNodes.every(node => isLiteralNumber(node))) { |
| 72 | + yield replaceSecondArgument(firstArgument.value + secondArgument.value); |
| 73 | + return; |
| 74 | + } |
102 | 75 |
|
103 | | - case 2: { |
104 | | - const secondNumber = getNumericValue(argumentNodes[1]); |
| 76 | + if (argumentNodes.every(node => isNumber(node, context.getScope()))) { |
| 77 | + const firstArgumentText = getParenthesizedText(firstArgument, sourceCode); |
105 | 78 |
|
106 | | - if (firstNumber !== undefined && secondNumber !== undefined) { |
107 | | - return firstNumber > secondNumber |
108 | | - ? [Math.max(0, secondNumber), Math.max(0, firstNumber)] |
109 | | - : [Math.max(0, firstNumber), Math.max(0, secondNumber)]; |
110 | | - } |
| 79 | + yield fixer.insertTextBeforeRange(secondArgumentRange, `${firstArgumentText} + `); |
| 80 | + return; |
| 81 | + } |
111 | 82 |
|
112 | | - if (firstNumber === 0 || secondNumber === 0) { |
113 | | - return [0, `Math.max(0, ${firstNumber === 0 ? secondArgument : firstArgument})`]; |
114 | | - } |
| 83 | + return abort(); |
| 84 | +} |
| 85 | + |
| 86 | +function * fixSubstringArguments({node, fixer, context, abort}) { |
| 87 | + const sourceCode = context.getSourceCode(); |
| 88 | + const [firstArgument, secondArgument] = node.arguments; |
115 | 89 |
|
116 | | - // As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue: |
117 | | - // .substring(0, 2) and .substring(2, 0) returns the same result |
118 | | - // .slice(0, 2) and .slice(2, 0) doesn't return the same result |
119 | | - // There's also an issue with us now knowing whether the value will be negative or not, due to: |
120 | | - // .substring() treats a negative number the same as it treats a zero. |
121 | | - // The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice |
| 90 | + const firstNumber = firstArgument ? getNumericValue(firstArgument) : undefined; |
| 91 | + const firstArgumentText = getParenthesizedText(firstArgument, sourceCode); |
| 92 | + const replaceFirstArgument = text => replaceArgument(fixer, firstArgument, text, sourceCode); |
122 | 93 |
|
123 | | - break; |
124 | | - } |
125 | | - // No default |
| 94 | + if (!secondArgument) { |
| 95 | + if (isLengthProperty(firstArgument)) { |
| 96 | + return; |
126 | 97 | } |
| 98 | + |
| 99 | + if (firstNumber !== undefined) { |
| 100 | + yield replaceFirstArgument(Math.max(0, firstNumber)); |
| 101 | + return; |
| 102 | + } |
| 103 | + |
| 104 | + const firstArgumentRange = getParenthesizedRange(firstArgument, sourceCode); |
| 105 | + yield fixer.insertTextBeforeRange(firstArgumentRange, 'Math.max(0, '); |
| 106 | + yield fixer.insertTextAfterRange(firstArgumentRange, ')'); |
| 107 | + return; |
127 | 108 | } |
128 | | -} |
129 | 109 |
|
130 | | -/** @param {import('eslint').Rule.RuleContext} context */ |
131 | | -const create = context => { |
132 | | - const sourceCode = context.getSourceCode(); |
| 110 | + const secondNumber = getNumericValue(secondArgument); |
| 111 | + const secondArgumentText = getParenthesizedText(secondArgument, sourceCode); |
| 112 | + const replaceSecondArgument = text => replaceArgument(fixer, secondArgument, text, sourceCode); |
| 113 | + |
| 114 | + if (firstNumber !== undefined && secondNumber !== undefined) { |
| 115 | + const argumentsValue = [Math.max(0, firstNumber), Math.max(0, secondNumber)]; |
| 116 | + if (firstNumber > secondNumber) { |
| 117 | + argumentsValue.reverse(); |
| 118 | + } |
| 119 | + |
| 120 | + if (argumentsValue[0] !== firstNumber) { |
| 121 | + yield replaceFirstArgument(argumentsValue[0]); |
| 122 | + } |
133 | 123 |
|
134 | | - return { |
135 | | - [selector](node) { |
136 | | - const problem = { |
137 | | - node, |
138 | | - messageId: node.callee.property.name, |
139 | | - }; |
| 124 | + if (argumentsValue[1] !== secondNumber) { |
| 125 | + yield replaceSecondArgument(argumentsValue[1]); |
| 126 | + } |
140 | 127 |
|
141 | | - const sliceCallArguments = getFixArguments(node, context); |
142 | | - if (!sliceCallArguments) { |
143 | | - return problem; |
144 | | - } |
| 128 | + return; |
| 129 | + } |
145 | 130 |
|
146 | | - const objectNode = node.callee.object; |
147 | | - const objectText = getParenthesizedText(objectNode, sourceCode); |
148 | | - const optionalMemberSuffix = node.callee.optional ? '?' : ''; |
149 | | - const optionalCallSuffix = node.optional ? '?.' : ''; |
| 131 | + if (firstNumber === 0 || secondNumber === 0) { |
| 132 | + yield replaceFirstArgument(0); |
| 133 | + yield replaceSecondArgument(`Math.max(0, ${firstNumber === 0 ? secondArgumentText : firstArgumentText})`); |
| 134 | + return; |
| 135 | + } |
150 | 136 |
|
151 | | - problem.fix = fixer => fixer.replaceText(node, `${objectText}${optionalMemberSuffix}.slice${optionalCallSuffix}(${sliceCallArguments.join(', ')})`); |
| 137 | + // As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue: |
| 138 | + // .substring(0, 2) and .substring(2, 0) returns the same result |
| 139 | + // .slice(0, 2) and .slice(2, 0) doesn't return the same result |
| 140 | + // There's also an issue with us now knowing whether the value will be negative or not, due to: |
| 141 | + // .substring() treats a negative number the same as it treats a zero. |
| 142 | + // The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice |
152 | 143 |
|
153 | | - return problem; |
154 | | - }, |
155 | | - }; |
156 | | -}; |
| 144 | + return abort(); |
| 145 | +} |
| 146 | + |
| 147 | +/** @param {import('eslint').Rule.RuleContext} context */ |
| 148 | +const create = context => ({ |
| 149 | + [selector](node) { |
| 150 | + const method = node.callee.property.name; |
| 151 | + |
| 152 | + return { |
| 153 | + node, |
| 154 | + messageId: method, |
| 155 | + * fix(fixer, {abort}) { |
| 156 | + yield fixer.replaceText(node.callee.property, 'slice'); |
| 157 | + |
| 158 | + if (node.arguments.length === 0) { |
| 159 | + return; |
| 160 | + } |
| 161 | + |
| 162 | + if ( |
| 163 | + node.arguments.length > 2 |
| 164 | + || node.arguments.some(node => node.type === 'SpreadElement') |
| 165 | + ) { |
| 166 | + return abort(); |
| 167 | + } |
| 168 | + |
| 169 | + const fixArguments = method === 'substr' ? fixSubstrArguments : fixSubstringArguments; |
| 170 | + yield * fixArguments({node, fixer, context, abort}); |
| 171 | + }, |
| 172 | + }; |
| 173 | + }, |
| 174 | +}); |
157 | 175 |
|
158 | 176 | /** @type {import('eslint').Rule.RuleModule} */ |
159 | 177 | module.exports = { |
|
0 commit comments