File: parser.rb

package info (click to toggle)
ruby-test-prof 0.12.2%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 508 kB
  • sloc: ruby: 4,075; makefile: 4
file content (154 lines) | stat: -rw-r--r-- 4,012 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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# frozen_string_literal: true

require "ripper"

module TestProf
  module RSpecStamp
    # Parse examples headers
    module Parser
      # Contains the result of parsing
      class Result
        attr_accessor :fname, :desc, :desc_const
        attr_reader :tags, :htags

        def add_tag(v)
          @tags ||= []
          @tags << v
        end

        def add_htag(k, v)
          @htags ||= []
          @htags << [k, v]
        end

        def remove_tag(tag)
          @tags&.delete(tag)
          @htags&.delete_if { |(k, _v)| k == tag }
        end
      end

      class << self
        # rubocop: disable Metrics/CyclomaticComplexity
        # rubocop: disable Metrics/PerceivedComplexity
        def parse(code)
          sexp = Ripper.sexp(code)
          return unless sexp

          # sexp has the following format:
          # [:program,
          #   [
          #     [
          #       :command,
          #       [:@ident, "it", [1, 0]],
          #       [:args_add_block, [ ... ]]
          #     ]
          #   ]
          # ]
          #
          # or
          #
          # [:program,
          #   [
          #     [
          #       :vcall,
          #       [:@ident, "it", [1, 0]]
          #     ]
          #   ]
          # ]
          res = Result.new

          fcall = sexp[1][0][1]
          args_block = sexp[1][0][2]

          if fcall.first == :fcall
            fcall = fcall[1]
          elsif fcall.first == :var_ref
            res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
            args_block = sexp[1][0][4]
          end

          res.fname ||= fcall[1]

          return res if args_block.nil?

          args_block = args_block[1] if args_block.first == :arg_paren

          args = args_block[1]

          if args.first.first == :string_literal
            res.desc = parse_literal(args.shift)
          elsif args.first.first == :var_ref || args.first.first == :const_path_ref
            res.desc_const = parse_const(args.shift)
          end

          parse_arg(res, args.shift) until args.empty?

          res
        end
        # rubocop: enable Metrics/CyclomaticComplexity
        # rubocop: enable Metrics/PerceivedComplexity

        private

        def parse_arg(res, arg)
          if arg.first == :symbol_literal
            res.add_tag parse_literal(arg)
          elsif arg.first == :bare_assoc_hash
            parse_hash(res, arg[1])
          end
        end

        def parse_hash(res, hash_arg)
          hash_arg.each do |(_, label, val)|
            res.add_htag label[1][0..-2].to_sym, parse_value(val)
          end
        end

        # Expr of the form:
        #  bool - [:var_ref, [:@kw, "true", [1, 24]]]
        #  string - [:string_literal, [:string_content, [...]]]
        #  int - [:@int, "3", [1, 52]]]]
        def parse_value(expr)
          case expr.first
          when :var_ref
            expr[1][1] == "true"
          when :@int
            expr[1].to_i
          when :@float
            expr[1].to_f
          else
            parse_literal(expr)
          end
        end

        # Expr of the form:
        #  [:string_literal, [:string_content, [:@tstring_content, "is", [1, 4]]]]
        def parse_literal(expr)
          val = expr[1][1][1]
          val = val.to_sym if expr[0] == :symbol_literal ||
            expr[0] == :assoc_new
          val
        end

        # Expr of the form:
        #  [:var_ref, [:@const, "User", [1, 9]]]
        #
        #  or
        #
        #  [:const_path_ref, [:const_path_ref, [:var_ref,
        #    [:@const, "User", [1, 17]]],
        #    [:@const, "Guest", [1, 23]]],
        #    [:@const, "Collection", [1, 30]]
        def parse_const(expr)
          if expr.first == :var_ref
            expr[1][1]
          elsif expr.first == :@const
            expr[1]
          elsif expr.first == :const_path_ref
            expr[1..-1].map(&method(:parse_const)).join("::")
          end
        end
      end
    end
  end
end