forked from sindresorhus/eslint-plugin-unicorn
/
prefer-starts-ends-with.js
124 lines (106 loc) · 2.94 KB
/
prefer-starts-ends-with.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
'use strict';
const {isParenthesized} = require('eslint-utils');
const getDocumentationUrl = require('./utils/get-documentation-url');
const methodSelector = require('./utils/method-selector');
const quoteString = require('./utils/quote-string');
const MESSAGE_STARTS_WITH = 'prefer-starts-with';
const MESSAGE_ENDS_WITH = 'prefer-ends-with';
const doesNotContain = (string, characters) => characters.every(character => !string.includes(character));
const isSimpleString = string => doesNotContain(
string,
['^', '$', '+', '[', '{', '(', '\\', '.', '?', '*', '|']
);
const regexTestSelector = [
methodSelector({name: 'test', length: 1}),
'[callee.object.regex]'
].join('');
const stringMatchSelector = [
methodSelector({name: 'match', length: 1}),
'[arguments.0.regex]'
].join('');
const checkRegex = ({pattern, flags}) => {
if (flags.includes('i') || flags.includes('m')) {
return;
}
if (pattern.startsWith('^')) {
const string = pattern.slice(1);
if (isSimpleString(string)) {
return {
messageId: MESSAGE_STARTS_WITH,
string
};
}
}
if (pattern.endsWith('$')) {
const string = pattern.slice(0, -1);
if (isSimpleString(string)) {
return {
messageId: MESSAGE_ENDS_WITH,
string
};
}
}
};
const create = context => {
const sourceCode = context.getSourceCode();
return {
[regexTestSelector](node) {
const regexNode = node.callee.object;
const {regex} = regexNode;
const result = checkRegex(regex);
if (!result) {
return;
}
context.report({
node,
messageId: result.messageId,
fix: fixer => {
const method = result.messageId === MESSAGE_STARTS_WITH ? 'startsWith' : 'endsWith';
const [target] = node.arguments;
let targetString = sourceCode.getText(target);
if (
// If regex is parenthesized, we can use it, so we don't need add again
!isParenthesized(regexNode, sourceCode) &&
(isParenthesized(target, sourceCode) || target.type === 'AwaitExpression')
) {
targetString = `(${targetString})`;
}
// The regex literal always starts with `/` or `(`, so we don't need check ASI
return [
// Replace regex with string
fixer.replaceText(regexNode, targetString),
// `.test` => `.startsWith` / `.endsWith`
fixer.replaceText(node.callee.property, method),
// Replace argument with result.string
fixer.replaceText(target, quoteString(result.string))
];
}
});
},
[stringMatchSelector](node) {
const {regex} = node.arguments[0];
const result = checkRegex(regex);
if (!result) {
return;
}
context.report({
node,
messageId: result.messageId
});
}
};
};
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
messages: {
[MESSAGE_STARTS_WITH]: 'Prefer `String#startsWith()` over a regex with `^`.',
[MESSAGE_ENDS_WITH]: 'Prefer `String#endsWith()` over a regex with `$`.'
},
fixable: 'code'
}
};