eslint-plugin-remeda 1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/docs/rules/collection-method-value.md +27 -0
  4. package/docs/rules/collection-return.md +29 -0
  5. package/docs/rules/prefer-constant.md +53 -0
  6. package/docs/rules/prefer-do-nothing.md +29 -0
  7. package/docs/rules/prefer-filter.md +25 -0
  8. package/docs/rules/prefer-find.md +39 -0
  9. package/docs/rules/prefer-flat-map.md +25 -0
  10. package/docs/rules/prefer-is-empty.md +39 -0
  11. package/docs/rules/prefer-is-nil.md +27 -0
  12. package/docs/rules/prefer-map.md +29 -0
  13. package/docs/rules/prefer-nullish-coalescing.md +25 -0
  14. package/docs/rules/prefer-remeda-typecheck.md +36 -0
  15. package/docs/rules/prefer-some.md +29 -0
  16. package/docs/rules/prefer-times.md +36 -0
  17. package/package.json +62 -0
  18. package/src/index.js +35 -0
  19. package/src/rules/collection-method-value.js +82 -0
  20. package/src/rules/collection-return.js +71 -0
  21. package/src/rules/prefer-constant.js +87 -0
  22. package/src/rules/prefer-do-nothing.js +44 -0
  23. package/src/rules/prefer-filter.js +82 -0
  24. package/src/rules/prefer-find.js +68 -0
  25. package/src/rules/prefer-flat-map.js +50 -0
  26. package/src/rules/prefer-is-empty.js +134 -0
  27. package/src/rules/prefer-is-nil.js +97 -0
  28. package/src/rules/prefer-map.js +62 -0
  29. package/src/rules/prefer-nullish-coalescing.js +63 -0
  30. package/src/rules/prefer-remeda-typecheck.js +93 -0
  31. package/src/rules/prefer-some.js +43 -0
  32. package/src/rules/prefer-times.js +37 -0
  33. package/src/util/LodashContext.js +128 -0
  34. package/src/util/astUtil.js +353 -0
  35. package/src/util/getDocsUrl.js +17 -0
  36. package/src/util/importUtil.js +24 -0
  37. package/src/util/lodashUtil.js +123 -0
  38. package/src/util/methodData.js +2417 -0
  39. package/src/util/methodDataUtil.js +115 -0
  40. package/src/util/ruleUtil.js +13 -0
  41. package/src/util/settingsUtil.js +31 -0
