@onrails/biome-plugin 0.1.3 → 0.2.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
@@ -18,7 +18,8 @@ Reference the rule files in `biome.json` plugins:
18
18
  "./node_modules/@onrails/biome-plugin/rules/no-promise-result.grit",
19
19
  "./node_modules/@onrails/biome-plugin/rules/no-unsafe-unwrap.grit",
20
20
  "./node_modules/@onrails/biome-plugin/rules/no-deprecated-synonyms.grit",
21
- "./node_modules/@onrails/biome-plugin/rules/fluent-stays-local.grit"
21
+ "./node_modules/@onrails/biome-plugin/rules/fluent-stays-local.grit",
22
+ "./node_modules/@onrails/biome-plugin/rules/carrier-sinks-need-from.grit"
22
23
  ]
23
24
  }
24
25
  ```
@@ -29,8 +30,19 @@ Reference the rule files in `biome.json` plugins:
29
30
  |------|------------------|
30
31
  | `no-promise-result` | `Promise<Result<T, E>>` in any type position — return `ResultAsync<T, E>` and use `fromAsync` / `tryAsync` at the boundary (error) |
31
32
  | `no-unsafe-unwrap` | `._unsafeUnwrap()` / `._unsafeUnwrapErr()` calls — use `match` / `resolve` / `yieldResult` instead (error) |
32
- | `no-deprecated-synonyms` | Deprecated synonyms (`chain`, `fold`, `getOrElse`, `of`, …) — use the canonical names (error) |
33
+ | `no-deprecated-synonyms` | Deprecated synonyms (`chain`, `fold`, `getOrElse`, `of`, …) — use the canonical names (error, partly fixable) |
33
34
  | `fluent-stays-local` | Bare `FluentResult<…>` / `FluentMaybe<…>` type references — broader net than the ESLint rule, see `ENGINE_DIVERGENCES` (error) |
35
+ | `carrier-sinks-need-from` | `JSON.parse` / `structuredClone` / `.json()` output cast to `Result<…>` / `Maybe<…>` without `Result.from` / `Maybe.from`; cast form only, see `ENGINE_DIVERGENCES` (error) |
36
+
37
+ ## Fixes
38
+
39
+ `no-deprecated-synonyms` rewrites. `.chain()` → `.flatMap()` is a **safe** fix, applied by `biome check --write`. The free-function renames (`matchResult`/`matchMaybe` → `match`, `getOrElse` → `unwrapOr`, `collect` → `combine`, `sequenceTupleAsync` → `ResultAsync.combineTuple`) are **unsafe** — the canonical name still has to be imported, which GritQL cannot do — so they need `biome check --write --unsafe` plus an import fix, or `@onrails/codemod`, which rewrites the imports too. `fold`, `of`, and `ResultAsync.isOk()` / `.isErr()` have no mechanical rewrite and stay diagnostic-only.
40
+
41
+ ## Scoping
42
+
43
+ `no-deprecated-synonyms` only fires in files that mention an `@onrails/*` module source, so RxJS `of` or fp-ts `fold` in unrelated files are left alone. Files that reach onrails through a local alias or re-export are not matched — use the ESLint plugin, which matches by name everywhere. Declared as `no-deprecated-synonyms` / `import-scoped` in `ENGINE_DIVERGENCES`.
44
+
45
+ Requires Biome >= 2.5.12 for plugin rewrites and `fix_kind`.
34
46
 
35
47
  ## Tests
36
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onrails/biome-plugin",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Biome GritQL plugin: boundary diagnostics for @onrails/result (Promise<Result<…>>, _unsafeUnwrap*).",
6
6
  "license": "MIT",
@@ -40,7 +40,8 @@
40
40
  "./rules/no-promise-result.grit": "./rules/no-promise-result.grit",
41
41
  "./rules/no-unsafe-unwrap.grit": "./rules/no-unsafe-unwrap.grit",
42
42
  "./rules/no-deprecated-synonyms.grit": "./rules/no-deprecated-synonyms.grit",
43
- "./rules/fluent-stays-local.grit": "./rules/fluent-stays-local.grit"
43
+ "./rules/fluent-stays-local.grit": "./rules/fluent-stays-local.grit",
44
+ "./rules/carrier-sinks-need-from.grit": "./rules/carrier-sinks-need-from.grit"
44
45
  },
45
46
  "scripts": {
46
47
  "typecheck": "echo 'noop (grit files only)'",
@@ -48,9 +49,9 @@
48
49
  "check": "bun test"
49
50
  },
50
51
  "peerDependencies": {
51
- "@biomejs/biome": ">=2.0.0"
52
+ "@biomejs/biome": ">=2.5.12"
52
53
  },
53
54
  "devDependencies": {
54
- "@biomejs/biome": "^2.4.16"
55
+ "@biomejs/biome": "^2.5.12"
55
56
  }
56
57
  }
@@ -0,0 +1,36 @@
1
+ // @onrails/biome-plugin: carrier-sinks-need-from
2
+ //
3
+ // Flag `JSON.parse(…)`, `structuredClone(…)` and `….json()` output cast to
4
+ // `Result<…>` / `Maybe<…>`. Plain data from those sinks has no methods once
5
+ // carriers keep them on a prototype, so the cast lies. Lift with
6
+ // `Result.from` / `Maybe.from` instead.
7
+ //
8
+ // Limitation: matches the direct `as` cast form only. The ESLint rule also
9
+ // follows annotated declarators, carrier-typed returns and parameter defaults,
10
+ // `const` bindings of a sink result, and `res.json<Result<…>>()`; GritQL cannot
11
+ // relate a declarator, return type or binding to its use. Declared in ENGINE_DIVERGENCES
12
+ // (`carrier-sinks-need-from` / `cast-form-only`).
13
+
14
+ engine biome(1.0)
15
+ language js
16
+
17
+ or {
18
+ `$sink as Result<$a, $b>`,
19
+ `$sink as Maybe<$a>`
20
+ } as $t where {
21
+ // the sink call itself (optionally awaited / parenthesized), not any call nested inside it:
22
+ // `Result.from(JSON.parse(raw)) as Result<T, E>` is lifted and must stay quiet
23
+ $sink <: or {
24
+ `JSON.parse($_)`,
25
+ `structuredClone($_)`,
26
+ `$_.json()`,
27
+ `await $_.json()`,
28
+ `($_.json())`,
29
+ `(await $_.json())`
30
+ },
31
+ register_diagnostic(
32
+ span = $t,
33
+ message = "Data from JSON.parse/structuredClone/.json() is typed as Result/Maybe but carries no methods — lift it with Result.from / Maybe.from.",
34
+ severity = "error"
35
+ )
36
+ }
@@ -9,32 +9,45 @@
9
9
  // - collect() -> combine()
10
10
  // - ResultAsync.isOk() / isErr() -> await and narrow with isOk() / isErr()
11
11
  //
12
- // Limitation: matching is name-based — GritQL cannot scope by import, so
13
- // same-named calls from other libraries (RxJS `of`, fp-ts `fold`,
14
- // neverthrow `.isOk()`) are flagged too. Suppress with an inline
15
- // biome-ignore comment in mixed codebases.
12
+ // Scoping: only files that mention an `@onrails/*` module source are matched,
13
+ // so same-named calls from other libraries (RxJS `of`, fp-ts `fold`) are left
14
+ // alone. The ESLint rule is name-based everywhere; declared in
15
+ // ENGINE_DIVERGENCES (`no-deprecated-synonyms` / `import-scoped`).
16
+ //
17
+ // Fixes: `.chain()` -> `.flatMap()` is a safe rewrite (method rename, no
18
+ // import churn). The free-function renames are unsafe fixes — they need the
19
+ // canonical name imported, which GritQL cannot add; run `--write --unsafe`
20
+ // and fix the import, or use `@onrails/codemod`, which rewrites both.
16
21
 
17
22
  engine biome(1.0)
18
- language js
23
+ language js(typescript)
19
24
 
20
25
  or {
21
- `$_.chain($_)` as $call,
22
- `$_?.chain($_)` as $call,
23
- `$_.isOk()` as $call,
24
- `$_?.isOk()` as $call,
25
- `$_.isErr()` as $call,
26
- `$_?.isErr()` as $call,
27
- `fold($_)` as $call,
28
- `matchResult($_)` as $call,
29
- `matchMaybe($_)` as $call,
30
- `getOrElse($_)` as $call,
31
- `sequenceTupleAsync($_)` as $call,
32
- `collect($_)` as $call,
33
- `of($_)` as $call
34
- } where {
26
+ `$x.chain($args)` => `$x.flatMap($args)`,
27
+ `$x?.chain($args)` => `$x?.flatMap($args)`,
28
+ `matchResult($args)` => `match($args)`,
29
+ `matchMaybe($args)` => `match($args)`,
30
+ `getOrElse($args)` => `unwrapOr($args)`,
31
+ `collect($args)` => `combine($args)`,
32
+ `sequenceTupleAsync($args)` => `ResultAsync.combineTuple($args)`,
33
+ `fold($args)`,
34
+ `of($args)`,
35
+ `$x.isOk()`,
36
+ `$x?.isOk()`,
37
+ `$x.isErr()`,
38
+ `$x?.isErr()`
39
+ } as $call where {
40
+ $call <: within JsModule(items = $items),
41
+ $items <: contains JsModuleSource() as $source,
42
+ $source <: r".*@onrails/.*",
43
+ or {
44
+ { $call <: or { `$_.chain($_)`, `$_?.chain($_)` }, $kind = "safe" },
45
+ { $kind = "unsafe" }
46
+ },
35
47
  register_diagnostic(
36
48
  span = $call,
37
49
  message = "Avoid deprecated onrails synonyms/methods. Use the canonical equivalents instead.",
38
- severity = "error"
50
+ severity = "error",
51
+ fix_kind = $kind
39
52
  )
40
53
  }