oxlint-plugin-effect 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/package.json +6 -7
  3. package/src/presets/core.ts +23 -12
  4. package/src/presets/effect-native.ts +18 -25
  5. package/src/presets/full.ts +32 -46
  6. package/src/presets/functional.ts +18 -0
  7. package/src/presets/index.ts +2 -0
  8. package/src/presets/strict.ts +77 -0
  9. package/src/rules/index.ts +17 -28
  10. package/src/rules/no-arrow-ladder.ts +1 -2
  11. package/src/rules/no-async-function.ts +1 -2
  12. package/src/rules/no-catch-all-to-map-error.ts +89 -0
  13. package/src/rules/no-effect-fn-generator.ts +1 -2
  14. package/src/rules/no-effect-gen-adapter.ts +1 -2
  15. package/src/rules/no-effect-map-flatten.ts +82 -0
  16. package/src/rules/no-effect-map-void.ts +1 -2
  17. package/src/rules/no-effect-orElse-ladder.ts +1 -2
  18. package/src/rules/no-effect-succeed-string.ts +1 -2
  19. package/src/rules/no-effect-succeed-variable.ts +1 -2
  20. package/src/rules/no-effect-succeed-void.ts +1 -2
  21. package/src/rules/no-effect-sync-console.ts +1 -2
  22. package/src/rules/no-effect-sync-wrapper.ts +1 -2
  23. package/src/rules/no-effect-type-alias.ts +1 -2
  24. package/src/rules/no-effect-wrapper-alias.ts +1 -4
  25. package/src/rules/no-extends-native-error.ts +1 -2
  26. package/src/rules/no-flatmap-ladder.ts +1 -3
  27. package/src/rules/no-fromnullable-coalesce.ts +1 -2
  28. package/src/rules/no-global-error-in-catch.ts +104 -0
  29. package/src/rules/no-global-error-in-failure.ts +83 -0
  30. package/src/rules/no-globals.ts +178 -0
  31. package/src/rules/no-iife-wrapper.ts +1 -2
  32. package/src/rules/{no-inline-runtime-provide.ts → no-inline-provide.ts} +3 -4
  33. package/src/rules/no-instanceof-schema.ts +2 -3
  34. package/src/rules/no-manual-effect-channels.ts +1 -2
  35. package/src/rules/no-match-effect-branch.ts +1 -2
  36. package/src/rules/no-match-void-branch.ts +1 -2
  37. package/src/rules/no-multiple-effect-provide.ts +75 -0
  38. package/src/rules/no-nested-effect-call.ts +1 -3
  39. package/src/rules/no-nested-effect-gen.ts +1 -2
  40. package/src/rules/no-nested-pipe.ts +1 -4
  41. package/src/rules/no-new-error.ts +46 -10
  42. package/src/rules/no-new-promise.ts +43 -7
  43. package/src/rules/no-node-builtin-import.ts +1 -3
  44. package/src/rules/no-option-boolean-normalization.ts +1 -2
  45. package/src/rules/no-positional-log-error.ts +76 -0
  46. package/src/rules/no-redundant-schema-tag-identifier.ts +62 -0
  47. package/src/rules/no-return-in-arrow.ts +1 -2
  48. package/src/rules/no-return-nullish.ts +80 -0
  49. package/src/rules/no-schema-struct-with-tag.ts +64 -0
  50. package/src/rules/no-schema-union-of-literals.ts +48 -0
  51. package/src/rules/no-string-sentinel-const.ts +1 -2
  52. package/src/rules/no-ternary.ts +1 -3
  53. package/src/rules/no-throw-statement.ts +32 -6
  54. package/src/rules/no-try-catch.ts +32 -7
  55. package/src/rules/no-unnecessary-arrow-block.ts +1 -2
  56. package/src/rules/no-unnecessary-effect-gen.ts +1 -3
  57. package/src/rules/no-unnecessary-pipe-chain.ts +47 -0
  58. package/src/rules/no-unnecessary-pipe.ts +1 -2
  59. package/src/vendor/effect-oxlint/Rule.ts +111 -173
  60. package/tests/ban-rules.test.ts +0 -167
  61. package/tests/context-rules.test.ts +75 -158
  62. package/tests/pattern-rules.test.ts +39 -50
  63. package/src/rules/no-console-in-effect.ts +0 -46
  64. package/src/rules/no-date-in-effect.ts +0 -61
  65. package/src/rules/no-dynamic-import.ts +0 -13
  66. package/src/rules/no-fetch-in-effect.ts +0 -47
  67. package/src/rules/no-global-console.ts +0 -17
  68. package/src/rules/no-global-date.ts +0 -20
  69. package/src/rules/no-global-fetch.ts +0 -14
  70. package/src/rules/no-global-random.ts +0 -13
  71. package/src/rules/no-global-timers.ts +0 -17
  72. package/src/rules/no-json-in-effect.ts +0 -45
  73. package/src/rules/no-json-parse.ts +0 -18
  74. package/src/rules/no-platform-globals.ts +0 -117
  75. package/src/rules/no-process-env-in-effect.ts +0 -39
  76. package/src/rules/no-process-env.ts +0 -13
  77. package/src/rules/no-random-in-effect.ts +0 -39
  78. package/src/rules/no-return-null.ts +0 -47
  79. package/src/rules/no-throw-in-effect-gen.ts +0 -42
  80. package/src/rules/no-timers-in-effect.ts +0 -47
  81. package/src/rules/no-try-catch-in-effect-gen.ts +0 -42
