の続き。eslint-plugin-neverthrow
に手を入れて動くようにしてみるぞー!そして記事を書き終わるぞー!
eslint-plugin-neverthrow に手を入れてみる
そのままでは動かなさそうだから手を入れて動くようにしてみたい。パッケージを作ればできそうではあるけど、もうちょっと簡単にできないかな?と思って探してみたら、こんな記事を見つけた。ありがたい。
srcのとなりにコードを置いて、それを使うようにできるのかー。これなら簡単に eslint-plugin-neverthrow
に手を入れられそう。この記事や、ESLintのプラグイン自作ページを見ながら試してみたら、サンプルのプラグインが動いた。この仕組みで eslint-plugin-neverthrow
に対して手を入れてみよう。
my-plugin という名前でプラグインを作る
# プラグイン用のフォルダを作る ❯ mkdir my-plugin
index.js
はこんな感じ。
import {rule} from './must-use-result.js' const plugin = { meta: { name: 'my-plugin', version: '1.0.0', }, rules: { 'must-use-result': rule }, } export default plugin
そして、must-use-result ルールのファイルをダウンロードしてくる。TypeScriptのファイルだから、tscでjsにコンパイルすればいいか。
❯ cd my-plugin ❯ curl https://raw.githubusercontent.com/mdbetancourt/eslint-plugin-neverthrow/refs/heads/master/src/rules/must-use-result.ts -O ❯ ls index.js must-use-result.ts
で、コンパイルエラーが出てるからまずはそれを解消しよう。
import部分のエラー
import部分では↓の3ヶ所がエラーになる。
(1) tsutils
がない:
import { unionTypeParts } from 'tsutils';
(2) @typescript-eslint/experimental-utils
がない:
import type { TSESLint, ParserServices, } from '@typescript-eslint/experimental-utils';
(3) ../utils
がない
import { MessageIds } from '../utils';
import部分のエラーを解消する
(1) tsutils
は追加したらよさそう。
❯ pnpm add -D tsutils
はい、消えたー。
(2) @typescript-eslint/experimental-utils
は、たぶん最新版ではもう experimental ではなくなってそう。と思って探してみたら @typescript-eslint/utils
に入ってた。
import type { TSESLint, ParserServices, -} from '@typescript-eslint/experimental-utils'; +} from '@typescript-eslint/utils';
はい、消えたー。
(3) ../utils
は↓のファイルで3行だけなのでコピペしたらよさそう。
https://github.com/mdbetancourt/eslint-plugin-neverthrow/blob/master/src/utils.ts
-import { MessageIds } from '../utils'; + +export enum MessageIds { + MUST_USE = 'mustUseResult' +}
はい、消えたー。
rule定義部分のエラーを解消する
meta情報が違うよとか、defaultOptionsがないよとか、export のやり方がダメだよとか怒られるので、それを修正。
@@ -186,14 +186,11 @@ function processSelector( return true; } -const rule: TSESLint.RuleModule<MessageIds, []> = { +export const rule: TSESLint.RuleModule<MessageIds, []> = { meta: { docs: { description: 'Not handling neverthrow result is a possible error because errors could remain unhandleds.', - recommended: 'error', - category: 'Possible Errors', - url: '', }, messages: { mustUseResult: @@ -202,6 +199,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = { schema: [], type: 'problem', }, + defaultOptions: [], create(context) { const parserServices = context.parserServices; @@ -220,5 +218,3 @@ const rule: TSESLint.RuleModule<MessageIds, []> = { }; }, }; - -export = rule;
tscでJSにトランスパイルしておく
# my-pluginにいる場合はリポジトリルートに戻っておく ❯ cd .. # tscはファイルを指定する場合はtsconfigを見てくれないので自分でオプションを指定する ❯ pnpm tsc my-plugin/must-use-result.ts --module NodeNext --moduleResolution NodeNext ❯ ls my-plugin/must-use-result.js my-plugin/must-use-result.js
これでプラグインはできたかな。まだ手をいれてないから、同じエラーになるはずなので、確認してみよう。
my-pluginを使うように設定
こういう設定にしてみた。色々調べているうちにFlat Configに少し慣れてきて、eslint-plugin-neverthrow
の旧フォーマットの設定は、新フォーマットでこう書けばいいじゃんって分かってきた。
import myPlugin from "./my-plugin/index.js"; export default [ ... { plugins: { myPlugin, }, languageOptions: { parser: tseslint.parser, parserOptions: { ecmaVersion: 2021, sourceType: 'module', project: ['./tsconfig.json'], tsconfigRootDir: __dirname, } }, rules: { 'myPlugin/must-use-result': 'error', }, }, ];
からの実行!
❯ pnpm eslint Oops! Something went wrong! :( ESLint: 9.15.0 Error: Error while loading rule 'myPlugin/must-use-result': types not available, maybe you need set the parser to @typescript-eslint/parser
やったー!ローカルでも同じエラーになったー。
手を入れる
これでやっと手を入れられる。↓この部分が取れてないから
const parserServices = context.parserServices;
↓を参考に修正してみる。
@typescript-eslint/utils
の ESLintUtils
を使って parserServices
を取得してみる。
@@ -1,9 +1,10 @@ import { TypeChecker } from 'typescript'; import { unionTypeParts } from 'tsutils'; import { TSESTree } from '@typescript-eslint/types'; -import type { - TSESLint, - ParserServices, +import { + type TSESLint, + type ParserServices, + ESLintUtils, } from '@typescript-eslint/utils'; export enum MessageIds { @@ -202,7 +203,7 @@ export const rule: TSESLint.RuleModule<MessageIds, []> = { defaultOptions: [], create(context) { - const parserServices = context.parserServices; + const parserServices = ESLintUtils.getParserServices(context); const checker = parserServices?.program?.getTypeChecker(); if (!checker || !parserServices) {
からの、実行。
❯ pnpm tsc my-plugin/must-use-result.ts --module NodeNext --moduleResolution NodeNext \ ∙ && pnpm eslint Oops! Something went wrong! :( ESLint: 9.15.0 TypeError: context.getScope is not a function
お、違うエラーになった。一歩前進!
Rules APIに変更が入ってた
新しく出てきたエラーメッセージ:
context.getScope is not a function
これは、探したらすぐに見つかった。
あぁ、ESLintのv9で設定だけじゃなくてRules APIにも変更が入ったのか。納得。
ESLint v9.0.0 introduces changes to the rules API that plugin rules use, which included moving some methods from the context object to the sourceCode object. If you’re seeing one of these errors, that means the plugin has not yet been updated to use the new rules API.
からの、compatibility utilitiesがあるのか。
Use the compatibility utilities to patch the plugin in your config file
compatibility utilitiesを使ってみる
終わりが見えてきたぞー!
# @eslint/compat を追加 ❯ pnpm install @eslint/compat -D
fixupPluginRules
を使うように eslint.config.js
を修正
@@ -7,6 +7,7 @@ import path from "path"; import { fileURLToPath } from "url"; import myPlugin from "./my-plugin/index.js"; +import { fixupPluginRules } from "@eslint/compat"; // mimic CommonJS variables -- not needed if using CommonJS const __filename = fileURLToPath(import.meta.url); @@ -49,7 +50,7 @@ export default [ { plugins: { - myPlugin, + myPlugin: fixupPluginRules(myPlugin), }, languageOptions: { parser: tseslint.parser,
からの実行!!!
❯ pnpm tsc my-plugin/must-use-result.ts --module NodeNext --moduleResolution NodeNext \ && pnpm eslint src/hello-neverthrow.ts (Redacted)/hello-result/src/hello-neverthrow.ts 10:16 error Result must be handled with either of match, unwrapOr or _unsafeUnwrap myPlugin/must-use-result ✖ 1 problem (1 error, 0 warnings)
きた〜!「neverthrowのResult型をちゃんと処理してないよ」ってエラーになった。
一旦立ち止まる
eslint-plugin-neverthrow
が動かない理由は2つあった
- ESLint v9でFlatConfigに変わったけど
eslint-plugin-neverthrow
の設定が旧形式だった- 僕がそれを新形式に変換する知識がなくて、最初はFlatCompatを使ってみた
- けど、結局色々調べてるうちに新形式の書き方が分かったからFlatCompatいらなくなった
- ESLint v9でRules APIに変更が入った
- から実行するとエラーになった
- ローカルで触っているあいだに、本当の原因のエラーメッセージがでてきてESLintのヘルプページにたどり着けた
fixupPluginRules
ユーティリティを使ったら動いた
ということは?
eslint-plugin-neverthrow
そのままで、FlatConfig形式で設定を書いて、 fixupPluginRules
でRules APIを旧形式に変換してやれば動くのでは?
やってみる
eslint-plugin-neverthrow
をインポートして、fixupPluginRules
をかけて、FlatConfigで設定を書いてあげるとこうなる。
... import { fixupPluginRules } from "@eslint/compat"; import eslintPluginNeverthrow from "eslint-plugin-neverthrow"; ... export default [ ... { plugins: { eslintPluginNeverthrow: fixupPluginRules(eslintPluginNeverthrow), }, languageOptions: { parser: tseslint.parser, parserOptions: { ecmaVersion: 2021, sourceType: 'module', project: ['./tsconfig.json'], tsconfigRootDir: __dirname, } }, rules: { 'eslintPluginNeverthrow/must-use-result': 'error', }, } ];
で、実行すると、実行エラーにならずにちゃんと怒ってくれるー!
❯ pnpm eslint src/hello-neverthrow.ts (Redacted)/hello-result/src/hello-neverthrow.ts 10:16 error Result must be handled with either of match, unwrapOr or _unsafeUnwrap eslintPluginNeverthrow/must-use-result ✖ 1 problem (1 error, 0 warnings)
やったー!!!
おしまい
NeverThrowを触ろうと思ったらESLintのFlat Configを触っていて、ずいぶん遠回りしてESLintが提供してくれている変換ツールにたどり着いてNeverThrowのESLintが動くようになった!道中で、Flat Config, FlatComat, ローカルプラグイン, fixupPluginRules を触って楽しんだ!面白かったー!
最終的なコードはここに置いといた。