diff --git a/CHANGELOG.md b/CHANGELOG.md index bff4b5e..0e905f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## HEAD (unreleased) - Fix bug where empty lines were interpreted to have a zero indentation (https://github.com/zombocom/dead_end/pull/39) +- Better results when missing "end" comes at the end of a capturing block (such as a class or module definition) (https://github.com/zombocom/dead_end/issues/32) ## 1.0.1 diff --git a/lib/dead_end/capture_code_context.rb b/lib/dead_end/capture_code_context.rb index 0518344..1fa146d 100644 --- a/lib/dead_end/capture_code_context.rb +++ b/lib/dead_end/capture_code_context.rb @@ -35,20 +35,9 @@ def initialize(blocks: , code_lines:) def call @blocks.each do |block| - around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block) - .start_at_next_line - .capture_neighbor_context - - around_lines -= block.lines - - @lines_to_output.concat(around_lines) - - AroundBlockScan.new( - block: block, - code_lines: @code_lines, - ).on_falling_indent do |line| - @lines_to_output << line - end + capture_last_end_same_indent(block) + capture_before_after_kws(block) + capture_falling_indent(block) end @lines_to_output.select!(&:not_empty?) @@ -58,5 +47,70 @@ def call return @lines_to_output end + + def capture_falling_indent(block) + AroundBlockScan.new( + block: block, + code_lines: @code_lines, + ).on_falling_indent do |line| + @lines_to_output << line + end + end + + def capture_before_after_kws(block) + around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block) + .start_at_next_line + .capture_neighbor_context + + around_lines -= block.lines + + @lines_to_output.concat(around_lines) + end + + # Problems heredocs are back in play + def capture_last_end_same_indent(block) + start_index = block.visible_lines.first.index + lines = @code_lines[start_index..block.lines.last.index] + kw_end_lines = lines.select {|line| line.indent == block.current_indent && (line.is_end? || line.is_kw?) } + + + # TODO handle case of heredocs showing up here + # + # Due to https://github.com/zombocom/dead_end/issues/32 + # There's a special case where a keyword right before the last + # end of a valid block accidentally ends up identifying that the problem + # was with the block instead of before it. To handle that + # special case, we can re-parse back through the internals of blocks + # and if they have mis-matched keywords and ends show the last one + end_lines = kw_end_lines.select(&:is_end?) + end_lines.each_with_index do |end_line, i| + start_index = i.zero? ? 0 : end_lines[i-1].index + end_index = end_line.index - 1 + lines = @code_lines[start_index..end_index] + + stop_next = false + kw_count = 0 + end_count = 0 + lines = lines.reverse.take_while do |line| + next false if stop_next + + end_count += 1 if line.is_end? + kw_count += 1 if line.is_kw? + + stop_next = true if !kw_count.zero? && kw_count >= end_count + true + end.reverse + + next unless kw_count > end_count + + lines = lines.select {|line| line.is_kw? || line.is_end? } + + next if lines.empty? + + @lines_to_output << end_line + @lines_to_output << lines.first + @lines_to_output << lines.last + end + end end end diff --git a/lib/dead_end/code_line.rb b/lib/dead_end/code_line.rb index 86148f7..f4463fd 100644 --- a/lib/dead_end/code_line.rb +++ b/lib/dead_end/code_line.rb @@ -65,9 +65,10 @@ def initialize(line: , index:) end end + @is_comment = lex.detect {|lex| lex.type != :on_sp}&.type == :on_comment + return if @is_comment @is_kw = (kw_count - end_count) > 0 @is_end = (end_count - kw_count) > 0 - @is_comment = lex.detect {|lex| lex.type != :on_sp}&.type == :on_comment @is_trailing_slash = lex.last.token == TRAILING_SLASH end diff --git a/spec/unit/capture_code_context_spec.rb b/spec/unit/capture_code_context_spec.rb new file mode 100644 index 0000000..30160b3 --- /dev/null +++ b/spec/unit/capture_code_context_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require_relative "../spec_helper.rb" + +module DeadEnd + RSpec.describe CaptureCodeContext do + it "shows ends of captured block" do + lines = fixtures_dir.join("rexe.rb.txt").read.lines + lines.delete_at(148 - 1) + source = lines.join + + search = CodeSearch.new(source) + search.call + + # expect(search.invalid_blocks.join.strip).to eq('class Dog') + display = CaptureCodeContext.new( + blocks: search.invalid_blocks, + code_lines: search.code_lines + ) + lines = display.call + + lines = lines.sort.map(&:original) + expect(lines.join).to eq(<<~EOM) + class Rexe + VERSION = '1.5.1' + PROJECT_URL = 'https://github.com/keithrbennett/rexe' + class Lookups + def format_requires + end + class CommandLineParser + end + end + EOM + end + + it "shows ends of captured block" do + source = <<~'EOM' + class Dog + def bark + puts "woof" + end + EOM + search = CodeSearch.new(source) + search.call + + expect(search.invalid_blocks.join.strip).to eq('class Dog') + display = CaptureCodeContext.new( + blocks: search.invalid_blocks, + code_lines: search.code_lines + ) + lines = display.call.sort.map(&:original) + expect(lines.join).to eq(<<~EOM) + class Dog + def bark + end + EOM + end + + it "captures surrounding context on falling indent" do + syntax_string = <<~EOM + class Blerg + end + + class OH + + def hello + it "foo" do + end + end + + class Zerg + end + EOM + + search = CodeSearch.new(syntax_string) + search.call + + expect(search.invalid_blocks.join.strip).to eq('it "foo" do') + + display = CaptureCodeContext.new( + blocks: search.invalid_blocks, + code_lines: search.code_lines + ) + lines = display.call.sort.map(&:original) + expect(lines.join).to eq(<<~EOM) + class OH + def hello + it "foo" do + end + end + EOM + end + + it "captures surrounding context on same indent" do + syntax_string = <<~EOM + class Blerg + end + class OH + + def nope + end + + def lol + end + + it "foo" + puts "here" + end + + def haha + end + + def nope + end + end + + class Zerg + end + EOM + + search = CodeSearch.new(syntax_string) + search.call + + code_context = CaptureCodeContext.new( + blocks: search.invalid_blocks, + code_lines: search.code_lines + ) + + # Finds lines previously hidden + lines = code_context.call + # expect(lines.select(&:hidden?).map(&:line_number)).to eq([11, 12]) + + out = DisplayCodeWithLineNumbers.new( + lines: lines, + ).call + + expect(out).to eq(<<~EOM.indent(2)) + 3 class OH + 8 def lol + 9 end + 11 it "foo" + 13 end + 15 def haha + 16 end + 20 end + EOM + end + end +end diff --git a/spec/unit/display_invalid_blocks_spec.rb b/spec/unit/display_invalid_blocks_spec.rb index 6f108ec..b78629d 100644 --- a/spec/unit/display_invalid_blocks_spec.rb +++ b/spec/unit/display_invalid_blocks_spec.rb @@ -67,96 +67,6 @@ def meow expect(display.banner).to include("DeadEnd: Missing `end` detected") end - it "captures surrounding context on same indent" do - syntax_string = <<~EOM - class Blerg - end - class OH - - def nope - end - - def lol - end - - it "foo" - puts "here" - end - - def haha - end - - def nope - end - end - - class Zerg - end - EOM - - search = CodeSearch.new(syntax_string) - search.call - - code_context = CaptureCodeContext.new( - blocks: search.invalid_blocks, - code_lines: search.code_lines - ) - - # Finds lines previously hidden - lines = code_context.call - # expect(lines.select(&:hidden?).map(&:line_number)).to eq([11, 12]) - - out = DisplayCodeWithLineNumbers.new( - lines: lines, - ).call - - expect(out).to eq(<<~EOM.indent(2)) - 3 class OH - 8 def lol - 9 end - 11 it "foo" - 13 end - 15 def haha - 16 end - 20 end - EOM - end - - it "captures surrounding context on falling indent" do - syntax_string = <<~EOM - class Blerg - end - - class OH - - def hello - it "foo" do - end - end - - class Zerg - end - EOM - - search = CodeSearch.new(syntax_string) - search.call - - expect(search.invalid_blocks.join.strip).to eq('it "foo" do') - - display = CaptureCodeContext.new( - blocks: search.invalid_blocks, - code_lines: search.code_lines - ) - lines = display.call.sort - expect(lines.join).to eq(<<~EOM) - class OH - def hello - it "foo" do - end - end - EOM - end - it "works with valid code" do syntax_string = <<~EOM class OH diff --git a/spec/unit/exe_spec.rb b/spec/unit/exe_spec.rb deleted file mode 100644 index 120d015..0000000 --- a/spec/unit/exe_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -require_relative "../spec_helper.rb" - -module DeadEnd - RSpec.describe "exe" do - def exe_path - root_dir.join("exe").join("dead_end") - end - - def exe(cmd) - out = run!("#{exe_path} #{cmd}") - puts out if ENV["DEBUG"] - out - end - - it "parses valid code" do - ruby_file = exe_path - out = exe(ruby_file) - expect(out.strip).to include("Syntax OK") - end - - it "parses invalid code" do - ruby_file = fixtures_dir.join("this_project_extra_def.rb.txt") - out = exe("#{ruby_file} --no-terminal") - - expect(out.strip).to include("Missing `end` detected") - expect(out.strip).to include("❯ 36 def filename") - end - - it "handles heredocs" do - Tempfile.create do |file| - lines = fixtures_dir.join("rexe.rb.txt").read.lines - lines.delete_at(85 - 1) - - Pathname(file.path).write(lines.join) - - out = exe("#{file.path} --no-terminal") - - expect(out).to include(<<~EOM.indent(4)) - 16 class Rexe - 40 class Options < Struct.new( - 71 end - ❯ 77 class Lookups - ❯ 78 def input_modes - ❯ 148 end - 152 class CommandLineParser - 418 end - 551 end - EOM - end - end - - it "records search" do - Dir.mktmpdir do |dir| - dir = Pathname(dir) - tmp_dir = dir.join("tmp").tap(&:mkpath) - ruby_file = dir.join("file.rb") - ruby_file.write("def foo\n end\nend") - - expect(tmp_dir).to be_empty - - out = exe("#{ruby_file} --record #{tmp_dir}") - - expect(out.strip).to include("Unmatched `end` detected") - expect(tmp_dir).to_not be_empty - end - end - end -end