@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +3 -3
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/rules/acronym-case.d.ts.map +1 -1
  7. package/dist/rules/acronym-case.js +0 -2
  8. package/dist/rules/acronym-case.js.map +1 -1
  9. package/dist/rules/effect-default-helpers.d.ts.map +1 -1
  10. package/dist/rules/effect-default-helpers.js +105 -28
  11. package/dist/rules/effect-default-helpers.js.map +1 -1
  12. package/dist/rules/effect-default.d.ts.map +1 -1
  13. package/dist/rules/effect-default.js +269 -133
  14. package/dist/rules/effect-default.js.map +1 -1
  15. package/dist/rules/effect-rule-core.d.ts +8 -0
  16. package/dist/rules/effect-rule-core.d.ts.map +1 -1
  17. package/dist/rules/effect-rule-core.js +208 -61
  18. package/dist/rules/effect-rule-core.js.map +1 -1
  19. package/dist/rules/effect-source-helpers.d.ts.map +1 -1
  20. package/dist/rules/effect-source-helpers.js +37 -12
  21. package/dist/rules/effect-source-helpers.js.map +1 -1
  22. package/dist/rules/effect-source-scan.d.ts.map +1 -1
  23. package/dist/rules/effect-source-scan.js +18 -10
  24. package/dist/rules/effect-source-scan.js.map +1 -1
  25. package/dist/rules/effect-strict-helpers.d.ts.map +1 -1
  26. package/dist/rules/effect-strict-helpers.js +180 -11
  27. package/dist/rules/effect-strict-helpers.js.map +1 -1
  28. package/dist/rules/effect-strict.d.ts.map +1 -1
  29. package/dist/rules/effect-strict.js +87 -23
  30. package/dist/rules/effect-strict.js.map +1 -1
  31. package/dist/rules/max-line-length.d.ts.map +1 -1
  32. package/dist/rules/max-line-length.js +25 -7
  33. package/dist/rules/max-line-length.js.map +1 -1
  34. package/dist/rules/plugin.d.ts +2 -121
  35. package/dist/rules/plugin.d.ts.map +1 -1
  36. package/dist/rules/plugin.js +78 -94
  37. package/dist/rules/plugin.js.map +1 -1
  38. package/dist/rules/require-function-doc.d.ts.map +1 -1
  39. package/dist/rules/require-function-doc.js +32 -21
  40. package/dist/rules/require-function-doc.js.map +1 -1
  41. package/dist/rules/source-cache.d.ts +11 -0
  42. package/dist/rules/source-cache.d.ts.map +1 -0
  43. package/dist/rules/source-cache.js +50 -0
  44. package/dist/rules/source-cache.js.map +1 -0
  45. 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 isEffectMember(node, source, methods) {
40
- const { objectName, propertyName } = memberParts(node);
41
- return Boolean(objectName &&
42
- propertyName &&
43
- effectImportAliases(source).includes(objectName) &&
44
- methods.has(propertyName));
45
- }
46
- function isEffectFunctionCall(callee, source, names) {
47
- const calleeName = identifierName(callee);
48
- return Boolean(calleeName &&
49
- [...names].some((name) => effectFunctionAliases(source, 'Effect', name).includes(calleeName)));
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
- ast: (context, source) => ({
243
- CallExpression(node) {
244
- if (!hasEffectSignal(source)) {
245
- return;
246
- }
247
- const { callee } = node;
248
- const { propertyName } = memberParts(callee);
249
- if (propertyName === 'then' || propertyName === 'catch') {
250
- reportAst(context, 'Use Effect combinators instead of Promise then/catch chains in Effect modules.', node);
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
- CallExpression(node) {
266
- const call = node;
267
- if ((isEffectMember(call.callee, source, new Set(['fail'])) ||
268
- isEffectFunctionCall(call.callee, source, new Set(['fail']))) &&
269
- isStringLikeLiteral(call.arguments?.[0])) {
270
- reportAst(context, 'Use structured tagged errors instead of string failures.', node);
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
- CallExpression(node) {
281
- const call = node;
282
- const firstArg = call.arguments?.[0];
283
- if (!isEffectMember(call.callee, source, new Set(['fail'])) &&
284
- !isEffectFunctionCall(call.callee, source, new Set(['fail']))) {
285
- return;
286
- }
287
- if (!firstArg) {
288
- return;
289
- }
290
- if (nodeType(firstArg) === 'NewExpression' && identifierName(firstArg.callee) === 'Error') {
291
- reportAst(context, 'Use tagged/data/schema errors in Effect.fail instead of global Error values.', node);
292
- return;
293
- }
294
- if (nodeType(firstArg) === 'ObjectExpression' &&
295
- !firstArg.properties?.some((property) => propertyKeyName(property.key) === '_tag')) {
296
- reportAst(context, 'Use tagged/data/schema errors in Effect.fail instead of global Error values.', node);
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
- ast: (context, source) => ({
377
- CallExpression(node) {
378
- const { objectName, propertyName } = memberParts(node.callee);
379
- if (hasEffectSignal(source) &&
380
- objectName === 'console' &&
381
- propertyName &&
382
- ['debug', 'error', 'info', 'log', 'warn'].includes(propertyName)) {
383
- reportAst(context, 'Use Effect logging APIs instead of console logging in Effect code.', node);
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
- ast: (context, source) => ({
392
- MemberExpression(node) {
393
- const { objectName, propertyName } = memberParts(node);
394
- const processEnv = objectName === 'process' && propertyName === 'env';
395
- if (hasEffectSignal(source) && !isConfiguredPath(context, 'configLayers') && processEnv) {
396
- reportAst(context, 'Use Effect Config instead of process.env in Effect code.', node);
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
- ast: (context, source) => ({
405
- CallExpression(node) {
406
- const { objectName, propertyName } = memberParts(node.callee);
407
- if (hasEffectSignal(source) &&
408
- !isConfiguredPath(context, 'adapterLayers') &&
409
- objectName === 'Date' &&
410
- propertyName === 'now') {
411
- reportAst(context, 'Use Effect Clock instead of Date.now in Effect code.', node);
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
- ast: (context, source) => ({
420
- CallExpression(node) {
421
- const { objectName, propertyName } = memberParts(node.callee);
422
- if (hasEffectSignal(source) &&
423
- !isConfiguredPath(context, 'adapterLayers') &&
424
- objectName === 'Math' &&
425
- propertyName === 'random') {
426
- reportAst(context, 'Use Effect Random instead of Math.random in Effect code.', node);
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
- ast: (context, source) => ({
667
- NewExpression(node) {
668
- if (hasEffectSignal(source) && identifierName(node.callee) === 'Promise') {
669
- reportAst(context, 'Use Effect.async, Effect.promise, or Effect.tryPromise instead of new Promise.', node);
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
- ast: (context, source) => ({
678
- CallExpression(node) {
679
- const calleeName = identifierName(node.callee);
680
- if (hasEffectSignal(source) &&
681
- calleeName &&
682
- ['clearInterval', 'clearTimeout', 'setInterval', 'setTimeout'].includes(calleeName)) {
683
- reportAst(context, 'Use Effect.sleep, Schedule, or Clock instead of global timers in Effect modules.', node);
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
- ClassDeclaration(node) {
694
- if (hasEffectSignal(source) && identifierName(node.superClass) === 'Error') {
695
- reportAst(context, 'Use tagged/data/schema errors instead of classes extending native Error.', node);
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
- CallExpression(node) {
732
- const outerCallee = node.callee;
733
- const middleCallee = outerCallee?.callee;
734
- const innerCallee = middleCallee?.callee;
735
- if (nodeType(outerCallee) === 'CallExpression' &&
736
- nodeType(middleCallee) === 'CallExpression' &&
737
- isEffectMember(innerCallee, source, new Set(['fn', 'fnUntraced', 'fnUntracedEager']))) {
738
- reportAst(context, 'Do not call Effect.fn as an IIFE; use Effect.gen for local one-shot workflows.', node);
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, { schema: strictPathOptionsSchema });
877
+ const effectDefaultRules = makeRules(effectDefaultSpecs, {
878
+ defaultTokens: effectDefaultRuleTokens,
879
+ schema: strictPathOptionsSchema,
880
+ });
745
881
  export default effectDefaultRules;
746
882
  //# sourceMappingURL=effect-default.js.map