eslint-plugin-unicorn 50.0.0 → 51.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  module.exports = {
3
3
  'unicorn/better-regex': 'error',
4
4
  'unicorn/catch-error-name': 'error',
5
- 'unicorn/consistent-destructuring': 'error',
5
+ 'unicorn/consistent-destructuring': 'off',
6
6
  'unicorn/consistent-function-scoping': 'error',
7
7
  'unicorn/custom-error-definition': 'off',
8
8
  'unicorn/empty-brace-spaces': 'error',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-unicorn",
3
- "version": "50.0.0",
3
+ "version": "51.0.0",
4
4
  "description": "More than 100 powerful ESLint rules",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/eslint-plugin-unicorn",
@@ -77,7 +77,7 @@
77
77
  "chalk": "^5.3.0",
78
78
  "enquirer": "^2.4.1",
79
79
  "eslint": "^8.56.0",
80
- "eslint-ava-rule-tester": "^4.2.0",
80
+ "eslint-ava-rule-tester": "^5.0.1",
81
81
  "eslint-doc-generator": "^1.6.1",
82
82
  "eslint-plugin-eslint-plugin": "^5.2.1",
83
83
  "eslint-plugin-internal-rules": "file:./scripts/internal-rules/",
@@ -91,6 +91,7 @@
91
91
  "npm-package-json-lint": "^7.1.0",
92
92
  "npm-run-all2": "^6.1.1",
93
93
  "outdent": "^0.8.0",
94
+ "pretty-ms": "^8.0.0",
94
95
  "typescript": "^5.3.3",
95
96
  "vue-eslint-parser": "^9.3.2",
96
97
  "xo": "^0.56.0",
package/readme.md CHANGED
@@ -112,7 +112,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
112
112
  | :----------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
113
113
  | [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. | ✅ | 🔧 | |
114
114
  | [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. | ✅ | 🔧 | |
115
- | [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. || 🔧 | 💡 |
115
+ | [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | | 🔧 | 💡 |
116
116
  | [consistent-function-scoping](docs/rules/consistent-function-scoping.md) | Move function definitions to the highest possible scope. | ✅ | | |
117
117
  | [custom-error-definition](docs/rules/custom-error-definition.md) | Enforce correct `Error` subclassing. | | 🔧 | |
118
118
  | [empty-brace-spaces](docs/rules/empty-brace-spaces.md) | Enforce no spaces between braces. | ✅ | 🔧 | |
@@ -244,7 +244,7 @@ import eslintPluginUnicorn from 'eslint-plugin-unicorn';
244
244
 
245
245
  export default [
246
246
  // …
247
- eslintPluginUnicorn.config['flat/recommended'],
247
+ eslintPluginUnicorn.configs['flat/recommended'],
248
248
  {
249
249
  rules: {
250
250
  'unicorn/better-regex': 'warn',
@@ -261,7 +261,7 @@ const eslintPluginUnicorn = require('eslint-plugin-unicorn');
261
261
 
262
262
  module.exports = [
263
263
  // …
264
- eslintPluginUnicorn.config['flat/recommended'],
264
+ eslintPluginUnicorn.configs['flat/recommended'],
265
265
  {
266
266
  rules: {
267
267
  'unicorn/better-regex': 'warn',
@@ -281,7 +281,7 @@ import eslintPluginUnicorn from 'eslint-plugin-unicorn';
281
281
 
282
282
  export default [
283
283
  // …
284
- eslintPluginUnicorn.config['flat/all'],
284
+ eslintPluginUnicorn.configs['flat/all'],
285
285
  {
286
286
  rules: {
287
287
  'unicorn/better-regex': 'warn',
@@ -298,7 +298,7 @@ const eslintPluginUnicorn = require('eslint-plugin-unicorn');
298
298
 
299
299
  module.exports = [
300
300
  // …
301
- eslintPluginUnicorn.config['flat/all'],
301
+ eslintPluginUnicorn.configs['flat/all'],
302
302
  {
303
303
  rules: {
304
304
  'unicorn/better-regex': 'warn',
@@ -7,12 +7,14 @@ const {isNodeMatches} = require('./utils/is-node-matches.js');
7
7
  const {isNodeValueNotFunction} = require('./utils/index.js');
8
8
  const {isMethodCall} = require('./ast/index.js');
9
9
 
10
- const ERROR = 'error';
10
+ const ERROR_PROTOTYPE_METHOD = 'error-prototype-method';
11
+ const ERROR_STATIC_METHOD = 'error-static-method';
11
12
  const SUGGESTION_BIND = 'suggestion-bind';
12
13
  const SUGGESTION_REMOVE = 'suggestion-remove';
13
14
  const messages = {
14
- [ERROR]: 'Do not use the `this` argument in `Array#{{method}}()`.',
15
- [SUGGESTION_REMOVE]: 'Remove the second argument.',
15
+ [ERROR_PROTOTYPE_METHOD]: 'Do not use the `this` argument in `Array#{{method}}()`.',
16
+ [ERROR_STATIC_METHOD]: 'Do not use the `this` argument in `Array.{{method}}()`.',
17
+ [SUGGESTION_REMOVE]: 'Remove this argument.',
16
18
  [SUGGESTION_BIND]: 'Use a bound function.',
17
19
  };
18
20
 
@@ -70,107 +72,141 @@ const ignored = [
70
72
  'underscore.some',
71
73
  ];
72
74
 
73
- function removeThisArgument(callExpression, sourceCode) {
74
- return fixer => removeArgument(fixer, callExpression.arguments[1], sourceCode);
75
+ function removeThisArgument(thisArgumentNode, sourceCode) {
76
+ return fixer => removeArgument(fixer, thisArgumentNode, sourceCode);
75
77
  }
76
78
 
77
- function useBoundFunction(callExpression, sourceCode) {
79
+ function useBoundFunction(callbackNode, thisArgumentNode, sourceCode) {
78
80
  return function * (fixer) {
79
- yield removeThisArgument(callExpression, sourceCode)(fixer);
81
+ yield removeThisArgument(thisArgumentNode, sourceCode)(fixer);
80
82
 
81
- const [callback, thisArgument] = callExpression.arguments;
82
-
83
- const callbackParentheses = getParentheses(callback, sourceCode);
83
+ const callbackParentheses = getParentheses(callbackNode, sourceCode);
84
84
  const isParenthesized = callbackParentheses.length > 0;
85
85
  const callbackLastToken = isParenthesized
86
86
  ? callbackParentheses.at(-1)
87
- : callback;
87
+ : callbackNode;
88
88
  if (
89
89
  !isParenthesized
90
- && shouldAddParenthesesToMemberExpressionObject(callback, sourceCode)
90
+ && shouldAddParenthesesToMemberExpressionObject(callbackNode, sourceCode)
91
91
  ) {
92
92
  yield fixer.insertTextBefore(callbackLastToken, '(');
93
93
  yield fixer.insertTextAfter(callbackLastToken, ')');
94
94
  }
95
95
 
96
- const thisArgumentText = getParenthesizedText(thisArgument, sourceCode);
96
+ const thisArgumentText = getParenthesizedText(thisArgumentNode, sourceCode);
97
97
  // `thisArgument` was a argument, no need add extra parentheses
98
98
  yield fixer.insertTextAfter(callbackLastToken, `.bind(${thisArgumentText})`);
99
99
  };
100
100
  }
101
101
 
102
- /** @param {import('eslint').Rule.RuleContext} context */
103
- const create = context => {
104
- const {sourceCode} = context;
105
-
106
- return {
107
- CallExpression(callExpression) {
108
- if (
109
- !isMethodCall(callExpression, {
110
- methods: [
111
- 'every',
112
- 'filter',
113
- 'find',
114
- 'findLast',
115
- 'findIndex',
116
- 'findLastIndex',
117
- 'flatMap',
118
- 'forEach',
119
- 'map',
120
- 'some',
121
- ],
122
- argumentsLength: 2,
123
- optionalCall: false,
124
- optionalMember: false,
125
- })
126
- || isNodeMatches(callExpression.callee, ignored)
127
- || isNodeValueNotFunction(callExpression.arguments[0])
128
- ) {
129
- return;
130
- }
131
-
132
- const {callee} = callExpression;
133
- const method = callee.property.name;
134
- const [callback, thisArgument] = callExpression.arguments;
135
-
136
- const problem = {
137
- node: thisArgument,
138
- messageId: ERROR,
139
- data: {method},
140
- };
141
-
142
- const thisArgumentHasSideEffect = hasSideEffect(thisArgument, sourceCode);
143
- const isArrowCallback = callback.type === 'ArrowFunctionExpression';
144
-
145
- if (isArrowCallback) {
146
- if (thisArgumentHasSideEffect) {
147
- problem.suggest = [
148
- {
149
- messageId: SUGGESTION_REMOVE,
150
- fix: removeThisArgument(callExpression, sourceCode),
151
- },
152
- ];
153
- } else {
154
- problem.fix = removeThisArgument(callExpression, sourceCode);
155
- }
156
-
157
- return problem;
158
- }
102
+ function getProblem({
103
+ sourceCode,
104
+ callExpression,
105
+ callbackNode,
106
+ thisArgumentNode,
107
+ messageId,
108
+ }) {
109
+ const problem = {
110
+ node: thisArgumentNode,
111
+ messageId,
112
+ data: {
113
+ method: callExpression.callee.property.name,
114
+ },
115
+ };
159
116
 
117
+ const isArrowCallback = callbackNode.type === 'ArrowFunctionExpression';
118
+ if (isArrowCallback) {
119
+ const thisArgumentHasSideEffect = hasSideEffect(thisArgumentNode, sourceCode);
120
+ if (thisArgumentHasSideEffect) {
160
121
  problem.suggest = [
161
122
  {
162
123
  messageId: SUGGESTION_REMOVE,
163
- fix: removeThisArgument(callExpression, sourceCode),
164
- },
165
- {
166
- messageId: SUGGESTION_BIND,
167
- fix: useBoundFunction(callExpression, sourceCode),
124
+ fix: removeThisArgument(thisArgumentNode, sourceCode),
168
125
  },
169
126
  ];
127
+ } else {
128
+ problem.fix = removeThisArgument(thisArgumentNode, sourceCode);
129
+ }
130
+
131
+ return problem;
132
+ }
170
133
 
171
- return problem;
134
+ problem.suggest = [
135
+ {
136
+ messageId: SUGGESTION_REMOVE,
137
+ fix: removeThisArgument(thisArgumentNode, sourceCode),
172
138
  },
173
- };
139
+ {
140
+ messageId: SUGGESTION_BIND,
141
+ fix: useBoundFunction(callbackNode, thisArgumentNode, sourceCode),
142
+ },
143
+ ];
144
+
145
+ return problem;
146
+ }
147
+
148
+ /** @param {import('eslint').Rule.RuleContext} context */
149
+ const create = context => {
150
+ const {sourceCode} = context;
151
+
152
+ // Prototype methods
153
+ context.on('CallExpression', callExpression => {
154
+ if (
155
+ !isMethodCall(callExpression, {
156
+ methods: [
157
+ 'every',
158
+ 'filter',
159
+ 'find',
160
+ 'findLast',
161
+ 'findIndex',
162
+ 'findLastIndex',
163
+ 'flatMap',
164
+ 'forEach',
165
+ 'map',
166
+ 'some',
167
+ ],
168
+ argumentsLength: 2,
169
+ optionalCall: false,
170
+ optionalMember: false,
171
+ })
172
+ || isNodeMatches(callExpression.callee, ignored)
173
+ || isNodeValueNotFunction(callExpression.arguments[0])
174
+ ) {
175
+ return;
176
+ }
177
+
178
+ return getProblem({
179
+ sourceCode,
180
+ callExpression,
181
+ callbackNode: callExpression.arguments[0],
182
+ thisArgumentNode: callExpression.arguments[1],
183
+ messageId: ERROR_PROTOTYPE_METHOD,
184
+ });
185
+ });
186
+
187
+ // `Array.from()`
188
+ context.on('CallExpression', callExpression => {
189
+ if (
190
+ !isMethodCall(callExpression, {
191
+ object: 'Array',
192
+ method: 'from',
193
+ argumentsLength: 3,
194
+ optionalCall: false,
195
+ optionalMember: false,
196
+ })
197
+ || isNodeValueNotFunction(callExpression.arguments[1])
198
+ ) {
199
+ return;
200
+ }
201
+
202
+ return getProblem({
203
+ sourceCode,
204
+ callExpression,
205
+ callbackNode: callExpression.arguments[1],
206
+ thisArgumentNode: callExpression.arguments[2],
207
+ messageId: ERROR_STATIC_METHOD,
208
+ });
209
+ });
174
210
  };
175
211
 
176
212
  /** @type {import('eslint').Rule.RuleModule} */
@@ -13,6 +13,15 @@ const messages = {
13
13
 
14
14
  const isStringThen = (node, context) =>
15
15
  getStaticValue(node, context.sourceCode.getScope(node))?.value === 'then';
16
+ const isPropertyThen = (node, context) => {
17
+ // `getPropertyName` throws on `({[Symbol.prototype]: 0})`
18
+ // https://github.com/eslint-community/eslint-utils/pull/182
19
+ try {
20
+ return getPropertyName(node, context.sourceCode.getScope(node)) === 'then';
21
+ } catch {}
22
+
23
+ return false;
24
+ };
16
25
 
17
26
  const cases = [
18
27
  // `{then() {}}`,
@@ -23,10 +32,7 @@ const cases = [
23
32
  selector: 'ObjectExpression',
24
33
  * getNodes(node, context) {
25
34
  for (const property of node.properties) {
26
- if (
27
- property.type === 'Property'
28
- && getPropertyName(property, context.sourceCode.getScope(property)) === 'then'
29
- ) {
35
+ if (property.type === 'Property' && isPropertyThen(property, context)) {
30
36
  yield property.key;
31
37
  }
32
38
  }
@@ -2,7 +2,7 @@
2
2
  const path = require('node:path');
3
3
  const readPkgUp = require('read-pkg-up');
4
4
  const coreJsCompat = require('core-js-compat');
5
- const {camelCase} = require('lodash');
5
+ const {camelCase} = require('./utils/lodash.js');
6
6
  const isStaticRequire = require('./ast/is-static-require.js');
7
7
 
8
8
  const {data: compatData, entries: coreJsEntries} = coreJsCompat;
@@ -13,8 +13,10 @@ const ERROR_USE_STRICT_DIRECTIVE = 'error/use-strict-directive';
13
13
  const ERROR_GLOBAL_RETURN = 'error/global-return';
14
14
  const ERROR_IDENTIFIER = 'error/identifier';
15
15
  const SUGGESTION_USE_STRICT_DIRECTIVE = 'suggestion/use-strict-directive';
16
- const SUGGESTION_DIRNAME = 'suggestion/dirname';
17
- const SUGGESTION_FILENAME = 'suggestion/filename';
16
+ const SUGGESTION_IMPORT_META_DIRNAME = 'suggestion/import-meta-dirname';
17
+ const SUGGESTION_IMPORT_META_URL_TO_DIRNAME = 'suggestion/import-meta-url-to-dirname';
18
+ const SUGGESTION_IMPORT_META_FILENAME = 'suggestion/import-meta-filename';
19
+ const SUGGESTION_IMPORT_META_URL_TO_FILENAME = 'suggestion/import-meta-url-to-filename';
18
20
  const SUGGESTION_IMPORT = 'suggestion/import';
19
21
  const SUGGESTION_EXPORT = 'suggestion/export';
20
22
  const messages = {
@@ -22,12 +24,43 @@ const messages = {
22
24
  [ERROR_GLOBAL_RETURN]: '"return" should be used inside a function.',
23
25
  [ERROR_IDENTIFIER]: 'Do not use "{{name}}".',
24
26
  [SUGGESTION_USE_STRICT_DIRECTIVE]: 'Remove "use strict" directive.',
25
- [SUGGESTION_DIRNAME]: 'Replace "__dirname" with `"…(import.meta.url)"`.',
26
- [SUGGESTION_FILENAME]: 'Replace "__filename" with `"…(import.meta.url)"`.',
27
+ [SUGGESTION_IMPORT_META_DIRNAME]: 'Replace `__dirname` with `import.meta.dirname`.',
28
+ [SUGGESTION_IMPORT_META_URL_TO_DIRNAME]: 'Replace `__dirname` with `…(import.meta.url)`.',
29
+ [SUGGESTION_IMPORT_META_FILENAME]: 'Replace `__dirname` with `import.meta.filename`.',
30
+ [SUGGESTION_IMPORT_META_URL_TO_FILENAME]: 'Replace `__filename` with `…(import.meta.url)`.',
27
31
  [SUGGESTION_IMPORT]: 'Switch to `import`.',
28
32
  [SUGGESTION_EXPORT]: 'Switch to `export`.',
29
33
  };
30
34
 
35
+ const suggestions = new Map([
36
+ [
37
+ '__dirname',
38
+ [
39
+ {
40
+ messageId: SUGGESTION_IMPORT_META_DIRNAME,
41
+ replacement: 'import.meta.dirname',
42
+ },
43
+ {
44
+ messageId: SUGGESTION_IMPORT_META_URL_TO_DIRNAME,
45
+ replacement: 'path.dirname(url.fileURLToPath(import.meta.url))',
46
+ },
47
+ ],
48
+ ],
49
+ [
50
+ '__filename',
51
+ [
52
+ {
53
+ messageId: SUGGESTION_IMPORT_META_FILENAME,
54
+ replacement: 'import.meta.filename',
55
+ },
56
+ {
57
+ messageId: SUGGESTION_IMPORT_META_URL_TO_FILENAME,
58
+ replacement: 'url.fileURLToPath(import.meta.url)',
59
+ },
60
+ ],
61
+ ],
62
+ ]);
63
+
31
64
  function fixRequireCall(node, sourceCode) {
32
65
  if (!isStaticRequire(node.parent) || node.parent.callee !== node) {
33
66
  return;
@@ -277,14 +310,12 @@ function create(context) {
277
310
  switch (name) {
278
311
  case '__filename':
279
312
  case '__dirname': {
280
- const messageId = node.name === '__dirname' ? SUGGESTION_DIRNAME : SUGGESTION_FILENAME;
281
- const replacement = node.name === '__dirname'
282
- ? 'path.dirname(url.fileURLToPath(import.meta.url))'
283
- : 'url.fileURLToPath(import.meta.url)';
284
- problem.suggest = [{
285
- messageId,
286
- fix: fixer => replaceReferenceIdentifier(node, replacement, fixer),
287
- }];
313
+ problem.suggest = suggestions.get(node.name)
314
+ .map(({messageId, replacement}) => ({
315
+ messageId,
316
+ fix: fixer => replaceReferenceIdentifier(node, replacement, fixer),
317
+ }));
318
+
288
319
  return problem;
289
320
  }
290
321
 
@@ -30,7 +30,7 @@ function create(context) {
30
30
  // `[].foo.{apply,bind,call}(…)`
31
31
  // `({}).foo.{apply,bind,call}(…)`
32
32
  isMethodCall(callExpression, {
33
- names: ['apply', 'bind', 'call'],
33
+ methods: ['apply', 'bind', 'call'],
34
34
  optionalCall: false,
35
35
  optionalMember: false,
36
36
  })
@@ -180,9 +180,9 @@ module.exports.defaultReplacements = {
180
180
  request: true,
181
181
  },
182
182
  res: {
183
+ resource: true,
183
184
  response: true,
184
185
  result: true,
185
- resource: true,
186
186
  },
187
187
  ret: {
188
188
  returnValue: true,