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.
- package/.effect-lsp.json +60 -0
- package/.oxlintrc.json +33 -0
- package/package.json +51 -0
- package/scripts/add-rule.ts +87 -0
- package/scripts/codegen.ts +108 -0
- package/src/index.ts +9 -0
- package/src/plugin.ts +16 -0
- package/src/presets/core.ts +36 -0
- package/src/presets/effect-native.ts +41 -0
- package/src/presets/full.ts +84 -0
- package/src/presets/index.ts +3 -0
- package/src/rules/_effect-context.ts +28 -0
- package/src/rules/index.ts +83 -0
- package/src/rules/no-arrow-ladder.ts +71 -0
- package/src/rules/no-async-function.ts +48 -0
- package/src/rules/no-console-in-effect.ts +46 -0
- package/src/rules/no-date-in-effect.ts +61 -0
- package/src/rules/no-dynamic-import.ts +13 -0
- package/src/rules/no-effect-as.ts +14 -0
- package/src/rules/no-effect-async.ts +14 -0
- package/src/rules/no-effect-bind.ts +13 -0
- package/src/rules/no-effect-do.ts +14 -0
- package/src/rules/no-effect-fn-generator.ts +59 -0
- package/src/rules/no-effect-gen-adapter.ts +51 -0
- package/src/rules/no-effect-map-void.ts +81 -0
- package/src/rules/no-effect-never.ts +14 -0
- package/src/rules/no-effect-orElse-ladder.ts +80 -0
- package/src/rules/no-effect-succeed-string.ts +53 -0
- package/src/rules/no-effect-succeed-variable.ts +51 -0
- package/src/rules/no-effect-succeed-void.ts +64 -0
- package/src/rules/no-effect-sync-console.ts +92 -0
- package/src/rules/no-effect-sync-wrapper.ts +68 -0
- package/src/rules/no-effect-type-alias.ts +72 -0
- package/src/rules/no-effect-wrapper-alias.ts +98 -0
- package/src/rules/no-extends-native-error.ts +55 -0
- package/src/rules/no-fetch-in-effect.ts +47 -0
- package/src/rules/no-flatmap-ladder.ts +79 -0
- package/src/rules/no-fromnullable-coalesce.ts +63 -0
- package/src/rules/no-global-console.ts +17 -0
- package/src/rules/no-global-date.ts +20 -0
- package/src/rules/no-global-fetch.ts +14 -0
- package/src/rules/no-global-random.ts +13 -0
- package/src/rules/no-global-timers.ts +17 -0
- package/src/rules/no-if-statement.ts +16 -0
- package/src/rules/no-iife-wrapper.ts +52 -0
- package/src/rules/no-inline-runtime-provide.ts +42 -0
- package/src/rules/no-instanceof-schema.ts +43 -0
- package/src/rules/no-json-in-effect.ts +45 -0
- package/src/rules/no-json-parse.ts +18 -0
- package/src/rules/no-manual-effect-channels.ts +59 -0
- package/src/rules/no-match-effect-branch.ts +83 -0
- package/src/rules/no-match-void-branch.ts +73 -0
- package/src/rules/no-nested-effect-call.ts +59 -0
- package/src/rules/no-nested-effect-gen.ts +44 -0
- package/src/rules/no-nested-pipe.ts +76 -0
- package/src/rules/no-new-error.ts +17 -0
- package/src/rules/no-new-promise.ts +14 -0
- package/src/rules/no-node-builtin-import.ts +70 -0
- package/src/rules/no-option-as.ts +13 -0
- package/src/rules/no-option-boolean-normalization.ts +89 -0
- package/src/rules/no-platform-globals.ts +117 -0
- package/src/rules/no-process-env-in-effect.ts +39 -0
- package/src/rules/no-process-env.ts +13 -0
- package/src/rules/no-random-in-effect.ts +39 -0
- package/src/rules/no-return-in-arrow.ts +77 -0
- package/src/rules/no-return-null.ts +47 -0
- package/src/rules/no-run-in-effect.ts +18 -0
- package/src/rules/no-runtime-run-fork.ts +14 -0
- package/src/rules/no-string-sentinel-const.ts +50 -0
- package/src/rules/no-switch-statement.ts +13 -0
- package/src/rules/no-ternary.ts +32 -0
- package/src/rules/no-throw-in-effect-gen.ts +42 -0
- package/src/rules/no-throw-statement.ts +13 -0
- package/src/rules/no-timers-in-effect.ts +47 -0
- package/src/rules/no-try-catch-in-effect-gen.ts +42 -0
- package/src/rules/no-try-catch.ts +14 -0
- package/src/rules/no-unnecessary-arrow-block.ts +52 -0
- package/src/rules/no-unnecessary-effect-gen.ts +85 -0
- package/src/rules/no-unnecessary-pipe.ts +46 -0
- package/src/vendor/effect-oxlint/AST.ts +516 -0
- package/src/vendor/effect-oxlint/Comment.ts +86 -0
- package/src/vendor/effect-oxlint/Diagnostic.ts +199 -0
- package/src/vendor/effect-oxlint/Plugin.ts +71 -0
- package/src/vendor/effect-oxlint/Rule.ts +590 -0
- package/src/vendor/effect-oxlint/RuleContext.ts +144 -0
- package/src/vendor/effect-oxlint/Scope.ts +177 -0
- package/src/vendor/effect-oxlint/SourceCode.ts +377 -0
- package/src/vendor/effect-oxlint/Testing.ts +1227 -0
- package/src/vendor/effect-oxlint/Token.ts +131 -0
- package/src/vendor/effect-oxlint/Visitor.ts +287 -0
- package/src/vendor/effect-oxlint/index.ts +88 -0
- package/tests/ban-rules.test.ts +252 -0
- package/tests/context-rules.test.ts +281 -0
- package/tests/pattern-rules.test.ts +258 -0
- 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
|
+
});
|