eslint-plugin-effector 0.18.0 → 0.19.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/dist/index.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  import { ASTUtils, AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
2
  import { getContextualType, typeMatchesSpecifier } from "@typescript-eslint/type-utils";
3
- import { isExpression } from "typescript";
3
+ import ts, { isExpression } from "typescript";
4
4
  import esquery from "esquery";
5
5
  //#region package.json
6
6
  var name = "eslint-plugin-effector";
7
- var version = "0.18.0";
7
+ var version = "0.19.0";
8
8
  //#endregion
9
9
  //#region src/shared/create.ts
10
10
  const createRule = ESLintUtils.RuleCreator((name) => `https://eslint.effector.dev/rules/${name}`);
11
11
  //#endregion
12
12
  //#region src/shared/is.ts
13
- const check = (symbol, types, from) => {
13
+ const check$1 = (symbol, types, from) => {
14
14
  const name = symbol.getName();
15
15
  const declarations = symbol.declarations ?? [];
16
16
  return types.includes(name) && declarations.map((decl) => decl.getSourceFile().fileName).some((fname) => fname.includes("node_modules") && fname.includes(from));
@@ -31,6 +31,11 @@ const isType = {
31
31
  package: "effector",
32
32
  name: "Effect"
33
33
  }, program),
34
+ domain: (type, program) => typeMatchesSpecifier(type, {
35
+ from: "package",
36
+ package: "effector",
37
+ name: "Domain"
38
+ }, program),
34
39
  unit: (type, program) => {
35
40
  return typeMatchesSpecifier(type, {
36
41
  from: "package",
@@ -45,14 +50,9 @@ const isType = {
45
50
  ]
46
51
  }, program);
47
52
  },
48
- domain: (type, program) => typeMatchesSpecifier(type, {
49
- from: "package",
50
- package: "effector",
51
- name: "Domain"
52
- }, program),
53
53
  gate: (type) => {
54
54
  const symbol = type.getSymbol() ?? type.aliasSymbol;
55
- return symbol ? check(symbol, ["Gate"], "effector") : false;
55
+ return symbol ? check$1(symbol, ["Gate"], "effector") : false;
56
56
  },
57
57
  jsx: (type, program) => {
58
58
  return typeMatchesSpecifier(type, {
@@ -87,7 +87,7 @@ var enforce_effect_naming_convention_default = createRule({
87
87
  type: "problem",
88
88
  docs: { description: "Enforce Fx as a suffix for any Effector Effect." },
89
89
  messages: {
90
- invalid: "Effect `{{ current }}` should be named with suffix, rename it to `{{ fixed }}`",
90
+ invalid: "Effect \"{{ current }}\" should be named with `Fx` suffix, rename it to \"{{ fixed }}\"",
91
91
  rename: "Rename \"{{ current }}\" to \"{{ fixed }}\""
92
92
  },
93
93
  schema: [],
@@ -96,33 +96,188 @@ var enforce_effect_naming_convention_default = createRule({
96
96
  defaultOptions: [],
97
97
  create: (context) => {
98
98
  const services = ESLintUtils.getParserServices(context);
99
- return { [`VariableDeclarator[id.name!=${FxRegex}]`]: (node) => {
100
- const type = services.getTypeAtLocation(node);
101
- if (!isType.effect(type, services.program)) return;
102
- const current = node.id.name;
103
- const fixed = current + "Fx";
104
- const data = {
105
- current,
106
- fixed
107
- };
108
- const suggestion = {
109
- messageId: "rename",
110
- data: {
111
- current,
112
- fixed
113
- },
114
- fix: (fixer) => fixer.replaceText(node.id, fixed)
115
- };
116
- context.report({
117
- node: node.id,
118
- messageId: "invalid",
119
- data,
120
- suggest: [suggestion]
121
- });
122
- } };
99
+ return {
100
+ [`${selector$14.variable}, ${selector$14.array.identifier}, ${selector$14.array.assignment}, ${selector$14.function.identifier}, ${selector$14.function.assignment}`]: (node) => {
101
+ const type = services.getTypeAtLocation(node);
102
+ if (!isType.effect(type, services.program)) return;
103
+ const data = {
104
+ current: node.name,
105
+ fixed: node.name + "Fx"
106
+ };
107
+ if (node.typeAnnotation) return context.report({
108
+ node,
109
+ messageId: "invalid",
110
+ data
111
+ });
112
+ const suggestion = {
113
+ messageId: "rename",
114
+ data: {
115
+ current: node.name,
116
+ fixed: data.fixed
117
+ },
118
+ fix: (fixer) => fixer.replaceText(node, data.fixed)
119
+ };
120
+ context.report({
121
+ node,
122
+ messageId: "invalid",
123
+ data,
124
+ suggest: [suggestion]
125
+ });
126
+ },
127
+ [`${selector$14.shape.identifier}, ${selector$14.shape.assignment}`]: (node) => {
128
+ const type = services.getTypeAtLocation(node.value);
129
+ const ident = node.value.type === AST_NODE_TYPES.Identifier ? node.value : node.value.left;
130
+ if (!isType.effect(type, services.program)) return;
131
+ const data = {
132
+ current: ident.name,
133
+ fixed: ident.name + "Fx"
134
+ };
135
+ const suggestion = {
136
+ messageId: "rename",
137
+ data: {
138
+ current: ident.name,
139
+ fixed: data.fixed
140
+ },
141
+ fix: (fixer) => node.shorthand ? fixer.insertTextAfter(node.key, `: ${data.fixed}`) : fixer.replaceText(ident, data.fixed)
142
+ };
143
+ context.report({
144
+ node: ident,
145
+ messageId: "invalid",
146
+ data,
147
+ suggest: [suggestion]
148
+ });
149
+ }
150
+ };
123
151
  }
124
152
  });
125
153
  const FxRegex = /Fx$/;
154
+ const selector$14 = {
155
+ variable: `VariableDeclarator > Identifier.id[name!=${FxRegex}]`,
156
+ array: {
157
+ identifier: `ArrayPattern > Identifier.elements[name!=${FxRegex}]`,
158
+ assignment: `ArrayPattern > AssignmentPattern > Identifier.left[name!=${FxRegex}]`
159
+ },
160
+ shape: {
161
+ identifier: `ObjectPattern > Property:has(> Identifier.value[name!=${FxRegex}])`,
162
+ assignment: `ObjectPattern > Property:has(> AssignmentPattern:has(> Identifier.left[name!=${FxRegex}]))`
163
+ },
164
+ function: {
165
+ identifier: `:function > Identifier.params[name!=${FxRegex}]`,
166
+ assignment: `:function > AssignmentPattern > Identifier.left[name!=${FxRegex}]`
167
+ }
168
+ };
169
+ //#endregion
170
+ //#region src/shared/package.ts
171
+ const PACKAGE_NAME$1 = {
172
+ core: /^effector(?:\u002Fcompat)?$/,
173
+ react: /^effector-react$/,
174
+ storage: /^@?effector-storage(\u002F[\w-]+)*$/
175
+ };
176
+ //#endregion
177
+ //#region src/rules/enforce-exhaustive-useUnit-destructuring/enforce-exhaustive-useUnit-destructuring.ts
178
+ var enforce_exhaustive_useUnit_destructuring_default = createRule({
179
+ name: "enforce-exhaustive-useUnit-destructuring",
180
+ meta: {
181
+ type: "problem",
182
+ docs: { description: "Ensure all units passed to useUnit are properly destructured." },
183
+ messages: {
184
+ unusedKey: "Property \"{{name}}\" is passed but not destructured.",
185
+ missingKey: "Property \"{{name}}\" is destructured but not passed in the unit object."
186
+ },
187
+ schema: [],
188
+ defaultOptions: []
189
+ },
190
+ create(context) {
191
+ const importedAs = /* @__PURE__ */ new Set();
192
+ return {
193
+ [selector$13.import]: (node) => void importedAs.add(node.local.name),
194
+ [`${selector$13.variable.shape}:has(> ${selector$13.call}:has(${selector$13.arg.shape}))`](node) {
195
+ if (!importedAs.has(node.init.callee.name)) return;
196
+ const provided = shapeToKeyMap(node.init.arguments[0]);
197
+ const consumed = shapeToKeyMap(node.id);
198
+ if (provided === null || consumed === null) return;
199
+ for (const { type, name } of check(provided, consumed)) if (type === "unused") context.report({
200
+ node: node.init.arguments[0],
201
+ messageId: "unusedKey",
202
+ data: { name }
203
+ });
204
+ else context.report({
205
+ node: node.id,
206
+ messageId: "missingKey",
207
+ data: { name }
208
+ });
209
+ },
210
+ [`${selector$13.variable.list}:has(> ${selector$13.call}:has(${selector$13.arg.list}))`](node) {
211
+ if (!importedAs.has(node.init.callee.name)) return;
212
+ const provided = listToKeyMap(node.init.arguments[0]);
213
+ const consumed = listToKeyMap(node.id);
214
+ if (provided === null || consumed === null) return;
215
+ for (const { type, name } of check(provided, consumed)) if (type === "unused") context.report({
216
+ node: node.init.arguments[0],
217
+ messageId: "unusedKey",
218
+ data: { name }
219
+ });
220
+ else context.report({
221
+ node: node.id,
222
+ messageId: "missingKey",
223
+ data: { name }
224
+ });
225
+ }
226
+ };
227
+ }
228
+ });
229
+ const selector$13 = {
230
+ import: `ImportDeclaration[source.value=${PACKAGE_NAME$1.react}] > ImportSpecifier[imported.name=useUnit]`,
231
+ variable: {
232
+ shape: "VariableDeclarator[id.type=ObjectPattern]",
233
+ list: "VariableDeclarator[id.type=ArrayPattern]"
234
+ },
235
+ call: "CallExpression.init[arguments.length=1][callee.type=Identifier]",
236
+ arg: {
237
+ shape: "ObjectExpression.arguments",
238
+ list: "ArrayExpression.arguments"
239
+ }
240
+ };
241
+ function toName$1(key, node) {
242
+ if (node.type === AST_NODE_TYPES.Identifier) return node.name;
243
+ if (node.type === AST_NODE_TYPES.Literal) return String(node.value);
244
+ if (node.type === AST_NODE_TYPES.MemberExpression && node.property.type === AST_NODE_TYPES.Identifier) return `${toName$1(key, node.object)}.${node.property.name}`;
245
+ return `<unknown at ${key}>`;
246
+ }
247
+ function toKey(prop) {
248
+ if (prop.computed) return null;
249
+ else if (prop.key.type === AST_NODE_TYPES.Identifier) return prop.key.name;
250
+ else return prop.key.value;
251
+ }
252
+ function* check(provided, consumed) {
253
+ for (const [key, node] of provided) if (!consumed.has(key)) yield {
254
+ type: "unused",
255
+ name: toName$1(key, node)
256
+ };
257
+ for (const [key, node] of consumed) if (!provided.has(key)) yield {
258
+ type: "missing",
259
+ name: toName$1(key, node)
260
+ };
261
+ }
262
+ function shapeToKeyMap(shape) {
263
+ const map = /* @__PURE__ */ new Map();
264
+ for (const prop of shape.properties) {
265
+ if (prop.type !== AST_NODE_TYPES.Property) return null;
266
+ const key = toKey(prop);
267
+ if (key === null) return null;
268
+ else map.set(key, prop.key);
269
+ }
270
+ return map;
271
+ }
272
+ function listToKeyMap(list) {
273
+ const map = /* @__PURE__ */ new Map();
274
+ for (const [index, element] of list.elements.entries()) {
275
+ if (element === null) continue;
276
+ if (element.type === AST_NODE_TYPES.RestElement || element.type === AST_NODE_TYPES.SpreadElement) return null;
277
+ map.set(index, element);
278
+ }
279
+ return map;
280
+ }
126
281
  //#endregion
127
282
  //#region src/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.ts
128
283
  var enforce_gate_naming_convention_default = createRule({
@@ -190,43 +345,81 @@ var enforce_store_naming_convention_default = createRule({
190
345
  defaultOptions: [{ mode: "prefix" }],
191
346
  create: (context, [options]) => {
192
347
  const services = ESLintUtils.getParserServices(context);
193
- return { [`VariableDeclarator[id.name=${options.mode === "prefix" ? PrefixRegex : PostfixRegex}]`]: (node) => {
194
- const type = services.getTypeAtLocation(node);
195
- if (!isType.store(type, services.program)) return;
196
- const current = node.id.name;
197
- const trimmed = current.replaceAll(options.mode === "prefix" ? /\$+$/g : /^\$+/g, "");
348
+ const selector = createSelector(options.mode === "prefix" ? PrefixRegex : PostfixRegex);
349
+ const rename = (node) => {
350
+ const trimmed = node.name.replace(options.mode === "prefix" ? /\$+$/g : /^\$+/g, "");
198
351
  const fixed = options.mode === "prefix" ? `$${trimmed}` : `${trimmed}$`;
199
- const data = {
200
- current,
352
+ return {
353
+ current: node.name,
201
354
  convention: options.mode,
202
355
  fixed
203
356
  };
204
- const suggestion = {
205
- messageId: "rename",
206
- data: {
207
- current,
208
- fixed
209
- },
210
- fix: (fixer) => fixer.replaceText(node.id, fixed)
211
- };
212
- context.report({
213
- node: node.id,
214
- messageId: "invalid",
215
- data,
216
- suggest: [suggestion]
217
- });
218
- } };
357
+ };
358
+ return {
359
+ [`${selector.variable}, ${selector.array.identifier}, ${selector.array.assignment}, ${selector.function.identifier}, ${selector.function.assignment}`]: (node) => {
360
+ const type = services.getTypeAtLocation(node);
361
+ if (!isType.store(type, services.program)) return;
362
+ const data = rename(node);
363
+ if (node.typeAnnotation) return context.report({
364
+ node,
365
+ messageId: "invalid",
366
+ data
367
+ });
368
+ const suggestion = {
369
+ messageId: "rename",
370
+ data: {
371
+ current: node.name,
372
+ fixed: data.fixed
373
+ },
374
+ fix: (fixer) => fixer.replaceText(node, data.fixed)
375
+ };
376
+ context.report({
377
+ node,
378
+ messageId: "invalid",
379
+ data,
380
+ suggest: [suggestion]
381
+ });
382
+ },
383
+ [`${selector.shape.identifier}, ${selector.shape.assignment}`]: (node) => {
384
+ const type = services.getTypeAtLocation(node.value);
385
+ const ident = node.value.type === AST_NODE_TYPES.Identifier ? node.value : node.value.left;
386
+ if (!isType.store(type, services.program)) return;
387
+ const data = rename(ident);
388
+ const suggestion = {
389
+ messageId: "rename",
390
+ data: {
391
+ current: ident.name,
392
+ fixed: data.fixed
393
+ },
394
+ fix: (fixer) => node.shorthand ? fixer.insertTextAfter(node.key, `: ${data.fixed}`) : fixer.replaceText(ident, data.fixed)
395
+ };
396
+ context.report({
397
+ node: ident,
398
+ messageId: "invalid",
399
+ data,
400
+ suggest: [suggestion]
401
+ });
402
+ }
403
+ };
219
404
  }
220
405
  });
221
406
  const PrefixRegex = /^[^$]/;
222
407
  const PostfixRegex = /[^$]$/;
223
- //#endregion
224
- //#region src/shared/package.ts
225
- const PACKAGE_NAME$1 = {
226
- core: /^effector(?:\u002Fcompat)?$/,
227
- react: /^effector-react$/,
228
- storage: /^@?effector-storage(\u002F[\w-]+)*$/
229
- };
408
+ const createSelector = (regex) => ({
409
+ variable: `VariableDeclarator > Identifier.id[name=${regex}]`,
410
+ array: {
411
+ identifier: `ArrayPattern > Identifier.elements[name=${regex}]`,
412
+ assignment: `ArrayPattern > AssignmentPattern > Identifier.left[name=${regex}]`
413
+ },
414
+ shape: {
415
+ identifier: `ObjectPattern > Property:has(> Identifier.value[name=${regex}])`,
416
+ assignment: `ObjectPattern > Property:has(> AssignmentPattern:has(> Identifier.left[name=${regex}]))`
417
+ },
418
+ function: {
419
+ identifier: `:function > Identifier.params[name=${regex}]`,
420
+ assignment: `:function > AssignmentPattern > Identifier.left[name=${regex}]`
421
+ }
422
+ });
230
423
  //#endregion
231
424
  //#region src/rules/keep-options-order/keep-options-order.ts
232
425
  var keep_options_order_default = createRule({
@@ -246,8 +439,8 @@ var keep_options_order_default = createRule({
246
439
  const source = context.sourceCode;
247
440
  const imports = /* @__PURE__ */ new Set();
248
441
  return {
249
- [`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ${selector$11.method}`]: (node) => imports.add(node.local.name),
250
- [`CallExpression${selector$11.call}:has(${selector$11.argument})`]: (node) => {
442
+ [`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ${selector$12.method}`]: (node) => imports.add(node.local.name),
443
+ [`CallExpression${selector$12.call}:has(${selector$12.argument})`]: (node) => {
251
444
  if (!imports.has(node.callee.name)) return;
252
445
  const [config] = node.arguments;
253
446
  if (config.properties.some((prop) => prop.type === AST_NODE_TYPES.SpreadElement || prop.key.type !== AST_NODE_TYPES.Identifier)) return;
@@ -286,7 +479,7 @@ const TRUE_ORDER = [
286
479
  "batch",
287
480
  "name"
288
481
  ];
289
- const selector$11 = {
482
+ const selector$12 = {
290
483
  method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
291
484
  call: `[callee.type="Identifier"][arguments.length=1]`,
292
485
  argument: `ObjectExpression.arguments`
@@ -311,70 +504,109 @@ function functionToName(node) {
311
504
  if (node.parent.type === AST_NODE_TYPES.AssignmentPattern && node.parent.left.type === AST_NODE_TYPES.Identifier) return node.parent.left;
312
505
  return null;
313
506
  }
314
- const nameOf = { function: functionToName };
507
+ function calleeToName(callee) {
508
+ if (callee.type === AST_NODE_TYPES.Identifier) return callee;
509
+ else if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) return callee.property;
510
+ else return null;
511
+ }
512
+ function simpleExpressionToName(node) {
513
+ if (node.type === AST_NODE_TYPES.Identifier) return node.name;
514
+ if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed) return node.property.name;
515
+ return null;
516
+ }
517
+ const nameOf = {
518
+ function: functionToName,
519
+ callee: calleeToName,
520
+ expression: { simple: simpleExpressionToName }
521
+ };
315
522
  //#endregion
316
523
  //#region src/rules/mandatory-scope-binding/mandatory-scope-binding.ts
317
524
  var mandatory_scope_binding_default = createRule({
318
525
  name: "mandatory-scope-binding",
319
526
  meta: {
320
527
  type: "problem",
321
- docs: { description: "Forbid `Event` and `Effect` usage without `useUnit` in React components." },
322
- messages: { useUnitNeeded: "\"{{ name }}\" must be wrapped with `useUnit` from `effector-react` before usage inside React components." },
528
+ docs: { description: "Forbid `Event` and `Effect` usage without `useUnit` in React." },
529
+ messages: { useUnitNeeded: "\"{{ name }}\" must be wrapped with `useUnit` from `effector-react` before usage inside React." },
323
530
  schema: []
324
531
  },
325
532
  defaultOptions: [],
326
533
  create: (context) => {
327
534
  const services = ESLintUtils.getParserServices(context);
328
535
  const checker = services.program.getTypeChecker();
329
- const stack = {
330
- render: [],
331
- hook: []
536
+ const inRender = [];
537
+ const inHook = [];
538
+ /** check if the expression is used in a context specifically expecting a unit */
539
+ const isExpectingUnit = (slot) => {
540
+ const tsnode = services.esTreeNodeToTSNodeMap.get(slot);
541
+ const type = checker.getContextualType(tsnode);
542
+ if (type) return isType.event(type, services.program) || isType.effect(type, services.program);
543
+ else return false;
544
+ };
545
+ const check = (mode, node) => {
546
+ if (!(inRender.at(-1) ?? false)) return;
547
+ const type = services.getTypeAtLocation(node);
548
+ if (!isType.event(type, services.program) && !isType.effect(type, services.program)) return;
549
+ if (mode === "call") return report(node);
550
+ const delegated = isExpectingUnit(node);
551
+ if ((mode === "jsx" || (inHook.at(-1) ?? false)) && delegated) return;
552
+ else return report(node);
553
+ };
554
+ const report = (node) => {
555
+ const name = nameOf.expression.simple(node) ?? "<expression>";
556
+ context.report({
557
+ node,
558
+ messageId: "useUnitNeeded",
559
+ data: { name }
560
+ });
332
561
  };
333
562
  return {
334
- [`FunctionDeclaration, FunctionExpression, ArrowFunctionExpression`]: (node) => {
335
- if (stack.render.at(-1) ?? false) return void stack.render.push(true);
563
+ [`:matches(${selector$11.function})`]: (node) => {
564
+ if (inRender.at(-1) ?? false) return void inRender.push(true);
336
565
  const name = nameOf.function(node);
337
- if (name && UseRegex$1.test(name.name)) return void stack.render.push(true);
566
+ if (name && UseRegex$1.test(name.name)) return void inRender.push(true);
338
567
  const tsnode = services.esTreeNodeToTSNodeMap.get(node);
339
568
  const signature = checker.getSignatureFromDeclaration(tsnode);
340
569
  const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getVoidType();
341
- if (returnType.isUnion() ? returnType.types.some((type) => isType.jsx(type, services.program)) : isType.jsx(returnType, services.program)) return void stack.render.push(true);
342
- const inferred = isExpression(tsnode) && getContextualType(checker, tsnode) || checker.getUnknownType();
343
- if (inferred.isUnion() ? inferred.types.some((type) => isType.component(type, services.program)) : isType.component(inferred, services.program)) return void stack.render.push(true);
344
- stack.render.push(false);
570
+ if (returnType.isUnion() ? returnType.types.some((type) => isType.jsx(type, services.program)) : isType.jsx(returnType, services.program)) return void inRender.push(true);
571
+ const inferred = ts.isExpression(tsnode) && getContextualType(checker, tsnode) || checker.getUnknownType();
572
+ if (inferred.isUnion() ? inferred.types.some((type) => isType.component(type, services.program)) : isType.component(inferred, services.program)) return void inRender.push(true);
573
+ inRender.push(false);
345
574
  },
346
- [`:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression):exit`]: () => void stack.render.pop(),
347
- "ClassDeclaration": () => void stack.render.push(false),
348
- "ClassDeclaration:exit": () => void stack.render.pop(),
575
+ [`:matches(${selector$11.function}):exit`]: () => void inRender.pop(),
576
+ "ClassDeclaration": () => void inRender.push(false),
577
+ "ClassDeclaration:exit": () => void inRender.pop(),
349
578
  "CallExpression": (node) => {
350
- const isHook = typeMatchesSpecifier(services.getTypeAtLocation(node.callee), {
351
- from: "package",
352
- package: "effector-react",
353
- name: [
354
- "useStore",
355
- "useStoreMap",
356
- "useList",
357
- "useEvent",
358
- "useUnit"
359
- ]
360
- }, services.program);
361
- stack.hook.push(isHook);
579
+ const id = nameOf.callee(node.callee), isEnteringHook = id !== null && UseRegex$1.test(id.name);
580
+ inHook.push(isEnteringHook);
362
581
  },
363
- "Identifier": (node) => {
364
- if (!(stack.render.at(-1) ?? false)) return;
365
- if (stack.hook.at(-1) ?? false) return;
366
- const type = services.getTypeAtLocation(node);
367
- if (!isType.event(type, services.program) && !isType.effect(type, services.program)) return;
368
- context.report({
369
- node,
370
- messageId: "useUnitNeeded",
371
- data: { name: node.name }
372
- });
373
- }
582
+ "CallExpression:exit": () => void inHook.pop(),
583
+ [`${selector$11.callee.direct}, ${selector$11.callee.member}`]: (node) => check("call", node),
584
+ [`${selector$11.arg.direct}, ${selector$11.arg.member}`]: (node) => check("arg", node),
585
+ [`${selector$11.prop.direct}, ${selector$11.prop.member}`]: (node) => check("prop", node),
586
+ [`${selector$11.jsx.direct}, ${selector$11.jsx.member}`]: (node) => check("jsx", node)
374
587
  };
375
588
  }
376
589
  });
377
590
  const UseRegex$1 = /^use[A-Z0-9].*$/;
591
+ const selector$11 = {
592
+ function: "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression",
593
+ callee: {
594
+ direct: "CallExpression > Identifier.callee",
595
+ member: "CallExpression > MemberExpression[computed=false].callee"
596
+ },
597
+ arg: {
598
+ direct: "CallExpression > Identifier:not(.callee)",
599
+ member: "CallExpression > MemberExpression[computed=false]:not(.callee)"
600
+ },
601
+ prop: {
602
+ direct: "CallExpression > ObjectExpression > Property > Identifier.value",
603
+ member: "CallExpression > ObjectExpression > Property > MemberExpression[computed=false].value"
604
+ },
605
+ jsx: {
606
+ direct: "JSXExpressionContainer > Identifier",
607
+ member: "JSXExpressionContainer > MemberExpression[computed=false]"
608
+ }
609
+ };
378
610
  //#endregion
379
611
  //#region src/shared/locate.ts
380
612
  const property = (key, node) => node.properties.find((prop) => prop.type == AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === key);
@@ -713,7 +945,7 @@ var no_getState_default = createRule({
713
945
  return { [`CallExpression[callee.type="MemberExpression"][callee.property.name="getState"]`]: (node) => {
714
946
  const type = services.getTypeAtLocation(node.callee.object);
715
947
  if (!isType.store(type, services.program)) return;
716
- const name = toName$1(node.callee.object);
948
+ const name = nameOf.expression.simple(node.callee.object);
717
949
  if (name) context.report({
718
950
  node,
719
951
  messageId: "named",
@@ -726,11 +958,6 @@ var no_getState_default = createRule({
726
958
  } };
727
959
  }
728
960
  });
729
- const toName$1 = (node) => {
730
- if (node.type === AST_NODE_TYPES.Identifier) return node.name;
731
- if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed) return node.property.name;
732
- return null;
733
- };
734
961
  //#endregion
735
962
  //#region src/rules/no-guard/no-guard.ts
736
963
  var no_guard_default = createRule({
@@ -1028,6 +1255,8 @@ function hasEffectorUnitInType(ctx, type, depth = 3) {
1028
1255
  }
1029
1256
  function isEffectorFactorioHook(callee, getTypeAtLocation) {
1030
1257
  if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
1258
+ if (callee.property.type !== AST_NODE_TYPES.Identifier) return false;
1259
+ if (callee.property.name !== "useModel") return false;
1031
1260
  const objectType = getTypeAtLocation(callee.object);
1032
1261
  const propertyNames = new Set(objectType.getProperties().map((p) => p.getName()));
1033
1262
  return EFFECTOR_FACTORIO_SHAPE.every((name) => propertyNames.has(name));
@@ -1218,10 +1447,8 @@ var no_useless_methods_default = createRule({
1218
1447
  if (locate.property("target", config)?.value) return;
1219
1448
  }
1220
1449
  const grandparent = node.parent.parent;
1221
- if (grandparent) {
1222
- const ancestry = source.getAncestors(grandparent);
1223
- if (esquery.matches(grandparent, query.watch, ancestry, { visitorKeys })) return;
1224
- }
1450
+ const ancestry = source.getAncestors(grandparent);
1451
+ if (esquery.matches(grandparent, query.watch, ancestry, { visitorKeys })) return;
1225
1452
  const method = node.callee.name;
1226
1453
  context.report({
1227
1454
  node,
@@ -1351,7 +1578,6 @@ var strict_effect_handlers_default = createRule({
1351
1578
  };
1352
1579
  const exit = (node) => {
1353
1580
  const scope = stack.pop();
1354
- if (!scope) return;
1355
1581
  if (scope.effect && scope.regular) context.report({
1356
1582
  node,
1357
1583
  messageId: "mixed"
@@ -1390,6 +1616,7 @@ const ruleset = {
1390
1616
  },
1391
1617
  react: {
1392
1618
  "effector/enforce-gate-naming-convention": "error",
1619
+ "effector/enforce-exhaustive-useUnit-destructuring": "warn",
1393
1620
  "effector/mandatory-scope-binding": "error",
1394
1621
  "effector/no-units-spawn-in-render": "error",
1395
1622
  "effector/prefer-useUnit": "error"
@@ -1406,6 +1633,7 @@ const base = {
1406
1633
  },
1407
1634
  rules: {
1408
1635
  "enforce-effect-naming-convention": enforce_effect_naming_convention_default,
1636
+ "enforce-exhaustive-useUnit-destructuring": enforce_exhaustive_useUnit_destructuring_default,
1409
1637
  "enforce-gate-naming-convention": enforce_gate_naming_convention_default,
1410
1638
  "enforce-store-naming-convention": enforce_store_naming_convention_default,
1411
1639
  "keep-options-order": keep_options_order_default,