diff --git a/CHANGELOG.md b/CHANGELOG.md index d44600e..dc2c5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## HEAD (unreleased) -- Do not count trailing if/unless as a kewyword (https://github.com/zombocom/dead_end/pull/44) +- Annotate NoMethodError in non-production environments (https://github.com/zombocom/dead_end/pull/46) +- Do not count trailing if/unless as a keyword (https://github.com/zombocom/dead_end/pull/44) ## 1.0.2 diff --git a/README.md b/README.md index f2c225c..6ab8477 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DeadEnd -An AI powered library to find syntax errors in your source code: +An error in your code forces you to stop. DeadEnd helps you find those errors to get you back on your way faster. DeadEnd: Unmatched `end` detected @@ -18,7 +18,7 @@ An AI powered library to find syntax errors in your source code: ## Installation in your codebase -To automatically search syntax errors when they happen, add this to your Gemfile: +To automatically annotate errors when they happen, add this to your Gemfile: ```ruby gem 'dead_end' @@ -44,7 +44,7 @@ If you're using rspec add this to your `.rspec` file: ## Install the CLI -To get the CLI and manually search for syntax errors, install the gem: +To get the CLI and manually search for syntax errors (but not automatically annotate them), you can manually install the gem: $ gem install dead_end @@ -54,7 +54,7 @@ This gives you the CLI command `$ dead_end` for more info run `$ dead_end --help - Missing `end`: -``` +```ruby class Dog def bark puts "bark" @@ -68,7 +68,7 @@ end - Unexpected `end` -``` +```ruby class Dog def speak @sounds.each |sound| # Note the missing `do` here @@ -81,6 +81,21 @@ end As well as unmatched `|` and unmatched `}`. These errors can be time consuming to debug because Ruby often only tells you the last line in the file. The command `ruby -wc path/to/file.rb` can narrow it down a little bit, but this library does a better job. +## What other errors does it handle? + +In addition to syntax errors, the NoMethodError is annotated to show the line where the error occured, and the surrounding context: + +``` +scratch.rb:7:in `call': undefined method `upcase' for nil:NilClass (NoMethodError) + + + 1 class Pet + 6 def call +❯ 7 puts "Come here #{@neam.upcase}" + 8 end + 9 end +``` + ## Sounds cool, but why isn't this baked into Ruby directly? I would love to get something like this directly in Ruby, but I first need to prove it's useful. The `did_you_mean` functionality started as a gem that was eventually adopted by a bunch of people and then Ruby core liked it enough that they included it in the source. The goal of this gem is to: @@ -90,7 +105,7 @@ I would love to get something like this directly in Ruby, but I first need to pr ## Artificial Inteligence? -This library uses a goal-seeking algorithm similar to that of a path-finding search. For more information [read the blog post about how it works under the hood](https://schneems.com/2020/12/01/squash-unexpectedend-errors-with-syntaxsearch/). +This library uses a goal-seeking algorithm for syntax error detection similar to that of a path-finding search. For more information [read the blog post about how it works under the hood](https://schneems.com/2020/12/01/squash-unexpectedend-errors-with-syntaxsearch/). ## How does it detect syntax error locations? diff --git a/lib/dead_end/auto.rb b/lib/dead_end/auto.rb index 3ee1905..3ad6263 100644 --- a/lib/dead_end/auto.rb +++ b/lib/dead_end/auto.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true +# require_relative "../dead_end/internals" # Monkey patch kernel to ensure that all `require` calls call the same @@ -49,3 +51,49 @@ def require(path) end end +module DeadEnd + IsProduction = -> { + ENV["RAILS_ENV"] == "production" || ENV["RACK_ENV"] == "production" + } +end + +# Unlike a syntax error, a NoMethodError can occur hundreds or thousands of times and +# chew up CPU and other resources. Since this is primarilly a "development" optimization +# we can attempt to disable this behavior in a production context. +if !DeadEnd::IsProduction.call + class NoMethodError + def to_s + return super if DeadEnd::IsProduction.call + + file, line, _ = backtrace[0].split(":") + return super if !File.exist?(file) + + index = line.to_i - 1 + source = File.read(file) + code_lines = DeadEnd::CodeLine.parse(source) + + block = DeadEnd::CodeBlock.new(lines: code_lines[index]) + lines = DeadEnd::CaptureCodeContext.new( + blocks: block, + code_lines: code_lines + ).call + + message = super.dup + message << $/ + message << $/ + + message << DeadEnd::DisplayCodeWithLineNumbers.new( + lines: lines, + highlight_lines: block.lines, + terminal: self.class.to_tty? + ).call + + message << $/ + message + rescue => e + puts "DeadEnd Internal error: #{e.message}" + puts "DeadEnd Internal backtrace: #{e.backtrace}" + super + end + end +end diff --git a/lib/dead_end/code_line.rb b/lib/dead_end/code_line.rb index 24bc129..449b493 100644 --- a/lib/dead_end/code_line.rb +++ b/lib/dead_end/code_line.rb @@ -31,6 +31,12 @@ module DeadEnd class CodeLine TRAILING_SLASH = ("\\" + $/).freeze + def self.parse(source) + source.lines.map.with_index do |line, index| + CodeLine.new(line: line, index: index) + end + end + attr_reader :line, :index, :indent, :original_line def initialize(line: , index:) diff --git a/spec/integration/ruby_command_line_spec.rb b/spec/integration/ruby_command_line_spec.rb index ba71d02..0164a73 100644 --- a/spec/integration/ruby_command_line_spec.rb +++ b/spec/integration/ruby_command_line_spec.rb @@ -4,6 +4,52 @@ module DeadEnd RSpec.describe "Requires with ruby cli" do + it "annotates NoMethodError" do + Dir.mktmpdir do |dir| + @tmpdir = Pathname(dir) + @script = @tmpdir.join("script.rb") + @script.write <<~'EOM' + class Pet + def initialize + @name = "cinco" + end + + def call + puts "Come here #{@neam.upcase}" + end + end + + Pet.new.call + EOM + + out = `ruby -I#{lib_dir} -rdead_end/auto #{@script} 2>&1` + + error_line = <<~'EOM' + ❯ 7 puts "Come here #{@neam.upcase}" + EOM + + expect(out).to include("NoMethodError") + expect(out).to include(error_line) + expect(out).to include(<<~'EOM') + 1 class Pet + 6 def call + ❯ 7 puts "Come here #{@neam.upcase}" + 8 end + 9 end + EOM + expect($?.success?).to be_falsey + + # Test production check + out = `RAILS_ENV=production ruby -I#{lib_dir} -rdead_end/auto #{@script} 2>&1` + expect(out).to include("NoMethodError") + expect(out).to_not include(error_line) + + out = `RACK_ENV=production ruby -I#{lib_dir} -rdead_end/auto #{@script} 2>&1` + expect(out).to include("NoMethodError") + expect(out).to_not include(error_line) + end + end + it "detects require error and adds a message with auto mode" do Dir.mktmpdir do |dir| @tmpdir = Pathname(dir)