@commonplaces/oxlint-plugin 0.6.1 → 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 +25 -6
- package/dist/alchemy.js +2 -1
- package/dist/conventions/contracts.js +1 -1
- package/dist/http-client-error-classification.d.ts +1 -0
- package/dist/http-client-error-classification.js +93 -0
- package/dist/index.js +1 -0
- package/dist/service-instrumentation.js +1 -1
- package/dist/strict-effect.js +2 -0
- package/package.json +1 -1
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
|
|
@@ -320,7 +320,9 @@ accepted options; no marketplace plugin is needed to read or run them.
|
|
|
320
320
|
The portable Effect checks use the existing `strict-effect/` names, and the
|
|
321
321
|
Alchemy checks use `alchemy-secrets/`. Keep those names when moving from copied
|
|
322
322
|
rule files. Remove the copied implementations and change `jsPlugins` to the
|
|
323
|
-
package subpaths; keep your existing rule settings.
|
|
323
|
+
package subpaths; keep your existing rule settings. The native-source rule
|
|
324
|
+
recognises both `alchemy/Doppler` and the Alchemy beta.84 provider package
|
|
325
|
+
`@alchemy.run/doppler`.
|
|
324
326
|
|
|
325
327
|
## Immutable accumulation
|
|
326
328
|
|
|
@@ -431,6 +433,23 @@ are bounded directory prefixes; this is not a TypeScript, bundler, conditional
|
|
|
431
433
|
export or symlink resolver. Same-owner asset queries are allowed; `file:` imports
|
|
432
434
|
are unsupported and rejected. Invalid or contradictory options fail closed.
|
|
433
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
|
+
|
|
434
453
|
### Subprocess pipe capture
|
|
435
454
|
|
|
436
455
|
The `effect` group enables `strict-effect/subprocess-pipe-capture`. It follows
|
package/dist/alchemy.js
CHANGED
|
@@ -22,7 +22,8 @@ const nativeSources = defineRule({
|
|
|
22
22
|
create(context) {
|
|
23
23
|
return {
|
|
24
24
|
ArrayExpression(node) {
|
|
25
|
-
const doppler = (element) => Option.isSome(providerCall(context, element, "alchemy/Doppler", "Secrets"))
|
|
25
|
+
const doppler = (element) => Option.isSome(providerCall(context, element, "alchemy/Doppler", "Secrets")) ||
|
|
26
|
+
Option.isSome(providerCall(context, element, "@alchemy.run/doppler", "Secrets"));
|
|
26
27
|
if (!Array.some(node.elements, (element) => element !== null && doppler(element)))
|
|
27
28
|
return;
|
|
28
29
|
const valid = node.elements.length === 3 &&
|
|
@@ -65,7 +65,7 @@ export const contractRules = {
|
|
|
65
65
|
"no-host-api-in-contract": rule("Keep service, Schema, error and config contracts portable; acquire host APIs in private adapters.", (context) => ({
|
|
66
66
|
ImportDeclaration(node) {
|
|
67
67
|
if (/(?:^|\/)(?:services?|schemas?|errors|config)\.ts$/u.test(context.filename) &&
|
|
68
|
-
/^(?:node:|bun$|playwright$|lighthouse$|chrome-launcher$|alchemy(?:\/|$)|@cloudflare(?:\/|$)|cloudflare$|@octokit(?:\/|$)|@actions(?:\/|$)|@sentry(?:\/|$)|stripe$|resend$|@aws(?:\/|$)|aws-sdk$)/u.test(node.source.value))
|
|
68
|
+
/^(?:node:|bun$|playwright$|lighthouse$|chrome-launcher$|alchemy(?:\/|$)|@alchemy\.run\/|@cloudflare(?:\/|$)|cloudflare$|@octokit(?:\/|$)|@actions(?:\/|$)|@sentry(?:\/|$)|stripe$|resend$|@aws(?:\/|$)|aws-sdk$)/u.test(node.source.value))
|
|
69
69
|
report(context, node.source);
|
|
70
70
|
},
|
|
71
71
|
})),
|
|
@@ -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",
|
|
@@ -42,7 +42,7 @@ const serviceOf = (context, node, visited = []) => {
|
|
|
42
42
|
if (Array.some(visited, (previous) => previous === node))
|
|
43
43
|
return Option.none();
|
|
44
44
|
const next = [...visited, node];
|
|
45
|
-
const imported = referenceOf(context, node).pipe(Option.filter((reference) => !/^(?:effect|alchemy)(?:\/|$)/u.test(reference.module)), Option.flatMap((reference) => reference.name), Option.filter((name) => /^[A-Z]/u.test(name)), Option.map((name) => ({ name, declaration: Option.none() })));
|
|
45
|
+
const imported = referenceOf(context, node).pipe(Option.filter((reference) => !/^(?:(?:effect|alchemy)(?:\/|$)|@alchemy\.run\/)/u.test(reference.module)), Option.flatMap((reference) => reference.name), Option.filter((name) => /^[A-Z]/u.test(name)), Option.map((name) => ({ name, declaration: Option.none() })));
|
|
46
46
|
if (Option.isSome(imported))
|
|
47
47
|
return imported;
|
|
48
48
|
if (node.type !== "Identifier")
|
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,
|