UNPKG

5.83 kBJavaScriptView Raw
1'use strict';
2const eslintTemplateVisitor = require('eslint-template-visitor');
3const getDocumentationUrl = require('./utils/get-documentation-url');
4
5const templates = eslintTemplateVisitor();
6
7const objectVariable = templates.variable();
8const argumentsVariable = templates.spreadVariable();
9
10const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`;
11const substringCallTemplate = templates.template`${objectVariable}.substring(${argumentsVariable})`;
12
13const isLiteralNumber = node => node && node.type === 'Literal' && typeof node.value === 'number';
14
15const getNumericValue = node => {
16 if (isLiteralNumber(node)) {
17 return node.value;
18 }
19
20 if (node.type === 'UnaryExpression' && node.operator === '-') {
21 return -getNumericValue(node.argument);
22 }
23};
24
25// This handles cases where the argument is very likely to be a number, such as `.substring('foo'.length)`.
26const isLengthProperty = node => (
27 node &&
28 node.type === 'MemberExpression' &&
29 node.computed === false &&
30 node.property.type === 'Identifier' &&
31 node.property.name === 'length'
32);
33
34const isLikelyNumeric = node => isLiteralNumber(node) || isLengthProperty(node);
35
36const create = context => {
37 const sourceCode = context.getSourceCode();
38
39 const getNodeText = node => {
40 const text = sourceCode.getText(node);
41 const before = sourceCode.getTokenBefore(node);
42 const after = sourceCode.getTokenAfter(node);
43 if (
44 (before && before.type === 'Punctuator' && before.value === '(') &&
45 (after && after.type === 'Punctuator' && after.value === ')')
46 ) {
47 return `(${text})`;
48 }
49
50 return text;
51 };
52
53 return templates.visitor({
54 [substrCallTemplate](node) {
55 const objectNode = substrCallTemplate.context.getMatch(objectVariable);
56 const argumentNodes = substrCallTemplate.context.getMatch(argumentsVariable);
57
58 const problem = {
59 node,
60 message: 'Prefer `String#slice()` over `String#substr()`.'
61 };
62
63 const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined;
64 const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined;
65
66 let sliceArguments;
67
68 if (argumentNodes.length === 0) {
69 sliceArguments = [];
70 } else if (argumentNodes.length === 1) {
71 sliceArguments = [firstArgument];
72 } else if (argumentNodes.length === 2) {
73 if (firstArgument === '0') {
74 sliceArguments = [firstArgument];
75 if (isLiteralNumber(secondArgument) || isLengthProperty(argumentNodes[1])) {
76 sliceArguments.push(secondArgument);
77 } else if (typeof getNumericValue(argumentNodes[1]) === 'number') {
78 sliceArguments.push(Math.max(0, getNumericValue(argumentNodes[1])));
79 } else {
80 sliceArguments.push(`Math.max(0, ${secondArgument})`);
81 }
82 } else if (
83 isLiteralNumber(argumentNodes[0]) &&
84 isLiteralNumber(argumentNodes[1])
85 ) {
86 sliceArguments = [
87 firstArgument,
88 argumentNodes[0].value + argumentNodes[1].value
89 ];
90 } else if (
91 isLikelyNumeric(argumentNodes[0]) &&
92 isLikelyNumeric(argumentNodes[1])
93 ) {
94 sliceArguments = [firstArgument, firstArgument + ' + ' + secondArgument];
95 }
96 }
97
98 if (sliceArguments) {
99 const objectText = getNodeText(objectNode);
100
101 problem.fix = fixer => fixer.replaceText(node, `${objectText}.slice(${sliceArguments.join(', ')})`);
102 }
103
104 context.report(problem);
105 },
106
107 [substringCallTemplate](node) {
108 const objectNode = substringCallTemplate.context.getMatch(objectVariable);
109 const argumentNodes = substringCallTemplate.context.getMatch(argumentsVariable);
110
111 const problem = {
112 node,
113 message: 'Prefer `String#slice()` over `String#substring()`.'
114 };
115
116 const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined;
117 const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined;
118
119 const firstNumber = argumentNodes[0] ? getNumericValue(argumentNodes[0]) : undefined;
120
121 let sliceArguments;
122
123 if (argumentNodes.length === 0) {
124 sliceArguments = [];
125 } else if (argumentNodes.length === 1) {
126 if (firstNumber !== undefined) {
127 sliceArguments = [Math.max(0, firstNumber)];
128 } else if (isLengthProperty(argumentNodes[0])) {
129 sliceArguments = [firstArgument];
130 } else {
131 sliceArguments = [`Math.max(0, ${firstArgument})`];
132 }
133 } else if (argumentNodes.length === 2) {
134 const secondNumber = getNumericValue(argumentNodes[1]);
135
136 if (firstNumber !== undefined && secondNumber !== undefined) {
137 sliceArguments = firstNumber > secondNumber ?
138 [Math.max(0, secondNumber), Math.max(0, firstNumber)] :
139 [Math.max(0, firstNumber), Math.max(0, secondNumber)];
140 } else if (firstNumber === 0 || secondNumber === 0) {
141 sliceArguments = [0, `Math.max(0, ${firstNumber === 0 ? secondArgument : firstArgument})`];
142 } else {
143 // As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue:
144 // .substring(0, 2) and .substring(2, 0) returns the same result
145 // .slice(0, 2) and .slice(2, 0) doesn't return the same result
146 // There's also an issue with us now knowing whether the value will be negative or not, due to:
147 // .substring() treats a negative number the same as it treats a zero.
148 // The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice
149 }
150 }
151
152 if (sliceArguments) {
153 const objectText = getNodeText(objectNode);
154 problem.fix = fixer => fixer.replaceText(node, `${objectText}.slice(${sliceArguments.join(', ')})`);
155 }
156
157 context.report(problem);
158 }
159 });
160};
161
162module.exports = {
163 create,
164 meta: {
165 type: 'suggestion',
166 docs: {
167 url: getDocumentationUrl(__filename)
168 },
169 fixable: 'code'
170 }
171};