eslint-plugin-th-rules 3.4.3 → 3.6.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.
package/README.md CHANGED
@@ -166,18 +166,20 @@ Do not edit below this line.
166
166
  🎲 Set in the `recommendedTypescriptReact` configuration.\
167
167
  🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
168
168
 
169
- | Name                      | Description | 💼 | 🔧 |
170
- | :------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------- | :- |
171
- | [no-boolean-coercion](docs/rules/no-boolean-coercion.md) | Disallow Boolean(value) or !!value. Enforce explicit checks: !_.isNil(value) for scalars and !_.isEmpty(value) for strings, arrays, and objects. If the value is already boolean, remove coercion. | ✅ ⚛️ 🟦 🎲 | 🔧 |
172
- | [no-comments](docs/rules/no-comments.md) | Disallow comments except for specified allowed patterns. | ✅ ⚛️ 🟦 🎲 | 🔧 |
173
- | [no-default-export](docs/rules/no-default-export.md) | Convert unnamed default exports to named default exports based on the file name. | ✅ ⚛️ 🟦 🎲 | 🔧 |
174
- | [no-destructuring](docs/rules/no-destructuring.md) | Disallow destructuring that does not meet certain conditions. | ✅ ⚛️ 🟦 🎲 | |
175
- | [no-explicit-nil-compare](docs/rules/no-explicit-nil-compare.md) | Disallow direct comparisons to null or undefined. Use _.isNull(x) / _.isUndefined(x) instead. | ✅ ⚛️ 🟦 🎲 | 🔧 |
176
- | [prefer-explicit-nil-check](docs/rules/prefer-explicit-nil-check.md) | Disallow implicit truthy/falsy checks in boolean-test positions. Prefer explicit _.isNil(value) or _.isEmpty(value) (depending on the value type). | ✅ ⚛️ 🟦 🎲 | 🔧 |
177
- | [prefer-is-empty](docs/rules/prefer-is-empty.md) | Require _.isEmpty instead of length comparisons or boolean checks on .length. | ✅ ⚛️ 🟦 🎲 | 🔧 |
178
- | [schemas-in-schemas-file](docs/rules/schemas-in-schemas-file.md) | Require Zod schema declarations to be placed in a .schemas.ts file. | ✅ ⚛️ 🟦 🎲 | |
179
- | [top-level-functions](docs/rules/top-level-functions.md) | Require all top-level functions to be named regular functions. | ✅ ⚛️ 🟦 🎲 | 🔧 |
180
- | [types-in-dts](docs/rules/types-in-dts.md) | Require TypeScript type declarations (type/interface/enum) to be placed in .d.ts files. | ✅ ⚛️ 🟦 🎲 | |
169
+ | Name                             | Description | 💼 | 🔧 |
170
+ | :--------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------- | :- |
171
+ | [no-boolean-coercion](docs/rules/no-boolean-coercion.md) | Disallow Boolean(value) or !!value. Enforce explicit checks: !_.isNil(value) for scalars and !_.isEmpty(value) for strings, arrays, and objects. If the value is already boolean, remove coercion. | ✅ ⚛️ 🟦 🎲 | 🔧 |
172
+ | [no-comments](docs/rules/no-comments.md) | Disallow comments except for specified allowed patterns. | ✅ ⚛️ 🟦 🎲 | 🔧 |
173
+ | [no-default-export](docs/rules/no-default-export.md) | Convert unnamed default exports to named default exports based on the file name. | ✅ ⚛️ 🟦 🎲 | 🔧 |
174
+ | [no-destructuring](docs/rules/no-destructuring.md) | Disallow destructuring that does not meet certain conditions. | ✅ ⚛️ 🟦 🎲 | |
175
+ | [no-explicit-nil-compare](docs/rules/no-explicit-nil-compare.md) | Disallow direct comparisons to null or undefined. Use _.isNull(x) / _.isUndefined(x) instead. | ✅ ⚛️ 🟦 🎲 | 🔧 |
176
+ | [no-isnil-isempty-on-boolean](docs/rules/no-isnil-isempty-on-boolean.md) | Disallow _.isNil(...) / _.isEmpty(...) when the argument type is boolean or a union containing boolean (e.g., boolean \| undefined, boolean \| X). | ✅ ⚛️ 🟦 🎲 | |
177
+ | [prefer-explicit-nil-check](docs/rules/prefer-explicit-nil-check.md) | Disallow implicit truthy/falsy checks in boolean-test positions. Prefer explicit _.isNil(value) or _.isEmpty(value) (depending on the value type). | ✅ ⚛️ 🟦 🎲 | 🔧 |
178
+ | [prefer-is-empty](docs/rules/prefer-is-empty.md) | Require _.isEmpty instead of length comparisons or boolean checks on .length. | ✅ ⚛️ 🟦 🎲 | 🔧 |
179
+ | [prefer-lodash-iteratee-shorthand](docs/rules/prefer-lodash-iteratee-shorthand.md) | Prefer Lodash iteratee shorthands. Example: _.find(collection, (x) => x.Y === z) -> _.find(collection, {Y: z}). Also prefers property shorthands like _.map(collection, (x) => _.get(x, path)) -> _.map(collection, path). | ✅ ⚛️ 🟦 🎲 | 🔧 |
180
+ | [schemas-in-schemas-file](docs/rules/schemas-in-schemas-file.md) | Require Zod schema declarations to be placed in a .schemas.ts file. | ✅ ⚛️ 🟦 🎲 | |
181
+ | [top-level-functions](docs/rules/top-level-functions.md) | Require all top-level functions to be named regular functions. | ✅ ⚛️ 🟦 🎲 | 🔧 |
182
+ | [types-in-dts](docs/rules/types-in-dts.md) | Require TypeScript type declarations (type/interface/enum) to be placed in .d.ts files. | ✅ ⚛️ 🟦 🎲 | |
181
183
 
182
184
  <!-- end auto-generated rules list -->
183
185
 
package/dist/plugin.d.ts CHANGED
@@ -11,9 +11,10 @@ export declare const rules: {
11
11
  'no-default-export': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unnamed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
12
12
  name: string;
13
13
  };
14
- 'no-destructuring': import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooDeep" | "tooMany" | "tooLong", [{
15
- maximumDestructuredVariables: number;
16
- maximumLineLength: number;
14
+ 'no-destructuring': import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooDeep" | "tooMany" | "tooLong" | "tooManyCumulative" | "directAccessRequired", [{
15
+ maximumDestructuredVariables?: number;
16
+ maximumLineLength?: number;
17
+ directAccessIdentifiers?: string[];
17
18
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
18
19
  name: string;
