diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af0cff..78dc793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## HEAD (unreleased) +- [Breaking] Remove previously deprecated `require "dead_end/fyi"` interface (https://github.com/zombocom/dead_end/pull/94) +- DeadEnd is now fired on EVERY syntax error (https://github.com/zombocom/dead_end/pull/94) +- Output format changes + - The "banner" is removed in favor of original parse error messages (https://github.com/zombocom/dead_end/pull/94) + - Parse errors emitted per-block rather than for the whole document (https://github.com/zombocom/dead_end/pull/94) + ## 2.0.2 - Don't print terminal color codes when output is not tty (https://github.com/zombocom/dead_end/pull/91) diff --git a/lib/dead_end.rb b/lib/dead_end.rb index cd55c53..b30aa4a 100644 --- a/lib/dead_end.rb +++ b/lib/dead_end.rb @@ -1,4 +1,144 @@ # frozen_string_literal: true -require_relative "dead_end/internals" +require_relative "dead_end/version" + +require "tmpdir" +require "stringio" +require "pathname" +require "ripper" +require "timeout" + +module DeadEnd + # Used to indicate a default value that cannot + # be confused with another input + DEFAULT_VALUE = Object.new.freeze + + class Error < StandardError; end + TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i + + def self.handle_error(e) + filename = e.message.split(":").first + $stderr.sync = true + + call( + source: Pathname(filename).read, + filename: filename + ) + + raise e + end + + def self.call(source:, filename:, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr) + search = nil + Timeout.timeout(timeout) do + record_dir ||= ENV["DEBUG"] ? "tmp" : nil + search = CodeSearch.new(source, record_dir: record_dir).call + end + + blocks = search.invalid_blocks + DisplayInvalidBlocks.new( + io: io, + blocks: blocks, + filename: filename, + terminal: terminal, + code_lines: search.code_lines + ).call + rescue Timeout::Error => e + io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info" + io.puts e.backtrace.first(3).join($/) + end + + # Used for counting spaces + module SpaceCount + def self.indent(string) + string.split(/\S/).first&.length || 0 + end + end + + # This will tell you if the `code_lines` would be valid + # if you removed the `without_lines`. In short it's a + # way to detect if we've found the lines with syntax errors + # in our document yet. + # + # code_lines = [ + # CodeLine.new(line: "def foo\n", index: 0) + # CodeLine.new(line: " def bar\n", index: 1) + # CodeLine.new(line: "end\n", index: 2) + # ] + # + # DeadEnd.valid_without?( + # without_lines: code_lines[1], + # code_lines: code_lines + # ) # => true + # + # DeadEnd.valid?(code_lines) # => false + def self.valid_without?(without_lines:, code_lines:) + lines = code_lines - Array(without_lines).flatten + + if lines.empty? + true + else + valid?(lines) + end + end + + def self.invalid?(source) + source = source.join if source.is_a?(Array) + source = source.to_s + + Ripper.new(source).tap(&:parse).error? + end + + # Returns truthy if a given input source is valid syntax + # + # DeadEnd.valid?(<<~EOM) # => true + # def foo + # end + # EOM + # + # DeadEnd.valid?(<<~EOM) # => false + # def foo + # def bar # Syntax error here + # end + # EOM + # + # You can also pass in an array of lines and they'll be + # joined before evaluating + # + # DeadEnd.valid?( + # [ + # "def foo\n", + # "end\n" + # ] + # ) # => true + # + # DeadEnd.valid?( + # [ + # "def foo\n", + # " def bar\n", # Syntax error here + # "end\n" + # ] + # ) # => false + # + # As an FYI the CodeLine class instances respond to `to_s` + # so passing a CodeLine in as an object or as an array + # will convert it to it's code representation. + def self.valid?(source) + !invalid?(source) + end +end + +require_relative "dead_end/code_line" +require_relative "dead_end/code_block" +require_relative "dead_end/code_search" +require_relative "dead_end/code_frontier" +require_relative "dead_end/clean_document" + +require_relative "dead_end/lex_all" +require_relative "dead_end/block_expand" +require_relative "dead_end/around_block_scan" +require_relative "dead_end/ripper_errors" +require_relative "dead_end/display_invalid_blocks" +require_relative "dead_end/parse_blocks_from_indent_line" + require_relative "dead_end/auto" diff --git a/lib/dead_end/auto.rb b/lib/dead_end/auto.rb index 00c113c..c99059b 100644 --- a/lib/dead_end/auto.rb +++ b/lib/dead_end/auto.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "../dead_end/internals" +require_relative "../dead_end" # Monkey patch kernel to ensure that all `require` calls call the same # method diff --git a/lib/dead_end/banner.rb b/lib/dead_end/banner.rb deleted file mode 100644 index b04ad35..0000000 --- a/lib/dead_end/banner.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module DeadEnd - class Banner - attr_reader :invalid_obj - - def initialize(invalid_obj:) - @invalid_obj = invalid_obj - end - - def call - case invalid_obj.error_symbol - when :missing_end - <<~EOM - DeadEnd: Missing `end` detected - - This code has a missing `end`. Ensure that all - syntax keywords (`def`, `do`, etc.) have a matching `end`. - EOM - when :unmatched_syntax - case unmatched_symbol - when :end - <<~EOM - DeadEnd: Unmatched `end` detected - - This code has an unmatched `end`. Ensure that all `end` lines - in your code have a matching syntax keyword (`def`, `do`, etc.) - and that you don't have any extra `end` lines. - EOM - when :| - <<~EOM - DeadEnd: Unmatched `|` character detected - - Example: - - `do |x` should be `do |x|` - EOM - when *WhoDisSyntaxError::CHARACTERS.keys - <<~EOM - DeadEnd: Unmatched `#{unmatched_symbol}` character detected - - It appears a `#{missing_character}` might be missing. - EOM - else - "DeadEnd: Unmatched `#{unmatched_symbol}` detected" - end - end - end - - private def unmatched_symbol - invalid_obj.unmatched_symbol - end - - private def missing_character - WhoDisSyntaxError::CHARACTERS[unmatched_symbol] - end - end -end diff --git a/lib/dead_end/code_search.rb b/lib/dead_end/code_search.rb index 79818f6..400cf32 100644 --- a/lib/dead_end/code_search.rb +++ b/lib/dead_end/code_search.rb @@ -73,12 +73,13 @@ def record(block:, name: "record") puts " block indent: #{block.current_indent}" end @record_dir.join(filename).open(mode: "a") do |f| - display = DisplayInvalidBlocks.new( - blocks: block, + document = DisplayCodeWithLineNumbers.new( + lines: @code_lines.select(&:visible?), terminal: false, - code_lines: @code_lines - ) - f.write(display.indent(display.code_with_lines)) + highlight_lines: block.lines + ).call + + f.write(document) end end diff --git a/lib/dead_end/display_invalid_blocks.rb b/lib/dead_end/display_invalid_blocks.rb index c0b0959..c840f63 100644 --- a/lib/dead_end/display_invalid_blocks.rb +++ b/lib/dead_end/display_invalid_blocks.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_relative "banner" require_relative "capture_code_context" require_relative "display_code_with_line_numbers" @@ -9,18 +8,13 @@ module DeadEnd class DisplayInvalidBlocks attr_reader :filename - def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: DEFAULT_VALUE, invalid_obj: WhoDisSyntaxError::Null.new) - @terminal = terminal == DEFAULT_VALUE ? io.isatty : terminal - - @filename = filename + def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: DEFAULT_VALUE) @io = io - @blocks = Array(blocks) - - @invalid_lines = @blocks.map(&:lines).flatten + @filename = filename @code_lines = code_lines - @invalid_obj = invalid_obj + @terminal = terminal == DEFAULT_VALUE ? io.isatty : terminal end def document_ok? @@ -30,46 +24,43 @@ def document_ok? def call if document_ok? @io.puts "Syntax OK" - else - found_invalid_blocks + return self end - self - end - - private def no_invalid_blocks - @io.puts <<~EOM - EOM - end - private def found_invalid_blocks + @io.puts("--> #{filename}") if filename @io.puts - if banner - @io.puts banner - @io.puts + @blocks.each do |block| + display_block(block) end - @io.puts("file: #{filename}") if filename - @io.puts <<~EOM - simplified: - #{indent(code_block)} - EOM + self end - def banner - Banner.new(invalid_obj: @invalid_obj).call - end + private def display_block(block) + lines = CaptureCodeContext.new( + blocks: block, + code_lines: @code_lines + ).call + + document = DisplayCodeWithLineNumbers.new( + lines: lines, + terminal: @terminal, + highlight_lines: block.lines + ).call + + RipperErrors.new(block.lines.map(&:original).join).call.errors.each do |e| + @io.puts e + end + @io.puts - def indent(string, with: " ") - string.each_line.map { |l| with + l }.join + @io.puts(document) end - def code_block - string = +"" - string << code_with_context - string + private def banner + Banner.new(invalid_obj: @invalid_obj).call end - def code_with_context + private def code_with_context lines = CaptureCodeContext.new( blocks: @blocks, code_lines: @code_lines @@ -81,13 +72,5 @@ def code_with_context highlight_lines: @invalid_lines ).call end - - def code_with_lines - DisplayCodeWithLineNumbers.new( - lines: @code_lines.select(&:visible?), - terminal: @terminal, - highlight_lines: @invalid_lines - ).call - end end end diff --git a/lib/dead_end/fyi.rb b/lib/dead_end/fyi.rb deleted file mode 100644 index 57b08da..0000000 --- a/lib/dead_end/fyi.rb +++ /dev/null @@ -1,8 +0,0 @@ -require_relative "../dead_end/internals" - -require_relative "auto" - -DeadEnd.send(:remove_const, :SEARCH_SOURCE_ON_ERROR_DEFAULT) -DeadEnd::SEARCH_SOURCE_ON_ERROR_DEFAULT = false - -warn "DEPRECATED: calling `require 'dead_end/fyi'` is deprecated, `require 'dead_end'` instead" diff --git a/lib/dead_end/internals.rb b/lib/dead_end/internals.rb deleted file mode 100644 index 9dc4a76..0000000 --- a/lib/dead_end/internals.rb +++ /dev/null @@ -1,157 +0,0 @@ -# frozen_string_literal: true - -# This is the top level file, but is moved to `internals` -# so the top level require can instead enable the "automatic" behavior - -require_relative "version" - -require "tmpdir" -require "stringio" -require "pathname" -require "ripper" -require "timeout" - -module DeadEnd - # Used to indicate a default value that cannot - # be confused with another input - DEFAULT_VALUE = Object.new.freeze - - class Error < StandardError; end - SEARCH_SOURCE_ON_ERROR_DEFAULT = true - TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i - - def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT) - raise e unless e.message.include?("end-of-input") - - filename = e.message.split(":").first - - $stderr.sync = true - warn "Run `$ dead_end #{filename}` for more options\n" - - if search_source_on_error - call( - source: Pathname(filename).read, - filename: filename - ) - end - - raise e - end - - def self.call(source:, filename:, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr) - search = nil - Timeout.timeout(timeout) do - record_dir ||= ENV["DEBUG"] ? "tmp" : nil - search = CodeSearch.new(source, record_dir: record_dir).call - end - - blocks = search.invalid_blocks - DisplayInvalidBlocks.new( - blocks: blocks, - filename: filename, - terminal: terminal, - code_lines: search.code_lines, - invalid_obj: invalid_type(source), - io: io - ).call - rescue Timeout::Error => e - io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info" - io.puts e.backtrace.first(3).join($/) - end - - # Used for counting spaces - module SpaceCount - def self.indent(string) - string.split(/\S/).first&.length || 0 - end - end - - # This will tell you if the `code_lines` would be valid - # if you removed the `without_lines`. In short it's a - # way to detect if we've found the lines with syntax errors - # in our document yet. - # - # code_lines = [ - # CodeLine.new(line: "def foo\n", index: 0) - # CodeLine.new(line: " def bar\n", index: 1) - # CodeLine.new(line: "end\n", index: 2) - # ] - # - # DeadEnd.valid_without?( - # without_lines: code_lines[1], - # code_lines: code_lines - # ) # => true - # - # DeadEnd.valid?(code_lines) # => false - def self.valid_without?(without_lines:, code_lines:) - lines = code_lines - Array(without_lines).flatten - - if lines.empty? - true - else - valid?(lines) - end - end - - def self.invalid?(source) - source = source.join if source.is_a?(Array) - source = source.to_s - - Ripper.new(source).tap(&:parse).error? - end - - # Returns truthy if a given input source is valid syntax - # - # DeadEnd.valid?(<<~EOM) # => true - # def foo - # end - # EOM - # - # DeadEnd.valid?(<<~EOM) # => false - # def foo - # def bar # Syntax error here - # end - # EOM - # - # You can also pass in an array of lines and they'll be - # joined before evaluating - # - # DeadEnd.valid?( - # [ - # "def foo\n", - # "end\n" - # ] - # ) # => true - # - # DeadEnd.valid?( - # [ - # "def foo\n", - # " def bar\n", # Syntax error here - # "end\n" - # ] - # ) # => false - # - # As an FYI the CodeLine class instances respond to `to_s` - # so passing a CodeLine in as an object or as an array - # will convert it to it's code representation. - def self.valid?(source) - !invalid?(source) - end - - def self.invalid_type(source) - WhoDisSyntaxError.new(source).call - end -end - -require_relative "code_line" -require_relative "code_block" -require_relative "code_search" -require_relative "code_frontier" -require_relative "clean_document" - -require_relative "lex_all" -require_relative "block_expand" -require_relative "around_block_scan" -require_relative "who_dis_syntax_error" -require_relative "display_invalid_blocks" -require_relative "parse_blocks_from_indent_line" diff --git a/lib/dead_end/ripper_errors.rb b/lib/dead_end/ripper_errors.rb new file mode 100644 index 0000000..c8d7c42 --- /dev/null +++ b/lib/dead_end/ripper_errors.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module DeadEnd + # Capture parse errors from ripper + # + # Example: + # + # puts RipperErrors.new(" def foo").call.errors + # # => ["syntax error, unexpected end-of-input, expecting ';' or '\\n'"] + class RipperErrors < Ripper + attr_reader :errors + + # Comes from ripper, called + # on every parse error, msg + # is a string + def on_parse_error(msg) + @errors ||= [] + @errors << msg + end + + def call + @run_once ||= begin + @errors = [] + parse + true + end + self + end + end +end diff --git a/lib/dead_end/who_dis_syntax_error.rb b/lib/dead_end/who_dis_syntax_error.rb deleted file mode 100644 index cd43c49..0000000 --- a/lib/dead_end/who_dis_syntax_error.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -module DeadEnd - # Determines what type of syntax error that is in the source - # - # Example: - # - # puts WhoDisSyntaxError.new("def foo;").call.error_symbol - # # => :missing_end - class WhoDisSyntaxError < Ripper - CHARACTERS = {"{": :"}", "}": :"{", "[": :"]", "]": :"[", "(": :")", ")": :"("} - class Null - def error_symbol - :missing_end - end - - def unmatched_symbol - :end - end - end - attr_reader :error, :run_once - - # Return options: - # - :missing_end - # - :unmatched_syntax - # - :unknown - def error_symbol - call - @error_symbol - end - - # Return options: - # - :end - # - :| - # - :} - # - :unknown - def unmatched_symbol - call - @unmatched_symbol - end - - def call - @run_once ||= begin - parse - true - end - self - end - - def on_parse_error(msg) - return if @error_symbol && @unmatched_symbol - - @error = msg - @unmatched_symbol = :unknown - - case @error - when /unexpected end-of-input/ - @error_symbol = :missing_end - when /expecting end-of-input/ - @unmatched_symbol = :end - @error_symbol = :unmatched_syntax - when /unexpected .* expecting ['`]?(?[^']*)/ - if $1 - character = $1.to_sym - @unmatched_symbol = CHARACTERS[character] || character - @unmatched_symbol = :end if @unmatched_symbol == :keyword_end - end - @error_symbol = :unmatched_syntax - when /unexpected '(?.*)'/ - @unmatched_symbol = $1.to_sym - @unmatched_symbol = :end if @unmatched_symbol == :keyword_end - @error_symbol = :unmatched_syntax - when /unexpected `end'/, # Ruby 2.7 and 3.0 - /unexpected end/, # Ruby 2.6 - /unexpected keyword_end/i # Ruby 2.5 - - @error_symbol = :unmatched_syntax - else - @error_symbol = :unknown - end - end - end -end diff --git a/spec/integration/exe_cli_spec.rb b/spec/integration/exe_cli_spec.rb index 7e26d8f..a6a891f 100644 --- a/spec/integration/exe_cli_spec.rb +++ b/spec/integration/exe_cli_spec.rb @@ -25,9 +25,7 @@ def exe(cmd) ruby_file = fixtures_dir.join("this_project_extra_def.rb.txt") out = exe(ruby_file) - expect(out.strip).to include("Missing `end` detected") expect(out.strip).to include("❯ 36 def filename") - expect($?.success?).to be_falsey end @@ -40,7 +38,7 @@ def exe(cmd) out = exe(file.path) - expect(out).to include(<<~EOM.indent(4)) + expect(out).to include(<<~EOM) 16 class Rexe ❯ 77 class Lookups ❯ 78 def input_modes @@ -57,7 +55,6 @@ def exe(cmd) ruby_file = fixtures_dir.join("this_project_extra_def.rb.txt") out = exe("#{ruby_file} --terminal") - expect(out.strip).to include("Missing `end` detected") expect(out.strip).to include("\e[0m❯ 36 \e[1;3m def filename") end @@ -70,9 +67,8 @@ def exe(cmd) expect(tmp_dir).to be_empty - out = exe("#{ruby_file} --record #{tmp_dir}") + exe("#{ruby_file} --record #{tmp_dir}") - expect(out.strip).to include("Unmatched `end` detected") expect(tmp_dir).to_not be_empty end end diff --git a/spec/integration/improvement_regression_spec.rb b/spec/integration/improvement_regression_spec.rb index 07423b5..fc7c796 100644 --- a/spec/integration/improvement_regression_spec.rb +++ b/spec/integration/improvement_regression_spec.rb @@ -14,7 +14,7 @@ module DeadEnd filename: "none" ) - expect(io.string).to include(<<~'EOM'.indent(4)) + expect(io.string).to include(<<~'EOM') 1 Rails.application.routes.draw do ❯ 113 namespace :admin do ❯ 116 match "/foobar(*path)", via: :all, to: redirect { |_params, req| @@ -33,7 +33,7 @@ module DeadEnd filename: "none" ) - expect(io.string).to include(<<~'EOM'.indent(4)) + expect(io.string).to include(<<~'EOM') 1 describe "webmock tests" do 22 it "body" do 27 query = Cutlass::FunctionQuery.new( @@ -55,7 +55,7 @@ module DeadEnd filename: "none" ) - expect(io.string).to include(<<~'EOM'.indent(4)) + expect(io.string).to include(<<~'EOM') 5 module DerailedBenchmarks 6 class RequireTree 7 REQUIRED_BY = {} diff --git a/spec/integration/ruby_command_line_spec.rb b/spec/integration/ruby_command_line_spec.rb index 429f017..f79a048 100644 --- a/spec/integration/ruby_command_line_spec.rb +++ b/spec/integration/ruby_command_line_spec.rb @@ -50,46 +50,8 @@ module DeadEnd require_relative "./script.rb" EOM - out = `ruby -I#{lib_dir} -rdead_end/auto #{require_rb} 2>&1` + `ruby -I#{lib_dir} -rdead_end #{require_rb} 2>&1` - expect(out).to include("Unmatched `end` detected") - expect(out).to include("Run `$ dead_end") - expect($?.success?).to be_falsey - - out = `ruby -I#{lib_dir} -rdead_end #{require_rb} 2>&1` - - expect(out).to include("Unmatched `end` detected") - expect(out).to include("Run `$ dead_end") - expect($?.success?).to be_falsey - end - end - - it "detects require error and adds a message with fyi mode" do - Dir.mktmpdir do |dir| - @tmpdir = Pathname(dir) - @script = @tmpdir.join("script.rb") - @script.write <<~EOM - describe "things" do - it "blerg" do - end - - it "flerg" - end - - it "zlerg" do - end - end - EOM - - require_rb = @tmpdir.join("require.rb") - require_rb.write <<~EOM - require_relative "./script.rb" - EOM - - out = `ruby -I#{lib_dir} -rdead_end/fyi #{require_rb} 2>&1` - - expect(out).to_not include("This code has an unmatched") - expect(out).to include("Run `$ dead_end") expect($?.success?).to be_falsey end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c63a84c..39f3b9e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "bundler/setup" -require "dead_end/internals" # Don't auto load code to +require "dead_end" require "tempfile" @@ -56,8 +56,4 @@ def indent(number) end end.join end - - def strip_control_codes - gsub(/\e\[[^\x40-\x7E]*[\x40-\x7E]/, "") - end end diff --git a/spec/unit/banner_spec.rb b/spec/unit/banner_spec.rb deleted file mode 100644 index 86f86a5..0000000 --- a/spec/unit/banner_spec.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -require_relative "../spec_helper" - -module DeadEnd - RSpec.describe Banner do - it "Unmatched | banner" do - source = <<~EOM - Foo.call do | - end - EOM - - invalid_obj = WhoDisSyntaxError.new(source) - banner = Banner.new(invalid_obj: invalid_obj) - expect(banner.call).to include("Unmatched `|` character detected") - end - - it "Unmatched { banner" do - source = <<~EOM - class Cat - lol = { - end - EOM - - invalid_obj = WhoDisSyntaxError.new(source) - banner = Banner.new(invalid_obj: invalid_obj) - expect(banner.call).to include("Unmatched `{` character detected") - end - - it "Unmatched } banner" do - skip("Unsupported ruby version") unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7") - - source = <<~EOM - def foo - lol = } - end - EOM - - invalid_obj = WhoDisSyntaxError.new(source) - banner = Banner.new(invalid_obj: invalid_obj) - expect(banner.call).to include("Unmatched `}` character detected") - end - - it "Unmatched [ banner" do - source = <<~EOM - class Cat - lol = [ - end - EOM - - invalid_obj = WhoDisSyntaxError.new(source) - banner = Banner.new(invalid_obj: invalid_obj) - expect(banner.call).to include("Unmatched `[` character detected") - end - - it "Unmatched ] banner" do - source = <<~EOM - def foo - lol = ] - end - EOM - - invalid_obj = WhoDisSyntaxError.new(source) - banner = Banner.new(invalid_obj: invalid_obj) - expect(banner.call).to include("Unmatched `]` character detected") - end - - it "Unmatched end banner" do - source = <<~EOM - class Cat - end - end - EOM - - invalid_obj = WhoDisSyntaxError.new(source) - banner = Banner.new(invalid_obj: invalid_obj) - expect(banner.call).to include("DeadEnd: Unmatched `end` detected") - end - - it "Unmatched unknown banner" do - source = <<~EOM - class Cat - def meow - 1 * - end - end - EOM - - invalid_obj = WhoDisSyntaxError.new(source) - banner = Banner.new(invalid_obj: invalid_obj) - expect(banner.call).to include("DeadEnd: Unmatched `unknown` detected") - end - - it "missing end banner" do - source = <<~EOM - class Cat - def meow - end - EOM - - invalid_obj = WhoDisSyntaxError.new(source) - banner = Banner.new(invalid_obj: invalid_obj) - expect(banner.call).to include("DeadEnd: Missing `end` detected") - end - - it "naked (closing) parenthesis" do - invalid_obj = WhoDisSyntaxError.new("def initialize; ); end").call - - expect( - Banner.new(invalid_obj: invalid_obj).call - ).to include("Unmatched `)` character detected") - end - - it "naked (opening) parenthesis" do - invalid_obj = WhoDisSyntaxError.new("def initialize; (; end").call - - expect( - Banner.new(invalid_obj: invalid_obj).call - ).to include("Unmatched `(` character detected") - end - end -end diff --git a/spec/unit/code_search_spec.rb b/spec/unit/code_search_spec.rb index 2341aed..f3d391a 100644 --- a/spec/unit/code_search_spec.rb +++ b/spec/unit/code_search_spec.rb @@ -42,8 +42,8 @@ class Foo EOM end - it "handles no spaces between blocks" do - search = CodeSearch.new(<<~'EOM') + it "handles no spaces between blocks and trailing slash" do + source = <<~'EOM' require "rails_helper" RSpec.describe Foo, type: :model do describe "#bar" do @@ -64,13 +64,14 @@ class Foo end EOM + search = CodeSearch.new(source) search.call expect(search.invalid_blocks.join.strip).to eq('it "returns true" do # <== HERE') end it "handles no spaces between blocks" do - search = CodeSearch.new(<<~EOM) + source = <<~EOM context "foo bar" do it "bars the foo" do travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do @@ -81,13 +82,13 @@ class Foo it "should" do end EOM - + search = CodeSearch.new(source) search.call expect(search.invalid_blocks.join.strip).to eq('it "should" do') end - it "recording" do + it "records debugging steps to a directory" do Dir.mktmpdir do |dir| dir = Pathname(dir) search = CodeSearch.new(<<~EOM, record_dir: dir) @@ -100,7 +101,7 @@ def hai search.call expect(search.record_dir.entries.map(&:to_s)).to include("1-add-1.txt") - expect(search.record_dir.join("1-add-1.txt").read).to eq(<<~EOM.indent(4)) + expect(search.record_dir.join("1-add-1.txt").read).to eq(<<~EOM) 1 class OH 2 def hello ❯ 3 def hai @@ -154,20 +155,15 @@ def hello it "finds hanging def in this project" do source_string = fixtures_dir.join("this_project_extra_def.rb.txt").read search = CodeSearch.new(source_string) - search.call - blocks = search.invalid_blocks - io = StringIO.new - display = DisplayInvalidBlocks.new( - code_lines: search.code_lines, - blocks: blocks, - io: io - ) - display.call - # puts io.string - - expect(display.code_with_lines.strip_control_codes).to include(<<~EOM) + document = DisplayCodeWithLineNumbers.new( + lines: search.code_lines.select(&:visible?), + terminal: false, + highlight_lines: search.invalid_blocks.flat_map(&:lines) + ).call + + expect(document).to include(<<~EOM) ❯ 36 def filename EOM end @@ -208,18 +204,13 @@ def hello EOM search.call - blocks = search.invalid_blocks - io = StringIO.new - display = DisplayInvalidBlocks.new( - io: io, - blocks: blocks, - code_lines: search.code_lines, - filename: "fake/spec/lol.rb" - ) - display.call - # io.string - - expect(display.code_with_lines).to include(<<~EOM) + document = DisplayCodeWithLineNumbers.new( + lines: search.code_lines.select(&:visible?), + terminal: false, + highlight_lines: search.invalid_blocks.flat_map(&:lines) + ).call + + expect(document).to include(<<~EOM) 1 require 'rails_helper' 2 3 RSpec.describe AclassNameHere, type: :worker do diff --git a/spec/unit/display_invalid_blocks_spec.rb b/spec/unit/display_invalid_blocks_spec.rb index 96da06b..887f30e 100644 --- a/spec/unit/display_invalid_blocks_spec.rb +++ b/spec/unit/display_invalid_blocks_spec.rb @@ -16,6 +16,7 @@ def hai search = CodeSearch.new(syntax_string) search.call + io = StringIO.new display = DisplayInvalidBlocks.new( io: io, @@ -92,7 +93,6 @@ def hai ) display.call expect(io.string).to include("❯ 2 def hello") - expect(io.string).to include("DeadEnd") end it " wraps code with github style codeblocks" do @@ -107,12 +107,14 @@ def hai code_lines = CleanDocument.new(source: source).call.lines block = CodeBlock.new(lines: code_lines[1]) - display = DisplayInvalidBlocks.new( + io = StringIO.new + DisplayInvalidBlocks.new( + io: io, blocks: block, terminal: false, code_lines: code_lines - ) - expect(display.code_block).to eq(<<~EOM) + ).call + expect(io.string).to include(<<~EOM) 1 class OH ❯ 2 def hello 4 def hai @@ -130,36 +132,36 @@ def hai end EOM + io = StringIO.new block = CodeBlock.new(lines: code_lines[1]) - display = DisplayInvalidBlocks.new( + DisplayInvalidBlocks.new( + io: io, blocks: block, terminal: false, code_lines: code_lines - ) + ).call - expect(display.code_with_lines).to eq( - [ - " 1 class OH", - "❯ 2 def hello", - " 3 def hai", - " 4 end", - " 5 end", - "" - ].join($/) - ) + expect(io.string).to include([ + " 1 class OH", + "❯ 2 def hello", + " 4 end", + " 5 end", + "" + ].join($/)) block = CodeBlock.new(lines: code_lines[1]) - display = DisplayInvalidBlocks.new( + io = StringIO.new + DisplayInvalidBlocks.new( + io: io, blocks: block, terminal: true, code_lines: code_lines - ) + ).call - expect(display.code_with_lines).to eq( + expect(io.string).to include( [ " 1 class OH", ["❯ 2 ", DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT, " def hello"].join, - " 3 def hai", " 4 end", " 5 end", "" diff --git a/spec/unit/who_dis_syntax_error_spec.rb b/spec/unit/who_dis_syntax_error_spec.rb deleted file mode 100644 index cdfb41c..0000000 --- a/spec/unit/who_dis_syntax_error_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require_relative "../spec_helper" - -module DeadEnd - RSpec.describe WhoDisSyntaxError do - context "determines the type of syntax error to be an unmatched end" do - it "with missing or extra end's" do - expect( - WhoDisSyntaxError.new("def foo;").call.error_symbol - ).to eq(:missing_end) - - expect( - WhoDisSyntaxError.new("def foo; end; end").call.error_symbol - ).to eq(:unmatched_syntax) - - expect( - WhoDisSyntaxError.new("def foo; end; end").call.unmatched_symbol - ).to eq(:end) - end - - it "with unexpected rescue" do - source = <<~EOM - def foo - if bar - "baz" - else - "foo" - rescue FooBar - nil - end - EOM - - expect( - WhoDisSyntaxError.new(source).call.error_symbol - ).to eq(:unmatched_syntax) - - expect( - WhoDisSyntaxError.new(source).call.unmatched_symbol - ).to eq(:end) - end - end - - context "determines the type of syntax error to be an unmatched pipe" do - it "with unexpected 'end'" do - source = <<~EOM - class Blerg - Foo.call do |a - end # one - - puts lol - class Foo - end # two - end # three - EOM - - expect( - DeadEnd.invalid_type(source).error_symbol - ).to eq(:unmatched_syntax) - - expect( - DeadEnd.invalid_type(source).unmatched_symbol - ).to eq(:|) - end - - it "with unexpected local variable or method" do - source = <<~EOM - class Blerg - [].each do |a - puts a - end - end - EOM - - expect( - DeadEnd.invalid_type(source).error_symbol - ).to eq(:unmatched_syntax) - - expect( - DeadEnd.invalid_type(source).unmatched_symbol - ).to eq(:|) - end - end - - context "determines the type of syntax error to be an unmatched bracket" do - it "with missing bracket" do - source = <<~EOM - module Hey - class Foo - def initialize - [1,2,3 - end - - def call - end - end - end - EOM - - expect( - DeadEnd.invalid_type(source).error_symbol - ).to eq(:unmatched_syntax) - - expect( - DeadEnd.invalid_type(source).unmatched_symbol - ).to eq(:"[") - end - - it "with naked bracket" do - source = <<~EOM - def initialize - ] - end - EOM - - expect( - DeadEnd.invalid_type(source).error_symbol - ).to eq(:unmatched_syntax) - - expect( - DeadEnd.invalid_type(source).unmatched_symbol - ).to eq(:"]") - end - end - end -end