@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.
- package/CHANGELOG.md +22 -0
- package/README.md +16 -199
- package/docs/user-guide/README.md +24 -0
- package/docs/user-guide/alongside-effect-tsgo.md +23 -0
- package/docs/user-guide/common-limits.md +10 -0
- package/docs/user-guide/configuration.md +43 -0
- package/docs/user-guide/default-configuration.md +88 -0
- package/docs/user-guide/installation.md +12 -0
- package/docs/user-guide/rules/adapter-import-boundary.md +27 -0
- package/docs/user-guide/rules/all-errors-handled-at-boundary.md +30 -0
- package/docs/user-guide/rules/anti-slop-no-manual-effect-error-tag.md +26 -0
- package/docs/user-guide/rules/anti-slop-no-service-constructor-imports.md +32 -0
- package/docs/user-guide/rules/anti-slop-prefer-effect-match.md +30 -0
- package/docs/user-guide/rules/exported-schema-identifier.md +26 -0
- package/docs/user-guide/rules/no-asymmetric-match-handlers.md +30 -0
- package/docs/user-guide/rules/no-effect-promise.md +26 -0
- package/docs/user-guide/rules/no-effect-return-type.md +29 -0
- package/docs/user-guide/rules/no-inline-schema-codec.md +26 -0
- package/docs/user-guide/rules/no-inline-yield-star.md +29 -0
- package/docs/user-guide/rules/no-manual-discriminant-comparison.md +36 -0
- package/docs/user-guide/rules/no-manual-discriminant-construction.md +32 -0
- package/docs/user-guide/rules/no-nested-match.md +44 -0
- package/docs/user-guide/rules/no-parameterless-catch-all.md +28 -0
- package/docs/user-guide/rules/no-provide-on-definition.md +27 -0
- package/docs/user-guide/rules/no-throw-or-primitive-failure.md +34 -0
- package/docs/user-guide/rules/no-transport-types-in-generator.md +41 -0
- package/docs/user-guide/rules/no-yielded-match.md +35 -0
- package/docs/user-guide/rules/prefer-effect-fn.md +31 -0
- package/docs/user-guide/rules/prefer-effect-vitest.md +30 -0
- package/docs/user-guide/rules/provide-named-layer.md +27 -0
- package/docs/user-guide/rules/redacted-secret-names.md +31 -0
- package/docs/user-guide/rules/run-only-in-runner.md +32 -0
- package/docs/user-guide/rules/service-built-with-of.md +36 -0
- package/docs/user-guide/rules/service-key-matches-name.md +29 -0
- package/docs/user-guide/rules/service-port-shape-only.md +27 -0
- package/docs/user-guide/rules/single-style-per-function.md +36 -0
- package/docs/user-guide/rules/split-success-path-from-error-handling.md +37 -0
- package/docs/user-guide/rules/try-object-form.md +27 -0
- package/docs/user-guide/rules/untraced-recursion.md +31 -0
- package/docs/user-guide/rules.md +26 -0
- package/docs/user-guide/shared-settings.md +65 -0
- package/docs/user-guide/troubleshooting.md +30 -0
- package/docs/user-guide/versioning.md +8 -0
- package/package.json +15 -9
- 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
|
|
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`](
|
|
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
|
-
##
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
The guide is on the [website](https://doubleloop-io.github.io/oxlint-plugin-effect/):
|
|
224
43
|
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
-
|
|
231
|
-
-
|
|
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
|
-
|
|
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).
|