@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
- 107 custom Oxlint rules for strict Effect TypeScript, Alchemy and general code
4
- checks. Version 0.6.1 supports **Oxlint 1.87.0, 1.88.0 or 1.89.0** and **Node 22.14 or newer**.
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.6.1 and save its exact version:
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.6.1 \
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.6.1 supports exactly Oxlint 1.87.0, 1.88.0 or 1.89.0. Older releases
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",
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonplaces/oxlint-plugin",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Shared Oxlint rules for strict Effect TypeScript, Alchemy and repository code.",
5
5
  "license": "MIT",
6
6
  "author": "Cooper Corbett",