@doubleloop-io/oxlint-plugin-effect 0.1.0-rc.2 → 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 (45) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +16 -199
  3. package/docs/user-guide/README.md +24 -0
  4. package/docs/user-guide/alongside-effect-tsgo.md +23 -0
  5. package/docs/user-guide/common-limits.md +10 -0
  6. package/docs/user-guide/configuration.md +43 -0
  7. package/docs/user-guide/default-configuration.md +88 -0
  8. package/docs/user-guide/installation.md +12 -0
  9. package/docs/user-guide/rules/adapter-import-boundary.md +27 -0
  10. package/docs/user-guide/rules/all-errors-handled-at-boundary.md +30 -0
  11. package/docs/user-guide/rules/anti-slop-no-manual-effect-error-tag.md +26 -0
  12. package/docs/user-guide/rules/anti-slop-no-service-constructor-imports.md +32 -0
  13. package/docs/user-guide/rules/anti-slop-prefer-effect-match.md +30 -0
  14. package/docs/user-guide/rules/exported-schema-identifier.md +26 -0
  15. package/docs/user-guide/rules/no-asymmetric-match-handlers.md +30 -0
  16. package/docs/user-guide/rules/no-effect-promise.md +26 -0
  17. package/docs/user-guide/rules/no-effect-return-type.md +29 -0
  18. package/docs/user-guide/rules/no-inline-schema-codec.md +26 -0
  19. package/docs/user-guide/rules/no-inline-yield-star.md +29 -0
  20. package/docs/user-guide/rules/no-manual-discriminant-comparison.md +36 -0
  21. package/docs/user-guide/rules/no-manual-discriminant-construction.md +32 -0
  22. package/docs/user-guide/rules/no-nested-match.md +44 -0
  23. package/docs/user-guide/rules/no-parameterless-catch-all.md +28 -0
  24. package/docs/user-guide/rules/no-provide-on-definition.md +27 -0
  25. package/docs/user-guide/rules/no-throw-or-primitive-failure.md +34 -0
  26. package/docs/user-guide/rules/no-transport-types-in-generator.md +41 -0
  27. package/docs/user-guide/rules/no-yielded-match.md +35 -0
  28. package/docs/user-guide/rules/prefer-effect-fn.md +31 -0
  29. package/docs/user-guide/rules/prefer-effect-vitest.md +30 -0
  30. package/docs/user-guide/rules/provide-named-layer.md +27 -0
  31. package/docs/user-guide/rules/redacted-secret-names.md +31 -0
  32. package/docs/user-guide/rules/run-only-in-runner.md +32 -0
  33. package/docs/user-guide/rules/service-built-with-of.md +36 -0
  34. package/docs/user-guide/rules/service-key-matches-name.md +29 -0
  35. package/docs/user-guide/rules/service-port-shape-only.md +27 -0
  36. package/docs/user-guide/rules/single-style-per-function.md +36 -0
  37. package/docs/user-guide/rules/split-success-path-from-error-handling.md +37 -0
  38. package/docs/user-guide/rules/try-object-form.md +27 -0
  39. package/docs/user-guide/rules/untraced-recursion.md +31 -0
  40. package/docs/user-guide/rules.md +26 -0
  41. package/docs/user-guide/shared-settings.md +65 -0
  42. package/docs/user-guide/troubleshooting.md +30 -0
  43. package/docs/user-guide/versioning.md +8 -0
  44. package/package.json +15 -9
  45. package/RULES.md +0 -1056
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # @doubleloop-io/oxlint-plugin-effect
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`2c67281`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/2c672814db1d169752d04295374001b935b785f5) Thanks [@iacoware](https://github.com/iacoware)! - The documentation has a website, now the package homepage: https://doubleloop-io.github.io/oxlint-plugin-effect/
8
+
9
+ - The rule catalog moves from `RULES.md`, no longer in the package, to one file per rule in `docs/user-guide/rules/`; the guide ships in the package.
10
+ - The README becomes the essential guide and links to the website.
11
+ - New Troubleshooting page.
12
+ - Improve rules documentation.
13
+
14
+ ## 0.1.0
15
+
16
+ First stable release.
17
+
18
+ - 30 rules for Effect code, grouped by topic in [`docs/user-guide/rules.md`](docs/user-guide/rules.md): error channel, tagged values, services and Layers, runtime and entry point, design and boundaries, effectful functions, code shape, Schema, config and secrets. Three are absorbed from [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop).
19
+ - An `all` preset that enables every rule at `warn`, for `oxlint.config.ts` and `.oxlintrc.json`.
20
+ - Shared settings under `settings["dl-effect"]`: `entryFiles`, `testFiles`, `discriminants`.
21
+ - Written for Effect 4; rules also recognize the Effect 3 name of the same API.
22
+ - Requires Node `^22.20.0 || >=24.8.0` and oxlint `^1.82.0`.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  An oxlint plugin that enforces a catalog of patterns for Effect 4 code.
4
4
 
5
- The rules are written and tested against Effect 4. Where an Effect 3 API has the same intent, the rules recognize its v3 name too: each rule's Trigger in [`RULES.md`](RULES.md) names them.
5
+ The rules are written and tested against Effect 4. Where an Effect 3 API has the same intent, the rules recognize its v3 name too: each rule's Trigger names them. The [Rules overview](https://doubleloop-io.github.io/oxlint-plugin-effect/rules-overview/) lists every rule.
6
6
 
7
- It complements the type-aware rules of [`@effect/tsgo`](#alongside-effecttsgo): it does not need them, and it checks what they do not.
7
+ It complements the type-aware rules of [`@effect/tsgo`](https://doubleloop-io.github.io/oxlint-plugin-effect/alongside-effect-tsgo/): it does not need them, and it checks what they do not.
8
8
 
9
9
  ## Installation
10
10
 
@@ -12,9 +12,9 @@ It complements the type-aware rules of [`@effect/tsgo`](#alongside-effecttsgo):
12
12
  pnpm add -D @doubleloop-io/oxlint-plugin-effect oxlint
13
13
  ```
14
14
 
15
- Requires Node `^22.20.0 || >=24.8.0` and oxlint `^1.82.0`.
15
+ Requires Node `^22.20.0 || >=24.8.0` and oxlint `^1.82.0`. What a release may change and which oxlint versions are supported: [Versioning](https://doubleloop-io.github.io/oxlint-plugin-effect/versioning/).
16
16
 
17
- ## Configuration
17
+ ## Usage
18
18
 
19
19
  Extend the `all` preset. Prefer `oxlint.config.ts`, where `extends` takes config objects and the preset is imported by package name:
20
20
 
@@ -33,203 +33,20 @@ With `.oxlintrc.json`, `extends` takes file paths only, so the preset is reached
33
33
  }
34
34
  ```
35
35
 
36
- `all` enables every rule at `warn`, so adopting it never breaks a build: look at the warnings first, then raise to `error` the rules you want to enforce. A `recommended` preset with reasoned severities will come from measurements on real codebases.
36
+ `all` enables every rule at `warn`, so adopting it never breaks a build: look at the warnings first, then raise to `error` the rules you want to enforce.
37
37
 
38
- To pick rules without the preset, load the plugin by package name and list the rules:
38
+ **Set `entryFiles` before reading the warnings** on a layout that is not a Node CLI, such as Next.js or a monorepo. Its default, `**/main.ts`, `**/cli.ts`, `**/bin/**`, decides which files may run Effects and import adapters, so on another layout it turns correct code into warnings. How to set it: [Shared settings](https://doubleloop-io.github.io/oxlint-plugin-effect/shared-settings/#setting-a-value).
39
39
 
40
- ```json
41
- {
42
- "jsPlugins": ["@doubleloop-io/oxlint-plugin-effect"],
43
- "rules": {
44
- "dl-effect/no-effect-promise": "error",
45
- "dl-effect/try-object-form": "error"
46
- }
47
- }
48
- ```
49
-
50
- ### Configuration options
51
-
52
- Three settings describe your project, not a rule, so they are declared once under `settings["dl-effect"]` and every rule that uses them reads them from there. Their defaults fit a Node CLI; on another layout (Next.js, a monorepo) set them before reading the warnings.
53
-
54
- - `entryFiles`: the files allowed to run Effects and import adapters. Default `**/main.ts`, `**/cli.ts`, `**/bin/**`.
55
- - `testFiles`: the test files, exempt from the rules that tests legitimately break and the only files `prefer-effect-vitest` checks. Default `**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`, `**/test/**`, `**/tests/**`.
56
- - `discriminants`: the fields that discriminate a closed union. Default `_tag`; add the ones your unions use, such as `type` or `kind`.
57
-
58
- The rules that use each one are listed in [Shared settings](RULES.md#shared-settings). Set one only where its default does not fit: it **replaces** the default instead of extending it. An unknown key or a value that is not an array of strings stops the lint with an error, so a typo is never silently ignored.
59
-
60
- ```ts
61
- import dlEffectAll from "@doubleloop-io/oxlint-plugin-effect/presets/all"
62
- import { defineConfig } from "oxlint"
63
-
64
- export default defineConfig({
65
- extends: [dlEffectAll],
66
- settings: {
67
- "dl-effect": { entryFiles: ["apps/web/src/app/**/route.ts", "apps/web/src/runtime.ts"] },
68
- },
69
- })
70
- ```
71
-
72
- ```json
73
- {
74
- "extends": ["./node_modules/@doubleloop-io/oxlint-plugin-effect/dist/presets/all.json"],
75
- "settings": {
76
- "dl-effect": { "entryFiles": ["apps/web/src/app/**/route.ts", "apps/web/src/runtime.ts"] }
77
- }
78
- }
79
- ```
80
-
81
- The package exports the defaults as `defaultEntryFiles`, `defaultTestFiles` and `defaultDiscriminants`: spread them to extend a default (`testFiles: [...defaultTestFiles, "e2e/**"]`).
82
-
83
- Settings belong to the config that lints: oxlint does not inherit them from a preset in `extends`, and rejects them in `overrides`. For a value that differs by group of files, use `overrides` on the rules instead.
84
-
85
- The other options and every default are in [Default configuration](#default-configuration).
86
-
87
- ### Alongside `@effect/tsgo`
88
-
89
- [`@effect/tsgo`](https://github.com/Effect-TS/tsgo) is the official Effect language service on top of the TypeScript Go compiler; its `effecttsgo` oxlint presets add more than 100 type-aware rules. This plugin does not need it, and its rules target what those presets do not check, so the two complement each other. To use both, follow its [Oxlint Setup guide](https://github.com/Effect-TS/tsgo/blob/main/docs/README.md) and put its preset in `extends` **before** `all`:
90
-
91
- ```ts
92
- import dlEffectAll from "@doubleloop-io/oxlint-plugin-effect/presets/all"
93
- import { recommended as effectTsgoRecommended } from "@effect/tsgo/oxlint-presets"
94
- import { defineConfig } from "oxlint"
95
-
96
- export default defineConfig({ extends: [effectTsgoRecommended, dlEffectAll] })
97
- ```
98
-
99
- ```json
100
- {
101
- "extends": [
102
- "./node_modules/@effect/tsgo/oxlint-presets/recommended.json",
103
- "./node_modules/@doubleloop-io/oxlint-plugin-effect/dist/presets/all.json"
104
- ]
105
- }
106
- ```
107
-
108
- ## Rules
109
-
110
- What each rule reports and why is in [`RULES.md`](RULES.md).
111
-
112
- | Topic | Rules |
113
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
114
- | Error channel | [`no-throw-or-primitive-failure`](RULES.md#no-throw-or-primitive-failure), [`try-object-form`](RULES.md#try-object-form), [`no-effect-promise`](RULES.md#no-effect-promise), [`no-parameterless-catch-all`](RULES.md#no-parameterless-catch-all), [`split-success-path-from-error-handling`](RULES.md#split-success-path-from-error-handling), [`anti-slop-no-manual-effect-error-tag`](RULES.md#anti-slop-no-manual-effect-error-tag) |
115
- | Tagged values | [`no-manual-discriminant-comparison`](RULES.md#no-manual-discriminant-comparison), [`no-manual-discriminant-construction`](RULES.md#no-manual-discriminant-construction) |
116
- | Services & Layers | [`service-port-shape-only`](RULES.md#service-port-shape-only), [`service-key-matches-name`](RULES.md#service-key-matches-name), [`service-built-with-of`](RULES.md#service-built-with-of), [`provide-named-layer`](RULES.md#provide-named-layer), [`no-provide-on-definition`](RULES.md#no-provide-on-definition), [`anti-slop-no-service-constructor-imports`](RULES.md#anti-slop-no-service-constructor-imports) |
117
- | Runtime & entrypoint | [`run-only-in-runner`](RULES.md#run-only-in-runner), [`prefer-effect-vitest`](RULES.md#prefer-effect-vitest), [`all-errors-handled-at-boundary`](RULES.md#all-errors-handled-at-boundary) |
118
- | Design & boundaries | [`adapter-import-boundary`](RULES.md#adapter-import-boundary), [`no-transport-types-in-generator`](RULES.md#no-transport-types-in-generator) |
119
- | Effectful functions | [`prefer-effect-fn`](RULES.md#prefer-effect-fn), [`untraced-recursion`](RULES.md#untraced-recursion), [`single-style-per-function`](RULES.md#single-style-per-function) |
120
- | Code shape | [`no-nested-match`](RULES.md#no-nested-match), [`no-yielded-match`](RULES.md#no-yielded-match), [`no-inline-yield-star`](RULES.md#no-inline-yield-star), [`no-effect-return-type`](RULES.md#no-effect-return-type), [`no-asymmetric-match-handlers`](RULES.md#no-asymmetric-match-handlers), [`anti-slop-prefer-effect-match`](RULES.md#anti-slop-prefer-effect-match) |
121
- | Schema | [`exported-schema-identifier`](RULES.md#exported-schema-identifier), [`no-inline-schema-codec`](RULES.md#no-inline-schema-codec) |
122
- | Config & secrets | [`redacted-secret-names`](RULES.md#redacted-secret-names) |
123
-
124
- ### Absorbed from anti-slop
125
-
126
- Some rules come from the Effect rules of [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop), MIT-licensed: see [`LICENSE.anti-slop`](LICENSE.anti-slop).
127
-
128
- - Imported: [`anti-slop-no-manual-effect-error-tag`](RULES.md#anti-slop-no-manual-effect-error-tag), [`anti-slop-no-service-constructor-imports`](RULES.md#anti-slop-no-service-constructor-imports), [`anti-slop-prefer-effect-match`](RULES.md#anti-slop-prefer-effect-match).
129
- - Superseded: `no-manual-tag-comparison` by [`no-manual-discriminant-comparison`](RULES.md#no-manual-discriminant-comparison), `no-manual-tagged-construction` by [`no-manual-discriminant-construction`](RULES.md#no-manual-discriminant-construction).
130
-
131
- ## Default configuration
132
-
133
- `all` is equivalent to the config below, with every setting and option spelled out at its default. Both **replace** the defaults instead of extending them: see [Shared settings](RULES.md#shared-settings).
134
-
135
- ```ts
136
- import { defineConfig } from "oxlint"
137
-
138
- // Constructors that must not appear inside an Effect generator: the conversion to the
139
- // transport belongs in the second argument of Effect.fn. Add your own transport DTOs.
140
- const transportTypes = ["Response", "NextResponse"]
141
-
142
- // Schema fields holding a secret: a field matches an entry or ends with it capitalized
143
- // (`password` matches `dbPassword`). No generic `token`: `nextPageToken` is not a secret.
144
- const secretFields = ["password", "secret", "apiKey", "accessToken", "refreshToken", "idToken"]
145
-
146
- export default defineConfig({
147
- jsPlugins: ["@doubleloop-io/oxlint-plugin-effect"],
148
- settings: {
149
- "dl-effect": {
150
- // Files allowed to run Effects (Effect.run*, runMain, ManagedRuntime.make) and to
151
- // import adapters. `index.ts` is left out on purpose: as a barrel it would let
152
- // adapters leak through re-exports.
153
- entryFiles: ["**/main.ts", "**/cli.ts", "**/bin/**"],
154
- // Files exempt from the rules that tests legitimately break. Helper directories such
155
- // as `src/testing/` are support code, not tests, so they are not matched.
156
- testFiles: [
157
- "**/*.test.*",
158
- "**/*.spec.*",
159
- "**/__tests__/**",
160
- "**/test/**",
161
- "**/tests/**",
162
- ],
163
- // Fields that discriminate a closed union, compared or switched on only through an
164
- // exhaustive match and set only by a constructor. Add the ones your unions use, such
165
- // as `type` or `kind`.
166
- discriminants: ["_tag"],
167
- },
168
- },
169
- // Every rule at `warn`, to measure noise without blocking.
170
- rules: {
171
- // Error channel
172
- "dl-effect/no-throw-or-primitive-failure": "warn",
173
- "dl-effect/try-object-form": "warn",
174
- "dl-effect/no-effect-promise": "warn",
175
- "dl-effect/no-parameterless-catch-all": "warn",
176
- "dl-effect/split-success-path-from-error-handling": "warn",
177
- "dl-effect/anti-slop-no-manual-effect-error-tag": "warn",
178
- // Tagged values
179
- "dl-effect/no-manual-discriminant-comparison": "warn",
180
- "dl-effect/no-manual-discriminant-construction": "warn",
181
- // Services & Layers
182
- "dl-effect/service-port-shape-only": "warn",
183
- "dl-effect/service-key-matches-name": "warn",
184
- "dl-effect/service-built-with-of": "warn",
185
- "dl-effect/provide-named-layer": "warn",
186
- "dl-effect/no-provide-on-definition": "warn",
187
- "dl-effect/anti-slop-no-service-constructor-imports": "warn",
188
- // Runtime & entrypoint
189
- "dl-effect/run-only-in-runner": "warn",
190
- "dl-effect/prefer-effect-vitest": "warn",
191
- "dl-effect/all-errors-handled-at-boundary": "warn",
192
- // Design & boundaries
193
- "dl-effect/adapter-import-boundary": "warn",
194
- "dl-effect/no-transport-types-in-generator": ["warn", { transportTypes }],
195
- // Effectful functions
196
- "dl-effect/prefer-effect-fn": "warn",
197
- "dl-effect/untraced-recursion": "warn",
198
- "dl-effect/single-style-per-function": "warn",
199
- // Code shape
200
- "dl-effect/no-nested-match": "warn",
201
- "dl-effect/no-yielded-match": "warn",
202
- "dl-effect/no-inline-yield-star": "warn",
203
- "dl-effect/no-effect-return-type": ["warn", { checkExported: false }],
204
- "dl-effect/no-asymmetric-match-handlers": "warn",
205
- "dl-effect/anti-slop-prefer-effect-match": "warn",
206
- // Schema
207
- "dl-effect/exported-schema-identifier": "warn",
208
- "dl-effect/no-inline-schema-codec": "warn",
209
- // Config & secrets
210
- "dl-effect/redacted-secret-names": ["warn", { secretFields }],
211
- },
212
- })
213
- ```
214
-
215
- To change a setting, declare it in `settings`; to change a rule option, extend `all` and override only that rule.
216
-
217
- ## Versioning
218
-
219
- Versions are `0.x`: any minor release may add, remove or change rules, options, settings and messages.
220
-
221
- Supported oxlint is the `peerDependencies` range, `^1.82.0`; CI tests the tarball against `1.82.0`. oxlint's JS plugin API is still experimental, so a newer oxlint minor may break the plugin before the range is narrowed.
40
+ ## Documentation
222
41
 
223
- ## Development
42
+ The guide is on the [website](https://doubleloop-io.github.io/oxlint-plugin-effect/):
224
43
 
225
- - `pnpm format`: formats with oxfmt; the only script that rewrites sources.
226
- - `pnpm check`: format check, typecheck, lint and tests, writing nothing. The tests are the `RuleTester`s from `oxlint/plugins-dev` with `node --test`, plus an alignment check of `RULES.md` with one section per rule.
227
- - `pnpm build`: compiles to `dist/` and generates `dist/presets/all.json` from `presets/all.ts` (the rule list comes from the plugin).
228
- - `pnpm pack:check`: checks the built preset, then the tarball `npm pack` would publish: only the files in `files`, no local absolute paths, `publint --strict`.
229
- - `pnpm smoke-test`: installs the tarball in `smoke-fixture/` with pnpm and checks the diagnostics of oxlint with each config form: `oxlint.config.ts` and `.oxlintrc.json` with the preset, `.oxlintrc.json` with the plugin by package name.
230
- - `pnpm build-and-pack`: `check`, `build`, `pack:check` and `smoke-test`; run it after every change.
231
- - `pnpm release <bump>`: from a clean `main` equal to `origin/main`, runs `build-and-pack`, bumps with `npm version <bump> --preid rc` (`prerelease` for the next rc, `x.y.z` for a stable), publishes, then pushes the commit and the tag. A prerelease is `next`, and also `latest` until the first stable version; a stable version is `latest`. A failed publish undoes the bump. Without `<bump>`, prints the version in `package.json` and the npm dist-tags.
232
- - `pnpm build:w`: reruns `pnpm build`, preset included, on every change to `index.ts`, `rules/`, `presets/`, `shared/` or `tsconfig.build.json`; a failed build waits for the next change.
233
- - `pnpm consumer:link <consumer>`: after `pnpm build`, replaces the pnpm symlinks of the plugin in every workspace package of `<consumer>` with links to the checkout, so the consumer's own lint runs the checkout's rules; no tracked file of the consumer changes. `pnpm consumer:unlink <consumer>` restores them with `pnpm install --frozen-lockfile`, as does any `pnpm install` in the consumer.
44
+ - [Configuration](https://doubleloop-io.github.io/oxlint-plugin-effect/configuration/): the `all` preset and picking rules.
45
+ - [Shared settings](https://doubleloop-io.github.io/oxlint-plugin-effect/shared-settings/): the settings that describe your project, their defaults and the rules that use them.
46
+ - [Default configuration](https://doubleloop-io.github.io/oxlint-plugin-effect/default-configuration/): `all` with every setting and option spelled out.
47
+ - [Alongside `@effect/tsgo`](https://doubleloop-io.github.io/oxlint-plugin-effect/alongside-effect-tsgo/): combining its presets with this one.
48
+ - [Rules overview](https://doubleloop-io.github.io/oxlint-plugin-effect/rules-overview/): every rule, grouped by topic.
49
+ - [Versioning](https://doubleloop-io.github.io/oxlint-plugin-effect/versioning/): what a release may change.
50
+ - [Troubleshooting](https://doubleloop-io.github.io/oxlint-plugin-effect/troubleshooting/): the problems most often hit when adopting the plugin.
234
51
 
235
- A new rule must be registered in `index.ts` and documented in `RULES.md` under a `` ### `rule-name` `` heading; `pnpm build` adds it to the preset.
52
+ Coding agents: the same pages ship in the package under `docs/user-guide/`, starting from `docs/user-guide/README.md`.
@@ -0,0 +1,24 @@
1
+ ---
2
+ title: "@doubleloop-io/oxlint-plugin-effect"
3
+ ---
4
+
5
+ An oxlint plugin that enforces a catalog of patterns for Effect 4 code.
6
+
7
+ The rules are written and tested against Effect 4. Where an Effect 3 API has the same intent, the rules recognize its v3 name too: each rule's Trigger names them.
8
+
9
+ It complements the type-aware rules of [`@effect/tsgo`](alongside-effect-tsgo.md): it does not need them, and it checks what they do not.
10
+
11
+ ## Guide
12
+
13
+ - [Installation](installation.md): the package and the Node and oxlint versions it requires.
14
+ - [Configuration](configuration.md): the `all` preset and picking rules.
15
+ - [Shared settings](shared-settings.md): the settings that describe your project, their defaults and the rules that use them.
16
+ - [Alongside `@effect/tsgo`](alongside-effect-tsgo.md): combining its presets with this one.
17
+ - [Default configuration](default-configuration.md): `all` with every setting and option spelled out.
18
+ - [Versioning](versioning.md): what a release may change and which oxlint versions are supported.
19
+ - [Troubleshooting](troubleshooting.md): the problems most often hit when adopting the plugin.
20
+
21
+ ## Rules
22
+
23
+ - [Rules overview](rules.md): every rule, grouped by topic.
24
+ - [Common limits](common-limits.md): the limits every rule shares.
@@ -0,0 +1,23 @@
1
+ ---
2
+ title: Alongside `@effect/tsgo`
3
+ order: 4
4
+ ---
5
+
6
+ [`@effect/tsgo`](https://github.com/Effect-TS/tsgo) is the official Effect language service on top of the TypeScript Go compiler; its `effecttsgo` oxlint presets add more than 100 type-aware rules. This plugin does not need it, and its rules target what those presets do not check, so the two complement each other. To use both, follow its [Oxlint Setup guide](https://github.com/Effect-TS/tsgo/blob/main/docs/README.md) and put its preset in `extends` **before** `all`:
7
+
8
+ ```ts
9
+ import dlEffectAll from "@doubleloop-io/oxlint-plugin-effect/presets/all"
10
+ import { recommended as effectTsgoRecommended } from "@effect/tsgo/oxlint-presets"
11
+ import { defineConfig } from "oxlint"
12
+
13
+ export default defineConfig({ extends: [effectTsgoRecommended, dlEffectAll] })
14
+ ```
15
+
16
+ ```json
17
+ {
18
+ "extends": [
19
+ "./node_modules/@effect/tsgo/oxlint-presets/recommended.json",
20
+ "./node_modules/@doubleloop-io/oxlint-plugin-effect/dist/presets/all.json"
21
+ ]
22
+ }
23
+ ```
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: Common limits
3
+ order: 6
4
+ ---
5
+
6
+ The limits every rule shares.
7
+
8
+ - **Syntax only**: no rule uses type information. Only two rules look past the file: [`adapter-import-boundary`](rules/adapter-import-boundary.md), to find which directories are adapters, and [`prefer-effect-vitest`](rules/prefer-effect-vitest.md), to find whether `@effect/vitest` is installed.
9
+ - **Literal names**: `Effect.gen` is recognized, `import { Effect as E }` or `import * as L from "effect/Layer"` slip through. On code that uses aliases, silence is not compliance.
10
+ - **Effect 4**: the rules are written against v4. Where a v3 API has the same intent, they recognize its v3 name too (`catchAll`, `Either`, `Context.Tag`, `Config.redacted`, …), and the Trigger names it.
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: Configuration
3
+ order: 2
4
+ ---
5
+
6
+ Extend the `all` preset. Prefer `oxlint.config.ts`, where `extends` takes config objects and the preset is imported by package name:
7
+
8
+ ```ts
9
+ import dlEffectAll from "@doubleloop-io/oxlint-plugin-effect/presets/all"
10
+ import { defineConfig } from "oxlint"
11
+
12
+ export default defineConfig({ extends: [dlEffectAll] })
13
+ ```
14
+
15
+ With `.oxlintrc.json`, `extends` takes file paths only, so the preset is reached through `node_modules`:
16
+
17
+ ```json
18
+ {
19
+ "extends": ["./node_modules/@doubleloop-io/oxlint-plugin-effect/dist/presets/all.json"]
20
+ }
21
+ ```
22
+
23
+ `all` enables every rule at `warn`, so adopting it never breaks a build: look at the warnings first, then raise to `error` the rules you want to enforce. A `recommended` preset with reasoned severities will come from measurements on real codebases.
24
+
25
+ To pick rules without the preset, load the plugin by package name and list the rules:
26
+
27
+ ```json
28
+ {
29
+ "jsPlugins": ["@doubleloop-io/oxlint-plugin-effect"],
30
+ "rules": {
31
+ "dl-effect/no-effect-promise": "error",
32
+ "dl-effect/try-object-form": "error"
33
+ }
34
+ }
35
+ ```
36
+
37
+ ## Settings
38
+
39
+ The settings that describe your project, such as its entry files and test files, are on [Shared settings](shared-settings.md). Their defaults fit a Node CLI: on another layout (Next.js, a monorepo), set them before reading the warnings.
40
+
41
+ The other options and every default are in [Default configuration](default-configuration.md).
42
+
43
+ To use it with the `@effect/tsgo` presets, see [Alongside `@effect/tsgo`](alongside-effect-tsgo.md).
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: Default configuration
3
+ order: 5
4
+ ---
5
+
6
+ `all` is equivalent to the config below, with every setting and option spelled out at its default. Both **replace** the defaults instead of extending them: see [Shared settings](shared-settings.md).
7
+
8
+ ```ts
9
+ import { defineConfig } from "oxlint"
10
+
11
+ // Constructors that must not appear inside an Effect generator: the conversion to the
12
+ // transport belongs in the second argument of Effect.fn. Add your own transport DTOs.
13
+ const transportTypes = ["Response", "NextResponse"]
14
+
15
+ // Schema fields holding a secret: a field matches an entry or ends with it capitalized
16
+ // (`password` matches `dbPassword`). No generic `token`: `nextPageToken` is not a secret.
17
+ const secretFields = ["password", "secret", "apiKey", "accessToken", "refreshToken", "idToken"]
18
+
19
+ export default defineConfig({
20
+ jsPlugins: ["@doubleloop-io/oxlint-plugin-effect"],
21
+ settings: {
22
+ "dl-effect": {
23
+ // Files allowed to run Effects (Effect.run*, runMain, ManagedRuntime.make) and to
24
+ // import adapters. `index.ts` is left out on purpose: as a barrel it would let
25
+ // adapters leak through re-exports.
26
+ entryFiles: ["**/main.ts", "**/cli.ts", "**/bin/**"],
27
+ // Files exempt from the rules that tests legitimately break. Helper directories such
28
+ // as `src/testing/` are support code, not tests, so they are not matched.
29
+ testFiles: [
30
+ "**/*.test.*",
31
+ "**/*.spec.*",
32
+ "**/__tests__/**",
33
+ "**/test/**",
34
+ "**/tests/**",
35
+ ],
36
+ // Fields that discriminate a closed union, compared or switched on only through an
37
+ // exhaustive match and set only by a constructor. Add the ones your unions use, such
38
+ // as `type` or `kind`.
39
+ discriminants: ["_tag"],
40
+ },
41
+ },
42
+ // Every rule at `warn`, to measure noise without blocking.
43
+ rules: {
44
+ // Error channel
45
+ "dl-effect/no-throw-or-primitive-failure": "warn",
46
+ "dl-effect/try-object-form": "warn",
47
+ "dl-effect/no-effect-promise": "warn",
48
+ "dl-effect/no-parameterless-catch-all": "warn",
49
+ "dl-effect/split-success-path-from-error-handling": "warn",
50
+ "dl-effect/anti-slop-no-manual-effect-error-tag": "warn",
51
+ // Tagged values
52
+ "dl-effect/no-manual-discriminant-comparison": "warn",
53
+ "dl-effect/no-manual-discriminant-construction": "warn",
54
+ // Services & Layers
55
+ "dl-effect/service-port-shape-only": "warn",
56
+ "dl-effect/service-key-matches-name": "warn",
57
+ "dl-effect/service-built-with-of": "warn",
58
+ "dl-effect/provide-named-layer": "warn",
59
+ "dl-effect/no-provide-on-definition": "warn",
60
+ "dl-effect/anti-slop-no-service-constructor-imports": "warn",
61
+ // Runtime & entrypoint
62
+ "dl-effect/run-only-in-runner": "warn",
63
+ "dl-effect/prefer-effect-vitest": "warn",
64
+ "dl-effect/all-errors-handled-at-boundary": "warn",
65
+ // Design & boundaries
66
+ "dl-effect/adapter-import-boundary": "warn",
67
+ "dl-effect/no-transport-types-in-generator": ["warn", { transportTypes }],
68
+ // Effectful functions
69
+ "dl-effect/prefer-effect-fn": "warn",
70
+ "dl-effect/untraced-recursion": "warn",
71
+ "dl-effect/single-style-per-function": "warn",
72
+ // Code shape
73
+ "dl-effect/no-nested-match": "warn",
74
+ "dl-effect/no-yielded-match": "warn",
75
+ "dl-effect/no-inline-yield-star": "warn",
76
+ "dl-effect/no-effect-return-type": ["warn", { checkExported: false }],
77
+ "dl-effect/no-asymmetric-match-handlers": "warn",
78
+ "dl-effect/anti-slop-prefer-effect-match": "warn",
79
+ // Schema
80
+ "dl-effect/exported-schema-identifier": "warn",
81
+ "dl-effect/no-inline-schema-codec": "warn",
82
+ // Config & secrets
83
+ "dl-effect/redacted-secret-names": ["warn", { secretFields }],
84
+ },
85
+ })
86
+ ```
87
+
88
+ To change a setting, declare it in `settings`; to change a rule option, extend `all` and override only that rule.
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: Installation
3
+ order: 1
4
+ ---
5
+
6
+ ```bash
7
+ pnpm add -D @doubleloop-io/oxlint-plugin-effect oxlint
8
+ ```
9
+
10
+ Requires Node `^22.20.0 || >=24.8.0` and oxlint `^1.82.0`. What a release may change and which oxlint versions are supported: [Versioning](versioning.md).
11
+
12
+ Then extend the `all` preset: see [Configuration](configuration.md).
@@ -0,0 +1,27 @@
1
+ ---
2
+ title: adapter-import-boundary
3
+ group: Design and boundaries
4
+ ---
5
+
6
+ **Intent**: domain code and other adapters do not import an adapter; only entry files choose it.
7
+
8
+ ```ts
9
+ // ❌ no: in src/orders/place-order.ts
10
+ import { GoogleCalendar } from "../calendar/google/GoogleCalendar.ts"
11
+
12
+ // ✅ yes: depend on the port, main.ts provides GoogleCalendar.layer
13
+ import { Calendar } from "../calendar/Calendar.ts"
14
+ ```
15
+
16
+ **Trigger**: a relative import of a file in another directory that is an adapter: one of its `.ts` files (tests excluded) builds a `Layer` with `Layer.effect`, `succeed`, `sync`, `scoped`, `effectContext`, `unwrap`, `mock` and similar, or declares a v3 `Effect.Service`, which always carries its implementation. Composition alone (`mergeAll`, `provide`, `merge`) does not make an adapter.
17
+
18
+ **Exemptions and blind spots**: entry files, where adapters are chosen, and test files, which compose the Layers they exercise. Non-relative imports (path aliases such as `@/calendar/google`) and directory imports through an `index.ts` (`"../google"`) slip through. On a layout other than one directory per adapter, check what it reports before trusting it. Infrastructure requirements in the port's `R` channel need types: see `leaking-requirements` (effecttsgo).
19
+
20
+ **Why**: the adapter can be replaced without touching its consumers.
21
+
22
+ **Options**:
23
+
24
+ - `entryFiles`: shared setting, see [Shared settings](../shared-settings.md).
25
+ - `testFiles`: shared setting, see [Shared settings](../shared-settings.md).
26
+
27
+ **Fix**: none
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: all-errors-handled-at-boundary
3
+ group: Runtime and entry point
4
+ ---
5
+
6
+ **Intent**: the entry point runs an Effect whose error channel is closed to `never`.
7
+
8
+ ```ts
9
+ // ❌ no
10
+ export const POST = (request: Request) => Effect.runPromise(createEvent(request))
11
+
12
+ // ✅ yes: in an entry file
13
+ export const POST = (request: Request) =>
14
+ Effect.runPromise(createEvent(request) satisfies Effect.Effect<Response, never>)
15
+
16
+ // ✅ yes: a typed helper
17
+ const runBoundary = (effect: Effect.Effect<Response, never, never>) => Effect.runPromise(effect)
18
+ ```
19
+
20
+ **Trigger**: `Effect.runPromise`, `runSync`, `runFork` and `runCallback` whose argument is not `… satisfies Effect.Effect<A, never>`, a parameter declared with that type, or a program ending in `Effect.result`/`Effect.exit`, or the v3 `Effect.either` (`Effect.result(program)`, `program.pipe(…, Effect.result)`).
21
+
22
+ **Exemptions and blind spots**: the `*Exit` variants, which return failures as values; `NodeRuntime.runMain`/`BunRuntime.runMain`, which log the Cause and exit with code 1, the right outcome for a process that cannot start; runners inside an `async` function that renders JSX itself, a Server Component, which hands failures to React rather than to an HTTP response; test files, where a rejected run fails the test with the error. The rule checks the shape; `tsc` gives the guarantee.
23
+
24
+ **Why**: `Effect.runPromise` turns any error left over into a rejected promise, a generic 500. With `never`, a new untranslated error does not compile, and `R = never` forces all Layers to be provided.
25
+
26
+ **Options**:
27
+
28
+ - `testFiles`: shared setting, see [Shared settings](../shared-settings.md).
29
+
30
+ **Fix**: none
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: anti-slop-no-manual-effect-error-tag
3
+ group: Error channel
4
+ ---
5
+
6
+ **Intent**: a catch handler does not discriminate errors by `_tag`; `catchTag` and `catchReason` do.
7
+
8
+ ```ts
9
+ // ❌ no
10
+ program.pipe(Effect.catch((error) => (error._tag === "NotFound" ? recover : Effect.fail(error))))
11
+
12
+ // ✅ yes
13
+ program.pipe(Effect.catchTag("NotFound", () => recover))
14
+ ```
15
+
16
+ **Trigger**: an equality comparison between `_tag` (or `reason._tag`) and a string literal, or a `switch` on `_tag`, whose nearest enclosing function is a handler passed to `Effect.catch`, `catchAll` or `catchIf`. On `reason._tag` the message proposes `Effect.catchReason(s)`.
17
+
18
+ **Exemptions and blind spots**: handlers of other combinators (`catchTag`, `catchCause`, `mapError`), and `_tag` checks in a function nested inside the handler. Outside these handlers the same checks belong to [`no-manual-discriminant-comparison`](no-manual-discriminant-comparison.md): the two never fire on the same node.
19
+
20
+ **Why**: a hand-written dispatch keeps the caught errors in the type even when handled, and silently misses tags added later.
21
+
22
+ **Options**: none
23
+
24
+ **Fix**: none
25
+
26
+ **Origin**: absorbed from [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop), MIT-licensed: see [`LICENSE.anti-slop`](../../../LICENSE.anti-slop).
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: anti-slop-no-service-constructor-imports
3
+ group: Services and Layers
4
+ ---
5
+
6
+ **Intent**: runtime code obtains a service from its Layer, never by calling its `make…` constructor.
7
+
8
+ ```ts
9
+ // ❌ no: in src/runtime.ts
10
+ import { makeIssueService } from "./issue-service.ts"
11
+
12
+ // ✅ yes
13
+ import { IssueService } from "./issue-service.ts"
14
+
15
+ Effect.gen(function* () {
16
+ const issues = yield* IssueService
17
+ })
18
+ ```
19
+
20
+ **Trigger**: a named import of `make` followed by an uppercase letter (`makeIssueService`, also when renamed) from a relative module.
21
+
22
+ **Exemptions and blind spots**: test files, which build services from their constructors to inject fakes. Non-relative imports (packages, path aliases), namespace and default imports, and constructors with another prefix slip through. The same import can also fire [`adapter-import-boundary`](adapter-import-boundary.md).
23
+
24
+ **Why**: calling the constructor wires its dependencies by hand, and its requirements stop propagating to the composition root.
25
+
26
+ **Options**:
27
+
28
+ - `testFiles`: shared setting, see [Shared settings](../shared-settings.md).
29
+
30
+ **Fix**: none
31
+
32
+ **Origin**: absorbed from [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop), MIT-licensed: see [`LICENSE.anti-slop`](../../../LICENSE.anti-slop).
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: anti-slop-prefer-effect-match
3
+ group: Code shape
4
+ ---
5
+
6
+ **Intent**: a choice among several literal values of the same expression is a `Match`, not a chain of ternaries.
7
+
8
+ ```ts
9
+ // ❌ no
10
+ const label = kind === "a" ? "A" : kind === "b" ? "B" : "Other"
11
+
12
+ // ✅ yes
13
+ const label = Match.value(kind).pipe(
14
+ Match.when("a", () => "A"),
15
+ Match.when("b", () => "B"),
16
+ Match.orElse(() => "Other"),
17
+ )
18
+ ```
19
+
20
+ **Trigger**: a ternary chain of at least two levels where every condition compares the same expression, by source text, with a literal (`==`, `===`, `!=`, `!==`).
21
+
22
+ **Exemptions and blind spots**: a single ternary, chains comparing different expressions, and the same expression written differently (`a.b` vs `a["b"]`). A chain returned by a match handler also fires [`no-nested-match`](no-nested-match.md): extract the handler first, since a `Match.value` left inside it fires `no-nested-match` again.
23
+
24
+ **Why**: a ternary chain hides which value is the fallback and cannot be made exhaustive.
25
+
26
+ **Options**: none
27
+
28
+ **Fix**: none
29
+
30
+ **Origin**: absorbed from [`dmmulroy/anti-slop`](https://github.com/dmmulroy/anti-slop), MIT-licensed: see [`LICENSE.anti-slop`](../../../LICENSE.anti-slop).