File: parser_gem.rb

package info (click to toggle)
ruby-proc-to-ast 0.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 116 kB
  • sloc: ruby: 253; makefile: 4
file content (119 lines) | stat: -rw-r--r-- 2,845 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
require "proc_to_ast/version"

require "parser/current"
require "unparser"
require "rouge"

module ProcToAst
  class MultiMatchError < StandardError; end

  class Parser
    attr_reader :parser

    @formatter = Rouge::Formatters::Terminal256.new
    @lexer = Rouge::Lexers::Ruby.new

    class << self
      def highlight(source)
        @formatter.format(@lexer.lex(source))
      end
    end

    def initialize
      @parser = ::Parser::CurrentRuby.default_parser
      @parser.diagnostics.consumer = ->(diagnostic) {} # suppress error message
    end

    # Read file and try parsing
    # if success parse, find proc AST
    #
    # @param filename [String] reading file path
    # @param linenum [Integer] start line number
    # @return [Parser::AST::Node] Proc AST
    def parse(filename, linenum)
      @filename, @linenum = filename, linenum
      buf = []
      File.open(filename, "rb").each_with_index do |line, index|
        next if index < linenum - 1
        buf << line
        begin
          return do_parse(buf.join)
        rescue ::Parser::SyntaxError
          node = trim_and_retry(buf)
          return node if node
        end
      end
      fail(::Parser::SyntaxError, "Unknown error")
    end

    private

    def do_parse(source)
      parser.reset

      source_buffer = ::Parser::Source::Buffer.new(@filename, @linenum)
      source_buffer.source = source
      node = parser.parse(source_buffer)
      block_nodes = traverse_node(node)

      if block_nodes.length == 1
        block_nodes.first
      else
        raise ProcToAst::MultiMatchError
      end
    end

    # Remove tail comma and wrap dummy method, and retry parsing
    # For proc inner Array or Hash
    def trim_and_retry(buf)
      *lines, last = buf

      # For inner Array or Hash or Arguments list.
      lines << last.gsub(/,\s*$/, "")
      do_parse("a(#{lines.join})") # wrap dummy method
    rescue ::Parser::SyntaxError
    end

    def traverse_node(node)
      if node.type != :block
        node.children.flat_map { |child|
          if child.is_a?(AST::Node)
            traverse_node(child)
          end
        }.compact
      else
        [node]
      end
    end
  end
end

class Proc
  # @return [Parser::AST::Node] Proc AST
  def to_ast
    filename, linenum = source_location
    parser = ProcToAst::Parser.new
    parser.parse(filename, linenum)
  end

  # @param highlight [Boolean] enable output highlight
  # @return [String] proc source code
  def to_source(highlight: false)
    source = Unparser.unparse(to_ast)
    if highlight
      ProcToAst::Parser.highlight(source)
    else
      source
    end
  end

  def to_raw_source(highlight: false)
    source = to_ast.loc.expression.source.force_encoding("UTF-8")

    if highlight
      ProcToAst::Parser.highlight(source)
    else
      source
    end
  end
end