@@ -1,13 +1,39 @@
1
1
  /**
2
- * Ban `throw` statements.
2
+ * Ban `throw` statements. Context-aware messaging.
3
3
  *
4
- * Use Effect.fail or tagged errors instead.
4
+ * Inside Effect.gen/fn: "Use yield* Effect.fail() or yield* new MyError()"
5
+ * Outside: "Model errors with Effect.fail() — wrap this function with Effect.fn"
5
6
  *
6
7
  * Source: effect convention — errors should be in the type system
7
8
  */
8
- import { Rule } from "../vendor/effect-oxlint/index.js"
9
+ import { Diagnostic, Rule, Visitor, RuleContext } from "../vendor/effect-oxlint/index.js"
10
+ import * as Effect from "effect/Effect"
11
+ import * as Ref from "effect/Ref"
9
12
 
10
- export const noThrowStatement = Rule.banStatement("ThrowStatement", {
11
- message: "Avoid throw. Use Effect.fail with tagged errors.",
12
- meta: { type: "problem" },
13
+ import { makeEffectContextTracker } from "./_effect-context.js"
14
+
15
+ export const noThrowStatement = Rule.define({
16
+ name: "no-throw-statement",
17
+ meta: Rule.meta({
18
+ type: "problem",
19
+ description: "Avoid throw. Use Effect.fail with tagged errors.",
20
+ }),
21
+ create: function* () {
22
+ const ctx = yield* RuleContext
23
+ const [depth, tracker] = yield* makeEffectContextTracker
24
+
25
+ return Visitor.merge(tracker, {
26
+ ThrowStatement: (node) =>
27
+ Effect.flatMap(Ref.get(depth), (d) =>
28
+ ctx.report(
29
+ Diagnostic.make({
30
+ node,
31
+ message: d > 0
32
+ ? "Avoid throw inside Effect.gen/fn. Use yield* Effect.fail(new MyError()) or yield* new MyError() for yieldable errors."
33
+ : "Avoid throw. Model errors with Effect — wrap this function with Effect.fn and use Effect.fail with tagged errors.",
34
+ }),
35
+ ),
36
+ ),
37
+ })
38
+ },
13
39
  })
@@ -1,14 +1,39 @@
1
1
  /**
2
- * Ban `try/catch` statements.
2
+ * Ban `try/catch` statements. Context-aware messaging.
3
3
  *
4
- * Model failures in Effect's typed error channel.
4
+ * Inside Effect.gen/fn: "Use Effect.try or Effect.tryPromise"
5
+ * Outside: "Wrap with Effect.fn and use Effect.try for error handling"
5
6
  *
6
7
  * Source: biome-effect-linting-rules/no-try-catch, language-service/tryCatchInEffectGen
7
8
  */
8
- import { Rule } from "../vendor/effect-oxlint/index.js"
9
+ import { Diagnostic, Rule, Visitor, RuleContext } from "../vendor/effect-oxlint/index.js"
10
+ import * as Effect from "effect/Effect"
11
+ import * as Ref from "effect/Ref"
9
12
 
10
- export const noTryCatch = Rule.banStatement("TryStatement", {
11
- message:
12
- "Avoid try/catch. Model failures in Effect's typed error channel using Effect.try or Effect.tryPromise.",
13
- meta: { type: "suggestion" },
13
+ import { makeEffectContextTracker } from "./_effect-context.js"
14
+
15
+ export const noTryCatch = Rule.define({
16
+ name: "no-try-catch",
17
+ meta: Rule.meta({
18
+ type: "suggestion",
19
+ description: "Avoid try/catch. Use Effect.try or Effect.tryPromise.",
20
+ }),
21
+ create: function* () {
22
+ const ctx = yield* RuleContext
23
+ const [depth, tracker] = yield* makeEffectContextTracker
24
+
25
+ return Visitor.merge(tracker, {
26
+ TryStatement: (node) =>
27
+ Effect.flatMap(Ref.get(depth), (d) =>
28
+ ctx.report(
29
+ Diagnostic.make({
30
+ node,
31
+ message: d > 0
32
+ ? "Avoid try/catch inside Effect.gen/fn. Use Effect.try({ try: () => ..., catch: (e) => new MyError({ cause: e }) })."
33
+ : "Avoid try/catch. Wrap this function with Effect.fn and use Effect.try or Effect.tryPromise for error handling.",
34
+ }),
35
+ ),
36
+ ),
37
+ })
38
+ },
14
39
  })
@@ -3,8 +3,7 @@
3
3
  *
4
4
  * Source: language-service/unnecessaryArrowBlock
5
5
  */
6
- import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
7
- import { RuleContext } from "../vendor/effect-oxlint/index.js"
6
+ import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
8
7
  import * as Effect from "effect/Effect"
9
8
 
10
9
  export const noUnnecessaryArrowBlock = Rule.define({
@@ -12,10 +12,8 @@
12
12
  * Source: language-service/unnecessaryEffectGen
13
13
  */
14
14
  import type { ESTree } from "@oxlint/plugins"
15
- import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
16
- import { RuleContext } from "../vendor/effect-oxlint/index.js"
15
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
17
16
  import * as Effect from "effect/Effect"
18
- import * as Option from "effect/Option"
19
17
 
20
18
  export const noUnnecessaryEffectGen = Rule.define({
21
19
  name: "no-unnecessary-effect-gen",
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Ban `x.pipe(f).pipe(g)` — merge into `x.pipe(f, g)`.
3
+ *
4
+ * Source: language-service/unnecessaryPipeChain
5
+ */
6
+ import type { ESTree } from "@oxlint/plugins"
7
+ import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
8
+ import * as Effect from "effect/Effect"
9
+
10
+ const isMethodPipe = (node: ESTree.Node): boolean => {
11
+ if (node.type !== "CallExpression") return false
12
+ const callee = (node as unknown as Record<string, unknown>)["callee"]
13
+ if (callee == null || typeof callee !== "object" || !("type" in callee)) return false
14
+ if (callee.type !== "MemberExpression") return false
15
+ const prop = (callee as unknown as Record<string, unknown>)["property"]
16
+ if (prop == null || typeof prop !== "object" || !("type" in prop)) return false
17
+ return prop.type === "Identifier" && "name" in prop && prop.name === "pipe"
18
+ }
19
+
20
+ export const noUnnecessaryPipeChain = Rule.define({
21
+ name: "no-unnecessary-pipe-chain",
22
+ meta: Rule.meta({
23
+ type: "suggestion",
24
+ description:
25
+ "Avoid chaining .pipe().pipe(). Merge into a single .pipe() call.",
26
+ }),
27
+ create: function* () {
28
+ const ctx = yield* RuleContext
29
+ return {
30
+ CallExpression: (node) => {
31
+ if (!isMethodPipe(node)) return Effect.void
32
+ const callee = (node as unknown as Record<string, unknown>)["callee"] as Record<string, unknown>
33
+ const obj = callee["object"]
34
+ if (obj != null && typeof obj === "object" && isMethodPipe(obj as ESTree.Node)) {
35
+ return ctx.report(
36
+ Diagnostic.make({
37
+ node,
38
+ message:
39
+ "Unnecessary .pipe() chain. Merge into a single .pipe() call.",
40
+ }),
41
+ )
42
+ }
43
+ return Effect.void
44
+ },
45
+ }
46
+ },
47
+ })
@@ -6,8 +6,7 @@
6
6
  * Source: language-service/unnecessaryPipe
7
7
  */
8
8
  import type { ESTree } from "@oxlint/plugins"
9
- import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
10
- import { RuleContext } from "../vendor/effect-oxlint/index.js"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
  import * as Option from "effect/Option"
13
12
 
@@ -396,195 +396,133 @@ export const banStatement = (
396
396
  // ---------------------------------------------------------------------------
397
397
 
398
398
  /**
399
- * Specification for `banMultiple`: which patterns to ban under one rule.
399
+ * A single ban specification. Each spec carries its own message so
400
+ * diagnostics are specific to the exact pattern matched.
400
401
  *
401
- * @since 0.2.0
402
+ * @since 0.3.0
402
403
  */
403
- export interface BanMultipleSpec {
404
- /** Bare identifier calls to ban (e.g. `'fetch'` or `['useState', 'useEffect']`). */
405
- readonly calls?: string | ReadonlyArray<string> | undefined;
406
- /** `new` expressions to ban (e.g. `'Date'` or `['Error', 'TypeError']`). */
407
- readonly newExprs?: string | ReadonlyArray<string> | undefined;
408
- /** Member expressions to ban: `[object, property | properties]` tuples. */
409
- readonly members?:
410
- | ReadonlyArray<
411
- readonly [obj: string, prop: string | ReadonlyArray<string>]
412
- >
413
- | undefined;
414
- /** Import sources to ban (string or predicate). */
415
- readonly imports?:
416
- | ReadonlyArray<string | ((source: string) => boolean)>
417
- | undefined;
418
- /** Statement node types to ban (e.g. `'ThrowStatement'`). */
419
- readonly statements?: ReadonlyArray<string> | undefined;
420
- }
404
+ export type BanSpec =
405
+ | { readonly type: 'member'; readonly object: string; readonly property: string | ReadonlyArray<string>; readonly message: string }
406
+ | { readonly type: 'call'; readonly name: string | ReadonlyArray<string>; readonly message: string }
407
+ | { readonly type: 'new'; readonly name: string | ReadonlyArray<string>; readonly message: string }
408
+ | { readonly type: 'import'; readonly source: string | ((source: string) => boolean); readonly message: string }
409
+ | { readonly type: 'statement'; readonly nodeType: string; readonly message: string };
421
410
 
422
411
  /**
423
- * Create a rule that bans multiple patterns with a shared message.
424
- *
425
- * Combines call bans, `new` expression bans, member bans, import bans,
426
- * and statement bans into a single rule with merged visitors.
412
+ * Create a rule that bans multiple patterns, each with its own message.
427
413
  *
428
414
  * @example
429
415
  * ```ts
430
- * // Ban 5 loop statement types
431
- * Rule.banMultiple(
432
- * {
433
- * statements: [
434
- * 'ForStatement', 'ForInStatement', 'ForOfStatement',
435
- * 'WhileStatement', 'DoWhileStatement'
436
- * ]
437
- * },
438
- * { message: 'Use Arr.map / Effect.forEach instead' }
439
- * )
440
- *
441
- * // Combine import + member bans
442
- * Rule.banMultiple(
443
- * {
444
- * imports: ['node:fs'],
445
- * members: [['fs', ['readFileSync', 'writeFileSync']]]
446
- * },
447
- * { message: 'Use Effect FileSystem service' }
448
- * )
449
- *
450
- * // Combine new expression + member bans
451
- * Rule.banMultiple(
452
- * {
453
- * newExprs: 'Date',
454
- * members: [['Date', 'now']]
455
- * },
456
- * { message: 'Use Clock service' }
457
- * )
416
+ * Rule.banMultiple({
417
+ * name: "no-global-date",
418
+ * meta: { type: "suggestion" },
419
+ * specs: [
420
+ * { type: "new", name: "Date", message: "Avoid new Date(). Use Clock service." },
421
+ * { type: "member", object: "Date", property: "now", message: "Avoid Date.now(). Use Clock service." },
422
+ * ],
423
+ * })
458
424
  * ```
459
425
  *
460
- * @since 0.2.0
426
+ * @since 0.3.0
461
427
  */
462
- export const banMultiple = (
463
- spec: BanMultipleSpec,
464
- opts: {
465
- readonly name?: string | undefined;
466
- readonly message: string;
467
- readonly meta?:
468
- | { readonly type?: 'problem' | 'suggestion' }
469
- | undefined;
470
- }
471
- ): CreateRule =>
472
- define({
473
- name: opts.name ?? 'ban-multiple',
428
+ export const banMultiple = (config: {
429
+ readonly name: string;
430
+ readonly meta?: { readonly type?: 'problem' | 'suggestion' } | undefined;
431
+ readonly specs: ReadonlyArray<BanSpec>;
432
+ }): CreateRule => {
433
+ const description = config.specs.map((s) => s.message).join(' ');
434
+ return define({
435
+ name: config.name,
474
436
  meta: meta({
475
- type: opts.meta?.type ?? 'suggestion',
476
- description: opts.message
437
+ type: config.meta?.type ?? 'suggestion',
438
+ description
477
439
  }),
478
440
  create: function* () {
479
441
  const ctx = yield* RuleContext;
480
- const report = (node: ESTree.Node) =>
481
- ctx.report(makeDiagnostic({ node, message: opts.message }));
482
-
483
- // Statement bans
484
- const stmtVisitors: ReadonlyArray<EffectVisitor> =
485
- spec.statements !== undefined
486
- ? Arr.map(
487
- spec.statements,
488
- (nodeType): EffectVisitor => ({
489
- [nodeType]: report
490
- })
491
- )
492
- : [];
493
442
 
494
- // Call bans
495
- const callVisitors: ReadonlyArray<EffectVisitor> =
496
- spec.calls !== undefined
497
- ? ((names: ReadonlyArray<string>) => [
498
- {
499
- CallExpression: (node: ESTree.Node) =>
500
- pipe(
501
- AST.narrow(node, 'CallExpression'),
502
- Option.flatMap(AST.calleeName),
503
- Option.filter((n) =>
504
- Arr.contains(names, n)
505
- ),
506
- Option.match({
507
- onNone: () => Effect.void,
508
- onSome: () => report(node)
509
- })
510
- )
511
- } satisfies EffectVisitor
512
- ])(P.isString(spec.calls) ? [spec.calls] : spec.calls)
513
- : [];
443
+ const visitors: Array<EffectVisitor> = [];
514
444
 
515
- // NewExpression bans
516
- const newExprVisitors: ReadonlyArray<EffectVisitor> =
517
- spec.newExprs !== undefined
518
- ? ((names: ReadonlyArray<string>) => [
519
- {
520
- NewExpression: (node: ESTree.Node) =>
521
- pipe(
522
- AST.narrow(node, 'NewExpression'),
523
- Option.flatMap((n) =>
524
- newExprCalleeName(n.callee)
525
- ),
526
- Option.filter((n) =>
527
- Arr.contains(names, n)
528
- ),
529
- Option.match({
530
- onNone: () => Effect.void,
531
- onSome: () => report(node)
532
- })
533
- )
534
- } satisfies EffectVisitor
535
- ])(
536
- P.isString(spec.newExprs)
537
- ? [spec.newExprs]
538
- : spec.newExprs
539
- )
540
- : [];
541
-
542
- // Member bans
543
- const memberVisitors: ReadonlyArray<EffectVisitor> =
544
- spec.members !== undefined
545
- ? Arr.map(
546
- spec.members,
547
- ([obj, prop]): EffectVisitor => ({
548
- MemberExpression: (node: ESTree.Node) =>
549
- pipe(
550
- AST.narrow(node, 'MemberExpression'),
551
- Option.flatMap(
552
- AST.matchMember(obj, prop)
553
- ),
554
- Option.match({
555
- onNone: () => Effect.void,
556
- onSome: (matched) => report(matched)
557
- })
558
- )
559
- })
560
- )
561
- : [];
562
-
563
- // Import bans
564
- const importVisitors: ReadonlyArray<EffectVisitor> =
565
- spec.imports !== undefined
566
- ? Arr.map(
567
- spec.imports,
568
- (source): EffectVisitor => ({
569
- ImportDeclaration: (node: ESTree.Node) =>
570
- pipe(
571
- AST.narrow(node, 'ImportDeclaration'),
572
- Option.flatMap(AST.matchImport(source)),
573
- Option.match({
574
- onNone: () => Effect.void,
575
- onSome: (matched) => report(matched)
576
- })
577
- )
578
- })
579
- )
580
- : [];
445
+ for (const spec of config.specs) {
446
+ switch (spec.type) {
447
+ case 'member': {
448
+ const report = (node: ESTree.Node) =>
449
+ ctx.report(makeDiagnostic({ node, message: spec.message }));
450
+ visitors.push({
451
+ MemberExpression: (node: ESTree.Node) =>
452
+ pipe(
453
+ AST.narrow(node, 'MemberExpression'),
454
+ Option.flatMap(AST.matchMember(spec.object, spec.property)),
455
+ Option.match({
456
+ onNone: () => Effect.void,
457
+ onSome: (matched) => report(matched)
458
+ })
459
+ )
460
+ });
461
+ break;
462
+ }
463
+ case 'call': {
464
+ const names = P.isString(spec.name) ? [spec.name] : spec.name;
465
+ const report = (node: ESTree.Node) =>
466
+ ctx.report(makeDiagnostic({ node, message: spec.message }));
467
+ visitors.push({
468
+ CallExpression: (node: ESTree.Node) =>
469
+ pipe(
470
+ AST.narrow(node, 'CallExpression'),
471
+ Option.flatMap(AST.calleeName),
472
+ Option.filter((n) => Arr.contains(names, n)),
473
+ Option.match({
474
+ onNone: () => Effect.void,
475
+ onSome: () => report(node)
476
+ })
477
+ )
478
+ });
479
+ break;
480
+ }
481
+ case 'new': {
482
+ const names = P.isString(spec.name) ? [spec.name] : spec.name;
483
+ const report = (node: ESTree.Node) =>
484
+ ctx.report(makeDiagnostic({ node, message: spec.message }));
485
+ visitors.push({
486
+ NewExpression: (node: ESTree.Node) =>
487
+ pipe(
488
+ AST.narrow(node, 'NewExpression'),
489
+ Option.flatMap((n) => newExprCalleeName(n.callee)),
490
+ Option.filter((n) => Arr.contains(names, n)),
491
+ Option.match({
492
+ onNone: () => Effect.void,
493
+ onSome: () => report(node)
494
+ })
495
+ )
496
+ });
497
+ break;
498
+ }
499
+ case 'import': {
500
+ const report = (node: ESTree.Node) =>
501
+ ctx.report(makeDiagnostic({ node, message: spec.message }));
502
+ visitors.push({
503
+ ImportDeclaration: (node: ESTree.Node) =>
504
+ pipe(
505
+ AST.narrow(node, 'ImportDeclaration'),
506
+ Option.flatMap(AST.matchImport(spec.source)),
507
+ Option.match({
508
+ onNone: () => Effect.void,
509
+ onSome: (matched) => report(matched)
510
+ })
511
+ )
512
+ });
513
+ break;
514
+ }
515
+ case 'statement': {
516
+ visitors.push({
517
+ [spec.nodeType]: (node: ESTree.Node) =>
518
+ ctx.report(makeDiagnostic({ node, message: spec.message }))
519
+ });
520
+ break;
521
+ }
522
+ }
523
+ }
581
524
 
582
- return mergeVisitors(
583
- ...stmtVisitors,
584
- ...callVisitors,
585
- ...newExprVisitors,
586
- ...memberVisitors,
587
- ...importVisitors
588
- );
525
+ return mergeVisitors(...visitors);
589
526
  }
590
527
  });
528
+ };
@@ -9,19 +9,6 @@ import {
9
9
  noOptionAs,
10
10
  noRuntimeRunFork,
11
11
  noRunInEffect,
12
- noGlobalFetch,
13
- noGlobalConsole,
14
- noGlobalDate,
15
- noGlobalRandom,
16
- noGlobalTimers,
17
- noNewPromise,
18
- noNewError,
19
- noProcessEnv,
20
- noNodeBuiltinImport,
21
- noDynamicImport,
22
- noThrowStatement,
23
- noTryCatch,
24
- noJsonParse,
25
12
  } from "../src/rules/index.js"
26
13
 
27
14
  // --- API ban rules ---
@@ -96,157 +83,3 @@ describe("noRunInEffect", () => {
96
83
  Testing.expectNoDiagnostics(result)
97
84
  })
98
85
  })
99
-
100
- // --- Global ban rules ---
101
-
102
- describe("noGlobalFetch", () => {
103
- test("reports fetch()", () => {
104
- const result = Testing.runRule(noGlobalFetch, "CallExpression", Testing.callExpr("fetch"))
105
- expect(result.length).toBe(1)
106
- })
107
-
108
- test("ignores other calls", () => {
109
- const result = Testing.runRule(noGlobalFetch, "CallExpression", Testing.callExpr("getData"))
110
- Testing.expectNoDiagnostics(result)
111
- })
112
- })
113
-
114
- describe("noGlobalConsole", () => {
115
- test("reports console.log", () => {
116
- const result = Testing.runRule(noGlobalConsole, "MemberExpression", Testing.memberExpr("console", "log"))
117
- expect(result.length).toBe(1)
118
- })
119
-
120
- test("reports console.error", () => {
121
- const result = Testing.runRule(noGlobalConsole, "MemberExpression", Testing.memberExpr("console", "error"))
122
- expect(result.length).toBe(1)
123
- })
124
-
125
- test("ignores logger.log", () => {
126
- const result = Testing.runRule(noGlobalConsole, "MemberExpression", Testing.memberExpr("logger", "log"))
127
- Testing.expectNoDiagnostics(result)
128
- })
129
- })
130
-
131
- describe("noGlobalDate", () => {
132
- test("reports Date.now", () => {
133
- const result = Testing.runRule(noGlobalDate, "MemberExpression", Testing.memberExpr("Date", "now"))
134
- expect(result.length).toBe(1)
135
- })
136
-
137
- test("reports new Date()", () => {
138
- const result = Testing.runRule(noGlobalDate, "NewExpression", Testing.newExpr("Date"))
139
- expect(result.length).toBe(1)
140
- })
141
- })
142
-
143
- describe("noGlobalRandom", () => {
144
- test("reports Math.random", () => {
145
- const result = Testing.runRule(noGlobalRandom, "MemberExpression", Testing.memberExpr("Math", "random"))
146
- expect(result.length).toBe(1)
147
- })
148
- })
149
-
150
- describe("noGlobalTimers", () => {
151
- test("reports setTimeout", () => {
152
- const result = Testing.runRule(noGlobalTimers, "CallExpression", Testing.callExpr("setTimeout"))
153
- expect(result.length).toBe(1)
154
- })
155
-
156
- test("reports setInterval", () => {
157
- const result = Testing.runRule(noGlobalTimers, "CallExpression", Testing.callExpr("setInterval"))
158
- expect(result.length).toBe(1)
159
- })
160
- })
161
-
162
- describe("noNewPromise", () => {
163
- test("reports new Promise()", () => {
164
- const result = Testing.runRule(noNewPromise, "NewExpression", Testing.newExpr("Promise"))
165
- expect(result.length).toBe(1)
166
- })
167
- })
168
-
169
- describe("noNewError", () => {
170
- test("reports new Error()", () => {
171
- const result = Testing.runRule(noNewError, "NewExpression", Testing.newExpr("Error"))
172
- expect(result.length).toBe(1)
173
- })
174
-
175
- test("reports new TypeError()", () => {
176
- const result = Testing.runRule(noNewError, "NewExpression", Testing.newExpr("TypeError"))
177
- expect(result.length).toBe(1)
178
- })
179
-
180
- test("ignores new MyError()", () => {
181
- const result = Testing.runRule(noNewError, "NewExpression", Testing.newExpr("MyError"))
182
- Testing.expectNoDiagnostics(result)
183
- })
184
- })
185
-
186
- describe("noProcessEnv", () => {
187
- test("reports process.env", () => {
188
- const result = Testing.runRule(noProcessEnv, "MemberExpression", Testing.memberExpr("process", "env"))
189
- expect(result.length).toBe(1)
190
- })
191
- })
192
-
193
- // --- Import ban rules ---
194
-
195
- describe("noNodeBuiltinImport", () => {
196
- test("reports import from 'fs'", () => {
197
- const result = Testing.runRule(noNodeBuiltinImport, "ImportDeclaration", Testing.importDecl("fs"))
198
- expect(result.length).toBe(1)
199
- })
200
-
201
- test("reports import from 'node:fs'", () => {
202
- const result = Testing.runRule(noNodeBuiltinImport, "ImportDeclaration", Testing.importDecl("node:fs"))
203
- expect(result.length).toBe(1)
204
- })
205
-
206
- test("reports import from 'child_process'", () => {
207
- const result = Testing.runRule(noNodeBuiltinImport, "ImportDeclaration", Testing.importDecl("child_process"))
208
- expect(result.length).toBe(1)
209
- })
210
-
211
- test("ignores import from 'effect'", () => {
212
- const result = Testing.runRule(noNodeBuiltinImport, "ImportDeclaration", Testing.importDecl("effect"))
213
- Testing.expectNoDiagnostics(result)
214
- })
215
- })
216
-
217
- // --- Statement ban rules ---
218
-
219
- describe("noThrowStatement", () => {
220
- test("reports throw", () => {
221
- const result = Testing.runRule(noThrowStatement, "ThrowStatement", Testing.throwStmt())
222
- expect(result.length).toBe(1)
223
- })
224
- })
225
-
226
- describe("noTryCatch", () => {
227
- test("reports try/catch", () => {
228
- const result = Testing.runRule(noTryCatch, "TryStatement", Testing.tryStmt())
229
- expect(result.length).toBe(1)
230
- })
231
- })
232
-
233
- describe("noDynamicImport", () => {
234
- test("reports dynamic import()", () => {
235
- const result = Testing.runRule(noDynamicImport, "ImportExpression", { type: "ImportExpression" } as never)
236
- expect(result.length).toBe(1)
237
- })
238
- })
239
-
240
- // --- JSON ---
241
-
242
- describe("noJsonParse", () => {
243
- test("reports JSON.parse", () => {
244
- const result = Testing.runRule(noJsonParse, "MemberExpression", Testing.memberExpr("JSON", "parse"))
245
- expect(result.length).toBe(1)
246
- })
247
-
248
- test("reports JSON.stringify", () => {
249
- const result = Testing.runRule(noJsonParse, "MemberExpression", Testing.memberExpr("JSON", "stringify"))
250
- expect(result.length).toBe(1)
251
- })
252
- })