Mastodonの自鯖にRBSを導入した

はじめに

この記事はMastodonRBSを導入したときの導入背景や手順などをまとめた記事になります。 主に僕個人の備忘録としてまとめています。

導入背景

2017年5月から6年ほどCreatodonというMastodonのサーバーを個人的に運営しています。

gamelinks007.net

直近で開発環境周りの体験向上としてCIの修正とかE2Eテストの追加とか色々やっていたんですが、型関係はまだ手を入れていませんでした。 また独自機能の実装やそのメンテナンスなども行っており、既存のModelServiceなどの型情報が簡単に確認できる状況が欲しくもなっていました。

そこで

  • TypeProfを使い、既存のModelServiceなどのRBSを自動生成
  • 既存のライブラリの型情報などはgem_rbs_collectionを使って導入
  • Steepで生成したRBSの型情報をVS Codeで表示&型情報のチェック

までをMastodonに導入してみました

やったこと

TypeProfを使い、既存のModelやServiceなどのRBSを自動生成

まずはMastodonにある既存のModelServiceContorollerなどの型をTypeProfを使って自動生成しました。

コードとしては以下のような型の自動生成用タスクを追加して対応しました。

# frozen_string_literal: true

require 'fileutils'

top_level = self

using(Module.new do
  refine(top_level.singleton_class) do
    def generate_type_signature(dir_name)
      files = Dir.glob("app/#{dir_name}/**/*")

      files.each do |file|
        next unless file.include?('.rb')

        result = `bundle exec typeprof #{file}`

        dir_name = File.dirname(file)

        FileUtils.mkdir_p("sig/#{dir_name}")

        File.write("sig/#{file}s", result)
      end
    end
  end
end)

namespace :type do
  namespace :prof do
    task models: :environment do
      generate_type_signature('models')
    end

    task controllers: :environment do
      generate_type_signature('controllers')
    end

    task helpers: :environment do
      generate_type_signature('helpers')
    end

    task lib: :environment do
      generate_type_signature('lib')
    end

    task presenters: :environment do
      generate_type_signature('presenters')
    end

    task serializers: :environment do
      generate_type_signature('serializers')
    end

    task services: :environment do
      generate_type_signature('services')
    end

    task validators: :environment do
      generate_type_signature('validators')
    end

    task workers: :environment do
      generate_type_signature('workers')
    end
  end

  task prof: ['prof:models', 'prof:controllers', 'prof:helpers', 'prof:lib', 'prof:presenters', 'prof:serializers', 'prof:services', 'prof:validators', 'prof:workers']
end

あとは欲しい場所(Modelならbundle exec rake type:prof:modelsのような感じで)の型情報をタスクとして実行すればOKです。

既存のライブラリの型情報などはgem_rbs_collectionを使って導入

次に既存のライブラリの型情報をgem_rbs_collectionを使って導入しました。

こちらのドキュメントを参考に以下のコマンドを実行。

rbs collection init

実行するとrbs_collection.yamlが生成されます。これを以下のように変更しています。

# Download sources
sources:
  - type: git
    name: ruby/gem_rbs_collection
    remote: https://github.com/ruby/gem_rbs_collection.git
    revision: main
    repo_dir: gems

# You can specify local directories as sources also.
# - type: local
#   path: path/to/your/local/repository

# A directory to install the downloaded RBSs
path: .gem_rbs_collection

gems:
  # Skip loading rbs gem's RBS.
  # It's unnecessary if you don't use rbs as a library.
  - name: rbs
    ignore: true
  - name: steep
    ignore: true
  - name: ipaddr

変更内容としては後述するSteepでの型チェック時にパスするために必要なライブラリの設定だけ追加してる感じです。

あとは.gem_rbs_collection.gitignoreに追加して、以下のコマンドを実行するだけです。

rbs collection install

これで既存のライブラリの型情報が導入できました。

Steepで生成したRBSの型情報をVS Codeで表示

次にSteepを導入し、TypeProfgem_rbs_collectionで導入した型情報をVS Code上に表示させます。

まずはGemfileSteepを追加してbundle installを実行。

gem 'steep'

その後bundle exec steep initを実行して、設定ファイルを生成します。

bundle exec steep init

生成されたSteepfileを以下のように変更します。

target :app do
  check 'app'

  signature 'sig'
end

checkには型情報をチェックしたい場所のディレクトリを指定し、signatureRBSがあるディレクトリを指定している感じです。

あとは以下のVS CodeExtensionをインストールし、VS Codeを再起動すればOKです。

github.com

再起動すると以下の画像のように型情報が表示されているかと思います。

型チェックがパスするように修正

次にbundle exec steep checkがパスするように修正しました。

やったこととしては

  • デフォルトの設定だと大量のエラーが出るので一時的にIgnore
  • TypeProfで生成された一部のRBSでの型情報が正しくないと表示されたのを修正
  • app/lib配下の一部のコードで重複エラーが返されたので修正

という感じです。

エラーを一時的にIgnoreするのは以下のようにSteepfileを修正しました。

ignores = [
  Steep::Diagnostic::Ruby::MethodDefinitionMissing,
  Steep::Diagnostic::Ruby::UnknownConstant,
  Steep::Diagnostic::Ruby::NoMethod,
  Steep::Diagnostic::Ruby::UnexpectedBlockGiven,
  Steep::Diagnostic::Ruby::IncompatibleAssignment,
  Steep::Diagnostic::Ruby::UnknownInstanceVariable,
  Steep::Diagnostic::Ruby::UnexpectedPositionalArgument,
  Steep::Diagnostic::Ruby::UnresolvedOverloading,
  Steep::Diagnostic::Ruby::InsufficientPositionalArguments,
  Steep::Diagnostic::Ruby::UnknownGlobalVariable,
  Steep::Diagnostic::Ruby::UnexpectedError,
  Steep::Diagnostic::Ruby::MethodBodyTypeMismatch,
  Steep::Diagnostic::Ruby::ArgumentTypeMismatch,
  Steep::Diagnostic::Ruby::UnsupportedSyntax,
  Steep::Diagnostic::Ruby::UnsupportedSyntax,
  Steep::Diagnostic::Ruby::UnexpectedSuper,
  Steep::Diagnostic::Ruby::UnexpectedYield,
  Steep::Diagnostic::Ruby::BreakTypeMismatch,
  Steep::Diagnostic::Ruby::RequiredBlockMissing,
  Steep::Diagnostic::Ruby::ReturnTypeMismatch,
  Steep::Diagnostic::Ruby::ImplicitBreakValueMismatch,
]

target :app do
  check 'app'

  signature 'sig'

  configure_code_diagnostics do |hash|
    ignores.each do |ignore|
      hash[ignore] = :information
    end
  end
end

あとTypeProfで生成されたRBSの型情報が正しくない件については、以下のようなRangeが返ってくると期待されているものでエラーが返ってきました。

    def show: -> Range

現状では泣く泣くuntypedに置き換えて、一旦パスするようにしています......。

    def show: -> untyped

app/lib配下のコードの重複エラーについては以下のように対応しました。

    def current_time: -> Float
                    | ...

今後

おおよその型情報についてはTypeProfgem_rbs_collectionのおかげでなんとかなっていますが、untypedなままのものも多い状況です。 なので、この辺を少しづつ片付けていきたいですねー。

参考

github.com

github.com

github.com

github.com

techlife.cookpad.com

pocke.hatenablog.com

github.com

zenn.dev

sinsoku.hatenablog.com

pocke.hatenablog.com

joker1007.hatenablog.com