Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby 정적 분석 입문: parser 젬으로 소스 코드에서 메서드 정보 추출하기

소스 코드를 분석해서 모든 메서드가 어디에 정의되어 있는지, 그리고 어떤 인자를 받는지 찾아내고 싶다고 가정해 봅시다.

어떻게 하면 이 작업을 수행할 수 있을까요?

첫 번째로 떠오르는 아이디어는 정규표현식(regexp)을 작성하는 것일 수 있습니다.

하지만 더 좋은 방법이 있지 않을까요?

Ruby 정적 분석 입문: parser 젬으로 소스 코드에서 메서드 정보 추출하기

네, 있습니다!

정적 분석(Static Analysis)은 소스 코드 자체에서 정보를 추출해야 할 때 사용할 수 있는 기법입니다.

이 과정은 소스 코드를 토큰 단위로 변환하는 것(파싱)부터 시작됩니다.

그럼 바로 시작해 보겠습니다!

parser 젬 사용하기

Ruby는 표준 라이브러리에 Ripper라는 파서를 포함하고 있습니다. 하지만 Ripper의 출력 결과는 다루기가 까다롭기 때문에, 저는 훨씬 편리한 parser 젬을 사용하는 것을 선호합니다. 실제로 유명한 코드 분석 도구인 Rubocop도 이 젬을 활용해 마법 같은 일을 해냅니다.

이 젬에는 코드를 직접 파싱하고 그 결과로 생성된 파스 트리(parse tree)를 확인할 수 있는 실행 파일도 함께 포함되어 있습니다.

다음은 그 예시입니다:

ruby-parse -e '%w(hello world).map { |c| c.upcase }'

출력 결과는 다음과 같습니다:

(block
  (send
    (array
      (str "hello")
      (str "world")) :map)
  (args
    (arg :c))
  (send
    (lvar :c) :upcase))

이 출력은 Ruby가 특정 코드를 어떻게 해석하는지 이해하려고 할 때 매우 유용합니다. 하지만 직접 분석 도구를 만들고 싶다면, 소스 파일을 읽고 파싱한 뒤 생성된 트리를 순회(traverse)하는 과정을 거쳐야 합니다.

예시:

require 'parser/current'

code = File.read('app.rb')
parsed_code = Parser::CurrentRuby.parse(code)

파서는 여러분의 코드에 대한 AST(추상 구문 트리, Abstract Syntax Tree)를 반환합니다. 이름이 다소 어려워 보일 수 있지만, 실제로는 들리는 것만큼 복잡하지 않으니 겁먹지 않아도 됩니다 🙂

AST 순회하기

parser 젬으로 코드를 파싱했다면, 이제 생성된 AST를 순회해야 합니다.

이 작업은 AST::Processor를 상속받는 클래스를 만들면 됩니다.

예시:

class Processor < AST::Processor
end

그런 다음 이 클래스의 인스턴스를 생성하고 .process 메서드를 호출합니다:

ast = Processor.new
ast.process(parsed_code)

여기서 on_으로 시작하는 메서드들을 몇 가지 정의해야 합니다. 이 메서드들은 AST의 노드 이름과 대응됩니다.

어떤 메서드들을 정의해야 하는지 알아내려면 Processor 클래스에 handler_missing 메서드를 추가하면 됩니다. 또한 on_begin 메서드도 필요합니다.

class Processor < AST::Processor
  def on_begin(node)
    node.children.each { |c| process(c) }
  end

  def handler_missing(node)
    puts "missing #{node.type}"
  end
end

현재까지의 진행 상황:

이제 Ruby AST와 기본적인 프로세서가 준비되었습니다. 이 코드를 실행하면 AST에 포함된 노드 타입들이 화면에 출력되는 것을 확인할 수 있습니다.

이제 남은 작업:

사용하고자 하는 on_ 메서드들을 모두 구현하면 됩니다. 예를 들어, 모든 인스턴스 메서드의 이름과 해당 라인 번호를 얻고 싶다면 다음과 같이 작성할 수 있습니다:

def on_def(node)
  line_num    = node.loc.line
  method_name = node.children[0]

  puts "Found #{method_name} at line #{line_num}"
end

프로그램을 실행하면 발견된 모든 메서드 이름이 출력될 것입니다.

마무리

Ruby 정적 분석 도구를 만드는 일은 생각보다 어렵지 않습니다. 더 완성도 높은 예시를 보고 싶다면 class_indexer 젬을 참고해 보세요. 이제 여러분 차례입니다. 직접 나만의 분석 도구를 만들어 보세요!

이 글이 도움이 되었다면 주변에 공유해 주세요! 🙂