lib/mutant/ast/pattern/parser.rb



# frozen_string_literal: true

module Mutant
  class AST
    # rubocop:disable Metrics/ClassLength
    # rubocop:disable Metrics/MethodLength
    class Pattern
      class Parser
        def initialize(tokens)
          @tokens        = tokens
          @next_position = 0
        end

        def self.call(tokens)
          new(tokens).__send__(:run)
        end

      private

        def error?
          instance_variable_defined?(:@error)
        end

        def run
          node = catch(:abort) do
            parse_node.tap do
              if next?
                token = peek
                error(
                  message: "Unexpected token: #{token.type}",
                  token:
                )
              end
            end
          end

          if error?
            Either::Left.new(@error)
          else
            Either::Right.new(node)
          end
        end

        def parse_node
          structure = parse_node_type

          attribute, descendant = nil

          if optional(:properties_start)
            loop do
              break if optional(:properties_end)

              name = expect(:string)

              name_sym = name.value.to_sym

              if structure.maybe_attribute(name_sym)
                expect(:eq)
                attribute = parse_attribute(name_sym)
                next
              end

              if structure.maybe_descendant(name_sym)
                expect(:eq)
                descendant = parse_descendant(name_sym)
                next
              end

              error(
                message: "Node: #{structure.type} has no property named: #{name_sym}",
                token:   name
              )
            end
          end

          Node.new(
            attribute:,
            descendant:,
            type:       structure.type
          )
        end

        def parse_attribute(name)
          Node::Attribute.new(
            name:,
            value: parse_alternative(
              group_start: method(:parse_attribute_group),
              string:      method(:parse_attribute_value)
            )
          )
        end

        def parse_alternative(alternatives)
          token = peek

          alternatives.fetch(token.type) do
            error(
              message: "Expected one of: #{alternatives.keys.join(',')} but got: #{token.type}",
              token:
            )
          end.call
        end

        def parse_descendant(name)
          Node::Descendant.new(
            name:,
            pattern: parse_node
          )
        end

        def parse_attribute_group
          expect(:group_start)

          values = []

          loop do
            values << parse_attribute_value
            break unless optional(:delimiter)
          end

          expect(:group_end)

          Node::Attribute::Value::Group.new(values:)
        end

        def parse_attribute_value
          Node::Attribute::Value::Single.new(value: expect(:string).value.to_sym)
        end

        def error(message:, token: nil)
          @error =
            if token
              "#{message}\n#{token.display_location}"
            else
              message
            end

          throw(:abort)
        end

        def optional(type)
          token = peek

          return unless token&.type.equal?(type)

          advance_position
          token
        end

        def parse_node_type
          token = expect(:string)

          type = token.value.to_sym

          Structure::ALL.fetch(type) do
            error(token:, message: "Expected valid node type got: #{type}")
          end
        end

        def expect(type)
          token = peek

          unless token
            error(message: "Expected token of type: #{type}, but got no token at all")
          end

          if token.type.eql?(type)
            advance_position
            token
          else
            error(
              token:,
              message: "Expected token type: #{type} but got: #{token.type}"
            )
          end
        end

        def peek
          @tokens.at(@next_position)
        end

        def advance_position
          @next_position += 1
        end

        def next?
          @next_position < @tokens.length
        end
      end
    end # Pattern
    # rubocop:enable Metrics/ClassLength
    # rubocop:enable Metrics/MethodLength
  end # AST
end # Mutant