Skip to content

Commit 38d68f3

Browse files
angleesindresorhus
authored andcommitted
Add prefer-starts-ends-with rule - fixes #13 (#64)
1 parent 9357b57 commit 38d68f3

File tree

5 files changed

+134
-2
lines changed

5 files changed

+134
-2
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Prefer `String#startsWidth` & `String#endsWidth` over more complex alternatives
2+
3+
There are several ways of checking whether a string starts or ends with a certain string, such as `string.indexOf('foo') === 0` or using regexes with `/^foo/` or `/foo$/`. ES2015 introduced simpler alternatives named [`String#startsWith`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith) and [`String#endsWith`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith). This rule enforces the use of those whenever possible.
4+
5+
6+
## Fail
7+
8+
```js
9+
/^bar/.test(foo);
10+
/bar$/.test(foo);
11+
```
12+
13+
14+
## Pass
15+
16+
```js
17+
foo.startsWith('bar');
18+
foo.endsWith('bar');
19+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ module.exports = {
2424
'unicorn/no-array-instanceof': 'error',
2525
'unicorn/no-new-buffer': 'error',
2626
'unicorn/no-hex-escape': 'error',
27-
'unicorn/custom-error-definition': 'error'
27+
'unicorn/custom-error-definition': 'error',
28+
'unicorn/prefer-starts-ends-with': 'error'
2829
}
2930
}
3031
}

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ Configure it in `package.json`.
4444
"unicorn/no-array-instanceof": "error",
4545
"unicorn/no-new-buffer": "error",
4646
"unicorn/no-hex-escape": "error",
47-
"unicorn/custom-error-definition": "error"
47+
"unicorn/custom-error-definition": "error",
48+
"unicorn/prefer-starts-ends-with": "error"
4849
}
4950
}
5051
}
@@ -65,6 +66,7 @@ Configure it in `package.json`.
6566
- [no-new-buffer](docs/rules/no-new-buffer.md) - Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. *(fixable)*
6667
- [no-hex-escape](docs/rules/no-hex-escape.md) - Enforce the use of unicode escapes instead of hexadecimal escapes. *(fixable)*
6768
- [custom-error-definition](docs/rules/custom-error-definition.md) - Enforces the only valid way of `Error` subclassing. *(fixable)*
69+
- [prefer-starts-ends-with](docs/rules/prefer-starts-ends-with.md) - Prefer `String#startsWidth` & `String#endsWidth` over more complex alternatives.
6870

6971

7072
## Recommended config

rules/prefer-starts-ends-with.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
const doesNotContain = (string, chars) => chars.every(char => !string.includes(char));
3+
4+
const isSimpleString = string => doesNotContain(
5+
string,
6+
['^', '$', '+', '[', '{', '(', '\\', '.', '?', '*']
7+
);
8+
9+
const create = context => {
10+
return {
11+
CallExpression(node) {
12+
const callee = node.callee;
13+
const args = node.arguments;
14+
15+
let regex;
16+
if (callee.property.name === 'test' && callee.object.regex) {
17+
regex = callee.object.regex;
18+
} else if (callee.property.name === 'match' && args && args[0] && args[0].regex) {
19+
regex = args[0].regex;
20+
} else {
21+
return;
22+
}
23+
24+
if (regex.flags && regex.flags.includes('i')) {
25+
return;
26+
}
27+
28+
const pattern = regex.pattern;
29+
if (pattern.startsWith('^') && isSimpleString(pattern.slice(1))) {
30+
context.report({
31+
node,
32+
message: 'Prefer `String#startsWith` over a regex with `^`.'
33+
});
34+
} else if (pattern.endsWith('$') && isSimpleString(pattern.slice(0, -1))) {
35+
context.report({
36+
node,
37+
message: 'Prefer `String#endsWith` over a regex with `$`.'
38+
});
39+
}
40+
}
41+
};
42+
};
43+
44+
module.exports = {
45+
create,
46+
meta: {}
47+
};
48+

test/prefer-starts-ends-with.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import test from 'ava';
2+
import avaRuleTester from 'eslint-ava-rule-tester';
3+
import rule from '../rules/prefer-starts-ends-with';
4+
5+
const ruleTester = avaRuleTester(test, {
6+
env: {
7+
es6: true
8+
}
9+
});
10+
11+
const errors = {
12+
startsWith: [{
13+
ruleId: 'prefer-starts-ends-with',
14+
message: 'Prefer `String#startsWith` over a regex with `^`.'
15+
}],
16+
endsWith: [{
17+
ruleId: 'prefer-starts-ends-with',
18+
message: 'Prefer `String#endsWith` over a regex with `$`.'
19+
}]
20+
};
21+
22+
const validRegex = [
23+
/foo/,
24+
/^foo$/,
25+
/^foo+/,
26+
/foo+$/,
27+
/^[f,a]/,
28+
/[f,a]$/,
29+
/^\w/,
30+
/\w$/,
31+
/^foo./,
32+
/foo.$/,
33+
/\^foo/,
34+
/^foo/i
35+
];
36+
37+
const invalidRegex = [
38+
/^foo/,
39+
/foo$/,
40+
/^!/,
41+
/!$/,
42+
/^ /,
43+
/ $/
44+
];
45+
46+
ruleTester.run('prefer-starts-ends-with', rule, {
47+
valid: [
48+
'foo.startsWith("bar")',
49+
'foo.endsWith("bar")'
50+
]
51+
.concat(validRegex.map(re => `${re}.test(bar)`))
52+
.concat(validRegex.map(re => `bar.match(${re})`)),
53+
invalid: []
54+
.concat(invalidRegex.map(re => ({
55+
code: `${re}.test(bar)`,
56+
errors: errors[`${re}`.startsWith('/^') ? 'startsWith' : 'endsWith']
57+
})))
58+
.concat(invalidRegex.map(re => ({
59+
code: `bar.match(${re})`,
60+
errors: errors[`${re}`.startsWith('/^') ? 'startsWith' : 'endsWith']
61+
})))
62+
});

0 commit comments

Comments
 (0)