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/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  <h1 align="center">eslint-plugin-effector</h1>
2
2
 
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/eslint-plugin-effector"><img src="https://badgen.net/npm/v/eslint-plugin-effector?color=blue" alt="npm version"></a>
5
+ <a href="https://www.npmjs.com/package/eslint-plugin-effector"><img src="https://badgen.net/badge/provenance/yes?color=green&icon=npm" alt="npm provenance"></a>
6
+ <a href="https://www.npmjs.com/package/eslint-plugin-effector"><img src="https://badgen.net/npm/dm/eslint-plugin-effector?color=orange" alt="monthly downloads"></a>
7
+ </p>
8
+
3
9
  An ESLint plugin for enforcing best practices for [Effector](https://effector.dev).
4
10
 
5
11
  For comprehensive documentation, including rules and configuration guides, visit official documentation at [eslint.effector.dev](https://eslint.effector.dev).
package/dist/index.cjs CHANGED
@@ -23,17 +23,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  let _typescript_eslint_utils = require("@typescript-eslint/utils");
24
24
  let _typescript_eslint_type_utils = require("@typescript-eslint/type-utils");
25
25
  let typescript = require("typescript");
26
+ typescript = __toESM(typescript, 1);
26
27
  let esquery = require("esquery");
27
- esquery = __toESM(esquery);
28
+ esquery = __toESM(esquery, 1);
28
29
  //#region package.json
29
30
  var name = "eslint-plugin-effector";
30
- var version = "0.18.0";
31
+ var version = "0.19.0";
31
32
  //#endregion
32
33
  //#region src/shared/create.ts
33
34
  const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://eslint.effector.dev/rules/${name}`);
34
35
  //#endregion
35
36
  //#region src/shared/is.ts
36
- const check = (symbol, types, from) => {
37
+ const check$1 = (symbol, types, from) => {
37
38
  const name = symbol.getName();
38
39
  const declarations = symbol.declarations ?? [];
39
40
  return types.includes(name) && declarations.map((decl) => decl.getSourceFile().fileName).some((fname) => fname.includes("node_modules") && fname.includes(from));
@@ -54,6 +55,11 @@ const isType = {
54
55
  package: "effector",
55
56
  name: "Effect"
56
57
  }, program),
58
+ domain: (type, program) => (0, _typescript_eslint_type_utils.typeMatchesSpecifier)(type, {
59
+ from: "package",
60
+ package: "effector",
61
+ name: "Domain"
62
+ }, program),
57
63
  unit: (type, program) => {
58
64
  return (0, _typescript_eslint_type_utils.typeMatchesSpecifier)(type, {
59
65
  from: "package",
@@ -68,14 +74,9 @@ const isType = {
68
74
  ]
69
75
  }, program);
70
76
  },
71
- domain: (type, program) => (0, _typescript_eslint_type_utils.typeMatchesSpecifier)(type, {
72
- from: "package",
73
- package: "effector",
74
- name: "Domain"
75
- }, program),
76
77
  gate: (type) => {
77
78
  const symbol = type.getSymbol() ?? type.aliasSymbol;
78
- return symbol ? check(symbol, ["Gate"], "effector") : false;
79
+ return symbol ? check$1(symbol, ["Gate"], "effector") : false;
79
80
  },
80
81
  jsx: (type, program) => {
81
82
  return (0, _typescript_eslint_type_utils.typeMatchesSpecifier)(type, {
@@ -110,7 +111,7 @@ var enforce_effect_naming_convention_default = createRule({
110
111
  type: "problem",
111
112
  docs: { description: "Enforce Fx as a suffix for any Effector Effect." },
112
113
  messages: {
113
- invalid: "Effect `{{ current }}` should be named with suffix, rename it to `{{ fixed }}`",
114
+ invalid: "Effect \"{{ current }}\" should be named with `Fx` suffix, rename it to \"{{ fixed }}\"",
114
115
  rename: "Rename \"{{ current }}\" to \"{{ fixed }}\""
115
116
  },
116
117
  schema: [],
@@ -119,33 +120,188 @@ var enforce_effect_naming_convention_default = createRule({
119
120
  defaultOptions: [],
120
121
  create: (context) => {
121
122
  const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
122
- return { [`VariableDeclarator[id.name!=${FxRegex}]`]: (node) => {
123
- const type = services.getTypeAtLocation(node);
124
- if (!isType.effect(type, services.program)) return;
125
- const current = node.id.name;
126
- const fixed = current + "Fx";
127
- const data = {
128
- current,
129
- fixed
130
- };
131
- const suggestion = {
132
- messageId: "rename",
133
- data: {
134
- current,
135
- fixed
136
- },
137
- fix: (fixer) => fixer.replaceText(node.id, fixed)
138
- };
139
- context.report({
140
- node: node.id,
141
- messageId: "invalid",
142
- data,
143
- suggest: [suggestion]
144
- });
145
- } };
123
+ return {
124
+ [`${selector$14.variable}, ${selector$14.array.identifier}, ${selector$14.array.assignment}, ${selector$14.function.identifier}, ${selector$14.function.assignment}`]: (node) => {
125
+ const type = services.getTypeAtLocation(node);
126
+ if (!isType.effect(type, services.program)) return;
127
+ const data = {
128
+ current: node.name,
129
+ fixed: node.name + "Fx"
130
+ };
131
+ if (node.typeAnnotation) return context.report({
132
+ node,
133
+ messageId: "invalid",
134
+ data
135
+ });
136
+ const suggestion = {
137
+ messageId: "rename",
138
+ data: {
139
+ current: node.name,
140
+ fixed: data.fixed
141
+ },
142
+ fix: (fixer) => fixer.replaceText(node, data.fixed)
143
+ };
144
+ context.report({
145
+ node,
146
+ messageId: "invalid",
147
+ data,
148
+ suggest: [suggestion]
149
+ });
150
+ },
151
+ [`${selector$14.shape.identifier}, ${selector$14.shape.assignment}`]: (node) => {
152
+ const type = services.getTypeAtLocation(node.value);
153
+ const ident = node.value.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier ? node.value : node.value.left;
154
+ if (!isType.effect(type, services.program)) return;
155
+ const data = {
156
+ current: ident.name,
157
+ fixed: ident.name + "Fx"
158
+ };
159
+ const suggestion = {
160
+ messageId: "rename",
161
+ data: {
162
+ current: ident.name,
163
+ fixed: data.fixed
164
+ },
165
+ fix: (fixer) => node.shorthand ? fixer.insertTextAfter(node.key, `: ${data.fixed}`) : fixer.replaceText(ident, data.fixed)
166
+ };
167
+ context.report({
168
+ node: ident,
169
+ messageId: "invalid",
170
+ data,
171
+ suggest: [suggestion]
172
+ });
173
+ }
174
+ };
146
175
  }
147
176
  });
148
177
  const FxRegex = /Fx$/;
178
+ const selector$14 = {
179
+ variable: `VariableDeclarator > Identifier.id[name!=${FxRegex}]`,
180
+ array: {
181
+ identifier: `ArrayPattern > Identifier.elements[name!=${FxRegex}]`,
182
+ assignment: `ArrayPattern > AssignmentPattern > Identifier.left[name!=${FxRegex}]`
183
+ },
184
+ shape: {
185
+ identifier: `ObjectPattern > Property:has(> Identifier.value[name!=${FxRegex}])`,
186
+ assignment: `ObjectPattern > Property:has(> AssignmentPattern:has(> Identifier.left[name!=${FxRegex}]))`
187
+ },
188
+ function: {
189
+ identifier: `:function > Identifier.params[name!=${FxRegex}]`,
190
+ assignment: `:function > AssignmentPattern > Identifier.left[name!=${FxRegex}]`
191
+ }
192
+ };
193
+ //#endregion
194
+ //#region src/shared/package.ts
195
+ const PACKAGE_NAME$1 = {
196
+ core: /^effector(?:\u002Fcompat)?$/,
197
+ react: /^effector-react$/,
198
+ storage: /^@?effector-storage(\u002F[\w-]+)*$/
199
+ };
200
+ //#endregion
201
+ //#region src/rules/enforce-exhaustive-useUnit-destructuring/enforce-exhaustive-useUnit-destructuring.ts
202
+ var enforce_exhaustive_useUnit_destructuring_default = createRule({
203
+ name: "enforce-exhaustive-useUnit-destructuring",
204
+ meta: {
205
+ type: "problem",
206
+ docs: { description: "Ensure all units passed to useUnit are properly destructured." },
207
+ messages: {
208
+ unusedKey: "Property \"{{name}}\" is passed but not destructured.",
209
+ missingKey: "Property \"{{name}}\" is destructured but not passed in the unit object."
210
+ },
211
+ schema: [],
212
+ defaultOptions: []
213
+ },
214
+ create(context) {
215
+ const importedAs = /* @__PURE__ */ new Set();
216
+ return {
217
+ [selector$13.import]: (node) => void importedAs.add(node.local.name),
218
+ [`${selector$13.variable.shape}:has(> ${selector$13.call}:has(${selector$13.arg.shape}))`](node) {
219
+ if (!importedAs.has(node.init.callee.name)) return;
220
+ const provided = shapeToKeyMap(node.init.arguments[0]);
221
+ const consumed = shapeToKeyMap(node.id);
222
+ if (provided === null || consumed === null) return;
223
+ for (const { type, name } of check(provided, consumed)) if (type === "unused") context.report({
224
+ node: node.init.arguments[0],
225
+ messageId: "unusedKey",
226
+ data: { name }
227
+ });
228
+ else context.report({
229
+ node: node.id,
230
+ messageId: "missingKey",
231
+ data: { name }
232
+ });
233
+ },
234
+ [`${selector$13.variable.list}:has(> ${selector$13.call}:has(${selector$13.arg.list}))`](node) {
235
+ if (!importedAs.has(node.init.callee.name)) return;
236
+ const provided = listToKeyMap(node.init.arguments[0]);
237
+ const consumed = listToKeyMap(node.id);
238
+ if (provided === null || consumed === null) return;
239
+ for (const { type, name } of check(provided, consumed)) if (type === "unused") context.report({
240
+ node: node.init.arguments[0],
241
+ messageId: "unusedKey",
242
+ data: { name }
243
+ });
244
+ else context.report({
245
+ node: node.id,
246
+ messageId: "missingKey",
247
+ data: { name }
248
+ });
249
+ }
250
+ };
251
+ }
252
+ });
253
+ const selector$13 = {
254
+ import: `ImportDeclaration[source.value=${PACKAGE_NAME$1.react}] > ImportSpecifier[imported.name=useUnit]`,
255
+ variable: {
256
+ shape: "VariableDeclarator[id.type=ObjectPattern]",
257
+ list: "VariableDeclarator[id.type=ArrayPattern]"
258
+ },
259
+ call: "CallExpression.init[arguments.length=1][callee.type=Identifier]",
260
+ arg: {
261
+ shape: "ObjectExpression.arguments",
262
+ list: "ArrayExpression.arguments"
263
+ }
264
+ };
265
+ function toName$1(key, node) {
266
+ if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return node.name;
267
+ if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.Literal) return String(node.value);
268
+ if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && node.property.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return `${toName$1(key, node.object)}.${node.property.name}`;
269
+ return `<unknown at ${key}>`;
270
+ }
271
+ function toKey(prop) {
272
+ if (prop.computed) return null;
273
+ else if (prop.key.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return prop.key.name;
274
+ else return prop.key.value;
275
+ }
276
+ function* check(provided, consumed) {
277
+ for (const [key, node] of provided) if (!consumed.has(key)) yield {
278
+ type: "unused",
279
+ name: toName$1(key, node)
280
+ };
281
+ for (const [key, node] of consumed) if (!provided.has(key)) yield {
282
+ type: "missing",
283
+ name: toName$1(key, node)
284
+ };
285
+ }
286
+ function shapeToKeyMap(shape) {
287
+ const map = /* @__PURE__ */ new Map();
288
+ for (const prop of shape.properties) {
289
+ if (prop.type !== _typescript_eslint_utils.AST_NODE_TYPES.Property) return null;
290
+ const key = toKey(prop);
291
+ if (key === null) return null;
292
+ else map.set(key, prop.key);
293
+ }
294
+ return map;
295
+ }
296
+ function listToKeyMap(list) {
297
+ const map = /* @__PURE__ */ new Map();
298
+ for (const [index, element] of list.elements.entries()) {
299
+ if (element === null) continue;
300
+ if (element.type === _typescript_eslint_utils.AST_NODE_TYPES.RestElement || element.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) return null;
301
+ map.set(index, element);
302
+ }
303
+ return map;
304
+ }
149
305
  //#endregion
150
306
  //#region src/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.ts
151
307
  var enforce_gate_naming_convention_default = createRule({
@@ -213,43 +369,81 @@ var enforce_store_naming_convention_default = createRule({
213
369
  defaultOptions: [{ mode: "prefix" }],
214
370
  create: (context, [options]) => {
215
371
  const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
216
- return { [`VariableDeclarator[id.name=${options.mode === "prefix" ? PrefixRegex : PostfixRegex}]`]: (node) => {
217
- const type = services.getTypeAtLocation(node);
218
- if (!isType.store(type, services.program)) return;
219
- const current = node.id.name;
220
- const trimmed = current.replaceAll(options.mode === "prefix" ? /\$+$/g : /^\$+/g, "");
372
+ const selector = createSelector(options.mode === "prefix" ? PrefixRegex : PostfixRegex);
373
+ const rename = (node) => {
374
+ const trimmed = node.name.replace(options.mode === "prefix" ? /\$+$/g : /^\$+/g, "");
221
375
  const fixed = options.mode === "prefix" ? `$${trimmed}` : `${trimmed}$`;
222
- const data = {
223
- current,
376
+ return {
377
+ current: node.name,
224
378
  convention: options.mode,
225
379
  fixed
226
380
  };
227
- const suggestion = {
228
- messageId: "rename",
229
- data: {
230
- current,
231
- fixed
232
- },
233
- fix: (fixer) => fixer.replaceText(node.id, fixed)
234
- };
235
- context.report({
236
- node: node.id,
237
- messageId: "invalid",
238
- data,
239
- suggest: [suggestion]
240
- });
241
- } };
381
+ };
382
+ return {
383
+ [`${selector.variable}, ${selector.array.identifier}, ${selector.array.assignment}, ${selector.function.identifier}, ${selector.function.assignment}`]: (node) => {
384
+ const type = services.getTypeAtLocation(node);
385
+ if (!isType.store(type, services.program)) return;
386
+ const data = rename(node);
387
+ if (node.typeAnnotation) return context.report({
388
+ node,
389
+ messageId: "invalid",
390
+ data
391
+ });
392
+ const suggestion = {
393
+ messageId: "rename",
394
+ data: {
395
+ current: node.name,
396
+ fixed: data.fixed
397
+ },
398
+ fix: (fixer) => fixer.replaceText(node, data.fixed)
399
+ };
400
+ context.report({
401
+ node,
402
+ messageId: "invalid",
403
+ data,
404
+ suggest: [suggestion]
405
+ });
406
+ },
407
+ [`${selector.shape.identifier}, ${selector.shape.assignment}`]: (node) => {
408
+ const type = services.getTypeAtLocation(node.value);
409
+ const ident = node.value.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier ? node.value : node.value.left;
410
+ if (!isType.store(type, services.program)) return;
411
+ const data = rename(ident);
412
+ const suggestion = {
413
+ messageId: "rename",
414
+ data: {
415
+ current: ident.name,
416
+ fixed: data.fixed
417
+ },
418
+ fix: (fixer) => node.shorthand ? fixer.insertTextAfter(node.key, `: ${data.fixed}`) : fixer.replaceText(ident, data.fixed)
419
+ };
420
+ context.report({
421
+ node: ident,
422
+ messageId: "invalid",
423
+ data,
424
+ suggest: [suggestion]
425
+ });
426
+ }
427
+ };
242
428
  }
243
429
  });
244
430
  const PrefixRegex = /^[^$]/;
245
431
  const PostfixRegex = /[^$]$/;
246
- //#endregion
247
- //#region src/shared/package.ts
248
- const PACKAGE_NAME$1 = {
249
- core: /^effector(?:\u002Fcompat)?$/,
250
- react: /^effector-react$/,
251
- storage: /^@?effector-storage(\u002F[\w-]+)*$/
252
- };
432
+ const createSelector = (regex) => ({
433
+ variable: `VariableDeclarator > Identifier.id[name=${regex}]`,
434
+ array: {
435
+ identifier: `ArrayPattern > Identifier.elements[name=${regex}]`,
436
+ assignment: `ArrayPattern > AssignmentPattern > Identifier.left[name=${regex}]`
437
+ },
438
+ shape: {
439
+ identifier: `ObjectPattern > Property:has(> Identifier.value[name=${regex}])`,
440
+ assignment: `ObjectPattern > Property:has(> AssignmentPattern:has(> Identifier.left[name=${regex}]))`
441
+ },
442
+ function: {
443
+ identifier: `:function > Identifier.params[name=${regex}]`,
444
+ assignment: `:function > AssignmentPattern > Identifier.left[name=${regex}]`
445
+ }
446
+ });
253
447
  //#endregion
254
448
  //#region src/rules/keep-options-order/keep-options-order.ts
255
449
  var keep_options_order_default = createRule({
@@ -269,8 +463,8 @@ var keep_options_order_default = createRule({
269
463
  const source = context.sourceCode;
270
464
  const imports = /* @__PURE__ */ new Set();
271
465
  return {
272
- [`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ${selector$11.method}`]: (node) => imports.add(node.local.name),
273
- [`CallExpression${selector$11.call}:has(${selector$11.argument})`]: (node) => {
466
+ [`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ${selector$12.method}`]: (node) => imports.add(node.local.name),
467
+ [`CallExpression${selector$12.call}:has(${selector$12.argument})`]: (node) => {
274
468
  if (!imports.has(node.callee.name)) return;
275
469
  const [config] = node.arguments;
276
470
  if (config.properties.some((prop) => prop.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement || prop.key.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier)) return;
@@ -309,7 +503,7 @@ const TRUE_ORDER = [
309
503
  "batch",
310
504
  "name"
311
505
  ];
312
- const selector$11 = {
506
+ const selector$12 = {
313
507
  method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
314
508
  call: `[callee.type="Identifier"][arguments.length=1]`,
315
509
  argument: `ObjectExpression.arguments`
@@ -334,70 +528,109 @@ function functionToName(node) {
334
528
  if (node.parent.type === _typescript_eslint_utils.AST_NODE_TYPES.AssignmentPattern && node.parent.left.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return node.parent.left;
335
529
  return null;
336
530
  }
337
- const nameOf = { function: functionToName };
531
+ function calleeToName(callee) {
532
+ if (callee.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return callee;
533
+ else if (callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && callee.property.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return callee.property;
534
+ else return null;
535
+ }
536
+ function simpleExpressionToName(node) {
537
+ if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return node.name;
538
+ if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && !node.computed) return node.property.name;
539
+ return null;
540
+ }
541
+ const nameOf = {
542
+ function: functionToName,
543
+ callee: calleeToName,
544
+ expression: { simple: simpleExpressionToName }
545
+ };
338
546
  //#endregion
339
547
  //#region src/rules/mandatory-scope-binding/mandatory-scope-binding.ts
340
548
  var mandatory_scope_binding_default = createRule({
341
549
  name: "mandatory-scope-binding",
342
550
  meta: {
343
551
  type: "problem",
344
- docs: { description: "Forbid `Event` and `Effect` usage without `useUnit` in React components." },
345
- messages: { useUnitNeeded: "\"{{ name }}\" must be wrapped with `useUnit` from `effector-react` before usage inside React components." },
552
+ docs: { description: "Forbid `Event` and `Effect` usage without `useUnit` in React." },
553
+ messages: { useUnitNeeded: "\"{{ name }}\" must be wrapped with `useUnit` from `effector-react` before usage inside React." },
346
554
  schema: []
347
555
  },
348
556
  defaultOptions: [],
349
557
  create: (context) => {
350
558
  const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
351
559
  const checker = services.program.getTypeChecker();
352
- const stack = {
353
- render: [],
354
- hook: []
560
+ const inRender = [];
561
+ const inHook = [];
562
+ /** check if the expression is used in a context specifically expecting a unit */
563
+ const isExpectingUnit = (slot) => {
564
+ const tsnode = services.esTreeNodeToTSNodeMap.get(slot);
565
+ const type = checker.getContextualType(tsnode);
566
+ if (type) return isType.event(type, services.program) || isType.effect(type, services.program);
567
+ else return false;
568
+ };
569
+ const check = (mode, node) => {
570
+ if (!(inRender.at(-1) ?? false)) return;
571
+ const type = services.getTypeAtLocation(node);
572
+ if (!isType.event(type, services.program) && !isType.effect(type, services.program)) return;
573
+ if (mode === "call") return report(node);
574
+ const delegated = isExpectingUnit(node);
575
+ if ((mode === "jsx" || (inHook.at(-1) ?? false)) && delegated) return;
576
+ else return report(node);
577
+ };
578
+ const report = (node) => {
579
+ const name = nameOf.expression.simple(node) ?? "<expression>";
580
+ context.report({
581
+ node,
582
+ messageId: "useUnitNeeded",
583
+ data: { name }
584
+ });
355
585
  };
356
586
  return {
357
- [`FunctionDeclaration, FunctionExpression, ArrowFunctionExpression`]: (node) => {
358
- if (stack.render.at(-1) ?? false) return void stack.render.push(true);
587
+ [`:matches(${selector$11.function})`]: (node) => {
588
+ if (inRender.at(-1) ?? false) return void inRender.push(true);
359
589
  const name = nameOf.function(node);
360
- if (name && UseRegex$1.test(name.name)) return void stack.render.push(true);
590
+ if (name && UseRegex$1.test(name.name)) return void inRender.push(true);
361
591
  const tsnode = services.esTreeNodeToTSNodeMap.get(node);
362
592
  const signature = checker.getSignatureFromDeclaration(tsnode);
363
593
  const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getVoidType();
364
- if (returnType.isUnion() ? returnType.types.some((type) => isType.jsx(type, services.program)) : isType.jsx(returnType, services.program)) return void stack.render.push(true);
365
- const inferred = (0, typescript.isExpression)(tsnode) && (0, _typescript_eslint_type_utils.getContextualType)(checker, tsnode) || checker.getUnknownType();
366
- if (inferred.isUnion() ? inferred.types.some((type) => isType.component(type, services.program)) : isType.component(inferred, services.program)) return void stack.render.push(true);
367
- stack.render.push(false);
594
+ if (returnType.isUnion() ? returnType.types.some((type) => isType.jsx(type, services.program)) : isType.jsx(returnType, services.program)) return void inRender.push(true);
595
+ const inferred = typescript.default.isExpression(tsnode) && (0, _typescript_eslint_type_utils.getContextualType)(checker, tsnode) || checker.getUnknownType();
596
+ if (inferred.isUnion() ? inferred.types.some((type) => isType.component(type, services.program)) : isType.component(inferred, services.program)) return void inRender.push(true);
597
+ inRender.push(false);
368
598
  },
369
- [`:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression):exit`]: () => void stack.render.pop(),
370
- "ClassDeclaration": () => void stack.render.push(false),
371
- "ClassDeclaration:exit": () => void stack.render.pop(),
599
+ [`:matches(${selector$11.function}):exit`]: () => void inRender.pop(),
600
+ "ClassDeclaration": () => void inRender.push(false),
601
+ "ClassDeclaration:exit": () => void inRender.pop(),
372
602
  "CallExpression": (node) => {
373
- const isHook = (0, _typescript_eslint_type_utils.typeMatchesSpecifier)(services.getTypeAtLocation(node.callee), {
374
- from: "package",
375
- package: "effector-react",
376
- name: [
377
- "useStore",
378
- "useStoreMap",
379
- "useList",
380
- "useEvent",
381
- "useUnit"
382
- ]
383
- }, services.program);
384
- stack.hook.push(isHook);
603
+ const id = nameOf.callee(node.callee), isEnteringHook = id !== null && UseRegex$1.test(id.name);
604
+ inHook.push(isEnteringHook);
385
605
  },
386
- "Identifier": (node) => {
387
- if (!(stack.render.at(-1) ?? false)) return;
388
- if (stack.hook.at(-1) ?? false) return;
389
- const type = services.getTypeAtLocation(node);
390
- if (!isType.event(type, services.program) && !isType.effect(type, services.program)) return;
391
- context.report({
392
- node,
393
- messageId: "useUnitNeeded",
394
- data: { name: node.name }
395
- });
396
- }
606
+ "CallExpression:exit": () => void inHook.pop(),
607
+ [`${selector$11.callee.direct}, ${selector$11.callee.member}`]: (node) => check("call", node),
608
+ [`${selector$11.arg.direct}, ${selector$11.arg.member}`]: (node) => check("arg", node),
609
+ [`${selector$11.prop.direct}, ${selector$11.prop.member}`]: (node) => check("prop", node),
610
+ [`${selector$11.jsx.direct}, ${selector$11.jsx.member}`]: (node) => check("jsx", node)
397
611
  };
398
612
  }
399
613
  });
400
614
  const UseRegex$1 = /^use[A-Z0-9].*$/;
615
+ const selector$11 = {
616
+ function: "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression",
617
+ callee: {
618
+ direct: "CallExpression > Identifier.callee",
619
+ member: "CallExpression > MemberExpression[computed=false].callee"
620
+ },
621
+ arg: {
622
+ direct: "CallExpression > Identifier:not(.callee)",
623
+ member: "CallExpression > MemberExpression[computed=false]:not(.callee)"
624
+ },
625
+ prop: {
626
+ direct: "CallExpression > ObjectExpression > Property > Identifier.value",
627
+ member: "CallExpression > ObjectExpression > Property > MemberExpression[computed=false].value"
628
+ },
629
+ jsx: {
630
+ direct: "JSXExpressionContainer > Identifier",
631
+ member: "JSXExpressionContainer > MemberExpression[computed=false]"
632
+ }
633
+ };
401
634
  //#endregion
402
635
  //#region src/shared/locate.ts
403
636
  const property = (key, node) => node.properties.find((prop) => prop.type == _typescript_eslint_utils.AST_NODE_TYPES.Property && prop.key.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && prop.key.name === key);
@@ -736,7 +969,7 @@ var no_getState_default = createRule({
736
969
  return { [`CallExpression[callee.type="MemberExpression"][callee.property.name="getState"]`]: (node) => {
737
970
  const type = services.getTypeAtLocation(node.callee.object);
738
971
  if (!isType.store(type, services.program)) return;
739
- const name = toName$1(node.callee.object);
972
+ const name = nameOf.expression.simple(node.callee.object);
740
973
  if (name) context.report({
741
974
  node,
742
975
  messageId: "named",
@@ -749,11 +982,6 @@ var no_getState_default = createRule({
749
982
  } };
750
983
  }
751
984
  });
752
- const toName$1 = (node) => {
753
- if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return node.name;
754
- if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && !node.computed) return node.property.name;
755
- return null;
756
- };
757
985
  //#endregion
758
986
  //#region src/rules/no-guard/no-guard.ts
759
987
  var no_guard_default = createRule({
@@ -1051,6 +1279,8 @@ function hasEffectorUnitInType(ctx, type, depth = 3) {
1051
1279
  }
1052
1280
  function isEffectorFactorioHook(callee, getTypeAtLocation) {
1053
1281
  if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression) return false;
1282
+ if (callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return false;
1283
+ if (callee.property.name !== "useModel") return false;
1054
1284
  const objectType = getTypeAtLocation(callee.object);
1055
1285
  const propertyNames = new Set(objectType.getProperties().map((p) => p.getName()));
1056
1286
  return EFFECTOR_FACTORIO_SHAPE.every((name) => propertyNames.has(name));
@@ -1241,10 +1471,8 @@ var no_useless_methods_default = createRule({
1241
1471
  if (locate.property("target", config)?.value) return;
1242
1472
  }
1243
1473
  const grandparent = node.parent.parent;
1244
- if (grandparent) {
1245
- const ancestry = source.getAncestors(grandparent);
1246
- if (esquery.default.matches(grandparent, query.watch, ancestry, { visitorKeys })) return;
1247
- }
1474
+ const ancestry = source.getAncestors(grandparent);
1475
+ if (esquery.default.matches(grandparent, query.watch, ancestry, { visitorKeys })) return;
1248
1476
  const method = node.callee.name;
1249
1477
  context.report({
1250
1478
  node,
@@ -1374,7 +1602,6 @@ var strict_effect_handlers_default = createRule({
1374
1602
  };
1375
1603
  const exit = (node) => {
1376
1604
  const scope = stack.pop();
1377
- if (!scope) return;
1378
1605
  if (scope.effect && scope.regular) context.report({
1379
1606
  node,
1380
1607
  messageId: "mixed"
@@ -1413,6 +1640,7 @@ const ruleset = {
1413
1640
  },
1414
1641
  react: {
1415
1642
  "effector/enforce-gate-naming-convention": "error",
1643
+ "effector/enforce-exhaustive-useUnit-destructuring": "warn",
1416
1644
  "effector/mandatory-scope-binding": "error",
1417
1645
  "effector/no-units-spawn-in-render": "error",
1418
1646
  "effector/prefer-useUnit": "error"
@@ -1429,6 +1657,7 @@ const base = {
1429
1657
  },
1430
1658
  rules: {
1431
1659
  "enforce-effect-naming-convention": enforce_effect_naming_convention_default,
1660
+ "enforce-exhaustive-useUnit-destructuring": enforce_exhaustive_useUnit_destructuring_default,
1432
1661
  "enforce-gate-naming-convention": enforce_gate_naming_convention_default,
1433
1662
  "enforce-store-naming-convention": enforce_store_naming_convention_default,
1434
1663
  "keep-options-order": keep_options_order_default,