diff --git a/lib/syntax_error_search.rb b/lib/syntax_error_search.rb index 2e7e743..5109a6c 100644 --- a/lib/syntax_error_search.rb +++ b/lib/syntax_error_search.rb @@ -6,5 +6,34 @@ module SyntaxErrorSearch class Error < StandardError; end - # Your code goes here... + + # Used for counting spaces + module SpaceCount + def self.indent(string) + string.split(/\w/).first&.length || 0 + end + end + + + def self.valid?(source) + source = source.join if source.is_a?(Array) + source = source.to_s + + # Parser writes to stderr even if you catch the error + # + stderr = $stderr + $stderr = StringIO.new + + Parser::CurrentRuby.parse(source) + true + rescue Parser::SyntaxError + false + ensure + $stderr = stderr if stderr + end end + +require_relative "syntax_error_search/code_line" +require_relative "syntax_error_search/code_block" +require_relative "syntax_error_search/code_frontier" +require_relative "syntax_error_search/code_search" diff --git a/lib/syntax_error_search/code_block.rb b/lib/syntax_error_search/code_block.rb new file mode 100644 index 0000000..98c1432 --- /dev/null +++ b/lib/syntax_error_search/code_block.rb @@ -0,0 +1,209 @@ +module SyntaxErrorSearch + # Multiple lines form a singular CodeBlock + # + # Source code is made of multiple CodeBlocks. A code block + # has a reference to the source code that created itself, this allows + # a code block to "expand" when needed + # + # The most important ability of a CodeBlock is this ability to expand: + # + # Example: + # + # code_block.to_s # => + # # def foo + # # puts "foo" + # # end + # + # code_block.expand_until_next_boundry + # + # code_block.to_s # => + # # class Foo + # # def foo + # # puts "foo" + # # end + # # end + # + class CodeBlock + attr_reader :lines + + def initialize(code_lines:, lines: []) + @lines = Array(lines) + @code_lines = code_lines + end + + def is_end? + to_s.strip == "end" + end + + def starts_at + @lines.first&.line_number + end + + def code_lines + @code_lines + end + + # This is used for frontier ordering, we are searching from + # the largest indentation to the smallest. This allows us to + # populate an array with multiple code blocks then call `sort!` + # on it without having to specify the sorting criteria + def <=>(other) + self.current_indent <=> other.current_indent + end + + # Only the lines that are not empty and visible + def visible_lines + @lines + .select(&:not_empty?) + .select(&:visible?) + end + + # This method is used to expand a code block to capture it's calling context + def expand_until_next_boundry + expand_to_indent(next_indent) + self + end + + # This method expands the given code block until it captures + # its nearest neighbors. This is used to expand a single line of code + # to its smallest likely block. + # + # code_block.to_s # => + # # puts "foo" + # code_block.expand_until_neighbors + # + # code_block.to_s # => + # # puts "foo" + # # puts "bar" + # # puts "baz" + # + def expand_until_neighbors + expand_to_indent(current_indent) + + expand_hidden_parner_line if self.to_s.strip == "end" + self + end + + def expand_hidden_parner_line + index = @lines.first.index + indent = current_indent + partner_line = code_lines.select {|line| line.index < index && line.indent == indent }.last + + if partner_line&.hidden? + partner_line.mark_visible + @lines.prepend(partner_line) + end + end + + # This method expands the existing code block up (before) + # and down (after). It will break on change in indentation + # and empty lines. + # + # code_block.to_s # => + # # def foo + # # puts "foo" + # # end + # + # code_block.expand_to_indent(0) + # code_block.to_s # => + # # class Foo + # # def foo + # # puts "foo" + # # end + # # end + # + private def expand_to_indent(indent) + array = [] + before_lines(skip_empty: false).each do |line| + if line.empty? + array.prepend(line) + break + end + + if line.indent == indent + array.prepend(line) + else + break + end + end + + array << @lines + + after_lines(skip_empty: false).each do |line| + if line.empty? + array << line + break + end + + if line.indent == indent + array << line + else + break + end + end + + @lines = array.flatten + end + + def next_indent + [ + before_line&.indent || 0, + after_line&.indent || 0 + ].max + end + + def current_indent + lines.detect(&:not_empty?)&.indent || 0 + end + + def before_line + before_lines.first + end + + def after_line + after_lines.first + end + + def before_lines(skip_empty: true) + index = @lines.first.index + lines = code_lines.select {|line| line.index < index } + lines.select!(&:not_empty?) if skip_empty + lines.select!(&:visible?) + lines.reverse! + + lines + end + + def after_lines(skip_empty: true) + index = @lines.last.index + lines = code_lines.select {|line| line.index > index } + lines.select!(&:not_empty?) if skip_empty + lines.select!(&:visible?) + lines + end + + # Returns a code block of the source that does not include + # the current lines. This is useful for checking if a source + # with the given lines removed parses successfully. If so + # + # Then it's proof that the current block is invalid + def block_without + @block_without ||= CodeBlock.new( + source: @source, + lines: @source.code_lines - @lines + ) + end + + def document_valid_without? + block_without.valid? + end + + def valid? + SyntaxErrorSearch.valid?(self.to_s) + end + + def to_s + @lines.join + end + end +end diff --git a/lib/syntax_error_search/code_frontier.rb b/lib/syntax_error_search/code_frontier.rb new file mode 100644 index 0000000..7cd6670 --- /dev/null +++ b/lib/syntax_error_search/code_frontier.rb @@ -0,0 +1,110 @@ +module SyntaxErrorSearch + # This class is responsible for generating, storing, and sorting code blocks + class CodeFrontier + def initialize(code_lines: ) + @code_lines = code_lines + @frontier = [] + @indent_hash = {} + code_lines.each do |line| + next if line.empty? + + @indent_hash[line.indent] ||= [] + @indent_hash[line.indent] << line + end + end + + # Returns true if the document is valid with all lines + # removed. By default it checks all blocks in present in + # the frontier array, but can be used for arbitrary arrays + # of codeblocks as well + def holds_all_syntax_errors?(block_array = @frontier) + lines = @code_lines + block_array.each do |block| + lines -= block.lines + end + + return true if lines.empty? + + CodeBlock.new( + code_lines: @code_lines, + lines: lines + ).valid? + end + + # Returns a code block with the largest indentation possible + def pop + return nil if empty? + + if generate_new_block? + self << next_block + end + + return @frontier.pop + end + + def next_block + indent = @indent_hash.keys.sort.last + lines = @indent_hash[indent].first + + CodeBlock.new( + lines: lines, + code_lines: @code_lines + ).expand_until_neighbors + end + + # This method is responsible for determining if a new code + # block should be generated instead of evaluating an already + # existing block in the frontier + def generate_new_block? + return false if @indent_hash.empty? + return true if @frontier.empty? + + @frontier.last.current_indent <= @indent_hash.keys.sort.last + end + + # Add a block to the frontier + # + # This method ensures the frontier always remains sorted (in indentation order) + # and that each code block's lines are removed from the indentation hash so we + # don't re-evaluate the same line multiple times. + def <<(block) + block.lines.each do |line| + @indent_hash[line.indent]&.delete(line) + end + @indent_hash.select! {|k, v| !v.empty?} + + @frontier << block + @frontier.sort! + + self + end + + def any? + !empty? + end + + def empty? + @frontier.empty? && @indent_hash.empty? + end + + # Example: + # + # combination([:a, :b, :c, :d]) + # # => [[:a], [:b], [:c], [:d], [:a, :b], [:a, :c], [:a, :d], [:b, :c], [:b, :d], [:c, :d], [:a, :b, :c], [:a, :b, :d], [:a, :c, :d], [:b, :c, :d], [:a, :b, :c, :d]] + def self.combination(array) + guesses = [] + 1.upto(array.length).each do |size| + guesses.concat(array.combination(size).to_a) + end + guesses + end + + # Given that we know our syntax error exists somewhere in our frontier, we want to find + # the smallest possible set of blocks that contain all the syntax errors + def detect_invalid_blocks + self.class.combination(@frontier).detect do |block_array| + holds_all_syntax_errors?(block_array) + end || [] + end + end +end diff --git a/lib/syntax_error_search/code_line.rb b/lib/syntax_error_search/code_line.rb new file mode 100644 index 0000000..d090cf8 --- /dev/null +++ b/lib/syntax_error_search/code_line.rb @@ -0,0 +1,85 @@ +module SyntaxErrorSearch + # Represents a single line of code of a given source file + # + # This object contains metadata about the line such as + # amount of indentation. An if it is empty or not. + # + # While a given search for syntax errors is being performed + # state about the search can be stored in individual lines such + # as :valid or :invalid. + # + # Visibility of lines can be toggled on and off. + # + # Example: + # + # line = CodeLine.new(line: "def foo\n", index: 0) + # line.line_number => 1 + # line.empty? # => false + # line.visible? # => true + # line.mark_invisible + # line.visible? # => false + # + # A CodeBlock is made of multiple CodeLines + # + # Marking a line as invisible indicates that it should not be used + # for syntax checks. It's essentially the same as commenting it out + # + # Marking a line as invisible also lets the overall program know + # that it should not check that area for syntax errors. + class CodeLine + attr_reader :line, :index, :indent + + def initialize(line: , index:) + @original_line = line.freeze + @line = @original_line + @empty = line.strip.empty? + @index = index + @indent = SpaceCount.indent(line) + @status = nil # valid, invalid, unknown + @invalid = false + end + + def mark_invalid + @invalid = true + self + end + + def marked_invalid? + @invalid + end + + def mark_invisible + @line = "" + self + end + + def mark_visible + @line = @original_line + self + end + + def visible? + !line.empty? + end + + def hidden? + !visible? + end + + def line_number + index + 1 + end + + def not_empty? + !empty? + end + + def empty? + @empty + end + + def to_s + self.line + end + end +end diff --git a/lib/syntax_error_search/code_search.rb b/lib/syntax_error_search/code_search.rb new file mode 100644 index 0000000..a08a785 --- /dev/null +++ b/lib/syntax_error_search/code_search.rb @@ -0,0 +1,30 @@ +module SyntaxErrorSearch + class CodeSearch + private; attr_reader :frontier; public + public; attr_reader :invalid_blocks + + def initialize(string) + @code_lines = string.lines.map.with_index do |line, i| + CodeLine.new(line: line, index: i) + end + @frontier = CodeFrontier.new(code_lines: @code_lines) + @invalid_blocks = [] + end + + def call + until frontier.holds_all_syntax_errors? + block = frontier.pop + + if block.valid? + block.lines.each(&:mark_invisible) + else + block.expand_until_neighbors + frontier << block + end + end + + @invalid_blocks.concat(frontier.detect_invalid_blocks ) + self + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8a9ab5d..b3bf001 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,3 +12,28 @@ c.syntax = :expect end end + +def code_line_array(string) + code_lines = [] + string.lines.each_with_index do |line, index| + code_lines << SyntaxErrorSearch::CodeLine.new(line: line, index: index) + end + code_lines +end + +# Allows us to write cleaner tests since <<~EOM block quotes +# strip off all leading indentation and we need it to be preserved +# sometimes. +class String + def indent(number) + self.lines.map do |line| + if line.chomp.empty? + line + else + " " * number + line + end + end.join + end +end + + diff --git a/spec/syntax_error_search_spec.rb b/spec/syntax_error_search_spec.rb index 10ec220..e4f29bc 100644 --- a/spec/syntax_error_search_spec.rb +++ b/spec/syntax_error_search_spec.rb @@ -1,799 +1,44 @@ -RSpec.describe SyntaxErrorSearch do - it "has a version number" do - expect(SyntaxErrorSearch::VERSION).not_to be nil - end -end - - -RSpec.describe SyntaxErrorSearch do - def ruby(script) - `ruby -I#{lib_dir} -rdid_you_do #{script} 2>&1` - end - - describe "foo" do - around(:each) do |example| - Dir.mktmpdir do |dir| - @tmpdir = Pathname(dir) - @script = @tmpdir.join("script.rb") - example.run - end - end - - it "blerg" do - @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(require_rb) - # puts out - end - end -end - -module SpaceCount - def self.indent(string) - string.split(/\w/).first&.length || 0 - end -end - -class CodeLine - attr_reader :line, :index, :indent - - VALID_STATUS = [:valid, :invalid, :unknown].freeze - - def initialize(line: , index:) - @line = line - @stripped_line = line.strip - @index = index - @indent = SpaceCount.indent(line) - @is_end = line.strip == "end".freeze - @status = nil # valid, invalid, unknown - @visible = true - @block_memeber = nil - end - - def belongs_to_block? - @block_member - end - - def mark_block(code_block) - @block_member = code_block - end - - def marked_invalid? - @status == :invalid - end - - def mark_valid - @status = :valid - end - - def mark_invalid - @status = :invalid - end - - def mark_invisible - @visible = false - end - - def mark_visible - @visible = true - end - - def visible? - @visible - end - - def line_number - index + 1 - end - - def not_empty? - !empty? - end - - def empty? - @stripped_line.empty? - end - - def to_s - @line - end - - def is_end? - @is_end - end -end - -class CodeBlock - attr_reader :lines - - def initialize(source: , lines: []) - @lines = Array(lines) - @source = source - end - - - def <=>(other) - self.current_indent <=> other.current_indent - end - - def visible_lines - @lines - .select(&:not_empty?) - .select(&:visible?) - end - - def max_indent - visible_lines.map(&:indent).max - end - - def block_with_neighbors_while - array = [] - array << before_lines.take_while do |line| - yield line - end - array << lines - - array << after_lines.take_while do |line| - yield line - end - - CodeBlock.new( - source: @source, - lines: array.flatten - ) - end - - # We can guess a block boundry exists when there's - # a change in indentation (spaces decrease) or an empty line - # - # Expand on until boundry condition is met: - # - # - Indentation goes down (do not add this line, stop search) - # - empty line (add this line, stop search) - # - # Check valid/invalid - - # Two cases: - # - # - Search same indent - # - Search smaller indent - # - # Take a line, find the nearest indent - # - # Pick a line, expand up until we've hit an empty - def expand_until_next_boundry - expand_to_indent(next_indent) - end - - def expand_until_neighbors - expand_to_indent(current_indent) - end - - def expand_to_indent(indent) - array = [] - before_lines(skip_empty: false).each do |line| - if line.empty? - array.prepend(line) - break - end - - if line.indent == indent - array.prepend(line) - else - break - end - end - - array << @lines - - after_lines(skip_empty: false).each do |line| - if line.empty? - array << line - break - end - - if line.indent == indent - array << line - else - break - end - end - - @lines = array.flatten - end - - def next_indent - [ - before_line&.indent || 0, - after_line&.indent || 0 - ].max - end - - def current_indent - lines.detect(&:not_empty?)&.indent || 0 - end - - def before_line - before_lines.first - end - - def after_line - after_lines.first - end - - def before_lines(skip_empty: true) - index = @lines.first.index - lines = @source.code_lines.select {|line| line.index < index } - lines.select!(&:not_empty?) if skip_empty - lines.select!(&:visible?) - lines.reverse! - - lines - end - - def after_lines(skip_empty: true) - index = @lines.last.index - lines = @source.code_lines.select {|line| line.index > index } - lines.select!(&:not_empty?) if skip_empty - lines.select!(&:visible?) - lines - end - - # Returns a code block of the source that does not include - # the current lines. This is useful for checking if a source - # with the given lines removed parses successfully. If so - # - # Then it's proof that the current block is invalid - def block_without - @block_without ||= CodeBlock.new( - source: @source, - lines: @source.code_lines - @lines - ) - end - - def document_valid_without? - block_without.valid? - end - - def valid? - CodeSource.valid?(self.to_s) - end - - def to_s - CodeSource.code_lines_to_source(@lines) - end -end - -class CodeSource - attr_reader :lines, :indent_array, :indent_hash, :code_lines - - def initialize(source) - @frontier = [] - @lines = source.lines - @indent_array = [] - @indent_hash = Hash.new {|h, k| h[k] = [] } - - @code_lines = [] - lines.each_with_index do |line, i| - code_line = CodeLine.new( - line: line, - index: i, - ) - - @indent_array[i] = code_line.indent - @indent_hash[code_line.indent] << code_line - @code_lines << code_line - end - end - - def get_max_indent - @indent_hash.select! {|k, v| !v.empty?} - @indent_hash.keys.sort.last - end - - def indent_hash - @indent_hash - end - - def self.code_lines_to_source(source) - source = source.select(&:visible?) - source = source.join - end - - def self.valid?(source) - source = code_lines_to_source(source) if source.is_a?(Array) - source = source.to_s - - # Parser writes to stderr even if you catch the error - # - stderr = $stderr - $stderr = StringIO.new - - Parser::CurrentRuby.parse(source) - true - rescue Parser::SyntaxError - false - ensure - $stderr = stderr if stderr - end - - def pop_max_indent_line(indent = get_max_indent) - return nil if @indent_hash.empty? - - if (line = @indent_hash[indent].shift) - return line - else - pop_max_indent_line - end - end - - # Returns a CodeBlock based on the maximum indentation - # present in the source - def max_indent_to_block - if (line = pop_max_indent_line) - block = CodeBlock.new( - source: self, - lines: line - ) - block.expand_until_neighbors - clean_hash(block) - - return block - end - end - - # Returns the highest indentation code block from the - # frontier or if - def next_frontier - if @frontier.any? - @frontier.sort! - block = @frontier.pop - - if self.get_max_indent && block.current_indent <= self.get_max_indent - @frontier.push(block) - block = nil - else - - block.expand_until_next_boundry - clean_hash(block) - return block - end - end - - max_indent_to_block if block.nil? - end - - def clean_hash(block) - block.lines.each do |line| - @indent_hash[line.indent].delete(line) +module SyntaxErrorSearch + RSpec.describe SyntaxErrorSearch do + it "has a version number" do + expect(SyntaxErrorSearch::VERSION).not_to be nil end - end - - def invalid_code - CodeBlock.new( - lines: code_lines.select(&:marked_invalid?), - source: self - ) - end - - def frontier_holds_syntax_error? - lines = code_lines - @frontier.each do |block| - lines -= block.lines - end - - return true if lines.empty? - - CodeBlock.new( - source: self, - lines: lines - ).valid? - end - def detect_invalid - while block = next_frontier - if block.valid? - block.lines.each(&:mark_valid) - block.lines.each(&:mark_invisible) - next - end - - if block.document_valid_without? - block.lines.each(&:mark_invalid) - return - end - - @frontier << block - - if frontier_holds_syntax_error? - @frontier.each do |block| - block.lines.each(&:mark_invalid) - end - return - end + def ruby(script) + `ruby -I#{lib_dir} -rdid_you_do #{script} 2>&1` end - end -end -RSpec.describe CodeLine do - - it "detect" do - source = CodeSource.new(<<~EOM) - def foo - puts 'lol' - end - EOM - source.detect_invalid - expect(source.code_lines.map(&:marked_invalid?)).to eq([false, false, false]) - - source = CodeSource.new(<<~EOM) - def foo - end - end - EOM - source.detect_invalid - expect(source.code_lines.map(&:marked_invalid?)).to eq([false, true, false]) - - source = CodeSource.new(<<~EOM) - def foo - def blerg - end - EOM - source.detect_invalid - expect(source.code_lines.map(&:marked_invalid?)).to eq([false, true, false]) - end - it "frontier" do - source = CodeSource.new(<<~EOM) - def foo - puts 'lol' - end - EOM - block = source.next_frontier - expect(block.lines).to eq([source.code_lines[1]]) - - source.code_lines[1].mark_invisible - - block = source.next_frontier - expect(block.lines).to eq( - [source.code_lines[0], source.code_lines[2]]) - end - - it "frontier levels" do - - source_string = <<~EOM - describe "hi" do - Foo.call + describe "foo" do + around(:each) do |example| + Dir.mktmpdir do |dir| + @tmpdir = Pathname(dir) + @script = @tmpdir.join("script.rb") + example.run end end it "blerg" do - Bar.call - end - end - EOM + @script.write <<~EOM + describe "things" do + it "blerg" do + end - source = CodeSource.new(source_string) - - block = source.next_frontier - expect(block.to_s).to eq(<<-EOM) - Foo.call - end -EOM + it "flerg" + end - block = source.next_frontier - expect(block.to_s).to eq(<<-EOM) - Bar.call - end -EOM - end - - - it "max indent to block" do - source = CodeSource.new(<<~EOM) - def foo - puts 'lol' - end - EOM - block = source.max_indent_to_block - - expect(block.lines).to eq([source.code_lines[1]]) - - block = source.max_indent_to_block - expect(block.lines).to eq([source.code_lines[0]]) - - source = CodeSource.new(<<~EOM) - def foo - puts 'lol' - end - - def bar - puts 'boo' - end - EOM - block = source.max_indent_to_block - expect(block.lines).to eq([source.code_lines[1]]) - - block = source.max_indent_to_block - expect(block.lines).to eq([source.code_lines[5]]) - end - - it "code block can detect if it's valid or not" do - source = CodeSource.new(<<~EOM) - def foo - puts 'lol' - end - EOM - - block = CodeBlock.new(source: source, lines: source.code_lines[1]) - expect(block.valid?).to be_truthy - expect(block.document_valid_without?).to be_truthy - expect(block.block_without.lines).to eq([source.code_lines[0], source.code_lines[2]]) - expect(block.max_indent).to eq(2) - expect(block.before_lines).to eq([source.code_lines[0]]) - expect(block.before_line).to eq(source.code_lines[0]) - expect(block.after_lines).to eq([source.code_lines[2]]) - expect(block.after_line).to eq(source.code_lines[2]) - expect( - block.block_with_neighbors_while {|n| n.indent == block.max_indent - 2}.lines - ).to eq(source.code_lines) - - expect( - block.block_with_neighbors_while {|n| n.index == 1 }.lines - ).to eq([source.code_lines[1]]) - - source = CodeSource.new(<<~EOM) - def foo - bar; end - end - EOM - - block = CodeBlock.new(source: source, lines: source.code_lines[1]) - expect(block.valid?).to be_falsey - expect(block.document_valid_without?).to be_truthy - expect(block.block_without.lines).to eq([source.code_lines[0], source.code_lines[2]]) - expect(block.before_lines).to eq([source.code_lines[0]]) - expect(block.after_lines).to eq([source.code_lines[2]]) - end - - it "ignores marked valid lines" do - code_lines = [] - code_lines << CodeLine.new(line: "def foo\n", index: 0) - code_lines << CodeLine.new(line: " Array(value) |x|\n", index: 1) - code_lines << CodeLine.new(line: " end\n", index: 2) - code_lines << CodeLine.new(line: "end\n", index: 3) - - expect(CodeSource.valid?(code_lines)).to be_falsey - expect(CodeSource.code_lines_to_source(code_lines)).to eq(<<~EOM) - def foo - Array(value) |x| - end - end - EOM - - code_lines[0].mark_invisible - code_lines[3].mark_invisible - - expected = [" Array(value) |x|\n", " end\n"].join - expect(CodeSource.code_lines_to_source(code_lines)).to eq(expected) - expect(CodeSource.valid?(code_lines)).to be_falsey - end - - it "ignores marked invalid lines" do - code_lines = [] - code_lines << CodeLine.new(line: "def foo\n", index: 0) - code_lines << CodeLine.new(line: " Array(value) |x|\n", index: 1) - code_lines << CodeLine.new(line: " end\n", index: 2) - code_lines << CodeLine.new(line: "end\n", index: 3) - - expect(CodeSource.valid?(code_lines)).to be_falsey - expect(CodeSource.code_lines_to_source(code_lines)).to eq(<<~EOM) - def foo - Array(value) |x| - end - end - EOM - - code_lines[1].mark_invisible - code_lines[2].mark_invisible - - expect(CodeSource.code_lines_to_source(code_lines)).to eq(<<~EOM) - def foo - end - EOM - - expect(CodeSource.valid?(code_lines)).to be_truthy - end - - - it "empty code line" do - source = CodeSource.new(<<~EOM) - # Not empty - - # Not empty - EOM - - expect(source.code_lines.map(&:empty?)).to eq([false, true, false]) - expect(source.code_lines.map {|l| CodeSource.valid?(l) }).to eq([true, true, true]) - end - - it "blerg" do - source = CodeSource.new(<<~EOM) - def foo - puts 'lol' - end - EOM - - expect(source.indent_array).to eq([0, 2, 0]) - # expect(source.indent_hash).to eq({0 =>[0, 2], 2 =>[1]}) - expect(source.code_lines.join()).to eq(<<~EOM) - def foo - puts 'lol' - end - EOM - end - - describe "detect cases" do - it "finds one invalid code block with typo def" do - source_string = <<~EOM - defzfoo - puts "lol" - end - EOM - source = CodeSource.new(source_string) - source.detect_invalid - - expect(source.invalid_code.to_s).to eq(<<~EOM) - defzfoo - end - EOM - end - - it "finds TWO invalid code block with missing do at the depest indent" do - source = <<~EOM - describe "hi" do - Foo.call - end - end - - it "blerg" do - Bar.call - end - end - EOM - - source = CodeSource.new(source) - source.detect_invalid - - - expect(source.invalid_code.to_s).to eq(<<-EOM) - Foo.call - end - Bar.call - end -EOM - end - - it "finds one invalid code block with missing do at the depest indent" do - source = <<~EOM - describe "hi" do - Foo.call - end - end - - it "blerg" do - end - EOM - - source = CodeSource.new(source) - source.detect_invalid - - expect(source.code_lines[1].marked_invalid?).to be_truthy - expect(source.code_lines[2].marked_invalid?).to be_truthy - - expect(source.invalid_code.to_s).to eq(" Foo.call\n end\n") - end - end - - describe "expansion" do - - it "expand until next boundry (indentation)" do - source_string = <<~EOM - describe "what" do - Foo.call - end - - describe "hi" - Bar.call do - Foo.call + it "zlerg" do + end end - end + EOM - it "blerg" do - end - EOM - - source = CodeSource.new(source_string) - block = CodeBlock.new( - lines: source.code_lines[6], - source: source - ) - - block.expand_until_next_boundry - - expect(block.to_s).to eq(<<-EOM) - Bar.call do - Foo.call - end -EOM + require_rb = @tmpdir.join("require.rb") + require_rb.write <<~EOM + require_relative "./script.rb" + EOM - block.expand_until_next_boundry - - expect(block.to_s).to eq(<<-EOM) - -describe "hi" - Bar.call do - Foo.call - end -end - -EOM - end - - it "expand until next boundry (empty lines)" do - source_string = <<~EOM - describe "what" do - end - - describe "hi" - end - - it "blerg" do - end - EOM - - source = CodeSource.new(source_string) - block = CodeBlock.new( - lines: source.code_lines[0], - source: source - ) - block.expand_until_next_boundry - - expect(block.to_s.strip).to eq(<<~EOM.strip) - describe "what" do - end - EOM - - source = CodeSource.new(source_string) - block = CodeBlock.new( - lines: source.code_lines[3], - source: source - ) - block.expand_until_next_boundry - - expect(block.to_s.strip).to eq(<<~EOM.strip) - describe "hi" - end - EOM - - block.expand_until_next_boundry - - expect(block.to_s.strip).to eq(source_string.strip) + # out = ruby(require_rb) + # puts out + end end end end diff --git a/spec/unit/code_block_spec.rb b/spec/unit/code_block_spec.rb new file mode 100644 index 0000000..7b0bf51 --- /dev/null +++ b/spec/unit/code_block_spec.rb @@ -0,0 +1,155 @@ +require_relative "../spec_helper.rb" + +module SyntaxErrorSearch + RSpec.describe CodeBlock do + it "expand until next boundry (indentation)" do + source_string = <<~EOM + describe "what" do + Foo.call + end + + describe "hi" + Bar.call do + Foo.call + end + end + + it "blerg" do + end + EOM + + code_lines = code_line_array(source_string) + + block = CodeBlock.new( + lines: code_lines[6], + code_lines: code_lines + ) + + block.expand_until_next_boundry + + expect(block.to_s).to eq(<<~EOM.indent(2)) + Bar.call do + Foo.call + end + EOM + + block.expand_until_next_boundry + + expect(block.to_s).to eq(<<~EOM) + + describe "hi" + Bar.call do + Foo.call + end + end + + EOM + end + + it "expand until next boundry (empty lines)" do + source_string = <<~EOM + describe "what" do + end + + describe "hi" + end + + it "blerg" do + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new( + lines: code_lines[0], + code_lines: code_lines + ) + block.expand_until_next_boundry + + expect(block.to_s.strip).to eq(<<~EOM.strip) + describe "what" do + end + EOM + + block = CodeBlock.new( + lines: code_lines[3], + code_lines: code_lines + ) + block.expand_until_next_boundry + + expect(block.to_s.strip).to eq(<<~EOM.strip) + describe "hi" + end + EOM + + block.expand_until_next_boundry + + expect(block.to_s.strip).to eq(source_string.strip) + end + + it "can detect if it's valid or not" do + code_lines = code_line_array(<<~EOM) + def foo + puts 'lol' + end + EOM + + block = CodeBlock.new(code_lines: code_lines, lines: code_lines[1]) + expect(block.valid?).to be_truthy + end + + it "can be sorted in indentation order" do + code_lines = code_line_array(<<~EOM) + def foo + puts 'lol' + end + EOM + + block_0 = CodeBlock.new(code_lines: code_lines, lines: code_lines[0]) + block_1 = CodeBlock.new(code_lines: code_lines, lines: code_lines[1]) + block_2 = CodeBlock.new(code_lines: code_lines, lines: code_lines[2]) + + expect(block_0 <=> block_0).to eq(0) + expect(block_1 <=> block_0).to eq(1) + expect(block_1 <=> block_2).to eq(-1) + + array = [block_2, block_1, block_0].sort + expect(array.last).to eq(block_2) + + block = CodeBlock.new(code_lines: code_lines, lines: CodeLine.new(line: " " * 8 + "foo", index: 4)) + array.prepend(block) + expect(array.sort.last).to eq(block) + end + + it "knows it's current indentation level" do + code_lines = code_line_array(<<~EOM) + def foo + puts 'lol' + end + EOM + + block = CodeBlock.new(code_lines: code_lines, lines: code_lines[1]) + expect(block.current_indent).to eq(2) + expect(block.before_lines).to eq([code_lines[0]]) + expect(block.before_line).to eq(code_lines[0]) + expect(block.after_lines).to eq([code_lines[2]]) + expect(block.after_line).to eq(code_lines[2]) + + block = CodeBlock.new(code_lines: code_lines, lines: code_lines[0]) + expect(block.current_indent).to eq(0) + end + + + it "before lines and after lines" do + code_lines = code_line_array(<<~EOM) + def foo + bar; end + end + EOM + + block = CodeBlock.new(code_lines: code_lines, lines: code_lines[1]) + expect(block.valid?).to be_falsey + expect(block.before_lines).to eq([code_lines[0]]) + expect(block.after_lines).to eq([code_lines[2]]) + end + end +end diff --git a/spec/unit/code_frontier_spec.rb b/spec/unit/code_frontier_spec.rb new file mode 100644 index 0000000..e263cc6 --- /dev/null +++ b/spec/unit/code_frontier_spec.rb @@ -0,0 +1,205 @@ +require_relative "../spec_helper.rb" + +module SyntaxErrorSearch + RSpec.describe CodeFrontier do + it "search example" do + code_lines = code_line_array(<<~EOM) + describe "lol" do + foo + end + end + + it "lol" do + bar + end + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + + until frontier.holds_all_syntax_errors? + block = frontier.pop + + if block.valid? + block.lines.each(&:mark_invisible) + + else + block.expand_until_neighbors + frontier << block + end + end + + expect(frontier.detect_invalid_blocks.join).to eq(<<~EOM.indent(2)) + foo + end + bar + end + EOM + end + it "detect_bad_blocks" do + code_lines = code_line_array(<<~EOM) + describe "lol" do + end + end + + it "lol" do + end + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + blocks = [] + blocks << CodeBlock.new(lines: code_lines[1], code_lines: code_lines) + blocks << CodeBlock.new(lines: code_lines[5], code_lines: code_lines) + blocks.each do |b| + frontier << b + end + + expect(frontier.detect_invalid_blocks).to eq(blocks) + end + + it "self.combination" do + expect( + CodeFrontier.combination([:a, :b, :c, :d]) + ).to eq( + [ + [:a],[:b],[:c],[:d], + [:a, :b], + [:a, :c], + [:a, :d], + [:b, :c], + [:b, :d], + [:c, :d], + [:a, :b, :c], + [:a, :b, :d], + [:a, :c, :d], + [:b, :c, :d], + [:a, :b, :c, :d] + ] + ) + end + + it "detects if multiple syntax errors are found" do + code_lines = code_line_array(<<~EOM) + def foo + end + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + block = frontier.pop + expect(block.to_s).to eq(<<~EOM.indent(2)) + end + EOM + frontier << block + + expect(frontier.holds_all_syntax_errors?).to be_truthy + end + + it "detects if it has not captured all syntax errors" do + code_lines = code_line_array(<<~EOM) + def foo + puts "lol" + end + + describe "lol" + end + + it "lol" + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + block = frontier.pop + expect(block.to_s).to eq(<<~EOM.indent(2)) + puts "lol" + EOM + frontier << block + + expect(frontier.holds_all_syntax_errors?).to be_falsey + end + + it "generates a block when popping" do + code_lines = code_line_array(<<~EOM) + def foo + puts "lol1" + puts "lol2" + puts "lol3" + + puts "lol4" + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + expect(frontier.pop.to_s).to eq(<<~EOM.indent(2)) + puts "lol1" + puts "lol2" + puts "lol3" + + EOM + + expect(frontier.generate_new_block?).to be_truthy + + expect(frontier.pop.to_s).to eq(<<~EOM.indent(2)) + + puts "lol4" + EOM + + expect(frontier.pop.to_s).to eq(<<~EOM) + def foo + EOM + end + + it "generates continuous block lines" do + code_lines = code_line_array(<<~EOM) + def foo + puts "lol1" + puts "lol2" + puts "lol3" + + puts "lol4" + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + block = frontier.next_block + expect(block.to_s).to eq(<<~EOM.indent(2)) + puts "lol1" + puts "lol2" + puts "lol3" + + EOM + + expect(frontier.generate_new_block?).to be_truthy + + frontier << block + + block = frontier.next_block + expect(block.to_s).to eq(<<~EOM.indent(2)) + + puts "lol4" + EOM + frontier << block + + expect(frontier.generate_new_block?).to be_falsey + end + + it "detects empty" do + code_lines = code_line_array(<<~EOM) + def foo + puts "lol" + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + + expect(frontier.empty?).to be_falsey + expect(frontier.any?).to be_truthy + + frontier = CodeFrontier.new(code_lines: []) + + expect(frontier.empty?).to be_truthy + expect(frontier.any?).to be_falsey + end + end +end diff --git a/spec/unit/code_line_spec.rb b/spec/unit/code_line_spec.rb new file mode 100644 index 0000000..4dec437 --- /dev/null +++ b/spec/unit/code_line_spec.rb @@ -0,0 +1,75 @@ +require_relative "../spec_helper.rb" + +module SyntaxErrorSearch + RSpec.describe CodeLine do + it "can be marked as invalid or valid" do + code_lines = code_line_array(<<~EOM) + def foo + Array(value) |x| + end + end + EOM + + expect(code_lines[0].marked_invalid?).to be_falsey + code_lines[0].mark_invalid + expect(code_lines[0].marked_invalid?).to be_truthy + + end + + it "ignores marked lines" do + code_lines = code_line_array(<<~EOM) + def foo + Array(value) |x| + end + end + EOM + + expect(SyntaxErrorSearch.valid?(code_lines)).to be_falsey + expect(code_lines.join).to eq(<<~EOM) + def foo + Array(value) |x| + end + end + EOM + + expect(code_lines[0].visible?).to be_truthy + expect(code_lines[3].visible?).to be_truthy + + code_lines[0].mark_invisible + code_lines[3].mark_invisible + + expect(code_lines[0].visible?).to be_falsey + expect(code_lines[3].visible?).to be_falsey + + expect(code_lines.join).to eq(<<~EOM.indent(2)) + Array(value) |x| + end + EOM + expect(SyntaxErrorSearch.valid?(code_lines)).to be_falsey + end + + it "knows empty lines" do + code_lines = code_line_array(<<~EOM) + # Not empty + + # Not empty + EOM + + expect(code_lines.map(&:empty?)).to eq([false, true, false]) + expect(code_lines.map(&:not_empty?)).to eq([true, false, true]) + expect(code_lines.map {|l| SyntaxErrorSearch.valid?(l) }).to eq([true, true, true]) + end + + it "counts indentations" do + code_lines = code_line_array(<<~EOM) + def foo + Array(value) |x| + puts 'lol' + end + end + EOM + + expect(code_lines.map(&:indent)).to eq([0, 2, 4, 2, 0]) + end + end +end diff --git a/spec/unit/code_search_spec.rb b/spec/unit/code_search_spec.rb new file mode 100644 index 0000000..4d58b40 --- /dev/null +++ b/spec/unit/code_search_spec.rb @@ -0,0 +1,144 @@ + +require_relative "../spec_helper.rb" + +module SyntaxErrorSearch + RSpec.describe CodeSearch do + it "does not go into an infinite loop" do + skip("infinite loop") + search = CodeSearch.new(<<~EOM) + Foo.call + def foo + puts "lol" + puts "lol" + end + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~EOM) + end + EOM + end + + it "handles mis-matched-indentation-but-maybe-not-so-well" do + skip("wip") + search = CodeSearch.new(<<~EOM) + Foo.call + def foo + puts "lol" + puts "lol" + end + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~EOM) + end + EOM + end + + it "returns syntax error in outer block without inner block" do + search = CodeSearch.new(<<~EOM) + Foo.call + def foo + puts "lol" + puts "lol" + end + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~EOM) + Foo.call + end + EOM + end + + it "doesn't just return an empty `end`" do + search = CodeSearch.new(<<~EOM) + Foo.call + + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~EOM) + Foo.call + end + EOM + end + + it "finds multiple syntax errors" do + search = CodeSearch.new(<<~EOM) + describe "hi" do + Foo.call + end + end + + it "blerg" do + Bar.call + end + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~EOM.indent(2)) + Foo.call + end + Bar.call + end + EOM + end + + it "finds a typo def" do + search = CodeSearch.new(<<~EOM) + defzfoo + puts "lol" + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~EOM) + defzfoo + end + EOM + end + + it "finds a mis-matched def" do + search = CodeSearch.new(<<~EOM) + def foo + def blerg + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~EOM.indent(2)) + def blerg + EOM + end + + it "finds a naked end" do + search = CodeSearch.new(<<~EOM) + def foo + end + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~EOM.indent(2)) + end + EOM + end + + it "returns when no invalid blocks are found" do + search = CodeSearch.new(<<~EOM) + def foo + puts 'lol' + end + EOM + search.call + + expect(search.invalid_blocks).to eq([]) + end + end +end