19
20
  };
@@ -45,6 +46,9 @@ export declare const rules: {
45
46
  'no-isnil-isempty-on-boolean': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
46
47
  name: string;
47
48
  };
49
+ 'prefer-lodash-iteratee-shorthand': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useMatchesObject" | "usePropertyShorthand" | "useLodashMethod", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
50
+ name: string;
51
+ };
48
52
  };
49
53
  declare const plugin: {
50
54
  rules: {
@@ -60,9 +64,10 @@ declare const plugin: {
60
64
  'no-default-export': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unnamed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
61
65
  name: string;
62
66
  };
63
- 'no-destructuring': import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooDeep" | "tooMany" | "tooLong", [{
64
- maximumDestructuredVariables: number;
65
- maximumLineLength: number;
67
+ 'no-destructuring': import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooDeep" | "tooMany" | "tooLong" | "tooManyCumulative" | "directAccessRequired", [{
68
+ maximumDestructuredVariables?: number;
69
+ maximumLineLength?: number;
70
+ directAccessIdentifiers?: string[];
66
71
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
67
72
  name: string;
68
73
  };
@@ -94,6 +99,9 @@ declare const plugin: {
94
99
  'no-isnil-isempty-on-boolean': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
95
100
  name: string;
96
101
  };
102
+ 'prefer-lodash-iteratee-shorthand': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useMatchesObject" | "usePropertyShorthand" | "useLodashMethod", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
103
+ name: string;
104
+ };
97
105
  };
98
106
  };
99
107
  export default plugin;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAYjB,CAAC;AAEF,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAY,CAAC;AACzB,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAajB,CAAC;AAEF,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAY,CAAC;AACzB,eAAe,MAAM,CAAC"}
package/dist/plugin.js CHANGED
@@ -9,6 +9,7 @@ import schemasInSchemasFile from './rules/schemas-in-schemas-file.js';
9
9
  import topLevelFunctions from './rules/top-level-functions.js';
10
10
  import typesInDts from './rules/types-in-dts.js';
11
11
  import noIsNilOrIsEmptyOnBoolean from './rules/no-isnil-isempty-on-boolean.js';
