rmosolgo/graphql-ruby

View on GitHub
lib/graphql/upgrader/member.rb

Summary

Maintainability
D
1 day
Test Coverage
A
96%
# frozen_string_literal: true
begin
  require 'parser/current'
rescue LoadError
  raise LoadError, "GraphQL::Upgrader requires the 'parser' gem, please install it and/or add it to your Gemfile"
end

module GraphQL
  module Upgrader
    GRAPHQL_TYPES = '(Object|InputObject|Interface|Enum|Scalar|Union)'

    class Transform
      # @param input_text [String] Untransformed GraphQL-Ruby code
      # @return [String] The input text, with a transformation applied if necessary
      def apply(input_text)
        raise GraphQL::RequiredImplementationMissingError, "Return transformed text here"
      end

      # Recursively transform a `.define`-DSL-based type expression into a class-ready expression, for example:
      #
      # - `types[X]` -> `[X, null: true]`
      # - `types[X.to_non_null_type]` -> `[X]`
      # - `Int` -> `Integer`
      # - `X!` -> `X`
      #
      # Notice that `!` is removed sometimes, because it doesn't exist in the class API.
      #
      # @param type_expr [String] A `.define`-ready expression of a return type or input type
      # @return [String] A class-ready expression of the same type`
      def normalize_type_expression(type_expr, preserve_bang: false)
        case type_expr
        when /\A!/
          # Handle the bang, normalize the inside
          "#{preserve_bang ? "!" : ""}#{normalize_type_expression(type_expr[1..-1], preserve_bang: preserve_bang)}"
        when /\Atypes\[.*\]\Z/
          # Unwrap the brackets, normalize, then re-wrap
          inner_type = type_expr[6..-2]
          if inner_type.start_with?("!")
            nullable = false
            inner_type = inner_type[1..-1]
          elsif inner_type.end_with?(".to_non_null_type")
            nullable = false
            inner_type = inner_type[0...-17]
          else
            nullable = true
          end

          "[#{normalize_type_expression(inner_type, preserve_bang: preserve_bang)}#{nullable ? ", null: true" : ""}]"
        when /\Atypes\./
          # Remove the prefix
          normalize_type_expression(type_expr[6..-1], preserve_bang: preserve_bang)
        when /\A->/
          # Remove the proc wrapper, don't re-apply it
          # because stabby is not supported in class-based definition
          # (and shouldn't ever be necessary)
          unwrapped = type_expr
            .sub(/\A->\s?\{\s*/, "")
            .sub(/\s*\}/, "")
          normalize_type_expression(unwrapped, preserve_bang: preserve_bang)
        when "Int"
          "Integer"
        else
          type_expr
        end
      end

      def underscorize(str)
        str
          .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2') # URLDecoder -> URL_Decoder
          .gsub(/([a-z\d])([A-Z])/,'\1_\2')     # someThing -> some_Thing
          .downcase
      end

      def apply_processor(input_text, processor)
        ruby_ast = Parser::CurrentRuby.parse(input_text)
        processor.process(ruby_ast)
        processor
      rescue Parser::SyntaxError
        puts "Error text:"
        puts input_text
        raise
      end

      def reindent_lines(input_text, from_indent:, to_indent:)
        prev_indent = " " * from_indent
        next_indent = " " * to_indent
        # For each line, remove the previous indent, then add the new indent
        lines = input_text.split("\n").map do |line|
          line = line.sub(prev_indent, "")
          "#{next_indent}#{line}".rstrip
        end
        lines.join("\n")
      end

      # Remove trailing whitespace
      def trim_lines(input_text)
        input_text.gsub(/ +$/, "")
      end
    end

    # Turns `{X} = GraphQL::{Y}Type.define do` into `class {X} < Types::Base{Y}`.
    class TypeDefineToClassTransform < Transform
      # @param base_class_pattern [String] Replacement pattern for the base class name. Use this if your base classes have nonstandard names.
      def initialize(base_class_pattern: "Types::Base\\3")
        @find_pattern = /( *)([a-zA-Z_0-9:]*) = GraphQL::#{GRAPHQL_TYPES}Type\.define do/
        @replace_pattern = "\\1class \\2 < #{base_class_pattern}"
        @interface_replace_pattern = "\\1module \\2\n\\1  include #{base_class_pattern}"
      end

      def apply(input_text)
        if input_text.include?("GraphQL::InterfaceType.define")
          input_text.sub(@find_pattern, @interface_replace_pattern)
        else
          input_text.sub(@find_pattern, @replace_pattern)
        end
      end
    end

    # Turns `{X} = GraphQL::Relay::Mutation.define do` into `class {X} < Mutations::BaseMutation`
    class MutationDefineToClassTransform < Transform
      # @param base_class_name [String] Replacement pattern for the base class name. Use this if your Mutation base class has a nonstandard name.
      def initialize(base_class_name: "Mutations::BaseMutation")
        @find_pattern = /([a-zA-Z_0-9:]*) = GraphQL::Relay::Mutation.define do/
        @replace_pattern = "class \\1 < #{base_class_name}"
      end

      def apply(input_text)
        input_text.gsub(@find_pattern, @replace_pattern)
      end
    end

    # Remove `name "Something"` if it is redundant with the class name.
    # Or, if it is not redundant, move it to `graphql_name "Something"`.
    class NameTransform < Transform
      def apply(transformable)
        last_type_defn = transformable
          .split("\n")
          .select { |line| line.include?("class ") || line.include?("module ")}
          .last

        if last_type_defn && (matches = last_type_defn.match(/(class|module) (?<type_name>[a-zA-Z_0-9:]*)( <|$)/))
          type_name = matches[:type_name]
          # Get the name without any prefixes or suffixes
          type_name_without_the_type_part = type_name.split('::').last.gsub(/Type$/, '')
          # Find an overridden name value
          if matches = transformable.match(/ name ('|")(?<overridden_name>.*)('|")/)
            name = matches[:overridden_name]
            if type_name_without_the_type_part != name
              # If the overridden name is still required, use `graphql_name` for it
              transformable = transformable.gsub(/ name (.*)/, ' graphql_name \1')
            else
              # Otherwise, remove it altogether
              transformable = transformable.gsub(/\s+name ('|").*('|")/, '')
            end
          end
        end

        transformable
      end
    end

    # Remove newlines -- normalize the text for processing
    class RemoveNewlinesTransform
      def apply(input_text)
        keep_looking = true
        while keep_looking do
          keep_looking = false
          # Find the `field` call (or other method), and an open paren, but not a close paren, or a comma between arguments
          input_text = input_text.gsub(/(?<field>(?:field|input_field|return_field|connection|argument)(?:\([^)]*|.*,))\n\s*(?<next_line>.+)/) do
            keep_looking = true
            field = $~[:field].chomp
            next_line = $~[:next_line]

            "#{field} #{next_line}"
          end
        end
        input_text
      end
    end

    # Remove parens from method call - normalize for processing
    class RemoveMethodParensTransform < Transform
      def apply(input_text)
        input_text.sub(
          /(field|input_field|return_field|connection|argument)\( *(.*?) *\)( *)/,
          '\1 \2\3'
        )
      end
    end

    # Move `type X` to be the second positional argument to `field ...`
    class PositionalTypeArgTransform < Transform
      def apply(input_text)
        input_text.gsub(
          /(?<field>(?:field|input_field|return_field|connection|argument) :(?:[a-zA-Z_0-9]*)) do(?<block_contents>.*?)[ ]*type (?<return_type>.*?)\n/m
        ) do
          field = $~[:field]
          block_contents = $~[:block_contents]
          return_type = normalize_type_expression($~[:return_type], preserve_bang: true)

          "#{field}, #{return_type} do#{block_contents}"
        end
      end
    end

    # Find a configuration in the block and move it to a kwarg,
    # for example
    # ```
    # do
    #   property :thing
    # end
    # ```
    # becomes:
    # ```
    # property: thing
    # ```
    class ConfigurationToKwargTransform < Transform
      def initialize(kwarg:)
        @kwarg = kwarg
      end

      def apply(input_text)
        input_text.gsub(
          /(?<field>(?:field|return_field|input_field|connection|argument).*) do(?<block_contents>.*?)[ ]*#{@kwarg} (?<kwarg_value>.*?)\n/m
        ) do
          field = $~[:field]
          block_contents = $~[:block_contents]
          kwarg_value = $~[:kwarg_value].strip

          "#{field}, #{@kwarg}: #{kwarg_value} do#{block_contents}"
        end
      end
    end

    # Transform `property:` kwarg to `method:` kwarg
    class PropertyToMethodTransform < Transform
      def apply(input_text)
        input_text.gsub /property:/, 'method:'
      end
    end

    # Find a keyword whose value is a string or symbol,
    # and if the value is equivalent to the field name,
    # remove the keyword altogether.
    class RemoveRedundantKwargTransform < Transform
      def initialize(kwarg:)
        @kwarg = kwarg
        @finder_pattern = /(field|return_field|input_field|connection|argument) :(?<name>[a-zA-Z_0-9]*).*#{@kwarg}: ['":](?<kwarg_value>[a-zA-Z_0-9?!]+)['"]?/
      end

      def apply(input_text)
        if input_text =~ @finder_pattern
          field_name = $~[:name]
          kwarg_value = $~[:kwarg_value]
          if field_name == kwarg_value
            # It's redundant, remove it
            input_text = input_text.sub(/, #{@kwarg}: ['":]#{kwarg_value}['"]?/, "")
          end
        end
        input_text
      end
    end

    # Take camelized field names and convert them to underscore case.
    # (They'll be automatically camelized later.)
    class UnderscoreizeFieldNameTransform < Transform
      def apply(input_text)
        input_text.gsub /(?<field_type>input_field|return_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)/ do
          field_type = $~[:field_type]
          camelized_name = $~[:name]
          underscored_name = underscorize(camelized_name)
          "#{field_type} :#{underscored_name}"
        end
      end
    end

    class ProcToClassMethodTransform < Transform
      # @param proc_name [String] The name of the proc to be moved to `def self.#{proc_name}`
      def initialize(proc_name)
        @proc_name = proc_name
        # This will tell us whether to operate on the input or not
        @proc_check_pattern = /#{proc_name}\s?->/
      end

      def apply(input_text)
        if input_text =~ @proc_check_pattern
          processor = apply_processor(input_text, NamedProcProcessor.new(@proc_name))
          processor.proc_to_method_sections.reverse.each do |proc_to_method_section|
            proc_body = input_text[proc_to_method_section.proc_body_start..proc_to_method_section.proc_body_end]
            method_defn_indent = " " * proc_to_method_section.proc_defn_indent
            method_defn = "def self.#{@proc_name}(#{proc_to_method_section.proc_arg_names.join(", ")})\n#{method_defn_indent}  #{proc_body}\n#{method_defn_indent}end\n"
            method_defn = trim_lines(method_defn)
            # replace the proc with the new method
            input_text[proc_to_method_section.proc_defn_start..proc_to_method_section.proc_defn_end] = method_defn
          end
        end
        input_text
      end

      class NamedProcProcessor < Parser::AST::Processor
        attr_reader :proc_to_method_sections
        def initialize(proc_name)
          @proc_name_sym = proc_name.to_sym
          @proc_to_method_sections = []
        end

        class ProcToMethodSection
          attr_accessor :proc_arg_names, :proc_defn_start, :proc_defn_end, :proc_defn_indent, :proc_body_start, :proc_body_end, :inside_proc

          def initialize
            # @proc_name_sym = proc_name.to_sym
            @proc_arg_names = nil
            # Beginning of the `#{proc_name} -> {...}` call
            @proc_defn_start = nil
            # End of the last `end/}`
            @proc_defn_end = nil
            # Amount of whitespace to insert to the rewritten body
            @proc_defn_indent = nil
            # First statement of the proc
            @proc_body_start = nil
            # End of last statement in the proc
            @proc_body_end = nil
            # Used for identifying the proper block
            @inside_proc = false
          end
        end

        def on_send(node)
          receiver, method_name, _args = *node
          if method_name == @proc_name_sym && receiver.nil?
            proc_section = ProcToMethodSection.new
            source_exp = node.loc.expression
            proc_section.proc_defn_start = source_exp.begin.begin_pos
            proc_section.proc_defn_end = source_exp.end.end_pos
            proc_section.proc_defn_indent = source_exp.column
            proc_section.inside_proc = true

            @proc_to_method_sections << proc_section
          end
          res = super(node)
          @inside_proc = false
          res
        end

        def on_block(node)
          send_node, args_node, body_node = node.children
          _receiver, method_name, _send_args_node = *send_node
          if method_name == :lambda && !@proc_to_method_sections.empty? && @proc_to_method_sections[-1].inside_proc
            proc_to_method_section = @proc_to_method_sections[-1]

            source_exp = body_node.loc.expression
            proc_to_method_section.proc_arg_names = args_node.children.map { |arg_node| arg_node.children[0].to_s }
            proc_to_method_section.proc_body_start = source_exp.begin.begin_pos
            proc_to_method_section.proc_body_end = source_exp.end.end_pos
            proc_to_method_section.inside_proc = false
          end
          super(node)
        end
      end
    end

    class MutationResolveProcToMethodTransform < Transform
      # @param proc_name [String] The name of the proc to be moved to `def self.#{proc_name}`
      def initialize(proc_name: "resolve")
        @proc_name = proc_name
      end

      # TODO dedup with ResolveProcToMethodTransform
      def apply(input_text)
        if input_text =~ /GraphQL::Relay::Mutation\.define/
          named_proc_processor = apply_processor(input_text, ProcToClassMethodTransform::NamedProcProcessor.new(@proc_name))
          resolve_proc_processor = apply_processor(input_text, ResolveProcToMethodTransform::ResolveProcProcessor.new)

          named_proc_processor.proc_to_method_sections.zip(resolve_proc_processor.resolve_proc_sections).reverse.each do |pair|
            proc_to_method_section, resolve_proc_section = *pair
            proc_body = input_text[proc_to_method_section.proc_body_start..proc_to_method_section.proc_body_end]
            method_defn_indent = " " * proc_to_method_section.proc_defn_indent

            obj_arg_name, args_arg_name, ctx_arg_name = resolve_proc_section.proc_arg_names
            # This is not good, it will hit false positives
            # Should use AST to make this substitution
            if obj_arg_name != "_"
              proc_body.gsub!(/([^\w:.]|^)#{obj_arg_name}([^\w:]|$)/, '\1object\2')
            end
            if ctx_arg_name != "_"
              proc_body.gsub!(/([^\w:.]|^)#{ctx_arg_name}([^\w:]|$)/, '\1context\2')
            end

            method_defn = "def #{@proc_name}(**#{args_arg_name})\n#{method_defn_indent}  #{proc_body}\n#{method_defn_indent}end\n"
            method_defn = trim_lines(method_defn)
            # Update usage of args keys
            method_defn = method_defn.gsub(/#{args_arg_name}(?<method_begin>\.key\?\(?|\[)["':](?<arg_name>[a-zA-Z0-9_]+)["']?(?<method_end>\]|\))?/) do
              method_begin = $~[:method_begin]
              arg_name = underscorize($~[:arg_name])
              method_end = $~[:method_end]
              "#{args_arg_name}#{method_begin}:#{arg_name}#{method_end}"
            end
            # replace the proc with the new method
            input_text[proc_to_method_section.proc_defn_start..proc_to_method_section.proc_defn_end] = method_defn
          end
        end
        input_text
      end
    end

    # Find hash literals which are returned from mutation resolves,
    # and convert their keys to underscores. This catches a lot of cases but misses
    # hashes which are initialized anywhere except in the return expression.
    class UnderscorizeMutationHashTransform < Transform
      def apply(input_text)
        if input_text =~ /def resolve\(\*\*/
          processor = apply_processor(input_text, ReturnedHashLiteralProcessor.new)
          # Use reverse_each to avoid messing up positions
          processor.keys_to_upgrade.reverse_each do |key_data|
            underscored_key = underscorize(key_data[:key].to_s)
            if key_data[:operator] == ":"
              input_text[key_data[:start]...key_data[:end]] = underscored_key
            else
              input_text[key_data[:start]...key_data[:end]] = ":#{underscored_key}"
            end
          end
        end
        input_text
      end

      class ReturnedHashLiteralProcessor < Parser::AST::Processor
        attr_reader :keys_to_upgrade
        def initialize
          @keys_to_upgrade = []
        end

        def on_def(node)
          method_name, _args, body = *node
          if method_name == :resolve
            possible_returned_hashes = find_returned_hashes(body, returning: false)
            possible_returned_hashes.each do |hash_node|
              pairs = *hash_node
              pairs.each do |pair_node|
                if pair_node.type == :pair # Skip over :kwsplat
                  pair_k, _pair_v = *pair_node
                  if pair_k.type == :sym && pair_k.children[0].to_s =~ /[a-z][A-Z]/ # Does it have any camelcase boundaries?
                    source_exp = pair_k.loc.expression
                    @keys_to_upgrade << {
                      start: source_exp.begin.begin_pos,
                      end: source_exp.end.end_pos,
                      key: pair_k.children[0],
                      operator: pair_node.loc.operator.source,
                    }
                  end
                end
              end
            end
          end

        end

        private

        # Look for hash nodes, starting from `node`.
        # Return hash nodes that are valid candiates for returning from this method.
        def find_returned_hashes(node, returning:)
          if node.is_a?(Array)
            *possible_returns, last_expression = *node
            return possible_returns.map { |c| find_returned_hashes(c, returning: false) }.flatten +
              # Check the last expression of a method body
              find_returned_hashes(last_expression, returning: returning)
          end

          case node.type
          when :hash
            if returning
              [node]
            else
              # This is some random hash literal
              []
            end
          when :begin
            # Check the last expression of a method body
            find_returned_hashes(node.children, returning: true)
          when :resbody
            _condition, _assign, body = *node
            find_returned_hashes(body, returning: returning)
          when :kwbegin
            find_returned_hashes(node.children, returning: returning)
          when :rescue
            try_body, rescue_body, _ensure_body = *node
            find_returned_hashes(try_body, returning: returning) + find_returned_hashes(rescue_body, returning: returning)
          when :block
            # Check methods with blocks for possible returns
            method_call, _args, *body = *node
            if method_call.type == :send
              find_returned_hashes(body, returning: returning)
            end
          when :if
            # Check each branch of a conditional
            _condition, *branches = *node
            branches.compact.map { |b| find_returned_hashes(b, returning: returning) }.flatten
          when :return
            find_returned_hashes(node.children.first, returning: true)
          else
            []
          end
        rescue
          p "--- UnderscorizeMutationHashTransform crashed on node: ---"
          p node
          raise
        end

      end
    end

    class ResolveProcToMethodTransform < Transform
      def apply(input_text)
        if input_text =~ /resolve\(? ?->/
          # - Find the proc literal
          # - Get the three argument names (obj, arg, ctx)
          # - Get the proc body
          # - Find and replace:
          #  - The ctx argument becomes `context`
          #  - The obj argument becomes `object`
          # - Args is trickier:
          #   - If it's not used, remove it
          #   - If it's used, abandon ship and make it `**args`
          #   - Convert string args access to symbol access, since it's a Ruby **splat
          #   - Convert camelized arg names to underscored arg names
          #   - (It would be nice to correctly become Ruby kwargs, but that might be too hard)
          #   - Add a `# TODO` comment to the method source?
          # - Rebuild the method:
          #   - use the field name as the method name
          #   - handle args as described above
          #   - put the modified proc body as the method body

          input_text.match(/(?<field_type>input_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)/)
          field_name = $~[:name]
          processor = apply_processor(input_text, ResolveProcProcessor.new)

          processor.resolve_proc_sections.reverse.each do |resolve_proc_section|
            proc_body = input_text[resolve_proc_section.proc_start..resolve_proc_section.proc_end]
            obj_arg_name, args_arg_name, ctx_arg_name = resolve_proc_section.proc_arg_names
            # This is not good, it will hit false positives
            # Should use AST to make this substitution
            if obj_arg_name != "_"
              proc_body.gsub!(/([^\w:.]|^)#{obj_arg_name}([^\w:]|$)/, '\1object\2')
            end
            if ctx_arg_name != "_"
              proc_body.gsub!(/([^\w:.]|^)#{ctx_arg_name}([^\w:]|$)/, '\1context\2')
            end

            method_def_indent = " " * (resolve_proc_section.resolve_indent - 2)
            # Turn the proc body into a method body
            method_body = reindent_lines(proc_body, from_indent: resolve_proc_section.resolve_indent + 2, to_indent: resolve_proc_section.resolve_indent)
            # Add `def... end`
            method_def = if input_text.include?("argument ")
              # This field has arguments
              "def #{field_name}(**#{args_arg_name})"
            else
              # No field arguments, so, no method arguments
              "def #{field_name}"
            end
            # Wrap the body in def ... end
            method_body = "\n#{method_def_indent}#{method_def}\n#{method_body}\n#{method_def_indent}end\n"
            # Update Argument access to be underscore and symbols
            # Update `args[...]` and `args.key?`
            method_body = method_body.gsub(/#{args_arg_name}(?<method_begin>\.key\?\(?|\[)["':](?<arg_name>[a-zA-Z0-9_]+)["']?(?<method_end>\]|\))?/) do
              method_begin = $~[:method_begin]
              arg_name = underscorize($~[:arg_name])
              method_end = $~[:method_end]
              "#{args_arg_name}#{method_begin}:#{arg_name}#{method_end}"
            end

            # Replace the resolve proc with the method
            input_text[resolve_proc_section.resolve_start..resolve_proc_section.resolve_end] = ""
            # The replacement above might have left some preceeding whitespace,
            # so remove it by deleting all whitespace chars before `resolve`:
            preceeding_whitespace = resolve_proc_section.resolve_start - 1
            while input_text[preceeding_whitespace] == " " && preceeding_whitespace > 0
              input_text[preceeding_whitespace] = ""
              preceeding_whitespace -= 1
            end
            input_text += method_body
            input_text
          end
        end

        input_text
      end

      class ResolveProcProcessor < Parser::AST::Processor
        attr_reader :resolve_proc_sections
        def initialize
          @resolve_proc_sections = []
        end

        class ResolveProcSection
          attr_accessor :proc_start, :proc_end, :proc_arg_names, :resolve_start, :resolve_end, :resolve_indent
          def initialize
            @proc_arg_names = nil
            @resolve_start = nil
            @resolve_end = nil
            @resolve_indent = nil
            @proc_start = nil
            @proc_end = nil
          end
        end

        def on_send(node)
          receiver, method_name, _args = *node
          if method_name == :resolve && receiver.nil?
            resolve_proc_section = ResolveProcSection.new
            source_exp = node.loc.expression
            resolve_proc_section.resolve_start = source_exp.begin.begin_pos
            resolve_proc_section.resolve_end = source_exp.end.end_pos
            resolve_proc_section.resolve_indent = source_exp.column

            @resolve_proc_sections << resolve_proc_section
          end
          super(node)
        end

        def on_block(node)
          send_node, args_node, body_node = node.children
          _receiver, method_name, _send_args_node = *send_node
          # Assume that the first three-argument proc we enter is the resolve
          if (
            method_name == :lambda && args_node.children.size == 3 &&
            !@resolve_proc_sections.empty? && @resolve_proc_sections[-1].proc_arg_names.nil?
          )
            resolve_proc_section = @resolve_proc_sections[-1]
            source_exp = body_node.loc.expression
            resolve_proc_section.proc_arg_names = args_node.children.map { |arg_node| arg_node.children[0].to_s }
            resolve_proc_section.proc_start = source_exp.begin.begin_pos
            resolve_proc_section.proc_end = source_exp.end.end_pos
          end
          super(node)
        end
      end
    end

    # Transform `interfaces [A, B, C]` to `implements A\nimplements B\nimplements C\n`
    class InterfacesToImplementsTransform < Transform
      PATTERN = /(?<indent>\s*)(?:interfaces) \[\s*(?<interfaces>(?:[a-zA-Z_0-9:\.,\s]+))\]/m
      def apply(input_text)
        input_text.gsub(PATTERN) do
          indent = $~[:indent]
          interfaces = $~[:interfaces].split(',').map(&:strip).reject(&:empty?)
          # Preserve leading newlines before the `interfaces ...`
          # call, but don't re-insert them between `implements` calls.
          extra_leading_newlines = "\n" * (indent[/^\n*/].length - 1)
          indent = indent.sub(/^\n*/m, "")
          interfaces_calls = interfaces
            .map { |interface| "\n#{indent}implements #{interface}" }
            .join
          extra_leading_newlines + interfaces_calls
        end
      end
    end

    # Transform `possible_types [A, B, C]` to `possible_types(A, B, C)`
    class PossibleTypesTransform < Transform
      PATTERN = /(?<indent>\s*)(?:possible_types) \[\s*(?<possible_types>(?:[a-zA-Z_0-9:\.,\s]+))\]/m
      def apply(input_text)
        input_text.gsub(PATTERN) do
          indent = $~[:indent]
          possible_types = $~[:possible_types].split(',').map(&:strip).reject(&:empty?)
          extra_leading_newlines = indent[/^\n*/]
          method_indent = indent.sub(/^\n*/m, "")
          type_indent = "  " + method_indent
          possible_types_call = "#{method_indent}possible_types(\n#{possible_types.map { |t| "#{type_indent}#{t},"}.join("\n")}\n#{method_indent})"
          extra_leading_newlines + trim_lines(possible_types_call)
        end
      end
    end

    class UpdateMethodSignatureTransform < Transform
      def apply(input_text)
        input_text.scan(/(?:input_field|field|return_field|connection|argument) .*$/).each do |field|
          matches = /(?<field_type>input_field|return_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)?(:?, +(?<return_type>([A-Za-z\[\]\.\!_0-9\(\)]|::|-> ?\{ ?| ?\})+))?(?<remainder>( |,|$).*)/.match(field)
          if matches
            name = matches[:name]
            return_type = matches[:return_type]
            remainder = matches[:remainder]
            field_type = matches[:field_type]
            with_block = remainder.gsub!(/\ do$/, '')

            remainder.gsub! /,$/, ''
            remainder.gsub! /^,/, ''
            remainder.chomp!

            if return_type
              non_nullable = return_type.sub! /(^|[^\[])!/, '\1'
              non_nullable ||= return_type.sub! /([^\[])\.to_non_null_type([^\]]|$)/, '\1'
              nullable = !non_nullable
              return_type = normalize_type_expression(return_type)
            else
              non_nullable = nil
              nullable = nil
            end

            input_text.sub!(field) do
              is_argument = ['argument', 'input_field'].include?(field_type)
              f = "#{is_argument ? 'argument' : 'field'} :#{name}"

              if return_type
                f += ", #{return_type}"
              end

              unless remainder.empty?
                f += ',' + remainder
              end

              if is_argument
                if nullable
                  f += ', required: false'
                elsif non_nullable
                  f += ', required: true'
                end
              else
                if nullable
                  f += ', null: true'
                elsif non_nullable
                  f += ', null: false'
                end
              end

              if field_type == 'connection'
                f += ', connection: true'
              end

              if with_block
                f += ' do'
              end

              f
            end
          end
        end

        input_text
      end
    end

    class RemoveEmptyBlocksTransform < Transform
      def apply(input_text)
        input_text.gsub(/\s*do\s*end/m, "")
      end
    end

    # Remove redundant newlines, which may have trailing spaces
    # Remove double newline after `do`
    # Remove double newline before `end`
    # Remove lines with whitespace only
    class RemoveExcessWhitespaceTransform < Transform
      def apply(input_text)
        input_text
          .gsub(/\n{3,}/m, "\n\n")
          .gsub(/do\n{2,}/m, "do\n")
          .gsub(/\n{2,}(\s*)end/m, "\n\\1end")
          .gsub(/\n +\n/m, "\n\n")
      end
    end

    # Skip this file if you see any `field`
    # helpers with `null: true` or `null: false` keywords
    # or `argument` helpers with `required:` keywords,
    # because it's already been transformed
    class SkipOnNullKeyword
      def skip?(input_text)
        input_text =~ /field.*null: (true|false)/ || input_text =~ /argument.*required: (true|false)/
      end
    end

    class Member
      def initialize(member, skip: SkipOnNullKeyword, type_transforms: DEFAULT_TYPE_TRANSFORMS, field_transforms: DEFAULT_FIELD_TRANSFORMS, clean_up_transforms: DEFAULT_CLEAN_UP_TRANSFORMS)
        @member = member
        @skip = skip
        @type_transforms = type_transforms
        @field_transforms = field_transforms
        @clean_up_transforms = clean_up_transforms
      end

      DEFAULT_TYPE_TRANSFORMS = [
        TypeDefineToClassTransform,
        MutationResolveProcToMethodTransform, # Do this before switching to class, so we can detect that its a mutation
        UnderscorizeMutationHashTransform,
        MutationDefineToClassTransform,
        NameTransform,
        InterfacesToImplementsTransform,
        PossibleTypesTransform,
        ProcToClassMethodTransform.new("coerce_input"),
        ProcToClassMethodTransform.new("coerce_result"),
        ProcToClassMethodTransform.new("resolve_type"),
      ]

      DEFAULT_FIELD_TRANSFORMS = [
        RemoveNewlinesTransform,
        RemoveMethodParensTransform,
        PositionalTypeArgTransform,
        ConfigurationToKwargTransform.new(kwarg: "property"),
        ConfigurationToKwargTransform.new(kwarg: "description"),
        ConfigurationToKwargTransform.new(kwarg: "deprecation_reason"),
        ConfigurationToKwargTransform.new(kwarg: "hash_key"),
        PropertyToMethodTransform,
        UnderscoreizeFieldNameTransform,
        ResolveProcToMethodTransform,
        UpdateMethodSignatureTransform,
        RemoveRedundantKwargTransform.new(kwarg: "hash_key"),
        RemoveRedundantKwargTransform.new(kwarg: "method"),
      ]

      DEFAULT_CLEAN_UP_TRANSFORMS = [
        RemoveExcessWhitespaceTransform,
        RemoveEmptyBlocksTransform,
      ]

      def upgrade
        type_source = @member.dup
        should_skip = @skip.new.skip?(type_source)
        # return the unmodified code
        if should_skip
          return type_source
        end
        # Transforms on type defn code:
        type_source = apply_transforms(type_source, @type_transforms)
        # Transforms on each field:
        field_sources = find_fields(type_source)
        field_sources.each do |field_source|
          transformed_field_source = apply_transforms(field_source.dup, @field_transforms)
          # Replace the original source code with the transformed source code:
          type_source = type_source.gsub(field_source, transformed_field_source)
        end
        # Clean-up:
        type_source = apply_transforms(type_source, @clean_up_transforms)
        # Return the transformed source:
        type_source
      end

      def upgradeable?
        return false if @member.include? '< GraphQL::Schema::'
        return false if @member =~ /< Types::Base#{GRAPHQL_TYPES}/

        true
      end

      private

      def apply_transforms(source_code, transforms, idx: 0)
        next_transform = transforms[idx]
        case next_transform
        when nil
          # We got to the end of the list
          source_code
        when Class
          # Apply a class
          next_source_code = next_transform.new.apply(source_code)
          apply_transforms(next_source_code, transforms, idx: idx + 1)
        else
          # Apply an already-initialized object which responds to `apply`
          next_source_code = next_transform.apply(source_code)
          apply_transforms(next_source_code, transforms, idx: idx + 1)
        end
      end

      # Parse the type, find calls to `field` and `connection`
      # Return strings containing those calls
      def find_fields(type_source)
        type_ast = Parser::CurrentRuby.parse(type_source)
        finder = FieldFinder.new
        finder.process(type_ast)
        field_sources = []
        # For each of the locations we found, extract the text for that definition.
        # The text will be transformed independently,
        # then the transformed text will replace the original text.
        FieldFinder::DEFINITION_METHODS.each do |def_method|
          finder.locations[def_method].each do |name, (starting_idx, ending_idx)|
            field_source = type_source[starting_idx..ending_idx]
            field_sources << field_source
          end
        end
        # Here's a crazy thing: the transformation is pure,
        # so definitions like `argument :id, types.ID` can be transformed once
        # then replaced everywhere. So:
        # - make a unique array here
        # - use `gsub` after performing the transformation.
        field_sources.uniq!
        field_sources
      rescue Parser::SyntaxError
        puts "Error Source:"
        puts type_source
        raise
      end

      class FieldFinder < Parser::AST::Processor
        # These methods are definition DSLs which may accept a block,
        # each of these definitions is passed for transformation in its own right.
        # `field` and `connection` take priority. In fact, they upgrade their
        # own arguments, so those upgrades turn out to be no-ops.
        DEFINITION_METHODS = [:field, :connection, :input_field, :return_field, :argument]
        attr_reader :locations

        def initialize
          # Pairs of `{ { method_name => { name => [start, end] } }`,
          # since fields/arguments are unique by name, within their category
          @locations = Hash.new { |h,k| h[k] = {} }
        end

        # @param send_node [node] The node which might be a `field` call, etc
        # @param source_node [node] The node whose source defines the bounds of the definition (eg, the surrounding block)
        def add_location(send_node:,source_node:)
          receiver_node, method_name, *arg_nodes = *send_node
          # Implicit self and one of the recognized methods
          if receiver_node.nil? && DEFINITION_METHODS.include?(method_name)
            name = arg_nodes[0]
            # This field may have already been added because
            # we find `(block ...)` nodes _before_ we find `(send ...)` nodes.
            if @locations[method_name][name].nil?
              starting_idx = source_node.loc.expression.begin.begin_pos
              ending_idx = source_node.loc.expression.end.end_pos
              @locations[method_name][name] = [starting_idx, ending_idx]
            end
          end
        end

        def on_block(node)
          send_node, _args_node, _body_node = *node
          add_location(send_node: send_node, source_node: node)
          super(node)
        end

        def on_send(node)
          add_location(send_node: node, source_node: node)
          super(node)
        end
      end
    end
  end
end