oxlint-plugin-effect 0.0.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 (95) hide show
  1. package/.effect-lsp.json +60 -0
  2. package/.oxlintrc.json +33 -0
  3. package/package.json +51 -0
  4. package/scripts/add-rule.ts +87 -0
  5. package/scripts/codegen.ts +108 -0
  6. package/src/index.ts +9 -0
  7. package/src/plugin.ts +16 -0
  8. package/src/presets/core.ts +36 -0
  9. package/src/presets/effect-native.ts +41 -0
  10. package/src/presets/full.ts +84 -0
  11. package/src/presets/index.ts +3 -0
  12. package/src/rules/_effect-context.ts +28 -0
  13. package/src/rules/index.ts +83 -0
  14. package/src/rules/no-arrow-ladder.ts +71 -0
  15. package/src/rules/no-async-function.ts +48 -0
  16. package/src/rules/no-console-in-effect.ts +46 -0
  17. package/src/rules/no-date-in-effect.ts +61 -0
  18. package/src/rules/no-dynamic-import.ts +13 -0
  19. package/src/rules/no-effect-as.ts +14 -0
  20. package/src/rules/no-effect-async.ts +14 -0
  21. package/src/rules/no-effect-bind.ts +13 -0
  22. package/src/rules/no-effect-do.ts +14 -0
  23. package/src/rules/no-effect-fn-generator.ts +59 -0
  24. package/src/rules/no-effect-gen-adapter.ts +51 -0
  25. package/src/rules/no-effect-map-void.ts +81 -0
  26. package/src/rules/no-effect-never.ts +14 -0
  27. package/src/rules/no-effect-orElse-ladder.ts +80 -0
  28. package/src/rules/no-effect-succeed-string.ts +53 -0
  29. package/src/rules/no-effect-succeed-variable.ts +51 -0
  30. package/src/rules/no-effect-succeed-void.ts +64 -0
  31. package/src/rules/no-effect-sync-console.ts +92 -0
  32. package/src/rules/no-effect-sync-wrapper.ts +68 -0
  33. package/src/rules/no-effect-type-alias.ts +72 -0
  34. package/src/rules/no-effect-wrapper-alias.ts +98 -0
  35. package/src/rules/no-extends-native-error.ts +55 -0
  36. package/src/rules/no-fetch-in-effect.ts +47 -0
  37. package/src/rules/no-flatmap-ladder.ts +79 -0
  38. package/src/rules/no-fromnullable-coalesce.ts +63 -0
  39. package/src/rules/no-global-console.ts +17 -0
  40. package/src/rules/no-global-date.ts +20 -0
  41. package/src/rules/no-global-fetch.ts +14 -0
  42. package/src/rules/no-global-random.ts +13 -0
  43. package/src/rules/no-global-timers.ts +17 -0
  44. package/src/rules/no-if-statement.ts +16 -0
  45. package/src/rules/no-iife-wrapper.ts +52 -0
  46. package/src/rules/no-inline-runtime-provide.ts +42 -0
  47. package/src/rules/no-instanceof-schema.ts +43 -0
  48. package/src/rules/no-json-in-effect.ts +45 -0
  49. package/src/rules/no-json-parse.ts +18 -0
  50. package/src/rules/no-manual-effect-channels.ts +59 -0
  51. package/src/rules/no-match-effect-branch.ts +83 -0
  52. package/src/rules/no-match-void-branch.ts +73 -0
  53. package/src/rules/no-nested-effect-call.ts +59 -0
  54. package/src/rules/no-nested-effect-gen.ts +44 -0
  55. package/src/rules/no-nested-pipe.ts +76 -0
  56. package/src/rules/no-new-error.ts +17 -0
  57. package/src/rules/no-new-promise.ts +14 -0
  58. package/src/rules/no-node-builtin-import.ts +70 -0
  59. package/src/rules/no-option-as.ts +13 -0
  60. package/src/rules/no-option-boolean-normalization.ts +89 -0
  61. package/src/rules/no-platform-globals.ts +117 -0
  62. package/src/rules/no-process-env-in-effect.ts +39 -0
  63. package/src/rules/no-process-env.ts +13 -0
  64. package/src/rules/no-random-in-effect.ts +39 -0
  65. package/src/rules/no-return-in-arrow.ts +77 -0
  66. package/src/rules/no-return-null.ts +47 -0
  67. package/src/rules/no-run-in-effect.ts +18 -0
  68. package/src/rules/no-runtime-run-fork.ts +14 -0
  69. package/src/rules/no-string-sentinel-const.ts +50 -0
  70. package/src/rules/no-switch-statement.ts +13 -0
  71. package/src/rules/no-ternary.ts +32 -0
  72. package/src/rules/no-throw-in-effect-gen.ts +42 -0
  73. package/src/rules/no-throw-statement.ts +13 -0
  74. package/src/rules/no-timers-in-effect.ts +47 -0
  75. package/src/rules/no-try-catch-in-effect-gen.ts +42 -0
  76. package/src/rules/no-try-catch.ts +14 -0
  77. package/src/rules/no-unnecessary-arrow-block.ts +52 -0
  78. package/src/rules/no-unnecessary-effect-gen.ts +85 -0
  79. package/src/rules/no-unnecessary-pipe.ts +46 -0
  80. package/src/vendor/effect-oxlint/AST.ts +516 -0
  81. package/src/vendor/effect-oxlint/Comment.ts +86 -0
  82. package/src/vendor/effect-oxlint/Diagnostic.ts +199 -0
  83. package/src/vendor/effect-oxlint/Plugin.ts +71 -0
  84. package/src/vendor/effect-oxlint/Rule.ts +590 -0
  85. package/src/vendor/effect-oxlint/RuleContext.ts +144 -0
  86. package/src/vendor/effect-oxlint/Scope.ts +177 -0
  87. package/src/vendor/effect-oxlint/SourceCode.ts +377 -0
  88. package/src/vendor/effect-oxlint/Testing.ts +1227 -0
  89. package/src/vendor/effect-oxlint/Token.ts +131 -0
  90. package/src/vendor/effect-oxlint/Visitor.ts +287 -0
  91. package/src/vendor/effect-oxlint/index.ts +88 -0
  92. package/tests/ban-rules.test.ts +252 -0
  93. package/tests/context-rules.test.ts +281 -0
  94. package/tests/pattern-rules.test.ts +258 -0
  95. package/tsconfig.json +15 -0
