SteepのAnnotationに関する備忘録 #asakusa_bashi_rbs
Steepのmanual/annotations.mdを読みながら、実際にコードを書いて覚えたことをブログにまとめる。
変数
変数の型を String?
から String
にするときに便利そう。
# @type var value: String value = %w[a b c].sample puts('Hi, ' + value)
アノテーションがない場合、 sample: () -> String?
なので型検査エラーになる。
app/user.rb:2:14: [error] Cannot pass a value of type `(::String | nil)` as an argument of type `::string` │ (::String | nil) <: ::string │ (::String | nil) <: (::String | ::_ToStr) │ nil <: (::String | ::_ToStr) │ nil <: ::String │ │ Diagnostic ID: Ruby::ArgumentTypeMismatch │ └ puts('Hi, ' + value) ~~~~~ Detected 1 problem from 1 file
インスタンス変数
変数と同じく、インスタンス変数の型を変えるときに便利そう。
# ruby class User def initialize(name) @name = name end def hi # @type ivar @name: String puts('Hi, ' + @name) end def bye puts('bye, ' + @name) end end # rbs class User @name: String? def initialize: (String?) -> void def hi: () -> void def bye: () -> void end
これは bye
の方だけ型検査エラーになる。
app/user.rb:12:19: [error] Cannot pass a value of type `(::String | nil)` as an argument of type `::string` │ (::String | nil) <: ::string │ (::String | nil) <: (::String | ::_ToStr) │ nil <: (::String | ::_ToStr) │ nil <: ::String │ │ Diagnostic ID: Ruby::ArgumentTypeMismatch │ └ puts('bye, ' + @name) ~~~~~ Detected 1 problem from 1 file
クラスとメソッド
Rubyのコード内にアノテーションを書くと、RBSなしでもSteepで型チェックできる。
# @type const User : User class User # @type method say: (String, Integer) -> void def say(name, age) puts(name + age) # 型検査エラー end end User.new.say(1, 2) # ここは型エラーにならない
このコードをSteepで検査すると、 say
メソッドの中だけ型検査の対象になっていることがわかる。
app/user.rb:5:16: [error] Cannot pass a value of type `::Integer` as an argument of type `::string` │ ::Integer <: ::string │ ::Integer <: (::String | ::_ToStr) │ ::Integer <: ::String │ ::Numeric <: ::String │ ::Object <: ::String │ ::BasicObject <: ::String │ │ Diagnostic ID: Ruby::ArgumentTypeMismatch │ └ puts(name + age) # 型検査エラー ~~~ Detected 1 problem from 1 file
pure
メソッドに副作用がないことを明示するときに指定する。*1
# ruby class User def initialize(name) @name = name end def name @name end end user = User.new(nil) if user.name puts("Hi, " + user.name) end # rbs class User def initialize: (String?) -> void %a{pure} def name: () -> String? end
%a{pure}
を指定しない場合、以下のような型検査エラーが起きる。
app/user.rb:13:16: [error] Cannot pass a value of type `(::String | nil)` as an argument of type `::string` │ (::String | nil) <: ::string │ (::String | nil) <: (::String | ::_ToStr) │ nil <: (::String | ::_ToStr) │ nil <: ::String │ │ Diagnostic ID: Ruby::ArgumentTypeMismatch │ └ puts("Hi, " + user.name) ~~~~~~~~~ Detected 1 problem from 1 file
*1:ドキュメントには記載がない
RBSのuntyped, void, top, botの違いを理解する #asakusa_bashi_rbs
RBSのdocs/syntax.mdに記載されている untyped, void, top, bot の違いを理解したけど、また忘れそうなのでブログにまとめておく。
untyped
どんな型でも代入できて、その変数を型検査の対象にしない場合に使う。
# ruby class User def greet(user) user.something end end # rbs class User def greet: (untyped) -> void end
something
メソッドの定義は存在しないのに、Steepの型検査でエラーは出ない。
No type error detected. 🫖
void
メソッドの戻り値を使わない場合に使う。
# ruby class User def foo bar.upcase end def bar "hello" end end # rbs class User def foo: () -> String def bar: () -> void end
void である戻り値に対して upcase
のメソッドを呼び出しているため、Steep の型検査でエラーが出る。
app/user.rb:3:8: [error] Type `void` does not have method `upcase` │ Diagnostic ID: Ruby::NoMethod │ └ bar.upcase ~~~~~~
top
どんな型でも代入できるが、その変数でメソッド呼び出しを禁止したい場合に使う。TypeScriptのunknown型と同じ。
# ruby class User def greet(user) puts user.name end end user = User.new user.greet("Pikachu") user.greet(1) # rbs class User def greet: (top) -> void end
型検査で引数に String と Integer を渡している部分ではエラーになっていないが、name
のメソッドを呼ぶところはエラーが出る。
app/user.rb:3:14: [error] Type `top` does not have method `name` │ Diagnostic ID: Ruby::NoMethod │ └ puts user.name ~~~~
bot
例外が必ず発生するか、無限ループで終了しないメソッドで使う。TypeScriptのnever型と同じ。
# ruby class User def foo raise NotImplementedError end def bar loop do puts "hello" sleep 1 end end def buz puts "hi, Eevee" end end # rbs class User def foo: () -> bot def bar: () -> bot def buz: () -> bot end
Steepで型検査すると foo, bar は問題ないが、 buz は実装が誤っているためエラーが出る。
app/user.rb:13:6: [error] Cannot allow method body have type `nil` because declared as type `bot` │ nil <: bot │ │ Diagnostic ID: Ruby::MethodBodyTypeMismatch │ └ def buz ~~~
RubyVM::AbstractSyntaxTree で Node を辿る処理をシンプルに実装する
Ruby の Enumerator を使うと簡単にRubyのコードを処理できて便利だったので、ブログに書いておく。
ast = RubyVM::AbstractSyntaxTree.parse(<<~RUBY) class User < ApplicationRecord def say(text) puts text end def sum(x, y) x + y end end RUBY enum = Enumerator.new { |y| nodes = ast.children until nodes.empty? nodes.select! { _1.is_a?(RubyVM::AbstractSyntaxTree::Node) } nodes.each { y << _1 } nodes = nodes.flat_map(&:children) end } method_names = enum.select { _1.type == :DEFN }.map { _1.children[0] } pp method_names #=> [:say, :sum]
【追記】Rails v7.1.0 で `can't be blank` が `can’t be blank` に変わる(リバートされました)
既存アプリやライブラリへの影響が大きく、この変更に対してネガティブなフィードバックも多かったためリバートされました。 github.com
概要
表題の通り、Rails v7.1.0 で APOSTROPHE (U+0027) が SINGLE QUOTATION MARK (U+2019) に変わります。
既存のRailsアプリをアップグレードする際に影響が大きそうなので、記事を書きました。
影響範囲
テストでエラーメッセージを検証していた場合、Rails v7.1.0 のアップグレードによって検証に失敗するようになります。
Expected: "can't be blank" Actual: "can’t be blank"
今回の変更を知らない場合、このテストのエラーメッセージだけで ' と ’ の違いを見分けるのは厳しそう。
SINGLE QUOTATION MARK (U+2019) の入力方法
日本語入力モードで「Shift + 7」を押すと入力できました。*1
変更が戻る可能性
マージされるときも「I'll merge this one and see how people will react to them during the pre-releases.」と書いてあり、マージ後にrails/rails#45463でいくつかコメントが挙がっているので、もしかすると変更がリバートされる可能性はあります。
気になる方やこの変更に対して懸念がある方は v7.1.0.rc が出る前にGitHubで絵文字リアクションやコメントを投稿しておくと良さそうです。
*1:私はJIS配列キーボード使ってるので、英字キーボードだと入力方法が異なるかもしれない
プルリクで更新されたgemの一覧を出力する
Gemfile.lock
のdiffから更新されたgemを探す方法を調べたのでメモ
動作確認に使ったプルリク
GitHub APIでプルリクの情報を取得する
$ gh api '/repos/rails/rails/pulls/48955' --jq '.base.sha, .head.sha' 1e824aa8e0655b4717cf612e31010bdd808f4fdf 2d61a206f445ccd52645c8d47ce97de39a39ea9d
GitHub APIで変更前後のGemfileを取得する
$ gh api '/repos/rails/rails/contents/Gemfile.lock?ref=1e824aa8e0655b4717cf612e31010bdd808f4fdf' --jq '.content' | base64 --decode > Gemfile.lock.before $ gh api '/repos/rails/rails/contents/Gemfile.lock?ref=2d61a206f445ccd52645c8d47ce97de39a39ea9d' --jq '.content' | base64 --decode > Gemfile.lock.after
Gemfile.lock の差分を調べる
$ BUNDLE_GEMFILE=Gemfile.lock.before ruby -rbundler -e \ 'puts ["Gemfile.lock.after", "Gemfile.lock.before"].map { Bundler::LockfileParser.new(File.read(_1)).specs }.inject(:-)' nio4r (2.5.9) puma (6.3.0)
Gemfileがないディレクトリだと Could not locate Gemfile or .bundle/ directory
のエラーが出たので、 BUNDLE_GEMFILE
を指定することでエラーを回避している。
Rubyやgit-gsubを使って正規表現で複数行を置換する part2
の続き。
回答 3
Rubyで -0777
を指定するとファイル単位で読み込む方法を教えて頂きました。
$ cat foo.txt foo $ cat bar.txt bar $ cat buz.txt foo bar buz $ ruby -i -n0777 -e 'puts "hi, " + $_' foo.txt bar.txt buz.txt $ cat foo.txt hi, foo $ cat bar.txt hi, bar $ cat buz.txt hi, foo bar buz
詳細
-0777
のオプションは初めて知ったけど、 man ruby
に記載されていました。
-0[octal]. (The digit “zero”.) Specifies the input record separator ($/) as an octal number. If no digit is given, the null character is taken as the separator. Other switches may follow the digits. -00 turns Ruby into paragraph mode. -0777 makes Ruby read whole file at once as a single string since there is no legal character with that value.
まとめ
複数のファイルを一括で編集するときに -0777
は覚えておくと便利そう。
Rubyやgit-gsubを使って正規表現で複数行を置換する
150個を超える *.tf
をまとめて編集する方法を調べたので、備忘録としてまとめておく。
経緯
Terraformのコードを複数ファイルにすべきか、それとも1つのファイルにすべきか難しいな。hclをパースして、hclを書くプログラムを書いた方が良いのかなぁ...
— 神速 (@sinsoku_listy) 2023年6月2日
150ファイルくらいvimで1つずつ心を込めて編集している。
— 神速 (@sinsoku_listy) 2023年6月2日
30ファイル目くらいで正規表現で解決しようとしたけど、正規表現が難しくて時間が溶けてしまった...辛い... https://t.co/JRzCYN9g3x
— 神速 (@sinsoku_listy) 2023年6月2日
やりたかったこと
以下のようなコードが大量にあるとき、全てのファイルから指定のチームidを持つ team { ... }
を削除したい。
resource "github_repository_collaborators" "foo" { repository = github_repository.foo.name team { permission = "push" team_id = "rubyist" } team { permission = "admin" team_id = "ruby-committers" } }
ruby-jp に相談
Rubyが得意なのでRubyで置換する方法を調べていたが、うまくいかなかったので ruby-jp に相談した。
すると、何人かの方から回答を頂いた。
投稿した質問
Rubyでファイルの中身を置換する簡単な方法は何かあったりしないでしょうか?
ruby -e 'File.read("foo.txt").gsub(/def/, "!!").then { File.write("foo.txt", _1) }'
みたいな処理を簡単に書けないものかなと。
回答1
Rubyの -i -p
を使う方法を教えて頂きました。
$ ruby -i -p -e '$_.gsub!(/def/, "!!")' foo.txt
これらのオプションは初めて知って勉強になった。
-i[extension] edit ARGV files in place (make backup if extension supplied)
-p assume loop like -n but print line also like sed
あと、 $_
も初めてみた。
ただ、これだと team { ... }
のような複数行を処理できないため、別の方法が必要になった。
回答1-a
回答1 を参考に -i
を使いつつ、複数行を取得して正規表現で置換することで目的は達成できた。
$ ruby -i -e 'puts readlines.join.gsub(/ team {[\s\w="\.\[\]]+(ruby-committers)[\s\w="\.\[\]]+}\n/, "")'
回答2
Ruby ではないですが、git-gsub を使う方法も教えて頂きました。
$ git gsub "(?s)team {[^{]+?ruby-committers.+?}" ""
正規表現もシンプルですし、 git-gsub
は普段使いしやすそうなので、これも知ることができて勉強になった。
まとめ
ruby-jp に助けられたし、勉強になった。ありがとうございます。