@commonplaces/oxlint-plugin 0.5.0 → 0.6.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/README.md CHANGED
@@ -1,21 +1,26 @@
1
1
  # @commonplaces/oxlint-plugin
2
2
 
3
- 106 custom Oxlint rules for strict Effect TypeScript, Alchemy and general code
4
- checks. This package supports **Oxlint 1.86.0 or 1.89.0** and **Node 22.14 or newer**.
3
+ 107 custom Oxlint rules for strict Effect TypeScript, Alchemy and general code
4
+ checks. Version 0.6.1 supports **Oxlint 1.87.0, 1.88.0 or 1.89.0** and **Node 22.14 or newer**.
5
5
  Oxlint's JavaScript plugin API is experimental, so upgrade it only after running
6
6
  the rule tests. The development tools use 1.89.0; consuming projects may use any supported version.
7
- Both exact versions are checked with independently installed archives. ESLint is not a supported host.
7
+ All three exact versions are checked with independently installed archives. ESLint is not a supported host.
8
+
9
+ Version 0.6.0 adds native Effect correctness and immutable accumulation checks.
10
+ When upgrading from 0.5.0, use a supported Oxlint version and run the native
11
+ Effect setup below after dependency installation.
8
12
 
9
13
  ## Install
10
14
 
11
- Install version 0.5.0 and save its exact version:
15
+ Install version 0.6.1 and save its exact version:
12
16
 
13
17
  ```sh
14
- npm install --save-dev --save-exact @commonplaces/oxlint-plugin@0.5.0 oxlint@1.89.0
18
+ npm install --save-dev --save-exact @commonplaces/oxlint-plugin@0.6.1 \
19
+ @effect/tsgo@0.55.0 oxlint@1.89.0 oxlint-tsgolint@7.0.2004 typescript@7.0.2
15
20
  ```
16
21
 
17
- Version 0.5.0 adds the optional style preset and the host requirements above.
18
- Published 0.4.0 keeps its earlier host list. Consumer adoption is a separate step.
22
+ Version 0.6.1 supports exactly Oxlint 1.87.0, 1.88.0 or 1.89.0. Older releases
23
+ keep their earlier host lists. Consumer adoption is a separate step.
19
24
 
20
25
  The manifest and lockfile retain the resolved version. Historical GitHub release
21
26
  archives remain separate distribution artefacts; a GitHub release does not publish
@@ -59,6 +64,64 @@ the subpaths below. For example, a JSON configuration can enable one rule:
59
64
  }
