src/railties/lib/rails/command.rb
# frozen_string_literal: true

require "active_support"
require "active_support/rails"
require "active_support/core_ext/enumerable"
require "rails/deprecator"

require "thor"

module Rails
  module Command
    extend ActiveSupport::Autoload

    autoload :Behavior
    autoload :Base

    class CorrectableNameError < StandardError # :nodoc:
      attr_reader :name

      def initialize(message, name, alternatives)
        @name = name
        @alternatives = alternatives
        super(message)
      end

      if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
        include DidYouMean::Correctable

        def corrections
          @corrections ||= DidYouMean::SpellChecker.new(dictionary: @alternatives).correct(name)
        end
      end
    end

    class UnrecognizedCommandError < CorrectableNameError # :nodoc:
      def initialize(name)
        super("Unrecognized command #{name.inspect}", name, Command.printing_commands.map(&:first))
      end
    end

    include Behavior

    HELP_MAPPINGS = %w(-h -? --help).to_set
    VERSION_MAPPINGS = %w(-v --version).to_set

    class << self
      def hidden_commands # :nodoc:
        @hidden_commands ||= []
      end

      def environment # :nodoc:
        ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development"
      end

      # Receives a namespace, arguments, and the behavior to invoke the command.
      def invoke(full_namespace, args = [], **config)
        args = ["--help"] if rails_new_with_no_path?(args)

        full_namespace = full_namespace.to_s
        namespace, command_name = split_namespace(full_namespace)
        command = find_by_namespace(namespace, command_name)

        with_argv(args) do
          if command && command.all_commands[command_name]
            command.perform(command_name, args, config)
          else
            invoke_rake(full_namespace, args, config)
          end
        end
      rescue UnrecognizedCommandError => error
        if error.name == full_namespace && command && command_name == full_namespace
          command.perform("help", [], config)
        else
          puts error.detailed_message
        end
        exit(1)
      end

      # Rails finds namespaces similar to Thor, it only adds one rule:
      #
      # Command names must end with "_command.rb". This is required because Rails
      # looks in load paths and loads the command just before it's going to be used.
      #
      #   find_by_namespace :webrat, :integration
      #
      # Will search for the following commands:
      #
      #   "webrat", "webrat:integration", "rails:webrat", "rails:webrat:integration"
      #
      def find_by_namespace(namespace, command_name = nil) # :nodoc:
        lookups = [ namespace ]
        lookups << "#{namespace}:#{command_name}" if command_name
        lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

        lookup(lookups)

        namespaces = subclasses.index_by(&:namespace)
        namespaces[(lookups & namespaces.keys).first]
      end

      # Returns the root of the \Rails engine or app running the command.
      def root
        if defined?(ENGINE_ROOT)
          Pathname.new(ENGINE_ROOT)
        else
          application_root
        end
      end

      def application_root # :nodoc:
        Pathname.new(File.expand_path("../..", APP_PATH)) if defined?(APP_PATH)
      end

      def printing_commands # :nodoc:
        lookup!

        (subclasses - hidden_commands).flat_map(&:printing_commands)
      end

      private
        def rails_new_with_no_path?(args)
          args == ["new"]
        end

        def split_namespace(namespace)
          case namespace
          when /^(.+):(\w+)$/
            [$1, $2]
          when ""
            ["help", "help"]
          when HELP_MAPPINGS, "help"
            ["help", "help_extended"]
          when VERSION_MAPPINGS
            ["version", "version"]
          else
            [namespace, namespace]
          end
        end

        def with_argv(argv)
          original_argv = ARGV.dup
          ARGV.replace(argv)
          yield
        ensure
          ARGV.replace(original_argv)
        end

        def invoke_rake(task, args, config)
          args = ["--describe", task] if HELP_MAPPINGS.include?(args[0])
          find_by_namespace("rake").perform(task, args, config)
        end

        def command_type # :doc:
          @command_type ||= "command"
        end

        def lookup_paths # :doc:
          @lookup_paths ||= %w( rails/commands commands )
        end

        def file_lookup_paths # :doc:
          @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_command.rb" ]
        end
    end
  end
end
