UNPKG

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