diff options
author | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2022-08-19 15:37:45 +0900 |
---|---|---|
committer | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2022-08-26 12:15:47 +0900 |
commit | 0d9f4ea0d45f6577a4a13f898e981958a1f039c6 (patch) | |
tree | a5c1eb45bd2d5014baf93449c53efec3c4dccbd7 /spec/syntax_suggest/unit | |
parent | 3504be1bc13235407e01f55d3df6fe0b4cb5ba9e (diff) | |
download | ruby-0d9f4ea0d45f6577a4a13f898e981958a1f039c6.tar.gz |
Import spec examples from ruby/syntax_suggest
Diffstat (limited to 'spec/syntax_suggest/unit')
-rw-r--r-- | spec/syntax_suggest/unit/api_spec.rb | 83 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/around_block_scan_spec.rb | 165 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/block_expand_spec.rb | 200 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/capture_code_context_spec.rb | 202 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/clean_document_spec.rb | 259 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/cli_spec.rb | 224 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/code_block_spec.rb | 77 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/code_frontier_spec.rb | 135 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/code_line_spec.rb | 164 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/code_search_spec.rb | 505 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/display_invalid_blocks_spec.rb | 172 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/explain_syntax_spec.rb | 255 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/lex_all_spec.rb | 29 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/pathname_from_message_spec.rb | 56 | ||||
-rw-r--r-- | spec/syntax_suggest/unit/priority_queue_spec.rb | 95 |
15 files changed, 2621 insertions, 0 deletions
diff --git a/spec/syntax_suggest/unit/api_spec.rb b/spec/syntax_suggest/unit/api_spec.rb new file mode 100644 index 0000000000..284a4cdeec --- /dev/null +++ b/spec/syntax_suggest/unit/api_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" +require "ruby-prof" + +module SyntaxSuggest + RSpec.describe "Top level SyntaxSuggest api" do + it "has a `handle_error` interface" do + fake_error = Object.new + def fake_error.message + "#{__FILE__}:216: unterminated string meets end of file " + end + + def fake_error.is_a?(v) + true + end + + io = StringIO.new + SyntaxSuggest.handle_error( + fake_error, + re_raise: false, + io: io + ) + + expect(io.string.strip).to eq("Syntax OK") + end + + it "raises original error with warning if a non-syntax error is passed" do + error = NameError.new("blerg") + io = StringIO.new + expect { + SyntaxSuggest.handle_error( + error, + re_raise: false, + io: io + ) + }.to raise_error { |e| + expect(io.string).to include("Must pass a SyntaxError") + expect(e).to eq(error) + } + end + + it "raises original error with warning if file is not found" do + fake_error = SyntaxError.new + def fake_error.message + "#does/not/exist/lol/doesnotexist:216: unterminated string meets end of file " + end + + io = StringIO.new + expect { + SyntaxSuggest.handle_error( + fake_error, + re_raise: false, + io: io + ) + }.to raise_error { |e| + expect(io.string).to include("Could not find filename") + expect(e).to eq(fake_error) + } + end + + it "respects highlight API" do + skip if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2") + + error = SyntaxError.new("#{fixtures_dir.join("this_project_extra_def.rb.txt")}:1 ") + + require "syntax_suggest/core_ext" + + expect(error.detailed_message(highlight: true)).to include(SyntaxSuggest::DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT) + expect(error.detailed_message(highlight: false)).to_not include(SyntaxSuggest::DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT) + end + + it "can be disabled via falsey kwarg" do + skip if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2") + + error = SyntaxError.new("#{fixtures_dir.join("this_project_extra_def.rb.txt")}:1 ") + + require "syntax_suggest/core_ext" + + expect(error.detailed_message(syntax_suggest: true)).to_not eq(error.detailed_message(syntax_suggest: false)) + end + end +end diff --git a/spec/syntax_suggest/unit/around_block_scan_spec.rb b/spec/syntax_suggest/unit/around_block_scan_spec.rb new file mode 100644 index 0000000000..6053c3947e --- /dev/null +++ b/spec/syntax_suggest/unit/around_block_scan_spec.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe AroundBlockScan do + it "continues scan from last location even if scan is false" do + source = <<~'EOM' + print 'omg' + print 'lol' + print 'haha' + EOM + code_lines = CodeLine.from_source(source) + block = CodeBlock.new(lines: code_lines[1]) + expand = AroundBlockScan.new(code_lines: code_lines, block: block) + .scan_neighbors + + expect(expand.code_block.to_s).to eq(source) + expand.scan_while { |line| false } + + expect(expand.code_block.to_s).to eq(source) + end + + it "scan_adjacent_indent works on first or last line" do + source_string = <<~EOM + def foo + if [options.output_format_tty, options.output_format_block].include?(nil) + raise("Bad output mode '\#{v}'; each must be one of \#{lookups.output_formats.keys}.") + end + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new(lines: code_lines[4]) + expand = AroundBlockScan.new(code_lines: code_lines, block: block) + .scan_adjacent_indent + + expect(expand.code_block.to_s).to eq(<<~EOM) + def foo + if [options.output_format_tty, options.output_format_block].include?(nil) + raise("Bad output mode '\#{v}'; each must be one of \#{lookups.output_formats.keys}.") + end + end + EOM + end + + it "expands indentation" do + source_string = <<~EOM + def foo + if [options.output_format_tty, options.output_format_block].include?(nil) + raise("Bad output mode '\#{v}'; each must be one of \#{lookups.output_formats.keys}.") + end + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new(lines: code_lines[2]) + expand = AroundBlockScan.new(code_lines: code_lines, block: block) + .stop_after_kw + .scan_adjacent_indent + + expect(expand.code_block.to_s).to eq(<<~EOM.indent(2)) + if [options.output_format_tty, options.output_format_block].include?(nil) + raise("Bad output mode '\#{v}'; each must be one of \#{lookups.output_formats.keys}.") + end + EOM + end + + it "can stop before hitting another end" do + source_string = <<~EOM + def lol + end + def foo + puts "lol" + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new(lines: code_lines[3]) + expand = AroundBlockScan.new(code_lines: code_lines, block: block) + expand.stop_after_kw + expand.scan_while { true } + + expect(expand.code_block.to_s).to eq(<<~EOM) + def foo + puts "lol" + end + EOM + end + + it "captures multiple empty and hidden lines" do + source_string = <<~EOM + def foo + Foo.call + + puts "lol" + + end + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new(lines: code_lines[3]) + expand = AroundBlockScan.new(code_lines: code_lines, block: block) + expand.scan_while { true } + + expect(expand.before_index).to eq(0) + expect(expand.after_index).to eq(6) + expect(expand.code_block.to_s).to eq(source_string) + end + + it "only takes what you ask" do + source_string = <<~EOM + def foo + Foo.call + + puts "lol" + + end + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new(lines: code_lines[3]) + expand = AroundBlockScan.new(code_lines: code_lines, block: block) + expand.scan_while { |line| line.not_empty? } + + expect(expand.code_block.to_s).to eq(<<~EOM.indent(4)) + puts "lol" + EOM + end + + it "skips what you want" do + source_string = <<~EOM + def foo + Foo.call + + puts "haha" + # hide me + + puts "lol" + + end + end + EOM + + code_lines = code_line_array(source_string) + code_lines[4].mark_invisible + + block = CodeBlock.new(lines: code_lines[3]) + expand = AroundBlockScan.new(code_lines: code_lines, block: block) + expand.skip(:empty?) + expand.skip(:hidden?) + expand.scan_neighbors + + expect(expand.code_block.to_s).to eq(<<~EOM.indent(4)) + + puts "haha" + + puts "lol" + + EOM + end + end +end diff --git a/spec/syntax_suggest/unit/block_expand_spec.rb b/spec/syntax_suggest/unit/block_expand_spec.rb new file mode 100644 index 0000000000..ba0b0457a1 --- /dev/null +++ b/spec/syntax_suggest/unit/block_expand_spec.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe BlockExpand do + it "captures multiple empty and hidden lines" do + source_string = <<~EOM + def foo + Foo.call + + + puts "lol" + + # hidden + end + end + EOM + + code_lines = code_line_array(source_string) + + code_lines[6].mark_invisible + + block = CodeBlock.new(lines: [code_lines[3]]) + expansion = BlockExpand.new(code_lines: code_lines) + block = expansion.call(block) + + expect(block.to_s).to eq(<<~EOM.indent(4)) + + + puts "lol" + + EOM + end + + it "captures multiple empty lines" do + source_string = <<~EOM + def foo + Foo.call + + + puts "lol" + + end + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new(lines: [code_lines[3]]) + expansion = BlockExpand.new(code_lines: code_lines) + block = expansion.call(block) + + expect(block.to_s).to eq(<<~EOM.indent(4)) + + + puts "lol" + + EOM + end + + it "expands neighbors then indentation" do + source_string = <<~EOM + def foo + Foo.call + puts "hey" + puts "lol" + puts "sup" + end + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new(lines: [code_lines[3]]) + expansion = BlockExpand.new(code_lines: code_lines) + block = expansion.call(block) + + expect(block.to_s).to eq(<<~EOM.indent(4)) + puts "hey" + puts "lol" + puts "sup" + EOM + + block = expansion.call(block) + + expect(block.to_s).to eq(<<~EOM.indent(2)) + Foo.call + puts "hey" + puts "lol" + puts "sup" + end + EOM + end + + it "handles else code" do + source_string = <<~EOM + Foo.call + if blerg + puts "lol" + else + puts "haha" + end + end + EOM + + code_lines = code_line_array(source_string) + block = CodeBlock.new(lines: [code_lines[2]]) + expansion = BlockExpand.new(code_lines: code_lines) + block = expansion.call(block) + + expect(block.to_s).to eq(<<~EOM.indent(2)) + if blerg + puts "lol" + else + puts "haha" + end + EOM + end + + 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] + ) + + expansion = BlockExpand.new(code_lines: code_lines) + block = expansion.call(block) + + expect(block.to_s).to eq(<<~EOM.indent(2)) + Bar.call do + Foo.call + end + EOM + + block = expansion.call(block) + + 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) + expansion = BlockExpand.new(code_lines: code_lines) + + block = CodeBlock.new(lines: code_lines[3]) + block = expansion.call(block) + + expect(block.to_s).to eq(<<~EOM) + + describe "hi" + end + + EOM + + block = expansion.call(block) + + expect(block.to_s).to eq(<<~EOM) + describe "what" do + end + + describe "hi" + end + + it "blerg" do + end + EOM + end + end +end diff --git a/spec/syntax_suggest/unit/capture_code_context_spec.rb b/spec/syntax_suggest/unit/capture_code_context_spec.rb new file mode 100644 index 0000000000..e1bc281c13 --- /dev/null +++ b/spec/syntax_suggest/unit/capture_code_context_spec.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe CaptureCodeContext do + it "capture_before_after_kws" do + source = <<~'EOM' + def sit + end + + def bark + + def eat + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[0]) + + display = CaptureCodeContext.new( + blocks: [block], + code_lines: code_lines + ) + lines = display.call + expect(lines.join).to eq(<<~'EOM') + def sit + end + def bark + def eat + end + EOM + end + + it "handles ambiguous end" do + source = <<~'EOM' + def call # 0 + print "lol" # 1 + end # one # 2 + end # two # 3 + EOM + + code_lines = CleanDocument.new(source: source).call.lines + code_lines[0..2].each(&:mark_invisible) + block = CodeBlock.new(lines: code_lines) + + display = CaptureCodeContext.new( + blocks: [block], + code_lines: code_lines + ) + lines = display.call + + lines = lines.sort.map(&:original) + + expect(lines.join).to eq(<<~'EOM') + def call # 0 + end # one # 2 + end # two # 3 + EOM + end + + it "shows ends of captured block" do + lines = fixtures_dir.join("rexe.rb.txt").read.lines + lines.delete_at(148 - 1) + source = lines.join + + code_lines = CleanDocument.new(source: source).call.lines + + code_lines[0..75].each(&:mark_invisible) + code_lines[77..-1].each(&:mark_invisible) + expect(code_lines.join.strip).to eq("class Lookups") + + block = CodeBlock.new(lines: code_lines[76..149]) + + display = CaptureCodeContext.new( + blocks: [block], + code_lines: code_lines + ) + lines = display.call + + lines = lines.sort.map(&:original) + expect(lines.join).to include(<<~'EOM'.indent(2)) + class Lookups + def format_requires + end + EOM + end + + it "shows ends of captured block" do + source = <<~'EOM' + class Dog + def bark + puts "woof" + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines) + code_lines[1..-1].each(&:mark_invisible) + + expect(block.to_s.strip).to eq("class Dog") + + display = CaptureCodeContext.new( + blocks: [block], + code_lines: 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 + source = <<~'EOM' + class Blerg + end + + class OH + + def hello + it "foo" do + end + end + + class Zerg + end + EOM + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[6]) + + expect(block.to_s.strip).to eq('it "foo" do') + + display = CaptureCodeContext.new( + blocks: [block], + code_lines: 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 + source = <<~'EOM' + class Blerg + end + class OH + + def nope + end + + def lol + end + + end # here + + def haha + end + + def nope + end + end + + class Zerg + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[7..10]) + expect(block.to_s).to eq(<<~'EOM'.indent(2)) + def lol + end + + end # here + EOM + + code_context = CaptureCodeContext.new( + blocks: [block], + code_lines: code_lines + ) + + lines = code_context.call + out = DisplayCodeWithLineNumbers.new( + lines: lines + ).call + + expect(out).to eq(<<~'EOM'.indent(2)) + 3 class OH + 8 def lol + 9 end + 11 end # here + 18 end + EOM + end + end +end diff --git a/spec/syntax_suggest/unit/clean_document_spec.rb b/spec/syntax_suggest/unit/clean_document_spec.rb new file mode 100644 index 0000000000..fa049ad8df --- /dev/null +++ b/spec/syntax_suggest/unit/clean_document_spec.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe CleanDocument do + it "heredocs" do + source = fixtures_dir.join("this_project_extra_def.rb.txt").read + code_lines = CleanDocument.new(source: source).call.lines + + expect(code_lines[18 - 1].to_s).to eq(<<-'EOL') + @io.puts <<~EOM + + SyntaxSuggest: A syntax error was detected + + This code has an unmatched `end` this is caused by either + missing a syntax keyword (`def`, `do`, etc.) or inclusion + of an extra `end` line: + EOM + EOL + expect(code_lines[18].to_s).to eq("") + + expect(code_lines[27 - 1].to_s).to eq(<<-'EOL') + @io.puts(<<~EOM) if filename + file: #{filename} + EOM + EOL + expect(code_lines[27].to_s).to eq("") + + expect(code_lines[31 - 1].to_s).to eq(<<-'EOL') + @io.puts <<~EOM + #{code_with_filename} + EOM + EOL + expect(code_lines[31].to_s).to eq("") + end + + it "joins: multi line methods" do + source = <<~EOM + User + .where(name: 'schneems') + .first + EOM + + doc = CleanDocument.new(source: source).join_consecutive! + + expect(doc.lines[0].to_s).to eq(source) + expect(doc.lines[1].to_s).to eq("") + expect(doc.lines[2].to_s).to eq("") + expect(doc.lines[3]).to eq(nil) + + lines = doc.lines + expect( + DisplayCodeWithLineNumbers.new( + lines: lines + ).call + ).to eq(<<~'EOM'.indent(2)) + 1 User + 2 .where(name: 'schneems') + 3 .first + EOM + + expect( + DisplayCodeWithLineNumbers.new( + lines: lines, + highlight_lines: lines[0] + ).call + ).to eq(<<~'EOM') + ❯ 1 User + ❯ 2 .where(name: 'schneems') + ❯ 3 .first + EOM + end + + it "helper method: take_while_including" do + source = <<~EOM + User + .where(name: 'schneems') + .first + EOM + + doc = CleanDocument.new(source: source) + + lines = doc.take_while_including { |line| !line.to_s.include?("where") } + expect(lines.count).to eq(2) + end + + it "comments: removes comments" do + source = <<~EOM + # lol + puts "what" + # yolo + EOM + + out = CleanDocument.new(source: source).lines.join + expect(out.to_s).to eq(<<~EOM) + + puts "what" + + EOM + end + + it "whitespace: removes whitespace" do + source = " \n" + <<~EOM + puts "what" + EOM + + out = CleanDocument.new(source: source).lines.join + expect(out.to_s).to eq(<<~EOM) + + puts "what" + EOM + + expect(source.lines.first.to_s).to_not eq("\n") + expect(out.lines.first.to_s).to eq("\n") + end + + it "trailing slash: does not join trailing do" do + # Some keywords and syntaxes trigger the "ignored line" + # lex output, we ignore them by filtering by BEG + # + # The `do` keyword is one of these: + # https://gist.github.com/schneems/6a7d7f988d3329fb3bd4b5be3e2efc0c + source = <<~EOM + foo do + puts "lol" + end + EOM + + doc = CleanDocument.new(source: source).join_consecutive! + + expect(doc.lines[0].to_s).to eq(source.lines[0]) + expect(doc.lines[1].to_s).to eq(source.lines[1]) + expect(doc.lines[2].to_s).to eq(source.lines[2]) + end + + it "trailing slash: formats output" do + source = <<~'EOM' + context "timezones workaround" do + it "should receive a time in UTC format and return the time with the"\ + "office's UTC offset substracted from it" do + travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do + office = build(:office) + end + end + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + expect( + DisplayCodeWithLineNumbers.new( + lines: code_lines.select(&:visible?) + ).call + ).to eq(<<~'EOM'.indent(2)) + 1 context "timezones workaround" do + 2 it "should receive a time in UTC format and return the time with the"\ + 3 "office's UTC offset substracted from it" do + 4 travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do + 5 office = build(:office) + 6 end + 7 end + 8 end + EOM + + expect( + DisplayCodeWithLineNumbers.new( + lines: code_lines.select(&:visible?), + highlight_lines: code_lines[1] + ).call + ).to eq(<<~'EOM') + 1 context "timezones workaround" do + ❯ 2 it "should receive a time in UTC format and return the time with the"\ + ❯ 3 "office's UTC offset substracted from it" do + 4 travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do + 5 office = build(:office) + 6 end + 7 end + 8 end + EOM + end + + it "trailing slash: basic detection" do + source = <<~'EOM' + it "trailing s" \ + "lash" do + EOM + + code_lines = CleanDocument.new(source: source).call.lines + + expect(code_lines[0]).to_not be_hidden + expect(code_lines[1]).to be_hidden + + expect( + code_lines.join + ).to eq(code_lines.map(&:original).join) + end + + it "trailing slash: joins multiple lines" do + source = <<~'EOM' + it "should " \ + "keep " \ + "going " do + end + EOM + + doc = CleanDocument.new(source: source).join_trailing_slash! + expect(doc.lines[0].to_s).to eq(source.lines[0..2].join) + expect(doc.lines[1].to_s).to eq("") + expect(doc.lines[2].to_s).to eq("") + expect(doc.lines[3].to_s).to eq(source.lines[3]) + + lines = doc.lines + expect( + DisplayCodeWithLineNumbers.new( + lines: lines + ).call + ).to eq(<<~'EOM'.indent(2)) + 1 it "should " \ + 2 "keep " \ + 3 "going " do + 4 end + EOM + + expect( + DisplayCodeWithLineNumbers.new( + lines: lines, + highlight_lines: lines[0] + ).call + ).to eq(<<~'EOM') + ❯ 1 it "should " \ + ❯ 2 "keep " \ + ❯ 3 "going " do + 4 end + EOM + end + + it "trailing slash: no false positives" do + source = <<~'EOM' + def formatters + @formatters ||= { + amazing_print: ->(obj) { obj.ai + "\n" }, + inspect: ->(obj) { obj.inspect + "\n" }, + json: ->(obj) { obj.to_json }, + marshal: ->(obj) { Marshal.dump(obj) }, + none: ->(_obj) { nil }, + pretty_json: ->(obj) { JSON.pretty_generate(obj) }, + pretty_print: ->(obj) { obj.pretty_inspect }, + puts: ->(obj) { require 'stringio'; sio = StringIO.new; sio.puts(obj); sio.string }, + to_s: ->(obj) { obj.to_s + "\n" }, + yaml: ->(obj) { obj.to_yaml }, + } + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + expect(code_lines.join).to eq(code_lines.join) + end + end +end diff --git a/spec/syntax_suggest/unit/cli_spec.rb b/spec/syntax_suggest/unit/cli_spec.rb new file mode 100644 index 0000000000..fecf3e304c --- /dev/null +++ b/spec/syntax_suggest/unit/cli_spec.rb @@ -0,0 +1,224 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + class FakeExit + def initialize + @called = false + @value = nil + end + + def exit(value = nil) + @called = true + @value = value + end + + def called? + @called + end + + attr_reader :value + end + + RSpec.describe Cli do + it "parses valid code" do + Dir.mktmpdir do |dir| + dir = Pathname(dir) + file = dir.join("script.rb") + file.write("puts 'lol'") + + io = StringIO.new + exit_obj = FakeExit.new + Cli.new( + io: io, + argv: [file.to_s], + exit_obj: exit_obj + ).call + + expect(exit_obj.called?).to be_truthy + expect(exit_obj.value).to eq(0) + expect(io.string.strip).to eq("Syntax OK") + end + end + + it "parses invalid code" do + file = fixtures_dir.join("this_project_extra_def.rb.txt") + + io = StringIO.new + exit_obj = FakeExit.new + Cli.new( + io: io, + argv: [file.to_s], + exit_obj: exit_obj + ).call + + out = io.string + debug_display(out) + + expect(exit_obj.called?).to be_truthy + expect(exit_obj.value).to eq(1) + expect(out.strip).to include("❯ 36 def filename") + end + + it "parses valid code with flags" do + Dir.mktmpdir do |dir| + dir = Pathname(dir) + file = dir.join("script.rb") + file.write("puts 'lol'") + + io = StringIO.new + exit_obj = FakeExit.new + cli = Cli.new( + io: io, + argv: ["--terminal", file.to_s], + exit_obj: exit_obj + ) + cli.call + + expect(exit_obj.called?).to be_truthy + expect(exit_obj.value).to eq(0) + expect(cli.options[:terminal]).to be_truthy + expect(io.string.strip).to eq("Syntax OK") + end + end + + it "errors when no file given" do + io = StringIO.new + exit_obj = FakeExit.new + cli = Cli.new( + io: io, + argv: ["--terminal"], + exit_obj: exit_obj + ) + cli.call + + expect(exit_obj.called?).to be_truthy + expect(exit_obj.value).to eq(1) + expect(io.string.strip).to eq("No file given") + end + + it "errors when file does not exist" do + io = StringIO.new + exit_obj = FakeExit.new + cli = Cli.new( + io: io, + argv: ["lol-i-d-o-not-ex-ist-yololo.txtblerglol"], + exit_obj: exit_obj + ) + cli.call + + expect(exit_obj.called?).to be_truthy + expect(exit_obj.value).to eq(1) + expect(io.string.strip).to include("file not found:") + end + + # We cannot execute the parser here + # because it calls `exit` and it will exit + # our tests, however we can assert that the + # parser has the right value for version + it "-v version" do + io = StringIO.new + exit_obj = FakeExit.new + parser = Cli.new( + io: io, + argv: ["-v"], + exit_obj: exit_obj + ).parser + + expect(parser.version).to include(SyntaxSuggest::VERSION.to_s) + end + + it "SYNTAX_SUGGEST_RECORD_DIR" do + io = StringIO.new + exit_obj = FakeExit.new + cli = Cli.new( + io: io, + argv: [], + env: {"SYNTAX_SUGGEST_RECORD_DIR" => "hahaha"}, + exit_obj: exit_obj + ).parse + + expect(exit_obj.called?).to be_falsey + expect(cli.options[:record_dir]).to eq("hahaha") + end + + it "--record-dir=<dir>" do + io = StringIO.new + exit_obj = FakeExit.new + cli = Cli.new( + io: io, + argv: ["--record=lol"], + exit_obj: exit_obj + ).parse + + expect(exit_obj.called?).to be_falsey + expect(cli.options[:record_dir]).to eq("lol") + end + + it "terminal default to respecting TTY" do + io = StringIO.new + exit_obj = FakeExit.new + cli = Cli.new( + io: io, + argv: [], + exit_obj: exit_obj + ).parse + + expect(exit_obj.called?).to be_falsey + expect(cli.options[:terminal]).to eq(SyntaxSuggest::DEFAULT_VALUE) + end + + it "--terminal" do + io = StringIO.new + exit_obj = FakeExit.new + cli = Cli.new( + io: io, + argv: ["--terminal"], + exit_obj: exit_obj + ).parse + + expect(exit_obj.called?).to be_falsey + expect(cli.options[:terminal]).to be_truthy + end + + it "--no-terminal" do + io = StringIO.new + exit_obj = FakeExit.new + cli = Cli.new( + io: io, + argv: ["--no-terminal"], + exit_obj: exit_obj + ).parse + + expect(exit_obj.called?).to be_falsey + expect(cli.options[:terminal]).to be_falsey + end + + it "--help outputs help" do + io = StringIO.new + exit_obj = FakeExit.new + Cli.new( + io: io, + argv: ["--help"], + exit_obj: exit_obj + ).call + + expect(exit_obj.called?).to be_truthy + expect(io.string).to include("Usage: syntax_suggest <file> [options]") + end + + it "<empty args> outputs help" do + io = StringIO.new + exit_obj = FakeExit.new + Cli.new( + io: io, + argv: [], + exit_obj: exit_obj + ).call + + expect(exit_obj.called?).to be_truthy + expect(io.string).to include("Usage: syntax_suggest <file> [options]") + end + end +end diff --git a/spec/syntax_suggest/unit/code_block_spec.rb b/spec/syntax_suggest/unit/code_block_spec.rb new file mode 100644 index 0000000000..3ab2751b27 --- /dev/null +++ b/spec/syntax_suggest/unit/code_block_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe CodeBlock do + 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(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(lines: code_lines[0]) + block_1 = CodeBlock.new(lines: code_lines[1]) + block_2 = CodeBlock.new(lines: code_lines[2]) + + expect(block_0 <=> block_0.dup).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(lines: CodeLine.new(line: " " * 8 + "foo", index: 4, lex: [])) + array.prepend(block) + expect(array.max).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(lines: code_lines[1]) + expect(block.current_indent).to eq(2) + + block = CodeBlock.new(lines: code_lines[0]) + expect(block.current_indent).to eq(0) + end + + it "knows it's current indentation level when mismatched indents" do + code_lines = code_line_array(<<~EOM) + def foo + puts 'lol' + end + EOM + + block = CodeBlock.new(lines: [code_lines[1], code_lines[2]]) + expect(block.current_indent).to eq(1) + end + + it "before lines and after lines" do + code_lines = code_line_array(<<~EOM) + def foo + bar; end + end + EOM + + block = CodeBlock.new(lines: code_lines[1]) + expect(block.valid?).to be_falsey + end + end +end diff --git a/spec/syntax_suggest/unit/code_frontier_spec.rb b/spec/syntax_suggest/unit/code_frontier_spec.rb new file mode 100644 index 0000000000..c9aba7c8d8 --- /dev/null +++ b/spec/syntax_suggest/unit/code_frontier_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe CodeFrontier do + 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]) + blocks << CodeBlock.new(lines: code_lines[5]) + blocks.each do |b| + frontier << b + end + + expect(frontier.detect_invalid_blocks.sort).to eq(blocks.sort) + 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 "doesn't duplicate blocks" do + code_lines = code_line_array(<<~EOM) + def foo + puts "lol" + puts "lol" + puts "lol" + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + frontier << CodeBlock.new(lines: [code_lines[2]]) + expect(frontier.count).to eq(1) + + frontier << CodeBlock.new(lines: [code_lines[1], code_lines[2], code_lines[3]]) + # expect(frontier.count).to eq(1) + expect(frontier.pop.to_s).to eq(<<~EOM.indent(2)) + puts "lol" + puts "lol" + puts "lol" + EOM + + expect(frontier.pop).to be_nil + + code_lines = code_line_array(<<~EOM) + def foo + puts "lol" + puts "lol" + puts "lol" + end + EOM + + frontier = CodeFrontier.new(code_lines: code_lines) + frontier << CodeBlock.new(lines: [code_lines[2]]) + expect(frontier.count).to eq(1) + + frontier << CodeBlock.new(lines: [code_lines[3]]) + expect(frontier.count).to eq(2) + expect(frontier.pop.to_s).to eq(<<~EOM.indent(2)) + puts "lol" + EOM + 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) + + frontier << CodeBlock.new(lines: code_lines[1]) + 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) + frontier << CodeBlock.new(lines: [code_lines[1]]) + 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 + end +end diff --git a/spec/syntax_suggest/unit/code_line_spec.rb b/spec/syntax_suggest/unit/code_line_spec.rb new file mode 100644 index 0000000000..cc4fa48bc9 --- /dev/null +++ b/spec/syntax_suggest/unit/code_line_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe CodeLine do + it "bug in keyword detection" do + lines = CodeLine.from_source(<<~'EOM') + def to_json(*opts) + { + type: :module, + }.to_json(*opts) + end + EOM + expect(lines.count(&:is_kw?)).to eq(1) + expect(lines.count(&:is_end?)).to eq(1) + end + + it "supports endless method definitions" do + skip("Unsupported ruby version") unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3") + + line = CodeLine.from_source(<<~'EOM').first + def square(x) = x * x + EOM + + expect(line.is_kw?).to be_falsey + expect(line.is_end?).to be_falsey + end + + it "retains original line value, after being marked invisible" do + line = CodeLine.from_source(<<~'EOM').first + puts "lol" + EOM + expect(line.line).to match('puts "lol"') + line.mark_invisible + expect(line.line).to eq("") + expect(line.original).to match('puts "lol"') + end + + it "knows which lines can be joined" do + code_lines = CodeLine.from_source(<<~'EOM') + user = User. + where(name: 'schneems'). + first + puts user.name + EOM + + # Indicates line 1 can join 2, 2 can join 3, but 3 won't join it's next line + expect(code_lines.map(&:ignore_newline_not_beg?)).to eq([true, true, false, false]) + end + it "trailing if" do + code_lines = CodeLine.from_source(<<~'EOM') + puts "lol" if foo + if foo + end + EOM + + expect(code_lines.map(&:is_kw?)).to eq([false, true, false]) + end + + it "trailing unless" do + code_lines = CodeLine.from_source(<<~'EOM') + puts "lol" unless foo + unless foo + end + EOM + + expect(code_lines.map(&:is_kw?)).to eq([false, true, false]) + end + + it "trailing slash" do + code_lines = CodeLine.from_source(<<~'EOM') + it "trailing s" \ + "lash" do + EOM + + expect(code_lines.map(&:trailing_slash?)).to eq([true, false]) + + code_lines = CodeLine.from_source(<<~'EOM') + amazing_print: ->(obj) { obj.ai + "\n" }, + EOM + expect(code_lines.map(&:trailing_slash?)).to eq([false]) + end + + it "knows it's got an end" do + line = CodeLine.from_source(" end").first + + expect(line.is_end?).to be_truthy + expect(line.is_kw?).to be_falsey + end + + it "knows it's got a keyword" do + line = CodeLine.from_source(" if").first + + expect(line.is_end?).to be_falsey + expect(line.is_kw?).to be_truthy + end + + it "ignores marked lines" do + code_lines = CodeLine.from_source(<<~EOM) + def foo + Array(value) |x| + end + end + EOM + + expect(SyntaxSuggest.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(SyntaxSuggest.valid?(code_lines)).to be_falsey + end + + it "knows empty lines" do + code_lines = CodeLine.from_source(<<~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| SyntaxSuggest.valid?(l) }).to eq([true, true, true]) + end + + it "counts indentations" do + code_lines = CodeLine.from_source(<<~EOM) + def foo + Array(value) |x| + puts 'lol' + end + end + EOM + + expect(code_lines.map(&:indent)).to eq([0, 2, 4, 2, 0]) + end + + it "doesn't count empty lines as having an indentation" do + code_lines = CodeLine.from_source(<<~EOM) + + + EOM + + expect(code_lines.map(&:indent)).to eq([0, 0]) + end + end +end diff --git a/spec/syntax_suggest/unit/code_search_spec.rb b/spec/syntax_suggest/unit/code_search_spec.rb new file mode 100644 index 0000000000..b62b2c0a3c --- /dev/null +++ b/spec/syntax_suggest/unit/code_search_spec.rb @@ -0,0 +1,505 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe CodeSearch do + it "rexe regression" 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(<<~'EOM'.strip) + class Lookups + EOM + end + + it "squished do regression" do + source = <<~'EOM' + def call + trydo + + @options = CommandLineParser.new.parse + + options.requires.each { |r| require!(r) } + load_global_config_if_exists + options.loads.each { |file| load(file) } + + @user_source_code = ARGV.join(' ') + @user_source_code = 'self' if @user_source_code == '' + + @callable = create_callable + + init_rexe_context + init_parser_and_formatters + + # This is where the user's source code will be executed; the action will in turn call `execute`. + lookup_action(options.input_mode).call unless options.noop + + output_log_entry + end # one + end # two + EOM + + search = CodeSearch.new(source) + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2)) + trydo + end # one + EOM + end + + it "regression test ambiguous end" do + source = <<~'EOM' + def call # 0 + print "lol" # 1 + end # one # 2 + end # two # 3 + EOM + + search = CodeSearch.new(source) + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM') + end # two # 3 + EOM + end + + it "regression dog test" do + source = <<~'EOM' + class Dog + def bark + puts "woof" + end + EOM + search = CodeSearch.new(source) + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM') + class Dog + EOM + expect(search.invalid_blocks.first.lines.length).to eq(4) + end + + it "handles mismatched |" do + source = <<~EOM + class Blerg + Foo.call do |a + end # one + + puts lol + class Foo + end # two + end # three + EOM + search = CodeSearch.new(source) + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2)) + Foo.call do |a + end # one + EOM + end + + it "handles mismatched }" do + source = <<~EOM + class Blerg + Foo.call do { + + puts lol + class Foo + end # two + end # three + EOM + search = CodeSearch.new(source) + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2)) + Foo.call do { + EOM + end + + it "handles no spaces between blocks and trailing slash" do + source = <<~'EOM' + require "rails_helper" + RSpec.describe Foo, type: :model do + describe "#bar" do + context "context" do + it "foos the bar with a foo and then bazes the foo with a bar to"\ + "fooify the barred bar" do + travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do + foo = build(:foo) + end + end + end + end + describe "#baz?" do + context "baz has barred the foo" do + it "returns true" do # <== HERE + end + end + 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 + source = <<~'EOM' + context "foo bar" do + it "bars the foo" do + travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do + end + end + end + context "test" do + it "should" do + end + EOM + search = CodeSearch.new(source) + search.call + + expect(search.invalid_blocks.join.strip).to eq('it "should" do') + end + + it "records debugging steps to a directory" do + Dir.mktmpdir do |dir| + dir = Pathname(dir) + search = CodeSearch.new(<<~'EOM', record_dir: dir) + class OH + def hello + def hai + end + end + EOM + search.call + + expect(search.record_dir.entries.map(&:to_s)).to include("1-add-1-(3__4).txt") + expect(search.record_dir.join("1-add-1-(3__4).txt").read).to include(<<~EOM) + 1 class OH + 2 def hello + ❯ 3 def hai + ❯ 4 end + 5 end + EOM + end + end + + it "def with missing end" do + search = CodeSearch.new(<<~'EOM') + class OH + def hello + + def hai + puts "lol" + end + end + EOM + search.call + + expect(search.invalid_blocks.join.strip).to eq("def hello") + + search = CodeSearch.new(<<~'EOM') + class OH + def hello + + def hai + end + end + EOM + search.call + + expect(search.invalid_blocks.join.strip).to eq("def hello") + + search = CodeSearch.new(<<~'EOM') + class OH + def hello + def hai + end + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2)) + def hello + EOM + end + + describe "real world cases" do + 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 + + 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 + + it "Format Code blocks real world example" do + search = CodeSearch.new(<<~'EOM') + require 'rails_helper' + + RSpec.describe AclassNameHere, type: :worker do + describe "thing" do + context "when" do + let(:thing) { stuff } + let(:another_thing) { moarstuff } + subject { foo.new.perform(foo.id, true) } + + it "stuff" do + subject + + expect(foo.foo.foo).to eq(true) + end + end + end # line 16 accidental end, but valid block + + context "stuff" do + let(:thing) { create(:foo, foo: stuff) } + let(:another_thing) { create(:stuff) } + + subject { described_class.new.perform(foo.id, false) } + + it "more stuff" do + subject + + expect(foo.foo.foo).to eq(false) + end + end + end # mismatched due to 16 + end + EOM + search.call + + 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 + ❯ 4 describe "thing" do + ❯ 16 end # line 16 accidental end, but valid block + ❯ 30 end # mismatched due to 16 + 31 end + EOM + end + end + + # For code that's not perfectly formatted, we ideally want to do our best + # These examples represent the results that exist today, but I would like to improve upon them + describe "needs improvement" do + describe "mis-matched-indentation" do + it "extra space before end" do + search = CodeSearch.new(<<~'EOM') + Foo.call + def foo + puts "lol" + puts "lol" + end # one + end # two + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM') + Foo.call + end # two + EOM + end + + it "stacked ends 2" do + search = CodeSearch.new(<<~'EOM') + def cat + blerg + end + + Foo.call do + end # one + end # two + + def dog + end + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM') + Foo.call do + end # one + end # two + + EOM + end + + it "stacked ends " 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 "missing space before end" do + search = CodeSearch.new(<<~'EOM') + Foo.call + + def foo + puts "lol" + puts "lol" + end + end + EOM + search.call + + # expand-1 and expand-2 seem to be broken? + expect(search.invalid_blocks.join).to eq(<<~'EOM') + Foo.call + end + EOM + end + end + 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 # one + end # two + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM') + Foo.call + end # two + 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 # one + end # two + EOM + search.call + + expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2)) + end # one + 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 + + it "expands frontier by eliminating valid lines" do + search = CodeSearch.new(<<~'EOM') + def foo + puts 'lol' + end + EOM + search.create_blocks_from_untracked_lines + + expect(search.code_lines.join).to eq(<<~'EOM') + def foo + end + EOM + end + end +end diff --git a/spec/syntax_suggest/unit/display_invalid_blocks_spec.rb b/spec/syntax_suggest/unit/display_invalid_blocks_spec.rb new file mode 100644 index 0000000000..c696132782 --- /dev/null +++ b/spec/syntax_suggest/unit/display_invalid_blocks_spec.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe DisplayInvalidBlocks do + it "works with valid code" do + syntax_string = <<~EOM + class OH + def hello + end + def hai + end + end + EOM + + search = CodeSearch.new(syntax_string) + search.call + + io = StringIO.new + display = DisplayInvalidBlocks.new( + io: io, + blocks: search.invalid_blocks, + terminal: false, + code_lines: search.code_lines + ) + display.call + expect(io.string).to include("Syntax OK") + end + + it "selectively prints to terminal if input is a tty by default" do + source = <<~EOM + class OH + def hello + def hai + end + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + + io = StringIO.new + def io.isatty + true + end + + block = CodeBlock.new(lines: code_lines[1]) + display = DisplayInvalidBlocks.new( + io: io, + blocks: block, + code_lines: code_lines + ) + display.call + expect(io.string).to include([ + "❯ 2 ", + DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT, + " def hello" + ].join) + + io = StringIO.new + def io.isatty + false + end + + block = CodeBlock.new(lines: code_lines[1]) + display = DisplayInvalidBlocks.new( + io: io, + blocks: block, + code_lines: code_lines + ) + display.call + expect(io.string).to include("❯ 2 def hello") + end + + it "outputs to io when using `call`" do + source = <<~EOM + class OH + def hello + def hai + end + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + + io = StringIO.new + block = CodeBlock.new(lines: code_lines[1]) + display = DisplayInvalidBlocks.new( + io: io, + blocks: block, + terminal: false, + code_lines: code_lines + ) + display.call + expect(io.string).to include("❯ 2 def hello") + end + + it " wraps code with github style codeblocks" do + source = <<~EOM + class OH + def hello + + def hai + end + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[1]) + io = StringIO.new + DisplayInvalidBlocks.new( + io: io, + blocks: block, + terminal: false, + code_lines: code_lines + ).call + expect(io.string).to include(<<~EOM) + 1 class OH + ❯ 2 def hello + 4 def hai + 5 end + 6 end + EOM + end + + it "shows terminal characters" do + code_lines = code_line_array(<<~EOM) + class OH + def hello + def hai + end + end + EOM + + io = StringIO.new + block = CodeBlock.new(lines: code_lines[1]) + DisplayInvalidBlocks.new( + io: io, + blocks: block, + terminal: false, + code_lines: code_lines + ).call + + expect(io.string).to include([ + " 1 class OH", + "❯ 2 def hello", + " 4 end", + " 5 end", + "" + ].join($/)) + + block = CodeBlock.new(lines: code_lines[1]) + io = StringIO.new + DisplayInvalidBlocks.new( + io: io, + blocks: block, + terminal: true, + code_lines: code_lines + ).call + + expect(io.string).to include( + [ + " 1 class OH", + ["❯ 2 ", DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT, " def hello"].join, + " 4 end", + " 5 end", + "" + ].join($/ + DisplayCodeWithLineNumbers::TERMINAL_END) + ) + end + end +end diff --git a/spec/syntax_suggest/unit/explain_syntax_spec.rb b/spec/syntax_suggest/unit/explain_syntax_spec.rb new file mode 100644 index 0000000000..394981dcf6 --- /dev/null +++ b/spec/syntax_suggest/unit/explain_syntax_spec.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe "ExplainSyntax" do + it "handles shorthand syntaxes with non-bracket characters" do + source = <<~EOM + %Q* lol + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq([]) + expect(explain.errors.join).to include("unterminated string") + end + + it "handles %w[]" do + source = <<~EOM + node.is_a?(Op) && %w[| ||].include?(node.value) && + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq([]) + end + + it "doesn't falsely identify strings or symbols as critical chars" do + source = <<~EOM + a = ['(', '{', '[', '|'] + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq([]) + + source = <<~EOM + a = [:'(', :'{', :'[', :'|'] + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq([]) + end + + it "finds missing |" do + source = <<~EOM + Foo.call do | + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["|"]) + expect(explain.errors).to eq([explain.why("|")]) + end + + it "finds missing {" do + source = <<~EOM + class Cat + lol = { + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["}"]) + expect(explain.errors).to eq([explain.why("}")]) + end + + it "finds missing }" do + source = <<~EOM + def foo + lol = "foo" => :bar } + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["{"]) + expect(explain.errors).to eq([explain.why("{")]) + end + + it "finds missing [" do + source = <<~EOM + class Cat + lol = [ + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["]"]) + expect(explain.errors).to eq([explain.why("]")]) + end + + it "finds missing ]" do + source = <<~EOM + def foo + lol = ] + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["["]) + expect(explain.errors).to eq([explain.why("[")]) + end + + it "finds missing (" do + source = "def initialize; ); end" + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["("]) + expect(explain.errors).to eq([explain.why("(")]) + end + + it "finds missing )" do + source = "def initialize; (; end" + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq([")"]) + expect(explain.errors).to eq([explain.why(")")]) + end + + it "finds missing keyword" do + source = <<~EOM + class Cat + end + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["keyword"]) + expect(explain.errors).to eq([explain.why("keyword")]) + end + + it "finds missing end" do + source = <<~EOM + class Cat + def meow + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["end"]) + expect(explain.errors).to eq([explain.why("end")]) + end + + it "falls back to ripper on unknown errors" do + source = <<~EOM + class Cat + def meow + 1 * + end + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq([]) + expect(explain.errors).to eq(RipperErrors.new(source).call.errors) + end + + it "handles an unexpected rescue" do + source = <<~EOM + def foo + if bar + "baz" + else + "foo" + rescue FooBar + nil + end + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["end"]) + end + + # String embeds are `"#{foo} <-- here` + # + # We need to count a `#{` as a `{` + # otherwise it will report that we are + # missing a curly when we are using valid + # string embed syntax + it "is not confused by valid string embed" do + source = <<~'EOM' + foo = "#{hello}" + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + expect(explain.missing).to eq([]) + end + + # Missing string embed beginnings are not a + # syntax error. i.e. `"foo}"` or `"{foo}` or "#foo}" + # would just be strings with extra characters. + # + # However missing the end curly will trigger + # an error: i.e. `"#{foo` + # + # String embed beginning is a `#{` rather than + # a `{`, make sure we handle that case and + # report the correct missing `}` diagnosis + it "finds missing string embed end" do + source = <<~'EOM' + "#{foo + EOM + + explain = ExplainSyntax.new( + code_lines: CodeLine.from_source(source) + ).call + + expect(explain.missing).to eq(["}"]) + end + end +end diff --git a/spec/syntax_suggest/unit/lex_all_spec.rb b/spec/syntax_suggest/unit/lex_all_spec.rb new file mode 100644 index 0000000000..0c0df7cfaa --- /dev/null +++ b/spec/syntax_suggest/unit/lex_all_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe "EndBlockParse" do + it "finds blocks based on `end` keyword" do + source = <<~EOM + describe "cat" # 1 + Cat.call do # 2 + end # 3 + end # 4 + # 5 + it "dog" do # 6 + Dog.call do # 7 + end # 8 + end # 9 + EOM + + # raw_lex = Ripper.lex(source) + # expect(raw_lex.to_s).to_not include("dog") + + lex = LexAll.new(source: source) + expect(lex.map(&:token).to_s).to include("dog") + expect(lex.first.line).to eq(1) + expect(lex.last.line).to eq(9) + end + end +end diff --git a/spec/syntax_suggest/unit/pathname_from_message_spec.rb b/spec/syntax_suggest/unit/pathname_from_message_spec.rb new file mode 100644 index 0000000000..76756efda9 --- /dev/null +++ b/spec/syntax_suggest/unit/pathname_from_message_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe "PathnameFromMessage" do + it "handles filenames with colons in them" do + Dir.mktmpdir do |dir| + dir = Pathname(dir) + + file = dir.join("scr:atch.rb").tap { |p| FileUtils.touch(p) } + + message = "#{file}:2:in `require_relative': /private/tmp/bad.rb:1: syntax error, unexpected `end' (SyntaxError)" + file = PathnameFromMessage.new(message).call.name + + expect(file).to be_truthy + end + end + + it "checks if the file exists" do + Dir.mktmpdir do |dir| + dir = Pathname(dir) + + file = dir.join("scratch.rb") + # No touch, file does not exist + expect(file.exist?).to be_falsey + + message = "#{file}:2:in `require_relative': /private/tmp/bad.rb:1: syntax error, unexpected `end' (SyntaxError)" + io = StringIO.new + file = PathnameFromMessage.new(message, io: io).call.name + + expect(io.string).to include(file.to_s) + expect(file).to be_falsey + end + end + + it "does not output error message on syntax error inside of an (eval)" do + message = "(eval):1: invalid multibyte char (UTF-8) (SyntaxError)\n" + io = StringIO.new + file = PathnameFromMessage.new(message, io: io).call.name + + expect(io.string).to eq("") + expect(file).to be_falsey + end + + it "does not output error message on syntax error inside of streamed code" do + # An example of streamed code is: $ echo "def foo" | ruby + message = "-:1: syntax error, unexpected end-of-input\n" + io = StringIO.new + file = PathnameFromMessage.new(message, io: io).call.name + + expect(io.string).to eq("") + expect(file).to be_falsey + end + end +end diff --git a/spec/syntax_suggest/unit/priority_queue_spec.rb b/spec/syntax_suggest/unit/priority_queue_spec.rb new file mode 100644 index 0000000000..17361833e5 --- /dev/null +++ b/spec/syntax_suggest/unit/priority_queue_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + class CurrentIndex + attr_reader :current_indent + + def initialize(value) + @current_indent = value + end + + def <=>(other) + @current_indent <=> other.current_indent + end + + def inspect + @current_indent + end + end + + RSpec.describe CodeFrontier do + it "works" do + q = PriorityQueue.new + q << 1 + q << 2 + expect(q.elements).to eq([2, 1]) + + q << 3 + expect(q.elements).to eq([3, 1, 2]) + + expect(q.pop).to eq(3) + expect(q.pop).to eq(2) + expect(q.pop).to eq(1) + expect(q.pop).to eq(nil) + + array = [] + q = PriorityQueue.new + array.reverse_each do |v| + q << v + end + expect(q.elements).to eq(array) + + array = [100, 36, 17, 19, 25, 0, 3, 1, 7, 2] + array.reverse_each do |v| + q << v + end + + expect(q.pop).to eq(100) + expect(q.elements).to eq([36, 25, 19, 17, 0, 1, 7, 2, 3]) + + # expected [36, 25, 19, 17, 0, 1, 7, 2, 3] + expect(q.pop).to eq(36) + expect(q.pop).to eq(25) + expect(q.pop).to eq(19) + expect(q.pop).to eq(17) + expect(q.pop).to eq(7) + expect(q.pop).to eq(3) + expect(q.pop).to eq(2) + expect(q.pop).to eq(1) + expect(q.pop).to eq(0) + expect(q.pop).to eq(nil) + end + + it "priority queue" do + frontier = PriorityQueue.new + frontier << CurrentIndex.new(0) + frontier << CurrentIndex.new(1) + + expect(frontier.sorted.map(&:current_indent)).to eq([0, 1]) + + frontier << CurrentIndex.new(1) + expect(frontier.sorted.map(&:current_indent)).to eq([0, 1, 1]) + + frontier << CurrentIndex.new(0) + expect(frontier.sorted.map(&:current_indent)).to eq([0, 0, 1, 1]) + + frontier << CurrentIndex.new(10) + expect(frontier.sorted.map(&:current_indent)).to eq([0, 0, 1, 1, 10]) + + frontier << CurrentIndex.new(2) + expect(frontier.sorted.map(&:current_indent)).to eq([0, 0, 1, 1, 2, 10]) + + frontier = PriorityQueue.new + values = [18, 18, 0, 18, 0, 18, 18, 18, 18, 16, 18, 8, 18, 8, 8, 8, 16, 6, 0, 0, 16, 16, 4, 14, 14, 12, 12, 12, 10, 12, 12, 12, 12, 8, 10, 10, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 8, 10, 6, 6, 6, 6, 6, 6, 8, 10, 8, 8, 10, 8, 10, 8, 10, 8, 6, 8, 8, 6, 8, 6, 6, 8, 0, 8, 0, 0, 8, 8, 0, 8, 0, 8, 8, 0, 8, 8, 8, 0, 8, 0, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 6, 8, 6, 6, 6, 6, 8, 6, 8, 6, 6, 4, 4, 6, 6, 4, 6, 4, 6, 6, 4, 6, 4, 4, 6, 6, 6, 6, 4, 4, 4, 2, 4, 4, 4, 4, 4, 4, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 6, 6, 2] + + values.each do |v| + value = CurrentIndex.new(v) + frontier << value # CurrentIndex.new(v) + end + + expect(frontier.sorted.map(&:current_indent)).to eq(values.sort) + end + end +end |