@commonplaces/oxlint-plugin 0.6.2 → 0.7.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @commonplaces/oxlint-plugin
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
checks. Version 0.
|
|
3
|
+
108 custom Oxlint rules for strict Effect TypeScript, Alchemy and general code
|
|
4
|
+
checks. Version 0.7.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.
|
|
@@ -12,14 +12,14 @@ Effect setup below after dependency installation.
|
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
|
-
Install version 0.
|
|
15
|
+
Install version 0.7.0 and save its exact version:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npm install --save-dev --save-exact @commonplaces/oxlint-plugin@0.
|
|
18
|
+
npm install --save-dev --save-exact @commonplaces/oxlint-plugin@0.7.0 \
|
|
19
19
|
@effect/tsgo@0.55.0 oxlint@1.89.0 oxlint-tsgolint@7.0.2004 typescript@7.0.2
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Version 0.
|
|
22
|
+
Version 0.7.0 supports exactly Oxlint 1.87.0, 1.88.0 or 1.89.0. Older releases
|
|
23
23
|
keep their earlier host lists. Consumer adoption is a separate step.
|
|
24
24
|
|
|
25
25
|
The manifest and lockfile retain the resolved version. Historical GitHub release
|
|
@@ -433,6 +433,23 @@ are bounded directory prefixes; this is not a TypeScript, bundler, conditional
|
|
|
433
433
|
export or symlink resolver. Same-owner asset queries are allowed; `file:` imports
|
|
434
434
|
are unsupported and rejected. Invalid or contradictory options fail closed.
|
|
435
435
|
|
|
436
|
+
### HTTP response-body error classification
|
|
437
|
+
|
|
438
|
+
The `effect` and `strict` groups enable `strict-effect/http-client-error-classification`.
|
|
439
|
+
Effect's HTTP `DecodeError` includes connection failures while reading a response
|
|
440
|
+
body. Do not map every `DecodeError` to a payload/integrity failure while mapping
|
|
441
|
+
`TransportError` to unavailability. Use the transport recovery path by default;
|
|
442
|
+
distinguish deliberate size limits or other payload failures with a tagged cause.
|
|
443
|
+
Keep malformed JSON/schema errors separate and test failure after response headers.
|
|
444
|
+
|
|
445
|
+
The rule checks native `Match.tags`/`tagsExhaustive` tables containing
|
|
446
|
+
`DecodeError`, `TransportError` and `StatusCodeError`. It rejects unconditional
|
|
447
|
+
results using different error constructors, following immutable local aliases,
|
|
448
|
+
simple return helpers and `Effect.fail`/`succeed`. Imported aliases and shadowing
|
|
449
|
+
are respected. It does not prove conditional branches, opaque helpers, computed
|
|
450
|
+
tables or other dispatch styles: behavioural tests still own their semantics.
|
|
451
|
+
There are no project-specific names, configuration options or automatic fixes.
|
|
452
|
+
|
|
436
453
|
### Subprocess pipe capture
|
|
437
454
|
|
|
438
455
|
The `effect` group enables `strict-effect/subprocess-pipe-capture`. It follows
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const httpClientErrorClassification: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { Array, Option } from "effect";
|
|
3
|
+
import { constant, operation, referenceOf, valueOf } from "./native-reference.js";
|
|
4
|
+
import { propertyName, variableInScope } from "./syntax.js";
|
|
5
|
+
// Only prove unconditional constructor results. Conditional cause checks and
|
|
6
|
+
// opaque helpers need behavioural tests; this rule does not infer their meaning.
|
|
7
|
+
const constructedError = (context, input, visited = []) => {
|
|
8
|
+
if (Array.some(visited, (previous) => previous === input))
|
|
9
|
+
return Option.none();
|
|
10
|
+
const next = [...visited, input];
|
|
11
|
+
return valueOf(context, input).pipe(Option.flatMap((node) => {
|
|
12
|
+
if (node.type === "NewExpression")
|
|
13
|
+
return Option.some(node.callee);
|
|
14
|
+
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
|
|
15
|
+
const body = node.body;
|
|
16
|
+
if (!body)
|
|
17
|
+
return Option.none();
|
|
18
|
+
if (body.type !== "BlockStatement")
|
|
19
|
+
return constructedError(context, body, next);
|
|
20
|
+
return Array.head(body.body).pipe(Option.filter(() => body.body.length === 1), Option.filter((statement) => statement.type === "ReturnStatement"), Option.flatMap((statement) => Option.fromNullishOr(statement.argument)), Option.flatMap((result) => constructedError(context, result, next)));
|
|
21
|
+
}
|
|
22
|
+
if (node.type !== "CallExpression")
|
|
23
|
+
return Option.none();
|
|
24
|
+
if (operation(context, node.callee, "effect/Effect", "fail") ||
|
|
25
|
+
operation(context, node.callee, "effect/Effect", "succeed"))
|
|
26
|
+
return Array.head(node.arguments).pipe(Option.flatMap((argument) => constructedError(context, argument, next)));
|
|
27
|
+
return constructedError(context, node.callee, next);
|
|
28
|
+
}));
|
|
29
|
+
};
|
|
30
|
+
const localClass = (context, node, visited = []) => {
|
|
31
|
+
if (Array.some(visited, (previous) => previous === node))
|
|
32
|
+
return Option.none();
|
|
33
|
+
if (node.type === "ClassExpression")
|
|
34
|
+
return Option.some(node);
|
|
35
|
+
if (node.type !== "Identifier")
|
|
36
|
+
return Option.none();
|
|
37
|
+
return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.filter((variable) => Array.every(variable.references, (reference) => !reference.isWrite() || reference.init)), Option.flatMap((variable) => constant(variable).pipe(Option.flatMap((declaration) => Option.fromNullishOr(declaration.init)), Option.flatMap((initialiser) => localClass(context, initialiser, [...visited, node])), Option.orElse(() => Array.head(variable.defs).pipe(Option.filter(() => variable.defs.length === 1), Option.map((definition) => definition.node), Option.filter((definition) => definition.type === "ClassDeclaration"))))));
|
|
38
|
+
};
|
|
39
|
+
const differentConstructors = (context, left, right) => {
|
|
40
|
+
const imported = Option.all([referenceOf(context, left), referenceOf(context, right)]);
|
|
41
|
+
if (Option.isSome(imported)) {
|
|
42
|
+
const [a, b] = imported.value;
|
|
43
|
+
return (a.module !== b.module || !Option.makeEquivalence((x, y) => x === y)(a.name, b.name));
|
|
44
|
+
}
|
|
45
|
+
return Option.exists(Option.all([localClass(context, left), localClass(context, right)]), ([a, b]) => a !== b);
|
|
46
|
+
};
|
|
47
|
+
const handler = (table, name) => Array.findLast(table.properties, (property) => property.type === "Property" && Option.contains(propertyName(property.key), name)).pipe(Option.flatMap((property) => property.type === "Property" ? Option.some(property.value) : Option.none()));
|
|
48
|
+
export const httpClientErrorClassification = defineRule({
|
|
49
|
+
meta: {
|
|
50
|
+
type: "problem",
|
|
51
|
+
schema: [],
|
|
52
|
+
messages: {
|
|
53
|
+
classification: "HTTP DecodeError also covers failed response-body reads. Use the TransportError recovery path unless you inspect a separately tagged payload failure; do not classify every DecodeError as a different domain error.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
create(context) {
|
|
57
|
+
return {
|
|
58
|
+
CallExpression(node) {
|
|
59
|
+
if (!operation(context, node.callee, "effect/Match", "tags") &&
|
|
60
|
+
!operation(context, node.callee, "effect/Match", "tagsExhaustive"))
|
|
61
|
+
return;
|
|
62
|
+
const table = Array.head(node.arguments).pipe(Option.flatMap((argument) => valueOf(context, argument)), Option.filter((value) => value.type === "ObjectExpression"),
|
|
63
|
+
// Do not guess which handler an unresolved spread/computed key replaces.
|
|
64
|
+
Option.filter((value) => Array.every(value.properties, (property) => property.type === "Property" &&
|
|
65
|
+
(!property.computed || property.key.type === "Literal"))));
|
|
66
|
+
Option.match(table, {
|
|
67
|
+
onNone: () => { },
|
|
68
|
+
onSome: (table) => {
|
|
69
|
+
// This distinctive native reason table avoids domain DecodeError
|
|
70
|
+
// unions and does not depend on project-specific error names.
|
|
71
|
+
if (Option.isNone(handler(table, "StatusCodeError")))
|
|
72
|
+
return;
|
|
73
|
+
const handlers = Option.all([
|
|
74
|
+
handler(table, "DecodeError"),
|
|
75
|
+
handler(table, "TransportError"),
|
|
76
|
+
]);
|
|
77
|
+
Option.match(handlers, {
|
|
78
|
+
onNone: () => { },
|
|
79
|
+
onSome: ([decode, transport]) => {
|
|
80
|
+
const errors = Option.all([
|
|
81
|
+
constructedError(context, decode),
|
|
82
|
+
constructedError(context, transport),
|
|
83
|
+
]);
|
|
84
|
+
if (Option.exists(errors, ([left, right]) => differentConstructors(context, left, right)))
|
|
85
|
+
context.report({ node: decode, messageId: "classification" });
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ export const effect = {
|
|
|
44
44
|
"effect/require-service-operation-instrumentation": "error",
|
|
45
45
|
"strict-effect/static-layers": "error",
|
|
46
46
|
"strict-effect/subprocess-pipe-capture": "error",
|
|
47
|
+
"strict-effect/http-client-error-classification": "error",
|
|
47
48
|
"strict-effect/no-runtime-type-assertions": "error",
|
|
48
49
|
"strict-effect/no-unchecked-index": "error",
|
|
49
50
|
"strict-effect/no-native-at": "error",
|
package/dist/strict-effect.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { subprocessPipeCapture } from "./subprocess-pipe-capture.js";
|
|
2
|
+
import { httpClientErrorClassification } from "./http-client-error-classification.js";
|
|
2
3
|
import { noCopyingAccumulator } from "./accumulation.js";
|
|
3
4
|
import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
|
|
4
5
|
import * as Array from "effect/Array";
|
|
@@ -2549,6 +2550,7 @@ export default eslintCompatPlugin({
|
|
|
2549
2550
|
"website-rpc-boundary": websiteRpcBoundary,
|
|
2550
2551
|
"static-layers": staticLayers,
|
|
2551
2552
|
"subprocess-pipe-capture": subprocessPipeCapture,
|
|
2553
|
+
"http-client-error-classification": httpClientErrorClassification,
|
|
2552
2554
|
"no-unchecked-index": noUncheckedIndex,
|
|
2553
2555
|
"no-native-at": noNativeAt,
|
|
2554
2556
|
"runtime-file-convention": runtimeFileConvention,
|