60
65
  ```
61
66
 
67
+ ## Native Effect correctness
68
+
69
+ Both `effect` and `strict` include every check in the pinned
70
+ `@effect/tsgo` **0.55.0** correctness preset, all at error level. This catches
71
+ forgotten Effect programs, incorrect generator yields, unhandled errors and
72
+ requirements, unchecked Effect type assertions, missing Schema class brands and
73
+ unstable APIs. Type-aware checking is enabled by the group itself; consumers
74
+ do not need to import another preset. Upstream's unrelated style and maintainer
75
+ preferences are not added to the public group.
76
+
77
+ The package ships `@effect/tsgo` **0.55.0** and `oxlint-tsgolint` **7.0.2004**
78
+ as exact regular dependencies. Full Effect projects should also declare their
79
+ compiler and checker tools directly, as in the install command above. This
80
+ keeps the selected local tools explicit. The pinned checker supports TypeScript
81
+ **7.0.2** or **7.1.0-dev.20261019.1**, alongside the three Oxlint versions above.
82
+ The archive tests use TypeScript 7.0.2.
83
+ Check the installed checker's supported versions before changing these tools.
84
+
85
+ Use [Effect's recommended automatic setup](https://github.com/Effect-TS/tsgo/blob/main/docs/README.md).
86
+ Add this command to the consuming project's root `package.json`:
87
+
88
+ ```json
89
+ {
90
+ "scripts": {
91
+ "prepare": "effect-tsgo patch --oxlint"
92
+ }
93
+ }
94
+ ```
95
+
96
+ Merge the patch command into an existing `prepare` script with `&&`; retain its
97
+ hooks, code generation and build tasks. Add it once at the repository root.
98
+ Run `npm install` after editing the script. Future installs, including clean
99
+ CI installs, run it automatically. Then run the project's normal strict
100
+ TypeScript and Oxlint commands, including `oxlint --deny-warnings .`.
101
+
102
+ This prepares both the TypeScript compiler and Oxlint. Do not use
103
+ `--no-typescript` for a full Effect project. That option is only for an explicit
104
+ Oxlint-only host which has no supported native TypeScript compiler; record that
105
+ host constraint rather than making it the shared default. Do not use
106
+ `--skip-missing` to hide missing tools or incompatible versions.
107
+
108
+ For initial configuration, the installed `effect-tsgo setup` command can guide
109
+ TypeScript, Oxlint and editor setup. Review its proposed changes against the
110
+ project's existing strict rules and exact dependency versions. Keep the shared
111
+ presets and all project-specific restrictions active.
112
+
113
+ If a deliberate install or lint-package refresh uses `--ignore-scripts`, run
114
+ `effect-tsgo patch --oxlint` explicitly after that operation and before checking
115
+ code. An earlier successful prepare does not cover replaced binaries. Missing
116
+ native setup causes a configuration error rather than silently losing checks.
117
+
118
+ Keep a normal `tsconfig.json` covering the checked files. Native diagnostic
119
+ options, such as exact unstable APIs approved for particular test files, live
120
+ under the `@effect/language-service` compiler plugin configuration. Leave its
121
+ `diagnostics` option false when Oxlint owns reporting. The archive tests prove
122
+ that forgotten work fails even without `--deny-warnings` and without a caller
123
+ adding `--type-aware`.
124
+
62
125
  ## Optional style preset
63
126
 
64
127
  Add `style` before `strict`, then keep the project's own policy last:
@@ -74,8 +137,7 @@ export default defineConfig({ extends: [style, strict, project] });
74
137
  Run `oxlint --type-aware --deny-warnings .`. `style` also works alone and is
75
138
  available as the default export of `@commonplaces/oxlint-plugin/style`.
76
139
  It never imports or extends `strict`; `strict` still contains only its original
77
- six groups. Adding this release without selecting `style` leaves strict checks
78
- unchanged. This preset includes more than formatting preferences: Ultracite
140
+ six groups. Adding `style` leaves strict settings unchanged. This preset includes more than formatting preferences: Ultracite
79
141
  also supplies accessibility, test and correctness checks, browser globals,
80
142
  shared ignores and test-file overrides. Its ignores can reduce which files are
81
143
  checked; keep the project's own file policy explicit.
@@ -107,6 +169,37 @@ size limits remain in each consumer's project policy.
107
169
  | `no-inline-comments` | Allow short explanations beside the code they describe. |
108
170
  | `typescript/consistent-return` | Leave return types and Effect generator branches to TypeScript. |
109
171
 
172
+ ### Effect compatibility
173
+
174
+ Style must accept the code required by strict Effect. Putting `strict` last
175
+ only resolves two settings for the same rule; it cannot stop a different style
176
+ rule asking for forbidden code. For example, `typescript/promise-function-async`
177
+ asks for `async` on a host callback that returns `Effect.runPromise(...)`, while
178
+ both strict async checks reject the added keyword. Return the Promise directly
179
+ at the approved host boundary. Keep ordinary application work in Effect.
180
+
181
+ The preset turns off these additional style rules, including when used alone:
182
+
183
+ | Rules set to `off` | Reason |
184
+ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
185
+ | `require-await`, `typescript/promise-function-async`, `typescript/return-await` | Do not ask for `async` or `await`. Strict rules own Promise boundaries. |
186
+ | `promise/prefer-await-to-callbacks`, `promise/prefer-await-to-then` | Do not replace callbacks or Promise chains with `await`; use Effect at the owning adapter. |
187
+ | `typescript/prefer-for-of`, `unicorn/no-array-reduce` | Do not suggest loops. Allow Effect's immutable `Array.reduce`, including renamed imports. |
188
+ | `oxc/no-accumulating-spread` | Its mutation advice is replaced by `strict-effect/no-copying-accumulator` in the Effect group. |
189
+ | `unicorn/prefer-at` | Use checked Effect collection access instead of native `.at`. |
190
+ | `unicorn/prefer-date-now` | Use Effect's clock instead of `Date.now`. |
191
+ | `unicorn/prefer-reflect-apply` | Do not suggest `Reflect.apply`, which strict rejects. |
192
+ | `unicorn/prefer-set-has` | Use Effect collections instead of suggesting a native mutable `Set`. |
193
+ | `typescript/non-nullable-type-assertion-style` | Do not replace a type assertion with `!`; strict rejects both unchecked forms. |
194
+ | `unicorn/prefer-ternary` | Do not request ternaries while strict's `no-ternary` is enabled. |
195
+ | `max-classes-per-file` | Keep related Schema error classes or service contracts in their owning module. |
196
+
197
+ These changes do not grant runtime, SDK or Promise permissions. Keep exact host
198
+ files in the project's policy. Strict still rejects async/await, raw Promises,
199
+ loops, mutation, unchecked access, thrown failures and unapproved runtime calls.
200
+ The collection convention accepts `Array.reduce(initial, callback)(values)`;
201
+ its three-argument form `Array.reduce(values, initial, callback)` still fails.
202
+
110
203
  ### Supported-host adjustments
111
204
 
112
205
  The imported configurations need two small adjustments; these are separate
@@ -120,10 +213,14 @@ from the eleven shared overrides above:
120
213
  - Vitest's file override disabling `vitest/valid-title` is also omitted, so
121
214
  strict's correctness setting still applies inside test files.
122
215
 
123
- The tests install the packed archive on both supported hosts and compare
216
+ The tests install the packed archive on every supported host and compare
124
217
  every resolved strict rule, including its options, with `[style, strict]`.
125
- They also check inherited file overrides. These adjustments import no strict
126
- settings and leave every other upstream setting intact.
218
+ They also check inherited file overrides. Type-aware archive tests compile and
219
+ lint approved host callbacks, SDK adapters, Effect programs, immutable
220
+ collections and related error classes. Restoring the async style rule makes the
221
+ host example fail, proving the type-aware check runs. Forbidden examples must
222
+ retain every strict diagnostic after style is added. The adjustments import no
223
+ strict settings; other upstream settings remain intact.
127
224
 
128
225
  ### Ultracite 8.0.1 comparison
129
226
 
@@ -144,7 +241,8 @@ Both versions contain `no-unmodified-loop-condition` with
144
241
  `checkConditionalExpressions: true`; the supported hosts accept that option.
145
242
  Ultracite 8 requires Oxlint 1.86 or newer, so 1.80 and 1.85 are no longer
146
243
  supported. Its Oxfmt peer requires at least 0.71; repository tooling uses 0.74.0.
147
- The archive is checked on exactly 1.86.0 and the current 1.89.0.
244
+ The current archive is checked on exactly 1.87.0, 1.88.0 and 1.89.0, matching the
245
+ pinned native Effect checker. Published 0.5.0 was qualified on 1.86.0 and 1.89.0.
148
246
 
149
247
  ## Project settings
150
248
 
@@ -224,11 +322,38 @@ Alchemy checks use `alchemy-secrets/`. Keep those names when moving from copied
224
322
  rule files. Remove the copied implementations and change `jsPlugins` to the
225
323
  package subpaths; keep your existing rule settings.
226
324
 
325
+ ## Immutable accumulation
326
+
327
+ `effect` and `strict` enable `strict-effect/no-copying-accumulator` as an error.
328
+ It catches repeated array accumulator spreads, growing object spreads, copying
329
+ `Array.append`/`prepend`/`appendAll`/`prependAll`/`union`/`copy` calls and
330
+ `Record.set`/`union` calls in Effect reducers. Repeatedly copying an ever larger
331
+ result can make a modest input expensive to process.
332
+
333
+ Use `Array.map` or `Array.flatMap` to produce an array. Use `Record.fromEntries`
334
+ to build a record in one pass. When genuine accumulation is needed, use
335
+ persistent `Chunk.append`, `HashMap.set` or `HashSet.add`, which share existing
336
+ structure. The rule never suggests mutation and has no automatic fix.
337
+
338
+ It recognises imported Effect `reduce`, `reduceRight`, `scan` and `scanRight`
339
+ operations, including named, namespace, renamed and immutable aliased imports.
340
+ Both argument orders and locally declared callbacks are covered. It follows
341
+ accumulator aliases, fields and destructured fields, checks copied values by
342
+ their actual local binding, and recognises a copier as the first pipe operation.
343
+ Shadowed names, unrelated libraries, scalar folds, spreads of the current input
344
+ and fixed-shape object spreads remain valid. `Array.fromIterable` is allowed: the
345
+ installed Effect version reuses an input array rather than copying it.
346
+
347
+ This is a local written-pattern check. It does not follow imported callbacks,
348
+ opaque helper implementations, mutable aliases or an accumulator through later
349
+ pipe operations, and it does not measure input sizes or prove a runtime cost.
350
+ Native loops and mutation remain forbidden by the separate strict rules.
351
+
227
352
  ## Rule exports
228
353
 
229
354
  | Package subpath | Rule prefix | Rules |
230
355
  | ------------------- | --------------------- | ----: |
231
- | `/strict-effect` | `strict-effect/` | 29 |
356
+ | `/strict-effect` | `strict-effect/` | 30 |
232
357
  | `/effect` | `effect/` | 9 |
233
358
  | `/alchemy` | `alchemy-secrets/` | 3 |
234
359
  | `/bun` | `bun/` | 1 |
@@ -0,0 +1 @@
1
+ export declare const noCopyingAccumulator: import("@oxlint/plugins").Rule;
@@ -0,0 +1,140 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { Array, Option } from "effect";
3
+ import { constant, operation, referenceOf, valueOf } from "./native-reference.js";
4
+ import { childNodes, propertyName, variableInScope } from "./syntax.js";
5
+ const reducers = ["Array", "Chunk", "HashMap", "HashSet", "Record", "Effect"];
6
+ const isFunction = (node) => node.type === "ArrowFunctionExpression" ||
7
+ node.type === "FunctionExpression" ||
8
+ node.type === "FunctionDeclaration";
9
+ const callbackOf = (context, node, visited = []) => {
10
+ if (Array.some(visited, (entry) => entry === node))
11
+ return Option.none();
12
+ if (isFunction(node))
13
+ return Option.some(node);
14
+ if (node.type !== "Identifier")
15
+ return Option.none();
16
+ const next = [...visited, node];
17
+ return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap((variable) => constant(variable).pipe(Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.flatMap((value) => callbackOf(context, value, next)), Option.orElse(() => Array.head(variable.defs).pipe(Option.filter(() => Array.every(variable.references, (entry) => !entry.isWrite())), Option.map((definition) => definition.node), Option.filter(isFunction))))));
18
+ };
19
+ const bindings = (node) => {
20
+ if (node.type === "Identifier")
21
+ return [node];
22
+ if (node.type === "AssignmentPattern")
23
+ return bindings(node.left);
24
+ if (node.type === "RestElement")
25
+ return bindings(node.argument);
26
+ if (node.type === "ObjectPattern")
27
+ return Array.flatMap(node.properties, (property) => property.type === "Property"
28
+ ? bindings(property.value)
29
+ : property.type === "RestElement"
30
+ ? bindings(property.argument)
31
+ : []);
32
+ if (node.type === "ArrayPattern")
33
+ return Array.flatMap(node.elements, (element) => element === null ? [] : bindings(element));
34
+ return [];
35
+ };
36
+ // Compare bindings, not names: a nested parameter called `acc` is another value.
37
+ // Only immutable local aliases are followed; imported helpers remain opaque.
38
+ const accumulator = (context, node, variables, visited = []) => {
39
+ if (Array.some(visited, (entry) => entry === node))
40
+ return false;
41
+ const next = [...visited, node];
42
+ if (node.type === "TSAsExpression" ||
43
+ node.type === "TSSatisfiesExpression" ||
44
+ node.type === "TSNonNullExpression" ||
45
+ node.type === "ChainExpression")
46
+ return accumulator(context, node.expression, variables, next);
47
+ if (node.type === "MemberExpression")
48
+ return accumulator(context, node.object, variables, next);
49
+ if (node.type !== "Identifier")
50
+ return false;
51
+ return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.exists((variable) => Array.some(variables, (entry) => entry === variable) ||
52
+ constant(variable).pipe(Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.exists((value) => accumulator(context, value, variables, next)))));
53
+ };
54
+ // The argument count is the installed operation's data-first arity. Persistent
55
+ // Chunk/HashMap/HashSet updates share structure and deliberately do not appear.
56
+ const copierArity = (context, node) => referenceOf(context, node).pipe(Option.flatMap((reference) => Option.flatMap(reference.name, (name) => {
57
+ if (reference.module === "effect/Array") {
58
+ if (name === "copy")
59
+ return Option.some(1);
60
+ if (Array.contains(["append", "appendAll", "prepend", "prependAll", "union"], name))
61
+ return Option.some(2);
62
+ }
63
+ if (reference.module === "effect/Record") {
64
+ if (name === "set")
65
+ return Option.some(3);
66
+ if (name === "union")
67
+ return Option.some(2);
68
+ }
69
+ return Option.none();
70
+ })));
71
+ const curriedCopier = (context, node) => {
72
+ const resolved = Option.getOrElse(valueOf(context, node), () => node);
73
+ if (resolved.type !== "CallExpression")
74
+ return Option.contains(copierArity(context, resolved), 1);
75
+ return copierArity(context, resolved.callee).pipe(Option.exists((arity) => arity > 1 && resolved.arguments.length === arity - 1));
76
+ };
77
+ export const noCopyingAccumulator = defineRule({
78
+ meta: {
79
+ type: "problem",
80
+ schema: [],
81
+ docs: { description: "Reject repeated accumulator copies without recommending mutation." },
82
+ messages: {
83
+ copying: "Copying the growing result on every step can make a small input very slow. Use Effect Array.map/flatMap, Record.fromEntries, Chunk.append, HashMap.set or HashSet.add. Keep old values unchanged; do not use push, Object.assign or mutable builders.",
84
+ },
85
+ },
86
+ create(context) {
87
+ return {
88
+ CallExpression(node) {
89
+ const reducer = referenceOf(context, node.callee).pipe(Option.exists((reference) => Array.some(reducers, (module) => reference.module === `effect/${module}`) &&
90
+ Option.exists(reference.name, (name) => Array.contains(["reduce", "reduceRight", "scan", "scanRight"], name))));
91
+ if (!reducer || node.arguments.length < 2)
92
+ return;
93
+ const argument = Array.get(node.arguments, node.arguments.length >= 3 ? 2 : 1);
94
+ const callback = Option.flatMap(argument, (value) => callbackOf(context, value));
95
+ if (Option.isNone(callback) || !isFunction(callback.value))
96
+ return;
97
+ const first = Array.head(callback.value.params);
98
+ const variables = first.pipe(Option.map((parameter) => Array.flatMap(bindings(parameter), (binding) => Option.toArray(variableInScope(context.sourceCode.getScope(binding), binding.name)))), Option.getOrElse(() => []));
99
+ const report = (copy) => context.report({ node: copy, messageId: "copying" });
100
+ const isAccumulator = (value) => accumulator(context, value, variables);
101
+ const inspect = (value) => {
102
+ // A fixed-shape object update is bounded. Dynamic keys or another
103
+ // spread can grow the copied record with each incoming element.
104
+ if (value.type === "SpreadElement" &&
105
+ (value.parent.type === "ArrayExpression" ||
106
+ (value.parent.type === "ObjectExpression" &&
107
+ Array.some(value.parent.properties, (property) => (property.type === "Property" &&
108
+ property.computed &&
109
+ property.key.type !== "Literal") ||
110
+ (property.type === "SpreadElement" && property !== value)))) &&
111
+ isAccumulator(value.argument))
112
+ report(value);
113
+ if (value.type === "CallExpression") {
114
+ if (copierArity(context, value.callee).pipe(Option.exists((arity) => value.arguments.length >= arity)) &&
115
+ Array.head(value.arguments).pipe(Option.exists(isAccumulator)))
116
+ report(value);
117
+ if (value.callee.type === "CallExpression" &&
118
+ curriedCopier(context, value.callee) &&
119
+ Array.head(value.arguments).pipe(Option.exists(isAccumulator)))
120
+ report(value);
121
+ const pipe = operation(context, value.callee, "effect/Function", "pipe") ||
122
+ operation(context, value.callee, "effect", "pipe");
123
+ const method = value.callee.type === "MemberExpression" &&
124
+ Option.contains(propertyName(value.callee.property), "pipe") &&
125
+ isAccumulator(value.callee.object);
126
+ const transforms = method ? value.arguments : Array.drop(value.arguments, 1);
127
+ if (method || (pipe && Array.head(value.arguments).pipe(Option.exists(isAccumulator))))
128
+ Array.head(transforms).pipe(Option.map((transform) => {
129
+ if (curriedCopier(context, transform))
130
+ report(transform);
131
+ }));
132
+ }
133
+ Array.forEach(childNodes(context, value), inspect);
134
+ };
135
+ if (callback.value.body)
136
+ inspect(callback.value.body);
137
+ },
138
+ };
139
+ },
140
+ });
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
- import { Array, Option, Predicate, Record, Schema } from "effect";
2
+ import { Array, HashMap, Option, Predicate, Record, Schema } from "effect";
3
3
  import { childNodes, propertyName, variableInScope } from "./syntax.js";
4
4
  const RuleId = Schema.String.check(Schema.isPattern(/^\S+$/u));
5
5
  const Options = Schema.Struct({
@@ -37,7 +37,7 @@ const staticValue = (context, node, visited = []) => {
37
37
  const staticProperties = (context, node) => staticValue(context, node).pipe(Option.filter((value) => value.type === "ObjectExpression"), Option.flatMap((value) => {
38
38
  if (value.type !== "ObjectExpression")
39
39
  return Option.none();
40
- const empty = {};
40
+ const empty = HashMap.empty();
41
41
  return Option.some(Array.reduce(value.properties, empty, (known, entry) => {
42
42
  if (entry.type !== "Property")
43
43
  return empty;
@@ -48,9 +48,9 @@ const staticProperties = (context, node) => staticValue(context, node).pipe(Opti
48
48
  : propertyName(entry.key);
49
49
  return Option.match(key, {
50
50
  onNone: () => empty,
51
- onSome: (name) => Record.set(known, name, entry),
51
+ onSome: (name) => HashMap.set(known, name, entry),
52
52
  });
53
- }));
53
+ }).pipe(HashMap.toEntries, Record.fromEntries));
54
54
  }));
55
55
  const importedConfig = (context, node, namespace) => {
56
56
  if (node.type !== "Identifier")
@@ -51,7 +51,8 @@ export const syntaxRules = {
51
51
  "reduce",
52
52
  "some",
53
53
  "sort",
54
- ], name)))))
54
+ ], name) &&
55
+ (name !== "reduce" || node.arguments.length >= 3)))))
55
56
  report(context, node);
56
57
  },
57
58
  })),
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { OxlintConfig } from "oxlint";
1
2
  /** General code checks, with no project-specific file names or settings. */
2
3
  export declare const general: {
3
4
  jsPlugins: string[];
@@ -21,33 +22,7 @@ export declare const general: {
21
22
  };
22
23
  };
23
24
  /** Effect code checks. Add the general group separately if wanted. */
24
- export declare const effect: {
25
- jsPlugins: string[];
26
- rules: {
27
- "effect/no-ambient-process": "error";
28
- "effect/no-async-await-promise": "error";
29
- "effect/no-console": "error";
30
- "effect/no-instanceof": "error";
31
- "effect/no-manual-tag": "error";
32
- "effect/no-raw-fetch": "error";
33
- "effect/require-service-operation-instrumentation": "error";
34
- "strict-effect/static-layers": "error";
35
- "strict-effect/subprocess-pipe-capture": "error";
36
- "strict-effect/no-runtime-type-assertions": "error";
37
- "strict-effect/no-unchecked-index": "error";
38
- "strict-effect/no-native-at": "error";
39
- "strict-effect/tagged-error-name": "error";
40
- "strict-effect/error-constructor-new": "error";
41
- "strict-effect/no-promise-workflow": "error";
42
- "strict-effect/no-unsafe-option-unwrap": "error";
43
- "strict-effect/no-unchecked-json": "error";
44
- "strict-effect/no-native-work": "error";
45
- "strict-effect/no-imperative-collections": "error";
46
- "strict-effect/no-native-client-replica": "error";
47
- "bun/no-ambient-host-api": "error";
48
- "anti-slop-effect/no-service-constructor-imports": "error";
49
- };
50
- };
25
+ export declare const effect: OxlintConfig;
51
26
  /** Optional strict coding conventions; boundary permissions remain project facts. */
52
27
  export declare const conventions: {
53
28
  jsPlugins: string[];
@@ -95,7 +70,7 @@ export declare const alchemy: {
95
70
  };
96
71
  /** Complete strict Effect application profile; exact project owners stay local. */
97
72
  export declare const strict: {
98
- extends: ({
73
+ extends: (OxlintConfig | {
99
74
  jsPlugins: string[];
100
75
  rules: {
101
76
  "anti-slop/no-chained-type-assertions": "error";
@@ -115,32 +90,6 @@ export declare const strict: {
115
90
  "anti-slop/require-safety-comment-for-type-assertion": "error";
116
91
  "package/no-cross-package-source-imports": "error";
117
92
  };
118
- } | {
119
- jsPlugins: string[];
120
- rules: {
121
- "effect/no-ambient-process": "error";
122
- "effect/no-async-await-promise": "error";
123
- "effect/no-console": "error";
124
- "effect/no-instanceof": "error";
125
- "effect/no-manual-tag": "error";
126
- "effect/no-raw-fetch": "error";
127
- "effect/require-service-operation-instrumentation": "error";
128
- "strict-effect/static-layers": "error";
129
- "strict-effect/subprocess-pipe-capture": "error";
130
- "strict-effect/no-runtime-type-assertions": "error";
131
- "strict-effect/no-unchecked-index": "error";
132
- "strict-effect/no-native-at": "error";
133
- "strict-effect/tagged-error-name": "error";
134
- "strict-effect/error-constructor-new": "error";
135
- "strict-effect/no-promise-workflow": "error";
136
- "strict-effect/no-unsafe-option-unwrap": "error";
137
- "strict-effect/no-unchecked-json": "error";
138
- "strict-effect/no-native-work": "error";
139
- "strict-effect/no-imperative-collections": "error";
140
- "strict-effect/no-native-client-replica": "error";
141
- "bun/no-ambient-host-api": "error";
142
- "anti-slop-effect/no-service-constructor-imports": "error";
143
- };
144
93
  } | {
145
94
  jsPlugins: string[];
146
95
  rules: Record<`effect-conventions/${string}`, "error">;
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { correctness } from "@effect/tsgo/oxlint-presets";
1
2
  import { Record } from "effect";
2
3
  import conventionPlugin from "./conventions.js";
3
4
  import webPlugin from "./web.js";
@@ -25,6 +26,7 @@ export const general = {
25
26
  };
26
27
  /** Effect code checks. Add the general group separately if wanted. */
27
28
  export const effect = {
29
+ ...correctness,
28
30
  jsPlugins: [
29
31
  "@commonplaces/oxlint-plugin/effect",
30
32
  "@commonplaces/oxlint-plugin/strict-effect",
@@ -32,6 +34,7 @@ export const effect = {
32
34
  "@commonplaces/oxlint-plugin/anti-slop-effect",
33
35
  ],
34
36
  rules: {
37
+ ...Record.map(correctness.rules ?? {}, () => "error"),
35
38
  "effect/no-ambient-process": "error",
36
39
  "effect/no-async-await-promise": "error",
37
40
  "effect/no-console": "error",
@@ -51,6 +54,7 @@ export const effect = {
51
54
  "strict-effect/no-unchecked-json": "error",
52
55
  "strict-effect/no-native-work": "error",
53
56
  "strict-effect/no-imperative-collections": "error",
57
+ "strict-effect/no-copying-accumulator": "error",
54
58
  "strict-effect/no-native-client-replica": "error",
55
59
  "bun/no-ambient-host-api": "error",
56
60
  "anti-slop-effect/no-service-constructor-imports": "error",
@@ -1,5 +1,5 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
- import { Array, Number, Option, Predicate, Record, Schema } from "effect";
2
+ import { Array, Chunk, Number, Option, Predicate, Record, Schema } from "effect";
3
3
  import { variableInScope } from "../syntax.js";
4
4
  // A checked manifest consumer selects export conditions before configuring this
5
5
  // pure rule. A false entry preserves a manifest's explicit null export fence.
@@ -104,13 +104,13 @@ const moduleAddress = (context, node, visited = []) => {
104
104
  ? Option.fromNullishOr(definition.node.init)
105
105
  : Option.none()), Option.flatMap((initialiser) => moduleAddress(context, initialiser, next)));
106
106
  };
107
- const resolvePath = (base, address) => Array.join(Array.reduce(address.split("/"), base === "." || base === "" ? [] : base.split("/"), (parts, part) => part === ".."
108
- ? parts.length === 0 || Option.contains(Array.last(parts), "..")
109
- ? [...parts, ".."]
110
- : Array.dropRight(parts, 1)
107
+ const resolvePath = (base, address) => Chunk.join(Array.reduce(address.split("/"), Chunk.fromIterable(base === "." || base === "" ? [] : base.split("/")), (parts, part) => part === ".."
108
+ ? parts.length === 0 || Option.contains(Chunk.last(parts), "..")
109
+ ? Chunk.append(parts, "..")
110
+ : Chunk.dropRight(parts, 1)
111
111
  : part === "." || part === ""
112
112
  ? parts
113
- : [...parts, part]), "/");
113
+ : Chunk.append(parts, part)), "/");
114
114
  const relativeFile = (filename, cwd) => {
115
115
  const file = normalise(filename);
116
116
  const root = normalise(cwd).replace(/\/$/u, "");
package/dist/package.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
- import { Array, Option, Predicate } from "effect";
2
+ import { Array, Chunk, Option, Predicate } from "effect";
3
3
  import { workspaceImportBoundariesRule } from "./package/workspace-import-boundaries.js";
4
4
  import { noRedundantRuleRegistrations } from "./config.js";
5
- const resolveRelative = (fileName, specifier) => Array.join(Array.reduce(specifier.split("/"), Array.dropRight(fileName.split("/"), 1), (segments, segment) => segment === ".."
6
- ? Array.dropRight(segments, 1)
5
+ const resolveRelative = (fileName, specifier) => Chunk.join(Array.reduce(specifier.split("/"), Chunk.fromIterable(Array.dropRight(fileName.split("/"), 1)), (segments, segment) => segment === ".."
6
+ ? Chunk.dropRight(segments, 1)
7
7
  : segment === "." || segment.length === 0
8
8
  ? segments
9
- : [...segments, segment]), "/");
9
+ : Chunk.append(segments, segment)), "/");
10
10
  const workspaceSourceRoot = (fileName) => Option.fromNullishOr(fileName.match(/\/(?:apps|packages|content)\/[^/]+(?:\/|$)/u)).pipe(Option.flatMap(Array.head), Option.map((root) => root.replace(/\/$/u, "")));
11
11
  const isPrivatePackageAlias = (specifier) => /^@[^/]+\/[^/]+\/src(?:\/|$)/u.test(specifier);
12
12
  const isCrossWorkspaceSourcePath = (fileName, specifier) => {
@@ -1,5 +1,5 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
- import { Array, Option, Schema } from "effect";
2
+ import { Array, Chunk, Option, Schema } from "effect";
3
3
  import { constant, operation, referenceOf, valueOf } from "./native-reference.js";
4
4
  import { childNodes, propertyName, variableInScope } from "./syntax.js";
5
5
  const Options = Schema.Struct({
@@ -17,11 +17,11 @@ const adapted = (context, node, name, adapters) => {
17
17
  return false;
18
18
  return referenceOf(context, value.value.callee).pipe(Option.exists((reference) => Array.some(adapters, (adapter) => {
19
19
  const relative = reference.module.startsWith(".")
20
- ? Array.join(Array.reduce(reference.module.split("/"), Array.dropRight(context.filename.replaceAll("\\", "/").split("/"), 1), (parts, segment) => segment === ".."
21
- ? Array.dropRight(parts, 1)
20
+ ? Chunk.join(Array.reduce(reference.module.split("/"), Chunk.fromIterable(Array.dropRight(context.filename.replaceAll("\\", "/").split("/"), 1)), (parts, segment) => segment === ".."
21
+ ? Chunk.dropRight(parts, 1)
22
22
  : segment === "."
23
23
  ? parts
24
- : [...parts, segment]), "/")
24
+ : Chunk.append(parts, segment)), "/")
25
25
  : "";
26
26
  const source = (path) => path.replace(/\.[cm]?[jt]sx?$/u, "");
27
27
  return (adapter.operation === name &&
@@ -1,6 +1,8 @@
1
1
  import { subprocessPipeCapture } from "./subprocess-pipe-capture.js";
2
+ import { noCopyingAccumulator } from "./accumulation.js";
2
3
  import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
3
4
  import * as Array from "effect/Array";
5
+ import * as Chunk from "effect/Chunk";
4
6
  import * as Function from "effect/Function";
5
7
  import * as Option from "effect/Option";
6
8
  import * as Predicate from "effect/Predicate";
@@ -1510,15 +1512,15 @@ const relativeModule = (filename, address) => {
1510
1512
  return address;
1511
1513
  }
1512
1514
  const directory = filename.replaceAll("\\", "/").replace(/\/[^/]+$/u, "");
1513
- return Function.pipe(`${directory}/${address}`.split("/"), Array.reduce([], (parts, part) => {
1515
+ return Function.pipe(`${directory}/${address}`.split("/"), Array.reduce(Chunk.empty(), (parts, part) => {
1514
1516
  if (part === "." || part === "") {
1515
1517
  return parts;
1516
1518
  }
1517
1519
  if (part === "..") {
1518
- return Array.dropRight(parts, 1);
1520
+ return Chunk.dropRight(parts, 1);
1519
1521
  }
1520
- return [...parts, part];
1521
- }), Array.join("/"));
1522
+ return Chunk.append(parts, part);
1523
+ }), Chunk.join("/"));
1522
1524
  };
1523
1525
  const CheckedPageOptions = Schema.Struct({
1524
1526
  checkedHelper: Schema.Struct({ module: Schema.String, export: Schema.String }),
@@ -2560,6 +2562,7 @@ export default eslintCompatPlugin({
2560
2562
  "no-native-work": noNativeWork,
2561
2563
  "no-runtime-type-assertions": noRuntimeTypeAssertions,
2562
2564
  "no-imperative-collections": noImperativeCollections,
2565
+ "no-copying-accumulator": noCopyingAccumulator,
2563
2566
  "public-host-literals": publicHostLiterals,
2564
2567
  "route-literal-ownership": routeLiteralOwnership,
2565
2568
  "http-client-ownership": httpClientOwnership,
package/dist/style.js CHANGED
@@ -22,6 +22,26 @@ const style = {
22
22
  },
23
23
  ],
24
24
  rules: {
25
+ // Effect owns asynchronous work. Host callbacks may return a Promise
26
+ // directly without adding async, await or a Promise chain.
27
+ "require-await": "off",
28
+ "typescript/promise-function-async": "off",
29
+ "typescript/return-await": "off",
30
+ "promise/prefer-await-to-callbacks": "off",
31
+ "promise/prefer-await-to-then": "off",
32
+ // These rewrites suggest loops, mutation or native APIs that strict Effect
33
+ // rejects. Immutable Effect operations remain checked by the strict rules.
34
+ "typescript/prefer-for-of": "off",
35
+ "typescript/non-nullable-type-assertion-style": "off",
36
+ "oxc/no-accumulating-spread": "off",
37
+ "unicorn/no-array-reduce": "off",
38
+ "unicorn/prefer-at": "off",
39
+ "unicorn/prefer-date-now": "off",
40
+ "unicorn/prefer-reflect-apply": "off",
41
+ "unicorn/prefer-set-has": "off",
42
+ "unicorn/prefer-ternary": "off",
43
+ // An errors or service module may own several related Schema classes.
44
+ "max-classes-per-file": "off",
25
45
  "unicorn/no-array-for-each": "off",
26
46
  "unicorn/no-array-sort": "off",
27
47
  "unicorn/throw-new-error": "off",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonplaces/oxlint-plugin",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Shared Oxlint rules for strict Effect TypeScript, Alchemy and repository code.",
5
5
  "license": "MIT",
6
6
  "author": "Cooper Corbett",
@@ -72,8 +72,10 @@
72
72
  "prepack": "bun run build"
73
73
  },
74
74
  "dependencies": {
75
+ "@effect/tsgo": "0.55.0",
75
76
  "@oxlint/plugins": "1.89.0",
76
77
  "effect": "4.0.5",
78
+ "oxlint-tsgolint": "7.0.2004",
77
79
  "ultracite": "8.0.1"
78
80
  },
79
81
  "devDependencies": {
@@ -84,7 +86,7 @@
84
86
  "vitest": "5.0.5"
85
87
  },
86
88
  "peerDependencies": {
87
- "oxlint": "1.86.0 || 1.89.0"
89
+ "oxlint": "1.87.0 || 1.88.0 || 1.89.0"
88
90
  },
89
91
  "engines": {
90
92
  "node": ">=22.14.0"