File: traversal.rb

package info (click to toggle)
ruby-rubocop-ast 1.24.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 1,256 kB
  • sloc: ruby: 15,071; yacc: 90; makefile: 9
file content (182 lines) | stat: -rw-r--r-- 6,750 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# frozen_string_literal: true

module RuboCop
  module AST
    # Provides methods for traversing an AST.
    # Does not transform an AST; for that, use Parser::AST::Processor.
    # Override methods to perform custom processing. Remember to call `super`
    # if you want to recursively process descendant nodes.
    module Traversal
      # Only for debugging.
      # @api private
      class DebugError < RuntimeError
      end

      TYPE_TO_METHOD = Hash.new { |h, type| h[type] = :"on_#{type}" }

      def walk(node)
        return if node.nil?

        send(TYPE_TO_METHOD[node.type], node)
        nil
      end

      # @api private
      module CallbackCompiler
        SEND = 'send(TYPE_TO_METHOD[child.type], child)'
        assign_code = 'child = node.children[%<index>i]'
        code = "#{assign_code}\n#{SEND}"
        TEMPLATE = {
          skip: '',
          always: code,
          nil?: "#{code} if child"
        }.freeze

        def def_callback(type, *signature,
                         arity: signature.size..signature.size,
                         arity_check: ENV.fetch('RUBOCOP_DEBUG', nil) && self.arity_check(arity),
                         body: self.body(signature, arity_check))
          type, *aliases = type
          lineno = caller_locations(1, 1).first.lineno
          module_eval(<<~RUBY, __FILE__, lineno) # rubocop:disable Style/EvalWithLocation
            def on_#{type}(node)        # def on_send(node)
              #{body}                   #   # body ...
              nil                       #   nil
            end                         # end
          RUBY
          aliases.each do |m|
            alias_method "on_#{m}", "on_#{type}"
          end
        end

        def body(signature, prelude)
          signature
            .map.with_index do |arg, i|
              TEMPLATE[arg].gsub('%<index>i', i.to_s)
            end
            .unshift(prelude)
            .join("\n")
        end

        def arity_check(range)
          <<~RUBY
            n = node.children.size
            raise DebugError, [
              'Expected #{range} children, got',
              n, 'for', node.inspect
            ].join(' ') unless (#{range}).cover?(node.children.size)
          RUBY
        end
      end
      private_constant :CallbackCompiler
      extend CallbackCompiler
      send_code = CallbackCompiler::SEND

      ### arity == 0
      no_children = %i[true false nil self cbase zsuper redo retry
                       forward_args forwarded_args match_nil_pattern
                       forward_arg forwarded_restarg forwarded_kwrestarg
                       lambda empty_else kwnilarg
                       __FILE__ __LINE__ __ENCODING__]

      ### arity == 0..1
      opt_symbol_child = %i[restarg kwrestarg]
      opt_node_child = %i[splat kwsplat match_rest]

      ### arity == 1
      literal_child = %i[int float complex
                         rational str sym lvar
                         ivar cvar gvar nth_ref back_ref
                         arg blockarg shadowarg
                         kwarg match_var]

      many_symbol_children = %i[regopt]

      node_child = %i[not match_current_line defined?
                      arg_expr pin if_guard unless_guard
                      match_with_trailing_comma]
      node_or_nil_child = %i[block_pass preexe postexe]

      NO_CHILD_NODES = (no_children + opt_symbol_child + literal_child).to_set.freeze
      private_constant :NO_CHILD_NODES # Used by Commissioner

      ### arity > 1
      symbol_then_opt_node = %i[lvasgn ivasgn cvasgn gvasgn]
      symbol_then_node_or_nil = %i[optarg kwoptarg]
      node_then_opt_node = %i[while until module sclass]

      ### variable arity
      many_node_children = %i[dstr dsym xstr regexp array hash pair
                              mlhs masgn or_asgn and_asgn rasgn mrasgn
                              undef alias args super yield or and
                              while_post until_post iflipflop eflipflop
                              match_with_lvasgn begin kwbegin return
                              in_match match_alt break next
                              match_as array_pattern array_pattern_with_tail
                              hash_pattern const_pattern find_pattern
                              index indexasgn procarg0 kwargs]
      many_opt_node_children = %i[case rescue resbody ensure for when
                                  case_match in_pattern irange erange
                                  match_pattern match_pattern_p]

      ### Callbacks for above
      def_callback no_children
      def_callback opt_symbol_child, :skip, arity: 0..1
      def_callback opt_node_child, :nil?, arity: 0..1

      def_callback literal_child, :skip
      def_callback node_child, :always
      def_callback node_or_nil_child, :nil?

      def_callback symbol_then_opt_node, :skip, :nil?, arity: 1..2
      def_callback symbol_then_node_or_nil, :skip, :nil?
      def_callback node_then_opt_node, :always, :nil?

      def_callback many_symbol_children, :skip, arity_check: nil
      def_callback many_node_children, body: <<~RUBY
        node.children.each { |child| #{send_code} }
      RUBY
      def_callback many_opt_node_children,
                   body: <<~RUBY
                     node.children.each { |child| #{send_code} if child }
                   RUBY

      ### Other particular cases
      def_callback :const, :nil?, :skip
      def_callback :casgn, :nil?, :skip, :nil?, arity: 2..3
      def_callback :class, :always, :nil?, :nil?
      def_callback :def, :skip, :always, :nil?
      def_callback :op_asgn, :always, :skip, :always
      def_callback :if, :always, :nil?, :nil?
      def_callback :block, :always, :always, :nil?
      def_callback :numblock, :always, :skip, :nil?
      def_callback :defs, :always, :skip, :always, :nil?

      def_callback %i[send csend], body: <<~RUBY
        node.children.each_with_index do |child, i|
          next if i == 1

          #{send_code} if child
        end
      RUBY

      ### generic processing of any other node (forward compatibility)
      defined = instance_methods(false)
                .grep(/^on_/)
                .map { |s| s.to_s[3..].to_sym } # :on_foo => :foo

      to_define = ::Parser::Meta::NODE_TYPES.to_a
      to_define -= defined
      to_define -= %i[numargs ident] # transient
      to_define -= %i[blockarg_expr restarg_expr] # obsolete
      to_define -= %i[objc_kwarg objc_restarg objc_varargs] # mac_ruby
      def_callback to_define, body: <<~RUBY
        node.children.each do |child|
          next unless child.class == Node
          #{send_code}
        end
      RUBY
      MISSING = to_define if ENV['RUBOCOP_DEBUG']
    end
  end
end