oxlint-plugin-effector 0.0.1
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/LICENSE +24 -0
- package/README.md +121 -0
- package/dist/index.js +2031 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
// src/shared/ast.ts
|
|
2
|
+
var NodeType = {
|
|
3
|
+
ArrayExpression: "ArrayExpression",
|
|
4
|
+
ArrayPattern: "ArrayPattern",
|
|
5
|
+
ArrowFunctionExpression: "ArrowFunctionExpression",
|
|
6
|
+
AssignmentExpression: "AssignmentExpression",
|
|
7
|
+
AssignmentPattern: "AssignmentPattern",
|
|
8
|
+
CallExpression: "CallExpression",
|
|
9
|
+
ExpressionStatement: "ExpressionStatement",
|
|
10
|
+
FunctionDeclaration: "FunctionDeclaration",
|
|
11
|
+
FunctionExpression: "FunctionExpression",
|
|
12
|
+
Identifier: "Identifier",
|
|
13
|
+
Literal: "Literal",
|
|
14
|
+
MemberExpression: "MemberExpression",
|
|
15
|
+
ObjectExpression: "ObjectExpression",
|
|
16
|
+
ObjectPattern: "ObjectPattern",
|
|
17
|
+
Property: "Property",
|
|
18
|
+
RestElement: "RestElement",
|
|
19
|
+
SpreadElement: "SpreadElement",
|
|
20
|
+
VariableDeclarator: "VariableDeclarator"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/shared/services.ts
|
|
24
|
+
import { dirname, resolve } from "node:path";
|
|
25
|
+
import * as ts from "typescript";
|
|
26
|
+
function getParserServices(context) {
|
|
27
|
+
const existing = context.sourceCode?.parserServices;
|
|
28
|
+
if (existing?.program && existing.esTreeNodeToTSNodeMap) return existing;
|
|
29
|
+
return buildServices(context);
|
|
30
|
+
}
|
|
31
|
+
var programCache = /* @__PURE__ */ new Map();
|
|
32
|
+
function buildServices(context) {
|
|
33
|
+
const filename = context.filename ?? context.physicalFilename;
|
|
34
|
+
const program = getProgram(filename);
|
|
35
|
+
const sourceFile = program.getSourceFile(filename);
|
|
36
|
+
if (!sourceFile)
|
|
37
|
+
throw new Error(
|
|
38
|
+
`oxlint-plugin-effector: cannot load "${filename}" into a TypeScript program`
|
|
39
|
+
);
|
|
40
|
+
const checker = program.getTypeChecker();
|
|
41
|
+
const byRange = indexByRange(sourceFile);
|
|
42
|
+
const toTSNode = (node) => {
|
|
43
|
+
const [start, end] = node.range;
|
|
44
|
+
return byRange.get(key(start, end)) ?? deepestContaining(sourceFile, start, end);
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
program,
|
|
48
|
+
esTreeNodeToTSNodeMap: { get: toTSNode, has: () => true },
|
|
49
|
+
tsNodeToESTreeNodeMap: { get: () => void 0, has: () => false },
|
|
50
|
+
getTypeAtLocation: (node) => checker.getTypeAtLocation(toTSNode(node)),
|
|
51
|
+
getSymbolAtLocation: (node) => checker.getSymbolAtLocation(toTSNode(node))
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
var FALLBACK_OPTIONS = {
|
|
55
|
+
allowJs: true,
|
|
56
|
+
jsx: ts.JsxEmit.Preserve,
|
|
57
|
+
module: ts.ModuleKind.ESNext,
|
|
58
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
59
|
+
target: ts.ScriptTarget.ESNext,
|
|
60
|
+
skipLibCheck: true
|
|
61
|
+
};
|
|
62
|
+
var norm = (p) => p.replace(/\\/g, "/");
|
|
63
|
+
var parseConfig = (configPath) => ts.getParsedCommandLineOfConfigFile(configPath, {}, ts.sys) ?? void 0;
|
|
64
|
+
function projectContaining(configPath, filename, seen = /* @__PURE__ */ new Set()) {
|
|
65
|
+
if (seen.has(norm(configPath))) return void 0;
|
|
66
|
+
seen.add(norm(configPath));
|
|
67
|
+
const parsed = parseConfig(configPath);
|
|
68
|
+
if (!parsed) return void 0;
|
|
69
|
+
if (parsed.fileNames.some((f) => norm(f) === norm(filename))) return parsed;
|
|
70
|
+
for (const ref of parsed.projectReferences ?? []) {
|
|
71
|
+
const found = projectContaining(
|
|
72
|
+
ts.resolveProjectReferencePath(ref),
|
|
73
|
+
filename,
|
|
74
|
+
seen
|
|
75
|
+
);
|
|
76
|
+
if (found) return found;
|
|
77
|
+
}
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
function getProgram(filename) {
|
|
81
|
+
const override = process.env.OXLINT_EFFECTOR_TSCONFIG;
|
|
82
|
+
const rootConfig = override ? resolve(override) : ts.findConfigFile(dirname(filename), ts.sys.fileExists) ?? "";
|
|
83
|
+
const parsed = rootConfig ? projectContaining(rootConfig, filename) ?? parseConfig(rootConfig) : void 0;
|
|
84
|
+
const cacheKey = parsed?.options.configFilePath ?? rootConfig ?? filename;
|
|
85
|
+
const cached = programCache.get(cacheKey);
|
|
86
|
+
if (cached) return cached;
|
|
87
|
+
const options = parsed?.options ?? FALLBACK_OPTIONS;
|
|
88
|
+
const inProject = parsed?.fileNames.some((f) => norm(f) === norm(filename)) ?? false;
|
|
89
|
+
let rootNames = [filename];
|
|
90
|
+
if (parsed && parsed.fileNames.length)
|
|
91
|
+
rootNames = inProject ? parsed.fileNames : [...parsed.fileNames, filename];
|
|
92
|
+
const program = ts.createProgram({ rootNames, options });
|
|
93
|
+
programCache.set(cacheKey, program);
|
|
94
|
+
return program;
|
|
95
|
+
}
|
|
96
|
+
function deepestContaining(sourceFile, start, end) {
|
|
97
|
+
let best = sourceFile;
|
|
98
|
+
const visit = (node) => {
|
|
99
|
+
if (node.getStart(sourceFile) <= start && node.getEnd() >= end) {
|
|
100
|
+
best = node;
|
|
101
|
+
node.forEachChild(visit);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
sourceFile.forEachChild(visit);
|
|
105
|
+
return best;
|
|
106
|
+
}
|
|
107
|
+
var key = (start, end) => `${start}:${end}`;
|
|
108
|
+
function indexByRange(sourceFile) {
|
|
109
|
+
const map = /* @__PURE__ */ new Map();
|
|
110
|
+
const visit = (node) => {
|
|
111
|
+
map.set(key(node.getStart(sourceFile), node.getEnd()), node);
|
|
112
|
+
node.forEachChild(visit);
|
|
113
|
+
};
|
|
114
|
+
visit(sourceFile);
|
|
115
|
+
return map;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/shared/create.ts
|
|
119
|
+
var docsUrl = (name) => `https://eslint.effector.dev/rules/${name}`;
|
|
120
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
+
var mergeDeep = (base, override) => {
|
|
122
|
+
if (override === void 0) return base;
|
|
123
|
+
if (!isPlainObject(base) || !isPlainObject(override)) return override;
|
|
124
|
+
const result = { ...base };
|
|
125
|
+
for (const key2 of Object.keys(override))
|
|
126
|
+
result[key2] = mergeDeep(base[key2], override[key2]);
|
|
127
|
+
return result;
|
|
128
|
+
};
|
|
129
|
+
var applyDefaults = (provided, defaults) => {
|
|
130
|
+
const user = Array.isArray(provided) ? provided : [];
|
|
131
|
+
return defaults.map(
|
|
132
|
+
(value, index) => mergeDeep(value, user[index])
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
function createRule(rule) {
|
|
136
|
+
return {
|
|
137
|
+
meta: {
|
|
138
|
+
...rule.meta,
|
|
139
|
+
docs: { ...rule.meta.docs, url: docsUrl(rule.name) }
|
|
140
|
+
},
|
|
141
|
+
defaultOptions: rule.defaultOptions,
|
|
142
|
+
create(context) {
|
|
143
|
+
return rule.create(
|
|
144
|
+
context,
|
|
145
|
+
applyDefaults(context.options, rule.defaultOptions)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/shared/is.ts
|
|
152
|
+
import "typescript";
|
|
153
|
+
var symbolMatches = (symbol, names, from) => {
|
|
154
|
+
if (!names.includes(symbol.getName())) return false;
|
|
155
|
+
const declarations = symbol.declarations ?? [];
|
|
156
|
+
return declarations.map((decl) => decl.getSourceFile().fileName).some((fname) => fname.includes("node_modules") && fname.includes(from));
|
|
157
|
+
};
|
|
158
|
+
var typeMatches = (type, names, from, depth = 0) => {
|
|
159
|
+
if (depth > 10) return false;
|
|
160
|
+
const symbol = type.getSymbol() ?? type.aliasSymbol;
|
|
161
|
+
if (symbol && symbolMatches(symbol, names, from)) return true;
|
|
162
|
+
if (type.isUnion() || type.isIntersection())
|
|
163
|
+
return type.types.some(
|
|
164
|
+
(member) => typeMatches(member, names, from, depth + 1)
|
|
165
|
+
);
|
|
166
|
+
return false;
|
|
167
|
+
};
|
|
168
|
+
var isType = {
|
|
169
|
+
store: (type) => typeMatches(type, ["Store", "StoreWritable"], "effector"),
|
|
170
|
+
event: (type) => typeMatches(type, ["Event", "EventCallable"], "effector"),
|
|
171
|
+
effect: (type) => typeMatches(type, ["Effect"], "effector"),
|
|
172
|
+
domain: (type) => typeMatches(type, ["Domain"], "effector"),
|
|
173
|
+
unit: (type) => typeMatches(
|
|
174
|
+
type,
|
|
175
|
+
["Store", "StoreWritable", "Event", "EventCallable", "Effect", "Domain"],
|
|
176
|
+
"effector"
|
|
177
|
+
),
|
|
178
|
+
// Gate is an intersection type, which TypeScript erases from existence
|
|
179
|
+
gate: (type) => typeMatches(type, ["Gate"], "effector"),
|
|
180
|
+
jsx: (type) => typeMatches(type, ["Element", "ReactNode", "ReactElement"], "react"),
|
|
181
|
+
component: (type) => typeMatches(
|
|
182
|
+
type,
|
|
183
|
+
[
|
|
184
|
+
"FC",
|
|
185
|
+
"FunctionComponent",
|
|
186
|
+
"ComponentType",
|
|
187
|
+
"ComponentClass",
|
|
188
|
+
"ForwardRefRenderFunction"
|
|
189
|
+
],
|
|
190
|
+
"react"
|
|
191
|
+
)
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.ts
|
|
195
|
+
var enforce_effect_naming_convention_default = createRule({
|
|
196
|
+
name: "enforce-effect-naming-convention",
|
|
197
|
+
meta: {
|
|
198
|
+
type: "problem",
|
|
199
|
+
docs: {
|
|
200
|
+
description: "Enforce Fx as a suffix for any Effector Effect."
|
|
201
|
+
},
|
|
202
|
+
messages: {
|
|
203
|
+
invalid: 'Effect "{{ current }}" should be named with `Fx` suffix, rename it to "{{ fixed }}"',
|
|
204
|
+
rename: 'Rename "{{ current }}" to "{{ fixed }}"'
|
|
205
|
+
},
|
|
206
|
+
schema: [],
|
|
207
|
+
hasSuggestions: true
|
|
208
|
+
},
|
|
209
|
+
defaultOptions: [],
|
|
210
|
+
create: (context) => {
|
|
211
|
+
const services = getParserServices(context);
|
|
212
|
+
const identifierSelectors = [
|
|
213
|
+
selector.variable,
|
|
214
|
+
selector.array.identifier,
|
|
215
|
+
selector.array.assignment,
|
|
216
|
+
selector.function.identifier,
|
|
217
|
+
selector.function.assignment
|
|
218
|
+
].join(", ");
|
|
219
|
+
return {
|
|
220
|
+
[identifierSelectors]: (node) => {
|
|
221
|
+
const type = services.getTypeAtLocation(node);
|
|
222
|
+
const isEffect = isType.effect(type);
|
|
223
|
+
if (!isEffect) return;
|
|
224
|
+
const data = { current: node.name, fixed: node.name + "Fx" };
|
|
225
|
+
if (node.typeAnnotation)
|
|
226
|
+
return context.report({ node, messageId: "invalid", data });
|
|
227
|
+
const suggestion = {
|
|
228
|
+
messageId: "rename",
|
|
229
|
+
data: { current: node.name, fixed: data.fixed },
|
|
230
|
+
fix: (fixer) => fixer.replaceText(node, data.fixed)
|
|
231
|
+
};
|
|
232
|
+
context.report({
|
|
233
|
+
node,
|
|
234
|
+
messageId: "invalid",
|
|
235
|
+
data,
|
|
236
|
+
suggest: [suggestion]
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
[`${selector.shape.identifier}, ${selector.shape.assignment}`]: (node) => {
|
|
240
|
+
const type = services.getTypeAtLocation(node.value);
|
|
241
|
+
const ident = node.value.type === NodeType.Identifier ? node.value : node.value.left;
|
|
242
|
+
const isEffect = isType.effect(type);
|
|
243
|
+
if (!isEffect) return;
|
|
244
|
+
const data = { current: ident.name, fixed: ident.name + "Fx" };
|
|
245
|
+
const suggestion = {
|
|
246
|
+
messageId: "rename",
|
|
247
|
+
data: { current: ident.name, fixed: data.fixed },
|
|
248
|
+
fix: (fixer) => node.shorthand ? fixer.insertTextAfter(node.key, `: ${data.fixed}`) : fixer.replaceText(ident, data.fixed)
|
|
249
|
+
};
|
|
250
|
+
context.report({
|
|
251
|
+
node: ident,
|
|
252
|
+
messageId: "invalid",
|
|
253
|
+
data,
|
|
254
|
+
suggest: [suggestion]
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
var FxRegex = /Fx$/;
|
|
261
|
+
var selector = {
|
|
262
|
+
variable: `VariableDeclarator > Identifier.id[name!=${FxRegex}]`,
|
|
263
|
+
array: {
|
|
264
|
+
identifier: `ArrayPattern > Identifier.elements[name!=${FxRegex}]`,
|
|
265
|
+
assignment: `ArrayPattern > AssignmentPattern > Identifier.left[name!=${FxRegex}]`
|
|
266
|
+
},
|
|
267
|
+
shape: {
|
|
268
|
+
identifier: `ObjectPattern > Property:has(> Identifier.value[name!=${FxRegex}])`,
|
|
269
|
+
assignment: `ObjectPattern > Property:has(> AssignmentPattern:has(> Identifier.left[name!=${FxRegex}]))`
|
|
270
|
+
},
|
|
271
|
+
function: {
|
|
272
|
+
identifier: `:function > Identifier.params[name!=${FxRegex}]`,
|
|
273
|
+
assignment: `:function > AssignmentPattern > Identifier.left[name!=${FxRegex}]`
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/shared/package.ts
|
|
278
|
+
var PACKAGE_NAME = {
|
|
279
|
+
core: /^effector(?:\u002Fcompat)?$/,
|
|
280
|
+
react: /^effector-react$/,
|
|
281
|
+
storage: /^@?effector-storage(\u002F[\w-]+)*$/
|
|
282
|
+
};
|
|
283
|
+
var fromPackage = (pkg) => `ImportDeclaration[source.value=${pkg}]`;
|
|
284
|
+
|
|
285
|
+
// src/rules/enforce-exhaustive-useUnit-destructuring/enforce-exhaustive-useUnit-destructuring.ts
|
|
286
|
+
var enforce_exhaustive_useUnit_destructuring_default = createRule({
|
|
287
|
+
name: "enforce-exhaustive-useUnit-destructuring",
|
|
288
|
+
meta: {
|
|
289
|
+
type: "problem",
|
|
290
|
+
docs: {
|
|
291
|
+
description: "Ensure all units passed to useUnit are properly destructured."
|
|
292
|
+
},
|
|
293
|
+
messages: {
|
|
294
|
+
unusedKey: 'Property "{{name}}" is passed but not destructured.',
|
|
295
|
+
missingKey: 'Property "{{name}}" is destructured but not passed in the unit object.'
|
|
296
|
+
},
|
|
297
|
+
schema: []
|
|
298
|
+
},
|
|
299
|
+
defaultOptions: [],
|
|
300
|
+
create(context) {
|
|
301
|
+
const importedAs = /* @__PURE__ */ new Set();
|
|
302
|
+
return {
|
|
303
|
+
[selector2.import]: (node) => void importedAs.add(node.local.name),
|
|
304
|
+
[`${selector2.variable.shape}:has(> ${selector2.call}:has(${selector2.arg.shape}))`](node) {
|
|
305
|
+
if (!importedAs.has(node.init.callee.name)) return;
|
|
306
|
+
const provided = shapeToKeyMap(node.init.arguments[0]);
|
|
307
|
+
const consumed = shapeToKeyMap(node.id);
|
|
308
|
+
if (provided === null || consumed === null) return;
|
|
309
|
+
for (const { type, name } of check(provided, consumed))
|
|
310
|
+
if (type === "unused")
|
|
311
|
+
context.report({
|
|
312
|
+
node: node.init.arguments[0],
|
|
313
|
+
messageId: "unusedKey",
|
|
314
|
+
data: { name }
|
|
315
|
+
});
|
|
316
|
+
else
|
|
317
|
+
context.report({
|
|
318
|
+
node: node.id,
|
|
319
|
+
messageId: "missingKey",
|
|
320
|
+
data: { name }
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
[`${selector2.variable.list}:has(> ${selector2.call}:has(${selector2.arg.list}))`](node) {
|
|
324
|
+
if (!importedAs.has(node.init.callee.name)) return;
|
|
325
|
+
const provided = listToKeyMap(node.init.arguments[0]);
|
|
326
|
+
const consumed = listToKeyMap(node.id);
|
|
327
|
+
if (provided === null || consumed === null) return;
|
|
328
|
+
for (const { type, name } of check(provided, consumed))
|
|
329
|
+
if (type === "unused")
|
|
330
|
+
context.report({
|
|
331
|
+
node: node.init.arguments[0],
|
|
332
|
+
messageId: "unusedKey",
|
|
333
|
+
data: { name }
|
|
334
|
+
});
|
|
335
|
+
else
|
|
336
|
+
context.report({
|
|
337
|
+
node: node.id,
|
|
338
|
+
messageId: "missingKey",
|
|
339
|
+
data: { name }
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
var selector2 = {
|
|
346
|
+
import: `ImportDeclaration[source.value=${PACKAGE_NAME.react}] > ImportSpecifier[imported.name=useUnit]`,
|
|
347
|
+
variable: {
|
|
348
|
+
shape: "VariableDeclarator[id.type=ObjectPattern]",
|
|
349
|
+
list: "VariableDeclarator[id.type=ArrayPattern]"
|
|
350
|
+
},
|
|
351
|
+
call: "CallExpression.init[arguments.length=1][callee.type=Identifier]",
|
|
352
|
+
arg: {
|
|
353
|
+
shape: "ObjectExpression.arguments",
|
|
354
|
+
list: "ArrayExpression.arguments"
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
function toName(key2, node) {
|
|
358
|
+
if (node.type === NodeType.Identifier) return node.name;
|
|
359
|
+
if (node.type === NodeType.Literal) return String(node.value);
|
|
360
|
+
if (node.type === NodeType.MemberExpression && node.property.type === NodeType.Identifier)
|
|
361
|
+
return `${toName(key2, node.object)}.${node.property.name}`;
|
|
362
|
+
return `<unknown at ${key2}>`;
|
|
363
|
+
}
|
|
364
|
+
function toKey(prop) {
|
|
365
|
+
if (prop.computed) return null;
|
|
366
|
+
else if (prop.key.type === NodeType.Identifier) return prop.key.name;
|
|
367
|
+
else return prop.key.value;
|
|
368
|
+
}
|
|
369
|
+
function* check(provided, consumed) {
|
|
370
|
+
for (const [key2, node] of provided)
|
|
371
|
+
if (!consumed.has(key2)) yield { type: "unused", name: toName(key2, node) };
|
|
372
|
+
for (const [key2, node] of consumed)
|
|
373
|
+
if (!provided.has(key2)) yield { type: "missing", name: toName(key2, node) };
|
|
374
|
+
}
|
|
375
|
+
function shapeToKeyMap(shape) {
|
|
376
|
+
const map = /* @__PURE__ */ new Map();
|
|
377
|
+
for (const prop of shape.properties) {
|
|
378
|
+
if (prop.type !== NodeType.Property) return null;
|
|
379
|
+
const key2 = toKey(prop);
|
|
380
|
+
if (key2 === null) return null;
|
|
381
|
+
else map.set(key2, prop.key);
|
|
382
|
+
}
|
|
383
|
+
return map;
|
|
384
|
+
}
|
|
385
|
+
function listToKeyMap(list) {
|
|
386
|
+
const map = /* @__PURE__ */ new Map();
|
|
387
|
+
for (const [index, element] of list.elements.entries()) {
|
|
388
|
+
if (element === null) continue;
|
|
389
|
+
if (element.type === NodeType.RestElement || element.type === NodeType.SpreadElement)
|
|
390
|
+
return null;
|
|
391
|
+
map.set(index, element);
|
|
392
|
+
}
|
|
393
|
+
return map;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.ts
|
|
397
|
+
var enforce_gate_naming_convention_default = createRule({
|
|
398
|
+
name: "enforce-gate-naming-convention",
|
|
399
|
+
meta: {
|
|
400
|
+
type: "problem",
|
|
401
|
+
docs: {
|
|
402
|
+
description: "Enforce a Gate is named capitalized like a React Component"
|
|
403
|
+
},
|
|
404
|
+
messages: {
|
|
405
|
+
invalid: 'Gate "{{ current }}" should be named with first capital letter, rename it to "{{ fixed }}"',
|
|
406
|
+
rename: 'Rename "{{ current }}" to "{{ fixed }}"'
|
|
407
|
+
},
|
|
408
|
+
schema: [],
|
|
409
|
+
hasSuggestions: true
|
|
410
|
+
},
|
|
411
|
+
defaultOptions: [],
|
|
412
|
+
create: (context) => {
|
|
413
|
+
const services = getParserServices(context);
|
|
414
|
+
return {
|
|
415
|
+
[`VariableDeclarator[id.name=${GateRegex}]`]: (node) => {
|
|
416
|
+
const type = services.getTypeAtLocation(node);
|
|
417
|
+
const isGate = isType.gate(type);
|
|
418
|
+
if (!isGate) return;
|
|
419
|
+
const current = node.id.name;
|
|
420
|
+
const fixed = current[0].toUpperCase() + current.slice(1);
|
|
421
|
+
const data = { current, fixed };
|
|
422
|
+
const suggestion = {
|
|
423
|
+
messageId: "rename",
|
|
424
|
+
data: { current, fixed },
|
|
425
|
+
fix: (fixer) => fixer.replaceText(node.id, fixed)
|
|
426
|
+
};
|
|
427
|
+
context.report({
|
|
428
|
+
node: node.id,
|
|
429
|
+
messageId: "invalid",
|
|
430
|
+
data,
|
|
431
|
+
suggest: [suggestion]
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
var GateRegex = /^[^A-Z]/;
|
|
438
|
+
|
|
439
|
+
// src/rules/enforce-store-naming-convention/enforce-store-naming-convention.ts
|
|
440
|
+
var enforce_store_naming_convention_default = createRule({
|
|
441
|
+
name: "enforce-store-naming-convention",
|
|
442
|
+
meta: {
|
|
443
|
+
type: "problem",
|
|
444
|
+
docs: {
|
|
445
|
+
description: "Enforce $ as a prefix/postfix for any Effector `Store`"
|
|
446
|
+
},
|
|
447
|
+
messages: {
|
|
448
|
+
invalid: 'Store "{{ current }}" should be named with a `$` {{ convention }}, rename it to "{{ fixed }}"',
|
|
449
|
+
rename: 'Rename "{{ current }}" to "{{ fixed }}"'
|
|
450
|
+
},
|
|
451
|
+
schema: [
|
|
452
|
+
{
|
|
453
|
+
type: "object",
|
|
454
|
+
properties: { mode: { type: "string", enum: ["prefix", "postfix"] } }
|
|
455
|
+
}
|
|
456
|
+
],
|
|
457
|
+
hasSuggestions: true
|
|
458
|
+
},
|
|
459
|
+
defaultOptions: [{ mode: "prefix" }],
|
|
460
|
+
create: (context, [options]) => {
|
|
461
|
+
const services = getParserServices(context);
|
|
462
|
+
const selector16 = createSelector(
|
|
463
|
+
options.mode === "prefix" ? PrefixRegex : PostfixRegex
|
|
464
|
+
);
|
|
465
|
+
const rename = (node) => {
|
|
466
|
+
const trimmed = node.name.replace(
|
|
467
|
+
options.mode === "prefix" ? /\$+$/g : /^\$+/g,
|
|
468
|
+
""
|
|
469
|
+
);
|
|
470
|
+
const fixed = options.mode === "prefix" ? `$${trimmed}` : `${trimmed}$`;
|
|
471
|
+
return { current: node.name, convention: options.mode, fixed };
|
|
472
|
+
};
|
|
473
|
+
const identifierSelectors = [
|
|
474
|
+
selector16.variable,
|
|
475
|
+
selector16.array.identifier,
|
|
476
|
+
selector16.array.assignment,
|
|
477
|
+
selector16.function.identifier,
|
|
478
|
+
selector16.function.assignment
|
|
479
|
+
].join(", ");
|
|
480
|
+
return {
|
|
481
|
+
[identifierSelectors]: (node) => {
|
|
482
|
+
const type = services.getTypeAtLocation(node);
|
|
483
|
+
const isStore = isType.store(type);
|
|
484
|
+
if (!isStore) return;
|
|
485
|
+
const data = rename(node);
|
|
486
|
+
if (node.typeAnnotation)
|
|
487
|
+
return context.report({ node, messageId: "invalid", data });
|
|
488
|
+
const suggestion = {
|
|
489
|
+
messageId: "rename",
|
|
490
|
+
data: { current: node.name, fixed: data.fixed },
|
|
491
|
+
fix: (fixer) => fixer.replaceText(node, data.fixed)
|
|
492
|
+
};
|
|
493
|
+
context.report({
|
|
494
|
+
node,
|
|
495
|
+
messageId: "invalid",
|
|
496
|
+
data,
|
|
497
|
+
suggest: [suggestion]
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
[`${selector16.shape.identifier}, ${selector16.shape.assignment}`]: (node) => {
|
|
501
|
+
const type = services.getTypeAtLocation(node.value);
|
|
502
|
+
const ident = node.value.type === NodeType.Identifier ? node.value : node.value.left;
|
|
503
|
+
const isStore = isType.store(type);
|
|
504
|
+
if (!isStore) return;
|
|
505
|
+
const data = rename(ident);
|
|
506
|
+
const suggestion = {
|
|
507
|
+
messageId: "rename",
|
|
508
|
+
data: { current: ident.name, fixed: data.fixed },
|
|
509
|
+
fix: (fixer) => (
|
|
510
|
+
// { x } -> { x: $x } / { x: y } -> { x: $y }
|
|
511
|
+
node.shorthand ? fixer.insertTextAfter(node.key, `: ${data.fixed}`) : fixer.replaceText(ident, data.fixed)
|
|
512
|
+
)
|
|
513
|
+
};
|
|
514
|
+
context.report({
|
|
515
|
+
node: ident,
|
|
516
|
+
messageId: "invalid",
|
|
517
|
+
data,
|
|
518
|
+
suggest: [suggestion]
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
var PrefixRegex = /^[^$]/;
|
|
525
|
+
var PostfixRegex = /[^$]$/;
|
|
526
|
+
var createSelector = (regex) => ({
|
|
527
|
+
variable: `VariableDeclarator > Identifier.id[name=${regex}]`,
|
|
528
|
+
array: {
|
|
529
|
+
identifier: `ArrayPattern > Identifier.elements[name=${regex}]`,
|
|
530
|
+
assignment: `ArrayPattern > AssignmentPattern > Identifier.left[name=${regex}]`
|
|
531
|
+
},
|
|
532
|
+
shape: {
|
|
533
|
+
identifier: `ObjectPattern > Property:has(> Identifier.value[name=${regex}])`,
|
|
534
|
+
assignment: `ObjectPattern > Property:has(> AssignmentPattern:has(> Identifier.left[name=${regex}]))`
|
|
535
|
+
},
|
|
536
|
+
function: {
|
|
537
|
+
identifier: `:function > Identifier.params[name=${regex}]`,
|
|
538
|
+
assignment: `:function > AssignmentPattern > Identifier.left[name=${regex}]`
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// src/rules/keep-options-order/keep-options-order.ts
|
|
543
|
+
var keep_options_order_default = createRule({
|
|
544
|
+
name: "keep-options-order",
|
|
545
|
+
meta: {
|
|
546
|
+
type: "problem",
|
|
547
|
+
docs: {
|
|
548
|
+
description: "Enforce options order for Effector methods"
|
|
549
|
+
},
|
|
550
|
+
messages: {
|
|
551
|
+
invalidOrder: `Order of options should be \`{{ correctOrder }}\`, but found \`{{ currentOrder }}\`.`,
|
|
552
|
+
changeOrder: "Sort options to follow the recommended order."
|
|
553
|
+
},
|
|
554
|
+
schema: [],
|
|
555
|
+
hasSuggestions: true
|
|
556
|
+
},
|
|
557
|
+
defaultOptions: [],
|
|
558
|
+
create: (context) => {
|
|
559
|
+
const source = context.sourceCode;
|
|
560
|
+
const imports = /* @__PURE__ */ new Set();
|
|
561
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
562
|
+
return {
|
|
563
|
+
[`${importSelector} > ${selector3.method}`]: (node) => imports.add(node.local.name),
|
|
564
|
+
[`CallExpression${selector3.call}:has(${selector3.argument})`]: (node) => {
|
|
565
|
+
if (!imports.has(node.callee.name)) return;
|
|
566
|
+
const [config] = node.arguments;
|
|
567
|
+
const hasWeirdProperty = config.properties.some(
|
|
568
|
+
(prop) => prop.type === NodeType.SpreadElement || prop.key.type !== NodeType.Identifier
|
|
569
|
+
);
|
|
570
|
+
if (hasWeirdProperty) return;
|
|
571
|
+
const properties = config.properties;
|
|
572
|
+
const current = properties.map((prop) => prop.key.name);
|
|
573
|
+
if (isCorrectOrder(current)) return;
|
|
574
|
+
const correctOrder = TRUE_ORDER.filter(
|
|
575
|
+
(item) => current.includes(item)
|
|
576
|
+
);
|
|
577
|
+
const othersOrder = current.filter(
|
|
578
|
+
(item) => !TRUE_ORDER.includes(item)
|
|
579
|
+
);
|
|
580
|
+
const order = [...correctOrder, ...othersOrder];
|
|
581
|
+
const snippets = properties.toSorted(
|
|
582
|
+
(a, b) => order.indexOf(a.key.name) - order.indexOf(b.key.name)
|
|
583
|
+
).map((prop) => source.getText(prop));
|
|
584
|
+
const suggestion = {
|
|
585
|
+
messageId: "changeOrder",
|
|
586
|
+
fix: (fixer) => [
|
|
587
|
+
fixer.replaceText(config, `{ ${snippets.join(", ")} }`)
|
|
588
|
+
]
|
|
589
|
+
};
|
|
590
|
+
const data = {
|
|
591
|
+
correctOrder: correctOrder.join(" -> "),
|
|
592
|
+
currentOrder: current.join(" -> ")
|
|
593
|
+
};
|
|
594
|
+
context.report({
|
|
595
|
+
node: config,
|
|
596
|
+
messageId: "invalidOrder",
|
|
597
|
+
data,
|
|
598
|
+
suggest: [suggestion]
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
var TRUE_ORDER = [
|
|
605
|
+
"clock",
|
|
606
|
+
"source",
|
|
607
|
+
"filter",
|
|
608
|
+
"fn",
|
|
609
|
+
"target",
|
|
610
|
+
"greedy",
|
|
611
|
+
"batch",
|
|
612
|
+
"name"
|
|
613
|
+
];
|
|
614
|
+
var selector3 = {
|
|
615
|
+
method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
|
|
616
|
+
call: `[callee.type="Identifier"][arguments.length=1]`,
|
|
617
|
+
argument: `ObjectExpression.arguments`
|
|
618
|
+
};
|
|
619
|
+
var isCorrectOrder = (current) => {
|
|
620
|
+
let seen = -1;
|
|
621
|
+
for (const item of current) {
|
|
622
|
+
const index = TRUE_ORDER.indexOf(item);
|
|
623
|
+
const placement = index === -1 ? Infinity : index;
|
|
624
|
+
if (placement <= seen) return false;
|
|
625
|
+
seen = placement;
|
|
626
|
+
}
|
|
627
|
+
return true;
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// src/rules/mandatory-scope-binding/mandatory-scope-binding.ts
|
|
631
|
+
import ts2 from "typescript";
|
|
632
|
+
|
|
633
|
+
// src/shared/name.ts
|
|
634
|
+
function functionToName(node) {
|
|
635
|
+
if (node.id) return node.id;
|
|
636
|
+
if (node.parent.type === NodeType.VariableDeclarator && node.parent.id.type === NodeType.Identifier)
|
|
637
|
+
return node.parent.id;
|
|
638
|
+
if (node.parent.type === NodeType.AssignmentExpression && node.parent.left.type === NodeType.Identifier)
|
|
639
|
+
return node.parent.left;
|
|
640
|
+
if (node.parent.type === NodeType.Property && node.parent.key.type === NodeType.Identifier)
|
|
641
|
+
return node.parent.key;
|
|
642
|
+
if (node.parent.type === NodeType.AssignmentPattern && node.parent.left.type === NodeType.Identifier)
|
|
643
|
+
return node.parent.left;
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function calleeToName(callee) {
|
|
647
|
+
if (callee.type === NodeType.Identifier) return callee;
|
|
648
|
+
else if (callee.type === NodeType.MemberExpression && callee.property.type === NodeType.Identifier)
|
|
649
|
+
return callee.property;
|
|
650
|
+
else return null;
|
|
651
|
+
}
|
|
652
|
+
function simpleExpressionToName(node) {
|
|
653
|
+
if (node.type === NodeType.Identifier) return node.name;
|
|
654
|
+
if (node.type === NodeType.MemberExpression && !node.computed)
|
|
655
|
+
return node.property.name;
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
var nameOf = {
|
|
659
|
+
function: functionToName,
|
|
660
|
+
callee: calleeToName,
|
|
661
|
+
expression: { simple: simpleExpressionToName }
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// src/rules/mandatory-scope-binding/mandatory-scope-binding.ts
|
|
665
|
+
var mandatory_scope_binding_default = createRule({
|
|
666
|
+
name: "mandatory-scope-binding",
|
|
667
|
+
meta: {
|
|
668
|
+
type: "problem",
|
|
669
|
+
docs: {
|
|
670
|
+
description: "Forbid `Event` and `Effect` usage without `useUnit` in React."
|
|
671
|
+
},
|
|
672
|
+
messages: {
|
|
673
|
+
useUnitNeeded: '"{{ name }}" must be wrapped with `useUnit` from `effector-react` before usage inside React.'
|
|
674
|
+
},
|
|
675
|
+
schema: []
|
|
676
|
+
},
|
|
677
|
+
defaultOptions: [],
|
|
678
|
+
create: (context) => {
|
|
679
|
+
const services = getParserServices(context);
|
|
680
|
+
const checker = services.program.getTypeChecker();
|
|
681
|
+
const inRender = [];
|
|
682
|
+
const inHook = [];
|
|
683
|
+
const isExpectingUnit = (slot) => {
|
|
684
|
+
const tsnode = services.esTreeNodeToTSNodeMap.get(slot);
|
|
685
|
+
const type = checker.getContextualType(tsnode);
|
|
686
|
+
if (type) return isType.event(type) || isType.effect(type);
|
|
687
|
+
else return false;
|
|
688
|
+
};
|
|
689
|
+
const check2 = (mode, node) => {
|
|
690
|
+
const rendering = inRender.at(-1) ?? false;
|
|
691
|
+
if (!rendering) return;
|
|
692
|
+
const type = services.getTypeAtLocation(node);
|
|
693
|
+
if (!isType.event(type) && !isType.effect(type)) return;
|
|
694
|
+
if (mode === "call") return report(node);
|
|
695
|
+
const delegated = isExpectingUnit(node), eligible = mode === "jsx" || (inHook.at(-1) ?? false);
|
|
696
|
+
if (eligible && delegated) return;
|
|
697
|
+
else return report(node);
|
|
698
|
+
};
|
|
699
|
+
const report = (node) => {
|
|
700
|
+
const name = nameOf.expression.simple(node) ?? "<expression>";
|
|
701
|
+
context.report({ node, messageId: "useUnitNeeded", data: { name } });
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
// detect react render contexts
|
|
705
|
+
[`:matches(${selector4.function})`]: (node) => {
|
|
706
|
+
const current = inRender.at(-1) ?? false;
|
|
707
|
+
if (current) return void inRender.push(true);
|
|
708
|
+
const name = nameOf.function(node);
|
|
709
|
+
if (name && UseRegex.test(name.name)) return void inRender.push(true);
|
|
710
|
+
const tsnode = services.esTreeNodeToTSNodeMap.get(node);
|
|
711
|
+
const signature = checker.getSignatureFromDeclaration(tsnode);
|
|
712
|
+
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : void 0;
|
|
713
|
+
const isJSX = matchesType(returnType, (type) => isType.jsx(type));
|
|
714
|
+
if (isJSX) return void inRender.push(true);
|
|
715
|
+
const inferred = ts2.isExpression(tsnode) ? checker.getContextualType(tsnode) : void 0;
|
|
716
|
+
const isComponent = matchesType(
|
|
717
|
+
inferred,
|
|
718
|
+
(type) => isType.component(type)
|
|
719
|
+
);
|
|
720
|
+
if (isComponent) return void inRender.push(true);
|
|
721
|
+
return void inRender.push(false);
|
|
722
|
+
},
|
|
723
|
+
[`:matches(${selector4.function}):exit`]: () => void inRender.pop(),
|
|
724
|
+
// bail from tracking classes
|
|
725
|
+
ClassDeclaration: () => void inRender.push(false),
|
|
726
|
+
"ClassDeclaration:exit": () => void inRender.pop(),
|
|
727
|
+
// detect contexts where we may delegate `useUnit` binding to the callee
|
|
728
|
+
CallExpression: (node) => {
|
|
729
|
+
const id = nameOf.callee(node.callee), isEnteringHook = id !== null && UseRegex.test(id.name);
|
|
730
|
+
inHook.push(isEnteringHook);
|
|
731
|
+
},
|
|
732
|
+
"CallExpression:exit": () => void inHook.pop(),
|
|
733
|
+
// direct invocation site `event()` & `model.event()`
|
|
734
|
+
// - receiver is being invoked directly, always dangerous
|
|
735
|
+
[`${selector4.callee.direct}, ${selector4.callee.member}`]: (node) => check2("call", node),
|
|
736
|
+
// argument position `fn(event)` & `fn(model.event)`
|
|
737
|
+
// - dangerous unless scope binding delegated (arg expects unit)
|
|
738
|
+
[`${selector4.arg.direct}, ${selector4.arg.member}`]: (node) => check2("arg", node),
|
|
739
|
+
// one-level-deep object-property position `fn({ key: event })` & `fn({
|
|
740
|
+
// key: model.event })`
|
|
741
|
+
// - dangerous unless scope binding delegated (key expects unit)
|
|
742
|
+
[`${selector4.prop.direct}, ${selector4.prop.member}`]: (node) => check2("prop", node),
|
|
743
|
+
// jsx expression slot `<C prop={event}>`, `<C prop={model.event}>`
|
|
744
|
+
// - dangerous unless scope binding delegated (prop expects unit)
|
|
745
|
+
[`${selector4.jsx.direct}, ${selector4.jsx.member}`]: (node) => check2("jsx", node)
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
var matchesType = (type, predicate) => {
|
|
750
|
+
if (!type) return false;
|
|
751
|
+
if (type.isUnion()) return type.types.some(predicate);
|
|
752
|
+
return predicate(type);
|
|
753
|
+
};
|
|
754
|
+
var UseRegex = /^use[A-Z0-9].*$/;
|
|
755
|
+
var selector4 = {
|
|
756
|
+
function: "FunctionDeclaration, FunctionExpression, ArrowFunctionExpression",
|
|
757
|
+
callee: {
|
|
758
|
+
direct: "CallExpression > Identifier.callee",
|
|
759
|
+
member: "CallExpression > MemberExpression[computed=false].callee"
|
|
760
|
+
},
|
|
761
|
+
arg: {
|
|
762
|
+
direct: "CallExpression > Identifier:not(.callee)",
|
|
763
|
+
member: "CallExpression > MemberExpression[computed=false]:not(.callee)"
|
|
764
|
+
},
|
|
765
|
+
prop: {
|
|
766
|
+
direct: "CallExpression > ObjectExpression > Property > Identifier.value",
|
|
767
|
+
member: "CallExpression > ObjectExpression > Property > MemberExpression[computed=false].value"
|
|
768
|
+
},
|
|
769
|
+
jsx: {
|
|
770
|
+
direct: "JSXExpressionContainer > Identifier",
|
|
771
|
+
member: "JSXExpressionContainer > MemberExpression[computed=false]"
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// src/shared/locate.ts
|
|
776
|
+
var property = (key2, node) => node.properties.find(
|
|
777
|
+
(prop) => prop.type == NodeType.Property && prop.key.type === NodeType.Identifier && prop.key.name === key2
|
|
778
|
+
);
|
|
779
|
+
var locate = { property };
|
|
780
|
+
|
|
781
|
+
// src/rules/no-ambiguity-target/no-ambiguity-target.ts
|
|
782
|
+
var no_ambiguity_target_default = createRule({
|
|
783
|
+
name: "no-ambiguity-target",
|
|
784
|
+
meta: {
|
|
785
|
+
type: "problem",
|
|
786
|
+
docs: {
|
|
787
|
+
description: "Forbid ambiguous target in `sample` and `guard`."
|
|
788
|
+
},
|
|
789
|
+
messages: {
|
|
790
|
+
ambiguous: "Method `{{ method }}` both specifies `target` option and assigns the result to a variable. Consider removing one of them."
|
|
791
|
+
},
|
|
792
|
+
schema: []
|
|
793
|
+
},
|
|
794
|
+
defaultOptions: [],
|
|
795
|
+
create: (context) => {
|
|
796
|
+
const imports = /* @__PURE__ */ new Set();
|
|
797
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
798
|
+
const usageStack = [];
|
|
799
|
+
return {
|
|
800
|
+
ReturnStatement: () => usageStack.push(true),
|
|
801
|
+
"ReturnStatement:exit": () => usageStack.pop(),
|
|
802
|
+
VariableDeclarator: () => usageStack.push(true),
|
|
803
|
+
"VariableDeclarator:exit": () => usageStack.pop(),
|
|
804
|
+
ObjectExpression: () => usageStack.push(true),
|
|
805
|
+
"ObjectExpression:exit": () => usageStack.pop(),
|
|
806
|
+
BlockStatement: () => usageStack.push(false),
|
|
807
|
+
"BlockStatement:exit": () => usageStack.pop(),
|
|
808
|
+
[`${importSelector} > ${selector5.method}`]: (node) => imports.add(node.local.name),
|
|
809
|
+
[`CallExpression[callee.type="Identifier"]`]: (node) => {
|
|
810
|
+
const isTracked = imports.has(node.callee.name);
|
|
811
|
+
if (!isTracked) return;
|
|
812
|
+
const isUsed = usageStack.at(-1) ?? false;
|
|
813
|
+
if (!isUsed) return;
|
|
814
|
+
const [config] = node.arguments;
|
|
815
|
+
if (config?.type !== NodeType.ObjectExpression)
|
|
816
|
+
return;
|
|
817
|
+
const target = locate.property("target", config);
|
|
818
|
+
if (!target) return;
|
|
819
|
+
context.report({
|
|
820
|
+
node,
|
|
821
|
+
messageId: "ambiguous",
|
|
822
|
+
data: { method: node.callee.name }
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
var selector5 = {
|
|
829
|
+
method: `ImportSpecifier[imported.name=/(sample|guard)/]`
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// src/rules/no-domain-unit-creators/no-domain-unit-creators.ts
|
|
833
|
+
var no_domain_unit_creators_default = createRule({
|
|
834
|
+
name: "no-domain-unit-creators",
|
|
835
|
+
meta: {
|
|
836
|
+
type: "suggestion",
|
|
837
|
+
docs: {
|
|
838
|
+
description: "Disallow using Domain methods to create units."
|
|
839
|
+
},
|
|
840
|
+
messages: {
|
|
841
|
+
avoid: "Avoid using `.{{ method }}` on a Domain instance. Use a standard factory unit creator `{{ factory }}` with a `domain` option instead."
|
|
842
|
+
},
|
|
843
|
+
schema: []
|
|
844
|
+
},
|
|
845
|
+
defaultOptions: [],
|
|
846
|
+
create: (context) => {
|
|
847
|
+
const services = getParserServices(context);
|
|
848
|
+
return {
|
|
849
|
+
[`CallExpression:has(> ${selector6.member})`]: (node) => {
|
|
850
|
+
const name = node.callee.property.name;
|
|
851
|
+
if (!METHODS.has(name)) return;
|
|
852
|
+
const type = services.getTypeAtLocation(node.callee.object);
|
|
853
|
+
if (!isType.domain(type)) return;
|
|
854
|
+
const factory = ALIAS_MAP.get(name) ?? name;
|
|
855
|
+
context.report({
|
|
856
|
+
node,
|
|
857
|
+
messageId: "avoid",
|
|
858
|
+
data: { method: name, factory }
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
var ALIAS_MAP = (/* @__PURE__ */ new Map()).set("event", "createEvent").set("store", "createStore").set("effect", "createEffect").set("domain", "createDomain");
|
|
865
|
+
var METHODS = /* @__PURE__ */ new Set([...ALIAS_MAP.values(), ...ALIAS_MAP.keys()]);
|
|
866
|
+
var selector6 = {
|
|
867
|
+
member: `MemberExpression.callee[property.type="Identifier"]`
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// src/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.ts
|
|
871
|
+
var no_duplicate_clock_or_source_array_values_default = createRule({
|
|
872
|
+
name: "no-duplicate-clock-or-source-array-values",
|
|
873
|
+
meta: {
|
|
874
|
+
type: "problem",
|
|
875
|
+
docs: {
|
|
876
|
+
description: "Forbid providing duplicate units in `clock` and `source` arrays in `sample` and `guard`."
|
|
877
|
+
},
|
|
878
|
+
messages: {
|
|
879
|
+
duplicate: "`{{ field }}` contains a duplicate unit `{{ unit }}`.",
|
|
880
|
+
remove: "Remove duplicate unit `{{ unit }}`."
|
|
881
|
+
},
|
|
882
|
+
schema: [],
|
|
883
|
+
hasSuggestions: true
|
|
884
|
+
},
|
|
885
|
+
defaultOptions: [],
|
|
886
|
+
create: (context) => {
|
|
887
|
+
const imports = /* @__PURE__ */ new Set();
|
|
888
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
889
|
+
const analyze = (node, field) => {
|
|
890
|
+
const seen = /* @__PURE__ */ new Map();
|
|
891
|
+
const entries = node.elements.filter((item) => item !== null).filter((item) => item.type !== NodeType.SpreadElement);
|
|
892
|
+
for (const entry of entries) {
|
|
893
|
+
const root = traverseToRoot(entry);
|
|
894
|
+
if (!root) continue;
|
|
895
|
+
const name = [root.node.name, ...root.path].join(".");
|
|
896
|
+
if (seen.has(name)) report(entry, name, field);
|
|
897
|
+
else seen.set(name, entry);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
const report = (node, name, field) => {
|
|
901
|
+
const data = { field, unit: name };
|
|
902
|
+
const suggestion = {
|
|
903
|
+
messageId: "remove",
|
|
904
|
+
data: { unit: name },
|
|
905
|
+
fix: function* (fixer) {
|
|
906
|
+
yield fixer.remove(node);
|
|
907
|
+
const before = context.sourceCode.getTokenBefore(node);
|
|
908
|
+
if (before?.value === ",") yield fixer.remove(before);
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
context.report({
|
|
912
|
+
node,
|
|
913
|
+
messageId: "duplicate",
|
|
914
|
+
data,
|
|
915
|
+
suggest: [suggestion]
|
|
916
|
+
});
|
|
917
|
+
};
|
|
918
|
+
return {
|
|
919
|
+
[`${importSelector} > ${selector7.method}`]: (node) => imports.add(node.local.name),
|
|
920
|
+
[`CallExpression${selector7.call}:has(${selector7.argument})`]: (node) => {
|
|
921
|
+
if (!imports.has(node.callee.name)) return;
|
|
922
|
+
const [config] = node.arguments;
|
|
923
|
+
const clock = locate.property("clock", config);
|
|
924
|
+
const source = locate.property("source", config);
|
|
925
|
+
if (clock?.value?.type === NodeType.ArrayExpression)
|
|
926
|
+
analyze(clock.value, "clock");
|
|
927
|
+
if (source?.value?.type === NodeType.ArrayExpression)
|
|
928
|
+
analyze(source.value, "source");
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
var selector7 = {
|
|
934
|
+
method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
|
|
935
|
+
call: `[callee.type="Identifier"][arguments.length=1]`,
|
|
936
|
+
argument: `ObjectExpression.arguments`
|
|
937
|
+
};
|
|
938
|
+
function traverseToRoot(node, path = []) {
|
|
939
|
+
if (node.type === NodeType.Identifier) return { node, path };
|
|
940
|
+
if (node.type === NodeType.MemberExpression && node.property.type === NodeType.Identifier)
|
|
941
|
+
return traverseToRoot(node.object, [node.property.name, ...path]);
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/rules/no-duplicate-on/no-duplicate-on.ts
|
|
946
|
+
var no_duplicate_on_default = createRule({
|
|
947
|
+
name: "no-duplicate-on",
|
|
948
|
+
meta: {
|
|
949
|
+
type: "problem",
|
|
950
|
+
docs: {
|
|
951
|
+
description: "Forbid duplicate `.on` calls on Stores."
|
|
952
|
+
},
|
|
953
|
+
messages: {
|
|
954
|
+
duplicate: "Method `.on` is called on store `{{ store }}` more than once for `{{ unit }}`."
|
|
955
|
+
},
|
|
956
|
+
schema: []
|
|
957
|
+
},
|
|
958
|
+
defaultOptions: [],
|
|
959
|
+
create: (context) => {
|
|
960
|
+
const services = getParserServices(context);
|
|
961
|
+
const map = /* @__PURE__ */ new Map();
|
|
962
|
+
return {
|
|
963
|
+
[`CallExpression[callee.property.name="on"]`]: (node) => {
|
|
964
|
+
const type = services.getTypeAtLocation(node.callee.object);
|
|
965
|
+
const isStore = isType.store(type);
|
|
966
|
+
if (!isStore) return;
|
|
967
|
+
const arg = node.arguments[0];
|
|
968
|
+
if (!arg || arg.type === NodeType.SpreadElement) return;
|
|
969
|
+
const units = arg.type === NodeType.ArrayExpression ? arg.elements.filter(
|
|
970
|
+
(item) => item !== null && item.type !== NodeType.SpreadElement
|
|
971
|
+
) : [arg];
|
|
972
|
+
const scope2 = context.sourceCode.getScope(node);
|
|
973
|
+
const store = identify("store", node.callee.object, scope2);
|
|
974
|
+
if (!store) return;
|
|
975
|
+
const set = map.get(store.id) ?? /* @__PURE__ */ new Set();
|
|
976
|
+
for (const unit of units) {
|
|
977
|
+
const instance = identify("unit", unit, scope2);
|
|
978
|
+
if (!instance) continue;
|
|
979
|
+
if (set.has(instance.id)) {
|
|
980
|
+
const data = { store: store.name, unit: instance.name };
|
|
981
|
+
context.report({ messageId: "duplicate", node: unit, data });
|
|
982
|
+
} else set.add(instance.id);
|
|
983
|
+
}
|
|
984
|
+
map.set(store.id, set);
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
function traverseToRoot2(node, path = []) {
|
|
990
|
+
if (node.type === NodeType.Identifier) return { node, path };
|
|
991
|
+
if (node.type === NodeType.MemberExpression && node.property.type === NodeType.Identifier)
|
|
992
|
+
return traverseToRoot2(node.object, [node.property.name, ...path]);
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
var STORE_METHODS = ["on", "reset"];
|
|
996
|
+
function traverseStoreToRoot(node, path = []) {
|
|
997
|
+
if (node.type === NodeType.Identifier) return { node, path };
|
|
998
|
+
if (node.type === NodeType.MemberExpression && node.property.type === NodeType.Identifier)
|
|
999
|
+
return traverseStoreToRoot(node.object, [node.property.name, ...path]);
|
|
1000
|
+
if (node.type === NodeType.CallExpression && node.callee.type === NodeType.MemberExpression) {
|
|
1001
|
+
if (node.callee.property.type === NodeType.Identifier && STORE_METHODS.includes(node.callee.property.name))
|
|
1002
|
+
return traverseStoreToRoot(node.callee.object, path);
|
|
1003
|
+
}
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
function raiseStoreToVariable(node) {
|
|
1007
|
+
let current = node;
|
|
1008
|
+
while (current.parent) {
|
|
1009
|
+
if (current.parent.type === NodeType.VariableDeclarator)
|
|
1010
|
+
return current.parent;
|
|
1011
|
+
if (current.parent.type !== NodeType.MemberExpression || current.parent.object !== current)
|
|
1012
|
+
return null;
|
|
1013
|
+
if (current.parent.property.type !== NodeType.Identifier || !STORE_METHODS.includes(current.parent.property.name))
|
|
1014
|
+
return null;
|
|
1015
|
+
const grandparent = current.parent.parent;
|
|
1016
|
+
if (grandparent?.type !== NodeType.CallExpression || grandparent.callee !== current.parent)
|
|
1017
|
+
return null;
|
|
1018
|
+
current = current.parent.parent;
|
|
1019
|
+
}
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
function findSuitableRoot(type, node) {
|
|
1023
|
+
if (type === "unit") return traverseToRoot2(node);
|
|
1024
|
+
const root = traverseStoreToRoot(node);
|
|
1025
|
+
if (root) return root;
|
|
1026
|
+
const declarator = raiseStoreToVariable(node);
|
|
1027
|
+
if (declarator && declarator.id.type === NodeType.Identifier)
|
|
1028
|
+
return { node: declarator.id, path: [] };
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
function identify(type, node, scope2) {
|
|
1032
|
+
const root = findSuitableRoot(type, node);
|
|
1033
|
+
if (!root) return null;
|
|
1034
|
+
const variable = findVariable(scope2, root.node.name);
|
|
1035
|
+
if (!variable) return null;
|
|
1036
|
+
return {
|
|
1037
|
+
id: `${variableId(variable)}+${root.path.join(".")}`,
|
|
1038
|
+
name: [variable.name, ...root.path].join(".")
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function findVariable(scope2, name) {
|
|
1042
|
+
for (let current = scope2; current; current = current.upper) {
|
|
1043
|
+
const variable = current.set.get(name);
|
|
1044
|
+
if (variable) return variable;
|
|
1045
|
+
}
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
var variableCounter = 0;
|
|
1049
|
+
var variableIds = /* @__PURE__ */ new WeakMap();
|
|
1050
|
+
function variableId(variable) {
|
|
1051
|
+
let id = variableIds.get(variable);
|
|
1052
|
+
if (id === void 0) variableIds.set(variable, id = ++variableCounter);
|
|
1053
|
+
return id;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/rules/no-forward/no-forward.ts
|
|
1057
|
+
import esquery from "esquery";
|
|
1058
|
+
var no_forward_default = createRule({
|
|
1059
|
+
name: "no-forward",
|
|
1060
|
+
meta: {
|
|
1061
|
+
type: "problem",
|
|
1062
|
+
docs: {
|
|
1063
|
+
description: "Prefer `sample` over `forward`."
|
|
1064
|
+
},
|
|
1065
|
+
messages: {
|
|
1066
|
+
noForward: "Use `sample` operator instead of `forward` as a more universal approach.",
|
|
1067
|
+
replaceWithSample: "Replace `forward` with `sample`."
|
|
1068
|
+
},
|
|
1069
|
+
hasSuggestions: true,
|
|
1070
|
+
schema: []
|
|
1071
|
+
},
|
|
1072
|
+
defaultOptions: [],
|
|
1073
|
+
create: (context) => {
|
|
1074
|
+
let sample;
|
|
1075
|
+
const forwards = /* @__PURE__ */ new Map();
|
|
1076
|
+
const source = context.sourceCode;
|
|
1077
|
+
const visitorKeys = source.visitorKeys;
|
|
1078
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
1079
|
+
return {
|
|
1080
|
+
[`${importSelector} > ${selector8.forward}`]: (node) => forwards.set(node.local.name, node),
|
|
1081
|
+
[`${importSelector} > ${selector8.sample}`]: (node) => sample = node.local.name,
|
|
1082
|
+
[`CallExpression${selector8.call}:has(${selector8.argument})`]: (node) => {
|
|
1083
|
+
if (!forwards.has(node.callee.name)) return;
|
|
1084
|
+
const config = {};
|
|
1085
|
+
const arg = node.arguments[0];
|
|
1086
|
+
config.clock = locate.property("from", arg)?.value;
|
|
1087
|
+
config.target = locate.property("to", arg)?.value;
|
|
1088
|
+
if (config.target) {
|
|
1089
|
+
const [call] = esquery.match(config.target, query.prepend, { visitorKeys }).map((match) => match).filter((match) => match === config.target);
|
|
1090
|
+
if (call)
|
|
1091
|
+
[config.target, config.fn] = [
|
|
1092
|
+
call.callee.object,
|
|
1093
|
+
call.arguments[0]
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
if (config.clock && !config.fn) {
|
|
1097
|
+
const [call] = esquery.match(config.clock, query.map, { visitorKeys }).map((match) => match).filter((match) => match === config.clock);
|
|
1098
|
+
if (call)
|
|
1099
|
+
[config.clock, config.fn] = [call.callee.object, call.arguments[0]];
|
|
1100
|
+
}
|
|
1101
|
+
const code = ["clock", "fn", "target"].filter((key2) => config[key2] !== void 0).map((key2) => `${key2}: ${source.getText(config[key2])}`).join(", ");
|
|
1102
|
+
const suggestion = {
|
|
1103
|
+
messageId: "replaceWithSample",
|
|
1104
|
+
fix: function* (fixer) {
|
|
1105
|
+
const fn = sample ?? "sample";
|
|
1106
|
+
yield fixer.replaceText(node, `${fn}({ ${code} })`);
|
|
1107
|
+
if (!sample)
|
|
1108
|
+
yield fixer.insertTextAfter(
|
|
1109
|
+
forwards.get(node.callee.name),
|
|
1110
|
+
`, sample`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
context.report({
|
|
1115
|
+
messageId: "noForward",
|
|
1116
|
+
node: node.callee,
|
|
1117
|
+
suggest: [suggestion]
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
var selector8 = {
|
|
1124
|
+
forward: `ImportSpecifier[imported.name="forward"]`,
|
|
1125
|
+
sample: `ImportSpecifier[imported.name="sample"]`,
|
|
1126
|
+
call: `[callee.type="Identifier"][arguments.length=1]`,
|
|
1127
|
+
argument: `ObjectExpression.arguments`
|
|
1128
|
+
};
|
|
1129
|
+
var query = {
|
|
1130
|
+
map: esquery.parse(
|
|
1131
|
+
"CallExpression[arguments.length=1]:has(> :first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='map']))"
|
|
1132
|
+
),
|
|
1133
|
+
prepend: esquery.parse(
|
|
1134
|
+
"CallExpression[arguments.length=1]:has(> :first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='prepend']))"
|
|
1135
|
+
)
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
// src/rules/no-getState/no-getState.ts
|
|
1139
|
+
var no_getState_default = createRule({
|
|
1140
|
+
name: "no-getState",
|
|
1141
|
+
meta: {
|
|
1142
|
+
type: "problem",
|
|
1143
|
+
docs: {
|
|
1144
|
+
description: "Forbid `.getState` calls on Effector stores."
|
|
1145
|
+
},
|
|
1146
|
+
messages: {
|
|
1147
|
+
named: "Method `.getState` used on store `{{ name }}` can lead to race conditions. Replace with with `sample` or `attach`.",
|
|
1148
|
+
anonymous: "Method `.getState` used on store can lead to race conditions. Replace with with `sample` or `attach`."
|
|
1149
|
+
},
|
|
1150
|
+
schema: []
|
|
1151
|
+
},
|
|
1152
|
+
defaultOptions: [],
|
|
1153
|
+
create: (context) => {
|
|
1154
|
+
const services = getParserServices(context);
|
|
1155
|
+
return {
|
|
1156
|
+
[`CallExpression[callee.type="MemberExpression"][callee.property.name="getState"]`]: (node) => {
|
|
1157
|
+
const type = services.getTypeAtLocation(node.callee.object);
|
|
1158
|
+
const isStore = isType.store(type);
|
|
1159
|
+
if (!isStore) return;
|
|
1160
|
+
const name = nameOf.expression.simple(node.callee.object);
|
|
1161
|
+
if (name) context.report({ node, messageId: "named", data: { name } });
|
|
1162
|
+
else context.report({ node, messageId: "anonymous" });
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
// src/rules/no-guard/no-guard.ts
|
|
1169
|
+
import esquery2 from "esquery";
|
|
1170
|
+
var no_guard_default = createRule({
|
|
1171
|
+
name: "no-guard",
|
|
1172
|
+
meta: {
|
|
1173
|
+
type: "problem",
|
|
1174
|
+
docs: {
|
|
1175
|
+
description: "Prefer `sample` over `guard`."
|
|
1176
|
+
},
|
|
1177
|
+
messages: {
|
|
1178
|
+
noGuard: "Use `sample` operator instead of `guard` as a more universal approach.",
|
|
1179
|
+
replaceWithSample: "Replace `guard` with `sample`."
|
|
1180
|
+
},
|
|
1181
|
+
hasSuggestions: true,
|
|
1182
|
+
schema: []
|
|
1183
|
+
},
|
|
1184
|
+
defaultOptions: [],
|
|
1185
|
+
create: (context) => {
|
|
1186
|
+
let sample;
|
|
1187
|
+
const guards = /* @__PURE__ */ new Map();
|
|
1188
|
+
const source = context.sourceCode;
|
|
1189
|
+
const visitorKeys = source.visitorKeys;
|
|
1190
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
1191
|
+
return {
|
|
1192
|
+
[`${importSelector} > ${selector9.guard}`]: (node) => guards.set(node.local.name, node),
|
|
1193
|
+
[`${importSelector} > ${selector9.sample}`]: (node) => sample = node.local.name,
|
|
1194
|
+
[`CallExpression${selector9.call}`]: (node) => {
|
|
1195
|
+
if (!guards.has(node.callee.name)) return;
|
|
1196
|
+
const config = {};
|
|
1197
|
+
if (node.arguments.length === 1 && node.arguments[0].type === NodeType.ObjectExpression) {
|
|
1198
|
+
const [arg] = node.arguments;
|
|
1199
|
+
for (const key2 of ["clock", "source", "filter", "target"])
|
|
1200
|
+
config[key2] = locate.property(key2, arg)?.value;
|
|
1201
|
+
} else if (node.arguments.length === 2 && node.arguments[1].type === NodeType.ObjectExpression) {
|
|
1202
|
+
const [clock, arg] = node.arguments;
|
|
1203
|
+
config.clock = clock;
|
|
1204
|
+
for (const key2 of ["source", "filter", "target"])
|
|
1205
|
+
config[key2] = locate.property(key2, arg)?.value;
|
|
1206
|
+
} else return;
|
|
1207
|
+
if (config.target) {
|
|
1208
|
+
const [call] = esquery2.match(config.target, query2.prepend, { visitorKeys }).map((match) => match).filter((match) => match === config.target);
|
|
1209
|
+
if (call)
|
|
1210
|
+
[config.target, config.fn] = [
|
|
1211
|
+
call.callee.object,
|
|
1212
|
+
call.arguments[0]
|
|
1213
|
+
];
|
|
1214
|
+
}
|
|
1215
|
+
const code = ["clock", "source", "filter", "fn", "target"].filter((key2) => config[key2] !== void 0).map((key2) => `${key2}: ${source.getText(config[key2])}`).join(", ");
|
|
1216
|
+
const suggestion = {
|
|
1217
|
+
messageId: "replaceWithSample",
|
|
1218
|
+
fix: function* (fixer) {
|
|
1219
|
+
const fn = sample ?? "sample";
|
|
1220
|
+
yield fixer.replaceText(node, `${fn}({ ${code} })`);
|
|
1221
|
+
if (!sample)
|
|
1222
|
+
yield fixer.insertTextAfter(
|
|
1223
|
+
guards.get(node.callee.name),
|
|
1224
|
+
`, sample`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
context.report({
|
|
1229
|
+
messageId: "noGuard",
|
|
1230
|
+
node: node.callee,
|
|
1231
|
+
suggest: [suggestion]
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
var selector9 = {
|
|
1238
|
+
guard: `ImportSpecifier[imported.name="guard"]`,
|
|
1239
|
+
sample: `ImportSpecifier[imported.name="sample"]`,
|
|
1240
|
+
call: `[callee.type="Identifier"]`
|
|
1241
|
+
};
|
|
1242
|
+
var query2 = {
|
|
1243
|
+
prepend: esquery2.parse(
|
|
1244
|
+
"CallExpression[arguments.length=1]:has(:first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='prepend']))"
|
|
1245
|
+
)
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
// src/rules/no-patronum-debug/no-patronum-debug.ts
|
|
1249
|
+
var no_patronum_debug_default = createRule({
|
|
1250
|
+
name: "no-patronum-debug",
|
|
1251
|
+
meta: {
|
|
1252
|
+
type: "problem",
|
|
1253
|
+
docs: {
|
|
1254
|
+
description: "Disallow the use of `patronum` `debug`."
|
|
1255
|
+
},
|
|
1256
|
+
messages: {
|
|
1257
|
+
unexpected: "Unexpected `debug` call.",
|
|
1258
|
+
remove: "Remove this `debug` call."
|
|
1259
|
+
},
|
|
1260
|
+
schema: [],
|
|
1261
|
+
hasSuggestions: true
|
|
1262
|
+
},
|
|
1263
|
+
defaultOptions: [],
|
|
1264
|
+
create: (context) => {
|
|
1265
|
+
const debugs = /* @__PURE__ */ new Set();
|
|
1266
|
+
const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME2}]`;
|
|
1267
|
+
return {
|
|
1268
|
+
[`${importSelector} > ${selector10.debug}`]: (node) => debugs.add(node.local.name),
|
|
1269
|
+
[`CallExpression:matches(${selector10.call})`]: (node) => {
|
|
1270
|
+
const name = toName2(node);
|
|
1271
|
+
if (!debugs.has(name)) return;
|
|
1272
|
+
const suggestion = {
|
|
1273
|
+
messageId: "remove",
|
|
1274
|
+
fix: (fixer) => {
|
|
1275
|
+
if (node.parent.type === NodeType.ExpressionStatement)
|
|
1276
|
+
return fixer.remove(node.parent);
|
|
1277
|
+
else return fixer.replaceText(node, "undefined");
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
context.report({
|
|
1281
|
+
messageId: "unexpected",
|
|
1282
|
+
node: node.callee,
|
|
1283
|
+
suggest: [suggestion]
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
var PACKAGE_NAME2 = /^patronum(?:\u002Fdebug)?$/;
|
|
1290
|
+
var selector10 = {
|
|
1291
|
+
debug: `ImportSpecifier[imported.name="debug"]`,
|
|
1292
|
+
call: `[callee.type=Identifier], [callee.object.type=Identifier]`
|
|
1293
|
+
};
|
|
1294
|
+
var toName2 = (node) => {
|
|
1295
|
+
switch (node.callee.type) {
|
|
1296
|
+
case NodeType.Identifier:
|
|
1297
|
+
return node.callee.name;
|
|
1298
|
+
case NodeType.MemberExpression:
|
|
1299
|
+
return node.callee.object.name;
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
// src/rules/no-units-spawn-in-render/no-units-spawn-in-render.ts
|
|
1304
|
+
import {
|
|
1305
|
+
isExpression
|
|
1306
|
+
} from "typescript";
|
|
1307
|
+
var EFFECTOR_FACTORIES = /* @__PURE__ */ new Set([
|
|
1308
|
+
"createStore",
|
|
1309
|
+
"createEvent",
|
|
1310
|
+
"createEffect",
|
|
1311
|
+
"createDomain",
|
|
1312
|
+
"createApi",
|
|
1313
|
+
"restore"
|
|
1314
|
+
]);
|
|
1315
|
+
var EFFECTOR_OPERATORS = /* @__PURE__ */ new Set([
|
|
1316
|
+
"sample",
|
|
1317
|
+
"guard",
|
|
1318
|
+
"forward",
|
|
1319
|
+
"merge",
|
|
1320
|
+
"split",
|
|
1321
|
+
"combine",
|
|
1322
|
+
"attach"
|
|
1323
|
+
]);
|
|
1324
|
+
var REACT_HOOKS_SPEC = {
|
|
1325
|
+
from: "package",
|
|
1326
|
+
package: "react",
|
|
1327
|
+
name: [
|
|
1328
|
+
"useState",
|
|
1329
|
+
"useEffect",
|
|
1330
|
+
"useLayoutEffect",
|
|
1331
|
+
"useCallback",
|
|
1332
|
+
"useMemo",
|
|
1333
|
+
"useRef",
|
|
1334
|
+
"useReducer",
|
|
1335
|
+
"useImperativeHandle",
|
|
1336
|
+
"useDebugValue",
|
|
1337
|
+
"useDeferredValue",
|
|
1338
|
+
"useTransition",
|
|
1339
|
+
"useId",
|
|
1340
|
+
"useSyncExternalStore",
|
|
1341
|
+
"useInsertionEffect",
|
|
1342
|
+
"useContext"
|
|
1343
|
+
]
|
|
1344
|
+
};
|
|
1345
|
+
var EFFECTOR_FACTORY_SPEC = {
|
|
1346
|
+
from: "package",
|
|
1347
|
+
package: "effector",
|
|
1348
|
+
name: [...EFFECTOR_FACTORIES]
|
|
1349
|
+
};
|
|
1350
|
+
var EFFECTOR_OPERATOR_SPEC = {
|
|
1351
|
+
from: "package",
|
|
1352
|
+
package: "effector",
|
|
1353
|
+
name: [...EFFECTOR_OPERATORS]
|
|
1354
|
+
};
|
|
1355
|
+
var EFFECTOR_FACTORIO_SHAPE = [
|
|
1356
|
+
"useModel",
|
|
1357
|
+
"createModel",
|
|
1358
|
+
"Provider",
|
|
1359
|
+
"@@unitShape"
|
|
1360
|
+
];
|
|
1361
|
+
var no_units_spawn_in_render_default = createRule({
|
|
1362
|
+
name: "no-units-spawn-in-render",
|
|
1363
|
+
meta: {
|
|
1364
|
+
type: "problem",
|
|
1365
|
+
docs: {
|
|
1366
|
+
description: "Forbid creating Effector units or calling operators inside React components or hooks."
|
|
1367
|
+
},
|
|
1368
|
+
messages: {
|
|
1369
|
+
noFactoryInRender: 'Creating Effector units with "{{ name }}" inside React component or hook is forbidden, since it may cause memory leaks and other bugs.',
|
|
1370
|
+
noOperatorInRender: 'Using Effector operator "{{ name }}" inside React component or hook is forbidden, since it may cause memory leaks and other bugs.',
|
|
1371
|
+
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.'
|
|
1372
|
+
},
|
|
1373
|
+
schema: [
|
|
1374
|
+
{
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties: {
|
|
1377
|
+
detectCustomFactories: {
|
|
1378
|
+
oneOf: [
|
|
1379
|
+
{ type: "boolean" },
|
|
1380
|
+
{
|
|
1381
|
+
type: "object",
|
|
1382
|
+
properties: {
|
|
1383
|
+
allowlist: {
|
|
1384
|
+
type: "array",
|
|
1385
|
+
items: { type: "string" },
|
|
1386
|
+
uniqueItems: true
|
|
1387
|
+
}
|
|
1388
|
+
},
|
|
1389
|
+
required: ["allowlist"],
|
|
1390
|
+
additionalProperties: false
|
|
1391
|
+
}
|
|
1392
|
+
]
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
additionalProperties: false
|
|
1396
|
+
}
|
|
1397
|
+
]
|
|
1398
|
+
},
|
|
1399
|
+
defaultOptions: [{ detectCustomFactories: true }],
|
|
1400
|
+
create: (context, [options]) => {
|
|
1401
|
+
const services = getParserServices(context);
|
|
1402
|
+
const checker = services.program.getTypeChecker();
|
|
1403
|
+
const { detectCustomFactories } = options;
|
|
1404
|
+
const allowlist = typeof detectCustomFactories === "object" ? new Set(detectCustomFactories.allowlist) : void 0;
|
|
1405
|
+
const stack = { render: [] };
|
|
1406
|
+
const effectorImports = /* @__PURE__ */ new Map();
|
|
1407
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
1408
|
+
return {
|
|
1409
|
+
// ── Phase 1: Collect effector imports ──────────────────────────────────
|
|
1410
|
+
[`${importSelector} > ImportSpecifier[imported.type="Identifier"]`]: (node) => {
|
|
1411
|
+
const imported = node.imported.name;
|
|
1412
|
+
const local = node.local.name;
|
|
1413
|
+
if (EFFECTOR_FACTORIES.has(imported)) {
|
|
1414
|
+
effectorImports.set(local, "factory");
|
|
1415
|
+
} else if (EFFECTOR_OPERATORS.has(imported)) {
|
|
1416
|
+
effectorImports.set(local, "operator");
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
// ── Phase 2: Track render scope via function enter/exit ────────────────
|
|
1420
|
+
//
|
|
1421
|
+
// Determines if a function is a render context using a series of
|
|
1422
|
+
// heuristics,
|
|
1423
|
+
// ordered from cheapest to most expensive:
|
|
1424
|
+
// 1. Inherit from parent — if already inside render, every nested scope
|
|
1425
|
+
// is too
|
|
1426
|
+
// 2. Name check — `useXxx` convention means a custom hook
|
|
1427
|
+
// 3. Return type — if the function returns JSX, it's a component
|
|
1428
|
+
// 4. Contextual type — if the function is used where a component type is
|
|
1429
|
+
// expected
|
|
1430
|
+
// (e.g. React.memo, forwardRef), it's a component even without explicit
|
|
1431
|
+
// annotation
|
|
1432
|
+
[`FunctionDeclaration, FunctionExpression, ArrowFunctionExpression`]: (node) => {
|
|
1433
|
+
const current = stack.render.at(-1) ?? false;
|
|
1434
|
+
if (current) return void stack.render.push(true);
|
|
1435
|
+
const name = nameOf.function(node);
|
|
1436
|
+
if (name && UseRegex2.test(name.name))
|
|
1437
|
+
return void stack.render.push(true);
|
|
1438
|
+
const tsnode = services.esTreeNodeToTSNodeMap.get(node);
|
|
1439
|
+
const signature = checker.getSignatureFromDeclaration(tsnode);
|
|
1440
|
+
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : void 0;
|
|
1441
|
+
const isJSX = matchesType2(returnType, (type) => isType.jsx(type));
|
|
1442
|
+
if (isJSX) return void stack.render.push(true);
|
|
1443
|
+
const inferred = isExpression(tsnode) ? checker.getContextualType(tsnode) : void 0;
|
|
1444
|
+
const isComponent = matchesType2(
|
|
1445
|
+
inferred,
|
|
1446
|
+
(type) => isType.component(type)
|
|
1447
|
+
);
|
|
1448
|
+
if (isComponent) return void stack.render.push(true);
|
|
1449
|
+
return void stack.render.push(false);
|
|
1450
|
+
},
|
|
1451
|
+
[`:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression):exit`]: () => void stack.render.pop(),
|
|
1452
|
+
// Class bodies are never render contexts themselves — class methods (like
|
|
1453
|
+
// render())
|
|
1454
|
+
// will get their own stack entry and be evaluated independently.
|
|
1455
|
+
ClassDeclaration: () => void stack.render.push(false),
|
|
1456
|
+
"ClassDeclaration:exit": () => void stack.render.pop(),
|
|
1457
|
+
// ── Phase 3: Flag violating calls inside render ────────────────────────
|
|
1458
|
+
//
|
|
1459
|
+
// Detection is done in two tiers, ordered so we can avoid expensive type
|
|
1460
|
+
// analysis when possible and catch operators whose return type is not a
|
|
1461
|
+
// unit:
|
|
1462
|
+
//
|
|
1463
|
+
// Tier 1 — Import-based (no type analysis):
|
|
1464
|
+
// If the callee was imported from effector (tracked in Phase 1), we know
|
|
1465
|
+
// its kind immediately. This is essential for operators like `forward`
|
|
1466
|
+
// and `guard` that return Subscription instead of a unit.
|
|
1467
|
+
//
|
|
1468
|
+
// Tier 2 — Type-based (requires type checker):
|
|
1469
|
+
// For everything else, check if the call's return type contains effector
|
|
1470
|
+
// units. If it does, classify the callee via typeMatchesSpecifier:
|
|
1471
|
+
// - Known React hooks are excluded to avoid double-reporting: e.g.
|
|
1472
|
+
// `useMemo(() => createStore(0), [])` returns Store, but the inner
|
|
1473
|
+
// `createStore` and the like is already flagged — reporting `useMemo` too
|
|
1474
|
+
// would be noise.
|
|
1475
|
+
// `useContext` is excluded because it legitimately retrieves pre-created
|
|
1476
|
+
// units.
|
|
1477
|
+
// - effector-factorio's `factory.useModel()` is excluded — it retrieves
|
|
1478
|
+
// pre-created units from React context, similar to useContext
|
|
1479
|
+
// - Namespaced effector calls (e.g. `effector.createStore`) are matched
|
|
1480
|
+
// by callee type against the effector package
|
|
1481
|
+
// - Anything remaining is treated as a custom factory
|
|
1482
|
+
CallExpression: (node) => {
|
|
1483
|
+
const isWithinRender = stack.render.at(-1) ?? false;
|
|
1484
|
+
if (!isWithinRender) return;
|
|
1485
|
+
const calleeName = getCalleeName(node.callee);
|
|
1486
|
+
const importType = calleeName ? effectorImports.get(calleeName) : void 0;
|
|
1487
|
+
switch (importType) {
|
|
1488
|
+
case "factory":
|
|
1489
|
+
return context.report({
|
|
1490
|
+
node,
|
|
1491
|
+
messageId: "noFactoryInRender",
|
|
1492
|
+
data: { name: calleeName }
|
|
1493
|
+
});
|
|
1494
|
+
case "operator":
|
|
1495
|
+
return context.report({
|
|
1496
|
+
node,
|
|
1497
|
+
messageId: "noOperatorInRender",
|
|
1498
|
+
data: { name: calleeName }
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
if (detectCustomFactories === false) return;
|
|
1502
|
+
const returnType = services.getTypeAtLocation(node);
|
|
1503
|
+
const ctx = {
|
|
1504
|
+
node: services.esTreeNodeToTSNodeMap.get(node),
|
|
1505
|
+
checker,
|
|
1506
|
+
program: services.program
|
|
1507
|
+
};
|
|
1508
|
+
if (!hasEffectorUnitInType(ctx, returnType)) return;
|
|
1509
|
+
const calleeType = services.getTypeAtLocation(node.callee);
|
|
1510
|
+
const displayName = calleeName ?? "<expression>";
|
|
1511
|
+
if (typeMatches(
|
|
1512
|
+
calleeType,
|
|
1513
|
+
REACT_HOOKS_SPEC.name,
|
|
1514
|
+
REACT_HOOKS_SPEC.package
|
|
1515
|
+
))
|
|
1516
|
+
return;
|
|
1517
|
+
if (isEffectorFactorioHook(node.callee, services.getTypeAtLocation))
|
|
1518
|
+
return;
|
|
1519
|
+
if (typeMatches(
|
|
1520
|
+
calleeType,
|
|
1521
|
+
EFFECTOR_FACTORY_SPEC.name,
|
|
1522
|
+
EFFECTOR_FACTORY_SPEC.package
|
|
1523
|
+
))
|
|
1524
|
+
return context.report({
|
|
1525
|
+
node,
|
|
1526
|
+
messageId: "noFactoryInRender",
|
|
1527
|
+
data: { name: displayName }
|
|
1528
|
+
});
|
|
1529
|
+
if (typeMatches(
|
|
1530
|
+
calleeType,
|
|
1531
|
+
EFFECTOR_OPERATOR_SPEC.name,
|
|
1532
|
+
EFFECTOR_OPERATOR_SPEC.package
|
|
1533
|
+
))
|
|
1534
|
+
return context.report({
|
|
1535
|
+
node,
|
|
1536
|
+
messageId: "noOperatorInRender",
|
|
1537
|
+
data: { name: displayName }
|
|
1538
|
+
});
|
|
1539
|
+
if (allowlist && calleeName && allowlist.has(calleeName)) return;
|
|
1540
|
+
context.report({
|
|
1541
|
+
node,
|
|
1542
|
+
messageId: "noCustomFactoryInRender",
|
|
1543
|
+
data: { name: displayName }
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
var UseRegex2 = /^use[A-Z0-9].*$/;
|
|
1550
|
+
var matchesType2 = (type, predicate) => {
|
|
1551
|
+
if (!type) return false;
|
|
1552
|
+
if (type.isUnion()) return type.types.some(predicate);
|
|
1553
|
+
return predicate(type);
|
|
1554
|
+
};
|
|
1555
|
+
function getCalleeName(callee) {
|
|
1556
|
+
if (callee.type === NodeType.Identifier) return callee.name;
|
|
1557
|
+
if (callee.type === NodeType.MemberExpression && callee.property.type === NodeType.Identifier)
|
|
1558
|
+
return callee.property.name;
|
|
1559
|
+
else return null;
|
|
1560
|
+
}
|
|
1561
|
+
function hasEffectorUnitInType(ctx, type, depth = 3) {
|
|
1562
|
+
if (isType.unit(type)) return true;
|
|
1563
|
+
if (depth <= 0) return false;
|
|
1564
|
+
if (type.isUnion())
|
|
1565
|
+
return type.types.some(
|
|
1566
|
+
(member) => hasEffectorUnitInType(ctx, member, depth)
|
|
1567
|
+
);
|
|
1568
|
+
for (const property2 of type.getProperties()) {
|
|
1569
|
+
const propertyType = ctx.checker.getTypeOfSymbolAtLocation(
|
|
1570
|
+
property2,
|
|
1571
|
+
ctx.node
|
|
1572
|
+
);
|
|
1573
|
+
if (hasEffectorUnitInType(ctx, propertyType, depth - 1)) return true;
|
|
1574
|
+
}
|
|
1575
|
+
return false;
|
|
1576
|
+
}
|
|
1577
|
+
function isEffectorFactorioHook(callee, getTypeAtLocation) {
|
|
1578
|
+
if (callee.type !== NodeType.MemberExpression) return false;
|
|
1579
|
+
if (callee.property.type !== NodeType.Identifier) return false;
|
|
1580
|
+
if (callee.property.name !== "useModel")
|
|
1581
|
+
return false;
|
|
1582
|
+
const objectType = getTypeAtLocation(callee.object);
|
|
1583
|
+
const propertyNames = new Set(
|
|
1584
|
+
objectType.getProperties().map((p) => p.getName())
|
|
1585
|
+
);
|
|
1586
|
+
return EFFECTOR_FACTORIO_SHAPE.every((name) => propertyNames.has(name));
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/rules/no-unnecessary-combination/no-unnecessary-combination.ts
|
|
1590
|
+
var no_unnecessary_combination_default = createRule({
|
|
1591
|
+
name: "no-unnecessary-combination",
|
|
1592
|
+
meta: {
|
|
1593
|
+
type: "suggestion",
|
|
1594
|
+
docs: {
|
|
1595
|
+
description: "Forbid unnecessary combinations in `clock` and `source`."
|
|
1596
|
+
},
|
|
1597
|
+
messages: {
|
|
1598
|
+
unnecessary: "{{ method }} is used under the hood of {{ property }} in {{ operator }}, you can omit it."
|
|
1599
|
+
},
|
|
1600
|
+
schema: []
|
|
1601
|
+
},
|
|
1602
|
+
defaultOptions: [],
|
|
1603
|
+
create: (context) => {
|
|
1604
|
+
const services = getParserServices(context);
|
|
1605
|
+
const operators = /* @__PURE__ */ new Set();
|
|
1606
|
+
const combinators = /* @__PURE__ */ new Map();
|
|
1607
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
1608
|
+
return {
|
|
1609
|
+
[`${importSelector} > ${selector11.operator}`]: (node) => operators.add(node.local.name),
|
|
1610
|
+
[`${importSelector} > ${selector11.combinator}`]: (node) => combinators.set(
|
|
1611
|
+
node.local.name,
|
|
1612
|
+
node.imported.name
|
|
1613
|
+
),
|
|
1614
|
+
[`CallExpression${selector11.call}:has(${selector11.argument})`]: (node) => {
|
|
1615
|
+
if (!operators.has(node.callee.name)) return;
|
|
1616
|
+
const [config] = node.arguments;
|
|
1617
|
+
const clock = locate.property("clock", config)?.value;
|
|
1618
|
+
const source = locate.property("source", config)?.value;
|
|
1619
|
+
if (clock?.type === NodeType.CallExpression && clock.callee.type === NodeType.Identifier) {
|
|
1620
|
+
const method = combinators.get(clock.callee.name);
|
|
1621
|
+
if (method === "merge") {
|
|
1622
|
+
const data = {
|
|
1623
|
+
method: clock.callee.name,
|
|
1624
|
+
property: "clock",
|
|
1625
|
+
operator: node.callee.name
|
|
1626
|
+
};
|
|
1627
|
+
context.report({ node: clock, messageId: "unnecessary", data });
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
if (source?.type === NodeType.CallExpression && source.callee.type === NodeType.Identifier) {
|
|
1631
|
+
const method = combinators.get(source.callee.name);
|
|
1632
|
+
if (!method) return;
|
|
1633
|
+
if (method === "combine" && source.arguments.length > 1 && isFunction(source.arguments.at(-1), services))
|
|
1634
|
+
return;
|
|
1635
|
+
const data = {
|
|
1636
|
+
method: source.callee.name,
|
|
1637
|
+
property: "source",
|
|
1638
|
+
operator: node.callee.name
|
|
1639
|
+
};
|
|
1640
|
+
context.report({ node: source, messageId: "unnecessary", data });
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
var selector11 = {
|
|
1647
|
+
operator: `ImportSpecifier[imported.name=/(sample|guard)/]`,
|
|
1648
|
+
combinator: `ImportSpecifier[imported.name=/(combine|merge)/]`,
|
|
1649
|
+
call: `[callee.type="Identifier"][arguments.length=1]`,
|
|
1650
|
+
argument: `ObjectExpression.arguments`
|
|
1651
|
+
};
|
|
1652
|
+
function isFunction(node, services) {
|
|
1653
|
+
if (node.type === NodeType.ArrowFunctionExpression) return true;
|
|
1654
|
+
else if (node.type === NodeType.FunctionExpression) return true;
|
|
1655
|
+
else if (node.type === NodeType.Identifier) {
|
|
1656
|
+
const checker = services.program.getTypeChecker();
|
|
1657
|
+
const tsnode = services.esTreeNodeToTSNodeMap.get(node);
|
|
1658
|
+
const type = checker.getTypeAtLocation(tsnode);
|
|
1659
|
+
const signatures = type.getCallSignatures();
|
|
1660
|
+
return signatures.length > 0;
|
|
1661
|
+
} else return false;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/rules/no-unnecessary-duplication/no-unnecessary-duplication.ts
|
|
1665
|
+
var no_unnecessary_duplication_default = createRule({
|
|
1666
|
+
name: "no-unnecessary-duplication",
|
|
1667
|
+
meta: {
|
|
1668
|
+
type: "problem",
|
|
1669
|
+
docs: {
|
|
1670
|
+
description: "Forbid duplicate `source` and `clock` in `sample` and `guard`."
|
|
1671
|
+
},
|
|
1672
|
+
messages: {
|
|
1673
|
+
duplicate: "Method `{{ method }}` has the same value for `source` and `clock`. Consider using only one of them.",
|
|
1674
|
+
removeClock: "Remove the `clock`",
|
|
1675
|
+
removeSource: "Remove the `source`"
|
|
1676
|
+
},
|
|
1677
|
+
schema: [],
|
|
1678
|
+
hasSuggestions: true
|
|
1679
|
+
},
|
|
1680
|
+
defaultOptions: [],
|
|
1681
|
+
create: (context) => {
|
|
1682
|
+
const imports = /* @__PURE__ */ new Set();
|
|
1683
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
1684
|
+
return {
|
|
1685
|
+
[`${importSelector} > ${selector12.method}`]: (node) => imports.add(node.local.name),
|
|
1686
|
+
[`CallExpression${selector12.call}:has(${selector12.argument})`]: (node) => {
|
|
1687
|
+
if (!imports.has(node.callee.name)) return;
|
|
1688
|
+
const [config] = node.arguments;
|
|
1689
|
+
const source = locate.property("source", config)?.value;
|
|
1690
|
+
if (!source) return;
|
|
1691
|
+
const clock = locate.property("clock", config)?.value;
|
|
1692
|
+
if (!clock) return;
|
|
1693
|
+
const equal = compare(clock, source);
|
|
1694
|
+
if (!equal) return;
|
|
1695
|
+
const suggestions = [
|
|
1696
|
+
{
|
|
1697
|
+
messageId: "removeClock",
|
|
1698
|
+
fix: function* (fixer) {
|
|
1699
|
+
yield fixer.remove(clock.parent);
|
|
1700
|
+
const after = context.sourceCode.getTokenAfter(clock.parent);
|
|
1701
|
+
if (after?.value === ",") yield fixer.remove(after);
|
|
1702
|
+
}
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
messageId: "removeSource",
|
|
1706
|
+
fix: function* (fixer) {
|
|
1707
|
+
yield fixer.remove(source.parent);
|
|
1708
|
+
const after = context.sourceCode.getTokenAfter(source.parent);
|
|
1709
|
+
if (after?.value === ",") yield fixer.remove(after);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
];
|
|
1713
|
+
const data = { method: node.callee.name };
|
|
1714
|
+
context.report({
|
|
1715
|
+
node: config,
|
|
1716
|
+
messageId: "duplicate",
|
|
1717
|
+
data,
|
|
1718
|
+
suggest: suggestions
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
var selector12 = {
|
|
1725
|
+
method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
|
|
1726
|
+
call: `[callee.type="Identifier"][arguments.length=1]`,
|
|
1727
|
+
argument: `ObjectExpression.arguments`
|
|
1728
|
+
};
|
|
1729
|
+
function compare(clock, source, limit = 5) {
|
|
1730
|
+
if (limit <= 0) return false;
|
|
1731
|
+
if (clock.type === NodeType.Identifier)
|
|
1732
|
+
return source.type === NodeType.Identifier && clock.name === source.name;
|
|
1733
|
+
if (clock.type === NodeType.ArrayExpression) {
|
|
1734
|
+
if (clock.elements.length !== 1) return false;
|
|
1735
|
+
let a, b;
|
|
1736
|
+
if (source.type === NodeType.ArrayExpression)
|
|
1737
|
+
if (source.elements.length !== 1)
|
|
1738
|
+
return false;
|
|
1739
|
+
else
|
|
1740
|
+
[a, b] = [
|
|
1741
|
+
clock.elements[0],
|
|
1742
|
+
source.elements[0]
|
|
1743
|
+
];
|
|
1744
|
+
else [a, b] = [clock.elements[0], source];
|
|
1745
|
+
return a.type === NodeType.Identifier && b.type === NodeType.Identifier && a.name === b.name;
|
|
1746
|
+
}
|
|
1747
|
+
if (clock.type === NodeType.MemberExpression) {
|
|
1748
|
+
if (source.type !== NodeType.MemberExpression) return false;
|
|
1749
|
+
if (clock.computed || source.computed) return false;
|
|
1750
|
+
if (clock.property.name !== source.property.name) return false;
|
|
1751
|
+
return compare(clock.object, source.object, limit - 1);
|
|
1752
|
+
}
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/rules/no-useless-methods/no-useless-methods.ts
|
|
1757
|
+
import esquery3 from "esquery";
|
|
1758
|
+
var no_useless_methods_default = createRule({
|
|
1759
|
+
name: "no-useless-methods",
|
|
1760
|
+
meta: {
|
|
1761
|
+
type: "problem",
|
|
1762
|
+
docs: {
|
|
1763
|
+
description: "Forbid useless calls of `sample` and `guard`."
|
|
1764
|
+
},
|
|
1765
|
+
messages: {
|
|
1766
|
+
uselessMethod: "Method `{{ method }}` does nothing in this case. You should assign the result to variable or pass `target` to it."
|
|
1767
|
+
},
|
|
1768
|
+
schema: []
|
|
1769
|
+
},
|
|
1770
|
+
defaultOptions: [],
|
|
1771
|
+
create: (context) => {
|
|
1772
|
+
const imports = /* @__PURE__ */ new Set();
|
|
1773
|
+
const source = context.sourceCode;
|
|
1774
|
+
const visitorKeys = source.visitorKeys;
|
|
1775
|
+
const importSelector = fromPackage(PACKAGE_NAME.core);
|
|
1776
|
+
const usageStack = [];
|
|
1777
|
+
return {
|
|
1778
|
+
ReturnStatement: () => usageStack.push(true),
|
|
1779
|
+
"ReturnStatement:exit": () => usageStack.pop(),
|
|
1780
|
+
VariableDeclarator: () => usageStack.push(true),
|
|
1781
|
+
"VariableDeclarator:exit": () => usageStack.pop(),
|
|
1782
|
+
ObjectExpression: () => usageStack.push(true),
|
|
1783
|
+
"ObjectExpression:exit": () => usageStack.pop(),
|
|
1784
|
+
BlockStatement: () => usageStack.push(false),
|
|
1785
|
+
"BlockStatement:exit": () => usageStack.pop(),
|
|
1786
|
+
[`${importSelector} > ${selector13.method}`]: (node) => imports.add(node.local.name),
|
|
1787
|
+
[`CallExpression[callee.type="Identifier"]`]: (node) => {
|
|
1788
|
+
const isTracked = imports.has(node.callee.name);
|
|
1789
|
+
if (!isTracked) return;
|
|
1790
|
+
const isUsed = usageStack.at(-1) ?? false;
|
|
1791
|
+
if (isUsed) return;
|
|
1792
|
+
if (node.parent.type === NodeType.CallExpression)
|
|
1793
|
+
return;
|
|
1794
|
+
const [config] = node.arguments;
|
|
1795
|
+
if (config?.type === NodeType.ObjectExpression) {
|
|
1796
|
+
const target = locate.property("target", config)?.value;
|
|
1797
|
+
if (target) return;
|
|
1798
|
+
}
|
|
1799
|
+
const grandparent = node.parent.parent;
|
|
1800
|
+
const ancestry = source.getAncestors(grandparent);
|
|
1801
|
+
const isWatched = esquery3.matches(
|
|
1802
|
+
grandparent,
|
|
1803
|
+
query3.watch,
|
|
1804
|
+
ancestry,
|
|
1805
|
+
{ visitorKeys }
|
|
1806
|
+
);
|
|
1807
|
+
if (isWatched) return;
|
|
1808
|
+
const method = node.callee.name;
|
|
1809
|
+
context.report({ node, messageId: "uselessMethod", data: { method } });
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
var selector13 = {
|
|
1815
|
+
method: `ImportSpecifier[imported.name=/(sample|guard)/]`
|
|
1816
|
+
};
|
|
1817
|
+
var query3 = {
|
|
1818
|
+
// https://github.com/estools/esquery/pull/146
|
|
1819
|
+
watch: esquery3.parse(
|
|
1820
|
+
"CallExpression:has(> MemberExpression.callee[property.name=watch]:has(> CallExpression.object))"
|
|
1821
|
+
)
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
// src/rules/no-watch/no-watch.ts
|
|
1825
|
+
var no_watch_default = createRule({
|
|
1826
|
+
name: "no-watch",
|
|
1827
|
+
meta: {
|
|
1828
|
+
type: "suggestion",
|
|
1829
|
+
docs: {
|
|
1830
|
+
description: "Restrict usage of `.watch` on any Effector Unit."
|
|
1831
|
+
},
|
|
1832
|
+
messages: {
|
|
1833
|
+
restricted: "Using `.watch` method leads to imperative code. Replace it with an operator `sample` or use the `target` parameter of `sample` operator."
|
|
1834
|
+
},
|
|
1835
|
+
schema: []
|
|
1836
|
+
},
|
|
1837
|
+
defaultOptions: [],
|
|
1838
|
+
create: (context) => {
|
|
1839
|
+
const services = getParserServices(context);
|
|
1840
|
+
return {
|
|
1841
|
+
[`CallExpression[callee.type="MemberExpression"][callee.property.name="watch"]`]: (node) => {
|
|
1842
|
+
const type = services.getTypeAtLocation(node.callee.object);
|
|
1843
|
+
const isUnit = isType.unit(type);
|
|
1844
|
+
if (!isUnit) return;
|
|
1845
|
+
context.report({ node, messageId: "restricted" });
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
// src/rules/prefer-useUnit/prefer-useUnit.ts
|
|
1852
|
+
var prefer_useUnit_default = createRule({
|
|
1853
|
+
name: "prefer-useUnit",
|
|
1854
|
+
meta: {
|
|
1855
|
+
type: "suggestion",
|
|
1856
|
+
docs: {
|
|
1857
|
+
description: "Prefer `useUnit` over deprecated `useStore` and `useEvent` hooks."
|
|
1858
|
+
},
|
|
1859
|
+
messages: {
|
|
1860
|
+
useUseUnit: "`{{ name }}` should be replaced with `useUnit`."
|
|
1861
|
+
},
|
|
1862
|
+
schema: []
|
|
1863
|
+
},
|
|
1864
|
+
defaultOptions: [],
|
|
1865
|
+
create: (context) => {
|
|
1866
|
+
const imports = /* @__PURE__ */ new Map();
|
|
1867
|
+
const importSelector = fromPackage(PACKAGE_NAME.react);
|
|
1868
|
+
return {
|
|
1869
|
+
[`${importSelector} > ${selector14.useStore}`]: (node) => void imports.set(node.local.name, "useStore"),
|
|
1870
|
+
[`${importSelector} > ${selector14.useEvent}`]: (node) => void imports.set(node.local.name, "useEvent"),
|
|
1871
|
+
[`CallExpression[callee.type="Identifier"]`]: (node) => {
|
|
1872
|
+
const hook = imports.get(node.callee.name);
|
|
1873
|
+
if (!hook) return;
|
|
1874
|
+
context.report({ node, messageId: "useUseUnit", data: { name: hook } });
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
var selector14 = {
|
|
1880
|
+
useStore: `ImportSpecifier[imported.name=useStore]`,
|
|
1881
|
+
useEvent: `ImportSpecifier[imported.name=useEvent]`
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
// src/rules/require-pickup-in-persist/require-pickup-in-persist.ts
|
|
1885
|
+
var require_pickup_in_persist_default = createRule({
|
|
1886
|
+
name: "require-pickup-in-persist",
|
|
1887
|
+
meta: {
|
|
1888
|
+
type: "problem",
|
|
1889
|
+
docs: {
|
|
1890
|
+
description: "Require every `persist` call of `effector-storage` to use `pickup`."
|
|
1891
|
+
},
|
|
1892
|
+
messages: {
|
|
1893
|
+
missing: "This `persist` call does not specify a `pickup` event that is required for scoped usage of `effector-storage`."
|
|
1894
|
+
},
|
|
1895
|
+
schema: []
|
|
1896
|
+
},
|
|
1897
|
+
defaultOptions: [],
|
|
1898
|
+
create: (context) => {
|
|
1899
|
+
const imports = /* @__PURE__ */ new Set();
|
|
1900
|
+
const importSelector = fromPackage(PACKAGE_NAME.storage);
|
|
1901
|
+
return {
|
|
1902
|
+
[`${importSelector} > ${selector15.persist}`]: (node) => imports.add(node.local.name),
|
|
1903
|
+
[`CallExpression${selector15.call}${selector15.config}`]: (node) => {
|
|
1904
|
+
if (!imports.has(node.callee.name)) return;
|
|
1905
|
+
const config = node.arguments[0];
|
|
1906
|
+
if (config.properties.filter((prop) => prop.type === NodeType.Property).map((prop) => prop.key).filter((key2) => key2.type === NodeType.Identifier).some((key2) => key2.name === "pickup"))
|
|
1907
|
+
return;
|
|
1908
|
+
context.report({ node, messageId: "missing" });
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
var selector15 = {
|
|
1914
|
+
persist: `ImportSpecifier[imported.name="persist"]`,
|
|
1915
|
+
call: `[callee.type="Identifier"]`,
|
|
1916
|
+
config: `[arguments.length=1][arguments.0.type="ObjectExpression"]`
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1919
|
+
// src/rules/strict-effect-handlers/strict-effect-handlers.ts
|
|
1920
|
+
var strict_effect_handlers_default = createRule({
|
|
1921
|
+
name: "strict-effect-handlers",
|
|
1922
|
+
meta: {
|
|
1923
|
+
type: "problem",
|
|
1924
|
+
docs: {
|
|
1925
|
+
description: "Forbid mixing calls to both regular async functions and Effects in the same function."
|
|
1926
|
+
},
|
|
1927
|
+
messages: {
|
|
1928
|
+
mixed: "This function can lead to losing Scope in Effector Fork API."
|
|
1929
|
+
},
|
|
1930
|
+
schema: []
|
|
1931
|
+
},
|
|
1932
|
+
defaultOptions: [],
|
|
1933
|
+
create: (context) => {
|
|
1934
|
+
const services = getParserServices(context);
|
|
1935
|
+
const stack = [];
|
|
1936
|
+
const track = (node) => {
|
|
1937
|
+
const current = stack.at(-1);
|
|
1938
|
+
if (!current) return;
|
|
1939
|
+
const callee = node.argument.callee;
|
|
1940
|
+
const type = services.getTypeAtLocation(callee);
|
|
1941
|
+
const isEffect = isType.effect(type);
|
|
1942
|
+
if (isEffect) return current.effect = true;
|
|
1943
|
+
else return current.regular = true;
|
|
1944
|
+
};
|
|
1945
|
+
const enter = () => {
|
|
1946
|
+
stack.push({ effect: false, regular: false });
|
|
1947
|
+
};
|
|
1948
|
+
const exit = (node) => {
|
|
1949
|
+
const scope2 = stack.pop();
|
|
1950
|
+
if (scope2.effect && scope2.regular)
|
|
1951
|
+
context.report({ node, messageId: "mixed" });
|
|
1952
|
+
};
|
|
1953
|
+
return {
|
|
1954
|
+
ArrowFunctionExpression: enter,
|
|
1955
|
+
"ArrowFunctionExpression:exit": exit,
|
|
1956
|
+
FunctionExpression: enter,
|
|
1957
|
+
"FunctionExpression:exit": exit,
|
|
1958
|
+
FunctionDeclaration: enter,
|
|
1959
|
+
"FunctionDeclaration:exit": exit,
|
|
1960
|
+
[`AwaitExpression:matches([argument.type='CallExpression'], [argument.type='NewExpression'])`]: track
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// src/ruleset.ts
|
|
1966
|
+
var recommended = {
|
|
1967
|
+
"effector/enforce-effect-naming-convention": "error",
|
|
1968
|
+
"effector/enforce-store-naming-convention": "error",
|
|
1969
|
+
"effector/keep-options-order": "warn",
|
|
1970
|
+
"effector/no-ambiguity-target": "warn",
|
|
1971
|
+
"effector/no-duplicate-on": "error",
|
|
1972
|
+
"effector/no-forward": "error",
|
|
1973
|
+
"effector/no-getState": "error",
|
|
1974
|
+
"effector/no-guard": "error",
|
|
1975
|
+
"effector/no-unnecessary-combination": "warn",
|
|
1976
|
+
"effector/no-unnecessary-duplication": "warn",
|
|
1977
|
+
"effector/no-useless-methods": "error",
|
|
1978
|
+
"effector/no-watch": "warn"
|
|
1979
|
+
};
|
|
1980
|
+
var patronum = {
|
|
1981
|
+
"effector/no-patronum-debug": "warn"
|
|
1982
|
+
};
|
|
1983
|
+
var scope = {
|
|
1984
|
+
"effector/require-pickup-in-persist": "error",
|
|
1985
|
+
"effector/strict-effect-handlers": "error"
|
|
1986
|
+
};
|
|
1987
|
+
var react = {
|
|
1988
|
+
"effector/enforce-gate-naming-convention": "error",
|
|
1989
|
+
"effector/enforce-exhaustive-useUnit-destructuring": "warn",
|
|
1990
|
+
"effector/mandatory-scope-binding": "error",
|
|
1991
|
+
"effector/no-units-spawn-in-render": "error",
|
|
1992
|
+
"effector/prefer-useUnit": "error"
|
|
1993
|
+
};
|
|
1994
|
+
var future = {
|
|
1995
|
+
"effector/no-domain-unit-creators": "warn"
|
|
1996
|
+
};
|
|
1997
|
+
var ruleset = { recommended, patronum, scope, react, future };
|
|
1998
|
+
|
|
1999
|
+
// src/index.ts
|
|
2000
|
+
var plugin = {
|
|
2001
|
+
meta: { name: "effector", version: "0.0.1" },
|
|
2002
|
+
rules: {
|
|
2003
|
+
"enforce-effect-naming-convention": enforce_effect_naming_convention_default,
|
|
2004
|
+
"enforce-exhaustive-useUnit-destructuring": enforce_exhaustive_useUnit_destructuring_default,
|
|
2005
|
+
"enforce-gate-naming-convention": enforce_gate_naming_convention_default,
|
|
2006
|
+
"enforce-store-naming-convention": enforce_store_naming_convention_default,
|
|
2007
|
+
"keep-options-order": keep_options_order_default,
|
|
2008
|
+
"mandatory-scope-binding": mandatory_scope_binding_default,
|
|
2009
|
+
"no-ambiguity-target": no_ambiguity_target_default,
|
|
2010
|
+
"no-domain-unit-creators": no_domain_unit_creators_default,
|
|
2011
|
+
"no-duplicate-clock-or-source-array-values": no_duplicate_clock_or_source_array_values_default,
|
|
2012
|
+
"no-duplicate-on": no_duplicate_on_default,
|
|
2013
|
+
"no-forward": no_forward_default,
|
|
2014
|
+
"no-getState": no_getState_default,
|
|
2015
|
+
"no-guard": no_guard_default,
|
|
2016
|
+
"no-patronum-debug": no_patronum_debug_default,
|
|
2017
|
+
"no-units-spawn-in-render": no_units_spawn_in_render_default,
|
|
2018
|
+
"no-unnecessary-combination": no_unnecessary_combination_default,
|
|
2019
|
+
"no-unnecessary-duplication": no_unnecessary_duplication_default,
|
|
2020
|
+
"no-useless-methods": no_useless_methods_default,
|
|
2021
|
+
"no-watch": no_watch_default,
|
|
2022
|
+
"prefer-useUnit": prefer_useUnit_default,
|
|
2023
|
+
"require-pickup-in-persist": require_pickup_in_persist_default,
|
|
2024
|
+
"strict-effect-handlers": strict_effect_handlers_default
|
|
2025
|
+
},
|
|
2026
|
+
configs: ruleset
|
|
2027
|
+
};
|
|
2028
|
+
var index_default = plugin;
|
|
2029
|
+
export {
|
|
2030
|
+
index_default as default
|
|
2031
|
+
};
|