@thethracian/oxlint-config 0.2.0 → 0.2.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/CHANGELOG.md +6 -0
- package/README.md +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/rules/acronym-case.d.ts.map +1 -1
- package/dist/rules/acronym-case.js +0 -2
- package/dist/rules/acronym-case.js.map +1 -1
- package/dist/rules/effect-default-helpers.d.ts.map +1 -1
- package/dist/rules/effect-default-helpers.js +105 -28
- package/dist/rules/effect-default-helpers.js.map +1 -1
- package/dist/rules/effect-default.d.ts.map +1 -1
- package/dist/rules/effect-default.js +269 -133
- package/dist/rules/effect-default.js.map +1 -1
- package/dist/rules/effect-rule-core.d.ts +8 -0
- package/dist/rules/effect-rule-core.d.ts.map +1 -1
- package/dist/rules/effect-rule-core.js +208 -61
- package/dist/rules/effect-rule-core.js.map +1 -1
- package/dist/rules/effect-source-helpers.d.ts.map +1 -1
- package/dist/rules/effect-source-helpers.js +37 -12
- package/dist/rules/effect-source-helpers.js.map +1 -1
- package/dist/rules/effect-source-scan.d.ts.map +1 -1
- package/dist/rules/effect-source-scan.js +18 -10
- package/dist/rules/effect-source-scan.js.map +1 -1
- package/dist/rules/effect-strict-helpers.d.ts.map +1 -1
- package/dist/rules/effect-strict-helpers.js +180 -11
- package/dist/rules/effect-strict-helpers.js.map +1 -1
- package/dist/rules/effect-strict.d.ts.map +1 -1
- package/dist/rules/effect-strict.js +87 -23
- package/dist/rules/effect-strict.js.map +1 -1
- package/dist/rules/max-line-length.d.ts.map +1 -1
- package/dist/rules/max-line-length.js +25 -7
- package/dist/rules/max-line-length.js.map +1 -1
- package/dist/rules/plugin.d.ts +2 -121
- package/dist/rules/plugin.d.ts.map +1 -1
- package/dist/rules/plugin.js +78 -94
- package/dist/rules/plugin.js.map +1 -1
- package/dist/rules/require-function-doc.d.ts.map +1 -1
- package/dist/rules/require-function-doc.js +32 -21
- package/dist/rules/require-function-doc.js.map +1 -1
- package/dist/rules/source-cache.d.ts +11 -0
- package/dist/rules/source-cache.d.ts.map +1 -0
- package/dist/rules/source-cache.js +50 -0
- package/dist/rules/source-cache.js.map +1 -0
- package/package.json +6 -9
|
@@ -4,6 +4,24 @@ import { hasForkBeforeTestClockAdjust, hasRealSleepWithoutTestClock, hasTestCloc
|
|
|
4
4
|
import { isConfiguredPath, isEffectTestPath, strictPathOptionsSchema, } from './effect-path-options.js';
|
|
5
5
|
import { effectImportAliases, effectFunctionAliases, hasEffectSignal, hasRuntimeCall, makeRules, } from './effect-rule-core.js';
|
|
6
6
|
import { exportedCallableDeclarationSegments, exportedDeclarationSegments, findBalancedCallEnd, stripCommentsAndStrings, } from './effect-source-helpers.js';
|
|
7
|
+
const effectDefaultRuleTokens = [
|
|
8
|
+
'Effect',
|
|
9
|
+
'Schema',
|
|
10
|
+
'Config',
|
|
11
|
+
'Context',
|
|
12
|
+
'Queue',
|
|
13
|
+
'Stream',
|
|
14
|
+
'TestClock',
|
|
15
|
+
'from "effect"',
|
|
16
|
+
"from 'effect'",
|
|
17
|
+
'@effect/',
|
|
18
|
+
'"effect/',
|
|
19
|
+
"'effect/",
|
|
20
|
+
'it.effect',
|
|
21
|
+
'describe.effect',
|
|
22
|
+
'JSON.parse',
|
|
23
|
+
'response.json',
|
|
24
|
+
];
|
|
7
25
|
function reportAst(context, message, node) {
|
|
8
26
|
context.report({ message, node });
|
|
9
27
|
}
|
|
@@ -36,17 +54,18 @@ function memberParts(node) {
|
|
|
36
54
|
propertyName: identifierName(member.property),
|
|
37
55
|
};
|
|
38
56
|
}
|
|
39
|
-
function
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
function effectCallPredicate(source, names) {
|
|
58
|
+
const memberNames = new Set(names);
|
|
59
|
+
const importAliases = new Set(effectImportAliases(source));
|
|
60
|
+
const functionAliases = new Set(names.flatMap((name) => effectFunctionAliases(source, 'Effect', name)));
|
|
61
|
+
return (callee) => {
|
|
62
|
+
const { objectName, propertyName } = memberParts(callee);
|
|
63
|
+
if (objectName && propertyName) {
|
|
64
|
+
return importAliases.has(objectName) && memberNames.has(propertyName);
|
|
65
|
+
}
|
|
66
|
+
const calleeName = identifierName(callee);
|
|
67
|
+
return Boolean(calleeName && functionAliases.has(calleeName));
|
|
68
|
+
};
|
|
50
69
|
}
|
|
51
70
|
function propertyKeyName(node) {
|
|
52
71
|
const value = literalValue(node);
|
|
@@ -179,128 +198,152 @@ const effectDefaultSpecs = [
|
|
|
179
198
|
{
|
|
180
199
|
name: 'effect-require-yield-star',
|
|
181
200
|
message: 'Use yield* inside Effect.gen so generator composition unwraps the Effect value.',
|
|
201
|
+
tokenGroups: [['gen'], ['yield']],
|
|
182
202
|
check: hasYieldWithoutStarInGen,
|
|
183
203
|
},
|
|
184
204
|
{
|
|
185
205
|
name: 'effect-require-return-yield-star',
|
|
186
206
|
message: 'Do not return an Effect from Effect.gen; return a value or return yield* the Effect.',
|
|
207
|
+
tokenGroups: [['gen'], ['return']],
|
|
187
208
|
check: hasReturnEffectInGen,
|
|
188
209
|
},
|
|
189
210
|
{
|
|
190
211
|
name: 'effect-prefer-gen-for-nested-flatmap',
|
|
191
212
|
message: 'Replace nested Effect.flatMap callbacks with Effect.gen for readable sequencing.',
|
|
213
|
+
tokens: ['flatMap'],
|
|
192
214
|
check: hasNestedFlatMap,
|
|
193
215
|
},
|
|
194
216
|
{
|
|
195
217
|
name: 'effect-no-function-returning-gen',
|
|
196
218
|
message: 'Use Effect.fn for exported effectful functions instead of returning Effect.gen.',
|
|
219
|
+
tokens: ['gen'],
|
|
197
220
|
check: (source) => exportedCallableDeclarationSegments(source).some((segment) => /(?:^|\breturn\s+)Effect\.gen\s*\(/.test(segment.trim())),
|
|
198
221
|
},
|
|
199
222
|
{
|
|
200
223
|
name: 'effect-prefer-effect-fn-for-exported-effects',
|
|
201
224
|
message: 'Exported effectful functions should use Effect.fn for tracing and stable contracts.',
|
|
225
|
+
tokens: ['export'],
|
|
202
226
|
check: (source) => exportedCallableDeclarationSegments(source).some((segment) => /(?:^|\breturn\s+)Effect\.(?!fn\b|gen\b|isEffect\b|serviceFunction\b|runPromise\b)/.test(segment.trim())),
|
|
203
227
|
},
|
|
204
228
|
{
|
|
205
229
|
name: 'effect-no-unnecessary-gen',
|
|
206
230
|
message: 'Do not wrap a single Effect in Effect.gen when direct composition is clearer.',
|
|
231
|
+
tokenGroups: [['gen'], ['yield']],
|
|
207
232
|
patterns: [/Effect\.gen\s*\(\s*function\*\s*\([^)]*\)\s*{\s*return\s+yield\*\s+Effect\./],
|
|
208
233
|
},
|
|
209
234
|
{
|
|
210
235
|
name: 'effect-no-effect-in-array-foreach',
|
|
211
236
|
message: 'Use Effect.forEach instead of Array.forEach with Effect-returning callbacks.',
|
|
237
|
+
tokens: ['forEach'],
|
|
212
238
|
check: hasEffectInArrayForEach,
|
|
213
239
|
},
|
|
214
240
|
{
|
|
215
241
|
name: 'effect-no-effect-in-promise-callback',
|
|
216
242
|
message: 'Do not create Effect values inside Promise callbacks; compose at the Effect boundary.',
|
|
243
|
+
tokens: ['.then', '.catch'],
|
|
217
244
|
check: hasEffectInPromiseCallback,
|
|
218
245
|
},
|
|
219
246
|
{
|
|
220
247
|
name: 'effect-no-floating-fiber',
|
|
221
248
|
message: 'Forked fibers must be joined, interrupted, scoped, supervised, or returned.',
|
|
249
|
+
tokens: ['fork'],
|
|
222
250
|
check: hasUnobservedFork,
|
|
223
251
|
},
|
|
224
252
|
{
|
|
225
253
|
name: 'effect-require-suspend-for-recursion',
|
|
226
254
|
message: 'Recursive Effect construction must be wrapped in Effect.suspend.',
|
|
255
|
+
tokens: ['function', '=>'],
|
|
227
256
|
check: hasRecursiveEffectWithoutSuspend,
|
|
228
257
|
},
|
|
229
258
|
{
|
|
230
259
|
name: 'effect-require-suspend-for-lazy-evaluation',
|
|
231
260
|
message: 'Use Effect.suspend when Effect construction must defer eager JavaScript work.',
|
|
261
|
+
tokens: ['Date.now', 'Math.random', 'new Date', 'JSON.parse'],
|
|
232
262
|
check: hasUnsafeLazyEvaluation,
|
|
233
263
|
},
|
|
234
264
|
{
|
|
235
265
|
name: 'effect-no-async-await-in-effect',
|
|
236
266
|
message: 'Use Effect.tryPromise or Effect.promise boundaries instead of async/await in Effect code.',
|
|
267
|
+
tokens: ['async', 'await'],
|
|
237
268
|
check: hasAsyncAwaitInEffect,
|
|
238
269
|
},
|
|
239
270
|
{
|
|
240
271
|
name: 'effect-no-promise-then-in-effect',
|
|
241
272
|
message: 'Use Effect combinators instead of Promise then/catch chains in Effect modules.',
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
273
|
+
tokens: ['.then', '.catch'],
|
|
274
|
+
ast: (context, source) => {
|
|
275
|
+
const isEffectModule = hasEffectSignal(source);
|
|
276
|
+
return {
|
|
277
|
+
CallExpression(node) {
|
|
278
|
+
if (!isEffectModule) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const { callee } = node;
|
|
282
|
+
const { propertyName } = memberParts(callee);
|
|
283
|
+
if (propertyName === 'then' || propertyName === 'catch') {
|
|
284
|
+
reportAst(context, 'Use Effect combinators instead of Promise then/catch chains in Effect modules.', node);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
},
|
|
254
289
|
},
|
|
255
290
|
{
|
|
256
291
|
name: 'effect-no-throw',
|
|
257
292
|
message: 'Use typed Effect failures instead of throw inside Effect workflows.',
|
|
293
|
+
tokens: ['throw'],
|
|
258
294
|
check: hasThrowInEffect,
|
|
259
295
|
},
|
|
260
296
|
{
|
|
261
297
|
name: 'effect-no-string-errors',
|
|
262
298
|
message: 'Use structured tagged errors instead of string failures.',
|
|
299
|
+
tokens: ['fail'],
|
|
263
300
|
check: hasStringErrorFailure,
|
|
264
|
-
ast: (context, source) =>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
isStringLikeLiteral(call.arguments?.[0])) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
301
|
+
ast: (context, source) => {
|
|
302
|
+
const isEffectFail = effectCallPredicate(source, ['fail']);
|
|
303
|
+
return {
|
|
304
|
+
CallExpression(node) {
|
|
305
|
+
const call = node;
|
|
306
|
+
if (isEffectFail(call.callee) && isStringLikeLiteral(call.arguments?.[0])) {
|
|
307
|
+
reportAst(context, 'Use structured tagged errors instead of string failures.', node);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
},
|
|
274
312
|
},
|
|
275
313
|
{
|
|
276
314
|
name: 'effect-no-untagged-errors',
|
|
277
315
|
message: 'Use tagged/data/schema errors in Effect.fail instead of global Error values.',
|
|
316
|
+
tokens: ['fail'],
|
|
278
317
|
check: hasUntaggedErrorFailure,
|
|
279
|
-
ast: (context, source) =>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
318
|
+
ast: (context, source) => {
|
|
319
|
+
const isEffectFail = effectCallPredicate(source, ['fail']);
|
|
320
|
+
return {
|
|
321
|
+
CallExpression(node) {
|
|
322
|
+
const call = node;
|
|
323
|
+
const firstArg = call.arguments?.[0];
|
|
324
|
+
if (!isEffectFail(call.callee)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!firstArg) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (nodeType(firstArg) === 'NewExpression' &&
|
|
331
|
+
identifierName(firstArg.callee) === 'Error') {
|
|
332
|
+
reportAst(context, 'Use tagged/data/schema errors in Effect.fail instead of global Error values.', node);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (nodeType(firstArg) === 'ObjectExpression' &&
|
|
336
|
+
!firstArg.properties?.some((property) => propertyKeyName(property.key) === '_tag')) {
|
|
337
|
+
reportAst(context, 'Use tagged/data/schema errors in Effect.fail instead of global Error values.', node);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
},
|
|
300
342
|
},
|
|
301
343
|
{
|
|
302
344
|
name: 'effect-no-silent-error-swallowing',
|
|
303
345
|
message: 'Do not erase Effect failures without recovery, logging, or explicit typed replacement.',
|
|
346
|
+
tokens: ['catchAll', 'ignore'],
|
|
304
347
|
patterns: [
|
|
305
348
|
/Effect\.(?:catchAll|ignore)\s*\([\s\S]*?(?:Effect\.void|Effect\.succeed\s*\(\s*undefined|undefined)/,
|
|
306
349
|
],
|
|
@@ -308,129 +351,162 @@ const effectDefaultSpecs = [
|
|
|
308
351
|
{
|
|
309
352
|
name: 'effect-require-typed-error-in-trypromise',
|
|
310
353
|
message: 'Use Effect.tryPromise({ try, catch }) so Promise failures become typed Effect errors.',
|
|
354
|
+
tokens: ['tryPromise'],
|
|
311
355
|
check: hasTryPromiseWithoutTypedCatch,
|
|
312
356
|
},
|
|
313
357
|
{
|
|
314
358
|
name: 'effect-prefer-catchTag-over-catchAll',
|
|
315
359
|
message: 'Prefer catchTag or catchTags over broad catchAll recovery.',
|
|
360
|
+
tokens: ['catchAll'],
|
|
316
361
|
check: hasBroadCatchAllWithoutRethrow,
|
|
317
362
|
},
|
|
318
363
|
{
|
|
319
364
|
name: 'effect-no-catchAll-with-mapError',
|
|
320
365
|
message: 'Use mapError directly instead of catchAll when only transforming the error.',
|
|
366
|
+
tokens: ['catchAll'],
|
|
321
367
|
patterns: [/Effect\.catchAll\s*\([\s\S]*?=>\s*Effect\.fail\s*\(/],
|
|
322
368
|
},
|
|
323
369
|
{
|
|
324
370
|
name: 'effect-prefer-mapError-over-catchAll-rethrow',
|
|
325
371
|
message: 'Use Effect.mapError instead of catchAll followed by fail.',
|
|
372
|
+
tokens: ['catchAll'],
|
|
326
373
|
patterns: [/(?:^|[^\w$.])catchAll\s*\([\s\S]*?=>\s*Effect\.fail\s*\(/],
|
|
327
374
|
},
|
|
328
375
|
{
|
|
329
376
|
name: 'effect-require-error-cause-preserved',
|
|
330
377
|
message: 'Preserve the original cause when mapping or wrapping Effect errors.',
|
|
378
|
+
tokens: ['mapError', 'catchAll'],
|
|
331
379
|
check: hasErrorMappingWithoutCause,
|
|
332
380
|
},
|
|
333
381
|
{
|
|
334
382
|
name: 'effect-prefer-ignore-logged',
|
|
335
383
|
message: 'Ignored Effect failures must be logged or otherwise observable.',
|
|
384
|
+
tokens: ['ignore'],
|
|
336
385
|
check: hasUnloggedIgnore,
|
|
337
386
|
},
|
|
338
387
|
{
|
|
339
388
|
name: 'effect-prefer-catchTags-for-multiple-tags',
|
|
340
389
|
message: 'Use Effect.catchTags for multiple tagged Effect recoveries.',
|
|
390
|
+
tokens: ['catchTag'],
|
|
341
391
|
check: hasMultipleCatchTagsInOnePipe,
|
|
342
392
|
},
|
|
343
393
|
{
|
|
344
394
|
name: 'effect-no-error-channel-widening-to-unknown',
|
|
345
395
|
message: 'Do not widen Effect error channels to unknown.',
|
|
396
|
+
tokens: ['unknown'],
|
|
346
397
|
patterns: [/Effect\s*<[^>]*,\s*unknown\b/, /Effect\.fail\s*<\s*unknown\b/],
|
|
347
398
|
},
|
|
348
399
|
{
|
|
349
400
|
name: 'effect-no-run-inside-effect',
|
|
350
401
|
message: 'Do not run an Effect from inside another Effect; yield or compose it instead.',
|
|
402
|
+
tokens: ['run'],
|
|
351
403
|
check: hasRuntimeInEffect,
|
|
352
404
|
},
|
|
353
405
|
{
|
|
354
406
|
name: 'effect-no-runpromise-in-exported-api',
|
|
355
407
|
message: 'Exported APIs should expose Effect values instead of hiding execution behind Promise.',
|
|
408
|
+
tokens: ['runPromise'],
|
|
356
409
|
check: (source) => exportedDeclarationSegments(source).some((segment) => /Effect\.runPromise\s*\(/.test(segment)),
|
|
357
410
|
},
|
|
358
411
|
{
|
|
359
412
|
name: 'effect-no-runfork-without-observer',
|
|
360
413
|
message: 'Do not call runFork without explicit observation, supervision, or interruption.',
|
|
414
|
+
tokens: ['runFork'],
|
|
361
415
|
check: hasRunForkWithoutObserver,
|
|
362
416
|
},
|
|
363
417
|
{
|
|
364
418
|
name: 'effect-no-sync-for-promise',
|
|
365
419
|
message: 'Use Effect.tryPromise for Promise-returning code instead of Effect.sync.',
|
|
420
|
+
tokenGroups: [['sync'], ['async', 'fetch', 'Promise.']],
|
|
366
421
|
check: hasSyncForPromise,
|
|
367
422
|
},
|
|
368
423
|
{
|
|
369
424
|
name: 'effect-no-sync-for-throwing-ops',
|
|
370
425
|
message: 'Use Effect.try for synchronous code that can throw instead of Effect.sync.',
|
|
426
|
+
tokenGroups: [['sync'], ['throw', 'JSON.parse']],
|
|
371
427
|
check: hasSyncForThrowingOps,
|
|
372
428
|
},
|
|
373
429
|
{
|
|
374
430
|
name: 'effect-no-console-log-in-effect-code',
|
|
375
431
|
message: 'Use Effect logging APIs instead of console logging in Effect code.',
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
propertyName
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
432
|
+
tokens: ['console'],
|
|
433
|
+
ast: (context, source) => {
|
|
434
|
+
const isEffectModule = hasEffectSignal(source);
|
|
435
|
+
return {
|
|
436
|
+
CallExpression(node) {
|
|
437
|
+
const { objectName, propertyName } = memberParts(node.callee);
|
|
438
|
+
if (isEffectModule &&
|
|
439
|
+
objectName === 'console' &&
|
|
440
|
+
propertyName &&
|
|
441
|
+
['debug', 'error', 'info', 'log', 'warn'].includes(propertyName)) {
|
|
442
|
+
reportAst(context, 'Use Effect logging APIs instead of console logging in Effect code.', node);
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
},
|
|
387
447
|
},
|
|
388
448
|
{
|
|
389
449
|
name: 'effect-no-process-env-in-effect-code',
|
|
390
450
|
message: 'Use Effect Config instead of process.env in Effect code.',
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
451
|
+
tokens: ['process'],
|
|
452
|
+
ast: (context, source) => {
|
|
453
|
+
const isEffectModule = hasEffectSignal(source);
|
|
454
|
+
const isConfigLayer = isConfiguredPath(context, 'configLayers');
|
|
455
|
+
return {
|
|
456
|
+
MemberExpression(node) {
|
|
457
|
+
const { objectName, propertyName } = memberParts(node);
|
|
458
|
+
const processEnv = objectName === 'process' && propertyName === 'env';
|
|
459
|
+
if (isEffectModule && !isConfigLayer && processEnv) {
|
|
460
|
+
reportAst(context, 'Use Effect Config instead of process.env in Effect code.', node);
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
},
|
|
400
465
|
},
|
|
401
466
|
{
|
|
402
467
|
name: 'effect-no-date-now-in-effect-code',
|
|
403
468
|
message: 'Use Effect Clock instead of Date.now in Effect code.',
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
propertyName
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
469
|
+
tokens: ['Date'],
|
|
470
|
+
ast: (context, source) => {
|
|
471
|
+
const isEffectModule = hasEffectSignal(source);
|
|
472
|
+
const isAdapterLayer = isConfiguredPath(context, 'adapterLayers');
|
|
473
|
+
return {
|
|
474
|
+
CallExpression(node) {
|
|
475
|
+
const { objectName, propertyName } = memberParts(node.callee);
|
|
476
|
+
if (isEffectModule &&
|
|
477
|
+
!isAdapterLayer &&
|
|
478
|
+
objectName === 'Date' &&
|
|
479
|
+
propertyName === 'now') {
|
|
480
|
+
reportAst(context, 'Use Effect Clock instead of Date.now in Effect code.', node);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
},
|
|
415
485
|
},
|
|
416
486
|
{
|
|
417
487
|
name: 'effect-no-math-random-in-effect-code',
|
|
418
488
|
message: 'Use Effect Random instead of Math.random in Effect code.',
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
propertyName
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
489
|
+
tokens: ['Math'],
|
|
490
|
+
ast: (context, source) => {
|
|
491
|
+
const isEffectModule = hasEffectSignal(source);
|
|
492
|
+
const isAdapterLayer = isConfiguredPath(context, 'adapterLayers');
|
|
493
|
+
return {
|
|
494
|
+
CallExpression(node) {
|
|
495
|
+
const { objectName, propertyName } = memberParts(node.callee);
|
|
496
|
+
if (isEffectModule &&
|
|
497
|
+
!isAdapterLayer &&
|
|
498
|
+
objectName === 'Math' &&
|
|
499
|
+
propertyName === 'random') {
|
|
500
|
+
reportAst(context, 'Use Effect Random instead of Math.random in Effect code.', node);
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
},
|
|
430
505
|
},
|
|
431
506
|
{
|
|
432
507
|
name: 'effect-no-json-parse-cast',
|
|
433
508
|
message: 'Decode external JSON with Schema instead of casting parsed unknown data.',
|
|
509
|
+
tokenGroups: [[' as '], ['JSON.parse', '.json']],
|
|
434
510
|
patterns: [
|
|
435
511
|
/\bJSON\.parse\s*\([^)]*\)\s+as\s+[A-Za-z_$][\w$]*/,
|
|
436
512
|
/\.json\s*\(\s*\)\s+as\s+[A-Za-z_$][\w$]*/,
|
|
@@ -439,71 +515,85 @@ const effectDefaultSpecs = [
|
|
|
439
515
|
{
|
|
440
516
|
name: 'effect-schema-prefer-decodeUnknown-effect',
|
|
441
517
|
message: 'Use Schema.decodeUnknown to decode unknown input into the Effect error channel.',
|
|
518
|
+
tokens: ['decode'],
|
|
442
519
|
check: hasSchemaPromiseDecode,
|
|
443
520
|
},
|
|
444
521
|
{
|
|
445
522
|
name: 'effect-schema-require-parse-error-handling',
|
|
446
523
|
message: 'Schema parsing must expose parse errors through typed Effect handling.',
|
|
524
|
+
tokens: ['decode'],
|
|
447
525
|
check: hasUnhandledSchemaEffectDecode,
|
|
448
526
|
},
|
|
449
527
|
{
|
|
450
528
|
name: 'effect-schema-use-decodeUnknown-for-external-data',
|
|
451
529
|
message: 'External data must enter through Schema.decodeUnknown.',
|
|
530
|
+
tokens: ['.json'],
|
|
452
531
|
check: hasExternalJsonWithoutDecodeUnknown,
|
|
453
532
|
},
|
|
454
533
|
{
|
|
455
534
|
name: 'effect-schema-no-unsafe-sync-decode-in-effect-code',
|
|
456
535
|
message: 'Do not use throwing synchronous Schema decoders in Effect modules.',
|
|
536
|
+
tokens: ['decodeSync', 'decodeUnknownSync'],
|
|
457
537
|
check: hasSchemaSyncDecodeInEffectWorkflow,
|
|
458
538
|
},
|
|
459
539
|
{
|
|
460
540
|
name: 'effect-schema-require-parseJson-for-json-strings',
|
|
461
541
|
message: 'Use Schema.parseJson when decoding JSON strings with Schema.',
|
|
542
|
+
tokens: ['JSON.parse'],
|
|
462
543
|
check: hasJsonParsedBeforeSchemaStringDecode,
|
|
463
544
|
},
|
|
464
545
|
{
|
|
465
546
|
name: 'effect-schema-correct-number-type-for-parsed-json',
|
|
466
547
|
message: 'Use the correct Schema number type for already-parsed JSON numbers.',
|
|
548
|
+
tokens: ['JSON.parse'],
|
|
467
549
|
check: hasParsedJsonNumberFromString,
|
|
468
550
|
},
|
|
469
551
|
{
|
|
470
552
|
name: 'effect-schema-prefer-taggedClass-over-manual-tag',
|
|
471
553
|
message: 'Prefer Schema.TaggedClass over hand-written _tag fields.',
|
|
554
|
+
tokens: ['_tag'],
|
|
472
555
|
patterns: [/Schema\.Struct\s*\(\s*{[\s\S]*?_tag\s*:\s*Schema\.Literal/],
|
|
473
556
|
},
|
|
474
557
|
{
|
|
475
558
|
name: 'effect-schema-avoid-old-type-names',
|
|
476
559
|
message: 'Use current Effect Schema API names instead of obsolete lowercase helpers.',
|
|
560
|
+
tokens: ['Schema.'],
|
|
477
561
|
patterns: [/\bSchema\.(?:string|number|boolean|array|object)\s*\(/],
|
|
478
562
|
},
|
|
479
563
|
{
|
|
480
564
|
name: 'effect-schema-no-cast-after-decode',
|
|
481
565
|
message: 'Do not cast after Schema decoding; let the schema provide the type.',
|
|
566
|
+
tokenGroups: [['Schema.decode'], [' as ']],
|
|
482
567
|
check: hasCastAfterSchemaDecode,
|
|
483
568
|
},
|
|
484
569
|
{
|
|
485
570
|
name: 'effect-require-acquire-release',
|
|
486
571
|
message: 'Resource acquisition must use acquireRelease, scoped, or equivalent finalization.',
|
|
572
|
+
tokens: ['open', 'connect', 'subscribe', 'listen'],
|
|
487
573
|
check: hasUnreleasedAcquire,
|
|
488
574
|
},
|
|
489
575
|
{
|
|
490
576
|
name: 'effect-require-scoped-for-acquireRelease',
|
|
491
577
|
message: 'Use Effect.scoped around acquireRelease when exposing acquired resources.',
|
|
578
|
+
tokens: ['acquireRelease'],
|
|
492
579
|
check: hasUnscopedAcquireRelease,
|
|
493
580
|
},
|
|
494
581
|
{
|
|
495
582
|
name: 'effect-require-scoped-for-resources',
|
|
496
583
|
message: 'Resourceful workflows must be scoped.',
|
|
584
|
+
tokens: ['Socket.', 'Connection.'],
|
|
497
585
|
check: hasUnscopedResourceWorkflow,
|
|
498
586
|
},
|
|
499
587
|
{
|
|
500
588
|
name: 'effect-no-fork-daemon-without-cleanup',
|
|
501
589
|
message: 'Daemon fibers must have cleanup, interruption, or supervision.',
|
|
590
|
+
tokens: ['forkDaemon'],
|
|
502
591
|
check: hasForkDaemonWithoutCleanup,
|
|
503
592
|
},
|
|
504
593
|
{
|
|
505
594
|
name: 'effect-prefer-fork-scoped-for-listeners',
|
|
506
595
|
message: 'Long-running listeners should use forkScoped so they follow Scope lifetime.',
|
|
596
|
+
tokens: ['fork'],
|
|
507
597
|
patterns: [
|
|
508
598
|
/Effect\.fork\s*\([\s\S]*?\b(?:listen[A-Z]\w*|subscribe[A-Z]\w*|watch[A-Z]\w*|listen|subscribe|watch)\b/,
|
|
509
599
|
],
|
|
@@ -511,26 +601,31 @@ const effectDefaultSpecs = [
|
|
|
511
601
|
{
|
|
512
602
|
name: 'effect-require-restore-for-fork-in-uninterruptible',
|
|
513
603
|
message: 'Use restore when forking inside uninterruptible regions.',
|
|
604
|
+
tokenGroups: [['uninterruptible'], ['fork']],
|
|
514
605
|
check: hasForkInUninterruptibleWithoutRestore,
|
|
515
606
|
},
|
|
516
607
|
{
|
|
517
608
|
name: 'effect-require-bounded-concurrency',
|
|
518
609
|
message: 'Concurrent Effect traversal must declare an explicit concurrency bound.',
|
|
610
|
+
tokens: ['concurrency'],
|
|
519
611
|
check: hasUnboundedEffectConcurrency,
|
|
520
612
|
},
|
|
521
613
|
{
|
|
522
614
|
name: 'effect-require-bounded-flatMap-concurrency',
|
|
523
615
|
message: 'Concurrent Effect.flatMap usage must declare a bounded concurrency value.',
|
|
616
|
+
tokenGroups: [['flatMap'], ['concurrency']],
|
|
524
617
|
check: hasUnboundedFlatMapConcurrency,
|
|
525
618
|
},
|
|
526
619
|
{
|
|
527
620
|
name: 'effect-no-unbounded-queue',
|
|
528
621
|
message: 'Use bounded, sliding, or dropping queues instead of unbounded queues.',
|
|
622
|
+
tokens: ['unbounded'],
|
|
529
623
|
patterns: [/\bQueue\.unbounded\s*\(/],
|
|
530
624
|
},
|
|
531
625
|
{
|
|
532
626
|
name: 'effect-no-unbounded-stream-buffer',
|
|
533
627
|
message: 'Stream buffers must be explicitly bounded.',
|
|
628
|
+
tokens: ['unbounded', 'Infinity'],
|
|
534
629
|
patterns: [
|
|
535
630
|
/\bStream\.(?:buffer|fromQueue|async|asyncPush)\s*\([^)]*\b(?:Infinity|unbounded)\b/,
|
|
536
631
|
],
|
|
@@ -538,11 +633,13 @@ const effectDefaultSpecs = [
|
|
|
538
633
|
{
|
|
539
634
|
name: 'effect-test-no-runpromise',
|
|
540
635
|
message: 'Use @effect/vitest it.effect instead of manually running Effects in tests.',
|
|
636
|
+
tokens: ['run'],
|
|
541
637
|
check: (source, context) => isEffectTestPath(context) && hasRuntimeCall(source),
|
|
542
638
|
},
|
|
543
639
|
{
|
|
544
640
|
name: 'effect-prefer-it-effect-for-unit-tests',
|
|
545
641
|
message: 'Use it.effect for tests that exercise Effect programs.',
|
|
642
|
+
tokens: ['it('],
|
|
546
643
|
check: (source, context) => isEffectTestPath(context) &&
|
|
547
644
|
!hasRuntimeCall(source) &&
|
|
548
645
|
/\bit\s*\([\s\S]*?Effect\./.test(stripCommentsAndStrings(source)),
|
|
@@ -550,22 +647,26 @@ const effectDefaultSpecs = [
|
|
|
550
647
|
{
|
|
551
648
|
name: 'effect-testClock-requires-fork',
|
|
552
649
|
message: 'Fork time-dependent work before adjusting TestClock.',
|
|
650
|
+
tokens: ['TestClock.adjust'],
|
|
553
651
|
check: (source) => /TestClock\.adjust\s*\(/.test(stripCommentsAndStrings(source)) &&
|
|
554
652
|
!hasForkBeforeTestClockAdjust(source),
|
|
555
653
|
},
|
|
556
654
|
{
|
|
557
655
|
name: 'effect-testClock-requires-testContext',
|
|
558
656
|
message: 'Use Effect test context when using TestClock.',
|
|
657
|
+
tokens: ['TestClock'],
|
|
559
658
|
check: (source, context) => isEffectTestPath(context) && hasTestClockWithoutEffectContext(source),
|
|
560
659
|
},
|
|
561
660
|
{
|
|
562
661
|
name: 'effect-no-real-sleep-in-tests',
|
|
563
662
|
message: 'Use TestClock instead of real sleeps in Effect tests.',
|
|
663
|
+
tokens: ['sleep'],
|
|
564
664
|
check: (source, context) => isEffectTestPath(context) && hasRealSleepWithoutTestClock(source),
|
|
565
665
|
},
|
|
566
666
|
{
|
|
567
667
|
name: 'effect-use-exit-for-failure-tests',
|
|
568
668
|
message: 'Use Effect.exit inside it.effect when asserting Effect failures.',
|
|
669
|
+
tokens: ['rejects', 'toThrow'],
|
|
569
670
|
check: (source, context) => isEffectTestPath(context) &&
|
|
570
671
|
!hasRuntimeCall(source) &&
|
|
571
672
|
/\b(?:rejects|toThrow)\b/.test(stripCommentsAndStrings(source)) &&
|
|
@@ -574,48 +675,57 @@ const effectDefaultSpecs = [
|
|
|
574
675
|
{
|
|
575
676
|
name: 'effect-no-focused-effect-tests',
|
|
576
677
|
message: 'Focused Effect tests must not be committed.',
|
|
678
|
+
tokens: ['.only'],
|
|
577
679
|
check: (source, context) => isEffectTestPath(context) &&
|
|
578
680
|
/\b(?:it|describe)\.effect\.only\s*\(/.test(stripCommentsAndStrings(source)),
|
|
579
681
|
},
|
|
580
682
|
{
|
|
581
683
|
name: 'effect-no-skipped-effect-tests',
|
|
582
684
|
message: 'Skipped Effect tests must not be committed.',
|
|
685
|
+
tokens: ['.skip'],
|
|
583
686
|
check: (source, context) => isEffectTestPath(context) &&
|
|
584
687
|
/\b(?:it|describe)\.effect\.skip\s*\(/.test(stripCommentsAndStrings(source)),
|
|
585
688
|
},
|
|
586
689
|
{
|
|
587
690
|
name: 'effect-no-obsolete-imports',
|
|
588
691
|
message: 'Import Effect APIs from the main effect package; deprecated split packages are blocked.',
|
|
692
|
+
tokens: ['@effect/io', '@effect/data'],
|
|
589
693
|
patterns: [/from\s+['"]@effect\/(?:io|data)['"]/],
|
|
590
694
|
},
|
|
591
695
|
{
|
|
592
696
|
name: 'effect-no-known-fake-api',
|
|
593
697
|
message: 'This is not a known Effect API for the configured version.',
|
|
698
|
+
tokens: ['fromPromise', 'tryCatch', 'bracket', 'fromEither'],
|
|
594
699
|
check: (source) => /\bEffect\.(?:fromPromise|tryCatch|bracket|fromEither)\s*\(/.test(stripCommentsAndStrings(source)),
|
|
595
700
|
},
|
|
596
701
|
{
|
|
597
702
|
name: 'effect-prefer-gen-over-do',
|
|
598
703
|
message: 'Prefer Effect.gen over Effect.Do for agent-readable sequential workflows.',
|
|
704
|
+
tokens: ['Effect.Do'],
|
|
599
705
|
patterns: [/\bEffect\.Do\b/],
|
|
600
706
|
},
|
|
601
707
|
{
|
|
602
708
|
name: 'effect-prefer-direct-yield-star',
|
|
603
709
|
message: 'Use direct yield* effect style instead of adapter-style Effect.gen helpers.',
|
|
710
|
+
tokenGroups: [['gen'], ['$']],
|
|
604
711
|
patterns: [/Effect\.gen\s*\(\s*function\*\s*\(\s*\$\s*\)/],
|
|
605
712
|
},
|
|
606
713
|
{
|
|
607
714
|
name: 'effect-prefer-config-redacted',
|
|
608
715
|
message: 'Use Config.redacted for sensitive config values so secrets stay redacted in logs and errors.',
|
|
716
|
+
tokens: ['secret', 'Secret'],
|
|
609
717
|
patterns: [/Config\.(?:secret|Secret)\s*\(/],
|
|
610
718
|
},
|
|
611
719
|
{
|
|
612
720
|
name: 'effect-no-deprecated-schema-package',
|
|
613
721
|
message: 'Use Schema from effect/Schema instead of @effect/schema.',
|
|
722
|
+
tokens: ['@effect/schema'],
|
|
614
723
|
patterns: [/from\s+['"]@effect\/schema['"]/],
|
|
615
724
|
},
|
|
616
725
|
{
|
|
617
726
|
name: 'effect-no-deprecated-context-tag-function',
|
|
618
727
|
message: 'Use the current Context.Tag class/service pattern instead of deprecated tag helpers.',
|
|
728
|
+
tokens: ['Context.Tag'],
|
|
619
729
|
patterns: [
|
|
620
730
|
/\b(?:const|let|var)\s+[A-Z][\w$]*\s*=\s*Context\.Tag(?:<[^>]+>)?\s*\(\s*['"][^'"]+['"]\s*\)/,
|
|
621
731
|
/(?:^|[;\n]\s*)Context\.Tag(?:<[^>]+>)?\s*\(\s*['"][^'"]+['"]\s*\)/,
|
|
@@ -624,11 +734,13 @@ const effectDefaultSpecs = [
|
|
|
624
734
|
{
|
|
625
735
|
name: 'effect-no-global-error-channel',
|
|
626
736
|
message: 'Use domain-specific tagged errors instead of the global Error type channel.',
|
|
737
|
+
tokenGroups: [['Effect.Effect'], ['Error']],
|
|
627
738
|
patterns: [/Effect\.Effect\s*<[^,>]+,\s*Error\s*[,>]/],
|
|
628
739
|
},
|
|
629
740
|
{
|
|
630
741
|
name: 'effect-use-duration-constructors',
|
|
631
742
|
message: 'Use Duration constructors or string durations instead of naked millisecond numbers.',
|
|
743
|
+
tokens: ['sleep', 'timeout', 'delay'],
|
|
632
744
|
patterns: [
|
|
633
745
|
/Effect\.(?:sleep|timeout|delay)\s*\(\s*\d+\s*\)/,
|
|
634
746
|
/Effect\.(?:timeout|delay)\s*\([^,]+,\s*\d+\s*\)/,
|
|
@@ -637,16 +749,19 @@ const effectDefaultSpecs = [
|
|
|
637
749
|
{
|
|
638
750
|
name: 'effect-no-mixed-effect-import-styles',
|
|
639
751
|
message: 'Use one Effect import style per file.',
|
|
752
|
+
tokens: ['import'],
|
|
640
753
|
check: hasMixedEffectImportStyles,
|
|
641
754
|
},
|
|
642
755
|
{
|
|
643
756
|
name: 'effect-prefer-effect-is',
|
|
644
757
|
message: 'Use Effect.isEffect for Effect type checks.',
|
|
758
|
+
tokens: ['instanceof', '_op'],
|
|
645
759
|
patterns: [/\b[A-Za-z_$][\w$]*\s+instanceof\s+Effect\b/, /\._op\s*===\s*['"]Effect['"]/],
|
|
646
760
|
},
|
|
647
761
|
{
|
|
648
762
|
name: 'effect-no-try-catch-in-effect-gen',
|
|
649
763
|
message: 'Use Effect error combinators instead of try/catch inside Effect.gen.',
|
|
764
|
+
tokenGroups: [['gen'], ['try']],
|
|
650
765
|
ast: (context, source) => ({
|
|
651
766
|
TryStatement(node) {
|
|
652
767
|
const { start } = node;
|
|
@@ -663,43 +778,56 @@ const effectDefaultSpecs = [
|
|
|
663
778
|
{
|
|
664
779
|
name: 'effect-no-new-promise',
|
|
665
780
|
message: 'Use Effect.async, Effect.promise, or Effect.tryPromise instead of new Promise.',
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
781
|
+
tokenGroups: [['Promise'], ['Effect', 'effect']],
|
|
782
|
+
ast: (context, source) => {
|
|
783
|
+
const isEffectModule = hasEffectSignal(source);
|
|
784
|
+
return {
|
|
785
|
+
NewExpression(node) {
|
|
786
|
+
if (isEffectModule && identifierName(node.callee) === 'Promise') {
|
|
787
|
+
reportAst(context, 'Use Effect.async, Effect.promise, or Effect.tryPromise instead of new Promise.', node);
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
},
|
|
673
792
|
},
|
|
674
793
|
{
|
|
675
794
|
name: 'effect-no-global-timers',
|
|
676
795
|
message: 'Use Effect.sleep, Schedule, or Clock instead of global timers in Effect modules.',
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
796
|
+
tokens: ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
|
|
797
|
+
ast: (context, source) => {
|
|
798
|
+
const isEffectModule = hasEffectSignal(source);
|
|
799
|
+
return {
|
|
800
|
+
CallExpression(node) {
|
|
801
|
+
const calleeName = identifierName(node.callee);
|
|
802
|
+
if (isEffectModule &&
|
|
803
|
+
calleeName &&
|
|
804
|
+
['clearInterval', 'clearTimeout', 'setInterval', 'setTimeout'].includes(calleeName)) {
|
|
805
|
+
reportAst(context, 'Use Effect.sleep, Schedule, or Clock instead of global timers in Effect modules.', node);
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
},
|
|
687
810
|
},
|
|
688
811
|
{
|
|
689
812
|
name: 'effect-no-native-error-classes',
|
|
690
813
|
message: 'Use tagged/data/schema errors instead of classes extending native Error.',
|
|
814
|
+
tokens: ['Error'],
|
|
691
815
|
check: hasNativeErrorClassInEffectModule,
|
|
692
|
-
ast: (context, source) =>
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
816
|
+
ast: (context, source) => {
|
|
817
|
+
const isEffectModule = hasEffectSignal(source);
|
|
818
|
+
return {
|
|
819
|
+
ClassDeclaration(node) {
|
|
820
|
+
if (isEffectModule && identifierName(node.superClass) === 'Error') {
|
|
821
|
+
reportAst(context, 'Use tagged/data/schema errors instead of classes extending native Error.', node);
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
},
|
|
699
826
|
},
|
|
700
827
|
{
|
|
701
828
|
name: 'effect-no-unsafe-effect-type-assertion',
|
|
702
829
|
message: 'Do not assert Effect error or requirement channels with type casts.',
|
|
830
|
+
tokenGroups: [[' as '], ['Effect.Effect']],
|
|
703
831
|
check: hasUnsafeEffectTypeAssertion,
|
|
704
832
|
ast: (context) => ({
|
|
705
833
|
TSAsExpression(node) {
|
|
@@ -712,6 +840,7 @@ const effectDefaultSpecs = [
|
|
|
712
840
|
{
|
|
713
841
|
name: 'effect-require-service-self-match',
|
|
714
842
|
message: 'Effect service/tag self type must match the declaring class name.',
|
|
843
|
+
tokenGroups: [['class'], ['extends'], ['Context', 'Service', 'Tag']],
|
|
715
844
|
check: hasServiceSelfMismatch,
|
|
716
845
|
ast: (context, source) => ({
|
|
717
846
|
ClassDeclaration(node) {
|
|
@@ -726,21 +855,28 @@ const effectDefaultSpecs = [
|
|
|
726
855
|
{
|
|
727
856
|
name: 'effect-no-effect-fn-iife',
|
|
728
857
|
message: 'Do not call Effect.fn as an IIFE; use Effect.gen for local one-shot workflows.',
|
|
858
|
+
tokenGroups: [['fn'], ['Effect', 'effect']],
|
|
729
859
|
check: hasEffectFnIife,
|
|
730
|
-
ast: (context, source) =>
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
860
|
+
ast: (context, source) => {
|
|
861
|
+
const isEffectFn = effectCallPredicate(source, ['fn', 'fnUntraced', 'fnUntracedEager']);
|
|
862
|
+
return {
|
|
863
|
+
CallExpression(node) {
|
|
864
|
+
const outerCallee = node.callee;
|
|
865
|
+
const middleCallee = outerCallee?.callee;
|
|
866
|
+
const innerCallee = middleCallee?.callee;
|
|
867
|
+
if (nodeType(outerCallee) === 'CallExpression' &&
|
|
868
|
+
nodeType(middleCallee) === 'CallExpression' &&
|
|
869
|
+
isEffectFn(innerCallee)) {
|
|
870
|
+
reportAst(context, 'Do not call Effect.fn as an IIFE; use Effect.gen for local one-shot workflows.', node);
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
};
|
|
874
|
+
},
|
|
742
875
|
},
|
|
743
876
|
];
|
|
744
|
-
const effectDefaultRules = makeRules(effectDefaultSpecs, {
|
|
877
|
+
const effectDefaultRules = makeRules(effectDefaultSpecs, {
|
|
878
|
+
defaultTokens: effectDefaultRuleTokens,
|
|
879
|
+
schema: strictPathOptionsSchema,
|
|
880
|
+
});
|
|
745
881
|
export default effectDefaultRules;
|
|
746
882
|
//# sourceMappingURL=effect-default.js.map
|