@commonplaces/oxlint-plugin 0.0.0-stage → 0.1.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -2
  3. package/dist/alchemy.d.ts +2 -0
  4. package/dist/alchemy.js +196 -0
  5. package/dist/anti-slop/effect/index.d.ts +3 -0
  6. package/dist/anti-slop/effect/index.js +10 -0
  7. package/dist/anti-slop/effect/rules/no-service-constructor-imports.d.ts +2 -0
  8. package/dist/anti-slop/effect/rules/no-service-constructor-imports.js +45 -0
  9. package/dist/anti-slop/index.d.ts +3 -0
  10. package/dist/anti-slop/index.js +38 -0
  11. package/dist/anti-slop/rules/no-chained-type-assertions.d.ts +2 -0
  12. package/dist/anti-slop/rules/no-chained-type-assertions.js +57 -0
  13. package/dist/anti-slop/rules/no-conditional-empty-object-spread.d.ts +2 -0
  14. package/dist/anti-slop/rules/no-conditional-empty-object-spread.js +36 -0
  15. package/dist/anti-slop/rules/no-known-value-widening.d.ts +2 -0
  16. package/dist/anti-slop/rules/no-known-value-widening.js +171 -0
  17. package/dist/anti-slop/rules/no-module-mocking.d.ts +2 -0
  18. package/dist/anti-slop/rules/no-module-mocking.js +73 -0
  19. package/dist/anti-slop/rules/no-object-parameters.d.ts +2 -0
  20. package/dist/anti-slop/rules/no-object-parameters.js +86 -0
  21. package/dist/anti-slop/rules/no-reflect-apply.d.ts +2 -0
  22. package/dist/anti-slop/rules/no-reflect-apply.js +25 -0
  23. package/dist/anti-slop/rules/no-reflect-get.d.ts +2 -0
  24. package/dist/anti-slop/rules/no-reflect-get.js +25 -0
  25. package/dist/anti-slop/rules/no-runtime-typeof.d.ts +2 -0
  26. package/dist/anti-slop/rules/no-runtime-typeof.js +47 -0
  27. package/dist/anti-slop/rules/no-shape-in-symbol-names.d.ts +2 -0
  28. package/dist/anti-slop/rules/no-shape-in-symbol-names.js +33 -0
  29. package/dist/anti-slop/rules/no-unknown-parameters.d.ts +2 -0
  30. package/dist/anti-slop/rules/no-unknown-parameters.js +69 -0
  31. package/dist/anti-slop/rules/no-unknown-returns.d.ts +2 -0
  32. package/dist/anti-slop/rules/no-unknown-returns.js +77 -0
  33. package/dist/anti-slop/rules/no-unknown-type-aliases.d.ts +2 -0
  34. package/dist/anti-slop/rules/no-unknown-type-aliases.js +58 -0
  35. package/dist/anti-slop/rules/no-unsafe-dictionary-type.d.ts +2 -0
  36. package/dist/anti-slop/rules/no-unsafe-dictionary-type.js +111 -0
  37. package/dist/anti-slop/rules/no-widen-then-assert.d.ts +2 -0
  38. package/dist/anti-slop/rules/no-widen-then-assert.js +255 -0
  39. package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.d.ts +2 -0
  40. package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.js +51 -0
  41. package/dist/anti-slop/shared/dictionary-types.d.ts +21 -0
  42. package/dist/anti-slop/shared/dictionary-types.js +374 -0
  43. package/dist/anti-slop/shared/lexical-type-parameters.d.ts +3 -0
  44. package/dist/anti-slop/shared/lexical-type-parameters.js +25 -0
  45. package/dist/anti-slop/shared/reflect-method.d.ts +3 -0
  46. package/dist/anti-slop/shared/reflect-method.js +24 -0
  47. package/dist/bun.d.ts +9 -0
  48. package/dist/bun.js +30 -0
  49. package/dist/effect.d.ts +17 -0
  50. package/dist/effect.js +352 -0
  51. package/dist/index.d.ts +57 -0
  52. package/dist/index.js +62 -0
  53. package/dist/package.d.ts +9 -0
  54. package/dist/package.js +50 -0
  55. package/dist/strict-effect.d.ts +2 -0
  56. package/dist/strict-effect.js +2381 -0
  57. package/dist/syntax.d.ts +5 -0
  58. package/dist/syntax.js +26 -0
  59. package/package.json +77 -4
