File: block_snippet_extractor.rb

package info (click to toggle)
ruby-rspec 3.13.0c0e0m0s1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,856 kB
  • sloc: ruby: 70,868; sh: 1,423; makefile: 99
file content (253 lines) | stat: -rw-r--r-- 7,335 bytes parent folder | download | duplicates (4)
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
module RSpec
  module Expectations
    # @private
    class BlockSnippetExtractor # rubocop:disable Metrics/ClassLength
      # rubocop should properly handle `Struct.new {}` as an inner class definition.

      attr_reader :proc, :method_name

      def self.try_extracting_single_line_body_of(proc, method_name)
        lines = new(proc, method_name).body_content_lines
        return nil unless lines.count == 1
        lines.first
      rescue Error
        nil
      end

      def initialize(proc, method_name)
        @proc = proc
        @method_name = method_name.to_s.freeze
      end

      # Ideally we should properly handle indentations of multiline snippet,
      # but it's not implemented yet since because we use result of this method only when it's a
      # single line and implementing the logic introduces additional complexity.
      def body_content_lines
        raw_body_lines.map(&:strip).reject(&:empty?)
      end

    private

      def raw_body_lines
        raw_body_snippet.split("\n")
      end

      def raw_body_snippet
        block_token_extractor.body_tokens.map(&:string).join
      end

      def block_token_extractor
        @block_token_extractor ||= BlockTokenExtractor.new(method_name, source, beginning_line_number)
      end

      if RSpec.respond_to?(:world)
        def source
          raise TargetNotFoundError unless File.exist?(file_path)
          RSpec.world.source_from_file(file_path)
        end
      else
        RSpec::Support.require_rspec_support 'source'
        def source
          raise TargetNotFoundError unless File.exist?(file_path)
          @source ||= RSpec::Support::Source.from_file(file_path)
        end
      end

      def file_path
        source_location.first
      end

      def beginning_line_number
        source_location.last
      end

      def source_location
        proc.source_location || raise(TargetNotFoundError)
      end

      Error = Class.new(StandardError)
      TargetNotFoundError = Class.new(Error)
      AmbiguousTargetError = Class.new(Error)

      # @private
      # Performs extraction of block body snippet using tokens,
      # which cannot be done with node information.
      BlockTokenExtractor = Struct.new(:method_name, :source, :beginning_line_number) do
        attr_reader :state, :body_tokens

        def initialize(*)
          super
          parse!
        end

        private

        def parse!
          @state = :initial

          catch(:finish) do
            source.tokens.each do |token|
              invoke_state_handler(token)
            end
          end
        end

        def finish!
          throw :finish
        end

        def invoke_state_handler(token)
          __send__("#{state}_state", token)
        end

        def initial_state(token)
          @state = :after_method_call if token.location == block_locator.method_call_location
        end

        def after_method_call_state(token)
          @state = :after_opener if handle_opener_token(token)
        end

        def after_opener_state(token)
          if handle_closer_token(token)
            finish_or_find_next_block_if_incorrect!
          elsif pipe_token?(token)
            finalize_pending_tokens!
            @state = :after_beginning_of_args
          else
            pending_tokens << token
            handle_opener_token(token)
            @state = :after_beginning_of_body unless token.type == :on_sp
          end
        end

        def after_beginning_of_args_state(token)
          @state = :after_beginning_of_body if pipe_token?(token)
        end

        def after_beginning_of_body_state(token)
          if handle_closer_token(token)
            finish_or_find_next_block_if_incorrect!
          else
            pending_tokens << token
            handle_opener_token(token)
          end
        end

        def pending_tokens
          @pending_tokens ||= []
        end

        def finalize_pending_tokens!
          pending_tokens.freeze.tap do
            @pending_tokens = nil
          end
        end

        def finish_or_find_next_block_if_incorrect!
          body_tokens = finalize_pending_tokens!

          if correct_block?(body_tokens)
            @body_tokens = body_tokens
            finish!
          else
            @state = :after_method_call
          end
        end

        def handle_opener_token(token)
          opener_token?(token).tap do |boolean|
            opener_token_stack.push(token) if boolean
          end
        end

        def opener_token?(token)
          token.type == :on_lbrace || (token.type == :on_kw && token.string == 'do')
        end

        def handle_closer_token(token)
          if opener_token_stack.last.closed_by?(token)
            opener_token_stack.pop
            opener_token_stack.empty?
          else
            false
          end
        end

        def opener_token_stack
          @opener_token_stack ||= []
        end

        def pipe_token?(token)
          token.type == :on_op && token.string == '|'
        end

        def correct_block?(body_tokens)
          return true if block_locator.body_content_locations.empty?
          content_location = block_locator.body_content_locations.first
          content_location.between?(body_tokens.first.location, body_tokens.last.location)
        end

        def block_locator
          @block_locator ||= BlockLocator.new(method_name, source, beginning_line_number)
        end
      end

      # @private
      # Locates target block with node information (semantics), which tokens don't have.
      BlockLocator = Struct.new(:method_name, :source, :beginning_line_number) do
        def method_call_location
          @method_call_location ||= method_ident_node.location
        end

        def body_content_locations
          @body_content_locations ||= block_body_node.map(&:location).compact
        end

        private

        def method_ident_node
          method_call_node = block_wrapper_node.children.first
          method_call_node.find do |node|
            method_ident_node?(node)
          end
        end

        def block_body_node
          block_node = block_wrapper_node.children[1]
          block_node.children.last
        end

        def block_wrapper_node
          case candidate_block_wrapper_nodes.size
          when 1
            candidate_block_wrapper_nodes.first
          when 0
            raise TargetNotFoundError
          else
            raise AmbiguousTargetError
          end
        end

        def candidate_block_wrapper_nodes
          @candidate_block_wrapper_nodes ||= candidate_method_ident_nodes.map do |method_ident_node|
            block_wrapper_node = method_ident_node.each_ancestor.find { |node| node.type == :method_add_block }
            next nil unless block_wrapper_node
            method_call_node = block_wrapper_node.children.first
            method_call_node.include?(method_ident_node) ? block_wrapper_node : nil
          end.compact
        end

        def candidate_method_ident_nodes
          source.nodes_by_line_number[beginning_line_number].select do |node|
            method_ident_node?(node)
          end
        end

        def method_ident_node?(node)
          node.type == :@ident && node.args.first == method_name
        end
      end
    end
  end
end