@@ -0,0 +1,590 @@
1
+ /**
2
+ * Core rule builder for Effect-first oxlint rules.
3
+ *
4
+ * `Rule.define` is the primary entry point. It produces a standard
5
+ * `CreateRule` that oxlint can consume, while letting rule authors
6
+ * write fully effectful create generators and visitor handlers.
7
+ *
8
+ * @since 0.1.0
9
+ */
10
+ import type { CreateRule, ESTree, RuleDocs, RuleMeta } from '@oxlint/plugins';
11
+ import * as Arr from 'effect/Array';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as Option from 'effect/Option';
14
+ import * as P from 'effect/Predicate';
15
+ import * as Schema from 'effect/Schema';
16
+
17
+ import { pipe } from 'effect/Function';
18
+
19
+ import * as AST from './AST.ts';
20
+ import { make as makeDiagnostic } from './Diagnostic.ts';
21
+ import { fromOxlintContext, RuleContext } from './RuleContext.ts';
22
+ import type { EffectVisitor } from './Visitor.ts';
23
+ import { merge as mergeVisitors, toOxlintVisitor } from './Visitor.ts';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Internal helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Extract the identifier name from a `NewExpression` callee.
31
+ *
32
+ * `AST.calleeName` only accepts `CallExpression`. This mirrors the
33
+ * same logic for `NewExpression.callee`.
34
+ *
35
+ * @internal
36
+ */
37
+ const newExprCalleeName = (callee: ESTree.Expression): Option.Option<string> =>
38
+ callee.type === 'Identifier' && 'name' in callee && P.isString(callee.name)
39
+ ? Option.some(callee.name)
40
+ : Option.none();
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Types
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Configuration object for `Rule.define`.
48
+ *
49
+ * @since 0.1.0
50
+ */
51
+ export interface RuleConfig<Options = undefined> {
52
+ /** Rule name (used for tracing spans). */
53
+ readonly name: string;
54
+ /** Oxlint rule metadata. */
55
+ readonly meta: RuleMeta;
56
+ /**
57
+ * Optional Schema for rule options.
58
+ *
59
+ * When provided, the first element of the raw JSON options array
60
+ * is decoded at `create` time against this schema.
61
+ */
62
+ readonly options?: Schema.Decoder<Options> | undefined;
63
+ /**
64
+ * The create generator.
65
+ *
66
+ * Receives decoded options and returns an `EffectVisitor`.
67
+ * Runs inside an Effect context where `RuleContext` is available.
68
+ *
69
+ * May `yield* Ref.make(...)` for state, `yield* RuleContext` for
70
+ * context access, and return a visitor built with `Visitor.*` helpers.
71
+ */
72
+ readonly create: (
73
+ options: Options
74
+ ) => Effect.gen.Return<EffectVisitor, never, RuleContext>;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Builder
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Define an Effect-first oxlint lint rule.
83
+ *
84
+ * The `create` generator runs once per file via `Effect.runSync`.
85
+ * Each visitor handler is also executed via `Effect.runSync` per node.
86
+ * `Ref`-based state created in `create` persists across handler calls
87
+ * via closure.
88
+ *
89
+ * `Effect.runSync` is used here at the runtime boundary — the bridge
90
+ * between oxlint's synchronous plugin API and our Effect world.
91
+ *
92
+ * @since 0.1.0
93
+ */
94
+ export const define = <Options = undefined>(
95
+ config: RuleConfig<Options>
96
+ ): CreateRule => ({
97
+ meta: config.meta,
98
+ create(oxlintContext) {
99
+ const ruleCtx = fromOxlintContext(oxlintContext);
100
+
101
+ // Runtime boundary: execute effects with RuleContext provided.
102
+ // This is the FFI bridge between oxlint's sync API and Effect.
103
+ const run = <A>(effect: Effect.Effect<A, never, RuleContext>): A =>
104
+ Effect.runSync(Effect.provideService(effect, RuleContext, ruleCtx));
105
+
106
+ // Decode options from the raw JSON array.
107
+ // When no schema is configured, `Options` defaults to `undefined`.
108
+ const decodeOptions = (): Options => {
109
+ const schema = config.options;
110
+ if (schema === undefined) return undefined as Options;
111
+ return Schema.decodeUnknownSync(schema)(oxlintContext.options[0]);
112
+ };
113
+ const options = run(Effect.sync(decodeOptions));
114
+
115
+ // Run the create generator to set up Refs and get the visitor map.
116
+ // TypedEffectVisitor → EffectVisitor: the typed keys provide
117
+ // narrowed nodes to callers, but at runtime all handlers receive
118
+ // the same ESTree.Node values. The variance mismatch is safe
119
+ // because oxlint guarantees the node type matches the key.
120
+ const effectVisitor = run(
121
+ Effect.gen(() => config.create(options))
122
+ ) as EffectVisitor;
123
+
124
+ // Wrap each handler: Effect<void> → plain () => void
125
+ return toOxlintVisitor(effectVisitor, run);
126
+ }
127
+ });
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Metadata helper
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * Build `RuleMeta` with sensible defaults.
135
+ *
136
+ * @since 0.1.0
137
+ */
138
+ export const meta = (opts: {
139
+ readonly type: 'problem' | 'suggestion' | 'layout';
140
+ readonly description: string;
141
+ readonly fixable?: 'code' | 'whitespace' | undefined;
142
+ readonly hasSuggestions?: boolean | undefined;
143
+ readonly messages?: Record<string, string> | undefined;
144
+ readonly docs?: RuleDocs | undefined;
145
+ }): RuleMeta => ({
146
+ type: opts.type,
147
+ ...(opts.fixable !== undefined ? { fixable: opts.fixable } : {}),
148
+ ...(opts.hasSuggestions !== undefined
149
+ ? { hasSuggestions: opts.hasSuggestions }
150
+ : {}),
151
+ ...(opts.messages !== undefined ? { messages: opts.messages } : {}),
152
+ docs: {
153
+ description: opts.description,
154
+ ...opts.docs
155
+ }
156
+ });
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Convenience rule factories (common patterns)
160
+ // ---------------------------------------------------------------------------
161
+
162
+ /**
163
+ * Create a rule that bans `obj.prop` member expression access.
164
+ *
165
+ * Replaces the common `memberExprRule` utility pattern.
166
+ *
167
+ * @since 0.1.0
168
+ */
169
+ export const banMember = (
170
+ obj: string,
171
+ prop: string | ReadonlyArray<string>,
172
+ opts: {
173
+ readonly message: string;
174
+ readonly meta?:
175
+ | { readonly type?: 'problem' | 'suggestion' }
176
+ | undefined;
177
+ }
178
+ ): CreateRule =>
179
+ define({
180
+ name: `ban-${obj}-${P.isString(prop) ? prop : Arr.join(prop, '-')}`,
181
+ meta: meta({
182
+ type: opts.meta?.type ?? 'suggestion',
183
+ description: opts.message
184
+ }),
185
+ create: function* () {
186
+ const ctx = yield* RuleContext;
187
+ return {
188
+ MemberExpression: (node: ESTree.Node) =>
189
+ pipe(
190
+ AST.narrow(node, 'MemberExpression'),
191
+ Option.flatMap(AST.matchMember(obj, prop)),
192
+ Option.match({
193
+ onNone: () => Effect.void,
194
+ onSome: (matched) =>
195
+ ctx.report(
196
+ makeDiagnostic({
197
+ node: matched,
198
+ message: opts.message
199
+ })
200
+ )
201
+ })
202
+ )
203
+ };
204
+ }
205
+ });
206
+
207
+ /**
208
+ * Create a rule that bans imports matching a source string or predicate.
209
+ *
210
+ * Replaces the common `importRule` utility pattern.
211
+ *
212
+ * @since 0.1.0
213
+ */
214
+ export const banImport = (
215
+ source: string | ((source: string) => boolean),
216
+ opts: {
217
+ readonly message: string;
218
+ readonly meta?:
219
+ | { readonly type?: 'problem' | 'suggestion' }
220
+ | undefined;
221
+ }
222
+ ): CreateRule =>
223
+ define({
224
+ name: 'ban-import',
225
+ meta: meta({
226
+ type: opts.meta?.type ?? 'suggestion',
227
+ description: opts.message
228
+ }),
229
+ create: function* () {
230
+ const ctx = yield* RuleContext;
231
+ return {
232
+ ImportDeclaration: (node: ESTree.Node) =>
233
+ pipe(
234
+ AST.narrow(node, 'ImportDeclaration'),
235
+ Option.flatMap(AST.matchImport(source)),
236
+ Option.match({
237
+ onNone: () => Effect.void,
238
+ onSome: (matched) =>
239
+ ctx.report(
240
+ makeDiagnostic({
241
+ node: matched,
242
+ message: opts.message
243
+ })
244
+ )
245
+ })
246
+ )
247
+ };
248
+ }
249
+ });
250
+
251
+ /**
252
+ * Create a rule that bans bare identifier call expressions.
253
+ *
254
+ * Matches `CallExpression` nodes whose callee is an identifier in
255
+ * the given list (e.g. `fetch()`, `useState()`, `readFileSync()`).
256
+ *
257
+ * @example
258
+ * ```ts
259
+ * // Ban a single call
260
+ * Rule.banCallOf('fetch', { message: 'Use Effect HTTP client' })
261
+ *
262
+ * // Ban multiple calls
263
+ * Rule.banCallOf(['useState', 'useEffect'], { message: 'Use Effect' })
264
+ * ```
265
+ *
266
+ * @since 0.2.0
267
+ */
268
+ export const banCallOf = (
269
+ name: string | ReadonlyArray<string>,
270
+ opts: {
271
+ readonly message: string;
272
+ readonly meta?:
273
+ | { readonly type?: 'problem' | 'suggestion' }
274
+ | undefined;
275
+ }
276
+ ): CreateRule => {
277
+ const names = P.isString(name) ? [name] : name;
278
+ return define({
279
+ name: `ban-call-${Arr.join(names, '-')}`,
280
+ meta: meta({
281
+ type: opts.meta?.type ?? 'suggestion',
282
+ description: opts.message
283
+ }),
284
+ create: function* () {
285
+ const ctx = yield* RuleContext;
286
+ return {
287
+ CallExpression: (node: ESTree.Node) =>
288
+ pipe(
289
+ AST.narrow(node, 'CallExpression'),
290
+ Option.flatMap(AST.calleeName),
291
+ Option.filter((n) => Arr.contains(names, n)),
292
+ Option.match({
293
+ onNone: () => Effect.void,
294
+ onSome: () =>
295
+ ctx.report(
296
+ makeDiagnostic({
297
+ node,
298
+ message: opts.message
299
+ })
300
+ )
301
+ })
302
+ )
303
+ };
304
+ }
305
+ });
306
+ };
307
+
308
+ /**
309
+ * Create a rule that bans `new` expressions with the given callee name.
310
+ *
311
+ * Matches `NewExpression` nodes whose callee is an identifier in
312
+ * the given list (e.g. `new Date()`, `new Error()`).
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * // Ban a single constructor
317
+ * Rule.banNewExpr('Date', { message: 'Use Clock service' })
318
+ *
319
+ * // Ban multiple constructors
320
+ * Rule.banNewExpr(['Error', 'TypeError'], { message: 'Use tagged errors' })
321
+ * ```
322
+ *
323
+ * @since 0.2.0
324
+ */
325
+ export const banNewExpr = (
326
+ name: string | ReadonlyArray<string>,
327
+ opts: {
328
+ readonly message: string;
329
+ readonly meta?:
330
+ | { readonly type?: 'problem' | 'suggestion' }
331
+ | undefined;
332
+ }
333
+ ): CreateRule => {
334
+ const names = P.isString(name) ? [name] : name;
335
+ return define({
336
+ name: `ban-new-${Arr.join(names, '-')}`,
337
+ meta: meta({
338
+ type: opts.meta?.type ?? 'suggestion',
339
+ description: opts.message
340
+ }),
341
+ create: function* () {
342
+ const ctx = yield* RuleContext;
343
+ return {
344
+ NewExpression: (node: ESTree.Node) =>
345
+ pipe(
346
+ AST.narrow(node, 'NewExpression'),
347
+ Option.flatMap((n) => newExprCalleeName(n.callee)),
348
+ Option.filter((n) => Arr.contains(names, n)),
349
+ Option.match({
350
+ onNone: () => Effect.void,
351
+ onSome: () =>
352
+ ctx.report(
353
+ makeDiagnostic({
354
+ node,
355
+ message: opts.message
356
+ })
357
+ )
358
+ })
359
+ )
360
+ };
361
+ }
362
+ });
363
+ };
364
+
365
+ /**
366
+ * Create a rule that bans a specific statement type.
367
+ *
368
+ * @since 0.1.0
369
+ */
370
+ export const banStatement = (
371
+ nodeType: string,
372
+ opts: {
373
+ readonly message: string;
374
+ readonly meta?:
375
+ | { readonly type?: 'problem' | 'suggestion' }
376
+ | undefined;
377
+ }
378
+ ): CreateRule =>
379
+ define({
380
+ name: `ban-${nodeType}`,
381
+ meta: meta({
382
+ type: opts.meta?.type ?? 'suggestion',
383
+ description: opts.message
384
+ }),
385
+ create: function* () {
386
+ const ctx = yield* RuleContext;
387
+ return {
388
+ [nodeType]: (node: ESTree.Node) =>
389
+ ctx.report(makeDiagnostic({ node, message: opts.message }))
390
+ };
391
+ }
392
+ });
393
+
394
+ // ---------------------------------------------------------------------------
395
+ // Multi-ban combinator
396
+ // ---------------------------------------------------------------------------
397
+
398
+ /**
399
+ * Specification for `banMultiple`: which patterns to ban under one rule.
400
+ *
401
+ * @since 0.2.0
402
+ */
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
+ }
421
+
422
+ /**
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.
427
+ *
428
+ * @example
429
+ * ```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
+ * )
458
+ * ```
459
+ *
460
+ * @since 0.2.0
461
+ */
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',
474
+ meta: meta({
475
+ type: opts.meta?.type ?? 'suggestion',
476
+ description: opts.message
477
+ }),
478
+ create: function* () {
479
+ 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
+
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
+ : [];
514
+
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
+ : [];
581
+
582
+ return mergeVisitors(
583
+ ...stmtVisitors,
584
+ ...callVisitors,
585
+ ...newExprVisitors,
586
+ ...memberVisitors,
587
+ ...importVisitors
588
+ );
589
+ }
590
+ });