@doubleloop-io/oxlint-plugin-effect 0.1.0 → 0.1.2

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 (46) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +17 -200
  3. package/dist/rules/no-inline-yield-star.js +63 -14
  4. package/docs/user-guide/README.md +24 -0
  5. package/docs/user-guide/alongside-effect-tsgo.md +23 -0
  6. package/docs/user-guide/common-limits.md +10 -0
  7. package/docs/user-guide/configuration.md +43 -0
  8. package/docs/user-guide/default-configuration.md +88 -0
  9. package/docs/user-guide/installation.md +12 -0
  10. package/docs/user-guide/rules/adapter-import-boundary.md +27 -0
  11. package/docs/user-guide/rules/all-errors-handled-at-boundary.md +30 -0
  12. package/docs/user-guide/rules/anti-slop-no-manual-effect-error-tag.md +26 -0
  13. package/docs/user-guide/rules/anti-slop-no-service-constructor-imports.md +32 -0
  14. package/docs/user-guide/rules/anti-slop-prefer-effect-match.md +30 -0
  15. package/docs/user-guide/rules/exported-schema-identifier.md +26 -0
  16. package/docs/user-guide/rules/no-asymmetric-match-handlers.md +30 -0
  17. package/docs/user-guide/rules/no-effect-promise.md +26 -0
  18. package/docs/user-guide/rules/no-effect-return-type.md +29 -0
  19. package/docs/user-guide/rules/no-inline-schema-codec.md +26 -0
  20. package/docs/user-guide/rules/no-inline-yield-star.md +29 -0
  21. package/docs/user-guide/rules/no-manual-discriminant-comparison.md +36 -0
  22. package/docs/user-guide/rules/no-manual-discriminant-construction.md +32 -0
  23. package/docs/user-guide/rules/no-nested-match.md +44 -0
  24. package/docs/user-guide/rules/no-parameterless-catch-all.md +28 -0
  25. package/docs/user-guide/rules/no-provide-on-definition.md +27 -0
  26. package/docs/user-guide/rules/no-throw-or-primitive-failure.md +34 -0
  27. package/docs/user-guide/rules/no-transport-types-in-generator.md +41 -0
  28. package/docs/user-guide/rules/no-yielded-match.md +35 -0
  29. package/docs/user-guide/rules/prefer-effect-fn.md +31 -0
  30. package/docs/user-guide/rules/prefer-effect-vitest.md +30 -0
  31. package/docs/user-guide/rules/provide-named-layer.md +27 -0
  32. package/docs/user-guide/rules/redacted-secret-names.md +31 -0
  33. package/docs/user-guide/rules/run-only-in-runner.md +32 -0
  34. package/docs/user-guide/rules/service-built-with-of.md +36 -0
  35. package/docs/user-guide/rules/service-key-matches-name.md +29 -0
  36. package/docs/user-guide/rules/service-port-shape-only.md +27 -0
  37. package/docs/user-guide/rules/single-style-per-function.md +36 -0
  38. package/docs/user-guide/rules/split-success-path-from-error-handling.md +37 -0
  39. package/docs/user-guide/rules/try-object-form.md +27 -0
  40. package/docs/user-guide/rules/untraced-recursion.md +31 -0
  41. package/docs/user-guide/rules.md +26 -0
  42. package/docs/user-guide/shared-settings.md +65 -0
  43. package/docs/user-guide/troubleshooting.md +30 -0
  44. package/docs/user-guide/versioning.md +8 -0
  45. package/package.json +14 -9
  46. package/RULES.md +0 -1056
package/CHANGELOG.md CHANGED
@@ -1,10 +1,31 @@
1
1
  # @doubleloop-io/oxlint-plugin-effect