12
+ import preferLodashIterateeShorthand from './rules/prefer-lodash-iteratee-shorthand.js';
12
13
  export const rules = {
13
14
  'no-boolean-coercion': noBooleanCoercion,
14
15
  'no-comments': noComments,
@@ -21,6 +22,7 @@ export const rules = {
21
22
  'top-level-functions': topLevelFunctions,
22
23
  'types-in-dts': typesInDts,
23
24
  'no-isnil-isempty-on-boolean': noIsNilOrIsEmptyOnBoolean,
25
+ 'prefer-lodash-iteratee-shorthand': preferLodashIterateeShorthand,
24
26
  };
25
27
  const plugin = { rules };
26
28
  export default plugin;
@@ -1,8 +1,13 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- declare const noDestructuring: ESLintUtils.RuleModule<"tooDeep" | "tooMany" | "tooLong", [{
3
- maximumDestructuredVariables: number;
4
- maximumLineLength: number;
5
- }], unknown, ESLintUtils.RuleListener> & {
2
+ type Options = [
3
+ {
4
+ maximumDestructuredVariables?: number;
5
+ maximumLineLength?: number;
6
+ directAccessIdentifiers?: string[];
7
+ }
8
+ ];
9
+ type MessageIds = 'tooDeep' | 'tooMany' | 'tooLong' | 'tooManyCumulative' | 'directAccessRequired';
10
+ declare const noDestructuring: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
6
11
  name: string;
7
12
  };
8
13
  export default noDestructuring;
@@ -1 +1 @@
1
- {"version":3,"file":"no-destructuring.d.ts","sourceRoot":"","sources":["../../src/rules/no-destructuring.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAItF,QAAA,MAAM,eAAe;;;;;CAyInB,CAAC;AACH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"no-destructuring.d.ts","sourceRoot":"","sources":["../../src/rules/no-destructuring.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAItF,KAAK,OAAO,GAAG;IACd;QACC,4BAA4B,CAAC,EAAE,MAAM,CAAC;QACtC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;KACnC;CACD,CAAC;AAEF,KAAK,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,GAAG,sBAAsB,CAAC;AAEnG,QAAA,MAAM,eAAe;;CA0NnB,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -1,8 +1,7 @@
1
1
  /* eslint-disable new-cap */
2
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
2
  import _ from 'lodash';
4
3
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
5
- const MAX_TAB_COUNT = 3;
4
+ const MAX_INDENT_SPACES = 3;
6
5
  const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/no-destructuring.md')({
7
6
  name: 'no-destructuring',
8
7
  meta: {
@@ -16,69 +15,137 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
16
15
  properties: {
17
16
  maximumDestructuredVariables: { type: 'integer', minimum: 0 },
18
17
  maximumLineLength: { type: 'integer', minimum: 0 },
18
+ directAccessIdentifiers: {
19
+ type: 'array',
20
+ items: { type: 'string', minLength: 1 },
21
+ },
19
22
  },
20
23
  additionalProperties: false,
21
24
  },
22
25
  ],
23
26
  messages: {
24
- tooDeep: 'Destructuring at a nesting level above {{max}} is not allowed; found {{actual}} levels of nesting.',
27
+ tooDeep: 'Destructuring at an indentation above {{max}} is not allowed; found {{actual}}.',
25
28
  tooMany: 'Destructuring of more than {{max}} variables is not allowed.',
26
29
  tooLong: 'Destructuring spanning a line exceeding {{max}} characters is not allowed.',
30
+ tooManyCumulative: 'Too many destructured variables from "{{source}}" in the same scope. Max is {{max}}, total is {{total}}.',
31
+ directAccessRequired: 'Do not destructure from "{{identifier}}". Use direct member access, for example {{identifier}}.onChangeText.',
27
32
  },
28
33
  },
29
34
  defaultOptions: [
30
35
  {
31
36
  maximumDestructuredVariables: 2,
32
37
  maximumLineLength: 100,
38
+ directAccessIdentifiers: ['properties'],
33
39
  },
34
40
  ],
35
41
  create(context, [options]) {
36
- const MAX_VARIABLES = options.maximumDestructuredVariables ?? 2;
37
- const MAX_LINE_LENGTH = options.maximumLineLength ?? 100;
38
- function reportIfNeeded(patternNode, reportNode = patternNode) {
42
+ const maxVariables = options.maximumDestructuredVariables ?? 2;
43
+ const maxLineLength = options.maximumLineLength ?? 100;
44
+ const directAccessIdentifiers = new Set(options.directAccessIdentifiers ?? ['properties']);
45
+ const sourceTotalsByScope = new WeakMap();
46
+ function getLineText(lineNumber) {
47
+ return context.sourceCode.lines[lineNumber - 1] ?? '';
48
+ }
49
+ function getIndentSpacesForLine(lineNumber) {
50
+ const lineText = getLineText(lineNumber);
51
+ return lineText.search(/\S|$/);
52
+ }
53
+ function getMaxSpannedLineLength(startLine, endLine) {
54
+ let max = 0;
55
+ for (let i = startLine; i <= endLine; i++) {
56
+ const text = getLineText(i);
57
+ if (text.length > max) {
58
+ max = text.length;
59
+ }
60
+ }
61
+ return max;
62
+ }
63
+ function getScopeNode(node) {
64
+ const ancestors = context.sourceCode.getAncestors(node);
65
+ for (let i = ancestors.length - 1; i >= 0; i--) {
66
+ const a = ancestors[i];
67
+ if (a.type === AST_NODE_TYPES.Program ||
68
+ a.type === AST_NODE_TYPES.FunctionDeclaration ||
69
+ a.type === AST_NODE_TYPES.FunctionExpression ||
70
+ a.type === AST_NODE_TYPES.ArrowFunctionExpression ||
71
+ a.type === AST_NODE_TYPES.TSDeclareFunction) {
72
+ return a;
73
+ }
74
+ }
75
+ return context.sourceCode.ast;
76
+ }
77
+ function getOrCreateScopeMap(scope) {
78
+ const existing = sourceTotalsByScope.get(scope);
79
+ if (!_.isNil(existing)) {
80
+ return existing;
81
+ }
82
+ const created = new Map();
83
+ sourceTotalsByScope.set(scope, created);
84
+ return created;
85
+ }
86
+ function isDirectAccessForbiddenInitializer(init) {
87
+ if (_.isNil(init)) {
88
+ return false;
89
+ }
90
+ return init.type === AST_NODE_TYPES.Identifier && directAccessIdentifiers.has(init.name);
91
+ }
92
+ function reportIfNeeded(patternNode, reportNode, initExpression) {
39
93
  if (patternNode?.type !== AST_NODE_TYPES.ObjectPattern || _.isNil(patternNode.loc)) {
40
94
  return;
41
95
  }
96
+ if (isDirectAccessForbiddenInitializer(initExpression)) {
97
+ context.report({
98
+ node: reportNode,
99
+ messageId: 'directAccessRequired',
100
+ data: { identifier: initExpression.name },
101
+ });
102
+ return;
103
+ }
42
104
  const startLine = patternNode.loc.start.line;
43
105
  const endLine = patternNode.loc.end.line;
44
- const lineText = context.sourceCode.lines[startLine - 1] ?? '';
45
- const indentCount = lineText.search(/\S|$/);
106
+ const indentSpaces = getIndentSpacesForLine(startLine);
46
107
  const propertyCount = patternNode.properties?.length ?? 0;
47
- let maxSpannedLineLength = 0;
48
- for (let i = startLine; i <= endLine; i++) {
49
- const t = context.sourceCode.lines[i - 1] ?? '';
50
- if (t.length > maxSpannedLineLength) {
51
- maxSpannedLineLength = t.length;
52
- }
53
- }
54
- if (indentCount > MAX_TAB_COUNT) {
108
+ const maxSpannedLineLength = getMaxSpannedLineLength(startLine, endLine);
109
+ if (indentSpaces > MAX_INDENT_SPACES) {
55
110
  context.report({
56
111
  node: reportNode,
57
112
  messageId: 'tooDeep',
58
- data: {
59
- max: MAX_TAB_COUNT,
60
- actual: indentCount,
61
- },
113
+ data: { max: MAX_INDENT_SPACES, actual: indentSpaces },
62
114
  });
63
115
  }
64
- if (propertyCount > MAX_VARIABLES) {
116
+ if (propertyCount > maxVariables) {
65
117
  context.report({
66
118
  node: reportNode,
67
119
  messageId: 'tooMany',
68
- data: {
69
- max: MAX_VARIABLES,
70
- },
120
+ data: { max: maxVariables },
71
121
  });
72
122
  }
73
- if (maxSpannedLineLength > MAX_LINE_LENGTH) {
123
+ if (maxSpannedLineLength > maxLineLength) {
74
124
  context.report({
75
125
  node: reportNode,
76
126
  messageId: 'tooLong',
77
- data: {
78
- max: MAX_LINE_LENGTH,
79
- },
127
+ data: { max: maxLineLength },
80
128
  });
81
129
  }
130
+ if (!_.isNil(initExpression)) {
131
+ const scopeNode = getScopeNode(reportNode);
132
+ const scopeMap = getOrCreateScopeMap(scopeNode);
133
+ const sourceText = context.sourceCode.getText(initExpression);
134
+ const previousTotal = scopeMap.get(sourceText) ?? 0;
135
+ const newTotal = previousTotal + propertyCount;
136
+ scopeMap.set(sourceText, newTotal);
137
+ if (newTotal > maxVariables) {
138
+ context.report({
139
+ node: reportNode,
140
+ messageId: 'tooManyCumulative',
141
+ data: {
142
+ source: sourceText,
143
+ max: maxVariables,
144
+ total: newTotal,
145
+ },
146
+ });
147
+ }
148
+ }
82
149
  }
83
150
  function checkParameters(parameters) {
84
151
  for (const p of parameters || []) {
@@ -86,15 +153,15 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
86
153
  continue;
87
154
  }
88
155
  if (p.type === AST_NODE_TYPES.AssignmentPattern) {
89
- reportIfNeeded(p.left, p);
156
+ reportIfNeeded(p.left, p, undefined);
90
157
  continue;
91
158
  }
92
- reportIfNeeded(p, p);
159
+ reportIfNeeded(p, p, undefined);
93
160
  }
94
161
  }
95
162
  return {
96
163
  VariableDeclarator(node) {
97
- reportIfNeeded(node.id, node);
164
+ reportIfNeeded(node.id, node, node.init);
98
165
  },
99
166
  FunctionDeclaration(node) {
100
167
  checkParameters(node.params);
@@ -0,0 +1,19 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const PREDICATE_METHOD_NAMES: readonly ["find", "findLast", "findIndex", "findLastIndex", "filter", "reject", "some", "every", "partition", "dropWhile", "takeWhile", "remove", "findKey", "findLastKey"];
3
+ export declare const ITERATEE_METHOD_NAMES: readonly ["map", "flatMap", "flatMapDeep", "flatMapDepth", "forEach", "each", "groupBy", "keyBy", "countBy", "sortBy", "orderBy", "minBy", "maxBy", "sumBy", "meanBy", "uniqBy", "differenceBy", "intersectionBy", "unionBy", "xorBy", "sortedUniqBy"];
4
+ export declare const NATIVE_TO_LODASH_METHOD_NAMES: {
5
+ readonly find: "find";
6
+ readonly findLast: "findLast";
7
+ readonly findIndex: "findIndex";
8
+ readonly findLastIndex: "findLastIndex";
9
+ readonly filter: "filter";
10
+ readonly some: "some";
11
+ readonly every: "every";
12
+ readonly map: "map";
13
+ readonly flatMap: "flatMap";
14
+ };
15
+ declare const preferLodashIterateeShorthand: ESLintUtils.RuleModule<"useMatchesObject" | "usePropertyShorthand" | "useLodashMethod", [], unknown, ESLintUtils.RuleListener> & {
16
+ name: string;
17
+ };
18
+ export default preferLodashIterateeShorthand;
19
+ //# sourceMappingURL=prefer-lodash-iteratee-shorthand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-lodash-iteratee-shorthand.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-lodash-iteratee-shorthand.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,WAAW,EAAgC,MAAM,0BAA0B,CAAC;AAYrG,eAAO,MAAM,sBAAsB,6KAezB,CAAC;AAEX,eAAO,MAAM,qBAAqB,wPAsBxB,CAAC;AAEX,eAAO,MAAM,6BAA6B;;;;;;;;;;CAUhC,CAAC;AAUX,QAAA,MAAM,6BAA6B;;CAmsBjC,CAAC;AAEH,eAAe,6BAA6B,CAAC"}
@@ -0,0 +1,690 @@
1
+ /* eslint-disable th-rules/types-in-dts */
2
+ /* eslint-disable new-cap */
3
+ /* eslint-disable complexity */
4
+ import _ from 'lodash';
5
+ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
6
+ const RULE_NAME = 'prefer-lodash-iteratee-shorthand';
7
+ export const PREDICATE_METHOD_NAMES = [
8
+ 'find',
9
+ 'findLast',
10
+ 'findIndex',
11
+ 'findLastIndex',
12
+ 'filter',
13
+ 'reject',
14
+ 'some',
15
+ 'every',
16
+ 'partition',
17
+ 'dropWhile',
18
+ 'takeWhile',
19
+ 'remove',
20
+ 'findKey',
21
+ 'findLastKey',
22
+ ];
23
+ export const ITERATEE_METHOD_NAMES = [
24
+ 'map',
25
+ 'flatMap',
26
+ 'flatMapDeep',
27
+ 'flatMapDepth',
28
+ 'forEach',
29
+ 'each',
30
+ 'groupBy',
31
+ 'keyBy',
32
+ 'countBy',
33
+ 'sortBy',
34
+ 'orderBy',
35
+ 'minBy',
36
+ 'maxBy',
37
+ 'sumBy',
38
+ 'meanBy',
39
+ 'uniqBy',
40
+ 'differenceBy',
41
+ 'intersectionBy',
42
+ 'unionBy',
43
+ 'xorBy',
44
+ 'sortedUniqBy',
45
+ ];
46
+ export const NATIVE_TO_LODASH_METHOD_NAMES = {
47
+ find: 'find',
48
+ findLast: 'findLast',
49
+ findIndex: 'findIndex',
50
+ findLastIndex: 'findLastIndex',
51
+ filter: 'filter',
52
+ some: 'some',
53
+ every: 'every',
54
+ map: 'map',
55
+ flatMap: 'flatMap',
56
+ };
57
+ function typedKeys(object) {
58
+ return Object.keys(object);
59
+ }
60
+ const preferLodashIterateeShorthand = ESLintUtils.RuleCreator(() => `https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/${RULE_NAME}.md`)({
61
+ name: RULE_NAME,
62
+ meta: {
63
+ type: 'suggestion',
64
+ docs: {
65
+ description: 'Prefer Lodash iteratee shorthands. Example: _.find(collection, (x) => x.Y === z) -> _.find(collection, {Y: z}). Also prefers property shorthands like _.map(collection, (x) => _.get(x, path)) -> _.map(collection, path).',
66
+ },
67
+ fixable: 'code',
68
+ schema: [],
69
+ messages: {
70
+ useMatchesObject: 'Prefer Lodash iteratee shorthand. Use {{replacement}}.',
71
+ usePropertyShorthand: 'Prefer Lodash iteratee shorthand. Use {{replacement}}.',
72
+ useLodashMethod: 'Prefer Lodash iteratee shorthand. Use {{replacement}}.',
73
+ },
74
+ },
75
+ defaultOptions: [],
76
+ create(context) {
77
+ const { sourceCode } = context;
78
+ const PREDICATE_METHODS = new Set(PREDICATE_METHOD_NAMES);
79
+ const ITERATEE_METHODS = new Set(ITERATEE_METHOD_NAMES);
80
+ const nativeMethodNames = typedKeys(NATIVE_TO_LODASH_METHOD_NAMES);
81
+ const NATIVE_METHODS = new Set(nativeMethodNames);
82
+ function ensureLodashImport(fixer) {
83
+ const imports = sourceCode.ast.body.filter((node) => node.type === AST_NODE_TYPES.ImportDeclaration);
84
+ const hasLodash = imports.some((imp) => imp.source.value === 'lodash' && imp.specifiers.some((s) => s.type === AST_NODE_TYPES.ImportDefaultSpecifier || s.type === AST_NODE_TYPES.ImportNamespaceSpecifier));
85
+ if (hasLodash)
86
+ return null;
87
+ const firstImport = imports[0];
88
+ return _.isNil(firstImport) ? fixer.insertTextBeforeRange([0, 0], `import _ from 'lodash';\n`) : fixer.insertTextBefore(firstImport, `import _ from 'lodash';\n`);
89
+ }
90
+ function unwrapChain(node) {
91
+ return node?.type === AST_NODE_TYPES.ChainExpression ? node.expression : node;
92
+ }
93
+ function unwrapChainExpr(node) {
94
+ const unwrapped = unwrapChain(node);
95
+ return !_.isNil(unwrapped) && !_.isEmpty(unwrapped.type) ? unwrapped : undefined;
96
+ }
97
+ function isIdentifier(node, name) {
98
+ return !_.isNil(node) && node.type === AST_NODE_TYPES.Identifier && node.name === name;
99
+ }
100
+ function isStringLiteral(node) {
101
+ return !_.isNil(node) && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string';
102
+ }
103
+ function isNumberLiteral(node) {
104
+ return !_.isNil(node) && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'number';
105
+ }
106
+ function isTemplateLiteralWithoutExpressions(node) {
107
+ return !_.isNil(node) && node.type === AST_NODE_TYPES.TemplateLiteral && _.isEmpty(node.expressions) && node.quasis.length === 1;
108
+ }
109
+ function isLodashIdentifier(node) {
110
+ return isIdentifier(node, '_');
111
+ }
112
+ function isLodashMember(node, methodName) {
113
+ const unwrapped = unwrapChain(node);
114
+ return (!_.isNil(unwrapped) &&
115
+ unwrapped.type === AST_NODE_TYPES.MemberExpression &&
116
+ !unwrapped.computed &&
117
+ unwrapped.property.type === AST_NODE_TYPES.Identifier &&
118
+ unwrapped.property.name === methodName &&
119
+ isLodashIdentifier(unwrapped.object));
120
+ }
121
+ function isLodashStaticCall(node, methodName) {
122
+ return isLodashMember(node.callee, methodName);
123
+ }
124
+ function isLodashWrapperCallExpression(node) {
125
+ const unwrapped = unwrapChain(node);
126
+ return !_.isNil(unwrapped) && unwrapped.type === AST_NODE_TYPES.CallExpression && unwrapped.callee.type === AST_NODE_TYPES.Identifier && unwrapped.callee.name === '_';
127
+ }
128
+ function isLodashWrapperMethodCall(node, methodName) {
129
+ const callee = unwrapChain(node.callee);
130
+ if (_.isNil(callee) || callee.type !== AST_NODE_TYPES.MemberExpression)
131
+ return false;
132
+ if (callee.computed)
133
+ return false;
134
+ if (callee.property.type !== AST_NODE_TYPES.Identifier)
135
+ return false;
136
+ if (callee.property.name !== methodName)
137
+ return false;
138
+ return isLodashWrapperCallExpression(callee.object);
139
+ }
140
+ function getFunctionParameterName(fn) {
141
+ if (fn.params.length !== 1)
142
+ return null;
143
+ const parameter = fn.params[0];
144
+ return parameter.type === AST_NODE_TYPES.Identifier ? parameter.name : null;
145
+ }
146
+ function getReturnedExpression(fn) {
147
+ if (fn.body.type === AST_NODE_TYPES.BlockStatement) {
148
+ if (fn.body.body.length !== 1)
149
+ return null;
150
+ const only = fn.body.body[0];
151
+ if (only.type !== AST_NODE_TYPES.ReturnStatement)
152
+ return null;
153
+ return only.argument ?? null;
154
+ }
155
+ return fn.body;
156
+ }
157
+ function isFunctionLike(node) {
158
+ return !_.isNil(node) && (node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.FunctionExpression);
159
+ }
160
+ function normalizeStaticPathText(pathText) {
161
+ const trimmed = pathText.trim();
162
+ if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
163
+ return trimmed.slice(1, -1);
164
+ }
165
+ if (trimmed.startsWith('`') && trimmed.endsWith('`')) {
166
+ return trimmed.slice(1, -1);
167
+ }
168
+ return trimmed;
169
+ }
170
+ function isValidIdentifierName(name) {
171
+ return /^[A-Za-z_$][\w$]*$/.test(name);
172
+ }
173
+ function toSingleQuotedStringLiteral(value) {
174
+ const escaped = value.replaceAll('\\', '\\\\').replaceAll("'", String.raw `\'`);
175
+ return `'${escaped}'`;
176
+ }
177
+ function toDoubleQuotedRaw(value) {
178
+ return value.replaceAll('\\', '\\\\').replaceAll('"', String.raw `\"`);
179
+ }
180
+ function buildLodashPathStringFromSegments(segments) {
181
+ if (_.isEmpty(segments))
182
+ return null;
183
+ let out = '';
184
+ for (const seg of segments) {
185
+ if (typeof seg === 'number') {
186
+ out += `[${seg}]`;
187
+ continue;
188
+ }
189
+ if (isValidIdentifierName(seg)) {
190
+ out += _.isEmpty(out) ? seg : `.${seg}`;
191
+ continue;
192
+ }
193
+ const inner = toDoubleQuotedRaw(seg);
194
+ out += `["${inner}"]`;
195
+ }
196
+ return out;
197
+ }
198
+ function containsIdentifier(node, name) {
199
+ const { visitorKeys } = sourceCode;
200
+ const seen = new Set();
201
+ const stack = [node];
202
+ while (!_.isEmpty(stack)) {
203
+ const current = stack.pop();
204
+ if (_.isNil(current))
205
+ continue;
206
+ if (seen.has(current))
207
+ continue;
208
+ seen.add(current);
209
+ if (current.type === AST_NODE_TYPES.Identifier && current.name === name)
210
+ return true;
211
+ const keys = visitorKeys[current.type] ?? [];
212
+ for (const key of keys) {
213
+ const value = current[key];
214
+ if (_.isNil(value))
215
+ continue;
216
+ if (Array.isArray(value)) {
217
+ for (const item of value) {
218
+ if (!item || typeof item !== 'object')
219
+ continue;
220
+ stack.push(item);
221
+ }
222
+ continue;
223
+ }
224
+ if (typeof value === 'object') {
225
+ stack.push(value);
226
+ }
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+ function parseLodashPathSegments(path) {
232
+ const segments = [];
233
+ let i = 0;
234
+ const { length } = path;
235
+ const skipDot = () => {
236
+ if (path[i] === '.')
237
+ i += 1;
238
+ };
239
+ const readIdent = () => {
240
+ const start = i;
241
+ if (start >= length)
242
+ return null;
243
+ const first = path[start];
244
+ if (_.isEmpty(first) || !/[A-Za-z_$]/.test(first))
245
+ return null;
246
+ i += 1;
247
+ while (i < length && /[\w$]/.test(path[i] ?? ''))
248
+ i += 1;
249
+ return path.slice(start, i);
250
+ };
251
+ const readBracketIndex = () => {
252
+ if (path[i] !== '[')
253
+ return null;
254
+ i += 1;
255
+ const start = i;
256
+ while (i < length && /\d/.test(path[i] ?? ''))
257
+ i += 1;
258
+ if (start === i)
259
+ return null;
260
+ if (path[i] !== ']')
261
+ return null;
262
+ const number_ = Number(path.slice(start, i));
263
+ i += 1;
264
+ return Number.isFinite(number_) ? number_ : null;
265
+ };
266
+ while (i < length) {
267
+ skipDot();
268
+ if (i >= length)
269
+ break;
270
+ if (path[i] === '[') {
271
+ const idx = readBracketIndex();
272
+ if (_.isNil(idx))
273
+ return null;
274
+ segments.push(idx);
275
+ skipDot();
276
+ continue;
277
+ }
278
+ const ident = readIdent();
279
+ if (_.isNil(ident))
280
+ return null;
281
+ segments.push(ident);
282
+ skipDot();
283
+ }
284
+ return segments;
285
+ }
286
+ function buildNestedLiteralFromSegments(segments, valueText) {
287
+ if (_.isEmpty(segments))
288
+ return null;
289
+ const build = (idx) => {
290
+ const seg = segments[idx];
291
+ if (_.isNil(seg))
292
+ return null;
293
+ const tail = idx === segments.length - 1 ? valueText : build(idx + 1);
294
+ if (_.isNil(tail))
295
+ return null;
296
+ if (typeof seg === 'number') {
297
+ const holes = new Array(seg).fill('').join(',');
298
+ const prefix = seg === 0 ? '' : holes;
299
+ const comma = seg === 0 ? '' : ',';
300
+ return `[${prefix}${comma}${tail}]`;
301
+ }
302
+ const key = isValidIdentifierName(seg) ? seg : toSingleQuotedStringLiteral(seg);
303
+ return `{${key}: ${tail}}`;
304
+ };
305
+ return build(0);
306
+ }
307
+ function mergeObjectLiteralsIntoOne(literals) {
308
+ const trimmed = literals.map((s) => s.trim());
309
+ const allArrays = trimmed.every((s) => s.startsWith('['));
310
+ const anyArrays = trimmed.some((s) => s.startsWith('['));
311
+ if (anyArrays && !allArrays)
312
+ return null;
313
+ if (allArrays) {
314
+ return trimmed.length === 1 ? trimmed[0] : null;
315
+ }
316
+ const props = [];
317
+ for (const lit of trimmed) {
318
+ if (!lit.startsWith('{') || !lit.endsWith('}'))
319
+ return null;
320
+ const inner = lit.slice(1, -1).trim();
321
+ if (_.isEmpty(inner))
322
+ continue;
323
+ props.push(inner);
324
+ }
325
+ if (_.isEmpty(props))
326
+ return '{}';
327
+ return `{${props.join(', ')}}`;
328
+ }
329
+ function isGetCallOnParameter(expr, parameterName) {
330
+ const unwrapped = unwrapChain(expr);
331
+ if (_.isNil(unwrapped) || unwrapped.type !== AST_NODE_TYPES.CallExpression)
332
+ return null;
333
+ const callee = unwrapChain(unwrapped.callee);
334
+ if (!isLodashMember(callee, 'get'))
335
+ return null;
336
+ if (unwrapped.arguments.length < 2)
337
+ return null;
338
+ const arg0 = unwrapped.arguments[0];
339
+ const arg1 = unwrapped.arguments[1];
340
+ if (_.isNil(arg0) || arg0.type === AST_NODE_TYPES.SpreadElement)
341
+ return null;
342
+ if (_.isNil(arg1) || arg1.type === AST_NODE_TYPES.SpreadElement)
343
+ return null;
344
+ const object = unwrapChain(arg0);
345
+ if (_.isNil(object) || object.type !== AST_NODE_TYPES.Identifier || object.name !== parameterName)
346
+ return null;
347
+ if (isStringLiteral(arg1)) {
348
+ return { kind: 'get', pathText: sourceCode.getText(arg1), pathIsStaticString: true };
349
+ }
350
+ if (isTemplateLiteralWithoutExpressions(arg1)) {
351
+ return { kind: 'get', pathText: sourceCode.getText(arg1), pathIsStaticString: true };
352
+ }
353
+ if (arg1.type === AST_NODE_TYPES.Identifier) {
354
+ return { kind: 'get', pathText: arg1.name, pathIsStaticString: false };
355
+ }
356
+ return null;
357
+ }
358
+ function extractMemberPathSegments(expr, parameterName) {
359
+ const unwrapped = unwrapChainExpr(expr) ?? expr;
360
+ const segments = [];
361
+ let current = unwrapped;
362
+ while (current.type === AST_NODE_TYPES.MemberExpression) {
363
+ if (current.computed) {
364
+ const prop = unwrapChain(current.property);
365
+ if (!_.isNil(prop) && prop.type === AST_NODE_TYPES.Identifier)
366
+ return null;
367
+ if (isStringLiteral(prop)) {
368
+ segments.unshift(prop.value);
369
+ }
370
+ else if (isNumberLiteral(prop)) {
371
+ segments.unshift(prop.value);
372
+ }
373
+ else if (isTemplateLiteralWithoutExpressions(prop)) {
374
+ const cooked = prop.quasis[0]?.value.cooked ?? prop.quasis[0]?.value.raw ?? '';
375
+ segments.unshift(cooked);
376
+ }
377
+ else {
378
+ return null;
379
+ }
380
+ }
381
+ else {
382
+ if (current.property.type !== AST_NODE_TYPES.Identifier)
383
+ return null;
384
+ segments.unshift(current.property.name);
385
+ }
386
+ const object = unwrapChain(current.object);
387
+ if (_.isNil(object))
388
+ return null;
389
+ current = object;
390
+ }
391
+ if (current.type !== AST_NODE_TYPES.Identifier || current.name !== parameterName)
392
+ return null;
393
+ if (_.isEmpty(segments))
394
+ return null;
395
+ return segments;
396
+ }
397
+ function extractPathFromExpression(expr, parameterName) {
398
+ const memberSegments = extractMemberPathSegments(expr, parameterName);
399
+ if (!_.isNil(memberSegments))
400
+ return { kind: 'memberPath', segments: memberSegments };
401
+ return isGetCallOnParameter(expr, parameterName);
402
+ }
403
+ function extractPredicateClausesFromExpression(expr, parameterName) {
404
+ const unwrapped = unwrapChainExpr(expr);
405
+ if (_.isNil(unwrapped))
406
+ return null;
407
+ const flattenAnd = (e) => {
408
+ const u = unwrapChainExpr(e) ?? e;
409
+ if (u.type === AST_NODE_TYPES.LogicalExpression && u.operator === '&&') {
410
+ return [...flattenAnd(u.left), ...flattenAnd(u.right)];
411
+ }
412
+ return [u];
413
+ };
414
+ const parts = flattenAnd(unwrapped);
415
+ const clauses = [];
416
+ for (const part of parts) {
417
+ const u = unwrapChainExpr(part) ?? part;
418
+ if (u.type !== AST_NODE_TYPES.BinaryExpression)
419
+ return null;
420
+ if (u.operator !== '===' && u.operator !== '==')
421
+ return null;
422
+ const left = unwrapChainExpr(u.left) ?? u.left;
423
+ const right = unwrapChainExpr(u.right) ?? u.right;
424
+ const leftPath = extractPathFromExpression(left, parameterName);
425
+ const rightPath = extractPathFromExpression(right, parameterName);
426
+ if (!_.isNil(leftPath) && _.isNil(rightPath)) {
427
+ if (containsIdentifier(right, parameterName))
428
+ return null;
429
+ clauses.push({ path: leftPath, valueExpr: right });
430
+ continue;
431
+ }
432
+ if (_.isNil(leftPath) && !_.isNil(rightPath)) {
433
+ if (containsIdentifier(left, parameterName))
434
+ return null;
435
+ clauses.push({ path: rightPath, valueExpr: left });
436
+ continue;
437
+ }
438
+ return null;
439
+ }
440
+ return clauses;
441
+ }
442
+ function buildMatchesObjectFromClauses(clauses) {
443
+ const literals = [];
444
+ for (const clause of clauses) {
445
+ const valueText = sourceCode.getText(clause.valueExpr);
446
+ if (clause.path.kind === 'memberPath') {
447
+ const nested = buildNestedLiteralFromSegments(clause.path.segments, valueText);
448
+ if (_.isNil(nested))
449
+ return null;
450
+ literals.push(nested);
451
+ continue;
452
+ }
453
+ if (!clause.path.pathIsStaticString) {
454
+ return null;
455
+ }
456
+ const rawPath = normalizeStaticPathText(clause.path.pathText);
457
+ const segments = parseLodashPathSegments(rawPath);
458
+ if (_.isNil(segments))
459
+ return null;
460
+ const nested = buildNestedLiteralFromSegments(segments, valueText);
461
+ if (_.isNil(nested))
462
+ return null;
463
+ literals.push(nested);
464
+ }
465
+ return mergeObjectLiteralsIntoOne(literals);
466
+ }
467
+ function extractPropertyIterateeRewrite(fn) {
468
+ const parameterName = getFunctionParameterName(fn);
469
+ if (_.isNil(parameterName))
470
+ return null;
471
+ const expr = getReturnedExpression(fn);
472
+ if (_.isNil(expr))
473
+ return null;
474
+ const memberSegments = extractMemberPathSegments(expr, parameterName);
475
+ if (!_.isNil(memberSegments)) {
476
+ const raw = buildLodashPathStringFromSegments(memberSegments);
477
+ if (_.isNil(raw))
478
+ return null;
479
+ return toSingleQuotedStringLiteral(raw);
480
+ }
481
+ const unwrapped = unwrapChainExpr(expr) ?? expr;
482
+ if (unwrapped.type === AST_NODE_TYPES.MemberExpression) {
483
+ const object = unwrapChain(unwrapped.object);
484
+ if (!_.isNil(object) && object.type === AST_NODE_TYPES.Identifier && object.name === parameterName && unwrapped.computed) {
485
+ const prop = unwrapChain(unwrapped.property);
486
+ if (!_.isNil(prop) && prop.type === AST_NODE_TYPES.Identifier) {
487
+ return prop.name;
488
+ }
489
+ }
490
+ }
491
+ const get = isGetCallOnParameter(expr, parameterName);
492
+ if (!_.isNil(get) && get.kind === 'get') {
493
+ return get.pathText;
494
+ }
495
+ return null;
496
+ }
497
+ function getMethodNameFromMemberCallee(callee) {
498
+ const unwrapped = unwrapChain(callee);
499
+ if (_.isNil(unwrapped) || unwrapped.type !== AST_NODE_TYPES.MemberExpression)
500
+ return null;
501
+ if (unwrapped.computed)
502
+ return null;
503
+ if (unwrapped.property.type !== AST_NODE_TYPES.Identifier)
504
+ return null;
505
+ return unwrapped.property.name;
506
+ }
507
+ function isNativeArrayMethodCall(node) {
508
+ const callee = unwrapChain(node.callee);
509
+ if (_.isNil(callee) || callee.type !== AST_NODE_TYPES.MemberExpression)
510
+ return null;
511
+ const method = getMethodNameFromMemberCallee(callee);
512
+ if (_.isNil(method) || !NATIVE_METHODS.has(method))
513
+ return null;
514
+ if (isLodashIdentifier(callee.object))
515
+ return null;
516
+ if (isLodashWrapperCallExpression(callee.object))
517
+ return null;
518
+ const collectionText = sourceCode.getText(callee.object);
519
+ return { methodName: method, collectionText };
520
+ }
521
+ function isLodashMethodCall(node) {
522
+ const method = getMethodNameFromMemberCallee(node.callee);
523
+ if (_.isNil(method))
524
+ return null;
525
+ if (isLodashStaticCall(node, method))
526
+ return { mode: 'static', methodName: method };
527
+ if (isLodashWrapperMethodCall(node, method))
528
+ return { mode: 'wrapper', methodName: method };
529
+ return null;
530
+ }
531
+ function reportAndFixReplaceCallWithLodash(callNode, methodName, collectionText, iterateeText, fixer) {
532
+ const replacement = `_.${methodName}(${collectionText}, ${iterateeText})`;
533
+ const target = callNode.parent?.type === AST_NODE_TYPES.ChainExpression ? callNode.parent : callNode;
534
+ const fixes = [fixer.replaceText(target, replacement)];
535
+ const importFix = ensureLodashImport(fixer);
536
+ if (!_.isNil(importFix))
537
+ fixes.push(importFix);
538
+ return fixes;
539
+ }
540
+ function reportAndFixReplaceArgument(nodeToFix, replacementText, fixer) {
541
+ const fixes = [fixer.replaceText(nodeToFix, replacementText)];
542
+ const importFix = ensureLodashImport(fixer);
543
+ if (!_.isNil(importFix))
544
+ fixes.push(importFix);
545
+ return fixes;
546
+ }
547
+ function getNativeIterateeKind(methodName) {
548
+ const lodashMethod = NATIVE_TO_LODASH_METHOD_NAMES[methodName];
549
+ if (PREDICATE_METHODS.has(lodashMethod))
550
+ return 'predicate';
551
+ if (ITERATEE_METHODS.has(lodashMethod))
552
+ return 'iteratee';
553
+ return null;
554
+ }
555
+ return {
556
+ CallExpression(node) {
557
+ const nativeCall = isNativeArrayMethodCall(node);
558
+ if (!_.isNil(nativeCall)) {
559
+ const arg0 = node.arguments[0];
560
+ if (_.isNil(arg0) || arg0.type === AST_NODE_TYPES.SpreadElement)
561
+ return;
562
+ if (!isFunctionLike(arg0))
563
+ return;
564
+ const lodashMethod = NATIVE_TO_LODASH_METHOD_NAMES[nativeCall.methodName];
565
+ const kind = getNativeIterateeKind(nativeCall.methodName);
566
+ if (_.isNil(kind))
567
+ return;
568
+ if (kind === 'predicate') {
569
+ const expr = getReturnedExpression(arg0);
570
+ if (_.isNil(expr))
571
+ return;
572
+ const parameterName = getFunctionParameterName(arg0);
573
+ if (_.isNil(parameterName))
574
+ return;
575
+ const clauses = extractPredicateClausesFromExpression(expr, parameterName);
576
+ if (_.isNil(clauses) || _.isEmpty(clauses))
577
+ return;
578
+ const matchesObject = buildMatchesObjectFromClauses(clauses);
579
+ if (_.isNil(matchesObject))
580
+ return;
581
+ const replacement = `_.${lodashMethod}(${nativeCall.collectionText}, ${matchesObject})`;
582
+ context.report({
583
+ node,
584
+ messageId: 'useLodashMethod',
585
+ data: { replacement },
586
+ fix(fixer) {
587
+ return reportAndFixReplaceCallWithLodash(node, lodashMethod, nativeCall.collectionText, matchesObject, fixer);
588
+ },
589
+ });
590
+ return;
591
+ }
592
+ if (kind === 'iteratee') {
593
+ const replacementIteratee = extractPropertyIterateeRewrite(arg0);
594
+ if (_.isNil(replacementIteratee))
595
+ return;
596
+ const replacement = `_.${lodashMethod}(${nativeCall.collectionText}, ${replacementIteratee})`;
597
+ context.report({
598
+ node,
599
+ messageId: 'useLodashMethod',
600
+ data: { replacement },
601
+ fix(fixer) {
602
+ return reportAndFixReplaceCallWithLodash(node, lodashMethod, nativeCall.collectionText, replacementIteratee, fixer);
603
+ },
604
+ });
605
+ return;
606
+ }
607
+ }
608
+ const lodashCall = isLodashMethodCall(node);
609
+ if (_.isNil(lodashCall))
610
+ return;
611
+ if (PREDICATE_METHODS.has(lodashCall.methodName)) {
612
+ const predicateIndex = lodashCall.mode === 'static' ? 1 : 0;
613
+ const predicateArg = node.arguments[predicateIndex];
614
+ if (_.isNil(predicateArg) || predicateArg.type === AST_NODE_TYPES.SpreadElement)
615
+ return;
616
+ if (isFunctionLike(predicateArg)) {
617
+ const expr = getReturnedExpression(predicateArg);
618
+ if (_.isNil(expr))
619
+ return;
620
+ const parameterName = getFunctionParameterName(predicateArg);
621
+ if (_.isNil(parameterName))
622
+ return;
623
+ const clauses = extractPredicateClausesFromExpression(expr, parameterName);
624
+ if (_.isNil(clauses) || _.isEmpty(clauses))
625
+ return;
626
+ const matchesObject = buildMatchesObjectFromClauses(clauses);
627
+ if (_.isNil(matchesObject))
628
+ return;
629
+ context.report({
630
+ node: predicateArg,
631
+ messageId: 'useMatchesObject',
632
+ data: { replacement: matchesObject },
633
+ fix(fixer) {
634
+ return reportAndFixReplaceArgument(predicateArg, matchesObject, fixer);
635
+ },
636
+ });
637
+ return;
638
+ }
639
+ }
640
+ if (ITERATEE_METHODS.has(lodashCall.methodName)) {
641
+ const iterateeIndex = lodashCall.mode === 'static' ? 1 : 0;
642
+ const iterateeArg = node.arguments[iterateeIndex];
643
+ if (_.isNil(iterateeArg) || iterateeArg.type === AST_NODE_TYPES.SpreadElement)
644
+ return;
645
+ if (iterateeArg.type === AST_NODE_TYPES.ArrayExpression && (lodashCall.methodName === 'sortBy' || lodashCall.methodName === 'orderBy')) {
646
+ const rewrites = [];
647
+ for (const element of iterateeArg.elements) {
648
+ if (_.isNil(element) || element.type === AST_NODE_TYPES.SpreadElement)
649
+ continue;
650
+ if (!isFunctionLike(element))
651
+ continue;
652
+ const replacement = extractPropertyIterateeRewrite(element);
653
+ if (!_.isNil(replacement))
654
+ rewrites.push({ element, replacement });
655
+ }
656
+ if (_.isEmpty(rewrites))
657
+ return;
658
+ context.report({
659
+ node: iterateeArg,
660
+ messageId: 'usePropertyShorthand',
661
+ data: { replacement: sourceCode.getText(iterateeArg) },
662
+ fix(fixer) {
663
+ const fixes = rewrites.map(({ element, replacement }) => fixer.replaceText(element, replacement));
664
+ const importFix = ensureLodashImport(fixer);
665
+ if (!_.isNil(importFix))
666
+ fixes.push(importFix);
667
+ return fixes;
668
+ },
669
+ });
670
+ return;
671
+ }
672
+ if (isFunctionLike(iterateeArg)) {
673
+ const replacement = extractPropertyIterateeRewrite(iterateeArg);
674
+ if (_.isNil(replacement))
675
+ return;
676
+ context.report({
677
+ node: iterateeArg,
678
+ messageId: 'usePropertyShorthand',
679
+ data: { replacement },
680
+ fix(fixer) {
681
+ return reportAndFixReplaceArgument(iterateeArg, replacement, fixer);
682
+ },
683
+ });
684
+ }
685
+ }
686
+ },
687
+ };
688
+ },
689
+ });
690
+ export default preferLodashIterateeShorthand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-th-rules",
3
- "version": "3.4.3",
3
+ "version": "3.6.0",
4
4
  "description": "A List of custom ESLint rules created by Tomer Horowitz",
5
5
  "keywords": [
6
6
  "eslint",