summaryrefslogtreecommitdiff
path: root/lib/syntax_suggest/visitor.rb
blob: 6e25f7239cb0f14cccf9ca12a534be0ec5e1140b (plain)
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
# frozen_string_literal: true

module SyntaxSuggest
  # Walks the Prism AST to extract structural info that cannot be reliably determined from tokens
  # alone.
  #
  # Such as the location of lines that must be logically joined so the search algorithm will
  # treat them as one. Example:
  #
  #   source = <<~RUBY
  #     User                        # 1
  #       .where(name: "Earlopain") # 2
  #       .first                    # 3
  #   RUBY
  #   ast, _tokens = Prism.parse_lex(source).value
  #   visitor = Visitor.new
  #   visitor.visit(ast)
  #   visitor.consecutive_lines # => Set[2, 1]
  #
  # This output means that line 1 and line 2 need to be joined with their next line.
  #
  # And determining the location of "endless" method definitions. For example:
  #
  #   source = <<~RUBY
  #     def cube(x)
  #       x * x * x
  #     end
  #     def square(x) = x * x # 1
  #   RUBY
  #
  #   ast, _tokens = Prism.parse_lex(source).value
  #   visitor = Visitor.new
  #   visitor.visit(ast)
  #   visitor.endless_def_keyword_offsets # => Set[28]
  class Visitor < Prism::Visitor
    attr_reader :endless_def_keyword_offsets, :consecutive_lines

    def initialize
      @endless_def_keyword_offsets = Set.new
      @consecutive_lines = Set.new
    end

    # Called by Prism::Visitor for every method-call node in the AST
    # (e.g. `foo.bar`, `foo.bar.baz`).
    def visit_call_node(node)
      receiver_loc = node.receiver&.location
      call_operator_loc = node.call_operator_loc
      message_loc = node.message_loc
      if receiver_loc && call_operator_loc && message_loc
        # dot-leading (dot on the next line)
        #   foo        # line 1 - consecutive
        #     .bar     # line 2
        if receiver_loc.end_line != call_operator_loc.start_line && call_operator_loc.start_line == message_loc.start_line
          (receiver_loc.end_line..call_operator_loc.start_line - 1).each do |line|
            @consecutive_lines << line
          end
        end

        # dot-trailing (dot on the same line as the receiver)
        #   foo.       # line 1 - consecutive
        #     bar      # line 2
        if receiver_loc.end_line == call_operator_loc.start_line && call_operator_loc.start_line != message_loc.start_line
          (call_operator_loc.start_line..message_loc.start_line - 1).each do |line|
            @consecutive_lines << line
          end
        end
      end
      super
    end

    # Called by Prism::Visitor for every `def` node in the AST.
    # Records the keyword start location for endless method definitions
    # like `def foo = 123`. These are valid without a matching `end`,
    # so Token must exclude them when deciding if a line is a keyword.
    def visit_def_node(node)
      @endless_def_keyword_offsets << node.def_keyword_loc.start_offset if node.equal_loc
      super
    end
  end
end