1 | 'use strict';
|
2 | const {isParenthesized} = require('eslint-utils');
|
3 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
4 | const methodSelector = require('./utils/method-selector');
|
5 | const quoteString = require('./utils/quote-string');
|
6 |
|
7 | const MESSAGE_STARTS_WITH = 'prefer-starts-with';
|
8 | const MESSAGE_ENDS_WITH = 'prefer-ends-with';
|
9 |
|
10 | const doesNotContain = (string, characters) => characters.every(character => !string.includes(character));
|
11 |
|
12 | const isSimpleString = string => doesNotContain(
|
13 | string,
|
14 | ['^', '$', '+', '[', '{', '(', '\\', '.', '?', '*', '|']
|
15 | );
|
16 |
|
17 | const regexTestSelector = [
|
18 | methodSelector({name: 'test', length: 1}),
|
19 | '[callee.object.regex]'
|
20 | ].join('');
|
21 |
|
22 | const stringMatchSelector = [
|
23 | methodSelector({name: 'match', length: 1}),
|
24 | '[arguments.0.regex]'
|
25 | ].join('');
|
26 |
|
27 | const 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 |
|
55 | const 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 |
|
77 | !isParenthesized(regexNode, sourceCode) &&
|
78 | (isParenthesized(target, sourceCode) || target.type === 'AwaitExpression')
|
79 | ) {
|
80 | targetString = `(${targetString})`;
|
81 | }
|
82 |
|
83 |
|
84 |
|
85 | return [
|
86 |
|
87 | fixer.replaceText(regexNode, targetString),
|
88 |
|
89 | fixer.replaceText(node.callee.property, method),
|
90 |
|
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 |
|
111 | module.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 | };
|