1 | 'use strict';
|
2 | const eslintTemplateVisitor = require('eslint-template-visitor');
|
3 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
4 |
|
5 | const templates = eslintTemplateVisitor();
|
6 |
|
7 | const objectVariable = templates.variable();
|
8 | const argumentsVariable = templates.spreadVariable();
|
9 |
|
10 | const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`;
|
11 | const substringCallTemplate = templates.template`${objectVariable}.substring(${argumentsVariable})`;
|
12 |
|
13 | const isLiteralNumber = node => node && node.type === 'Literal' && typeof node.value === 'number';
|
14 |
|
15 | const 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 |
|
26 | const isLengthProperty = node => (
|
27 | node &&
|
28 | node.type === 'MemberExpression' &&
|
29 | node.computed === false &&
|
30 | node.property.type === 'Identifier' &&
|
31 | node.property.name === 'length'
|
32 | );
|
33 |
|
34 | const isLikelyNumeric = node => isLiteralNumber(node) || isLengthProperty(node);
|
35 |
|
36 | const 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 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
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 |
|
155 | module.exports = {
|
156 | create,
|
157 | meta: {
|
158 | type: 'suggestion',
|
159 | docs: {
|
160 | url: getDocumentationUrl(__filename)
|
161 | },
|
162 | fixable: 'code'
|
163 | }
|
164 | };
|