@commonplaces/oxlint-plugin 0.5.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 +113 -14
- 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 +3 -54
- package/dist/index.js +4 -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 +7 -4
- package/dist/style.js +20 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
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
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
|
+
|
|
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.
|
|
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@0.
|
|
18
|
+
npm install --save-dev --save-exact @commonplaces/oxlint-plugin@0.6.0 oxlint@1.89.0
|
|
15
19
|
```
|
|
16
20
|
|
|
17
|
-
Version 0.
|
|
18
|
-
|
|
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.
|
|
19
23
|
|
|
20
24
|
The manifest and lockfile retain the resolved version. Historical GitHub release
|
|
21
25
|
archives remain separate distribution artefacts; a GitHub release does not publish
|
|
@@ -59,6 +63,39 @@ the subpaths below. For example, a JSON configuration can enable one rule:
|
|
|
59
63
|
}
|
|
60
64
|
```
|
|
61
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
|
+
|
|
62
99
|
## Optional style preset
|
|
63
100
|
|
|
64
101
|
Add `style` before `strict`, then keep the project's own policy last:
|
|
@@ -74,8 +111,7 @@ export default defineConfig({ extends: [style, strict, project] });
|
|
|
74
111
|
Run `oxlint --type-aware --deny-warnings .`. `style` also works alone and is
|
|
75
112
|
available as the default export of `@commonplaces/oxlint-plugin/style`.
|
|
76
113
|
It never imports or extends `strict`; `strict` still contains only its original
|
|
77
|
-
six groups. Adding
|
|
78
|
-
unchanged. This preset includes more than formatting preferences: Ultracite
|
|
114
|
+
six groups. Adding `style` leaves strict settings unchanged. This preset includes more than formatting preferences: Ultracite
|
|
79
115
|
also supplies accessibility, test and correctness checks, browser globals,
|
|
80
116
|
shared ignores and test-file overrides. Its ignores can reduce which files are
|
|
81
117
|
checked; keep the project's own file policy explicit.
|
|
@@ -107,6 +143,37 @@ size limits remain in each consumer's project policy.
|
|
|
107
143
|
| `no-inline-comments` | Allow short explanations beside the code they describe. |
|
|
108
144
|
| `typescript/consistent-return` | Leave return types and Effect generator branches to TypeScript. |
|
|
109
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
|
+
|
|
110
177
|
### Supported-host adjustments
|
|
111
178
|
|
|
112
179
|
The imported configurations need two small adjustments; these are separate
|
|
@@ -120,10 +187,14 @@ from the eleven shared overrides above:
|
|
|
120
187
|
- Vitest's file override disabling `vitest/valid-title` is also omitted, so
|
|
121
188
|
strict's correctness setting still applies inside test files.
|
|
122
189
|
|
|
123
|
-
The tests install the packed archive on
|
|
190
|
+
The tests install the packed archive on every supported host and compare
|
|
124
191
|
every resolved strict rule, including its options, with `[style, strict]`.
|
|
125
|
-
They also check inherited file overrides.
|
|
126
|
-
|
|
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.
|
|
127
198
|
|
|
128
199
|
### Ultracite 8.0.1 comparison
|
|
129
200
|
|
|
@@ -144,7 +215,8 @@ Both versions contain `no-unmodified-loop-condition` with
|
|
|
144
215
|
`checkConditionalExpressions: true`; the supported hosts accept that option.
|
|
145
216
|
Ultracite 8 requires Oxlint 1.86 or newer, so 1.80 and 1.85 are no longer
|
|
146
217
|
supported. Its Oxfmt peer requires at least 0.71; repository tooling uses 0.74.0.
|
|
147
|
-
The archive is checked on exactly 1.
|
|
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.
|
|
148
220
|
|
|
149
221
|
## Project settings
|
|
150
222
|
|
|
@@ -224,11 +296,38 @@ Alchemy checks use `alchemy-secrets/`. Keep those names when moving from copied
|
|
|
224
296
|
rule files. Remove the copied implementations and change `jsPlugins` to the
|
|
225
297
|
package subpaths; keep your existing rule settings.
|
|
226
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
|
+
|
|
227
326
|
## Rule exports
|
|
228
327
|
|
|
229
328
|
| Package subpath | Rule prefix | Rules |
|
|
230
329
|
| ------------------- | --------------------- | ----: |
|
|
231
|
-
| `/strict-effect` | `strict-effect/` |
|
|
330
|
+
| `/strict-effect` | `strict-effect/` | 30 |
|
|
232
331
|
| `/effect` | `effect/` | 9 |
|
|
233
332
|
| `/alchemy` | `alchemy-secrets/` | 3 |
|
|
234
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">;
|
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) =>
|
|
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";
|
|
@@ -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(
|
|
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
|
|
1520
|
+
return Chunk.dropRight(parts, 1);
|
|
1519
1521
|
}
|
|
1520
|
-
return
|
|
1521
|
-
}),
|
|
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.
|
|
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",
|
|
@@ -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.
|
|
89
|
+
"oxlint": "1.87.0 || 1.88.0 || 1.89.0"
|
|
88
90
|
},
|
|
89
91
|
"engines": {
|
|
90
92
|
"node": ">=22.14.0"
|