Add "Did you mean?" for unrecognized CLI commands

This commit improves the error message that is displayed when a user
specifies an unrecognized `bin/rails` command by offering "Did you
mean?" suggestions.  The suggestions cover all visible commands, and
supersede any incomplete (Rake-task-only) suggestions that Rake might
display.

__Before (example)__

  ```console
  $ bin/rails credentails:edit
  rails aborted!
  Don't know how to build task 'credentails:edit' (See the list of available tasks with `rails --tasks`)

  (See full trace by running task with --trace)

  $ bin/rails --tasks
  ...
  rails cache_digests:dependencies         # Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)
  rails cache_digests:nested_dependencies  # Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)
  rails db:create                          # Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use ...
  rails db:drop                            # Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db...
  ...

  $ bin/rails credentails -h
  # no output

  $ bin/rails credentials -h
  # no output

  $ bin/rails versoin
  rails aborted!
  Don't know how to build task 'versoin' (See the list of available tasks with `rails --tasks`)
  Did you mean?  db:version

  (See full trace by running task with --trace)
  ```

__After (example)__

  ```console
  $ bin/rails credentails:edit
  Unrecognized command "credentails:edit" (Rails::Command::UnrecognizedCommandError)
  Did you mean?  credentials:edit

  $ bin/rails credentails -h
  Unrecognized command "credentails" (Rails::Command::UnrecognizedCommandError)
  Did you mean?  credentials:diff

  $ bin/rails credentials -h
  Unrecognized command "credentials" (Rails::Command::UnrecognizedCommandError)
  Did you mean?  credentials:diff

  $ bin/rails versoin
  Unrecognized command "versoin" (Rails::Command::UnrecognizedCommandError)
  Did you mean?  version
  ```
This commit is contained in:
Jonathan Hefner 2023-01-30 11:40:05 -06:00
parent 60bc028df8
commit 25301604b4
9 changed files with 58 additions and 45 deletions

View File

@ -37,6 +37,7 @@ ue
unqiue
upto
varius
vershen
vew
wil
wth

View File

@ -14,6 +14,36 @@ module Rails
autoload :Behavior
autoload :Base
class CorrectableNameError < StandardError # :nodoc:
attr_reader :name
def initialize(message, name, alternatives)
@name = name
@alternatives = alternatives
super(message)
end
if !Exception.method_defined?(:detailed_message)
def detailed_message(...)
message
end
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
@ -46,6 +76,9 @@ module Rails
args = ["--describe", full_namespace] if HELP_MAPPINGS.include?(args[0])
find_by_namespace("rake").perform(full_namespace, args, config)
end
rescue UnrecognizedCommandError => error
puts error.detailed_message
exit(1)
ensure
ARGV.replace(original_argv)
end

View File

@ -16,24 +16,6 @@ module Rails
class Error < Thor::Error # :nodoc:
end
class CorrectableError < Error # :nodoc:
attr_reader :key, :options
def initialize(message, key, options)
@key = key
@options = options
super(message)
end
if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable)
include DidYouMean::Correctable
def corrections
@corrections ||= DidYouMean::SpellChecker.new(dictionary: options).correct(key)
end
end
end
include Actions
class_attribute :bin, instance_accessor: false, default: "bin/rails"

View File

@ -12,28 +12,24 @@ module Rails
formatted_rake_tasks
end
def perform(task, args, config, optional: false)
def perform(task, args, config)
require_rake
Rake.with_application do |rake|
rake.init("rails", [task, *args])
rake.load_rakefile
if unrecognized_task = rake.top_level_tasks.find { |task| !rake.lookup(task) }
raise UnrecognizedCommandError.new(unrecognized_task)
end
if Rails.respond_to?(:root)
rake.options.suppress_backtrace_pattern = /\A(?!#{Regexp.quote(Rails.root.to_s)})/
end
rake.standard_exception_handling { rake.top_level } unless optional && !task_exists?(rake, task)
rake.standard_exception_handling { rake.top_level }
end
end
private
def task_exists?(rake, task)
name, _args = rake.parse_task_string(task)
rake[name]
true
rescue Exception
false
end
def rake_tasks
require_rake

View File

@ -271,14 +271,9 @@ module Rails
Run `#{executable} --help` for more options.
MSG
else
error = CorrectableError.new("Could not find server '#{server}'.", server, RACK_SERVERS)
if error.respond_to?(:detailed_message)
formatted_message = error.detailed_message
else
formatted_message = error.message
end
error = CorrectableNameError.new("Could not find server '#{server}'.", server, RACK_SERVERS)
<<~MSG
#{formatted_message}
#{error.detailed_message}
Run `#{executable} --help` for more options.
MSG
end

View File

@ -65,8 +65,10 @@ module Rails
private
def run_prepare_task(args)
if @force_prepare || args.empty?
Rails::Command::RakeCommand.perform("test:prepare", nil, {}, optional: true)
Rails::Command::RakeCommand.perform("test:prepare", [], {})
end
rescue UnrecognizedCommandError => error
raise unless error.name == "test:prepare"
end
end
end

View File

@ -261,16 +261,10 @@ module Rails
run_after_generate_callback if config[:behavior] == :invoke
else
options = sorted_groups.flat_map(&:last)
error = Command::Base::CorrectableError.new("Could not find generator '#{namespace}'.", namespace, options)
if error.respond_to?(:detailed_message)
formatted_message = error.detailed_message
else
formatted_message = error.message
end
error = Command::CorrectableNameError.new("Could not find generator '#{namespace}'.", namespace, options)
puts <<~MSG
#{formatted_message}
#{error.detailed_message}
Run `bin/rails generate --help` for more options.
MSG
end

View File

@ -1235,7 +1235,7 @@ module ApplicationTests
error = assert_raises do
rails "db:migrate:animals" ### Task not defined
end
assert_includes error.message, "See the list of available tasks"
assert_includes error.message, "Unrecognized command"
rails "db:schema:dump"
assert_not File.exist?("db/animals_schema.yml")

View File

@ -16,4 +16,14 @@ class Rails::Command::ApplicationTest < ActiveSupport::TestCase
assert output.include?("The `rails new` command creates a new Rails application with a default
directory structure and configuration at the path you specify.")
end
test "prints helpful error on unrecognized command" do
output = capture(:stdout) do
Rails::Command.invoke("vershen")
rescue SystemExit
end
assert_match %(Unrecognized command "vershen"), output
assert_match "Did you mean? version", output
end
end