@@ -0,0 +1,353 @@
1
+ 'use strict'
2
+ const _ = require('lodash')
3
+
4
+ /**
5
+ * Gets the object that called the method in a CallExpression
6
+ * @param {Object} node
7
+ * @returns {Object|undefined}
8
+ */
9
+ const getCaller = _.property(['callee', 'object'])
10
+
11
+ /**
12
+ * Gets the name of a method in a CallExpression
13
+ * @param {Object} node
14
+ * @returns {string|undefined}
15
+ */
16
+ const getMethodName = _.property(['callee', 'property', 'name'])
17
+
18
+ /**
19
+ * Returns whether the node is a method call
20
+ * @param {Object} node
21
+ * @returns {boolean}
22
+ */
23
+ const isMethodCall = _.matches({type: 'CallExpression', callee: {type: 'MemberExpression'}})
24
+
25
+ const isFunctionExpression = _.overSome(
26
+ _.matchesProperty('type', 'FunctionExpression'),
27
+ _.matchesProperty('type', 'FunctionDeclaration')
28
+ )
29
+ /**
30
+ * Returns whether the node is a function declaration that has a block
31
+ * @param {Object} node
32
+ * @returns {boolean}
33
+ */
34
+ const isFunctionDefinitionWithBlock = _.overSome(
35
+ isFunctionExpression,
36
+ _.matches({type: 'ArrowFunctionExpression', body: {type: 'BlockStatement'}})
37
+ )
38
+
39
+ /**
40
+ * If the node specified is a function, returns the node corresponding with the first statement/expression in that function
41
+ * @param {Object} node
42
+ * @returns {node|undefined}
43
+ */
44
+ const getFirstFunctionLine = _.cond([
45
+ [isFunctionDefinitionWithBlock, _.property(['body', 'body', 0])],
46
+ [_.matches({type: 'ArrowFunctionExpression'}), _.property('body')]
47
+ ])
48
+
49
+ /**
50
+ *
51
+ * @param {Object} node
52
+ * @returns {boolean|undefined}
53
+ */
54
+ const isPropAccess = _.overSome(_.matches({computed: false}), _.matchesProperty(['property', 'type'], 'Literal'))
55
+
56
+ /**
57
+ * Returns whether the node is a member expression starting with the same object, up to the specified length
58
+ * @param {Object} node
59
+ * @param {string} objectName
60
+ * @param {Object} [options]
61
+ * @param {number} [options.maxLength]
62
+ * @param {boolean} [options.allowComputed]
63
+ * @returns {boolean|undefined}
64
+ */
65
+ function isMemberExpOf(node, objectName, {maxLength = Number.MAX_VALUE, allowComputed} = {}) {
66
+ if (objectName) {
67
+ let curr = node
68
+ let depth = maxLength
69
+ while (curr && depth) {
70
+ if (allowComputed || isPropAccess(curr)) {
71
+ if (curr.type === 'MemberExpression' && curr.object.name === objectName) {
72
+ return true
73
+ }
74
+ curr = curr.object
75
+ depth--
76
+ } else {
77
+ return false
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Returns the name of the first parameter of a function, if it exists
85
+ * @param {Object} func
86
+ * @returns {string|undefined}
87
+ */
88
+ const getFirstParamName = _.property(['params', 0, 'name'])
89
+
90
+ /**
91
+ * Returns whether or not the expression is a return statement
92
+ * @param {Object} exp
93
+ * @returns {boolean|undefined}
94
+ */
95
+ const isReturnStatement = _.matchesProperty('type', 'ReturnStatement')
96
+
97
+ /**
98
+ * Returns whether the node specified has only one statement
99
+ * @param {Object} func
100
+ * @returns {boolean}
101
+ */
102
+ function hasOnlyOneStatement(func) {
103
+ if (isFunctionDefinitionWithBlock(func)) {
104
+ return _.get(func, 'body.body.length') === 1
105
+ }
106
+ if (func.type === 'ArrowFunctionExpression') {
107
+ return !_.get(func, 'body.body')
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Returns whether the node is an object of a method call
113
+ * @param {Object} node
114
+ * @returns {boolean}
115
+ */
116
+ function isObjectOfMethodCall(node) {
117
+ return _.get(node, 'parent.object') === node && _.get(node, 'parent.parent.type') === 'CallExpression'
118
+ }
119
+
120
+ /**
121
+ * Returns whether the node is a literal
122
+ * @param {Object} node
123
+ * @returns {boolean}
124
+ */
125
+ function isLiteral(node) {
126
+ return node.type === 'Literal'
127
+ }
128
+
129
+ /**
130
+ * Returns whether the expression specified is a binary expression with the specified operator and one of its sides is a member expression of the specified object name
131
+ * @param {string} operator
132
+ * @param {Object} exp
133
+ * @param {string} objectName
134
+ * @param {number} maxLength
135
+ * @param {boolean} allowComputed
136
+ * @param {boolean} onlyLiterals
137
+ * @returns {boolean|undefined}
138
+ */
139
+ function isBinaryExpWithMemberOf(operator, exp, objectName, {maxLength, allowComputed, onlyLiterals} = {}) {
140
+ if (!_.isMatch(exp, {type: 'BinaryExpression', operator})) {
141
+ return false
142
+ }
143
+ const [left, right] = [exp.left, exp.right].map(side => isMemberExpOf(side, objectName, {maxLength, allowComputed}))
144
+ return (left === !right) && (!onlyLiterals || isLiteral(exp.left) || isLiteral(exp.right))
145
+ }
146
+
147
+
148
+ /**
149
+ * Returns whether the specified expression is a negation.
150
+ * @param {Object} exp
151
+ * @returns {boolean|undefined}
152
+ */
153
+ const isNegationExpression = _.matches({type: 'UnaryExpression', operator: '!'})
154
+
155
+ /**
156
+ * Returns whether the expression is a negation of a member of objectName, in the specified depth.
157
+ * @param {Object} exp
158
+ * @param {string} objectName
159
+ * @param {number} maxLength
160
+ * @returns {boolean|undefined}
161
+ */
162
+ function isNegationOfMemberOf(exp, objectName, {maxLength} = {}) {
163
+ return isNegationExpression(exp) && isMemberExpOf(exp.argument, objectName, {maxLength, allowComputed: false})
164
+ }
165
+
166
+ /**
167
+ *
168
+ * @param {Object} exp
169
+ * @param {string} paramName
170
+ * @returns {boolean|undefined}
171
+ */
172
+ function isIdentifierWithName(exp, paramName) {
173
+ return exp && paramName && exp.type === 'Identifier' && exp.name === paramName
174
+ }
175
+
176
+ /**
177
+ * Returns the node of the value returned in the first line, if any
178
+ * @param {Object} func
179
+ * @returns {Object|undefined}
180
+ */
181
+ function getValueReturnedInFirstStatement(func) {
182
+ const firstLine = getFirstFunctionLine(func)
183
+ if (func) {
184
+ if (isFunctionDefinitionWithBlock(func)) {
185
+ return isReturnStatement(firstLine) ? firstLine.argument : undefined
186
+ }
187
+ if (func.type === 'ArrowFunctionExpression') {
188
+ return firstLine
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Returns whether the node is a call from the specified object name
195
+ * @param {Object} node
196
+ * @param {string} objName
197
+ * @returns {boolean|undefined}
198
+ */
199
+ function isCallFromObject(node, objName) {
200
+ return node && objName && node.type === 'CallExpression' && _.get(node, 'callee.object.name') === objName
201
+ }
202
+
203
+ /**
204
+ * Returns whether the node is actually computed (x['ab'] does not count, x['a' + 'b'] does
205
+ * @param {Object} node
206
+ * @returns {boolean|undefined}
207
+ */
208
+ function isComputed(node) {
209
+ return _.get(node, 'computed') && node.property.type !== 'Literal'
210
+ }
211
+
212
+ /**
213
+ * Returns whether the two expressions refer to the same object (e.g. a['b'].c and a.b.c)
214
+ * @param {Object} a
215
+ * @param {Object} b
216
+ * @returns {boolean}
217
+ */
218
+ function isEquivalentMemberExp(a, b) {
219
+ return _.isEqualWith(a, b, (left, right, key) => {
220
+ if (_.includes(['loc', 'range', 'computed', 'start', 'end', 'parent'], key)) {
221
+ return true
222
+ }
223
+ if (isComputed(left) || isComputed(right)) {
224
+ return false
225
+ }
226
+ if (key === 'property') {
227
+ const leftValue = left.name || left.value
228
+ const rightValue = right.name || right.value
229
+ return leftValue === rightValue
230
+ }
231
+ })
232
+ }
233
+
234
+ /**
235
+ * Returns whether the expression is a strict equality comparison, ===
236
+ * @param {Object} node
237
+ * @returns {boolean}
238
+ */
239
+ const isEqEqEq = _.matches({type: 'BinaryExpression', operator: '==='})
240
+
241
+
242
+ const isMinus = node => node.type === 'UnaryExpression' && node.operator === '-'
243
+
244
+ /**
245
+ * Enum for type of comparison to int literal
246
+ * @readonly
247
+ * @enum {number}
248
+ */
249
+ const comparisonType = {
250
+ exact: 0,
251
+ over: 1,
252
+ under: 2,
253
+ any: 3
254
+ }
255
+ const comparisonOperators = ['==', '!=', '===', '!==']
256
+
257
+ function getIsValue(value) {
258
+ return value < 0 ? _.overEvery(isMinus, _.matches({argument: {value: -value}})) : _.matches({value})
259
+ }
260
+
261
+ /**
262
+ * Returns the expression compared to the value in a binary expression, or undefined if there isn't one
263
+ * @param {Object} node
264
+ * @param {number} value
265
+ * @param {boolean} [checkOver=false]
266
+ * @returns {Object|undefined}
267
+ */
268
+ function getExpressionComparedToInt(node, value, checkOver) {
269
+ const isValue = getIsValue(value)
270
+ if (_.includes(comparisonOperators, node.operator)) {
271
+ if (isValue(node.right)) {
272
+ return node.left
273
+ }
274
+ if (isValue(node.left)) {
275
+ return node.right
276
+ }
277
+ }
278
+ if (checkOver) {
279
+ if (node.operator === '>' && isValue(node.right)) {
280
+ return node.left
281
+ }
282
+ if (node.operator === '<' && isValue(node.left)) {
283
+ return node.right
284
+ }
285
+ const isNext = getIsValue(value + 1)
286
+ if ((node.operator === '>=' || node.operator === '<') && isNext(node.right)) {
287
+ return node.left
288
+ }
289
+ if ((node.operator === '<=' || node.operator === '>') && isNext(node.left)) {
290
+ return node.right
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Returns whether the node is a call to indexOf
297
+ * @param {Object} node
298
+ * @returns {boolean}
299
+ */
300
+ const isIndexOfCall = node => isMethodCall(node) && getMethodName(node) === 'indexOf'
301
+
302
+ /**
303
+ * Returns whether the node is a call to findIndex
304
+ * @param {Object} node
305
+ * @returns {boolean}
306
+ */
307
+ const isFindIndexCall = node => isMethodCall(node) && getMethodName(node) === 'findIndex'
308
+
309
+ /**
310
+ * Returns an array of identifier names returned in a parameter or variable definition
311
+ * @param node an AST node which is a parameter or variable declaration
312
+ * @returns {string[]} List of names defined in the parameter
313
+ */
314
+ function collectParameterValues(node) {
315
+ switch (node && node.type) {
316
+ case 'Identifier':
317
+ return [node.name]
318
+ case 'ObjectPattern':
319
+ return _.flatMap(node.properties, prop => collectParameterValues(prop.value))
320
+ case 'ArrayPattern':
321
+ return _.flatMap(node.elements, collectParameterValues)
322
+ default:
323
+ return []
324
+ }
325
+ }
326
+
327
+ module.exports = {
328
+ getCaller,
329
+ getMethodName,
330
+ isMethodCall,
331
+ getFirstFunctionLine,
332
+ isMemberExpOf,
333
+ getFirstParamName,
334
+ hasOnlyOneStatement,
335
+ isObjectOfMethodCall,
336
+ isEqEqEqToMemberOf: isBinaryExpWithMemberOf.bind(null, '==='),
337
+ isNotEqEqToMemberOf: isBinaryExpWithMemberOf.bind(null, '!=='),
338
+ isNegationOfMemberOf,
339
+ isIdentifierWithName,
340
+ isNegationExpression,
341
+ getValueReturnedInFirstStatement,
342
+ isCallFromObject,
343
+ isComputed,
344
+ isEquivalentMemberExp,
345
+ isEqEqEq,
346
+ comparisonType,
347
+ getExpressionComparedToInt,
348
+ isIndexOfCall,
349
+ isFindIndexCall,
350
+ isFunctionExpression,
351
+ isFunctionDefinitionWithBlock,
352
+ collectParameterValues
353
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ const pkg = require("../../package.json");
4
+
5
+ const REPO_URL = "https://github.com/wix/eslint-plugin-lodash";
6
+
7
+ /**
8
+ * Generates the URL to documentation for the given rule name. It uses the
9
+ * package version to build the link to a tagged version of the
10
+ * documentation file.
11
+ *
12
+ * @param {string} ruleName - Name of the eslint rule
13
+ * @returns {string} URL to the documentation for the given rule
14
+ */
15
+ module.exports = function getDocsUrl(ruleName) {
16
+ return `${REPO_URL}/blob/v${pkg.version}/docs/rules/${ruleName}.md`;
17
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ const get = require("lodash/get");
3
+
4
+ function getNameFromCjsRequire(init) {
5
+ if (
6
+ get(init, "callee.name") === "require" &&
7
+ get(init, "arguments.length") === 1 &&
8
+ init.arguments[0].type === "Literal"
9
+ ) {
10
+ return init.arguments[0].value;
11
+ }
12
+ }
13
+
14
+ const isFullRemedaImport = (str) => /^remeda?(\/)?$/.test(str);
15
+ const getMethodImportFromName = (str) => {
16
+ const match = /^remeda([./])(\w+)$/.exec(str);
17
+ return match && match[2];
18
+ };
19
+
20
+ module.exports = {
21
+ getNameFromCjsRequire,
22
+ isFullRemedaImport,
23
+ getMethodImportFromName,
24
+ };
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ const _ = require("lodash");
3
+ const methodDataUtil = require("./methodDataUtil");
4
+ const astUtil = require("./astUtil");
5
+ const LodashContext = require("./LodashContext");
6
+
7
+ /**
8
+ * Returns whether the node is a chain breaker method
9
+ * @param {Object} node
10
+ * @returns {boolean}
11
+ */
12
+ function isChainBreaker(node) {
13
+ return methodDataUtil.isAliasOfMethod("value", astUtil.getMethodName(node));
14
+ }
15
+
16
+ /**
17
+ * Returns whether the node is a call to the specified method or one of its aliases.
18
+ * @param {Object} node
19
+ * @param {string} method
20
+ * @returns {boolean}
21
+ */
22
+ function isCallToMethod(node, method) {
23
+ return methodDataUtil.isAliasOfMethod(method, astUtil.getMethodName(node));
24
+ }
25
+
26
+ /**
27
+ * Gets the 'isX' method for a specified type, e.g. isObject
28
+ * @param {string} name
29
+ * @returns {string|null}
30
+ */
31
+ function getIsTypeMethod(name) {
32
+ const types = [
33
+ "number",
34
+ "boolean",
35
+ "function",
36
+ "Function",
37
+ "string",
38
+ // "object",
39
+ // "undefined",
40
+ "Date",
41
+ "Array",
42
+ "Error",
43
+ // "Element",
44
+ ];
45
+ return _.includes(types, name) ? `is${_.capitalize(name)}` : null;
46
+ }
47
+
48
+ /**
49
+ * Gets the context's Lodash settings and a function and returns a visitor that calls the function for every Lodash or chain call
50
+ * @param {LodashContext} lodashContext
51
+ * @param {LodashReporter} reporter
52
+ * @returns {NodeTypeVisitor}
53
+ */
54
+ function getRemedaMethodCallExpVisitor(lodashContext, reporter) {
55
+ return function (node) {
56
+ let iterateeIndex;
57
+ if (lodashContext.isLodashCall(node)) {
58
+ const method = astUtil.getMethodName(node);
59
+ iterateeIndex = methodDataUtil.getIterateeIndex(method);
60
+ reporter(node, node.arguments[iterateeIndex], {
61
+ callType: "method",
62
+ method,
63
+ lodashContext,
64
+ });
65
+ } else {
66
+ const method = lodashContext.getImportedRemedaMethod(node);
67
+ if (method) {
68
+ iterateeIndex = methodDataUtil.getIterateeIndex(method);
69
+ reporter(node, node.arguments[iterateeIndex], {
70
+ method,
71
+ callType: "single",
72
+ lodashContext,
73
+ });
74
+ }
75
+ }
76
+ };
77
+ }
78
+
79
+ function isRemedaCallToMethod(node, method, lodashContext) {
80
+ return lodashContext.isLodashCall(node) && isCallToMethod(node, method);
81
+ }
82
+
83
+ function isCallToRemedaMethod(node, method, lodashContext) {
84
+ if (!node || node.type !== "CallExpression") {
85
+ return false;
86
+ }
87
+ return (
88
+ isRemedaCallToMethod(node, method, lodashContext) ||
89
+ methodDataUtil.isAliasOfMethod(
90
+ method,
91
+ lodashContext.getImportedRemedaMethod(node),
92
+ )
93
+ );
94
+ }
95
+
96
+ function getRemedaMethodVisitors(context, lodashCallExpVisitor) {
97
+ const lodashContext = new LodashContext(context);
98
+ const visitors = lodashContext.getImportVisitors();
99
+ visitors.CallExpression = getRemedaMethodCallExpVisitor(
100
+ lodashContext,
101
+ lodashCallExpVisitor,
102
+ );
103
+ return visitors;
104
+ }
105
+
106
+ /**
107
+ *
108
+ * @param context
109
+ * @returns {LodashContext} a LodashContext for a given context
110
+ */
111
+ function getLodashContext(context) {
112
+ return new LodashContext(context);
113
+ }
114
+
115
+ module.exports = {
116
+ isChainBreaker,
117
+ isCallToMethod,
118
+ getIsTypeMethod,
119
+ getRemedaMethodCallExpVisitor,
120
+ isCallToLodashMethod: isCallToRemedaMethod,
121
+ getRemedaMethodVisitors,
122
+ getLodashContext,
123
+ };