@@ -0,0 +1,2381 @@
1
+ import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
2
+ import * as Array from "effect/Array";
3
+ import * as Function from "effect/Function";
4
+ import * as Option from "effect/Option";
5
+ import * as Predicate from "effect/Predicate";
6
+ import * as Record from "effect/Record";
7
+ import * as Schema from "effect/Schema";
8
+ // Resolve the nearest declaration, so a parameter or block-local name cannot
9
+ // borrow a constant or import from an outer scope.
10
+ const scopedVariable = (scope, name) => Array.findFirst((variable) => variable.name === name)(scope.variables).pipe(Option.orElse(() => Option.fromNullishOr(scope.upper).pipe(Option.flatMap((upper) => scopedVariable(upper, name)))));
11
+ // The one immutable `const` declaration among the definitions, never reassigned.
12
+ const constantDeclaration = (variable, definitions = variable.defs) => Array.head(definitions).pipe(Option.filter(() => definitions.length === 1), Option.filter(() => Array.every((reference) => !reference.isWrite() || reference.init)(variable.references)), Option.flatMap((definition) => {
13
+ if (definition.node.type === "VariableDeclarator" &&
14
+ definition.parent?.type === "VariableDeclaration" &&
15
+ definition.parent.kind === "const") {
16
+ return Option.some(definition.node);
17
+ }
18
+ return Option.none();
19
+ }));
20
+ // Only immutable local strings are safe to resolve. Unknown names fail closed
21
+ // at the owning rule; no code is evaluated to discover an import address.
22
+ const moduleAddress = (context, source, visited = []) => {
23
+ if (Array.some(visited, (previous) => previous === source)) {
24
+ return Option.none();
25
+ }
26
+ const next = [...visited, source];
27
+ if (source.type === "Literal" && Predicate.isString(source.value)) {
28
+ return Option.some(source.value);
29
+ }
30
+ if (source.type === "TemplateLiteral") {
31
+ const quasis = Array.map((quasi) => Option.fromNullishOr(quasi.value.cooked))(source.quasis);
32
+ const expressions = [
33
+ ...Array.map((expression) => moduleAddress(context, expression, next))(source.expressions),
34
+ Option.some(""),
35
+ ];
36
+ return Function.pipe(Array.zipWith(expressions, (quasi, expression) => Option.all([quasi, expression]).pipe(Option.map(([text, value]) => text + value)))(quasis), Option.all, Option.map(Array.join("")));
37
+ }
38
+ if (source.type === "BinaryExpression" && source.operator === "+") {
39
+ return Option.all([
40
+ moduleAddress(context, source.left, next),
41
+ moduleAddress(context, source.right, next),
42
+ ]).pipe(Option.map(([left, right]) => left + right));
43
+ }
44
+ if (source.type === "Identifier") {
45
+ const scope = context.sourceCode.getScope(source);
46
+ return scopedVariable(scope, source.name).pipe(Option.flatMap(constantDeclaration), Option.filter((declaration) => declaration.id.type === "Identifier"), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.flatMap((initialiser) => moduleAddress(context, initialiser, next)));
47
+ }
48
+ return Option.none();
49
+ };
50
+ const typeOnlyImport = (node) => node.importKind === "type" ||
51
+ (node.specifiers.length > 0 &&
52
+ Array.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type")(node.specifiers));
53
+ // Ownership checks inspect module operations, never harmless strings in config.
54
+ const moduleVisitors = (check, allowTypes) => ({
55
+ ImportDeclaration(node) {
56
+ if (!allowTypes || !typeOnlyImport(node)) {
57
+ check(node, node.source);
58
+ }
59
+ },
60
+ ImportExpression(node) {
61
+ check(node, node.source);
62
+ },
63
+ ExportNamedDeclaration(node) {
64
+ const onlyTypes = node.exportKind === "type" ||
65
+ (node.specifiers.length > 0 &&
66
+ Array.every((specifier) => specifier.exportKind === "type")(node.specifiers));
67
+ if (!allowTypes || !onlyTypes) {
68
+ Option.match(Option.fromNullishOr(node.source), {
69
+ onNone: Function.constVoid,
70
+ onSome: (source) => {
71
+ check(node, source);
72
+ },
73
+ });
74
+ }
75
+ },
76
+ ExportAllDeclaration(node) {
77
+ if (!allowTypes || node.exportKind !== "type") {
78
+ check(node, node.source);
79
+ }
80
+ },
81
+ TSImportType(node) {
82
+ if (!allowTypes) {
83
+ check(node, node.source);
84
+ }
85
+ },
86
+ TSImportEqualsDeclaration(node) {
87
+ if ((!allowTypes || node.importKind !== "type") &&
88
+ node.moduleReference.type === "TSExternalModuleReference") {
89
+ check(node, node.moduleReference.expression);
90
+ }
91
+ },
92
+ });
93
+ // Derived from DAW's owned rules at e47ecfb; the npm package is the portable rule owner.
94
+ const RuntimeOptions = Schema.Struct({ webSourceRoots: Schema.Array(Schema.String) });
95
+ const ModuleOwner = Schema.Struct({
96
+ module: Schema.String,
97
+ allowedFiles: Schema.Array(Schema.String),
98
+ allowTypes: Schema.Boolean,
99
+ });
100
+ const ModuleOwnershipOptions = Schema.Struct({ owners: Schema.Array(ModuleOwner) });
101
+ const moduleOwnership = defineRule({
102
+ meta: {
103
+ type: "problem",
104
+ schema: [
105
+ {
106
+ type: "object",
107
+ properties: {
108
+ owners: {
109
+ type: "array",
110
+ items: {
111
+ type: "object",
112
+ properties: {
113
+ module: { type: "string", minLength: 1 },
114
+ allowedFiles: { type: "array", items: { type: "string" } },
115
+ allowTypes: { type: "boolean" },
116
+ },
117
+ required: ["module", "allowedFiles", "allowTypes"],
118
+ additionalProperties: false,
119
+ },
120
+ },
121
+ },
122
+ required: ["owners"],
123
+ additionalProperties: false,
124
+ },
125
+ ],
126
+ messages: {
127
+ ownership: "Keep {{module}} imports in their exact configured owners. Use the checked service elsewhere.",
128
+ unknown: "Use a known immutable local string for dynamic imports so module ownership can be checked.",
129
+ },
130
+ },
131
+ create(context) {
132
+ const owners = Function.pipe(Array.head(context.options), Option.flatMap(Schema.decodeUnknownOption(ModuleOwnershipOptions)), Option.map((options) => options.owners), Option.getOrElse(() => []));
133
+ const check = (node, source, onlyTypes) => {
134
+ Option.match(moduleAddress(context, source), {
135
+ onNone: () => {
136
+ context.report({ node, messageId: "unknown" });
137
+ },
138
+ onSome: (address) => {
139
+ Array.forEach(owners, (owner) => {
140
+ if ((address === owner.module || address.startsWith(`${owner.module}/`)) &&
141
+ !(onlyTypes && owner.allowTypes) &&
142
+ !Array.some(owner.allowedFiles, (file) => exactOwnedFile(context.filename, context.cwd, file)))
143
+ context.report({ node, messageId: "ownership", data: { module: owner.module } });
144
+ });
145
+ },
146
+ });
147
+ };
148
+ return {
149
+ ...moduleVisitors((node, source) => check(node, source, false), true),
150
+ ImportDeclaration(node) {
151
+ check(node, node.source, typeOnlyImport(node));
152
+ },
153
+ ExportNamedDeclaration(node) {
154
+ const onlyTypes = node.exportKind === "type" ||
155
+ (node.specifiers.length > 0 &&
156
+ Array.every(node.specifiers, (specifier) => specifier.exportKind === "type"));
157
+ Option.match(Option.fromNullishOr(node.source), {
158
+ onNone: Function.constVoid,
159
+ onSome: (source) => check(node, source, onlyTypes),
160
+ });
161
+ },
162
+ ExportAllDeclaration(node) {
163
+ check(node, node.source, node.exportKind === "type");
164
+ },
165
+ TSImportType(node) {
166
+ check(node, node.source, true);
167
+ },
168
+ TSImportEqualsDeclaration(node) {
169
+ if (node.moduleReference.type === "TSExternalModuleReference")
170
+ check(node, node.moduleReference.expression, node.importKind === "type");
171
+ },
172
+ };
173
+ },
174
+ });
175
+ const noUncheckedIndex = defineRule({
176
+ meta: {
177
+ type: "problem",
178
+ schema: [],
179
+ messages: {
180
+ unchecked: "Use Effect Array.get or Record.get and handle the Option result. Unchecked bracket lookups are forbidden.",
181
+ },
182
+ },
183
+ create(context) {
184
+ return {
185
+ MemberExpression(node) {
186
+ if (node.computed) {
187
+ context.report({ node, messageId: "unchecked" });
188
+ }
189
+ },
190
+ };
191
+ },
192
+ });
193
+ const noNativeAt = defineRule({
194
+ meta: {
195
+ type: "problem",
196
+ schema: [],
197
+ messages: {
198
+ nativeAt: "Use Effect Array.get and handle its Option result instead of native .at().",
199
+ },
200
+ },
201
+ create(context) {
202
+ return {
203
+ MemberExpression(node) {
204
+ if (node.property.type === "Identifier" && node.property.name === "at") {
205
+ context.report({ node, messageId: "nativeAt" });
206
+ }
207
+ },
208
+ };
209
+ },
210
+ });
211
+ const runtimeFileConvention = defineRule({
212
+ meta: {
213
+ type: "problem",
214
+ schema: [
215
+ {
216
+ type: "object",
217
+ properties: { webSourceRoots: { type: "array", items: { type: "string" } } },
218
+ required: ["webSourceRoots"],
219
+ additionalProperties: false,
220
+ },
221
+ ],
222
+ messages: {
223
+ runtimeFile: "Name web Effect runtime modules runtime.server.ts or runtime.client.ts so TanStack checks the environment boundary.",
224
+ },
225
+ },
226
+ create(context) {
227
+ const filename = context.filename.replaceAll("\\", "/");
228
+ const roots = Function.pipe(Array.head(context.options), Option.flatMap(Schema.decodeUnknownOption(RuntimeOptions)), Option.map((options) => options.webSourceRoots), Option.getOrElse(() => ["apps/web/src"]));
229
+ return {
230
+ Program(node) {
231
+ if (Array.some(roots, (root) => filename.includes(`/${root.replaceAll("\\", "/").replace(/\/$/u, "")}/`)) &&
232
+ /(?:^|\/)(?:runtime|[^/]+[.-]runtime)\.(?:ts|tsx)$/u.test(filename)) {
233
+ context.report({ node, messageId: "runtimeFile" });
234
+ }
235
+ },
236
+ };
237
+ },
238
+ });
239
+ // Effect's error factories are called without `new` and return a class to extend.
240
+ const effectErrorFactories = [
241
+ ["effect/Schema", "TaggedError"],
242
+ ["effect/Schema", "Error"],
243
+ ["effect/Data", "TaggedError"],
244
+ ];
245
+ // The name test used by unicorn/throw-new-error: Error, TypeError, URIError, CustomError and so on.
246
+ const errorName = /^(?:[A-Z][\da-z]*)*Error$/u;
247
+ const exportedName = (name) => {
248
+ if (name.type === "Literal") {
249
+ return name.value;
250
+ }
251
+ return name.name;
252
+ };
253
+ const isEffectSource = (source) => source === "effect" ||
254
+ source.startsWith("effect/") ||
255
+ source === "alchemy" ||
256
+ source.startsWith("alchemy/");
257
+ const moduleReference = (module) => ({ module, name: Option.none() });
258
+ // A member of the root `effect` module is itself a module, such as `effect/Schema`. A member of any other module is an export.
259
+ const memberReference = (reference, property) => {
260
+ if (Option.isSome(reference.name)) {
261
+ return Option.none();
262
+ }
263
+ if (reference.module === "effect" ||
264
+ reference.module === "alchemy" ||
265
+ reference.module === "effect/reactivity" ||
266
+ reference.module === "effect/rpc" ||
267
+ reference.module === "effect/http" ||
268
+ reference.module === "effect/http-api") {
269
+ return Option.some(moduleReference(`${reference.module}/${property}`));
270
+ }
271
+ return Option.some({ module: reference.module, name: Option.some(property) });
272
+ };
273
+ const specifierBinding = (source, specifier) => {
274
+ if (specifier.type === "ImportDefaultSpecifier") {
275
+ return Option.none();
276
+ }
277
+ const local = specifier.local.name;
278
+ if (specifier.type === "ImportNamespaceSpecifier") {
279
+ return Option.some({ local, reference: moduleReference(source) });
280
+ }
281
+ if (specifier.importKind === "type") {
282
+ return Option.none();
283
+ }
284
+ const module = moduleReference(source);
285
+ const imported = exportedName(specifier.imported);
286
+ return Function.pipe(memberReference(module, imported), Option.map((reference) => ({ local, reference })));
287
+ };
288
+ const effectBindings = (program) => Function.pipe(program.body, Array.flatMap((statement) => {
289
+ if (statement.type !== "ImportDeclaration" ||
290
+ statement.importKind === "type" ||
291
+ !isEffectSource(statement.source.value)) {
292
+ return [];
293
+ }
294
+ return Function.pipe(statement.specifiers, Array.flatMap((specifier) => Option.toArray(specifierBinding(statement.source.value, specifier))));
295
+ }));
296
+ // Follows an identifier or a chain of static member reads, such as `Effect.Schema.TaggedError`, back to its Effect import.
297
+ const resolveReference = (bindings, expression) => {
298
+ if (expression.type === "Identifier") {
299
+ return Function.pipe(bindings, Array.findFirst((binding) => binding.local === expression.name), Option.map((binding) => binding.reference));
300
+ }
301
+ if (expression.type !== "MemberExpression" ||
302
+ expression.computed ||
303
+ expression.property.type !== "Identifier") {
304
+ return Option.none();
305
+ }
306
+ const property = expression.property.name;
307
+ return Function.pipe(resolveReference(bindings, expression.object), Option.flatMap((reference) => memberReference(reference, property)));
308
+ };
309
+ const isExport = (reference, module, name) => reference.module === module && Option.contains(reference.name, name);
310
+ const selfTypeName = (type) => {
311
+ if (type.type === "TSTypeReference" && type.typeName.type === "Identifier") {
312
+ return Option.some(type.typeName.name);
313
+ }
314
+ return Option.none();
315
+ };
316
+ const literalTag = (argument) => {
317
+ if (argument.type === "Literal") {
318
+ return Option.liftPredicate(argument.value, Predicate.isString);
319
+ }
320
+ return Option.none();
321
+ };
322
+ const missing = (name) => Option.getOrElse(name, () => "<missing>");
323
+ // Match the installed RC factory through its actual import.
324
+ const taggedErrorName = defineRule({
325
+ meta: {
326
+ type: "problem",
327
+ schema: [],
328
+ messages: {
329
+ mismatch: "Give a Schema.TaggedError class, its self type and its literal tag the same name. Found class {{className}}, self type {{selfTypeName}} and tag {{tagName}}.",
330
+ },
331
+ },
332
+ create(context) {
333
+ const check = (node) => {
334
+ const { superClass } = node;
335
+ if (superClass?.type !== "CallExpression") {
336
+ return;
337
+ }
338
+ const factory = superClass.callee;
339
+ if (factory.type !== "CallExpression") {
340
+ return;
341
+ }
342
+ const bindings = effectBindings(context.sourceCode.ast);
343
+ const isSchemaTaggedError = Function.pipe(resolveReference(bindings, factory.callee), Option.exists((reference) => isExport(reference, "effect/Schema", "TaggedError")));
344
+ if (!isSchemaTaggedError) {
345
+ return;
346
+ }
347
+ const className = Function.pipe(Option.fromNullishOr(node.id), Option.map((id) => id.name));
348
+ const selfType = Function.pipe(Option.fromNullishOr(factory.typeArguments), Option.flatMap((typeArguments) => Array.head(typeArguments.params)), Option.flatMap(selfTypeName));
349
+ const tag = Function.pipe(Array.head(superClass.arguments), Option.flatMap(literalTag));
350
+ const agrees = Function.pipe(Option.all([className, selfType, tag]), Option.exists(([name, self, literal]) => name === self && name === literal));
351
+ if (!agrees) {
352
+ context.report({
353
+ node,
354
+ messageId: "mismatch",
355
+ data: {
356
+ className: missing(className),
357
+ selfTypeName: missing(selfType),
358
+ tagName: missing(tag),
359
+ },
360
+ });
361
+ }
362
+ };
363
+ return { ClassDeclaration: check, ClassExpression: check };
364
+ },
365
+ });
366
+ const calleeName = (callee) => {
367
+ if (callee.type === "Identifier") {
368
+ return Option.some(callee.name);
369
+ }
370
+ if (callee.type === "MemberExpression" &&
371
+ !callee.computed &&
372
+ callee.property.type === "Identifier") {
373
+ return Option.some(callee.property.name);
374
+ }
375
+ return Option.none();
376
+ };
377
+ const isEffectErrorFactory = (reference) => Function.pipe(effectErrorFactories, Array.some(([module, name]) => isExport(reference, module, name)));
378
+ // Replaces unicorn/throw-new-error, whose Oxlint port reports every `*Error` call without `new`, including Effect's error factories.
379
+ const errorConstructorNew = defineRule({
380
+ meta: {
381
+ type: "problem",
382
+ schema: [],
383
+ messages: {
384
+ missingNew: "Construct {{name}} with `new`. Only Effect's Schema.TaggedError, Schema.Error and Data.TaggedError factories are called without it.",
385
+ },
386
+ },
387
+ create(context) {
388
+ return {
389
+ CallExpression(node) {
390
+ // Import bindings are read only for `*Error` calls, which are rare.
391
+ const isFactory = () => {
392
+ const bindings = effectBindings(context.sourceCode.ast);
393
+ return Function.pipe(resolveReference(bindings, node.callee), Option.exists(isEffectErrorFactory));
394
+ };
395
+ Function.pipe(calleeName(node.callee), Option.filter((name) => errorName.test(name) && !isFactory()), Option.match({
396
+ onNone: Function.constVoid,
397
+ onSome: (name) => {
398
+ context.report({ node, messageId: "missingNew", data: { name } });
399
+ },
400
+ }));
401
+ },
402
+ };
403
+ },
404
+ });
405
+ const BoundaryOptions = Schema.Struct({ allowedFiles: Schema.Array(Schema.String) });
406
+ const boundarySchema = [
407
+ {
408
+ type: "object",
409
+ properties: { allowedFiles: { type: "array", items: { type: "string" } } },
410
+ required: ["allowedFiles"],
411
+ additionalProperties: false,
412
+ },
413
+ ];
414
+ // Paths are exact, relative to the Oxlint working directory. Never allow a whole folder.
415
+ const isBoundary = (filename, cwd, options) => Function.pipe(Array.head(options), Option.flatMap(Schema.decodeUnknownOption(BoundaryOptions)), Option.exists(({ allowedFiles }) => Array.some(allowedFiles, (file) => !file.includes("*") &&
416
+ !file.includes("..") &&
417
+ filename.replaceAll("\\", "/") === `${cwd.replaceAll("\\", "/")}/${file}`)));
418
+ const isGlobalPromise = (expression) => (expression.type === "Identifier" && expression.name === "Promise") ||
419
+ (expression.type === "MemberExpression" &&
420
+ expression.object.type === "Identifier" &&
421
+ expression.object.name === "globalThis" &&
422
+ Option.contains(calleeName(expression), "Promise"));
423
+ const propertyName = (key) => {
424
+ if (key.type === "Identifier")
425
+ return Option.some(key.name);
426
+ if (key.type === "Literal" && Predicate.isString(key.value))
427
+ return Option.some(key.value);
428
+ return Option.none();
429
+ };
430
+ // Promise restrictions follow real imports, their aliases and local const
431
+ // aliases. Looking up the nearest declaration also avoids shadowing mistakes.
432
+ // These operations return a Layer when called in their data-first form.
433
+ const layerConstructors = [
434
+ ["fromBuild", 1],
435
+ ["fromBuildMemo", 1],
436
+ ["succeed", 2],
437
+ ["sync", 2],
438
+ ["effect", 2],
439
+ ["mock", 2],
440
+ ["succeedContext", 1],
441
+ ["syncContext", 1],
442
+ ["effectContext", 1],
443
+ ["effectDiscard", 1],
444
+ ["suspend", 1],
445
+ ["unwrap", 1],
446
+ ["mergeAll", 1],
447
+ ["merge", 2],
448
+ ["provide", 2],
449
+ ["provideMerge", 2],
450
+ ["fresh", 1],
451
+ ["catchTag", 3],
452
+ ["catchCause", 2],
453
+ ["flatMap", 2],
454
+ ["tap", 2],
455
+ ["tapError", 2],
456
+ ["tapCause", 2],
457
+ ["orDie", 1],
458
+ ["updateService", 3],
459
+ ["span", 1],
460
+ ["parentSpan", 1],
461
+ ["withParentSpan", 2],
462
+ ];
463
+ const layerTransforms = [
464
+ "provide",
465
+ "provideMerge",
466
+ "merge",
467
+ "fresh",
468
+ "catchTag",
469
+ "catchCause",
470
+ "flatMap",
471
+ "tap",
472
+ "tapError",
473
+ "tapCause",
474
+ "orDie",
475
+ "updateService",
476
+ "withSpan",
477
+ "withParentSpan",
478
+ "satisfiesSuccessType",
479
+ "satisfiesErrorType",
480
+ "satisfiesServicesType",
481
+ ];
482
+ const layerTypeReference = (context, name) => {
483
+ if (name.type === "TSQualifiedName") {
484
+ return layerTypeReference(context, name.left).pipe(Option.flatMap((reference) => memberReference(reference, name.right.name)));
485
+ }
486
+ if (name.type !== "Identifier") {
487
+ return Option.none();
488
+ }
489
+ const scope = context.sourceCode.getScope(name);
490
+ return scopedVariable(scope, name.name).pipe(Option.flatMap((variable) => Array.head(variable.defs)), Option.flatMap((definition) => {
491
+ if (definition.type !== "ImportBinding" ||
492
+ definition.parent?.type !== "ImportDeclaration" ||
493
+ !isEffectSource(definition.parent.source.value)) {
494
+ return Option.none();
495
+ }
496
+ const source = moduleReference(definition.parent.source.value);
497
+ if (definition.node.type === "ImportNamespaceSpecifier") {
498
+ return Option.some(source);
499
+ }
500
+ if (definition.node.type === "ImportSpecifier") {
501
+ const imported = exportedName(definition.node.imported);
502
+ return memberReference(source, imported);
503
+ }
504
+ return Option.none();
505
+ }));
506
+ };
507
+ const layerType = (context, type, visited = []) => {
508
+ if (Array.some(visited, (previous) => previous === type)) {
509
+ return false;
510
+ }
511
+ if (type.type === "TSUnionType" || type.type === "TSIntersectionType") {
512
+ return Array.some((member) => layerType(context, member, [...visited, type]))(type.types);
513
+ }
514
+ if (type.type !== "TSTypeReference") {
515
+ return false;
516
+ }
517
+ const reference = (name) => {
518
+ if (layerTypeReference(context, name).pipe(Option.exists((resolved) => isExport(resolved, "effect/Layer", "Layer")))) {
519
+ return true;
520
+ }
521
+ if (name.type !== "Identifier") {
522
+ return false;
523
+ }
524
+ const scope = context.sourceCode.getScope(name);
525
+ return scopedVariable(scope, name.name).pipe(Option.flatMap((variable) => Array.head(variable.defs)), Option.exists((definition) => {
526
+ if (definition.node.type === "TSTypeAliasDeclaration") {
527
+ return layerType(context, definition.node.typeAnnotation, [...visited, type]);
528
+ }
529
+ return false;
530
+ }));
531
+ };
532
+ return reference(type.typeName);
533
+ };
534
+ const layerExpression = (context, expression, visited = []) => {
535
+ if (Array.some(visited, (previous) => previous === expression)) {
536
+ return false;
537
+ }
538
+ const next = [...visited, expression];
539
+ if (expression.type === "Identifier") {
540
+ const scope = context.sourceCode.getScope(expression);
541
+ return (scopedEffectReference(context, expression).pipe(Option.exists((reference) => isExport(reference, "effect/Layer", "empty"))) ||
542
+ scopedVariable(scope, expression.name).pipe(Option.flatMap(constantDeclaration), Option.exists((declaration) => (declaration.id.type === "Identifier" &&
543
+ Option.fromNullishOr(declaration.id.typeAnnotation).pipe(Option.exists((annotation) => layerType(context, annotation.typeAnnotation)))) ||
544
+ Option.fromNullishOr(declaration.init).pipe(Option.exists((initialiser) => layerExpression(context, initialiser, next))))));
545
+ }
546
+ if (expression.type === "MemberExpression") {
547
+ return scopedEffectReference(context, expression).pipe(Option.exists((reference) => isExport(reference, "effect/Layer", "empty")));
548
+ }
549
+ if (expression.type === "ConditionalExpression") {
550
+ return (layerExpression(context, expression.consequent, next) ||
551
+ layerExpression(context, expression.alternate, next));
552
+ }
553
+ if (expression.type !== "CallExpression") {
554
+ return false;
555
+ }
556
+ const called = scopedEffectReference(context, expression.callee);
557
+ const constructor = called.pipe(Option.exists((reference) => Array.some(([name, count]) => isExport(reference, "effect/Layer", name) && expression.arguments.length >= count)(layerConstructors)));
558
+ const routes = called.pipe(Option.exists((reference) => Array.some((name) => isExport(reference, "effect/http/HttpRouter", name))([
559
+ "add",
560
+ "addAll",
561
+ "use",
562
+ "cors",
563
+ ])));
564
+ if (constructor || routes) {
565
+ return true;
566
+ }
567
+ // Curried constructors: Layer.succeed(Service)(implementation).
568
+ if (expression.callee.type === "CallExpression") {
569
+ const first = expression.callee;
570
+ return scopedEffectReference(context, first.callee).pipe(Option.exists((reference) => Array.some(([name, count]) => isExport(reference, "effect/Layer", name) &&
571
+ first.arguments.length + expression.arguments.length >= count)(layerConstructors)));
572
+ }
573
+ const transforms = (arguments_) => arguments_.length > 0 &&
574
+ Array.every((argument) => {
575
+ const calledExpression = () => {
576
+ if (argument.type === "CallExpression") {
577
+ return argument.callee;
578
+ }
579
+ return argument;
580
+ };
581
+ const callee = calledExpression();
582
+ return scopedEffectReference(context, callee).pipe(Option.exists((reference) => Array.some((name) => isExport(reference, "effect/Layer", name))(layerTransforms)));
583
+ })(arguments_);
584
+ if (called.pipe(Option.exists((reference) => isExport(reference, "effect/Function", "pipe")))) {
585
+ const tail = Array.drop(1)(expression.arguments);
586
+ if (Array.isReadonlyArrayEmpty(tail)) {
587
+ return Array.head(expression.arguments).pipe(Option.exists((argument) => layerExpression(context, argument, next)));
588
+ }
589
+ return transforms(tail);
590
+ }
591
+ if (expression.callee.type === "MemberExpression" &&
592
+ !expression.callee.computed &&
593
+ expression.callee.property.type === "Identifier" &&
594
+ expression.callee.property.name === "pipe") {
595
+ if (Array.isReadonlyArrayEmpty(expression.arguments)) {
596
+ return layerExpression(context, expression.callee.object, next);
597
+ }
598
+ return transforms(expression.arguments);
599
+ }
600
+ return false;
601
+ };
602
+ const returnsLayer = (context, statement) => {
603
+ if (statement.type === "ReturnStatement") {
604
+ return Option.fromNullishOr(statement.argument).pipe(Option.exists((argument) => layerExpression(context, argument)));
605
+ }
606
+ if (statement.type === "BlockStatement") {
607
+ return Array.some((child) => returnsLayer(context, child))(statement.body);
608
+ }
609
+ if (statement.type === "IfStatement") {
610
+ return (returnsLayer(context, statement.consequent) ||
611
+ Option.fromNullishOr(statement.alternate).pipe(Option.exists((alternate) => returnsLayer(context, alternate))));
612
+ }
613
+ if (statement.type === "SwitchStatement") {
614
+ return Array.some((branch) => Array.some((child) => returnsLayer(context, child))(branch.consequent))(statement.cases);
615
+ }
616
+ return false;
617
+ };
618
+ const staticLayers = defineRule({
619
+ meta: {
620
+ type: "problem",
621
+ schema: [],
622
+ messages: {
623
+ factory: "Declare a static Effect Layer. Supply dependencies through its Requirements type channel and replace Layers with Layer.succeed or Layer.mock in tests.",
624
+ },
625
+ },
626
+ create(context) {
627
+ const check = (node) => {
628
+ if (node.generator || node.async) {
629
+ return;
630
+ }
631
+ // Inline composition callbacks may return a Layer to Layer.unwrap,
632
+ // Layer.suspend, Effect.map, Atom.runtime or a matching operation.
633
+ const compositionCall = () => {
634
+ if (node.parent.type === "CallExpression") {
635
+ return Option.some(node.parent);
636
+ }
637
+ if (node.parent.type === "Property" &&
638
+ node.parent.parent.type === "ObjectExpression" &&
639
+ node.parent.parent.parent.type === "CallExpression") {
640
+ return Option.some(node.parent.parent.parent);
641
+ }
642
+ return Option.none();
643
+ };
644
+ if (compositionCall().pipe(Option.flatMap((call) => scopedEffectReference(context, call.callee)), Option.exists((reference) => Array.contains(reference.module)([
645
+ "effect/Layer",
646
+ "effect/Effect",
647
+ "effect/Match",
648
+ "effect/Option",
649
+ "effect/Result",
650
+ "effect/Boolean",
651
+ "effect/reactivity/Atom",
652
+ ])))) {
653
+ return;
654
+ }
655
+ // Native Service config accepts Layer callbacks at httpClient/protocol.
656
+ // Curried Service construction is resolved against the imported symbol.
657
+ if (node.parent.type === "Property" &&
658
+ (Option.contains(propertyName(node.parent.key), "httpClient") ||
659
+ Option.contains(propertyName(node.parent.key), "protocol")) &&
660
+ compositionCall().pipe(Option.filter((call) => call.callee.type === "CallExpression"), Option.flatMap((call) => call.callee.type === "CallExpression"
661
+ ? scopedEffectReference(context, call.callee.callee)
662
+ : Option.none()), Option.exists((reference) => (node.parent.type === "Property" &&
663
+ Option.contains(propertyName(node.parent.key), "httpClient") &&
664
+ isExport(reference, "effect/reactivity/AtomHttpApi", "Service")) ||
665
+ (node.parent.type === "Property" &&
666
+ Option.contains(propertyName(node.parent.key), "protocol") &&
667
+ isExport(reference, "effect/reactivity/AtomRpc", "Service")))))
668
+ return;
669
+ const typed = Option.fromNullishOr(node.returnType).pipe(Option.exists((annotation) => layerType(context, annotation.typeAnnotation)));
670
+ const returns = Option.fromNullishOr(node.body).pipe(Option.exists((body) => {
671
+ if (body.type === "BlockStatement") {
672
+ return returnsLayer(context, body);
673
+ }
674
+ return layerExpression(context, body);
675
+ }));
676
+ if (typed || returns) {
677
+ context.report({ node, messageId: "factory" });
678
+ }
679
+ };
680
+ return {
681
+ "ArrowFunctionExpression:exit": check,
682
+ "FunctionExpression:exit": check,
683
+ "FunctionDeclaration:exit": check,
684
+ TSFunctionType(node) {
685
+ if (layerType(context, node.returnType.typeAnnotation)) {
686
+ context.report({ node, messageId: "factory" });
687
+ }
688
+ },
689
+ };
690
+ },
691
+ });
692
+ const scopedImportedReference = (context, expression, visited = [], acceptsModule = Function.constTrue) => {
693
+ if (Array.some(visited, (previous) => previous === expression)) {
694
+ return Option.none();
695
+ }
696
+ const next = [...visited, expression];
697
+ if (expression.type === "MemberExpression" &&
698
+ (!expression.computed || expression.property.type === "Literal")) {
699
+ return propertyName(expression.property).pipe(Option.flatMap((name) => scopedImportedReference(context, expression.object, next, acceptsModule).pipe(Option.flatMap((reference) => memberReference(reference, name)))));
700
+ }
701
+ if (expression.type !== "Identifier") {
702
+ return Option.none();
703
+ }
704
+ const { name } = expression;
705
+ const scope = context.sourceCode.getScope(expression);
706
+ return scopedVariable(scope, name).pipe(Option.flatMap((variable) => Array.head(variable.defs).pipe(Option.filter(() => variable.defs.length === 1), Option.flatMap((definition) => {
707
+ if (definition.type === "ImportBinding" &&
708
+ definition.parent?.type === "ImportDeclaration" &&
709
+ definition.parent.importKind !== "type" &&
710
+ acceptsModule(definition.parent.source.value) &&
711
+ (definition.node.type === "ImportSpecifier" ||
712
+ definition.node.type === "ImportNamespaceSpecifier" ||
713
+ definition.node.type === "ImportDefaultSpecifier")) {
714
+ return specifierBinding(definition.parent.source.value, definition.node).pipe(Option.map((binding) => binding.reference));
715
+ }
716
+ return constantDeclaration(variable).pipe(Option.flatMap((declaration) => Option.fromNullishOr(declaration.init).pipe(Option.flatMap((initialiser) => {
717
+ const reference = scopedImportedReference(context, initialiser, next, acceptsModule);
718
+ if (declaration.id.type === "Identifier") {
719
+ return reference;
720
+ }
721
+ if (declaration.id.type === "ObjectPattern") {
722
+ return Array.findFirst((property) => property.type === "Property" &&
723
+ property.value.type === "Identifier" &&
724
+ property.value.name === name)(declaration.id.properties).pipe(Option.flatMap((property) => {
725
+ if (property.type === "Property") {
726
+ return propertyName(property.key).pipe(Option.flatMap((key) => reference.pipe(Option.flatMap((resolved) => memberReference(resolved, key)))));
727
+ }
728
+ return Option.none();
729
+ }));
730
+ }
731
+ return Option.none();
732
+ }))));
733
+ }))));
734
+ };
735
+ const scopedEffectReference = (context, expression, visited = []) => scopedImportedReference(context, expression, visited);
736
+ const WebsiteOptions = Schema.Struct({
737
+ serverRuntime: Schema.String,
738
+ browserRuntime: Schema.String,
739
+ registryTests: Schema.optionalKey(Schema.Array(Schema.String)),
740
+ });
741
+ const websiteRpcBoundary = defineRule({
742
+ meta: {
743
+ type: "problem",
744
+ schema: [
745
+ {
746
+ type: "object",
747
+ properties: {
748
+ serverRuntime: { type: "string" },
749
+ browserRuntime: { type: "string" },
750
+ registryTests: { type: "array", items: { type: "string" } },
751
+ },
752
+ required: ["serverRuntime", "browserRuntime"],
753
+ additionalProperties: false,
754
+ },
755
+ ],
756
+ messages: {
757
+ remote: "Website loaders call the native API client directly in each environment. Do not create a TanStack server-function endpoint or a Website RPC proxy.",
758
+ runtime: "Use the sole named server runtime or browser Atom runtime; native API Service clients share its factory and registry.",
759
+ environment: "Use a runtime only inside TanStack's matching server/client function, or its matching environment file. Do not re-export runtime values.",
760
+ },
761
+ },
762
+ create(context) {
763
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(WebsiteOptions)));
764
+ const owner = (kind) => Option.exists(options, (value) => isBoundary(context.filename, context.cwd, [
765
+ { allowedFiles: [kind === "serverRuntime" ? value.serverRuntime : value.browserRuntime] },
766
+ ]));
767
+ const registryTestOwner = () => /\.(?:test|spec)\.tsx?$/u.test(context.filename) &&
768
+ Option.exists(options, (value) => Option.exists(Option.fromNullishOr(value.registryTests), (allowedFiles) => isBoundary(context.filename, context.cwd, [{ allowedFiles }])));
769
+ const insideDeclaration = (node, name) => {
770
+ if (node.type === "VariableDeclarator" && node.id.type === "Identifier")
771
+ return node.id.name === name;
772
+ if (node.type === "FunctionDeclaration")
773
+ return node.id?.name === name;
774
+ return node.parent ? insideDeclaration(node.parent, name) : false;
775
+ };
776
+ const registryOwner = (node) => (owner("browserRuntime") && insideDeclaration(node, "createLoaderRegistry")) ||
777
+ registryTestOwner();
778
+ const environmentBuilder = (node) => {
779
+ if (node.type !== "CallExpression")
780
+ return false;
781
+ if (Option.exists(scopedEffectReference(context, node.callee), (reference) => isExport(reference, "@tanstack/react-start", "createIsomorphicFn")))
782
+ return true;
783
+ return node.callee.type === "MemberExpression" && environmentBuilder(node.callee.object);
784
+ };
785
+ const inEnvironment = (node, environment) => {
786
+ // Tests import the host-independent runtime definitions to exercise cleanup.
787
+ // Constructor ownership still applies to tests; this only permits imports.
788
+ if (/\.(?:test|spec)\.tsx?$/u.test(context.filename) ||
789
+ context.filename.endsWith(`.${environment}.ts`) ||
790
+ context.filename.endsWith(`.${environment}.tsx`))
791
+ return true;
792
+ const parent = node.parent;
793
+ if (!parent)
794
+ return false;
795
+ if (parent.type === "CallExpression") {
796
+ if (parent.callee.type === "MemberExpression" &&
797
+ Option.contains(propertyName(parent.callee.property), environment) &&
798
+ environmentBuilder(parent.callee.object))
799
+ return true;
800
+ if (Option.exists(scopedEffectReference(context, parent.callee), (reference) => isExport(reference, "@tanstack/react-start", environment === "server" ? "createServerOnlyFn" : "createClientOnlyFn")))
801
+ return true;
802
+ }
803
+ return inEnvironment(parent, environment);
804
+ };
805
+ const declaredAs = (node, name) => node.parent?.type === "VariableDeclarator" &&
806
+ node.parent.id.type === "Identifier" &&
807
+ node.parent.id.name === name;
808
+ const prohibited = (reference) => isExport(reference, "@tanstack/react-start", "createServerFn");
809
+ const runtimeEnvironment = (source) => source.endsWith("runtime.server") || source.endsWith("runtime.server.ts")
810
+ ? Option.some("server")
811
+ : source.endsWith("runtime.client") || source.endsWith("runtime.client.ts")
812
+ ? Option.some("client")
813
+ : Option.none();
814
+ return {
815
+ ImportDeclaration(node) {
816
+ if (typeOnlyImport(node))
817
+ return;
818
+ Array.forEach(node.specifiers, (specifier) => {
819
+ if (Option.exists(specifierBinding(node.source.value, specifier), ({ reference }) => prohibited(reference)))
820
+ context.report({ node: specifier, messageId: "remote" });
821
+ });
822
+ },
823
+ ExportNamedDeclaration(node) {
824
+ if (node.exportKind === "type")
825
+ return;
826
+ if (node.source) {
827
+ if (Option.isSome(runtimeEnvironment(node.source.value)))
828
+ context.report({ node, messageId: "environment" });
829
+ const source = node.source.value;
830
+ Array.forEach(node.specifiers, (specifier) => {
831
+ if (specifier.exportKind !== "type" &&
832
+ prohibited({ module: source, name: Option.some(exportedName(specifier.local)) }))
833
+ context.report({ node: specifier, messageId: "remote" });
834
+ });
835
+ }
836
+ else {
837
+ Array.forEach(node.specifiers, (specifier) => {
838
+ if (Option.exists(scopedEffectReference(context, specifier.local), prohibited))
839
+ context.report({ node: specifier, messageId: "remote" });
840
+ });
841
+ }
842
+ },
843
+ ExportAllDeclaration(node) {
844
+ if (node.exportKind === "type")
845
+ return;
846
+ if (node.source.value === "@tanstack/react-start")
847
+ context.report({ node, messageId: "remote" });
848
+ if (Option.isSome(runtimeEnvironment(node.source.value)))
849
+ context.report({ node, messageId: "environment" });
850
+ },
851
+ CallExpression(node) {
852
+ if (node.callee.type === "CallExpression" &&
853
+ Option.exists(scopedEffectReference(context, node.callee.callee), (reference) => isExport(reference, "effect/reactivity/AtomHttpApi", "Service") ||
854
+ isExport(reference, "effect/reactivity/AtomRpc", "Service"))) {
855
+ const sharedFactory = Array.some(node.arguments, (argument) => argument.type === "ObjectExpression" &&
856
+ Array.some(argument.properties, (property) => property.type === "Property" &&
857
+ Option.contains(propertyName(property.key), "runtime") &&
858
+ property.value.type === "MemberExpression" &&
859
+ Option.contains(propertyName(property.value.property), "factory") &&
860
+ property.value.object.type === "Identifier" &&
861
+ property.value.object.name === "browserRuntime"));
862
+ if (!(owner("browserRuntime") && sharedFactory))
863
+ context.report({ node, messageId: "runtime" });
864
+ }
865
+ Option.match(scopedEffectReference(context, node.callee), {
866
+ onNone: Function.constVoid,
867
+ onSome: (reference) => {
868
+ if (prohibited(reference))
869
+ context.report({ node, messageId: "remote" });
870
+ if (isExport(reference, "effect/ManagedRuntime", "make") &&
871
+ !(owner("serverRuntime") && declaredAs(node, "runtime")))
872
+ context.report({ node, messageId: "runtime" });
873
+ if (isExport(reference, "effect/reactivity/Atom", "runtime") &&
874
+ !(owner("browserRuntime") && declaredAs(node, "browserRuntime")))
875
+ context.report({ node, messageId: "runtime" });
876
+ if (isExport(reference, "effect/reactivity/AtomRegistry", "make") &&
877
+ !registryOwner(node))
878
+ context.report({ node, messageId: "runtime" });
879
+ if (isExport(reference, "effect/rpc/RpcClient", "make"))
880
+ context.report({ node, messageId: "runtime" });
881
+ Option.match(runtimeEnvironment(reference.module), {
882
+ onNone: Function.constVoid,
883
+ onSome: (environment) => {
884
+ if (!inEnvironment(node, environment))
885
+ context.report({ node, messageId: "environment" });
886
+ },
887
+ });
888
+ if ((isExport(reference, "@tanstack/react-router", "createFileRoute") ||
889
+ isExport(reference, "@tanstack/react-start", "createServerFileRoute")) &&
890
+ Option.exists(Array.head(node.arguments), (argument) => Option.exists(moduleAddress(context, argument), (value) => /^\/rpc(?:\/|$)/u.test(value))))
891
+ context.report({ node, messageId: "remote" });
892
+ },
893
+ });
894
+ },
895
+ Identifier(node) {
896
+ const read = scopedVariable(context.sourceCode.getScope(node), node.name).pipe(Option.exists((variable) => Array.some(variable.references, (reference) => reference.identifier === node && reference.isRead())));
897
+ if (!read)
898
+ return;
899
+ if (Option.exists(scopedEffectReference(context, node), (reference) => isExport(reference, "effect/reactivity/AtomRegistry", "make")) &&
900
+ !registryOwner(node))
901
+ context.report({ node, messageId: "runtime" });
902
+ Option.match(scopedEffectReference(context, node).pipe(Option.flatMap((reference) => runtimeEnvironment(reference.module))), {
903
+ onNone: Function.constVoid,
904
+ onSome: (environment) => {
905
+ if (!inEnvironment(node, environment))
906
+ context.report({ node, messageId: "environment" });
907
+ },
908
+ });
909
+ },
910
+ MemberExpression(node) {
911
+ if (Option.exists(scopedEffectReference(context, node), (reference) => isExport(reference, "effect/reactivity/AtomRegistry", "make")) &&
912
+ !registryOwner(node))
913
+ context.report({ node, messageId: "runtime" });
914
+ if (Option.exists(scopedEffectReference(context, node), prohibited))
915
+ context.report({ node, messageId: "remote" });
916
+ },
917
+ };
918
+ },
919
+ });
920
+ const PromiseOptions = Schema.Struct({
921
+ allowedFiles: Schema.Array(Schema.String),
922
+ promiseOnlyFiles: Schema.optionalKey(Schema.Array(Schema.String)),
923
+ });
924
+ const promiseBoundarySchema = [
925
+ {
926
+ type: "object",
927
+ properties: {
928
+ allowedFiles: { type: "array", items: { type: "string" } },
929
+ promiseOnlyFiles: { type: "array", items: { type: "string" } },
930
+ },
931
+ required: ["allowedFiles"],
932
+ additionalProperties: false,
933
+ },
934
+ ];
935
+ const noPromiseWorkflow = defineRule({
936
+ meta: {
937
+ type: "problem",
938
+ schema: promiseBoundarySchema,
939
+ messages: {
940
+ forbidden: "Use Effect for asynchronous work. Only an explicitly named host adapter may mention Promise; async, await and Promise chains remain forbidden there.",
941
+ bridge: "Use a native Effect or Alchemy operation. Promise conversion belongs only in an explicitly named host or SDK adapter.",
942
+ },
943
+ },
944
+ create(context) {
945
+ const allowed = isBoundary(context.filename, context.cwd, context.options);
946
+ const bindings = effectBindings(context.sourceCode.ast);
947
+ const promiseOnly = Function.pipe(Array.head(context.options), Option.flatMap(Schema.decodeUnknownOption(PromiseOptions)), Option.exists((options) => Array.some(options.promiseOnlyFiles ?? [], (file) => exactOwnedFile(context.filename, context.cwd, file))));
948
+ const checkBridge = (node) => {
949
+ if (!allowed &&
950
+ Option.exists(scopedEffectReference(context, node), (reference) => isExport(reference, "effect/Effect", "tryPromise") ||
951
+ (!promiseOnly && isExport(reference, "effect/Effect", "promise"))))
952
+ context.report({ node, messageId: "bridge" });
953
+ };
954
+ const report = (node) => context.report({ node, messageId: "forbidden" });
955
+ const checkFunction = (node) => {
956
+ if (node.async)
957
+ report(node);
958
+ };
959
+ return {
960
+ ArrowFunctionExpression: checkFunction,
961
+ FunctionExpression: checkFunction,
962
+ FunctionDeclaration: checkFunction,
963
+ AwaitExpression: report,
964
+ NewExpression(node) {
965
+ if (isGlobalPromise(node.callee))
966
+ report(node);
967
+ },
968
+ TSTypeReference(node) {
969
+ if (!allowed && node.typeName.type === "Identifier" && node.typeName.name === "Promise")
970
+ report(node);
971
+ },
972
+ Identifier(node) {
973
+ const read = Function.pipe(scopedVariable(context.sourceCode.getScope(node), node.name), Option.exists((variable) => Array.some(variable.references, (reference) => reference.identifier === node && reference.isRead())));
974
+ if (read)
975
+ checkBridge(node);
976
+ },
977
+ MemberExpression: checkBridge,
978
+ CallExpression(node) {
979
+ if (node.callee.type !== "MemberExpression")
980
+ return;
981
+ const name = calleeName(node.callee);
982
+ const chain = Option.exists(name, (value) => value === "then" || value === "catch" || value === "finally");
983
+ const promise = isGlobalPromise(node.callee.object);
984
+ const effectMethod = Option.exists(resolveReference(bindings, node.callee), (reference) => reference.module === "effect/Effect");
985
+ // Adapters and exact host-contract tests may hand a host a settled
986
+ // Promise value; construction and chains stay forbidden everywhere.
987
+ const settledValue = Option.exists(name, (value) => value === "resolve" || value === "reject");
988
+ if ((chain && !effectMethod) || (promise && (!(allowed || promiseOnly) || !settledValue)))
989
+ report(node);
990
+ },
991
+ };
992
+ },
993
+ });
994
+ const noUnsafeOptionUnwrap = defineRule({
995
+ meta: {
996
+ type: "problem",
997
+ schema: [],
998
+ messages: {
999
+ unsafe: "Handle Option with match, map or getOrElse. getOrThrow can turn an ordinary missing value into a defect.",
1000
+ },
1001
+ },
1002
+ create(context) {
1003
+ const check = (node) => {
1004
+ if (Option.exists(resolveReference(effectBindings(context.sourceCode.ast), node), (reference) => isExport(reference, "effect/Option", "getOrThrow"))) {
1005
+ context.report({ node, messageId: "unsafe" });
1006
+ }
1007
+ };
1008
+ return {
1009
+ MemberExpression: check,
1010
+ CallExpression(node) {
1011
+ if (node.callee.type === "Identifier")
1012
+ check(node.callee);
1013
+ },
1014
+ };
1015
+ },
1016
+ });
1017
+ const noUncheckedJson = defineRule({
1018
+ meta: {
1019
+ type: "problem",
1020
+ schema: [],
1021
+ messages: { json: "Decode JSON through an Effect Schema at the boundary." },
1022
+ },
1023
+ create(context) {
1024
+ return {
1025
+ CallExpression(node) {
1026
+ if (node.callee.type === "MemberExpression") {
1027
+ const name = calleeName(node.callee);
1028
+ const jsonMethod = Option.contains(name, "json");
1029
+ const globalMethod = Option.exists(name, (value) => value === "parse" || value === "stringify");
1030
+ const object = node.callee.object;
1031
+ const json = (object.type === "Identifier" && object.name === "JSON") ||
1032
+ (object.type === "MemberExpression" &&
1033
+ object.object.type === "Identifier" &&
1034
+ object.object.name === "globalThis" &&
1035
+ Option.contains(calleeName(object), "JSON"));
1036
+ if (jsonMethod || (json && globalMethod))
1037
+ context.report({ node, messageId: "json" });
1038
+ }
1039
+ },
1040
+ };
1041
+ },
1042
+ });
1043
+ const noRuntimeOutsideBoundary = defineRule({
1044
+ meta: {
1045
+ type: "problem",
1046
+ schema: boundarySchema,
1047
+ messages: {
1048
+ runtime: "Execute Effect only in an explicitly approved application or command runtime. Tests use @effect/vitest.",
1049
+ },
1050
+ },
1051
+ create(context) {
1052
+ const allowed = isBoundary(context.filename, context.cwd, context.options);
1053
+ return {
1054
+ CallExpression(node) {
1055
+ if (allowed)
1056
+ return;
1057
+ const name = calleeName(node.callee);
1058
+ const imported = resolveReference(effectBindings(context.sourceCode.ast), node.callee);
1059
+ const operation = Option.flatMap(imported, (reference) => reference.module === "effect/Effect" ? reference.name : Option.none());
1060
+ const runs = Option.exists(Option.orElse(operation, () => name), (value) => /^(?:runPromise(?:Exit)?|runSync(?:Exit)?|runFork|runCallback|runMain)$/u.test(value));
1061
+ const creates = Option.exists(resolveReference(effectBindings(context.sourceCode.ast), node.callee), (reference) => isExport(reference, "effect/ManagedRuntime", "make"));
1062
+ if (runs || creates)
1063
+ context.report({ node, messageId: "runtime" });
1064
+ },
1065
+ };
1066
+ },
1067
+ });
1068
+ // Recognises direct host spellings, not arbitrary local aliases or shadowed names.
1069
+ const hostPath = (expression) => {
1070
+ if (expression.type === "Identifier")
1071
+ return Option.some(expression.name);
1072
+ if (expression.type === "MetaProperty")
1073
+ return Option.some(`${expression.meta.name}.${expression.property.name}`);
1074
+ if (expression.type !== "MemberExpression")
1075
+ return Option.none();
1076
+ const property = expression.property.type === "Identifier" && !expression.computed
1077
+ ? Option.some(expression.property.name)
1078
+ : expression.property.type === "Literal"
1079
+ ? Option.liftPredicate(expression.property.value, Predicate.isString)
1080
+ : Option.none();
1081
+ return Function.pipe(Option.all([hostPath(expression.object), property]), Option.map(([object, name]) => `${object}.${name}`));
1082
+ };
1083
+ const noNativeWork = defineRule({
1084
+ meta: {
1085
+ type: "problem",
1086
+ schema: [],
1087
+ messages: {
1088
+ nativeWork: "Use typed Effect failures, Config, Clock and Random instead of throws, try/catch or ambient host work.",
1089
+ },
1090
+ },
1091
+ create(context) {
1092
+ const report = (node) => context.report({ node, messageId: "nativeWork" });
1093
+ const clock = (expression) => Option.exists(hostPath(expression), (path) => /^(?:(?:globalThis|window)\.)?Date$/u.test(path));
1094
+ return {
1095
+ ThrowStatement: report,
1096
+ TryStatement: report,
1097
+ NewExpression(node) {
1098
+ // Explicit timestamp conversion is pure; acquiring the current time is not.
1099
+ if (node.arguments.length === 0 && clock(node.callee))
1100
+ report(node);
1101
+ },
1102
+ CallExpression(node) {
1103
+ // Calling Date as a function reads the clock even when arguments are supplied.
1104
+ if (clock(node.callee))
1105
+ report(node);
1106
+ },
1107
+ MemberExpression(node) {
1108
+ if (Option.exists(hostPath(node), (path) => /^(?:(?:globalThis|window)\.)?(?:process\.env|Bun\.env|Date\.now|Math\.random|crypto\.(?:randomUUID|getRandomValues))$/u.test(path) || path === "import.meta.env"))
1109
+ report(node);
1110
+ },
1111
+ };
1112
+ },
1113
+ });
1114
+ const CollectionOptions = Schema.Struct({
1115
+ allowedAssignments: Schema.OptionFromOptionalKey(Schema.Array(Schema.Struct({
1116
+ file: Schema.String,
1117
+ target: Schema.String,
1118
+ }))),
1119
+ allowedMethods: Schema.OptionFromOptionalKey(Schema.Array(Schema.Struct({
1120
+ file: Schema.String,
1121
+ receiver: Schema.String,
1122
+ method: Schema.String,
1123
+ }))),
1124
+ });
1125
+ const exactOwnedFile = (filename, cwd, file) => !file.includes("*") &&
1126
+ !file.includes("..") &&
1127
+ !file.startsWith("/") &&
1128
+ filename.replaceAll("\\", "/") === `${cwd.replaceAll("\\", "/")}/${file}`;
1129
+ // Check lexical scope as well as the import path: a shadowed namespace is not the import.
1130
+ const referenceRoot = (expression) => expression.type === "Identifier"
1131
+ ? Option.some(expression.name)
1132
+ : expression.type === "MemberExpression" && !expression.computed
1133
+ ? referenceRoot(expression.object)
1134
+ : Option.none();
1135
+ const importedInScope = (scope, name) => Option.match(Array.findFirst(scope.variables, (variable) => variable.name === name), {
1136
+ onSome: (variable) => Array.some(variable.defs, (definition) => definition.type === "ImportBinding"),
1137
+ onNone: () => Option.match(Option.fromNullishOr(scope.upper), {
1138
+ onNone: () => false,
1139
+ onSome: (upper) => importedInScope(upper, name),
1140
+ }),
1141
+ });
1142
+ const collectionMethods = [
1143
+ "map",
1144
+ "flatMap",
1145
+ "filter",
1146
+ "reduce",
1147
+ "reduceRight",
1148
+ "find",
1149
+ "findLast",
1150
+ "findIndex",
1151
+ "findLastIndex",
1152
+ "some",
1153
+ "every",
1154
+ "forEach",
1155
+ "sort",
1156
+ "toSorted",
1157
+ "reverse",
1158
+ "toReversed",
1159
+ "push",
1160
+ "pop",
1161
+ "shift",
1162
+ "unshift",
1163
+ "splice",
1164
+ "fill",
1165
+ "copyWithin",
1166
+ "beginMutation",
1167
+ "endMutation",
1168
+ "mutate",
1169
+ ];
1170
+ const mutationMethods = ["beginMutation", "endMutation", "mutate"];
1171
+ const noImperativeCollections = defineRule({
1172
+ meta: {
1173
+ type: "problem",
1174
+ schema: [
1175
+ {
1176
+ type: "object",
1177
+ properties: {
1178
+ allowedAssignments: {
1179
+ type: "array",
1180
+ items: {
1181
+ type: "object",
1182
+ properties: { file: { type: "string" }, target: { type: "string" } },
1183
+ required: ["file", "target"],
1184
+ additionalProperties: false,
1185
+ },
1186
+ },
1187
+ allowedMethods: {
1188
+ type: "array",
1189
+ items: {
1190
+ type: "object",
1191
+ properties: {
1192
+ file: { type: "string" },
1193
+ receiver: { type: "string" },
1194
+ method: { type: "string" },
1195
+ },
1196
+ required: ["file", "receiver", "method"],
1197
+ additionalProperties: false,
1198
+ },
1199
+ },
1200
+ },
1201
+ additionalProperties: false,
1202
+ },
1203
+ ],
1204
+ messages: {
1205
+ traversal: "Use Effect Array folds/unfolds, a bounded Effect.forEach or an owned Stream instead of a loop.",
1206
+ mutable: "Use const and immutable Effect values. Ref owns genuine changing state with a pure immutable update.",
1207
+ collection: "Use immutable Effect collection functions instead of .{{method}}. Transient/mutable modes are forbidden.",
1208
+ matching: "Use exhaustive Match for closed alternatives, or Option/Result matching, instead of switch or manual _tag comparisons.",
1209
+ },
1210
+ },
1211
+ create(context) {
1212
+ const bindings = effectBindings(context.sourceCode.ast);
1213
+ const options = Function.pipe(Array.head(context.options), Option.flatMap(Schema.decodeUnknownOption(CollectionOptions)));
1214
+ const traversal = (node) => context.report({ node, messageId: "traversal" });
1215
+ const mutable = (node) => context.report({ node, messageId: "mutable" });
1216
+ return {
1217
+ ForStatement: traversal,
1218
+ ForOfStatement: traversal,
1219
+ ForInStatement: traversal,
1220
+ WhileStatement: traversal,
1221
+ DoWhileStatement: traversal,
1222
+ VariableDeclaration(node) {
1223
+ if (node.kind !== "const")
1224
+ mutable(node);
1225
+ },
1226
+ UpdateExpression: mutable,
1227
+ AssignmentExpression(node) {
1228
+ const allowed = Option.exists(options, (value) => Option.exists(value.allowedAssignments, (entries) => Array.some(entries, (entry) => exactOwnedFile(context.filename, context.cwd, entry.file) &&
1229
+ Option.contains(node.left.type === "Identifier" || node.left.type === "MemberExpression"
1230
+ ? hostPath(node.left)
1231
+ : Option.none(), entry.target))));
1232
+ if (!allowed)
1233
+ mutable(node);
1234
+ },
1235
+ MemberExpression(node) {
1236
+ if (node.computed || node.property.type !== "Identifier")
1237
+ return;
1238
+ const method = node.property.name;
1239
+ const nativeArray = Option.exists(hostPath(node.object), (path) => path === "Array" || path === "globalThis.Array");
1240
+ if (!Array.contains(collectionMethods, method) &&
1241
+ !(nativeArray && Array.contains(["from", "of", "isArray"], method)))
1242
+ return;
1243
+ // Resolve imported functional operations, including aliases and root modules.
1244
+ // A namespace's transient mutation functions remain forbidden.
1245
+ const functional = Option.exists(referenceRoot(node), (root) => importedInScope(context.sourceCode.getScope(node), root)) &&
1246
+ Option.exists(resolveReference(bindings, node), (reference) => !Array.contains(mutationMethods, method) &&
1247
+ (reference.module.startsWith("effect/") ||
1248
+ reference.module === "alchemy/Output" ||
1249
+ (reference.module === "alchemy/Namespace" && method === "push")));
1250
+ const allowed = Option.exists(options, (value) => Option.exists(value.allowedMethods, (entries) => Array.some(entries, (entry) => exactOwnedFile(context.filename, context.cwd, entry.file) &&
1251
+ entry.method === method &&
1252
+ Option.contains(hostPath(node.object), entry.receiver))));
1253
+ if (!functional && !allowed)
1254
+ context.report({ node, messageId: "collection", data: { method } });
1255
+ },
1256
+ CallExpression(node) {
1257
+ if (node.callee.type === "Identifier" &&
1258
+ Option.exists(resolveReference(bindings, node.callee), (reference) => reference.module.startsWith("effect/") &&
1259
+ Option.exists(reference.name, (name) => Array.contains(mutationMethods, name))))
1260
+ context.report({ node, messageId: "collection", data: { method: node.callee.name } });
1261
+ },
1262
+ UnaryExpression(node) {
1263
+ if (node.operator === "delete")
1264
+ mutable(node);
1265
+ },
1266
+ NewExpression(node) {
1267
+ if (Option.exists(hostPath(node.callee), (path) => path === "Array" || path === "globalThis.Array"))
1268
+ context.report({ node, messageId: "collection", data: { method: "Array constructor" } });
1269
+ },
1270
+ SwitchStatement(node) {
1271
+ context.report({ node, messageId: "matching" });
1272
+ },
1273
+ BinaryExpression(node) {
1274
+ if (Array.contains(["===", "!==", "==", "!="], node.operator) &&
1275
+ Array.some([node.left, node.right], (side) => side.type === "MemberExpression" &&
1276
+ side.property.type === "Identifier" &&
1277
+ side.property.name === "_tag"))
1278
+ context.report({ node, messageId: "matching" });
1279
+ },
1280
+ };
1281
+ },
1282
+ });
1283
+ // Computed identifiers are variable reads, not literal property names.
1284
+ const staticPropertyName = (key, computed) => computed && key.type !== "Literal" ? Option.none() : propertyName(key);
1285
+ const LoaderRequestOptions = Schema.Struct({
1286
+ loaderFiles: Schema.Array(Schema.String),
1287
+ clientServices: Schema.optionalKey(Schema.Array(Schema.Struct({ module: Schema.String, name: Schema.String }))),
1288
+ });
1289
+ const routeFactory = (context, expression) => {
1290
+ if (expression.type !== "CallExpression")
1291
+ return false;
1292
+ return (Option.exists(scopedEffectReference(context, expression.callee), (reference) => Array.some((name) => isExport(reference, "@tanstack/react-router", name))([
1293
+ "createFileRoute",
1294
+ "createRoute",
1295
+ "createRootRoute",
1296
+ "createRootRouteWithContext",
1297
+ ])) || routeFactory(context, expression.callee));
1298
+ };
1299
+ const routeLoaderProperty = (context, node) => node.type === "Property" &&
1300
+ Option.contains(staticPropertyName(node.key, node.computed), "loader") &&
1301
+ node.parent?.type === "ObjectExpression" &&
1302
+ node.parent.parent?.type === "CallExpression" &&
1303
+ routeFactory(context, node.parent.parent);
1304
+ const insideRouteLoader = (context, node) => {
1305
+ if (routeLoaderProperty(context, node))
1306
+ return true;
1307
+ if ((node.type === "VariableDeclarator" && node.id.type === "Identifier") ||
1308
+ (node.type === "FunctionDeclaration" && node.id)) {
1309
+ const id = node.id;
1310
+ if (id?.type === "Identifier") {
1311
+ const loader = scopedVariable(context.sourceCode.getScope(id), id.name).pipe(Option.exists((variable) => Array.some((reference) => Option.fromNullishOr(reference.identifier.parent).pipe(Option.exists((parent) => routeLoaderProperty(context, parent))))(variable.references)));
1312
+ if (loader)
1313
+ return true;
1314
+ }
1315
+ }
1316
+ return node.parent ? insideRouteLoader(context, node.parent) : false;
1317
+ };
1318
+ const noLoaderRequestState = defineRule({
1319
+ meta: {
1320
+ type: "problem",
1321
+ schema: [
1322
+ {
1323
+ type: "object",
1324
+ properties: {
1325
+ loaderFiles: { type: "array", items: { type: "string" } },
1326
+ clientServices: {
1327
+ type: "array",
1328
+ items: {
1329
+ type: "object",
1330
+ properties: {
1331
+ module: { type: "string" },
1332
+ name: { type: "string" },
1333
+ },
1334
+ required: ["module", "name"],
1335
+ additionalProperties: false,
1336
+ },
1337
+ },
1338
+ },
1339
+ required: ["loaderFiles"],
1340
+ additionalProperties: false,
1341
+ },
1342
+ ],
1343
+ messages: {
1344
+ state: "TanStack owns route loading and freshness. Call the checked client directly; keep request Atoms and result caches with component-owned reads.",
1345
+ },
1346
+ },
1347
+ create(context) {
1348
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(LoaderRequestOptions)));
1349
+ const loaderFile = Option.exists(options, ({ loaderFiles }) => isBoundary(context.filename, context.cwd, [{ allowedFiles: loaderFiles }]));
1350
+ const queryFactory = (expression, visited = []) => {
1351
+ if (Array.some(visited, (previous) => previous === expression))
1352
+ return false;
1353
+ if (expression.type === "MemberExpression" &&
1354
+ Option.contains(staticPropertyName(expression.property, expression.computed), "query")) {
1355
+ return Option.exists(scopedEffectReference(context, expression.object), (reference) => Option.exists(options, ({ clientServices }) => Array.some((service) => isExport(reference, service.module, service.name))(clientServices ?? [])));
1356
+ }
1357
+ if (expression.type !== "Identifier")
1358
+ return false;
1359
+ return scopedVariable(context.sourceCode.getScope(expression), expression.name).pipe(Option.flatMap(constantDeclaration), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.exists((value) => queryFactory(value, [...visited, expression])));
1360
+ };
1361
+ const queryAtom = (expression, visited = []) => {
1362
+ if (Array.some(visited, (previous) => previous === expression))
1363
+ return false;
1364
+ if (expression.type === "CallExpression")
1365
+ return queryFactory(expression.callee);
1366
+ if (expression.type !== "Identifier")
1367
+ return false;
1368
+ return scopedVariable(context.sourceCode.getScope(expression), expression.name).pipe(Option.flatMap(constantDeclaration), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.exists((value) => queryAtom(value, [...visited, expression])));
1369
+ };
1370
+ return {
1371
+ CallExpression(node) {
1372
+ if (!loaderFile && !insideRouteLoader(context, node))
1373
+ return;
1374
+ const ownsState = Option.exists(scopedEffectReference(context, node.callee), (reference) => Array.some((name) => isExport(reference, "effect/reactivity/Atom", name))([
1375
+ "make",
1376
+ "family",
1377
+ "fn",
1378
+ "runtime",
1379
+ ]) ||
1380
+ Array.some((name) => isExport(reference, "effect/Effect", name))([
1381
+ "cached",
1382
+ "cachedWithTTL",
1383
+ "cachedInvalidateWithTTL",
1384
+ ]) ||
1385
+ Array.some((name) => isExport(reference, "effect/Cache", name))([
1386
+ "make",
1387
+ "makeWith",
1388
+ ]));
1389
+ const readsQuery = Option.exists(scopedEffectReference(context, node.callee), (reference) => isExport(reference, "effect/reactivity/Atom", "getResult") &&
1390
+ Array.head(node.arguments).pipe(Option.exists((argument) => queryAtom(argument))));
1391
+ if (ownsState || readsQuery || queryFactory(node.callee))
1392
+ context.report({ node, messageId: "state" });
1393
+ },
1394
+ };
1395
+ },
1396
+ });
1397
+ const nativeHttpClientMaker = (context, expression) => expression.type === "CallExpression" &&
1398
+ Option.exists(scopedEffectReference(context, expression.callee), (reference) => isExport(reference, "effect/http-api/HttpApiClient", "make") ||
1399
+ isExport(reference, "effect/http-api/HttpApiClient", "makeWith"));
1400
+ const acquiredHttpClient = (context, expression, visited = []) => {
1401
+ if (Array.some(visited, (previous) => previous === expression))
1402
+ return false;
1403
+ if (expression.type === "MemberExpression")
1404
+ return acquiredHttpClient(context, expression.object, [...visited, expression]);
1405
+ if (expression.type !== "Identifier")
1406
+ return false;
1407
+ return scopedVariable(context.sourceCode.getScope(expression), expression.name).pipe(Option.flatMap(constantDeclaration), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.exists((value) => value.type === "YieldExpression" && value.argument
1408
+ ? nativeHttpClientMaker(context, value.argument)
1409
+ : acquiredHttpClient(context, value, [...visited, expression])));
1410
+ };
1411
+ // A direct method copy or a callback that only forwards arguments adds no policy.
1412
+ // A timeout, validation, mapping, named failure or other composition is not bare.
1413
+ const bareHttpForwarder = (context, expression) => {
1414
+ if (expression.type === "MemberExpression")
1415
+ return acquiredHttpClient(context, expression);
1416
+ if (expression.type === "ArrowFunctionExpression" || expression.type === "FunctionExpression") {
1417
+ const selectedBody = (body) => {
1418
+ if (body.type === "YieldExpression")
1419
+ return Option.fromNullishOr(body.argument);
1420
+ if (body.type !== "BlockStatement")
1421
+ return Option.some(body);
1422
+ return Array.head(body.body).pipe(Option.filter(() => body.body.length === 1), Option.flatMap((statement) => statement.type === "ReturnStatement"
1423
+ ? Option.fromNullishOr(statement.argument).pipe(Option.flatMap(selectedBody))
1424
+ : Option.none()));
1425
+ };
1426
+ return Option.fromNullishOr(expression.body).pipe(Option.flatMap(selectedBody), Option.exists((body) => body.type === "CallExpression" &&
1427
+ body.callee.type === "MemberExpression" &&
1428
+ acquiredHttpClient(context, body.callee) &&
1429
+ // Fixed selection and argument conversion can be genuine feature policy.
1430
+ Array.every((argument) => (argument.type === "ObjectExpression" && argument.properties.length === 0) ||
1431
+ (argument.type === "Identifier" &&
1432
+ Array.some((parameter) => parameter.type === "Identifier" && parameter.name === argument.name)(expression.params)))(body.arguments)));
1433
+ }
1434
+ if (expression.type === "YieldExpression" && expression.argument)
1435
+ return bareHttpForwarder(context, expression.argument);
1436
+ if (expression.type !== "CallExpression")
1437
+ return false;
1438
+ if (Option.exists(scopedEffectReference(context, expression.callee), (reference) => isExport(reference, "effect/Effect", "fn") ||
1439
+ isExport(reference, "effect/Effect", "fnUntraced")))
1440
+ return (expression.arguments.length === 1 &&
1441
+ Array.head(expression.arguments).pipe(Option.exists((callback) => bareHttpForwarder(context, callback))));
1442
+ if (expression.callee.type === "CallExpression" &&
1443
+ Option.exists(scopedEffectReference(context, expression.callee.callee), (reference) => isExport(reference, "effect/Effect", "fn") ||
1444
+ isExport(reference, "effect/Effect", "fnUntraced")))
1445
+ return (expression.arguments.length === 1 &&
1446
+ Array.head(expression.arguments).pipe(Option.exists((callback) => bareHttpForwarder(context, callback))));
1447
+ return (expression.callee.type === "MemberExpression" && acquiredHttpClient(context, expression.callee));
1448
+ };
1449
+ const noNativeClientReplica = defineRule({
1450
+ meta: {
1451
+ type: "problem",
1452
+ schema: [],
1453
+ messages: {
1454
+ replica: "Use the generated HttpApiClient operations directly. A second service that only copies or forwards those methods adds no policy.",
1455
+ },
1456
+ },
1457
+ create(context) {
1458
+ return {
1459
+ CallExpression(node) {
1460
+ if (node.callee.type !== "MemberExpression" ||
1461
+ !Option.contains(staticPropertyName(node.callee.property, node.callee.computed), "of"))
1462
+ return;
1463
+ const implementation = Array.head(node.arguments);
1464
+ if (!Option.exists(implementation, (value) => value.type === "ObjectExpression" &&
1465
+ value.properties.length > 0 &&
1466
+ Array.every((property) => property.type === "Property" && bareHttpForwarder(context, property.value))(value.properties)))
1467
+ return;
1468
+ // Only an actual Layer.effect service construction owns this restriction.
1469
+ const layerOwner = (parent) => {
1470
+ if (parent.type === "CallExpression" &&
1471
+ Option.exists(scopedEffectReference(context, parent.callee), (reference) => isExport(reference, "effect/Layer", "effect"))) {
1472
+ return Array.head(parent.arguments).pipe(Option.exists((tag) => node.callee.type === "MemberExpression" &&
1473
+ tag.type === "Identifier" &&
1474
+ node.callee.object.type === "Identifier" &&
1475
+ Option.all([
1476
+ scopedVariable(context.sourceCode.getScope(tag), tag.name),
1477
+ scopedVariable(context.sourceCode.getScope(node.callee.object), node.callee.object.name),
1478
+ ]).pipe(Option.exists(([outer, inner]) => outer === inner))));
1479
+ }
1480
+ return parent.parent ? layerOwner(parent.parent) : false;
1481
+ };
1482
+ if (layerOwner(node))
1483
+ context.report({ node, messageId: "replica" });
1484
+ },
1485
+ };
1486
+ },
1487
+ });
1488
+ // Resolve relative import addresses without evaluating code or trusting a name.
1489
+ const relativeModule = (filename, address) => {
1490
+ if (!address.startsWith(".")) {
1491
+ return address;
1492
+ }
1493
+ const directory = filename.replaceAll("\\", "/").replace(/\/[^/]+$/u, "");
1494
+ return Function.pipe(`${directory}/${address}`.split("/"), Array.reduce([], (parts, part) => {
1495
+ if (part === "." || part === "") {
1496
+ return parts;
1497
+ }
1498
+ if (part === "..") {
1499
+ return Array.dropRight(parts, 1);
1500
+ }
1501
+ return [...parts, part];
1502
+ }), Array.join("/"));
1503
+ };
1504
+ const CheckedPageOptions = Schema.Struct({
1505
+ checkedHelper: Schema.Struct({ module: Schema.String, export: Schema.String }),
1506
+ responseSchema: Schema.Struct({ module: Schema.String, export: Schema.String }),
1507
+ });
1508
+ const requestPageCall = (context, node, options) => node.type === "CallExpression" &&
1509
+ scopedImportedReference(context, node.callee, [], Function.constTrue).pipe(Option.exists((reference) => Option.contains(reference.name, options.checkedHelper.export) &&
1510
+ (relativeModule(context.filename, reference.module)
1511
+ .replace(/\.tsx?$/u, "")
1512
+ .endsWith(`/${options.checkedHelper.module}`) ||
1513
+ reference.module === options.checkedHelper.module)));
1514
+ const checkedPageEffect = (context, node, options, visited = []) => {
1515
+ if (Array.some(visited, (previous) => previous === node)) {
1516
+ return Option.none();
1517
+ }
1518
+ const next = [...visited, node];
1519
+ if (node.type === "CallExpression" && requestPageCall(context, node, options)) {
1520
+ return Option.some(node);
1521
+ }
1522
+ if (node.type === "Identifier") {
1523
+ return scopedVariable(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap(constantDeclaration), Option.filter((declaration) => declaration.id.type === "Identifier"), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.flatMap((initialiser) => checkedPageEffect(context, initialiser, options, next)));
1524
+ }
1525
+ if (node.type === "CallExpression" &&
1526
+ node.callee.type === "MemberExpression" &&
1527
+ Option.contains(staticPropertyName(node.callee.property, node.callee.computed), "pipe") &&
1528
+ Array.every((argument) => argument.type === "CallExpression" &&
1529
+ scopedEffectReference(context, argument.callee).pipe(Option.exists((reference) => Array.some((name) => isExport(reference, "effect/Effect", name))([
1530
+ "timeout",
1531
+ "filterOrFail",
1532
+ "mapError",
1533
+ "tap",
1534
+ "withSpan",
1535
+ ]))))(node.arguments)) {
1536
+ return checkedPageEffect(context, node.callee.object, options, next);
1537
+ }
1538
+ return Option.none();
1539
+ };
1540
+ const checkedPageValue = (context, node, options, visited = []) => {
1541
+ if (Array.some(visited, (previous) => previous === node) || node.type !== "Identifier") {
1542
+ return Option.none();
1543
+ }
1544
+ const next = [...visited, node];
1545
+ return scopedVariable(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap((variable) => {
1546
+ const constant = constantDeclaration(variable).pipe(Option.filter((declaration) => declaration.id.type === "Identifier"), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.flatMap((initialiser) => {
1547
+ if (initialiser.type === "YieldExpression") {
1548
+ return Option.fromNullishOr(initialiser.argument).pipe(Option.flatMap((effect) => checkedPageEffect(context, effect, options)));
1549
+ }
1550
+ return checkedPageValue(context, initialiser, options, next);
1551
+ }));
1552
+ const callback = variable.scope.block;
1553
+ if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression") {
1554
+ return constant;
1555
+ }
1556
+ const call = callback.parent;
1557
+ if (call.type !== "CallExpression" ||
1558
+ !Option.all([Array.head(callback.params), Array.head(variable.identifiers)]).pipe(Option.exists(([parameter, identifier]) => parameter === identifier)) ||
1559
+ !scopedEffectReference(context, call.callee).pipe(Option.exists((reference) => Array.some((name) => isExport(reference, "effect/Effect", name))([
1560
+ "map",
1561
+ "flatMap",
1562
+ "tap",
1563
+ "filterOrFail",
1564
+ ])))) {
1565
+ return constant;
1566
+ }
1567
+ if (call.arguments.length >= 2) {
1568
+ return Array.head(call.arguments).pipe(Option.flatMap((effect) => checkedPageEffect(context, effect, options)), Option.orElse(() => constant));
1569
+ }
1570
+ const pipe = call.parent;
1571
+ if (pipe.type === "CallExpression" &&
1572
+ pipe.callee.type === "MemberExpression" &&
1573
+ Option.contains(propertyName(pipe.callee.property), "pipe")) {
1574
+ return checkedPageEffect(context, pipe.callee.object, options).pipe(Option.orElse(() => constant));
1575
+ }
1576
+ return constant;
1577
+ }));
1578
+ };
1579
+ const literalPageFormat = (call) => Array.head(call.arguments).pipe(Option.filter((argument) => argument.type === "ObjectExpression"), Option.flatMap((argument) => {
1580
+ if (argument.type !== "ObjectExpression") {
1581
+ return Option.none();
1582
+ }
1583
+ return Array.findFirst((property) => property.type === "Property" &&
1584
+ Option.contains(staticPropertyName(property.key, property.computed), "input"))(argument.properties);
1585
+ }), Option.exists((input) => input.type === "Property" &&
1586
+ input.value.type === "ObjectExpression" &&
1587
+ Array.some((property) => property.type === "Property" &&
1588
+ Option.contains(staticPropertyName(property.key, property.computed), "format") &&
1589
+ property.value.type === "Literal" &&
1590
+ Predicate.isString(property.value.value))(input.value.properties)));
1591
+ const checkedPageResponse = defineRule({
1592
+ meta: {
1593
+ type: "problem",
1594
+ schema: [
1595
+ {
1596
+ type: "object",
1597
+ additionalProperties: false,
1598
+ required: ["checkedHelper", "responseSchema"],
1599
+ properties: {
1600
+ checkedHelper: {
1601
+ type: "object",
1602
+ additionalProperties: false,
1603
+ required: ["module", "export"],
1604
+ properties: {
1605
+ module: { type: "string", minLength: 1 },
1606
+ export: { type: "string", minLength: 1 },
1607
+ },
1608
+ },
1609
+ responseSchema: {
1610
+ type: "object",
1611
+ additionalProperties: false,
1612
+ required: ["module", "export"],
1613
+ properties: {
1614
+ module: { type: "string", minLength: 1 },
1615
+ export: { type: "string", minLength: 1 },
1616
+ },
1617
+ },
1618
+ },
1619
+ },
1620
+ ],
1621
+ messages: {
1622
+ redundant: "The configured helper already checks the reply and its requested format. Use its checked value; keep only feature checks such as page path, title or course identity.",
1623
+ },
1624
+ },
1625
+ create(context) {
1626
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(CheckedPageOptions)));
1627
+ if (Option.isNone(options)) {
1628
+ return {};
1629
+ }
1630
+ const settings = options.value;
1631
+ return {
1632
+ BinaryExpression(node) {
1633
+ if (!Array.contains(node.operator)(["===", "!==", "==", "!="])) {
1634
+ return;
1635
+ }
1636
+ if (Array.some((value) => value.type === "MemberExpression" &&
1637
+ Option.contains(staticPropertyName(value.property, value.computed), "format") &&
1638
+ checkedPageValue(context, value.object, settings).pipe(Option.exists(literalPageFormat)))([node.left, node.right])) {
1639
+ context.report({ node, messageId: "redundant" });
1640
+ }
1641
+ },
1642
+ CallExpression(node) {
1643
+ if (node.callee.type !== "CallExpression" ||
1644
+ !scopedEffectReference(context, node.callee.callee).pipe(Option.exists((reference) => reference.module === "effect/Schema" &&
1645
+ reference.name.pipe(Option.exists((name) => name.startsWith("decode"))))) ||
1646
+ !Array.head(node.callee.arguments).pipe(Option.flatMap((schema) => scopedImportedReference(context, schema, [], Function.constTrue)), Option.exists((reference) => isExport(reference, settings.responseSchema.module, settings.responseSchema.export))) ||
1647
+ !Array.head(node.arguments).pipe(Option.flatMap((value) => checkedPageValue(context, value, settings)), Option.isSome)) {
1648
+ return;
1649
+ }
1650
+ context.report({ node, messageId: "redundant" });
1651
+ },
1652
+ };
1653
+ },
1654
+ });
1655
+ const unboundGlobal = (context, node, name) => node.type === "Identifier" &&
1656
+ node.name === name &&
1657
+ scopedVariable(context.sourceCode.getScope(node), name).pipe(Option.match({ onNone: Function.constTrue, onSome: (variable) => variable.defs.length === 0 }));
1658
+ const globalObjectRead = (context, node, visited = []) => {
1659
+ if (Array.some(visited, (previous) => previous === node)) {
1660
+ return false;
1661
+ }
1662
+ if (unboundGlobal(context, node, "globalThis")) {
1663
+ return true;
1664
+ }
1665
+ if (node.type !== "Identifier") {
1666
+ return false;
1667
+ }
1668
+ return scopedVariable(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap(constantDeclaration), Option.filter((declaration) => declaration.id.type === "Identifier"), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.exists((initialiser) => globalObjectRead(context, initialiser, [...visited, node])));
1669
+ };
1670
+ const globalCacheRead = (context, node, visited = []) => {
1671
+ if (Array.some(visited, (previous) => previous === node)) {
1672
+ return false;
1673
+ }
1674
+ if (unboundGlobal(context, node, "caches")) {
1675
+ return true;
1676
+ }
1677
+ if (node.type === "MemberExpression") {
1678
+ return ((globalObjectRead(context, node.object) &&
1679
+ Option.contains(staticPropertyName(node.property, node.computed), "caches")) ||
1680
+ globalCacheRead(context, node.object, [...visited, node]));
1681
+ }
1682
+ if (node.type !== "Identifier") {
1683
+ return false;
1684
+ }
1685
+ return scopedVariable(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap(constantDeclaration), Option.exists((declaration) => {
1686
+ const initialiser = Option.fromNullishOr(declaration.init);
1687
+ if (declaration.id.type === "ObjectPattern") {
1688
+ const cacheProperty = Array.some((property) => property.type === "Property" &&
1689
+ property.value.type === "Identifier" &&
1690
+ property.value.name === node.name &&
1691
+ Option.contains(staticPropertyName(property.key, property.computed), "caches"))(declaration.id.properties);
1692
+ if (cacheProperty &&
1693
+ initialiser.pipe(Option.exists((value) => globalObjectRead(context, value)))) {
1694
+ return true;
1695
+ }
1696
+ }
1697
+ return initialiser.pipe(Option.exists((value) => globalCacheRead(context, value, [...visited, node])));
1698
+ }));
1699
+ };
1700
+ const nativeCloudflareCache = defineRule({
1701
+ meta: {
1702
+ type: "problem",
1703
+ schema: [],
1704
+ messages: {
1705
+ native: "Use native Alchemy Worker/Website caching. Do not add a custom runtime using the global Cache API.",
1706
+ },
1707
+ },
1708
+ create(context) {
1709
+ return {
1710
+ Identifier(node) {
1711
+ const variable = scopedVariable(context.sourceCode.getScope(node), node.name);
1712
+ const read = variable.pipe(Option.match({
1713
+ onNone: () => Array.some((reference) => reference.identifier === node && reference.isRead())(context.sourceCode.getScope(node).through),
1714
+ onSome: (binding) => Array.some((reference) => reference.identifier === node && reference.isRead())(binding.references),
1715
+ }));
1716
+ if (read && globalCacheRead(context, node)) {
1717
+ context.report({ node, messageId: "native" });
1718
+ }
1719
+ },
1720
+ MemberExpression(node) {
1721
+ if (globalObjectRead(context, node.object) &&
1722
+ Option.contains(staticPropertyName(node.property, node.computed), "caches")) {
1723
+ context.report({ node, messageId: "native" });
1724
+ }
1725
+ },
1726
+ };
1727
+ },
1728
+ });
1729
+ const localObject = (context, node, visited = []) => {
1730
+ if (Array.some(visited, (previous) => previous === node)) {
1731
+ return Option.none();
1732
+ }
1733
+ if (node.type === "ObjectExpression") {
1734
+ return Option.some(node);
1735
+ }
1736
+ if (node.type !== "Identifier") {
1737
+ return Option.none();
1738
+ }
1739
+ return scopedVariable(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap(constantDeclaration), Option.filter((declaration) => declaration.id.type === "Identifier"), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.flatMap((initialiser) => localObject(context, initialiser, [...visited, node])));
1740
+ };
1741
+ const objectField = (object, name) => Array.findFirst((property) => property.type === "Property" &&
1742
+ Option.contains(staticPropertyName(property.key, property.computed), name))(object.properties).pipe(Option.flatMap((property) => {
1743
+ if (property.type === "Property") {
1744
+ return Option.some(property.value);
1745
+ }
1746
+ return Option.none();
1747
+ }));
1748
+ const websiteDataProxy = defineRule({
1749
+ meta: {
1750
+ type: "problem",
1751
+ schema: [
1752
+ {
1753
+ type: "object",
1754
+ additionalProperties: false,
1755
+ required: ["websiteDirectory", "publicApiPaths"],
1756
+ properties: {
1757
+ websiteDirectory: { type: "string", minLength: 1 },
1758
+ publicApiPaths: { type: "array", minItems: 1, items: { type: "string", pattern: "^/" } },
1759
+ },
1760
+ },
1761
+ ],
1762
+ messages: {
1763
+ direct: "Browser reads call the API directly. Do not add a Website proxy at the configured public API paths; use native TanStack loaders for HTML.",
1764
+ },
1765
+ },
1766
+ create(context) {
1767
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(Schema.Struct({
1768
+ websiteDirectory: Schema.String,
1769
+ publicApiPaths: Schema.Array(Schema.String),
1770
+ }))));
1771
+ if (Option.isNone(options) ||
1772
+ !context.filename.replaceAll("\\", "/").includes(`/${options.value.websiteDirectory}/`)) {
1773
+ return {};
1774
+ }
1775
+ return {
1776
+ CallExpression(node) {
1777
+ const settings = Array.head(node.arguments).pipe(Option.flatMap((argument) => localObject(context, argument)));
1778
+ const path = () => {
1779
+ if (node.callee.type === "CallExpression" &&
1780
+ scopedImportedReference(context, node.callee.callee, [], Function.constTrue).pipe(Option.exists((reference) => isExport(reference, "@tanstack/react-router", "createFileRoute")))) {
1781
+ return Array.head(node.callee.arguments).pipe(Option.flatMap((argument) => moduleAddress(context, argument)));
1782
+ }
1783
+ if (scopedImportedReference(context, node.callee, [], Function.constTrue).pipe(Option.exists((reference) => isExport(reference, "@tanstack/react-router", "createRoute")))) {
1784
+ return settings.pipe(Option.flatMap((object) => objectField(object, "path")), Option.flatMap((argument) => moduleAddress(context, argument)));
1785
+ }
1786
+ return Option.none();
1787
+ };
1788
+ const dataPath = path().pipe(Option.exists((path) => Array.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))(options.value.publicApiPaths)));
1789
+ const serverHandler = settings.pipe(Option.flatMap((object) => objectField(object, "server")), Option.flatMap((server) => localObject(context, server)), Option.flatMap((server) => objectField(server, "handlers")), Option.isSome);
1790
+ if (dataPath && serverHandler) {
1791
+ context.report({ node, messageId: "direct" });
1792
+ }
1793
+ },
1794
+ };
1795
+ },
1796
+ });
1797
+ // Structure ownership: facts with one owner may be written only in that owner.
1798
+ // Owner paths are exact and relative to Oxlint's working directory.
1799
+ const testFile = (filename) => /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(filename);
1800
+ const ownedBy = (context, files) => Array.some(files, (file) => exactOwnedFile(context.filename, context.cwd, file));
1801
+ const ownerFilesSchema = { type: "array", items: { type: "string", minLength: 1 } };
1802
+ const escapePattern = (text) => text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
1803
+ // The text a source literal writes: strings, template parts, JSX text and the
1804
+ // unescaped source of a regular expression.
1805
+ const writtenText = (node) => {
1806
+ if (node.type === "Literal") {
1807
+ if (Predicate.isString(node.value))
1808
+ return Option.some(node.value);
1809
+ if ("regex" in node) {
1810
+ return Option.some(node.regex.pattern.replaceAll(/\\(.)/gu, "$1"));
1811
+ }
1812
+ return Option.none();
1813
+ }
1814
+ if (node.type === "TemplateElement")
1815
+ return Option.fromNullishOr(node.value.cooked);
1816
+ if (node.type === "JSXText")
1817
+ return Option.some(node.value);
1818
+ return Option.none();
1819
+ };
1820
+ const textVisitors = (check) => {
1821
+ const visit = (node) => {
1822
+ Option.match(writtenText(node), {
1823
+ onNone: Function.constVoid,
1824
+ onSome: (text) => {
1825
+ check(node, text);
1826
+ },
1827
+ });
1828
+ };
1829
+ return { Literal: visit, TemplateElement: visit, JSXText: visit };
1830
+ };
1831
+ const PublicHostOptions = Schema.Struct({
1832
+ hosts: Schema.Array(Schema.String),
1833
+ allowedFiles: Schema.Array(Schema.String),
1834
+ allowTestFiles: Schema.Boolean,
1835
+ });
1836
+ // A configured host or any subdomain of it, as a whole hostname.
1837
+ const hostPattern = (host) => new RegExp(`(?:^|[^a-z0-9-])(?:[a-z0-9-]+\\.)*${escapePattern(host)}(?![a-z0-9-])`, "iu");
1838
+ const publicHostLiterals = defineRule({
1839
+ meta: {
1840
+ type: "problem",
1841
+ schema: [
1842
+ {
1843
+ type: "object",
1844
+ properties: {
1845
+ hosts: {
1846
+ type: "array",
1847
+ minItems: 1,
1848
+ items: { type: "string", pattern: "^[a-z0-9-]+(?:\\.[a-z0-9-]+)+$" },
1849
+ },
1850
+ allowedFiles: ownerFilesSchema,
1851
+ allowTestFiles: { type: "boolean" },
1852
+ },
1853
+ required: ["hosts", "allowedFiles", "allowTestFiles"],
1854
+ additionalProperties: false,
1855
+ },
1856
+ ],
1857
+ messages: {
1858
+ host: "Write {{host}} only in its configured owners. Build addresses from the checked origin binding and take hostnames from the zone owner.",
1859
+ },
1860
+ },
1861
+ create(context) {
1862
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(PublicHostOptions)));
1863
+ if (Option.isNone(options) ||
1864
+ ownedBy(context, options.value.allowedFiles) ||
1865
+ (options.value.allowTestFiles && testFile(context.filename))) {
1866
+ return {};
1867
+ }
1868
+ const patterns = Array.map(options.value.hosts, (host) => [
1869
+ host,
1870
+ hostPattern(host),
1871
+ ]);
1872
+ return textVisitors((node, text) => {
1873
+ Option.match(Array.findFirst(patterns, ([, pattern]) => pattern.test(text)), {
1874
+ onNone: Function.constVoid,
1875
+ onSome: ([host]) => {
1876
+ context.report({ node, messageId: "host", data: { host } });
1877
+ },
1878
+ });
1879
+ });
1880
+ },
1881
+ });
1882
+ const RouteLiteralOptions = Schema.Struct({
1883
+ prefixes: Schema.Array(Schema.String),
1884
+ allowedFiles: Schema.Array(Schema.String),
1885
+ allowTestFiles: Schema.Boolean,
1886
+ });
1887
+ const routePrefix = (prefixes, text) => Array.findFirst(prefixes, (prefix) => {
1888
+ const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1889
+ return text === base || text.startsWith(`${base}/`) || text.startsWith(`${base}?`);
1890
+ });
1891
+ // TanStack requires each file route to name its own path; the route tree checks it.
1892
+ const fileRoutePath = (context, node) => Option.fromNullishOr(node.parent).pipe(Option.exists((parent) => parent.type === "CallExpression" &&
1893
+ Option.exists(Array.head(parent.arguments), (argument) => argument === node) &&
1894
+ scopedImportedReference(context, parent.callee, [], Function.constTrue).pipe(Option.exists((reference) => isExport(reference, "@tanstack/react-router", "createFileRoute") ||
1895
+ isExport(reference, "@tanstack/react-start", "createFileRoute")))));
1896
+ const routeLiteralOwnership = defineRule({
1897
+ meta: {
1898
+ type: "problem",
1899
+ schema: [
1900
+ {
1901
+ type: "object",
1902
+ properties: {
1903
+ prefixes: {
1904
+ type: "array",
1905
+ minItems: 1,
1906
+ items: { type: "string", pattern: "^/[^/]" },
1907
+ },
1908
+ allowedFiles: ownerFilesSchema,
1909
+ allowTestFiles: { type: "boolean" },
1910
+ },
1911
+ required: ["prefixes", "allowedFiles", "allowTestFiles"],
1912
+ additionalProperties: false,
1913
+ },
1914
+ ],
1915
+ messages: {
1916
+ route: "Paths under {{prefix}} belong to the route catalogue. Import its constants or Schemas instead of writing the path here.",
1917
+ },
1918
+ },
1919
+ create(context) {
1920
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(RouteLiteralOptions)));
1921
+ if (Option.isNone(options) ||
1922
+ ownedBy(context, options.value.allowedFiles) ||
1923
+ (options.value.allowTestFiles && testFile(context.filename))) {
1924
+ return {};
1925
+ }
1926
+ const { prefixes } = options.value;
1927
+ return textVisitors((node, text) => {
1928
+ if (node.type === "JSXText" || fileRoutePath(context, node))
1929
+ return;
1930
+ Option.match(routePrefix(prefixes, text), {
1931
+ onNone: Function.constVoid,
1932
+ onSome: (prefix) => {
1933
+ context.report({ node, messageId: "route", data: { prefix } });
1934
+ },
1935
+ });
1936
+ });
1937
+ },
1938
+ });
1939
+ // Fetch client Layers and request settings carry redirect and credential policy.
1940
+ const fetchClientExports = ["layer", "RequestInit", "Fetch"];
1941
+ const httpClientOwnership = defineRule({
1942
+ meta: {
1943
+ type: "problem",
1944
+ schema: boundarySchema,
1945
+ messages: {
1946
+ client: "Build Fetch HTTP client Layers and request settings only in their configured owners. Import the owner's client Layer for this traffic.",
1947
+ },
1948
+ },
1949
+ create(context) {
1950
+ if (isBoundary(context.filename, context.cwd, context.options))
1951
+ return {};
1952
+ const check = (node) => {
1953
+ if (Option.exists(scopedEffectReference(context, node), (reference) => reference.module === "effect/http/FetchHttpClient" &&
1954
+ Option.exists(reference.name, (name) => Array.contains(fetchClientExports, name))))
1955
+ context.report({ node, messageId: "client" });
1956
+ };
1957
+ return {
1958
+ Identifier(node) {
1959
+ const read = Function.pipe(scopedVariable(context.sourceCode.getScope(node), node.name), Option.exists((variable) => Array.some(variable.references, (reference) => reference.identifier === node && reference.isRead())));
1960
+ if (read)
1961
+ check(node);
1962
+ },
1963
+ MemberExpression: check,
1964
+ };
1965
+ },
1966
+ });
1967
+ const WorkerSettingsOptions = Schema.Struct({
1968
+ properties: Schema.Array(Schema.String),
1969
+ allowedFiles: Schema.Array(Schema.String),
1970
+ });
1971
+ // A value written in place, rather than taken from the shared settings owner.
1972
+ const writtenValue = (value) => value.type === "Literal" ||
1973
+ value.type === "TemplateLiteral" ||
1974
+ value.type === "ObjectExpression" ||
1975
+ value.type === "ArrayExpression" ||
1976
+ (value.type === "BinaryExpression" && value.operator === "+");
1977
+ const workerSettingsOwnership = defineRule({
1978
+ meta: {
1979
+ type: "problem",
1980
+ schema: [
1981
+ {
1982
+ type: "object",
1983
+ properties: {
1984
+ properties: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
1985
+ allowedFiles: ownerFilesSchema,
1986
+ },
1987
+ required: ["properties", "allowedFiles"],
1988
+ additionalProperties: false,
1989
+ },
1990
+ ],
1991
+ messages: {
1992
+ setting: "Take {{property}} from the shared Worker settings owner instead of writing it in this declaration.",
1993
+ },
1994
+ },
1995
+ create(context) {
1996
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(WorkerSettingsOptions)));
1997
+ if (Option.isNone(options) || ownedBy(context, options.value.allowedFiles))
1998
+ return {};
1999
+ const { properties } = options.value;
2000
+ return {
2001
+ Property(node) {
2002
+ if (node.parent.type !== "ObjectExpression" || !writtenValue(node.value))
2003
+ return;
2004
+ Option.match(staticPropertyName(node.key, node.computed).pipe(Option.filter((name) => Array.contains(properties, name))), {
2005
+ onNone: Function.constVoid,
2006
+ onSome: (property) => {
2007
+ context.report({ node, messageId: "setting", data: { property } });
2008
+ },
2009
+ });
2010
+ },
2011
+ };
2012
+ },
2013
+ });
2014
+ const OwnerPathOptions = Schema.Struct({ roots: Schema.Array(Schema.String) });
2015
+ const noOwnerPathsInRules = defineRule({
2016
+ meta: {
2017
+ type: "problem",
2018
+ schema: [
2019
+ {
2020
+ type: "object",
2021
+ properties: {
2022
+ roots: { type: "array", minItems: 1, items: { type: "string", pattern: "^[\\w.-]+$" } },
2023
+ },
2024
+ required: ["roots"],
2025
+ additionalProperties: false,
2026
+ },
2027
+ ],
2028
+ messages: {
2029
+ path: "Lint rules receive owner paths as configuration options. Do not write {{path}} in rule code or messages.",
2030
+ },
2031
+ },
2032
+ create(context) {
2033
+ const options = Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(OwnerPathOptions)));
2034
+ if (Option.isNone(options))
2035
+ return {};
2036
+ const roots = Array.join(Array.map(options.value.roots, escapePattern), "|");
2037
+ const pattern = new RegExp(`(?:^|[^\\w.-])\\/?((?:${roots})\\/[\\w./-]*\\.[cm]?[jt]sx?)(?!\\w)`, "u");
2038
+ return textVisitors((node, text) => {
2039
+ Option.match(Option.fromNullishOr(pattern.exec(text)), {
2040
+ onNone: Function.constVoid,
2041
+ onSome: (match) => {
2042
+ context.report({
2043
+ node,
2044
+ messageId: "path",
2045
+ data: { path: Option.getOrElse(Array.get(match, 1), () => text) },
2046
+ });
2047
+ },
2048
+ });
2049
+ });
2050
+ },
2051
+ });
2052
+ // Schema policy: shared Schema owners, checked constant maps and saved IDs.
2053
+ // Options name exact files and identifiers. Oxlint calls a rule factory
2054
+ // synchronously, so options get one non-throwing Schema check; missing or
2055
+ // malformed options are reported rather than silently skipping protection.
2056
+ const ruleOptions = (schema) => (context) => Array.head(context.options).pipe(Option.flatMap(Schema.decodeUnknownOption(schema)));
2057
+ const optionsVisitor = (context, options) => ({
2058
+ Program(node) {
2059
+ if (Option.isNone(options))
2060
+ context.report({ node, messageId: "options" });
2061
+ },
2062
+ });
2063
+ // Literal repository files, never suffixes, globs or parent paths.
2064
+ const policyFilePattern = /^(?!\/)(?!.*\/\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))[A-Za-z0-9_@./-]+\.[cm]?[jt]sx?$/u;
2065
+ const PolicyFile = Schema.String.check(Schema.isPattern(policyFilePattern)).pipe(Schema.brand("SchemaPolicyFile"));
2066
+ const PolicyName = Schema.String.check(Schema.isPattern(/^[A-Za-z_$][\w$]*$/u)).pipe(Schema.brand("SchemaPolicyName"));
2067
+ const PolicyModule = Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(300)).pipe(Schema.brand("SchemaPolicyModule"));
2068
+ const PolicyConstraint = Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(1000)).pipe(Schema.brand("SchemaPolicyConstraint"));
2069
+ const nonEmpty = (item) => Schema.Array(item).check(Schema.isMinLength(1));
2070
+ const SchemaOwner = Schema.Struct({
2071
+ file: PolicyFile,
2072
+ module: PolicyModule,
2073
+ names: nonEmpty(PolicyName),
2074
+ });
2075
+ const SharedSchemaOptions = Schema.Struct({
2076
+ owners: nonEmpty(SchemaOwner),
2077
+ fields: Schema.Array(Schema.Struct({
2078
+ file: PolicyFile,
2079
+ declaration: PolicyName,
2080
+ field: PolicyName,
2081
+ schema: PolicyName,
2082
+ })),
2083
+ });
2084
+ const ConstantMapOptions = Schema.Struct({
2085
+ maps: nonEmpty(Schema.Struct({ file: PolicyFile, name: PolicyName, constraint: PolicyConstraint })),
2086
+ });
2087
+ const PersistedIdOptions = Schema.Struct({
2088
+ schemas: nonEmpty(Schema.Struct({ file: PolicyFile, names: nonEmpty(PolicyName) })),
2089
+ });
2090
+ // JSON schemas mirroring the Schema shapes for Oxlint's configuration check.
2091
+ const jsonText = { type: "string", minLength: 1 };
2092
+ const jsonNames = { type: "array", items: jsonText, minItems: 1, uniqueItems: true };
2093
+ const jsonFile = { type: "string", pattern: policyFilePattern.source };
2094
+ const jsonObject = (properties) => ({
2095
+ type: "object",
2096
+ properties,
2097
+ required: Record.keys(properties),
2098
+ additionalProperties: false,
2099
+ });
2100
+ const jsonList = (properties, minItems) => ({
2101
+ type: "array",
2102
+ items: jsonObject(properties),
2103
+ minItems,
2104
+ });
2105
+ const isPolicyFile = (context, file) => exactOwnedFile(context.filename, context.cwd, file);
2106
+ const constantValue = (context, node) => node.type === "Identifier"
2107
+ ? scopedVariable(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap(constantDeclaration), Option.filter((declaration) => declaration.id.type === "Identifier"), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)))
2108
+ : Option.none();
2109
+ // Only local expression structure is inspected; imported implementations remain opaque.
2110
+ const expressionChildren = (node) => {
2111
+ const optional = (child) => Option.toArray(Option.fromNullishOr(child));
2112
+ if (node.type === "CallExpression" || node.type === "NewExpression")
2113
+ return [node.callee, ...node.arguments];
2114
+ if (node.type === "MemberExpression")
2115
+ return [node.object];
2116
+ if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression")
2117
+ return [node.expression];
2118
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression")
2119
+ return optional(node.body);
2120
+ if (node.type === "BlockStatement")
2121
+ return node.body;
2122
+ if (node.type === "ReturnStatement")
2123
+ return optional(node.argument);
2124
+ if (node.type === "ExpressionStatement")
2125
+ return [node.expression];
2126
+ if (node.type === "VariableDeclaration")
2127
+ return node.declarations;
2128
+ if (node.type === "VariableDeclarator")
2129
+ return optional(node.init);
2130
+ if (node.type === "ObjectExpression")
2131
+ return node.properties;
2132
+ if (node.type === "Property")
2133
+ return [node.value];
2134
+ if (node.type === "SpreadElement")
2135
+ return [node.argument];
2136
+ if (node.type === "ArrayExpression")
2137
+ return Array.flatMap(node.elements, optional);
2138
+ if (node.type === "ConditionalExpression")
2139
+ return [node.test, node.consequent, node.alternate];
2140
+ if (node.type === "BinaryExpression" || node.type === "LogicalExpression")
2141
+ return [node.left, node.right];
2142
+ return [];
2143
+ };
2144
+ // Every node an expression is built from, following local immutable names and
2145
+ // local function bodies. A repeated node stops a cycle.
2146
+ const expressionNodes = (context, node, visited = []) => {
2147
+ if (Array.some(visited, (previous) => previous === node))
2148
+ return [];
2149
+ const next = [...visited, node];
2150
+ const children = [...expressionChildren(node), ...Option.toArray(constantValue(context, node))];
2151
+ return [node, ...Array.flatMap(children, (child) => expressionNodes(context, child, next))];
2152
+ };
2153
+ const topLevel = (node) => node.parent?.type === "VariableDeclaration" &&
2154
+ (node.parent.parent?.type === "Program" || node.parent.parent?.type === "ExportNamedDeclaration");
2155
+ // A Schema and its public `typeof Schema.Type` alias share a TypeScript binding.
2156
+ // Accept that pair, while still requiring one immutable value declaration.
2157
+ const schemaDeclaration = (variable) => constantDeclaration(variable, Array.filter(variable.defs, (definition) => definition.node.type === "VariableDeclarator"));
2158
+ // An import names the owner by its module, or by a relative path that resolves
2159
+ // to the owner file. Nothing is loaded or executed.
2160
+ const ownerImport = (context, source, owner) => {
2161
+ if (source === owner.module)
2162
+ return true;
2163
+ if (!source.startsWith("."))
2164
+ return false;
2165
+ const resolved = relativeModule(context.filename, source);
2166
+ const target = relativeModule(`${context.cwd}/.`, `./${owner.file}`);
2167
+ return resolved === target || resolved === target.replace(/\.[cm]?[jt]sx?$/u, "");
2168
+ };
2169
+ const ownerOf = (owners, name) => Array.findFirst(owners, (owner) => Array.contains(owner.names, name));
2170
+ const ownerExport = (context, value, name, owner) => scopedImportedReference(context, value).pipe(Option.exists((reference) => Option.contains(reference.name, name) && ownerImport(context, reference.module, owner)));
2171
+ // The owner's export under any alias, or the owner's own top-level declaration.
2172
+ // Optional Schema wrappers keep the shared value.
2173
+ const optionalWrappers = [
2174
+ "OptionFromOptionalKey",
2175
+ "optionalKey",
2176
+ "optional",
2177
+ ];
2178
+ const isSharedValue = (context, value, name, owners, visited = []) => {
2179
+ if (Array.some(visited, (previous) => previous === value))
2180
+ return false;
2181
+ const next = [...visited, value];
2182
+ const owner = ownerOf(owners, name);
2183
+ if (Option.exists(owner, (candidate) => ownerExport(context, value, name, candidate))) {
2184
+ return true;
2185
+ }
2186
+ if (value.type === "Identifier") {
2187
+ const localOwner = scopedVariable(context.sourceCode.getScope(value), value.name).pipe(Option.flatMap(schemaDeclaration), Option.exists((local) => local.id.type === "Identifier" &&
2188
+ local.id.name === name &&
2189
+ topLevel(local) &&
2190
+ Option.exists(owner, (candidate) => isPolicyFile(context, candidate.file))));
2191
+ return (localOwner ||
2192
+ Option.exists(constantValue(context, value), (initialiser) => isSharedValue(context, initialiser, name, owners, next)));
2193
+ }
2194
+ return (value.type === "CallExpression" &&
2195
+ Option.exists(scopedEffectReference(context, value.callee), (reference) => Array.some(optionalWrappers, (wrapper) => isExport(reference, "effect/Schema", wrapper))) &&
2196
+ Option.exists(Array.head(value.arguments), (argument) => isSharedValue(context, argument, name, owners, next)));
2197
+ };
2198
+ // The single static value of one field in the declaration's `Schema.Struct`.
2199
+ // Spreads, computed keys and duplicates leave no single value to check.
2200
+ const structFieldValue = (context, node, field) => Array.findFirst(expressionNodes(context, node), (expression) => expression.type === "CallExpression" &&
2201
+ Option.exists(scopedEffectReference(context, expression.callee), (reference) => isExport(reference, "effect/Schema", "Struct"))).pipe(Option.flatMap((call) => call.type === "CallExpression" ? Array.head(call.arguments) : Option.none()), Option.filter((argument) => argument.type === "ObjectExpression"), Option.flatMap((object) => {
2202
+ const properties = Array.flatMap(object.properties, (property) => property.type === "Property" && (!property.computed || property.key.type === "Literal")
2203
+ ? [property]
2204
+ : []);
2205
+ const matches = Array.filter(properties, (property) => Option.contains(staticPropertyName(property.key, property.computed), field));
2206
+ return properties.length === object.properties.length && matches.length === 1
2207
+ ? Option.map(Array.head(matches), (property) => property.value)
2208
+ : Option.none();
2209
+ }));
2210
+ const sharedSchemaOwner = defineRule({
2211
+ meta: {
2212
+ type: "problem",
2213
+ schema: [
2214
+ jsonObject({
2215
+ owners: jsonList({ file: jsonFile, module: jsonText, names: jsonNames }, 1),
2216
+ fields: jsonList({ file: jsonFile, declaration: jsonText, field: jsonText, schema: jsonText }, 0),
2217
+ }),
2218
+ ],
2219
+ messages: {
2220
+ options: "Configure the shared schema owners and the fields that must reuse them.",
2221
+ owner: "Define {{name}} only in its configured owner ({{owner}}); import the shared schema elsewhere.",
2222
+ field: "Use the shared {{schema}} for {{declaration}}.{{field}}; do not copy or widen its validation.",
2223
+ },
2224
+ },
2225
+ create(context) {
2226
+ const options = ruleOptions(SharedSchemaOptions)(context);
2227
+ const owners = Option.match(options, { onNone: () => [], onSome: (value) => value.owners });
2228
+ const fields = Option.match(options, { onNone: () => [], onSome: (value) => value.fields });
2229
+ // A local `const` may alias the owner's export of the same name.
2230
+ const importedAlias = (node, name) => scopedVariable(context.sourceCode.getScope(node), name).pipe(Option.flatMap(constantDeclaration), Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.exists((value) => Array.some(owners, (owner) => Array.contains(owner.names, name) && ownerExport(context, value, name, owner))));
2231
+ const checkName = (node, name, alias) => {
2232
+ Option.match(ownerOf(owners, name), {
2233
+ onNone: Function.constVoid,
2234
+ onSome: (owner) => {
2235
+ if (!isPolicyFile(context, owner.file) && !(alias && importedAlias(node, name)))
2236
+ context.report({ node, messageId: "owner", data: { name, owner: owner.file } });
2237
+ },
2238
+ });
2239
+ };
2240
+ return {
2241
+ ...optionsVisitor(context, options),
2242
+ TSTypeAliasDeclaration(node) {
2243
+ checkName(node, node.id.name, false);
2244
+ },
2245
+ TSInterfaceDeclaration(node) {
2246
+ checkName(node, node.id.name, false);
2247
+ },
2248
+ VariableDeclarator(node) {
2249
+ if (node.id.type !== "Identifier")
2250
+ return;
2251
+ const { name } = node.id;
2252
+ checkName(node, name, true);
2253
+ Array.forEach(fields, (field) => {
2254
+ if (!isPolicyFile(context, field.file) || field.declaration !== name || !node.init)
2255
+ return;
2256
+ const shared = Option.exists(structFieldValue(context, node.init, field.field), (value) => isSharedValue(context, value, field.schema, owners));
2257
+ if (!shared)
2258
+ context.report({
2259
+ node,
2260
+ messageId: "field",
2261
+ data: { schema: field.schema, declaration: name, field: field.field },
2262
+ });
2263
+ });
2264
+ },
2265
+ };
2266
+ },
2267
+ });
2268
+ // This preserves a configured declaration, not TypeScript assignability. The
2269
+ // compiler checks the constraint; type and encoding tests protect its meaning.
2270
+ const compactText = (source) => source.replaceAll(/\s/gu, "");
2271
+ const checkedConstantMap = defineRule({
2272
+ meta: {
2273
+ type: "problem",
2274
+ schema: [
2275
+ jsonObject({ maps: jsonList({ file: jsonFile, name: jsonText, constraint: jsonText }, 1) }),
2276
+ ],
2277
+ messages: {
2278
+ options: "Configure exact files, map names and their checked type constraints.",
2279
+ checked: "Keep {{name}} as an unannotated const literal with `as const satisfies {{constraint}}`.",
2280
+ },
2281
+ },
2282
+ create(context) {
2283
+ const options = ruleOptions(ConstantMapOptions)(context);
2284
+ return {
2285
+ ...optionsVisitor(context, options),
2286
+ VariableDeclarator(node) {
2287
+ if (node.id.type !== "Identifier")
2288
+ return;
2289
+ const { name } = node.id;
2290
+ const map = Option.flatMap(options, ({ maps }) => Array.findFirst(maps, (entry) => isPolicyFile(context, entry.file) && entry.name === name));
2291
+ if (Option.isNone(map))
2292
+ return;
2293
+ const { constraint } = map.value;
2294
+ const { init } = node;
2295
+ const valid = node.parent?.type === "VariableDeclaration" &&
2296
+ node.parent.kind === "const" &&
2297
+ !node.id.typeAnnotation &&
2298
+ init?.type === "TSSatisfiesExpression" &&
2299
+ compactText(context.sourceCode.getText(init.typeAnnotation)) ===
2300
+ compactText(constraint) &&
2301
+ init.expression.type === "TSAsExpression" &&
2302
+ init.expression.typeAnnotation.type === "TSTypeReference" &&
2303
+ init.expression.typeAnnotation.typeName.type === "Identifier" &&
2304
+ init.expression.typeAnnotation.typeName.name === "const" &&
2305
+ (init.expression.expression.type === "ObjectExpression" ||
2306
+ init.expression.expression.type === "ArrayExpression");
2307
+ if (!valid)
2308
+ context.report({ node, messageId: "checked", data: { name, constraint } });
2309
+ },
2310
+ };
2311
+ },
2312
+ });
2313
+ // Validation and brands keep the saved text. Value-changing codecs need a
2314
+ // deliberate compatibility change, never an automatic fix.
2315
+ const persistedIdCodecs = [
2316
+ "decodeTo",
2317
+ "encodeTo",
2318
+ "decode",
2319
+ "encode",
2320
+ "Trim",
2321
+ ];
2322
+ const noPersistedIdNormalisation = defineRule({
2323
+ meta: {
2324
+ type: "problem",
2325
+ schema: [jsonObject({ schemas: jsonList({ file: jsonFile, names: jsonNames }, 1) })],
2326
+ messages: {
2327
+ options: "Configure the exact saved-ID schema files and names.",
2328
+ normalisation: "Keep {{name}}'s saved text unchanged. A transforming codec needs an explicit compatibility change and saved-data tests.",
2329
+ },
2330
+ },
2331
+ create(context) {
2332
+ const options = ruleOptions(PersistedIdOptions)(context);
2333
+ return {
2334
+ ...optionsVisitor(context, options),
2335
+ VariableDeclarator(node) {
2336
+ if (node.id.type !== "Identifier" || !node.init)
2337
+ return;
2338
+ const { name } = node.id;
2339
+ const saved = Option.exists(options, ({ schemas }) => Array.some(schemas, (schema) => isPolicyFile(context, schema.file) && Array.contains(schema.names, name)));
2340
+ const transforms = saved &&
2341
+ Array.some(expressionNodes(context, node.init), (expression) => Option.exists(scopedEffectReference(context, expression), (reference) => Array.some(persistedIdCodecs, (codec) => isExport(reference, "effect/Schema", codec)) ||
2342
+ (reference.module === "effect/SchemaTransformation" &&
2343
+ Option.isSome(reference.name))));
2344
+ if (transforms)
2345
+ context.report({ node, messageId: "normalisation", data: { name } });
2346
+ },
2347
+ };
2348
+ },
2349
+ });
2350
+ export default eslintCompatPlugin({
2351
+ meta: { name: "strict-effect" },
2352
+ rules: {
2353
+ "checked-page-response": checkedPageResponse,
2354
+ "native-cloudflare-cache": nativeCloudflareCache,
2355
+ "website-data-proxy": websiteDataProxy,
2356
+ "no-loader-request-state": noLoaderRequestState,
2357
+ "no-native-client-replica": noNativeClientReplica,
2358
+ "website-rpc-boundary": websiteRpcBoundary,
2359
+ "static-layers": staticLayers,
2360
+ "no-unchecked-index": noUncheckedIndex,
2361
+ "no-native-at": noNativeAt,
2362
+ "runtime-file-convention": runtimeFileConvention,
2363
+ "tagged-error-name": taggedErrorName,
2364
+ "error-constructor-new": errorConstructorNew,
2365
+ "no-promise-workflow": noPromiseWorkflow,
2366
+ "module-ownership": moduleOwnership,
2367
+ "no-unsafe-option-unwrap": noUnsafeOptionUnwrap,
2368
+ "no-unchecked-json": noUncheckedJson,
2369
+ "no-runtime-outside-boundary": noRuntimeOutsideBoundary,
2370
+ "no-native-work": noNativeWork,
2371
+ "no-imperative-collections": noImperativeCollections,
2372
+ "public-host-literals": publicHostLiterals,
2373
+ "route-literal-ownership": routeLiteralOwnership,
2374
+ "http-client-ownership": httpClientOwnership,
2375
+ "worker-settings-ownership": workerSettingsOwnership,
2376
+ "no-owner-paths-in-rules": noOwnerPathsInRules,
2377
+ "shared-schema-owner": sharedSchemaOwner,
2378
+ "checked-constant-map": checkedConstantMap,
2379
+ "no-persisted-id-normalisation": noPersistedIdNormalisation,
2380
+ },
2381
+ });