Hisakeyのブログ

エンジニアが色々呟くブログです。

「はじめてのAST」〜parserとRubocopで構文木を学ぶ〜

はじめに

ASTとはなにかというものを、簡単なコードを書きながら理解していく記事です。

今回は、parser を用いて Ruby のコードから AST(Abstract Syntax Tree)の構造を確認し、さらに Rubocop で簡単なカスタムCopを作成して、理解を深めてみました。

背景・動機

先日の柏rbの振り返り回が始まる前に、チラっと話題に出た 「AST」 というワード。

正直、自分はそれが何かすらまったくわかっていませんでした。
「抽象構文木?」「構文解析?」という状態。

なので、まずは手を動かしながら自分なりに調べてみよう!という気持ちで、実際に試してみました。

実例・やってみたこと

まずは、ASTとはどのような構造なのかを実際に確認してみました。

Parserをつかってみることで表せるみたいなので、簡単にファイルを作成してみます。

# sample.rb

require 'parser/current'
require 'pp'

code = 'puts "hello world"'
ast = Parser::CurrentRuby.parse(code)

pp ast

# 以下のように出力されます
# s(:send, nil, :puts,
#  s(:str, "hello world"))

ASTの理解

  • :sendは、 メソッド呼び出し
  • nilは、レシーバーが無いことを表している
  • :putsは、メソッド名
  • s(:srt, "hello world")は、引数の文字列

Rubyのコードが各ノードとして構造化されるようになる。これが、ASTと言われる構造

そのノードを使って、putsを使わないカスタムcopを作成する

custom_cops/no_puts_cop.rb

require 'rubocop'

module RuboCop
  module Cop
    module Custom
      class NoPuts < Base
        MSG = 'putsを使わず、Rails.loggerなどを使いましょう。'

        def on_send(node) # メソッドの呼び出しに反応するメソッド on_defとかもある
          return unless node.method?(:puts)
          return unless node.receiver.nil?

          add_offense(node) # ルール違反があれば、出力する
        end
      end
    end
  end
end

上記をrubocop.yml で定義する

# ./rubocop.yml

require:
  - ./custom_cops/no_puts_cop.rb

Custom/NoPuts:
  Enabled: true

実際に使用してみる(sample.rbにputsを使った処理を記述する)

rubocop sample.rb -config /.rubocop.yml
hello_world.rb:1:1: C: Custom/NoPuts: putsを使わず、Rails.loggerなどを使いましょう。
puts "hello world"
^^^^^^^^^^^^^^^^^^

学び・気づき・解釈

ASTは「コードをツリー構造で表したもの」として理解すれば、意外と読みやすかも?( 最初はとっつきにくいですが、、、)

parser gemで簡単にASTを出力でき、Copの設計にも役立つ

RubocopのCopはASTのノード(例: on_send, on_def)に反応して動く

「puts禁止」のような単純なルールなら、ASTの基本的な理解だけで十分実装できる

まとめ

初めて触れるASTは、最初こそとっつきにくく感じますが、「ツリー構造を読む」意識で見ると少しずつ見えてきます。

今回のように parser で構文を分解して見てみることで、Rubocopの仕組みにも親しみやすくなりました。

参考リンク