eslint-plugin-effector 0.17.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 +6 -0
- package/dist/index.cjs +634 -176
- package/dist/index.d.cts +71 -80
- package/dist/index.d.mts +71 -80
- package/dist/index.mjs +606 -146
- package/package.json +23 -21
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
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
|
+
//#region package.json
|
|
5
6
|
var name = "eslint-plugin-effector";
|
|
6
|
-
var version = "0.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
var version = "0.19.0";
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/shared/create.ts
|
|
10
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://eslint.effector.dev/rules/${name}`);
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/shared/is.ts
|
|
13
|
+
const check$1 = (symbol, types, from) => {
|
|
14
|
+
const name = symbol.getName();
|
|
10
15
|
const declarations = symbol.declarations ?? [];
|
|
11
|
-
return types.includes(name
|
|
16
|
+
return types.includes(name) && declarations.map((decl) => decl.getSourceFile().fileName).some((fname) => fname.includes("node_modules") && fname.includes(from));
|
|
12
17
|
};
|
|
13
18
|
const isType = {
|
|
14
19
|
store: (type, program) => typeMatchesSpecifier(type, {
|
|
@@ -26,6 +31,11 @@ const isType = {
|
|
|
26
31
|
package: "effector",
|
|
27
32
|
name: "Effect"
|
|
28
33
|
}, program),
|
|
34
|
+
domain: (type, program) => typeMatchesSpecifier(type, {
|
|
35
|
+
from: "package",
|
|
36
|
+
package: "effector",
|
|
37
|
+
name: "Domain"
|
|
38
|
+
}, program),
|
|
29
39
|
unit: (type, program) => {
|
|
30
40
|
return typeMatchesSpecifier(type, {
|
|
31
41
|
from: "package",
|
|
@@ -35,18 +45,14 @@ const isType = {
|
|
|
35
45
|
"StoreWritable",
|
|
36
46
|
"Event",
|
|
37
47
|
"EventCallable",
|
|
38
|
-
"Effect"
|
|
48
|
+
"Effect",
|
|
49
|
+
"Domain"
|
|
39
50
|
]
|
|
40
51
|
}, program);
|
|
41
52
|
},
|
|
42
|
-
domain: (type, program) => typeMatchesSpecifier(type, {
|
|
43
|
-
from: "package",
|
|
44
|
-
package: "effector",
|
|
45
|
-
name: "Domain"
|
|
46
|
-
}, program),
|
|
47
53
|
gate: (type) => {
|
|
48
54
|
const symbol = type.getSymbol() ?? type.aliasSymbol;
|
|
49
|
-
return symbol ? check(symbol, ["Gate"], "effector") : false;
|
|
55
|
+
return symbol ? check$1(symbol, ["Gate"], "effector") : false;
|
|
50
56
|
},
|
|
51
57
|
jsx: (type, program) => {
|
|
52
58
|
return typeMatchesSpecifier(type, {
|
|
@@ -73,13 +79,15 @@ const isType = {
|
|
|
73
79
|
}, program);
|
|
74
80
|
}
|
|
75
81
|
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.ts
|
|
76
84
|
var enforce_effect_naming_convention_default = createRule({
|
|
77
85
|
name: "enforce-effect-naming-convention",
|
|
78
86
|
meta: {
|
|
79
87
|
type: "problem",
|
|
80
88
|
docs: { description: "Enforce Fx as a suffix for any Effector Effect." },
|
|
81
89
|
messages: {
|
|
82
|
-
invalid: "Effect
|
|
90
|
+
invalid: "Effect \"{{ current }}\" should be named with `Fx` suffix, rename it to \"{{ fixed }}\"",
|
|
83
91
|
rename: "Rename \"{{ current }}\" to \"{{ fixed }}\""
|
|
84
92
|
},
|
|
85
93
|
schema: [],
|
|
@@ -88,33 +96,190 @@ var enforce_effect_naming_convention_default = createRule({
|
|
|
88
96
|
defaultOptions: [],
|
|
89
97
|
create: (context) => {
|
|
90
98
|
const services = ESLintUtils.getParserServices(context);
|
|
91
|
-
return {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
};
|
|
115
151
|
}
|
|
116
152
|
});
|
|
117
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
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.ts
|
|
118
283
|
var enforce_gate_naming_convention_default = createRule({
|
|
119
284
|
name: "enforce-gate-naming-convention",
|
|
120
285
|
meta: {
|
|
@@ -157,6 +322,8 @@ var enforce_gate_naming_convention_default = createRule({
|
|
|
157
322
|
}
|
|
158
323
|
});
|
|
159
324
|
const GateRegex = /^[^A-Z]/;
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/rules/enforce-store-naming-convention/enforce-store-naming-convention.ts
|
|
160
327
|
var enforce_store_naming_convention_default = createRule({
|
|
161
328
|
name: "enforce-store-naming-convention",
|
|
162
329
|
meta: {
|
|
@@ -178,41 +345,83 @@ var enforce_store_naming_convention_default = createRule({
|
|
|
178
345
|
defaultOptions: [{ mode: "prefix" }],
|
|
179
346
|
create: (context, [options]) => {
|
|
180
347
|
const services = ESLintUtils.getParserServices(context);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const current = node.id.name;
|
|
185
|
-
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, "");
|
|
186
351
|
const fixed = options.mode === "prefix" ? `$${trimmed}` : `${trimmed}$`;
|
|
187
|
-
|
|
188
|
-
current,
|
|
352
|
+
return {
|
|
353
|
+
current: node.name,
|
|
189
354
|
convention: options.mode,
|
|
190
355
|
fixed
|
|
191
356
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
};
|
|
207
404
|
}
|
|
208
405
|
});
|
|
209
406
|
const PrefixRegex = /^[^$]/;
|
|
210
407
|
const PostfixRegex = /[^$]$/;
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
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
|
+
});
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/rules/keep-options-order/keep-options-order.ts
|
|
216
425
|
var keep_options_order_default = createRule({
|
|
217
426
|
name: "keep-options-order",
|
|
218
427
|
meta: {
|
|
@@ -230,8 +439,8 @@ var keep_options_order_default = createRule({
|
|
|
230
439
|
const source = context.sourceCode;
|
|
231
440
|
const imports = /* @__PURE__ */ new Set();
|
|
232
441
|
return {
|
|
233
|
-
[`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ${selector$
|
|
234
|
-
[`CallExpression${selector$
|
|
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) => {
|
|
235
444
|
if (!imports.has(node.callee.name)) return;
|
|
236
445
|
const [config] = node.arguments;
|
|
237
446
|
if (config.properties.some((prop) => prop.type === AST_NODE_TYPES.SpreadElement || prop.key.type !== AST_NODE_TYPES.Identifier)) return;
|
|
@@ -270,7 +479,7 @@ const TRUE_ORDER = [
|
|
|
270
479
|
"batch",
|
|
271
480
|
"name"
|
|
272
481
|
];
|
|
273
|
-
const selector$
|
|
482
|
+
const selector$12 = {
|
|
274
483
|
method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
|
|
275
484
|
call: `[callee.type="Identifier"][arguments.length=1]`,
|
|
276
485
|
argument: `ObjectExpression.arguments`
|
|
@@ -285,6 +494,8 @@ const isCorrectOrder = (current) => {
|
|
|
285
494
|
}
|
|
286
495
|
return true;
|
|
287
496
|
};
|
|
497
|
+
//#endregion
|
|
498
|
+
//#region src/shared/name.ts
|
|
288
499
|
function functionToName(node) {
|
|
289
500
|
if (node.id) return node.id;
|
|
290
501
|
if (node.parent.type === AST_NODE_TYPES.VariableDeclarator && node.parent.id.type === AST_NODE_TYPES.Identifier) return node.parent.id;
|
|
@@ -293,70 +504,115 @@ function functionToName(node) {
|
|
|
293
504
|
if (node.parent.type === AST_NODE_TYPES.AssignmentPattern && node.parent.left.type === AST_NODE_TYPES.Identifier) return node.parent.left;
|
|
294
505
|
return null;
|
|
295
506
|
}
|
|
296
|
-
|
|
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
|
+
};
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/rules/mandatory-scope-binding/mandatory-scope-binding.ts
|
|
297
524
|
var mandatory_scope_binding_default = createRule({
|
|
298
525
|
name: "mandatory-scope-binding",
|
|
299
526
|
meta: {
|
|
300
527
|
type: "problem",
|
|
301
|
-
docs: { description: "Forbid `Event` and `Effect` usage without `useUnit` in React
|
|
302
|
-
messages: { useUnitNeeded: "\"{{ name }}\" must be wrapped with `useUnit` from `effector-react` before usage inside React
|
|
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." },
|
|
303
530
|
schema: []
|
|
304
531
|
},
|
|
305
532
|
defaultOptions: [],
|
|
306
533
|
create: (context) => {
|
|
307
534
|
const services = ESLintUtils.getParserServices(context);
|
|
308
535
|
const checker = services.program.getTypeChecker();
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
});
|
|
312
561
|
};
|
|
313
562
|
return {
|
|
314
|
-
[`
|
|
315
|
-
if (
|
|
316
|
-
const name
|
|
317
|
-
if (name
|
|
563
|
+
[`:matches(${selector$11.function})`]: (node) => {
|
|
564
|
+
if (inRender.at(-1) ?? false) return void inRender.push(true);
|
|
565
|
+
const name = nameOf.function(node);
|
|
566
|
+
if (name && UseRegex$1.test(name.name)) return void inRender.push(true);
|
|
318
567
|
const tsnode = services.esTreeNodeToTSNodeMap.get(node);
|
|
319
568
|
const signature = checker.getSignatureFromDeclaration(tsnode);
|
|
320
569
|
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getVoidType();
|
|
321
|
-
if (isType.jsx(returnType, services.program)) return void
|
|
322
|
-
const inferred = isExpression(tsnode)
|
|
323
|
-
if (inferred ? isType.component(
|
|
324
|
-
|
|
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);
|
|
325
574
|
},
|
|
326
|
-
[`:matches(
|
|
327
|
-
"ClassDeclaration": () => void
|
|
328
|
-
"ClassDeclaration:exit": () => void
|
|
575
|
+
[`:matches(${selector$11.function}):exit`]: () => void inRender.pop(),
|
|
576
|
+
"ClassDeclaration": () => void inRender.push(false),
|
|
577
|
+
"ClassDeclaration:exit": () => void inRender.pop(),
|
|
329
578
|
"CallExpression": (node) => {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
package: "effector-react",
|
|
333
|
-
name: [
|
|
334
|
-
"useStore",
|
|
335
|
-
"useStoreMap",
|
|
336
|
-
"useList",
|
|
337
|
-
"useEvent",
|
|
338
|
-
"useUnit"
|
|
339
|
-
]
|
|
340
|
-
}, services.program);
|
|
341
|
-
stack.hook.push(isHook);
|
|
579
|
+
const id = nameOf.callee(node.callee), isEnteringHook = id !== null && UseRegex$1.test(id.name);
|
|
580
|
+
inHook.push(isEnteringHook);
|
|
342
581
|
},
|
|
343
|
-
"
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
context.report({
|
|
349
|
-
node,
|
|
350
|
-
messageId: "useUnitNeeded",
|
|
351
|
-
data: { name: node.name }
|
|
352
|
-
});
|
|
353
|
-
}
|
|
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)
|
|
354
587
|
};
|
|
355
588
|
}
|
|
356
589
|
});
|
|
357
|
-
const UseRegex = /^use[A-Z0-9].*$/;
|
|
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
|
+
};
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/shared/locate.ts
|
|
358
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);
|
|
359
613
|
const locate = { property };
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region src/rules/no-ambiguity-target/no-ambiguity-target.ts
|
|
360
616
|
var no_ambiguity_target_default = createRule({
|
|
361
617
|
name: "no-ambiguity-target",
|
|
362
618
|
meta: {
|
|
@@ -396,6 +652,8 @@ var no_ambiguity_target_default = createRule({
|
|
|
396
652
|
}
|
|
397
653
|
});
|
|
398
654
|
const selector$10 = { method: `ImportSpecifier[imported.name=/(sample|guard)/]` };
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/rules/no-domain-unit-creators/no-domain-unit-creators.ts
|
|
399
657
|
var no_domain_unit_creators_default = createRule({
|
|
400
658
|
name: "no-domain-unit-creators",
|
|
401
659
|
meta: {
|
|
@@ -408,16 +666,16 @@ var no_domain_unit_creators_default = createRule({
|
|
|
408
666
|
create: (context) => {
|
|
409
667
|
const services = ESLintUtils.getParserServices(context);
|
|
410
668
|
return { [`CallExpression:has(> ${selector$9.member})`]: (node) => {
|
|
411
|
-
const name
|
|
412
|
-
if (!METHODS.has(name
|
|
669
|
+
const name = node.callee.property.name;
|
|
670
|
+
if (!METHODS.has(name)) return;
|
|
413
671
|
const type = services.getTypeAtLocation(node.callee.object);
|
|
414
672
|
if (!isType.domain(type, services.program)) return;
|
|
415
|
-
const factory = ALIAS_MAP.get(name
|
|
673
|
+
const factory = ALIAS_MAP.get(name) ?? name;
|
|
416
674
|
context.report({
|
|
417
675
|
node,
|
|
418
676
|
messageId: "avoid",
|
|
419
677
|
data: {
|
|
420
|
-
method: name
|
|
678
|
+
method: name,
|
|
421
679
|
factory
|
|
422
680
|
}
|
|
423
681
|
});
|
|
@@ -427,6 +685,8 @@ var no_domain_unit_creators_default = createRule({
|
|
|
427
685
|
const ALIAS_MAP = (/* @__PURE__ */ new Map()).set("event", "createEvent").set("store", "createStore").set("effect", "createEffect").set("domain", "createDomain");
|
|
428
686
|
const METHODS = new Set([...ALIAS_MAP.values(), ...ALIAS_MAP.keys()]);
|
|
429
687
|
const selector$9 = { member: `MemberExpression.callee[property.type="Identifier"]` };
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.ts
|
|
430
690
|
var no_duplicate_clock_or_source_array_values_default = createRule({
|
|
431
691
|
name: "no-duplicate-clock-or-source-array-values",
|
|
432
692
|
meta: {
|
|
@@ -449,19 +709,19 @@ var no_duplicate_clock_or_source_array_values_default = createRule({
|
|
|
449
709
|
for (const entry of entries) {
|
|
450
710
|
const root = traverseToRoot$1(entry);
|
|
451
711
|
if (!root) continue;
|
|
452
|
-
const name
|
|
453
|
-
if (seen.has(name
|
|
454
|
-
else seen.set(name
|
|
712
|
+
const name = [root.node.name, ...root.path].join(".");
|
|
713
|
+
if (seen.has(name)) report(entry, name, field);
|
|
714
|
+
else seen.set(name, entry);
|
|
455
715
|
}
|
|
456
716
|
};
|
|
457
|
-
const report = (node, name
|
|
717
|
+
const report = (node, name, field) => {
|
|
458
718
|
const data = {
|
|
459
719
|
field,
|
|
460
|
-
unit: name
|
|
720
|
+
unit: name
|
|
461
721
|
};
|
|
462
722
|
const suggestion = {
|
|
463
723
|
messageId: "remove",
|
|
464
|
-
data: { unit: name
|
|
724
|
+
data: { unit: name },
|
|
465
725
|
fix: function* (fixer) {
|
|
466
726
|
yield fixer.remove(node);
|
|
467
727
|
const before = context.sourceCode.getTokenBefore(node);
|
|
@@ -501,6 +761,8 @@ function traverseToRoot$1(node, path = []) {
|
|
|
501
761
|
if (node.type === AST_NODE_TYPES.MemberExpression && node.property.type === AST_NODE_TYPES.Identifier) return traverseToRoot$1(node.object, [node.property.name, ...path]);
|
|
502
762
|
return null;
|
|
503
763
|
}
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region src/rules/no-duplicate-on/no-duplicate-on.ts
|
|
504
766
|
var no_duplicate_on_default = createRule({
|
|
505
767
|
name: "no-duplicate-on",
|
|
506
768
|
meta: {
|
|
@@ -519,12 +781,12 @@ var no_duplicate_on_default = createRule({
|
|
|
519
781
|
const arg = node.arguments[0];
|
|
520
782
|
if (!arg || arg.type === AST_NODE_TYPES.SpreadElement) return;
|
|
521
783
|
const units = arg.type === AST_NODE_TYPES.ArrayExpression ? arg.elements.filter((item) => item !== null && item.type !== AST_NODE_TYPES.SpreadElement) : [arg];
|
|
522
|
-
const scope
|
|
523
|
-
const store = identify("store", node.callee.object, scope
|
|
784
|
+
const scope = context.sourceCode.getScope(node);
|
|
785
|
+
const store = identify("store", node.callee.object, scope);
|
|
524
786
|
if (!store) return;
|
|
525
787
|
const set = map.get(store.id) ?? /* @__PURE__ */ new Set();
|
|
526
788
|
for (const unit of units) {
|
|
527
|
-
const instance = identify("unit", unit, scope
|
|
789
|
+
const instance = identify("unit", unit, scope);
|
|
528
790
|
if (!instance) continue;
|
|
529
791
|
if (set.has(instance.id)) {
|
|
530
792
|
const data = {
|
|
@@ -585,16 +847,18 @@ function findSuitableRoot(type, node) {
|
|
|
585
847
|
};
|
|
586
848
|
return null;
|
|
587
849
|
}
|
|
588
|
-
function identify(type, node, scope
|
|
850
|
+
function identify(type, node, scope) {
|
|
589
851
|
const root = findSuitableRoot(type, node);
|
|
590
852
|
if (!root) return null;
|
|
591
|
-
const variable = ASTUtils.findVariable(scope
|
|
853
|
+
const variable = ASTUtils.findVariable(scope, root.node);
|
|
592
854
|
if (!variable) return null;
|
|
593
855
|
return {
|
|
594
856
|
id: `${variable.$id}+${root.path.join(".")}`,
|
|
595
857
|
name: [variable.name, ...root.path].join(".")
|
|
596
858
|
};
|
|
597
859
|
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/rules/no-forward/no-forward.ts
|
|
598
862
|
var no_forward_default = createRule({
|
|
599
863
|
name: "no-forward",
|
|
600
864
|
meta: {
|
|
@@ -624,11 +888,11 @@ var no_forward_default = createRule({
|
|
|
624
888
|
config.clock = locate.property("from", arg)?.value;
|
|
625
889
|
config.target = locate.property("to", arg)?.value;
|
|
626
890
|
if (config.target) {
|
|
627
|
-
const [call] = esquery.match(config.target, query$2.prepend, { visitorKeys }).map((node
|
|
891
|
+
const [call] = esquery.match(config.target, query$2.prepend, { visitorKeys }).map((node) => node).filter((node) => node === config.target);
|
|
628
892
|
if (call) [config.target, config.fn] = [call.callee.object, call.arguments[0]];
|
|
629
893
|
}
|
|
630
894
|
if (config.clock && !config.fn) {
|
|
631
|
-
const [call] = esquery.match(config.clock, query$2.map, { visitorKeys }).map((node
|
|
895
|
+
const [call] = esquery.match(config.clock, query$2.map, { visitorKeys }).map((node) => node).filter((node) => node === config.clock);
|
|
632
896
|
if (call) [config.clock, config.fn] = [call.callee.object, call.arguments[0]];
|
|
633
897
|
}
|
|
634
898
|
const code = [
|
|
@@ -662,6 +926,8 @@ const query$2 = {
|
|
|
662
926
|
map: esquery.parse("CallExpression[arguments.length=1]:has(> :first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='map']))"),
|
|
663
927
|
prepend: esquery.parse("CallExpression[arguments.length=1]:has(> :first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='prepend']))")
|
|
664
928
|
};
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/rules/no-getState/no-getState.ts
|
|
665
931
|
var no_getState_default = createRule({
|
|
666
932
|
name: "no-getState",
|
|
667
933
|
meta: {
|
|
@@ -679,11 +945,11 @@ var no_getState_default = createRule({
|
|
|
679
945
|
return { [`CallExpression[callee.type="MemberExpression"][callee.property.name="getState"]`]: (node) => {
|
|
680
946
|
const type = services.getTypeAtLocation(node.callee.object);
|
|
681
947
|
if (!isType.store(type, services.program)) return;
|
|
682
|
-
const name
|
|
683
|
-
if (name
|
|
948
|
+
const name = nameOf.expression.simple(node.callee.object);
|
|
949
|
+
if (name) context.report({
|
|
684
950
|
node,
|
|
685
951
|
messageId: "named",
|
|
686
|
-
data: { name
|
|
952
|
+
data: { name }
|
|
687
953
|
});
|
|
688
954
|
else context.report({
|
|
689
955
|
node,
|
|
@@ -692,11 +958,8 @@ var no_getState_default = createRule({
|
|
|
692
958
|
} };
|
|
693
959
|
}
|
|
694
960
|
});
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed) return node.property.name;
|
|
698
|
-
return null;
|
|
699
|
-
};
|
|
961
|
+
//#endregion
|
|
962
|
+
//#region src/rules/no-guard/no-guard.ts
|
|
700
963
|
var no_guard_default = createRule({
|
|
701
964
|
name: "no-guard",
|
|
702
965
|
meta: {
|
|
@@ -740,7 +1003,7 @@ var no_guard_default = createRule({
|
|
|
740
1003
|
]) config[key] = locate.property(key, arg)?.value;
|
|
741
1004
|
} else return;
|
|
742
1005
|
if (config.target) {
|
|
743
|
-
const [call] = esquery.match(config.target, query$1.prepend, { visitorKeys }).map((node
|
|
1006
|
+
const [call] = esquery.match(config.target, query$1.prepend, { visitorKeys }).map((node) => node).filter((node) => node === config.target);
|
|
744
1007
|
if (call) [config.target, config.fn] = [call.callee.object, call.arguments[0]];
|
|
745
1008
|
}
|
|
746
1009
|
const code = [
|
|
@@ -772,6 +1035,8 @@ const selector$6 = {
|
|
|
772
1035
|
call: `[callee.type="Identifier"]`
|
|
773
1036
|
};
|
|
774
1037
|
const query$1 = { prepend: esquery.parse("CallExpression[arguments.length=1]:has(:first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='prepend']))") };
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/rules/no-patronum-debug/no-patronum-debug.ts
|
|
775
1040
|
var no_patronum_debug_default = createRule({
|
|
776
1041
|
name: "no-patronum-debug",
|
|
777
1042
|
meta: {
|
|
@@ -790,8 +1055,8 @@ var no_patronum_debug_default = createRule({
|
|
|
790
1055
|
return {
|
|
791
1056
|
[`${`ImportDeclaration[source.value=${PACKAGE_NAME}]`} > ${selector$5.debug}`]: (node) => debugs.add(node.local.name),
|
|
792
1057
|
[`CallExpression:matches(${selector$5.call})`]: (node) => {
|
|
793
|
-
const name
|
|
794
|
-
if (!debugs.has(name
|
|
1058
|
+
const name = toName(node);
|
|
1059
|
+
if (!debugs.has(name)) return;
|
|
795
1060
|
context.report({
|
|
796
1061
|
messageId: "unexpected",
|
|
797
1062
|
node: node.callee,
|
|
@@ -818,6 +1083,186 @@ const toName = (node) => {
|
|
|
818
1083
|
case AST_NODE_TYPES.MemberExpression: return node.callee.object.name;
|
|
819
1084
|
}
|
|
820
1085
|
};
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/rules/no-units-spawn-in-render/no-units-spawn-in-render.ts
|
|
1088
|
+
const EFFECTOR_FACTORIES = new Set([
|
|
1089
|
+
"createStore",
|
|
1090
|
+
"createEvent",
|
|
1091
|
+
"createEffect",
|
|
1092
|
+
"createDomain",
|
|
1093
|
+
"createApi",
|
|
1094
|
+
"restore"
|
|
1095
|
+
]);
|
|
1096
|
+
const EFFECTOR_OPERATORS = new Set([
|
|
1097
|
+
"sample",
|
|
1098
|
+
"guard",
|
|
1099
|
+
"forward",
|
|
1100
|
+
"merge",
|
|
1101
|
+
"split",
|
|
1102
|
+
"combine",
|
|
1103
|
+
"attach"
|
|
1104
|
+
]);
|
|
1105
|
+
const REACT_HOOKS_SPEC = {
|
|
1106
|
+
from: "package",
|
|
1107
|
+
package: "react",
|
|
1108
|
+
name: [
|
|
1109
|
+
"useState",
|
|
1110
|
+
"useEffect",
|
|
1111
|
+
"useLayoutEffect",
|
|
1112
|
+
"useCallback",
|
|
1113
|
+
"useMemo",
|
|
1114
|
+
"useRef",
|
|
1115
|
+
"useReducer",
|
|
1116
|
+
"useImperativeHandle",
|
|
1117
|
+
"useDebugValue",
|
|
1118
|
+
"useDeferredValue",
|
|
1119
|
+
"useTransition",
|
|
1120
|
+
"useId",
|
|
1121
|
+
"useSyncExternalStore",
|
|
1122
|
+
"useInsertionEffect",
|
|
1123
|
+
"useContext"
|
|
1124
|
+
]
|
|
1125
|
+
};
|
|
1126
|
+
const EFFECTOR_FACTORY_SPEC = {
|
|
1127
|
+
from: "package",
|
|
1128
|
+
package: "effector",
|
|
1129
|
+
name: [...EFFECTOR_FACTORIES]
|
|
1130
|
+
};
|
|
1131
|
+
const EFFECTOR_OPERATOR_SPEC = {
|
|
1132
|
+
from: "package",
|
|
1133
|
+
package: "effector",
|
|
1134
|
+
name: [...EFFECTOR_OPERATORS]
|
|
1135
|
+
};
|
|
1136
|
+
const EFFECTOR_FACTORIO_SHAPE = [
|
|
1137
|
+
"useModel",
|
|
1138
|
+
"createModel",
|
|
1139
|
+
"Provider",
|
|
1140
|
+
"@@unitShape"
|
|
1141
|
+
];
|
|
1142
|
+
var no_units_spawn_in_render_default = createRule({
|
|
1143
|
+
name: "no-units-spawn-in-render",
|
|
1144
|
+
meta: {
|
|
1145
|
+
type: "problem",
|
|
1146
|
+
docs: { description: "Forbid creating Effector units or calling operators inside React components or hooks." },
|
|
1147
|
+
messages: {
|
|
1148
|
+
noFactoryInRender: "Creating Effector units with \"{{ name }}\" inside React component or hook is forbidden, since it may cause memory leaks and other bugs.",
|
|
1149
|
+
noOperatorInRender: "Using Effector operator \"{{ name }}\" inside React component or hook is forbidden, since it may cause memory leaks and other bugs.",
|
|
1150
|
+
noCustomFactoryInRender: "Creating Effector units with \"{{ name }}\" inside React component or hook is forbidden, since it may cause memory leaks and other bugs. If this is a false positive, add \"{{ name }}\" to the allowlist in the detectCustomFactories option."
|
|
1151
|
+
},
|
|
1152
|
+
schema: [{
|
|
1153
|
+
type: "object",
|
|
1154
|
+
properties: { detectCustomFactories: { oneOf: [{ type: "boolean" }, {
|
|
1155
|
+
type: "object",
|
|
1156
|
+
properties: { allowlist: {
|
|
1157
|
+
type: "array",
|
|
1158
|
+
items: { type: "string" },
|
|
1159
|
+
uniqueItems: true
|
|
1160
|
+
} },
|
|
1161
|
+
required: ["allowlist"],
|
|
1162
|
+
additionalProperties: false
|
|
1163
|
+
}] } },
|
|
1164
|
+
additionalProperties: false
|
|
1165
|
+
}]
|
|
1166
|
+
},
|
|
1167
|
+
defaultOptions: [{ detectCustomFactories: true }],
|
|
1168
|
+
create: (context, [options]) => {
|
|
1169
|
+
const services = ESLintUtils.getParserServices(context);
|
|
1170
|
+
const checker = services.program.getTypeChecker();
|
|
1171
|
+
const { detectCustomFactories } = options;
|
|
1172
|
+
const allowlist = typeof detectCustomFactories === "object" ? new Set(detectCustomFactories.allowlist) : void 0;
|
|
1173
|
+
const stack = { render: [] };
|
|
1174
|
+
const effectorImports = /* @__PURE__ */ new Map();
|
|
1175
|
+
return {
|
|
1176
|
+
[`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ImportSpecifier[imported.type="Identifier"]`]: (node) => {
|
|
1177
|
+
const imported = node.imported.name;
|
|
1178
|
+
const local = node.local.name;
|
|
1179
|
+
if (EFFECTOR_FACTORIES.has(imported)) effectorImports.set(local, "factory");
|
|
1180
|
+
else if (EFFECTOR_OPERATORS.has(imported)) effectorImports.set(local, "operator");
|
|
1181
|
+
},
|
|
1182
|
+
[`FunctionDeclaration, FunctionExpression, ArrowFunctionExpression`]: (node) => {
|
|
1183
|
+
if (stack.render.at(-1) ?? false) return void stack.render.push(true);
|
|
1184
|
+
const name = nameOf.function(node);
|
|
1185
|
+
if (name && UseRegex.test(name.name)) return void stack.render.push(true);
|
|
1186
|
+
const tsnode = services.esTreeNodeToTSNodeMap.get(node);
|
|
1187
|
+
const signature = checker.getSignatureFromDeclaration(tsnode);
|
|
1188
|
+
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getVoidType();
|
|
1189
|
+
if (returnType.isUnion() ? returnType.types.some((type) => isType.jsx(type, services.program)) : isType.jsx(returnType, services.program)) return void stack.render.push(true);
|
|
1190
|
+
const inferred = isExpression(tsnode) && getContextualType(checker, tsnode) || checker.getUnknownType();
|
|
1191
|
+
if (inferred.isUnion() ? inferred.types.some((type) => isType.component(type, services.program)) : isType.component(inferred, services.program)) return void stack.render.push(true);
|
|
1192
|
+
stack.render.push(false);
|
|
1193
|
+
},
|
|
1194
|
+
[`:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression):exit`]: () => void stack.render.pop(),
|
|
1195
|
+
"ClassDeclaration": () => void stack.render.push(false),
|
|
1196
|
+
"ClassDeclaration:exit": () => void stack.render.pop(),
|
|
1197
|
+
"CallExpression": (node) => {
|
|
1198
|
+
if (!(stack.render.at(-1) ?? false)) return;
|
|
1199
|
+
const calleeName = getCalleeName(node.callee);
|
|
1200
|
+
switch (calleeName ? effectorImports.get(calleeName) : void 0) {
|
|
1201
|
+
case "factory": return context.report({
|
|
1202
|
+
node,
|
|
1203
|
+
messageId: "noFactoryInRender",
|
|
1204
|
+
data: { name: calleeName }
|
|
1205
|
+
});
|
|
1206
|
+
case "operator": return context.report({
|
|
1207
|
+
node,
|
|
1208
|
+
messageId: "noOperatorInRender",
|
|
1209
|
+
data: { name: calleeName }
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
if (detectCustomFactories === false) return;
|
|
1213
|
+
const returnType = services.getTypeAtLocation(node);
|
|
1214
|
+
if (!hasEffectorUnitInType({
|
|
1215
|
+
node: services.esTreeNodeToTSNodeMap.get(node),
|
|
1216
|
+
checker,
|
|
1217
|
+
program: services.program
|
|
1218
|
+
}, returnType)) return;
|
|
1219
|
+
const calleeType = services.getTypeAtLocation(node.callee);
|
|
1220
|
+
const displayName = calleeName ?? "<expression>";
|
|
1221
|
+
if (typeMatchesSpecifier(calleeType, REACT_HOOKS_SPEC, services.program)) return;
|
|
1222
|
+
if (isEffectorFactorioHook(node.callee, services.getTypeAtLocation)) return;
|
|
1223
|
+
if (typeMatchesSpecifier(calleeType, EFFECTOR_FACTORY_SPEC, services.program)) return context.report({
|
|
1224
|
+
node,
|
|
1225
|
+
messageId: "noFactoryInRender",
|
|
1226
|
+
data: { name: displayName }
|
|
1227
|
+
});
|
|
1228
|
+
if (typeMatchesSpecifier(calleeType, EFFECTOR_OPERATOR_SPEC, services.program)) return context.report({
|
|
1229
|
+
node,
|
|
1230
|
+
messageId: "noOperatorInRender",
|
|
1231
|
+
data: { name: displayName }
|
|
1232
|
+
});
|
|
1233
|
+
if (allowlist && calleeName && allowlist.has(calleeName)) return;
|
|
1234
|
+
context.report({
|
|
1235
|
+
node,
|
|
1236
|
+
messageId: "noCustomFactoryInRender",
|
|
1237
|
+
data: { name: displayName }
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
const UseRegex = /^use[A-Z0-9].*$/;
|
|
1244
|
+
function getCalleeName(callee) {
|
|
1245
|
+
if (callee.type === AST_NODE_TYPES.Identifier) return callee.name;
|
|
1246
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) return callee.property.name;
|
|
1247
|
+
else return null;
|
|
1248
|
+
}
|
|
1249
|
+
function hasEffectorUnitInType(ctx, type, depth = 3) {
|
|
1250
|
+
if (isType.unit(type, ctx.program)) return true;
|
|
1251
|
+
if (depth <= 0) return false;
|
|
1252
|
+
if (type.isUnion()) return type.types.some((type) => hasEffectorUnitInType(ctx, type, depth));
|
|
1253
|
+
for (const property of type.getProperties()) if (hasEffectorUnitInType(ctx, ctx.checker.getTypeOfSymbolAtLocation(property, ctx.node), depth - 1)) return true;
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
function isEffectorFactorioHook(callee, getTypeAtLocation) {
|
|
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;
|
|
1260
|
+
const objectType = getTypeAtLocation(callee.object);
|
|
1261
|
+
const propertyNames = new Set(objectType.getProperties().map((p) => p.getName()));
|
|
1262
|
+
return EFFECTOR_FACTORIO_SHAPE.every((name) => propertyNames.has(name));
|
|
1263
|
+
}
|
|
1264
|
+
//#endregion
|
|
1265
|
+
//#region src/rules/no-unnecessary-combination/no-unnecessary-combination.ts
|
|
821
1266
|
var no_unnecessary_combination_default = createRule({
|
|
822
1267
|
name: "no-unnecessary-combination",
|
|
823
1268
|
meta: {
|
|
@@ -888,6 +1333,8 @@ function isFunction(node, services) {
|
|
|
888
1333
|
return checker.getTypeAtLocation(tsnode).getCallSignatures().length > 0;
|
|
889
1334
|
} else return false;
|
|
890
1335
|
}
|
|
1336
|
+
//#endregion
|
|
1337
|
+
//#region src/rules/no-unnecessary-duplication/no-unnecessary-duplication.ts
|
|
891
1338
|
var no_unnecessary_duplication_default = createRule({
|
|
892
1339
|
name: "no-unnecessary-duplication",
|
|
893
1340
|
meta: {
|
|
@@ -964,6 +1411,8 @@ function compare(clock, source, limit = 5) {
|
|
|
964
1411
|
}
|
|
965
1412
|
return false;
|
|
966
1413
|
}
|
|
1414
|
+
//#endregion
|
|
1415
|
+
//#region src/rules/no-useless-methods/no-useless-methods.ts
|
|
967
1416
|
var no_useless_methods_default = createRule({
|
|
968
1417
|
name: "no-useless-methods",
|
|
969
1418
|
meta: {
|
|
@@ -998,10 +1447,8 @@ var no_useless_methods_default = createRule({
|
|
|
998
1447
|
if (locate.property("target", config)?.value) return;
|
|
999
1448
|
}
|
|
1000
1449
|
const grandparent = node.parent.parent;
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (esquery.matches(grandparent, query.watch, ancestry, { visitorKeys })) return;
|
|
1004
|
-
}
|
|
1450
|
+
const ancestry = source.getAncestors(grandparent);
|
|
1451
|
+
if (esquery.matches(grandparent, query.watch, ancestry, { visitorKeys })) return;
|
|
1005
1452
|
const method = node.callee.name;
|
|
1006
1453
|
context.report({
|
|
1007
1454
|
node,
|
|
@@ -1014,6 +1461,8 @@ var no_useless_methods_default = createRule({
|
|
|
1014
1461
|
});
|
|
1015
1462
|
const selector$2 = { method: `ImportSpecifier[imported.name=/(sample|guard)/]` };
|
|
1016
1463
|
const query = { watch: esquery.parse("CallExpression:has(> MemberExpression.callee[property.name=watch]:has(> CallExpression.object))") };
|
|
1464
|
+
//#endregion
|
|
1465
|
+
//#region src/rules/no-watch/no-watch.ts
|
|
1017
1466
|
var no_watch_default = createRule({
|
|
1018
1467
|
name: "no-watch",
|
|
1019
1468
|
meta: {
|
|
@@ -1035,6 +1484,8 @@ var no_watch_default = createRule({
|
|
|
1035
1484
|
} };
|
|
1036
1485
|
}
|
|
1037
1486
|
});
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region src/rules/prefer-useUnit/prefer-useUnit.ts
|
|
1038
1489
|
var prefer_useUnit_default = createRule({
|
|
1039
1490
|
name: "prefer-useUnit",
|
|
1040
1491
|
meta: {
|
|
@@ -1066,6 +1517,8 @@ const selector$1 = {
|
|
|
1066
1517
|
useStore: `ImportSpecifier[imported.name=useStore]`,
|
|
1067
1518
|
useEvent: `ImportSpecifier[imported.name=useEvent]`
|
|
1068
1519
|
};
|
|
1520
|
+
//#endregion
|
|
1521
|
+
//#region src/rules/require-pickup-in-persist/require-pickup-in-persist.ts
|
|
1069
1522
|
var require_pickup_in_persist_default = createRule({
|
|
1070
1523
|
name: "require-pickup-in-persist",
|
|
1071
1524
|
meta: {
|
|
@@ -1095,6 +1548,8 @@ const selector = {
|
|
|
1095
1548
|
call: `[callee.type="Identifier"]`,
|
|
1096
1549
|
config: `[arguments.length=1][arguments.0.type="ObjectExpression"]`
|
|
1097
1550
|
};
|
|
1551
|
+
//#endregion
|
|
1552
|
+
//#region src/rules/strict-effect-handlers/strict-effect-handlers.ts
|
|
1098
1553
|
var strict_effect_handlers_default = createRule({
|
|
1099
1554
|
name: "strict-effect-handlers",
|
|
1100
1555
|
meta: {
|
|
@@ -1122,9 +1577,8 @@ var strict_effect_handlers_default = createRule({
|
|
|
1122
1577
|
});
|
|
1123
1578
|
};
|
|
1124
1579
|
const exit = (node) => {
|
|
1125
|
-
const scope
|
|
1126
|
-
if (
|
|
1127
|
-
if (scope$1.effect && scope$1.regular) context.report({
|
|
1580
|
+
const scope = stack.pop();
|
|
1581
|
+
if (scope.effect && scope.regular) context.report({
|
|
1128
1582
|
node,
|
|
1129
1583
|
messageId: "mixed"
|
|
1130
1584
|
});
|
|
@@ -1162,11 +1616,15 @@ const ruleset = {
|
|
|
1162
1616
|
},
|
|
1163
1617
|
react: {
|
|
1164
1618
|
"effector/enforce-gate-naming-convention": "error",
|
|
1619
|
+
"effector/enforce-exhaustive-useUnit-destructuring": "warn",
|
|
1165
1620
|
"effector/mandatory-scope-binding": "error",
|
|
1621
|
+
"effector/no-units-spawn-in-render": "error",
|
|
1166
1622
|
"effector/prefer-useUnit": "error"
|
|
1167
1623
|
},
|
|
1168
1624
|
future: { "effector/no-domain-unit-creators": "warn" }
|
|
1169
1625
|
};
|
|
1626
|
+
//#endregion
|
|
1627
|
+
//#region src/index.ts
|
|
1170
1628
|
const base = {
|
|
1171
1629
|
meta: {
|
|
1172
1630
|
name,
|
|
@@ -1175,6 +1633,7 @@ const base = {
|
|
|
1175
1633
|
},
|
|
1176
1634
|
rules: {
|
|
1177
1635
|
"enforce-effect-naming-convention": enforce_effect_naming_convention_default,
|
|
1636
|
+
"enforce-exhaustive-useUnit-destructuring": enforce_exhaustive_useUnit_destructuring_default,
|
|
1178
1637
|
"enforce-gate-naming-convention": enforce_gate_naming_convention_default,
|
|
1179
1638
|
"enforce-store-naming-convention": enforce_store_naming_convention_default,
|
|
1180
1639
|
"keep-options-order": keep_options_order_default,
|
|
@@ -1187,6 +1646,7 @@ const base = {
|
|
|
1187
1646
|
"no-getState": no_getState_default,
|
|
1188
1647
|
"no-guard": no_guard_default,
|
|
1189
1648
|
"no-patronum-debug": no_patronum_debug_default,
|
|
1649
|
+
"no-units-spawn-in-render": no_units_spawn_in_render_default,
|
|
1190
1650
|
"no-unnecessary-combination": no_unnecessary_combination_default,
|
|
1191
1651
|
"no-unnecessary-duplication": no_unnecessary_duplication_default,
|
|
1192
1652
|
"no-useless-methods": no_useless_methods_default,
|
|
@@ -1229,5 +1689,5 @@ const flatConfigs = {
|
|
|
1229
1689
|
const plugin = base;
|
|
1230
1690
|
plugin.configs = legacyConfigs;
|
|
1231
1691
|
plugin.flatConfigs = flatConfigs;
|
|
1232
|
-
|
|
1233
|
-
export {
|
|
1692
|
+
//#endregion
|
|
1693
|
+
export { plugin as default };
|