2
2
 
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#42](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/42) [`00de7c6`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/00de7c6780be261181bd49e34c183f020733754f) Thanks [@iacoware](https://github.com/iacoware)! - `no-inline-yield-star` adds a hint when the `yield*` sits inside a larger conditional branch, where binding it first would run it unconditionally: extract the effect into a named function and transform its result inside it (`Effect.map`), so the branch is a single `yield*`.
8
+
9
+ - [#41](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/41) [`a71cd0f`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/a71cd0fbcc82be7e9961550ec4924ae483b64016) Thanks [@iacoware](https://github.com/iacoware)! - `no-inline-yield-star` autofix names a `const` whose name is taken after the object it is read from (`reader.rows` → `readerRows`, `Effect.scope` → `effectScope`) before falling back to a numeric suffix.
10
+
11
+ - [#39](https://github.com/doubleloop-io/oxlint-plugin-effect/pull/39) [`469f03d`](https://github.com/doubleloop-io/oxlint-plugin-effect/commit/469f03d1eb92126a918d8cf456e8483b05c4fdd9) Thanks [@d4v1d82](https://github.com/d4v1d82)! - Prevent `no-inline-yield-star` autofixes from generating duplicate `const` bindings when multiple statements in the same generator extract the same base name.
12
+
13
+ ## 0.1.1
14
+
15
+ ### Patch Changes
16
+
17
+ - [`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/
18
+
19
+ - 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.
20
+ - The README becomes the essential guide and links to the website.
21
+ - New Troubleshooting page.
22
+ - Improve rules documentation.
23
+
3
24
  ## 0.1.0
4
25
 
5
26
  First stable release.
6
27
 
7
- - 30 rules for Effect code, grouped by topic in [`RULES.md`](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).
28
+ - 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).
8
29
  - An `all` preset that enables every rule at `warn`, for `oxlint.config.ts` and `.oxlintrc.json`.
9
30
  - Shared settings under `settings["dl-effect"]`: `entryFiles`, `testFiles`, `discriminants`.
10
31
  - Written for Effect 4; rules also recognize the Effect 3 name of the same API.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @doubleloop-io/oxlint-plugin-effect
2
2
 
3
- An oxlint plugin that enforces a catalog of patterns for Effect 4 code.
3
+ An [oxlint](https://github.com/oxc-project/oxc) plugin that enforces a catalog of patterns for [Effect](https://github.com/Effect-TS/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). Report false positives and missed cases in the [GitHub issues](https://github.com/doubleloop-io/oxlint-plugin-effect/issues).
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`.
@@ -35,6 +35,29 @@ const statementOf = (node) => {
35
35
  return current;
36
36
  };
37
37
  const logicalAssignments = new Set(["&&=", "||=", "??="]);
38
+ const isConditionalBranch = (child, parent) => {
39
+ switch (parent.type) {
40
+ case "ConditionalExpression":
41
+ return parent.test !== child;
42
+ case "LogicalExpression":
43
+ return parent.right === child;
44
+ case "AssignmentExpression":
45
+ return parent.right === child && logicalAssignments.has(parent.operator);
46
+ default:
47
+ return false;
48
+ }
49
+ };
50
+ // A branch that is the `yield*` itself can be bound whole; a larger branch cannot be bound first
51
+ // without running the effect unconditionally.
52
+ const isNestedInConditionalBranch = (node, statement) => {
53
+ let child = node.parent;
54
+ while (child !== statement && child.parent) {
55
+ if (isConditionalBranch(child, child.parent))
56
+ return true;
57
+ child = child.parent;
58
+ }
59
+ return false;
60
+ };
38
61
  // The extracted `const` runs before its statement, once and unconditionally: any position that
39
62
  // short-circuits, repeats or belongs to a nested statement would change when the effect runs.
40
63
  const runsOnceBeforeStatement = (child, parent, statement) => {
@@ -89,13 +112,32 @@ const trailingName = (node) => {
89
112
  return undefined;
90
113
  }
91
114
  };
92
- // `yield* readSheet()` becomes `sheet`, `yield* reader.rows` becomes `rows`.
93
- const baseName = (node) => {
94
- const name = node.argument ? trailingName(node.argument)?.replace(verbPrefix, "") : undefined;
95
- if (name === undefined || name === "")
96
- return "value";
97
- const variable = name.charAt(0).toLowerCase() + name.slice(1);
98
- return reservedWords.has(variable) ? "value" : variable;
115
+ const memberOwner = (node) => {
116
+ switch (node.type) {
117
+ case "MemberExpression":
118
+ return node.computed ? undefined : node.object;
119
+ case "CallExpression":
120
+ case "NewExpression":
121
+ return memberOwner(node.callee);
122
+ default:
123
+ return undefined;
124
+ }
125
+ };
126
+ const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1);
127
+ const upperFirst = (name) => name.charAt(0).toUpperCase() + name.slice(1);
128
+ // `yield* readSheet()` becomes `sheet`, `yield* reader.rows` becomes `rows`, or `readerRows`
129
+ // when `rows` is taken: the owner tells the reader where the value comes from, a suffix does not.
130
+ const names = (node) => {
131
+ const { argument } = node;
132
+ const name = argument ? trailingName(argument)?.replace(verbPrefix, "") : undefined;
133
+ if (!argument || name === undefined || name === "")
134
+ return { base: "value" };
135
+ const base = lowerFirst(name);
136
+ if (reservedWords.has(base))
137
+ return { base: "value" };
138
+ const owner = memberOwner(argument);
139
+ const ownerName = owner ? trailingName(owner) : undefined;
140
+ return ownerName ? { base, qualified: lowerFirst(ownerName) + upperFirst(base) } : { base };
99
141
  };
100
142
  const namesInUse = (scope) => {
101
143
  const names = new Set();
@@ -130,12 +172,14 @@ const replacedRange = (node, sourceCode) => {
130
172
  }
131
173
  return wrappers[wrappers.length - 1 - callParens] ?? node.range;
132
174
  };
133
- const extraction = (statement, hoisted, sourceCode) => {
134
- const taken = namesInUse(sourceCode.getScope(statement).variableScope);
175
+ const extraction = (statement, hoisted, sourceCode, namesByOwner, owner) => {
176
+ const taken = namesByOwner.get(owner) ?? namesInUse(sourceCode.getScope(statement).variableScope);
177
+ namesByOwner.set(owner, taken);
135
178
  const replacements = hoisted.map((node) => {
136
- let name = baseName(node);
179
+ const { base, qualified } = names(node);
180
+ let name = taken.has(base) && qualified !== undefined ? qualified : base;
137
181
  for (let suffix = 2; taken.has(name); suffix++)
138
- name = `${baseName(node)}${suffix}`;
182
+ name = `${base}${suffix}`;
139
183
  taken.add(name);
140
184
  return { node, name, range: replacedRange(node, sourceCode) };
141
185
  });
@@ -161,6 +205,7 @@ const extraction = (statement, hoisted, sourceCode) => {
161
205
  const guarded = startsLine && /^[[(`+\-/]/.test(remainder) ? `;${remainder}` : remainder;
162
206
  return [...declarations, guarded].join(startsLine ? `\n${indentation}` : "; ");
163
207
  };
208
+ const inlineYieldStarMessage = "This `yield*` runs inside a larger expression. Bind its result first (`const rows = yield* reader.rows`), so each statement runs one effect in reading order.";
164
209
  export const noInlineYieldStarRule = defineRule({
165
210
  meta: {
166
211
  type: "suggestion",
@@ -169,7 +214,8 @@ export const noInlineYieldStarRule = defineRule({
169
214
  description: "Bind the result of a `yield*` to a `const` instead of using it inside a larger expression.",
170
215
  },
171
216
  messages: {
172
- inlineYieldStar: "This `yield*` runs inside a larger expression. Bind its result first (`const rows = yield* reader.rows`), so each statement runs one effect in reading order.",
217
+ inlineYieldStar: inlineYieldStarMessage,
218
+ conditionalYieldStar: `${inlineYieldStarMessage} When inside a conditional branch, binding it first would run it unconditionally. Instead, extract the effect into a named function and transform its result inside it (\`Effect.map\`), so the branch is a single \`yield*\`.`,
173
219
  },
174
220
  },
175
221
  createOnce(context) {
@@ -197,6 +243,7 @@ export const noInlineYieldStarRule = defineRule({
197
243
  VariableDeclarator: collect,
198
244
  "Program:exit"() {
199
245
  const byStatement = new Map();
246
+ const namesByOwner = new Map();
200
247
  for (const node of inline) {
201
248
  const statement = statementOf(node);
202
249
  byStatement.set(statement, [...(byStatement.get(statement) ?? []), node]);
@@ -212,12 +259,14 @@ export const noInlineYieldStarRule = defineRule({
212
259
  hoisted.length > 0 &&
213
260
  !reordersEffects(hoisted, evaluatedInStatement);
214
261
  const replacement = fixable
215
- ? extraction(statement, hoisted, context.sourceCode)
262
+ ? extraction(statement, hoisted, context.sourceCode, namesByOwner, owner)
216
263
  : undefined;
217
264
  for (const node of yields) {
218
265
  context.report({
219
266
  node,
220
- messageId: "inlineYieldStar",
267
+ messageId: isNestedInConditionalBranch(node, statement)
268
+ ? "conditionalYieldStar"
269
+ : "inlineYieldStar",
221
270
  ...(replacement !== undefined && hoisted.includes(node)
222
271
  ? { fix: (fixer) => fixer.replaceText(statement, replacement) }
223
272
  : {}),
@@ -0,0 +1,24 @@
1
+ ---
2
+ title: "@doubleloop-io/oxlint-plugin-effect"
3
+ ---
4
+
5
+ An [oxlint](https://github.com/oxc-project/oxc) plugin that enforces a catalog of patterns for [Effect](https://github.com/Effect-TS/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).