UNPKG

5.37 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 slice;
67
68 if (argumentNodes.length === 0) {
69 slice = [];
70 } else if (argumentNodes.length === 1) {
71 slice = [firstArgument];
72 } else if (argumentNodes.length === 2) {
73 if (firstArgument === '0') {
74 slice = [firstArgument, secondArgument];
75 } else if (
76 isLiteralNumber(argumentNodes[0]) &&
77 isLiteralNumber(argumentNodes[1])
78 ) {
79 slice = [
80 firstArgument,
81 argumentNodes[0].value + argumentNodes[1].value
82 ];
83 } else if (
84 isLikelyNumeric(argumentNodes[0]) &&
85 isLikelyNumeric(argumentNodes[1])
86 ) {
87 slice = [firstArgument, firstArgument + ' + ' + secondArgument];
88 }
89 }
90
91 if (slice) {
92 const objectText = getNodeText(objectNode);
93
94 problem.fix = fixer => fixer.replaceText(node, `${objectText}.slice(${slice.join(', ')})`);
95 }
96
97 context.report(problem);
98 },
99
100 [substringCallTemplate](node) {
101 const objectNode = substringCallTemplate.context.getMatch(objectVariable);
102 const argumentNodes = substringCallTemplate.context.getMatch(argumentsVariable);
103
104 const problem = {
105 node,
106 message: 'Prefer `String#slice()` over `String#substring()`.'
107 };
108
109 const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined;
110 const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined;
111
112 const firstNumber = argumentNodes[0] ? getNumericValue(argumentNodes[0]) : undefined;
113
114 let slice;
115
116 if (argumentNodes.length === 0) {
117 slice = [];
118 } else if (argumentNodes.length === 1) {
119 if (firstNumber !== undefined) {
120 slice = [Math.max(0, firstNumber)];
121 } else if (isLengthProperty(argumentNodes[0])) {
122 slice = [firstArgument];
123 } else {
124 slice = [`Math.max(0, ${firstArgument})`];
125 }
126 } else if (argumentNodes.length === 2) {
127 const secondNumber = argumentNodes[1] ? getNumericValue(argumentNodes[1]) : undefined;
128
129 if (firstNumber !== undefined && secondNumber !== undefined) {
130 slice = firstNumber > secondNumber ?
131 [Math.max(0, secondNumber), Math.max(0, firstNumber)] :
132 [Math.max(0, firstNumber), Math.max(0, secondNumber)];
133 } else if (firstNumber === 0 || secondNumber === 0) {
134 slice = [0, `Math.max(0, ${firstNumber === 0 ? secondArgument : firstArgument})`];
135 } else {
136 // As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue:
137 // .substring(0, 2) and .substring(2, 0) returns the same result
138 // .slice(0, 2) and .slice(2, 0) doesn't return the same result
139 // There's also an issue with us now knowing whether the value will be negative or not, due to:
140 // .substring() treats a negative number the same as it treats a zero.
141 // The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice
142 }
143 }
144
145 if (slice) {
146 const objectText = getNodeText(objectNode);
147 problem.fix = fixer => fixer.replaceText(node, `${objectText}.slice(${slice.join(', ')})`);
148 }
149
150 context.report(problem);
151 }
152 });
153};
154
155module.exports = {
156 create,
157 meta: {
158 type: 'suggestion',
159 docs: {
160 url: getDocumentationUrl(__filename)
161 },
162 fixable: 'code'
163 }
164};