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 +6 -0
- package/dist/index.cjs +345 -116
- package/dist/index.d.cts +28 -24
- package/dist/index.d.mts +28 -24
- package/dist/index.mjs +344 -116
- package/package.json +19 -19
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.
|
|
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
|
|
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 {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
200
|
-
current,
|
|
352
|
+
return {
|
|
353
|
+
current: node.name,
|
|
201
354
|
convention: options.mode,
|
|
202
355
|
fixed
|
|
203
356
|
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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$
|
|
250
|
-
[`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) => {
|
|
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$
|
|
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
|
-
|
|
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
|
|
322
|
-
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." },
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
[`
|
|
335
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
344
|
-
|
|
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(
|
|
347
|
-
"ClassDeclaration": () => void
|
|
348
|
-
"ClassDeclaration:exit": () => void
|
|
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
|
|
351
|
-
|
|
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
|
-
"
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 =
|
|
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
|
-
|
|
1222
|
-
|
|
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,
|