Skip to content

Commit 36e7103

Browse files
fiskersindresorhus
andauthored
Add array-join-separator rule (#1284)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent a1c8984 commit 36e7103

File tree

7 files changed

+413
-0
lines changed

7 files changed

+413
-0
lines changed

docs/rules/array-join-separator.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Enforce using the separator argument with `Array#join()`
2+
3+
It's better to make it clear what the separator is when calling [Array#join()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join).
4+
5+
This rule is fixable.
6+
7+
## Fail
8+
9+
```js
10+
const string = array.join();
11+
```
12+
13+
```js
14+
const string = Array.prototype.join.call(arrayLike);
15+
```
16+
17+
```js
18+
const string = [].join.call(arrayLike);
19+
```
20+
21+
## Pass
22+
23+
```js
24+
const string = array.join(',');
25+
```
26+
27+
```js
28+
const string = array.join('|');
29+
```
30+
31+
```js
32+
const string = Array.prototype.join.call(arrayLike, '');
33+
```
34+
35+
```js
36+
const string = [].join.call(arrayLike, '\n');
37+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module.exports = {
3939
'unicorn'
4040
],
4141
rules: {
42+
'unicorn/array-join-separator': 'error',
4243
'unicorn/better-regex': 'error',
4344
'unicorn/catch-error-name': 'error',
4445
'unicorn/consistent-destructuring': 'error',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Configure it in `package.json`.
3535
"unicorn"
3636
],
3737
"rules": {
38+
"unicorn/array-join-separator": "error",
3839
"unicorn/better-regex": "error",
3940
"unicorn/catch-error-name": "error",
4041
"unicorn/consistent-destructuring": "error",
@@ -132,6 +133,7 @@ Each rule has emojis denoting:
132133

133134
| Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Description || 🔧 |
134135
| :-- | :-- | :-- | :-- |
136+
| [array-join-separator](docs/rules/array-join-separator.md) | Enforce using the separator argument with `Array#join()`. || 🔧 |
135137
| [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. || 🔧 |
136138
| [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. || 🔧 |
137139
| [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. || 🔧 |

rules/array-join-separator.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use strict';
2+
const {isCommaToken} = require('eslint-utils');
3+
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
const methodSelector = require('./utils/method-selector');
5+
6+
const MESSAGE_ID = 'array-join-separator';
7+
const messages = {
8+
[MESSAGE_ID]: 'Missing the separator argument.'
9+
};
10+
11+
const emptyArraySelector = path => {
12+
const prefix = `${path}.`;
13+
return [
14+
`[${prefix}type="ArrayExpression"]`,
15+
`[${prefix}elements.length=0]`
16+
].join('');
17+
};
18+
19+
const memberExpressionSelector = (path, {property, object}) => {
20+
const prefix = `${path}.`;
21+
22+
const parts = [
23+
`[${prefix}type="MemberExpression"]`,
24+
`[${prefix}computed=false]`,
25+
`[${prefix}optional!=true]`,
26+
`[${prefix}property.type="Identifier"]`,
27+
`[${prefix}property.name="${property}"]`
28+
];
29+
30+
if (object) {
31+
parts.push(
32+
`[${prefix}object.type="Identifier"]`,
33+
`[${prefix}object.name="${object}"]`
34+
);
35+
}
36+
37+
return parts.join('');
38+
};
39+
40+
// `foo.join()`
41+
const arrayJoin = methodSelector({
42+
name: 'join',
43+
length: 0
44+
});
45+
46+
// `[].join.call(foo)` and `Array.prototype.join.call(foo)`
47+
const arrayPrototypeJoin = [
48+
methodSelector({
49+
name: 'call',
50+
length: 1
51+
}),
52+
memberExpressionSelector('callee.object', {property: 'join'}),
53+
`:matches(${
54+
[
55+
emptyArraySelector('callee.object.object'),
56+
memberExpressionSelector('callee.object.object', {property: 'prototype', object: 'Array'})
57+
].join(', ')
58+
})`
59+
].join('');
60+
61+
const selector = `:matches(${arrayJoin}, ${arrayPrototypeJoin})`;
62+
63+
/** @param {import('eslint').Rule.RuleContext} context */
64+
const create = context => {
65+
return {
66+
[selector](node) {
67+
const [penultimateToken, lastToken] = context.getSourceCode().getLastTokens(node, 2);
68+
const isPrototypeMethod = node.arguments.length === 1;
69+
context.report({
70+
loc: {
71+
start: penultimateToken.loc[isPrototypeMethod ? 'end' : 'start'],
72+
end: lastToken.loc.end
73+
},
74+
messageId: MESSAGE_ID,
75+
/** @param {import('eslint').Rule.RuleFixer} fixer */
76+
fix(fixer) {
77+
let text = '\',\'';
78+
79+
if (isPrototypeMethod) {
80+
text = isCommaToken(penultimateToken) ? `${text},` : `, ${text}`;
81+
}
82+
83+
return fixer.insertTextBefore(lastToken, text);
84+
}
85+
});
86+
}
87+
};
88+
};
89+
90+
const schema = [];
91+
92+
module.exports = {
93+
create,
94+
meta: {
95+
type: 'suggestion',
96+
docs: {
97+
description: 'Enforce using the separator argument with `Array#join()`.',
98+
url: getDocumentationUrl(__filename)
99+
},
100+
fixable: 'code',
101+
schema,
102+
messages
103+
}
104+
};

test/array-join-separator.mjs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import outdent from 'outdent';
2+
import {getTester} from './utils/test.mjs';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
valid: [
8+
'foo.join(",")',
9+
'join()',
10+
'foo.join(...[])',
11+
'foo?.join()',
12+
'foo[join]()',
13+
'foo["join"]()',
14+
'[].join.call(foo, ",")',
15+
'[].join.call()',
16+
'[].join.call(...[foo])',
17+
'[].join?.call(foo)',
18+
'[]?.join.call(foo)',
19+
'[].join[call](foo)',
20+
'[][join].call(foo)',
21+
'[,].join.call(foo)',
22+
'[].join.notCall(foo)',
23+
'[].notJoin.call(foo)',
24+
'Array.prototype.join.call(foo, "")',
25+
'Array.prototype.join.call()',
26+
'Array.prototype.join.call(...[foo])',
27+
'Array.prototype.join?.call(foo)',
28+
'Array.prototype?.join.call(foo)',
29+
'Array?.prototype.join.call(foo)',
30+
'Array.prototype.join[call](foo, "")',
31+
'Array.prototype[join].call(foo)',
32+
'Array[prototype].join.call(foo)',
33+
'Array.prototype.join.notCall(foo)',
34+
'Array.prototype.notJoin.call(foo)',
35+
'Array.notPrototype.join.call(foo)',
36+
'NotArray.prototype.join.call(foo)',
37+
'path.join(__dirname, "./foo.js")'
38+
],
39+
invalid: [
40+
'foo.join()',
41+
'[].join.call(foo)',
42+
'[].join.call(foo,)',
43+
'[].join.call(foo , );',
44+
'Array.prototype.join.call(foo)',
45+
'Array.prototype.join.call(foo, )',
46+
outdent`
47+
(
48+
/**/
49+
[
50+
/**/
51+
]
52+
/**/
53+
.
54+
/**/
55+
join
56+
/**/
57+
.
58+
/**/
59+
call
60+
/**/
61+
(
62+
/**/
63+
(
64+
/**/
65+
foo
66+
/**/
67+
)
68+
/**/
69+
,
70+
/**/
71+
)/**/
72+
)
73+
`
74+
]
75+
});

0 commit comments

Comments
 (0)