@commonplaces/oxlint-plugin 0.4.0 → 0.6.0
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 +196 -7
- package/dist/accumulation.d.ts +1 -0
- package/dist/accumulation.js +140 -0
- package/dist/config.js +4 -4
- package/dist/conventions/syntax.js +2 -1
- package/dist/index.d.ts +4 -54
- package/dist/index.js +5 -0
- package/dist/package/workspace-import-boundaries.js +6 -6
- package/dist/package.js +4 -4
- package/dist/service-instrumentation.js +4 -4
- package/dist/strict-effect.js +24 -4
- package/dist/style.d.ts +4 -0
- package/dist/style.js +58 -0
- package/package.json +15 -8
package/README.md
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
# @commonplaces/oxlint-plugin
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
checks.
|
|
3
|
+
107 custom Oxlint rules for strict Effect TypeScript, Alchemy and general code
|
|
4
|
+
checks. Version 0.6.0 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
|
-
the rule tests. The development tools use 1.
|
|
6
|
+
the rule tests. The development tools use 1.89.0; consuming projects may use any supported version.
|
|
7
7
|
All three exact versions are checked with independently installed archives. ESLint is not a supported host.
|
|
8
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.
|
|
12
|
+
|
|
9
13
|
## Install
|
|
10
14
|
|
|
11
|
-
Install
|
|
15
|
+
Install version 0.6.0 and save its exact version:
|
|
12
16
|
|
|
13
17
|
```sh
|
|
14
|
-
npm install --save-dev --save-exact @commonplaces/oxlint-plugin@
|
|
18
|
+
npm install --save-dev --save-exact @commonplaces/oxlint-plugin@0.6.0 oxlint@1.89.0
|
|
15
19
|
```
|
|
16
20
|
|
|
21
|
+
Version 0.6.0 supports exactly Oxlint 1.87.0, 1.88.0 or 1.89.0. Older releases
|
|
22
|
+
keep their earlier host lists. Consumer adoption is a separate step.
|
|
23
|
+
|
|
17
24
|
The manifest and lockfile retain the resolved version. Historical GitHub release
|
|
18
25
|
archives remain separate distribution artefacts; a GitHub release does not publish
|
|
19
26
|
an npm version.
|
|
@@ -46,7 +53,7 @@ project facts.
|
|
|
46
53
|
`defineConfig({ extends: [strict, projectPolicy] })`. It adds no project paths
|
|
47
54
|
or source ignores. Keep exact ownership settings in the consuming project.
|
|
48
55
|
|
|
49
|
-
The root exports these six groups and the `strict` composition. Individual plugins use
|
|
56
|
+
The root exports these six groups, the optional `style` preset and the `strict` composition. Individual plugins use
|
|
50
57
|
the subpaths below. For example, a JSON configuration can enable one rule:
|
|
51
58
|
|
|
52
59
|
```json
|
|
@@ -56,6 +63,161 @@ the subpaths below. For example, a JSON configuration can enable one rule:
|
|
|
56
63
|
}
|
|
57
64
|
```
|
|
58
65
|
|
|
66
|
+
## Native Effect correctness
|
|
67
|
+
|
|
68
|
+
Both `effect` and `strict` include every check in the pinned
|
|
69
|
+
`@effect/tsgo` **0.55.0** correctness preset, all at error level. This catches
|
|
70
|
+
forgotten Effect programs, incorrect generator yields, unhandled errors and
|
|
71
|
+
requirements, unchecked Effect type assertions, missing Schema class brands and
|
|
72
|
+
unstable APIs. Type-aware checking is enabled by the group itself; consumers
|
|
73
|
+
do not need to import another preset. Upstream's unrelated style and maintainer
|
|
74
|
+
preferences are not added to the public group.
|
|
75
|
+
|
|
76
|
+
The package installs `@effect/tsgo` and `oxlint-tsgolint` **7.0.2004** as exact
|
|
77
|
+
regular dependencies. After installing the package,
|
|
78
|
+
prepare the locally installed Oxlint binaries:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
npx --no-install effect-tsgo patch --no-typescript --oxlint
|
|
82
|
+
npx --no-install oxlint --deny-warnings .
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Repeat the patch after dependency installation, normally in the consuming
|
|
86
|
+
project's existing `prepare` command. `--no-typescript` keeps this setup focused
|
|
87
|
+
on Oxlint; a separate TypeScript compiler installation is not needed for the
|
|
88
|
+
patch. Missing native setup causes a configuration error. It never silently
|
|
89
|
+
falls back to the custom syntax checks. The checker supports exactly the three
|
|
90
|
+
Oxlint versions above; 1.86.0 is no longer supported in version 0.6.0.
|
|
91
|
+
|
|
92
|
+
Keep a normal `tsconfig.json` covering the checked files. Native diagnostic
|
|
93
|
+
options, such as exact unstable APIs approved for particular test files, live
|
|
94
|
+
under the `@effect/language-service` compiler plugin configuration. Leave its
|
|
95
|
+
`diagnostics` option false when Oxlint owns reporting. The archive tests prove
|
|
96
|
+
that forgotten work fails even without `--deny-warnings` and without a caller
|
|
97
|
+
adding `--type-aware`.
|
|
98
|
+
|
|
99
|
+
## Optional style preset
|
|
100
|
+
|
|
101
|
+
Add `style` before `strict`, then keep the project's own policy last:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { style, strict } from "@commonplaces/oxlint-plugin";
|
|
105
|
+
import { defineConfig } from "oxlint";
|
|
106
|
+
import project from "./tools/oxlint/project.config.ts";
|
|
107
|
+
|
|
108
|
+
export default defineConfig({ extends: [style, strict, project] });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Run `oxlint --type-aware --deny-warnings .`. `style` also works alone and is
|
|
112
|
+
available as the default export of `@commonplaces/oxlint-plugin/style`.
|
|
113
|
+
It never imports or extends `strict`; `strict` still contains only its original
|
|
114
|
+
six groups. Adding `style` leaves strict settings unchanged. This preset includes more than formatting preferences: Ultracite
|
|
115
|
+
also supplies accessibility, test and correctness checks, browser globals,
|
|
116
|
+
shared ignores and test-file overrides. Its ignores can reduce which files are
|
|
117
|
+
checked; keep the project's own file policy explicit.
|
|
118
|
+
|
|
119
|
+
Ultracite **8.0.1** is an exact regular dependency, selected at the owner's
|
|
120
|
+
request to use the latest releases. It requires no separate consumer install. A peer would
|
|
121
|
+
make consumers choose and install a second package, and could make imports of
|
|
122
|
+
the root package fail when it is missing. The dependency also installs
|
|
123
|
+
Ultracite's CLI dependencies; only the three Oxlint configuration modules run
|
|
124
|
+
when importing this preset. No Ultracite CLI, formatter or Effect runtime runs.
|
|
125
|
+
|
|
126
|
+
### Shared overrides
|
|
127
|
+
|
|
128
|
+
These eleven rules are off in both historical root configurations: Common
|
|
129
|
+
Practice at `3b6beef^` and Site at `dda3820`. File-specific permissions and
|
|
130
|
+
size limits remain in each consumer's project policy.
|
|
131
|
+
|
|
132
|
+
| Rule set to `off` | Reason |
|
|
133
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| `unicorn/no-array-for-each` | Allow Effect's immutable `Array.forEach`; strict collection checks cover native mutation. |
|
|
135
|
+
| `unicorn/no-array-sort` | Allow Effect's immutable `Array.sort`; strict collection checks cover native mutation. |
|
|
136
|
+
| `unicorn/throw-new-error` | Allow the `Schema.TaggedError` factory; `strict-effect/error-constructor-new` checks error construction when strict is selected. |
|
|
137
|
+
| `no-redeclare` | Allow TypeScript overloads and merged declarations. |
|
|
138
|
+
| `react/function-component-definition` | Allow the project's existing component declaration forms. |
|
|
139
|
+
| `sort-keys` | Keep related object fields in meaningful order. |
|
|
140
|
+
| `func-names` | Allow unnamed library callbacks and Effect generators. |
|
|
141
|
+
| `func-style` | Allow function declarations and functions stored in constants. |
|
|
142
|
+
| `no-use-before-define` | Allow declarations to refer to later declarations. |
|
|
143
|
+
| `no-inline-comments` | Allow short explanations beside the code they describe. |
|
|
144
|
+
| `typescript/consistent-return` | Leave return types and Effect generator branches to TypeScript. |
|
|
145
|
+
|
|
146
|
+
### Effect compatibility
|
|
147
|
+
|
|
148
|
+
Style must accept the code required by strict Effect. Putting `strict` last
|
|
149
|
+
only resolves two settings for the same rule; it cannot stop a different style
|
|
150
|
+
rule asking for forbidden code. For example, `typescript/promise-function-async`
|
|
151
|
+
asks for `async` on a host callback that returns `Effect.runPromise(...)`, while
|
|
152
|
+
both strict async checks reject the added keyword. Return the Promise directly
|
|
153
|
+
at the approved host boundary. Keep ordinary application work in Effect.
|
|
154
|
+
|
|
155
|
+
The preset turns off these additional style rules, including when used alone:
|
|
156
|
+
|
|
157
|
+
| Rules set to `off` | Reason |
|
|
158
|
+
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
159
|
+
| `require-await`, `typescript/promise-function-async`, `typescript/return-await` | Do not ask for `async` or `await`. Strict rules own Promise boundaries. |
|
|
160
|
+
| `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. |
|
|
161
|
+
| `typescript/prefer-for-of`, `unicorn/no-array-reduce` | Do not suggest loops. Allow Effect's immutable `Array.reduce`, including renamed imports. |
|
|
162
|
+
| `oxc/no-accumulating-spread` | Its mutation advice is replaced by `strict-effect/no-copying-accumulator` in the Effect group. |
|
|
163
|
+
| `unicorn/prefer-at` | Use checked Effect collection access instead of native `.at`. |
|
|
164
|
+
| `unicorn/prefer-date-now` | Use Effect's clock instead of `Date.now`. |
|
|
165
|
+
| `unicorn/prefer-reflect-apply` | Do not suggest `Reflect.apply`, which strict rejects. |
|
|
166
|
+
| `unicorn/prefer-set-has` | Use Effect collections instead of suggesting a native mutable `Set`. |
|
|
167
|
+
| `typescript/non-nullable-type-assertion-style` | Do not replace a type assertion with `!`; strict rejects both unchecked forms. |
|
|
168
|
+
| `unicorn/prefer-ternary` | Do not request ternaries while strict's `no-ternary` is enabled. |
|
|
169
|
+
| `max-classes-per-file` | Keep related Schema error classes or service contracts in their owning module. |
|
|
170
|
+
|
|
171
|
+
These changes do not grant runtime, SDK or Promise permissions. Keep exact host
|
|
172
|
+
files in the project's policy. Strict still rejects async/await, raw Promises,
|
|
173
|
+
loops, mutation, unchecked access, thrown failures and unapproved runtime calls.
|
|
174
|
+
The collection convention accepts `Array.reduce(initial, callback)(values)`;
|
|
175
|
+
its three-argument form `Array.reduce(values, initial, callback)` still fails.
|
|
176
|
+
|
|
177
|
+
### Supported-host adjustments
|
|
178
|
+
|
|
179
|
+
The imported configurations need two small adjustments; these are separate
|
|
180
|
+
from the eleven shared overrides above:
|
|
181
|
+
|
|
182
|
+
- React's explicit disables for `jsx-a11y/no-autofocus` and
|
|
183
|
+
`react/forward-ref-uses-ref` are omitted. This lets the host's correctness
|
|
184
|
+
settings apply: selecting `strict` keeps both at error level. Merely putting
|
|
185
|
+
`strict` last would not overcome an explicit upstream disable when strict
|
|
186
|
+
enables a rule through its correctness category.
|
|
187
|
+
- Vitest's file override disabling `vitest/valid-title` is also omitted, so
|
|
188
|
+
strict's correctness setting still applies inside test files.
|
|
189
|
+
|
|
190
|
+
The tests install the packed archive on every supported host and compare
|
|
191
|
+
every resolved strict rule, including its options, with `[style, strict]`.
|
|
192
|
+
They also check inherited file overrides. Type-aware archive tests compile and
|
|
193
|
+
lint approved host callbacks, SDK adapters, Effect programs, immutable
|
|
194
|
+
collections and related error classes. Restoring the async style rule makes the
|
|
195
|
+
host example fail, proving the type-aware check runs. Forbidden examples must
|
|
196
|
+
retain every strict diagnostic after style is added. The adjustments import no
|
|
197
|
+
strict settings; other upstream settings remain intact.
|
|
198
|
+
|
|
199
|
+
### Ultracite 8.0.1 comparison
|
|
200
|
+
|
|
201
|
+
The installed npm archives were compared, including rule options and file
|
|
202
|
+
patterns, rather than relying on release notes. For the three selected presets:
|
|
203
|
+
|
|
204
|
+
| Preset | Rules in 7.12.0 | Rules in 8.0.1 | Change |
|
|
205
|
+
| ------ | --------------: | -------------: | ------------------------------------------------------------- |
|
|
206
|
+
| Core | 536 | 537 | Adds `typescript/no-generated-empty-object-type` as an error. |
|
|
207
|
+
| React | 124 | 124 | `react/jsx-no-target-blank` adds `{ forms: true }`. |
|
|
208
|
+
| Vitest | 74 | 75 | Adds `vitest/consistent-test-it` as an error. |
|
|
209
|
+
|
|
210
|
+
Counts include entries explicitly set to off and entries in file overrides.
|
|
211
|
+
No rule is removed and no other rule severity or option changes. Core's shared
|
|
212
|
+
test overrides and Vitest's test rules also start matching `.mts`, `.cts`,
|
|
213
|
+
`.mjs` and `.cjs` files. Shared ignores, globals and plugin lists are unchanged.
|
|
214
|
+
Both versions contain `no-unmodified-loop-condition` with
|
|
215
|
+
`checkConditionalExpressions: true`; the supported hosts accept that option.
|
|
216
|
+
Ultracite 8 requires Oxlint 1.86 or newer, so 1.80 and 1.85 are no longer
|
|
217
|
+
supported. Its Oxfmt peer requires at least 0.71; repository tooling uses 0.74.0.
|
|
218
|
+
The current archive is checked on exactly 1.87.0, 1.88.0 and 1.89.0, matching the
|
|
219
|
+
pinned native Effect checker. Published 0.5.0 was qualified on 1.86.0 and 1.89.0.
|
|
220
|
+
|
|
59
221
|
## Project settings
|
|
60
222
|
|
|
61
223
|
Rules that refer to project files, schemas or routes are available but excluded
|
|
@@ -134,11 +296,38 @@ Alchemy checks use `alchemy-secrets/`. Keep those names when moving from copied
|
|
|
134
296
|
rule files. Remove the copied implementations and change `jsPlugins` to the
|
|
135
297
|
package subpaths; keep your existing rule settings.
|
|
136
298
|
|
|
299
|
+
## Immutable accumulation
|
|
300
|
+
|
|
301
|
+
`effect` and `strict` enable `strict-effect/no-copying-accumulator` as an error.
|
|
302
|
+
It catches repeated array accumulator spreads, growing object spreads, copying
|
|
303
|
+
`Array.append`/`prepend`/`appendAll`/`prependAll`/`union`/`copy` calls and
|
|
304
|
+
`Record.set`/`union` calls in Effect reducers. Repeatedly copying an ever larger
|
|
305
|
+
result can make a modest input expensive to process.
|
|
306
|
+
|
|
307
|
+
Use `Array.map` or `Array.flatMap` to produce an array. Use `Record.fromEntries`
|
|
308
|
+
to build a record in one pass. When genuine accumulation is needed, use
|
|
309
|
+
persistent `Chunk.append`, `HashMap.set` or `HashSet.add`, which share existing
|
|
310
|
+
structure. The rule never suggests mutation and has no automatic fix.
|
|
311
|
+
|
|
312
|
+
It recognises imported Effect `reduce`, `reduceRight`, `scan` and `scanRight`
|
|
313
|
+
operations, including named, namespace, renamed and immutable aliased imports.
|
|
314
|
+
Both argument orders and locally declared callbacks are covered. It follows
|
|
315
|
+
accumulator aliases, fields and destructured fields, checks copied values by
|
|
316
|
+
their actual local binding, and recognises a copier as the first pipe operation.
|
|
317
|
+
Shadowed names, unrelated libraries, scalar folds, spreads of the current input
|
|
318
|
+
and fixed-shape object spreads remain valid. `Array.fromIterable` is allowed: the
|
|
319
|
+
installed Effect version reuses an input array rather than copying it.
|
|
320
|
+
|
|
321
|
+
This is a local written-pattern check. It does not follow imported callbacks,
|
|
322
|
+
opaque helper implementations, mutable aliases or an accumulator through later
|
|
323
|
+
pipe operations, and it does not measure input sizes or prove a runtime cost.
|
|
324
|
+
Native loops and mutation remain forbidden by the separate strict rules.
|
|
325
|
+
|
|
137
326
|
## Rule exports
|
|
138
327
|
|
|
139
328
|
| Package subpath | Rule prefix | Rules |
|
|
140
329
|
| ------------------- | --------------------- | ----: |
|
|
141
|
-
| `/strict-effect` | `strict-effect/` |
|
|
330
|
+
| `/strict-effect` | `strict-effect/` | 30 |
|
|
142
331
|
| `/effect` | `effect/` | 9 |
|
|
143
332
|
| `/alchemy` | `alchemy-secrets/` | 3 |
|
|
144
333
|
| `/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) =>
|
|
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")
|
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">;
|
|
@@ -180,3 +129,4 @@ export declare const strict: {
|
|
|
180
129
|
};
|
|
181
130
|
})[];
|
|
182
131
|
};
|
|
132
|
+
export { default as style } from "./style.js";
|
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",
|
|
@@ -106,3 +110,4 @@ export const alchemy = {
|
|
|
106
110
|
export const strict = {
|
|
107
111
|
extends: [general, effect, alchemy, conventions, web, typescript],
|
|
108
112
|
};
|
|
113
|
+
export { default as style } from "./style.js";
|
|
@@ -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) =>
|
|
108
|
-
? parts.length === 0 || Option.contains(
|
|
109
|
-
?
|
|
110
|
-
:
|
|
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
|
-
:
|
|
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) =>
|
|
6
|
-
?
|
|
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
|
-
:
|
|
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
|
-
?
|
|
21
|
-
?
|
|
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
|
-
:
|
|
24
|
+
: Chunk.append(parts, segment)), "/")
|
|
25
25
|
: "";
|
|
26
26
|
const source = (path) => path.replace(/\.[cm]?[jt]sx?$/u, "");
|
|
27
27
|
return (adapter.operation === name &&
|
package/dist/strict-effect.js
CHANGED
|
@@ -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";
|
|
@@ -596,6 +598,23 @@ const layerExpression = (context, expression, visited = []) => {
|
|
|
596
598
|
if (Array.isReadonlyArrayEmpty(expression.arguments)) {
|
|
597
599
|
return layerExpression(context, expression.callee.object, next);
|
|
598
600
|
}
|
|
601
|
+
// Completed Match.value pipelines return their branch values, including
|
|
602
|
+
// Layers. Inline native callbacks remain allowed; an owned factory does not.
|
|
603
|
+
const completesMatch = Array.last(expression.arguments).pipe(Option.exists((argument) => scopedEffectReference(context, argument.type === "CallExpression" ? argument.callee : argument).pipe(Option.exists((reference) => isExport(reference, "effect/Match", "exhaustive") ||
|
|
604
|
+
isExport(reference, "effect/Match", "orElse")))));
|
|
605
|
+
const valueMatch = expression.callee.object.type === "CallExpression" &&
|
|
606
|
+
scopedEffectReference(context, expression.callee.object.callee).pipe(Option.exists((reference) => isExport(reference, "effect/Match", "value")));
|
|
607
|
+
const branchReturnsLayer = (argument) => argument.type === "CallExpression" &&
|
|
608
|
+
scopedEffectReference(context, argument.callee).pipe(Option.exists((reference) => reference.module === "effect/Match")) &&
|
|
609
|
+
Array.some((callback) => (callback.type === "ArrowFunctionExpression" || callback.type === "FunctionExpression") &&
|
|
610
|
+
Option.fromNullishOr(callback.body).pipe(Option.exists((body) => body.type === "BlockStatement"
|
|
611
|
+
? returnsLayer(context, body)
|
|
612
|
+
: layerExpression(context, body, next))))(argument.arguments);
|
|
613
|
+
if (valueMatch &&
|
|
614
|
+
completesMatch &&
|
|
615
|
+
Array.some(branchReturnsLayer)(expression.arguments)) {
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
599
618
|
return transforms(expression.arguments);
|
|
600
619
|
}
|
|
601
620
|
return false;
|
|
@@ -1493,15 +1512,15 @@ const relativeModule = (filename, address) => {
|
|
|
1493
1512
|
return address;
|
|
1494
1513
|
}
|
|
1495
1514
|
const directory = filename.replaceAll("\\", "/").replace(/\/[^/]+$/u, "");
|
|
1496
|
-
return Function.pipe(`${directory}/${address}`.split("/"), Array.reduce(
|
|
1515
|
+
return Function.pipe(`${directory}/${address}`.split("/"), Array.reduce(Chunk.empty(), (parts, part) => {
|
|
1497
1516
|
if (part === "." || part === "") {
|
|
1498
1517
|
return parts;
|
|
1499
1518
|
}
|
|
1500
1519
|
if (part === "..") {
|
|
1501
|
-
return
|
|
1520
|
+
return Chunk.dropRight(parts, 1);
|
|
1502
1521
|
}
|
|
1503
|
-
return
|
|
1504
|
-
}),
|
|
1522
|
+
return Chunk.append(parts, part);
|
|
1523
|
+
}), Chunk.join("/"));
|
|
1505
1524
|
};
|
|
1506
1525
|
const CheckedPageOptions = Schema.Struct({
|
|
1507
1526
|
checkedHelper: Schema.Struct({ module: Schema.String, export: Schema.String }),
|
|
@@ -2543,6 +2562,7 @@ export default eslintCompatPlugin({
|
|
|
2543
2562
|
"no-native-work": noNativeWork,
|
|
2544
2563
|
"no-runtime-type-assertions": noRuntimeTypeAssertions,
|
|
2545
2564
|
"no-imperative-collections": noImperativeCollections,
|
|
2565
|
+
"no-copying-accumulator": noCopyingAccumulator,
|
|
2546
2566
|
"public-host-literals": publicHostLiterals,
|
|
2547
2567
|
"route-literal-ownership": routeLiteralOwnership,
|
|
2548
2568
|
"http-client-ownership": httpClientOwnership,
|
package/dist/style.d.ts
ADDED
package/dist/style.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Array, Record } from "effect";
|
|
2
|
+
import core from "ultracite/oxlint/core";
|
|
3
|
+
import react from "ultracite/oxlint/react";
|
|
4
|
+
import vitest from "ultracite/oxlint/vitest";
|
|
5
|
+
/** Optional shared style checks. Extend before strict and the project policy. */
|
|
6
|
+
const style = {
|
|
7
|
+
extends: [
|
|
8
|
+
core,
|
|
9
|
+
{
|
|
10
|
+
...react,
|
|
11
|
+
// Leave these correctness rules to the host category settings. Explicit
|
|
12
|
+
// upstream "off" values would beat strict's correctness category.
|
|
13
|
+
rules: Record.filter(react.rules ?? {}, (_, name) => name !== "jsx-a11y/no-autofocus" && name !== "react/forward-ref-uses-ref"),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
...vitest,
|
|
17
|
+
// Keep the host's correctness check for test titles in test files too.
|
|
18
|
+
overrides: Array.map(vitest.overrides ?? [], (override) => ({
|
|
19
|
+
...override,
|
|
20
|
+
rules: Record.filter(override.rules ?? {}, (_, name) => name !== "vitest/valid-title"),
|
|
21
|
+
})),
|
|
22
|
+
},
|
|
23
|
+
],
|
|
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",
|
|
45
|
+
"unicorn/no-array-for-each": "off",
|
|
46
|
+
"unicorn/no-array-sort": "off",
|
|
47
|
+
"unicorn/throw-new-error": "off",
|
|
48
|
+
"no-redeclare": "off",
|
|
49
|
+
"react/function-component-definition": "off",
|
|
50
|
+
"sort-keys": "off",
|
|
51
|
+
"func-names": "off",
|
|
52
|
+
"func-style": "off",
|
|
53
|
+
"no-use-before-define": "off",
|
|
54
|
+
"no-inline-comments": "off",
|
|
55
|
+
"typescript/consistent-return": "off",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
export default style;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonplaces/oxlint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Shared Oxlint rules for strict Effect TypeScript, Alchemy and repository code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Cooper Corbett",
|
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
"./anti-slop-effect": {
|
|
57
57
|
"types": "./dist/anti-slop/effect/index.d.ts",
|
|
58
58
|
"default": "./dist/anti-slop/effect/index.js"
|
|
59
|
+
},
|
|
60
|
+
"./style": {
|
|
61
|
+
"types": "./dist/style.d.ts",
|
|
62
|
+
"default": "./dist/style.js"
|
|
59
63
|
}
|
|
60
64
|
},
|
|
61
65
|
"publishConfig": {
|
|
@@ -68,18 +72,21 @@
|
|
|
68
72
|
"prepack": "bun run build"
|
|
69
73
|
},
|
|
70
74
|
"dependencies": {
|
|
71
|
-
"@
|
|
72
|
-
"
|
|
75
|
+
"@effect/tsgo": "0.55.0",
|
|
76
|
+
"@oxlint/plugins": "1.89.0",
|
|
77
|
+
"effect": "4.0.5",
|
|
78
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
79
|
+
"ultracite": "8.0.1"
|
|
73
80
|
},
|
|
74
81
|
"devDependencies": {
|
|
75
|
-
"@effect/platform-node": "4.0.
|
|
76
|
-
"@effect/vitest": "4.0.
|
|
77
|
-
"@types/bun": "1.4.
|
|
82
|
+
"@effect/platform-node": "4.0.5",
|
|
83
|
+
"@effect/vitest": "4.0.5",
|
|
84
|
+
"@types/bun": "1.4.3",
|
|
78
85
|
"typescript": "7.0.2",
|
|
79
|
-
"vitest": "5.0.
|
|
86
|
+
"vitest": "5.0.5"
|
|
80
87
|
},
|
|
81
88
|
"peerDependencies": {
|
|
82
|
-
"oxlint": "1.
|
|
89
|
+
"oxlint": "1.87.0 || 1.88.0 || 1.89.0"
|
|
83
90
|
},
|
|
84
91
|
"engines": {
|
|
85
92
|
"node": ">=22.14.0"
|