【Ruby Advent Calendar 2020】Ruby の AST から Ruby のソースコードを復元しよう【1日目】
Ruby Advent Calendar 2020 1日目の記事になります。
もう今年も Advent Calendar の時期ですね。
今年は 12/25日に Ruby 3.0 がリリースされるのでとても楽しみです。
並行して Ruby 3.0 Advent Calendar 2020 も開催しているので興味があるひとはぜひ参加してみてください!
Ruby Advent Calendar 2020 もまだ空きはあるのでこっちも参加してね!
さて、今年の Advent Calendar は Ruby の AST 周りについて書いてみようかと思います。
- 【Ruby Advent Calendar 2019】ピュア Ruby で Ruby 2.7 の Numbered parameter を実装してみよう!【1日目】 - Secret Garden(Instrumental)
- 【Ruby Advent Calendar 2018】あなたのしらない Refinements の世界【3日目】 - Secret Garden(Instrumental)
- 【Ruby Advent Calendar 2017】Ruby で型チェックを実装してみよう【1日目】 - Secret Garden(Instrumental)
- 【Ruby Advent Calendar 2016 1日目】 Ruby でブロックを簡潔に定義する - Secret Garden(Instrumental)
注意
本記事では Ruby 2.7.2で動作確認を行っています。
AST は Ruby のバージョンに深く依存するので AST 情報を扱う場合は使用する Ruby のバージョンに注意しましょう。
AST とは
AST とは『Abstract Syntax Tree』の略で日本語にすると『抽象構文木』と呼ばれるデータ構造になります。
Ruby では『Ruby のコードをバイトコードに変換する過程』で構文解析を行い、AST への変換を経て Ruby のコードを実行しています。
本記事ではその AST データを『Ruby のコードに復元すること』について書いていきます。
今年はこの『AST から Ruby のコードへ変換する実装』を主に趣味開発していました。
Ruby のコードを AST に変換する
さて、Ruby のコードを AST 情報へと変換する手段はいくつかあるんですが今回は標準ライブラリの RubyVM::AbstractSyntaxTree
を利用して AST 情報を取得していきます。
RubyVM::AbstractSyntaxTree
の使い方自体は簡単で RubyVM::AbstractSyntaxTree.parse
に構文解析したい Ruby のコードを文字列で渡すだけです。
渡した Ruby の AST 情報が RubyVM::AbstractSyntaxTree::Node
として返ってきます。
code = "1 + 2" ast = RubyVM::AbstractSyntaxTree.parse(code) pp ast.class # => RubyVM::AbstractSyntaxTree::Node pp ast # => (SCOPE@1:0-1:5 # tbl: [] # args: nil # body: (OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)))
RubyVM::AbstractSyntaxTree::Node
は『ノードの命令』と『そのノードの子ノード』の2つの情報を保持しており AST という名前の通り『ツリー構造』になっています。
ノードの命令は #type
で取得でき、子ノードは #children
で取得する事ができます。
code = "1 + 2" ast = RubyVM::AbstractSyntaxTree.parse(code) # type がノードの命令の Symbol を返す pp ast.type # => :SCOPE # children がそのノードの子ツリーを返す pp ast.children # => [[], # nil, # (OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil))]
他にもそのノードの定義位置の情報を保持していますが、本記事では扱わないので省略します。
Ruby のコードが複雑になればツリー構造もどんどん複雑になっていきます。
code = "1 + 2 * 3 / 4" ast = RubyVM::AbstractSyntaxTree.parse(code) # Ruby の構造が複雑になるとどんどんネストしてく pp ast.children # => [[], # nil, # (OPCALL@1:0-1:13 (LIT@1:0-1:1 1) :+ # (LIST@1:4-1:13 # (OPCALL@1:4-1:13 # (OPCALL@1:4-1:9 (LIT@1:4-1:5 2) :* # (LIST@1:8-1:9 (LIT@1:8-1:9 3) nil)) :/ # (LIST@1:12-1:13 (LIT@1:12-1:13 4) nil)) nil))]
また RubyVM::AbstractSyntaxTree.or
に Proc
オブジェクトを渡すとその Proc
が持っているブロックの中身の AST 情報を返します。
# proc のブロックの中身をパースする ast = RubyVM::AbstractSyntaxTree.of(proc{ 1 + 2 }) pp ast # => (SCOPE@2:40-2:49 # tbl: [] # args: nil # body: # (OPCALL@2:42-2:47 (LIT@2:42-2:43 1) :+ # (LIST@2:46-2:47 (LIT@2:46-2:47 2) nil)))
余談:Ripper.sexp
を使う
Ruby では RubyVM::AbstractSyntaxTree
の他に Ripper
というライブラリで構文解析を行うことができます。
Ripper
では Ripper.sexp
を使用して構文解析の結果を S式で取得する事ができます。
require "ripper" code = "1 + 2" ast = Ripper.sexp(code) pp ast # => [:program, [[:binary, [:@int, "1", [1, 0]], :+, [:@int, "2", [1, 4]]]]]
見て分かるとおり RubyVM::AbstractSyntaxTree
とはかなり情報が異なります。
また RubyVM::AbstractSyntaxTree.of
相当の機能は Ripper
にはありません(多分
今回はこの RubyVM::AbstractSyntaxTree.of
の機能を使いたかったので RubyVM::AbstractSyntaxTree
を使用しています。
1 + 2
はどういう AST になるのか
先程は 1 + 2
という Ruby のコードを RubyVM::AbstractSyntaxTree
というライブラリを使って構文解析しました。
code = "1 + 2" ast = RubyVM::AbstractSyntaxTree.parse(code) pp ast.class # => RubyVM::AbstractSyntaxTree::Node pp ast # => (SCOPE@1:0-1:5 # tbl: [] # args: nil # body: (OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)))
この時に SCOPE
という AST 情報が最初に来るのですが、この SCOPE
というノードは『Ruby が暗黙的に追加する』ノードになります。
なので実際の 1 + 2
というコードに対する AST は次のようになります。
code = "1 + 2" ast = RubyVM::AbstractSyntaxTree.parse(code) # SCOPE ノードの子を参照する # これが子ノードの最後が実際にパースしたい Ruby の AST 情報になる node = ast.children.last # 1 + 2 のノード情報 pp node # ノードの種類 pp node.type # => :OPCALL # 1 + 2 の子ノード pp node.children # => [(LIT@1:0-1:1 1), :+, (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)]
上記の node
の子ノードは、
(LIT@1:0-1:1 1)
:+
(LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)
という 3つの情報を持っています。
これはそれぞれ、
(LIT@1:0-1:1 1)
->+
の左辺情報:+
-> 演算する演算子情報(LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)
->+
の右辺情報
という情報になります。
また、 左辺
の (LIT@1:0-1:1 1)
や 右辺
の (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)
なども更にネストした AST 情報となります。
試しに左辺の AST を見てみると以下のようになっていることがわかります。
code = "1 + 2" ast = RubyVM::AbstractSyntaxTree.parse(code) node = ast.children.last # 左辺の AST を取得 left = node.children.first # 左辺のノードの種類 pp left.type # => :LIT # 左辺の子ノード pp left.children # => [1]
やっと 1
という値が出てきましたね。
こんな感じで AST がツリー構造となっているのが分かると思います。
余談: 1 + 2
と 1.+(2)
の違い
Ruby では 1 + 2
のような演算式は 1
の #+
メソッドを呼び出すことになるので 1.+(2)
と同じ意味になります。
しかし RubyVM::AbstractSyntaxTree
ではこの2つのコードの構文解析の結果が異なります。
# こっちは OPCALL 命令 pp RubyVM::AbstractSyntaxTree.parse("1 + 2").children.last # => (OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)) # こっちは CALL 命令 pp RubyVM::AbstractSyntaxTree.parse("1.+(2)").children.last # => (CALL@1:0-1:6 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil))
これは 1 + 2
のような演算式と .+
のようなメソッド呼び出しでは AST の意味が異なる為です。
AST から Ruby のコードに復元しよう
さて、ここからが本題になります。
実際に AST から Ruby のコードに変換してみましょう。
まずは簡単に 1
という値の AST を Ruby のコードに変換してみます。
1
の AST は以下のようになります。
code = "1" ast = RubyVM::AbstractSyntaxTree.parse(code) pp ast # => (SCOPE@1:0-1:1 tbl: [] args: nil body: (LIT@1:0-1:1 1))
ここでは SCOPE
と LIT
の 2種類のノードが出てきます。
まずはこの 2つのノードに対応する『アンパーサ』をつくってみます。
# AST のノード情報から Ruby のコードに変換する def unparse(node) # ノードの種類から Ruby のコードに変換するための分岐 case node.type when :SCOPE # SCOPE はかなり複雑な構造ので今回は body の値を参照するだけに留める tbl, args, body = node.children # body をそのまま再度 unparse する unparse(body) when :LIT # LIT はリテラル値を保持しているだけなのでその値をそのまま返す lit = node.children.first lit.to_s else "" end end code = "1" ast = RubyVM::AbstractSyntaxTree.parse(code) pp unparse(ast) # => "1"
このようにノードの種類を判別して、その子ノードから更にアンパースしていくような実装になります。
これを踏まえて次は 1 + 2
に対応してみましょう。
1 + 2
の AST は次のようになります。
code = "1 + 2" ast = RubyVM::AbstractSyntaxTree.parse(code) pp ast # => (SCOPE@1:0-1:5 # tbl: [] # args: nil # body: (OPCALL@1:0-1:5 (LIT@1:0-1:1 1) :+ (LIST@1:4-1:5 (LIT@1:4-1:5 2) nil)))
ここで OPCALL
と LIST
というノードが増えましたね。
OPCALL
は +
演算子を表現するノードで LIST
はその +
メソッドの引数を表現しています。
これを考慮して unparse
を実装すると以下のようになります。
# AST のノード情報から Ruby のコードに変換する def unparse(node) case node&.type when :SCOPE tbl, args, body = node.children unparse(body) when :LIT lit = node.children.first lit.to_s when :OPCALL # 『左辺』、『演算子』、『右辺』の情報を分割して取得する left, op, right = node.children # 左辺と右辺は再度アンパースして、演算子は Symbol なのでそのままにする "#{unparse(left)} #{op} #{unparse(right)}" when :LIST # 各子ノードをアンパースし、それを , で結合する node.children.compact.map { |node| unparse(node) }.join(", ") else "" end end code = "1 + 2" ast = RubyVM::AbstractSyntaxTree.parse(code) pp unparse(ast) # => "1 + 2" # 複数の式も変換できてる! code = "1 + 2 + 3 + 4" ast = RubyVM::AbstractSyntaxTree.parse(code) pp unparse(ast) # => "1 + 2 + 3 + 4"
この調子で試しに制御構文も実装してみましょう。
# AST のノード情報から Ruby のコードに変換する def unparse(node) case node&.type when :SCOPE tbl, args, body = node.children unparse(body) when :LIT lit = node.children.first lit.to_s when :OPCALL left, op, right = node.children "#{unparse(left)} #{op} #{unparse(right)}" when :LIST node.children.compact.map { |node| unparse(node) }.join(", ") when :IF # 子ノードは以下の3つになる # * 条件式 # * then 部 # * else 部 cond, body, else_ = node.children <<~EOS if #{unparse(cond)} #{unparse(body)} #{"else\n #{unparse(else_)}" if else_} end EOS when :VCALL # 引数がないメソッド呼び出し mid = node.children.first mid.to_s else "" end end code = <<~EOS if cond foo else bar end EOS ast = RubyVM::AbstractSyntaxTree.parse(code) puts unparse(ast) # => if cond # foo # else # bar # end # 後置 if も構文解析すると if 文と同じ構造になる ast = RubyVM::AbstractSyntaxTree.parse("foo if cond") puts unparse(ast) # => if cond # foo # # end
このように各ノードに対して Ruby のコードになるように実装していくことになります。
後置 if
が普通の if 文になるのが面白いですね。
余談:どんなノードがあるの
Ruby ではノードの種類がめちゃくちゃ多くて 100個以上のノードがあります。
制御構文を表すノードから変数の定義参照、メソッド呼び出しなどなど様々な種類のノードがあります。
実際にどういうノードがあるのかは Ruby の node.c
を参照するといいと思います。
また、以下の記事にも詳しくまとめられているのでこちらを見てみるのもいいと思います。
ちなみにノードの種類は細かく分けられているんですが先程の後置 if のように書き方が違うだけで意味が同じような構文は共通のノードで表される事があります。
例えば Ruby では &&
と and
は優先順位が異なるのですが、優先順位が異なるだけで意味は同じなので共通のノードが使用されています。
# 両方とも同じ AST になる pp RubyVM::AbstractSyntaxTree.parse("a && b").children.last # => (AND@1:0-1:6 (VCALL@1:0-1:1 :a) (VCALL@1:5-1:6 :b)) pp RubyVM::AbstractSyntaxTree.parse("a and b").children.last # => (AND@1:0-1:7 (VCALL@1:0-1:1 :a) (VCALL@1:6-1:7 :b))
では何が違うのかというと構文解析後のツリーの構造が異なるようになります。
# && と and は優先順位が異なるのでノードは同じだがツリーの行動が異なる # ((a && b) || c) というような構造になる pp RubyVM::AbstractSyntaxTree.parse("a && b || c").children.last # => (OR@1:0-1:11 (AND@1:0-1:6 (VCALL@1:0-1:1 :a) (VCALL@1:5-1:6 :b)) # (VCALL@1:10-1:11 :c)) # (a and (b || c)) というような構造になる pp RubyVM::AbstractSyntaxTree.parse("a and b || c").children.last # => (AND@1:0-1:12 (VCALL@1:0-1:1 :a) # (OR@1:6-1:12 (VCALL@1:6-1:7 :b) (VCALL@1:11-1:12 :c)))
AST から Ruby のコードに変換する rensei-gem をつくった
はい、では 1個1個のノードを解析していきましょうってやると日が暮れてしまいます。
ということで先程行ったような『ノードから Ruby のコードに変換する』ということを全ノードに対して実装した gem をつくりました。
$ gem install rensei
リポジトリはこちら
使い方も簡単で Rensei.unparse
に RubyVM::AbstractSyntaxTree::Node
を渡すだけです。
require "rensei" code = "1 + 2" ast = RubyVM::AbstractSyntaxTree.parse(code) pp Rensei.unparse(ast) # => "(1 + 2)"
ここで注目したいのが元のコードは 1 + 2
なんですが Rensei.unparse
の結果は (1 + 2)
となっており元々の Ruby のコードとはちょっと違っている点です。
これはなぜかというと Ruby の AST 情報は実際のコードから最低限の情報のみで AST を構築しているので元のコードを完全に復元するのは不可能だからです。
なので rensei-gem で復元した Ruby のコードはあくまでも『元となった AST と復元後の Ruby のコードの ASTが同じであること』のみを保証するような実装になっております。
実際にもっと複雑な Ruby のコードの AST を渡すと全く異なる Ruby のコードになります。
require "rensei" code = "1 + 2" code = <<~'RUBY' # AST のノード情報から Ruby のコードに変換する def unparse(node) case node&.type when :SCOPE tbl, args, body = node.children unparse(body) when :LIT lit = node.children.first lit.to_s when :OPCALL left, op, right = node.children "#{unparse(left)} #{op} #{unparse(right)}" when :LIST node.children.compact.map { |node| unparse(node) }.join(", ") when :IF # 子ノードは以下の3つになる # * 条件式 # * then 部 # * else 部 cond, body, else_ = node.children <<~EOS if #{unparse(cond)} #{unparse(body)} #{"else\n #{unparse(else_)}" if else_} end EOS when :VCALL # 引数がないメソッド呼び出し mid = node.children.first mid.to_s else "" end end RUBY ast = RubyVM::AbstractSyntaxTree.parse(code) puts Rensei.unparse(ast) __END__ output: def unparse(node) case node&.type() when :SCOPE (tbl, args, body, ) = node.children(); unparse(body) when :LIT (lit = node.children().first()); lit.to_s() when :OPCALL (left, op, right, ) = node.children(); "#{unparse(left)} #{op} #{unparse(right)}" when :LIST node.children().compact().map() { |node| unparse(node) }.join(", ") when :IF (cond, body, else_, ) = node.children(); "if #{unparse(cond)}\n #{unparse(body)}\n#{if else_ "else\n #{unparse(else_)}" end}\nend\n" when :VCALL (mid = node.children().first()); mid.to_s() else "" end end
こんな感じでコメントが消えていたり、インデントがおかしかったり、メソッド呼び出しでは必ず ()
が付いたり、ヒアドキュメントがただの文字列になっていたり、やたらめったら ()
が付いてたりと元のコードとは似ても似つかないコードになっています。
しかし、この異なるコードでも AST で見ると全く同じ情報になります。
rensei-gem の注意点
この rensei-gem ですが今回の Advent Calendar のために gem を公開したのですが実際のところまだ未完成です。
現時点ではこれぐらい複雑なコードの復元には成功しているんですが、まだまだエッジケースのバグは多いです。
例えば次のような Ruby のコードにはまだ対応していません。
require "rensei" # 元のコード code = <<~EOS a = begin foo bar end EOS ast = RubyVM::AbstractSyntaxTree.parse(code) # 意図しない Ruby のコードになっている puts Rensei.unparse(ast) # => (a = foo; bar)
また、 RubyVM::AbstractSyntaxTree
自体に不具合があることもあり、例えば次のようなコードは Ruby 2.7.1 時点で『演算子の情報』が欠落していました。
# 本来であれば += の情報が欲しいが AST でそれが欠落してしまっている code = "struct.field += foo" ast = RubyVM::AbstractSyntaxTree.parse(code) pp ast # => (SCOPE@1:0-1:19 # tbl: [] # args: nil # body: # (OP_ASGN2@1:0-1:19 (VCALL@1:0-1:6 :struct) false :field # (VCALL@1:16-1:19 :foo)))
この不具合はわたしが bugs.ruby に報告しパッチを投げて取り込んでもらいました。
このパッチは Ruby 2.7.2 にバックポートされているので Ruby 2.7.2 では『演算子の情報』がある AST になっています。
# Ruby 2.7.2 だと += 情報が取得できる code = "struct.field += foo" ast = RubyVM::AbstractSyntaxTree.parse(code) pp ast # => (SCOPE@1:0-1:19 # tbl: [] # args: nil # body: # (OP_ASGN2@1:0-1:19 (VCALL@1:0-1:6 :struct) false :field :+ # (VCALL@1:16-1:19 :foo)))
他にも後置 if
の場合、通常の if
文として復元されます。
require "rensei" code = "foo if cond" ast = RubyVM::AbstractSyntaxTree.parse(code) # 後置 if ではなくて if ~ end で復元する puts Rensei.unparse(ast) # => if cond # foo # end
これの影響により a = 42 if a
みたいな Ruby コードを復元しようとすると意味が変わってしまいます。
require "rensei" code = "a = 42 if a" ast = RubyVM::AbstractSyntaxTree.parse(code) puts Rensei.unparse(ast) # => if a # (a = 42) # end
Ruby の場合は a = 42 if a
と if a; a = 42; end
で変数が定義されるタイミングが異なり、前者は問題がないんですが後者の場合はエラーになってしまいます(実際に動かしてみるとわかります。
これはまだどう対応するべきか考え中です。
このように rensei-gem はまだまだ不安定となっております。
あとテストはガッツリ用意しているんですが実装コードはかなり適当なのであんまり読まないでね!
rensei-gem で遊んで見る
さてさて、まだまだ未完成ではあるんですが rensei-gem を使って AST から Ruby のコードに復元する事ができるようになりました。
では、これを利用して遊んでみたいと思います。
rensei-gem は任意の AST 情報から Ruby のコードに変換します。
つまり『AST の中身を書き換えれば Ruby のコードを変更すること』ができるようになります。
例えば、 obj.foo
みたいなメソッド呼び出しは CALL
で表現されます。
code = "obj.foo" ast = RubyVM::AbstractSyntaxTree.parse(code) pp ast.children.last # => (CALL@1:0-1:7 (VCALL@1:0-1:3 :obj) :foo nil)
一方で obj&.foo
のようにぼっち演算子を使用した場合は QCALL
というような命令になります。
code = "obj&.foo" ast = RubyVM::AbstractSyntaxTree.parse(code) pp ast.children.last # => (QCALL@1:0-1:7 (VCALL@1:0-1:3 :obj) :foo nil)
つまりこの CALL
という情報を QCALL
に置き換えることで obj.foo
というコードをぼっち演算子呼び出しの obj&.foo
という Ruby のコードに変換する事ができます。
require "rensei" class Hash # ノードの Hash をネストして操作する拡張する def each_node(&block) return enum_for(:each_node) unless block self[:children].each { |node| block.call self if Hash === node node.each_node(&block) end } end end # RubyVM::AbstractSyntaxTree::Node から Hash に変換する拡張 # RubyVM::AbstractSyntaxTree::Node#to_h で変換できる using Rensei::NodeToHash code = "obj.foo" ast = RubyVM::AbstractSyntaxTree.parse(code) # AST から必要な node だけ抽出 node = ast.children.last # AST の node から Hash に変換 node_h = node.to_h # AST の type の意味を変更 # hoge.foo から hoge&.foo に変換する node_h.each_node { |node| node[:type] = :QCALL if node[:type] == :CALL } # 変換した AST の Hash を Ruby のコードに戻す code = Rensei.unparse(node_h) # . から &. に変換されている pp code # => "obj&.foo()"
このように比較的簡単な実装で .
から &.
呼び出しに変換することができました。
更にこれを『特定のブロックのコード』で反映されるようにしてみましょう。
require "rensei" class Hash # ノードの Hash をネストして操作する拡張する def each_node(&block) return enum_for(:each_node) unless block self[:children].each { |node| block.call self if Hash === node node.each_node(&block) end } end end # RubyVM::AbstractSyntaxTree::Node から Hash に変換する拡張 # RubyVM::AbstractSyntaxTree::Node#to_h で変換できる using Rensei::NodeToHash def bocchi &block # ブロックの中身を AST に変換 ast = RubyVM::AbstractSyntaxTree.of(block) # AST から必要な node だけ抽出 node = ast.children.last # AST の node から Hash に変換 node_h = node.to_h # AST の type の意味を変更 # hoge.foo から hoge&.foo に変換する node_h.each_node { |node| node[:type] = :QCALL if node[:type] == :CALL } # 変換した AST の Hash を Ruby のコードに戻す code = Rensei.unparse(node_h) # メソッドの呼び出し元のコンテキストで評価する block.binding.eval(code) end obj = { foo: { hoge: {} } } # . を &. に変換して実行する bocchi { pp obj[:foo] # => {:hoge=>{}} pp obj[:foo][:bar] # => nil pp obj[:foo][:bar].to_s # => nil pp obj[:foo][:bar][:piyo] # => nil }
RubyVM::AbstractSyntaxTree
では『ブロック内のコードを AST 情報として取得する事ができる』のでこのように『特定のブロック内のコードのみに対して AST 情報を書き換えて実行する事』ができます。
こういう使い方であれば副作用は最小限に留める事ができるのでかなりいろんな事ができるのでは?と思っています。
所感
と、言うことで RubyVM::AbstractSyntaxTree
とその情報から Ruby のコードに復元するようなことを考えてみました。
元々はブロック内のコードを文字列として取得したくて何か便利なライブラリがないか探していたんですが、その時に RubyVM::AbstractSyntaxTree
がブロックから AST を生成できることを見つけて勢いで実装してみました。
実装するのはかなり大変だったんですが AST を見ていると Ruby の気持ちがわかってきてとても勉強になりました。
元のコードと復元したコードはかなり異なる形の文字列になるので元々やりたかった事には利用できなさそうなんですが、これはこれで便利なのでもっと色々と利用できないか今後も考えていきたいと思います。
将来的には rensei-gem を利用してマクロ的なものを実装したいと考えているんですがまだまだ先は長そうです…。
それでは今年の Ruby Advent Calendar 2020 の記事でした。
まだ空きはあるのでみんな書いてみてね!