From 653305cf3bf14a9310cf242f0a4a7a86c4170974 Mon Sep 17 00:00:00 2001 From: schneems Date: Thu, 10 Dec 2020 10:42:50 -0600 Subject: [PATCH 1/3] 5 second default search timeout --- lib/syntax_search.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/syntax_search.rb b/lib/syntax_search.rb index c58a844..9dd20c6 100644 --- a/lib/syntax_search.rb +++ b/lib/syntax_search.rb @@ -6,10 +6,12 @@ require 'stringio' require 'pathname' require 'ripper' +require 'timeout' module SyntaxErrorSearch class Error < StandardError; end SEARCH_SOURCE_ON_ERROR_DEFAULT = true + TIMEOUT_DEFAULT = ENV.fetch("SYNTAX_SEARCH_TIMEOUT", 5).to_i def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT) raise e if !e.message.include?("end-of-input") @@ -32,8 +34,11 @@ def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT) raise e end - def self.call(source: , filename: , terminal: false, record_dir: nil) - search = CodeSearch.new(source, record_dir: record_dir).call + def self.call(source: , filename: , terminal: false, record_dir: nil, timeout: TIMEOUT_DEFAULT) + search = nil + Timeout.timeout(timeout) do + search = CodeSearch.new(source, record_dir: record_dir).call + end blocks = search.invalid_blocks DisplayInvalidBlocks.new( @@ -44,6 +49,8 @@ def self.call(source: , filename: , terminal: false, record_dir: nil) invalid_type: invalid_type(source), io: $stderr ).call + rescue Timeout::Error + $stderr.puts "Syntax search timed out SYNTAX_SEARCH_TIMEOUT=#{timeout}" end # Used for counting spaces From 0b915d3d7673275da17713003fcdbc803b981f1f Mon Sep 17 00:00:00 2001 From: schneems Date: Thu, 10 Dec 2020 11:08:00 -0600 Subject: [PATCH 2/3] scratch.rb --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 98af4ab..192d9af 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /pkg/ /spec/reports/ /tmp/ +scratch.rb # rspec failure tracking .rspec_status From 4853d6359bdfb8ac575d71b225e767dc213c1ba6 Mon Sep 17 00:00:00 2001 From: schneems Date: Thu, 10 Dec 2020 11:08:08 -0600 Subject: [PATCH 3/3] [Close #21] Concat lines with trailing slash Ruby lines that end with a slash `\` indicate that the next line should be concatenated with the current line so: ```ruby if "trailing" \ "line" do end ``` Is functionally equivalent to: ```ruby if "trailing" "line" do endf ``` We can replicate this logic by taking note when a line ends with a trailing slash and then concatenating it with the next line. Functionally the TrailingSlashJoin class does this by concatenating the lines, and then hiding the original line. To allow for output, the DisplayCodeWithLineNumbers is now aware that a code line may contain multiple code lines, and it will split and render each. --- CHANGELOG.md | 2 + lib/syntax_search.rb | 3 +- lib/syntax_search/code_line.rb | 31 +++++-- lib/syntax_search/code_search.rb | 5 +- .../display_code_with_line_numbers.rb | 53 +++++++---- lib/syntax_search/trailing_slash_join.rb | 53 +++++++++++ spec/unit/code_line_spec.rb | 14 +++ spec/unit/code_search_spec.rb | 27 ++++++ spec/unit/trailing_slash_join_spec.rb | 91 +++++++++++++++++++ 9 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 lib/syntax_search/trailing_slash_join.rb create mode 100644 spec/unit/trailing_slash_join_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index b1a8fec..c40f54a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## HEAD (unreleased) +- Trailing slashes are now handled (joined) before the code search (https://github.com/zombocom/syntax_search/pull/28) + ## 0.2.0 - Simplify large file output so minimal context around the invalid section is shown (https://github.com/zombocom/syntax_search/pull/26) diff --git a/lib/syntax_search.rb b/lib/syntax_search.rb index 9dd20c6..bc4442d 100644 --- a/lib/syntax_search.rb +++ b/lib/syntax_search.rb @@ -50,7 +50,7 @@ def self.call(source: , filename: , terminal: false, record_dir: nil, timeout: T io: $stderr ).call rescue Timeout::Error - $stderr.puts "Syntax search timed out SYNTAX_SEARCH_TIMEOUT=#{timeout}" + $stderr.puts "Syntax search timed out SYNTAX_SEARCH_TIMEOUT=#{timeout}, run with DEBUG=1 for more info" end # Used for counting spaces @@ -150,3 +150,4 @@ def self.invalid_type(source) require_relative "syntax_search/who_dis_syntax_error" require_relative "syntax_search/heredoc_block_parse" require_relative "syntax_search/lex_all" +require_relative "syntax_search/trailing_slash_join" diff --git a/lib/syntax_search/code_line.rb b/lib/syntax_search/code_line.rb index 423e1e2..b5f7ea1 100644 --- a/lib/syntax_search/code_line.rb +++ b/lib/syntax_search/code_line.rb @@ -29,6 +29,8 @@ module SyntaxErrorSearch # Marking a line as invisible also lets the overall program know # that it should not check that area for syntax errors. class CodeLine + TRAILING_SLASH = ("\\" + $/).freeze + attr_reader :line, :index, :indent, :original_line def initialize(line: , index:) @@ -40,24 +42,34 @@ def initialize(line: , index:) @status = nil # valid, invalid, unknown @invalid = false - @kw_count = 0 - @end_count = 0 - @lex = LexAll.new(source: line) - @lex.each do |lex| + lex_detect! + end + + private def lex_detect! + lex = LexAll.new(source: line) + kw_count = 0 + end_count = 0 + lex.each do |lex| next unless lex.type == :on_kw case lex.token when 'def', 'case', 'for', 'begin', 'class', 'module', 'if', 'unless', 'while', 'until' , 'do' - @kw_count += 1 + kw_count += 1 when 'end' - @end_count += 1 + end_count += 1 end end - @is_comment = true if @lex.detect {|lex| lex.type != :on_sp}&.type == :on_comment + @is_kw = (kw_count - end_count) > 0 + @is_end = (end_count - kw_count) > 0 + @is_comment = lex.detect {|lex| lex.type != :on_sp}&.type == :on_comment + @is_trailing_slash = lex.last.token == TRAILING_SLASH + end + + alias :original :original_line - @is_kw = (@kw_count - @end_count) > 0 - @is_end = (@end_count - @kw_count) > 0 + def trailing_slash? + @is_trailing_slash end def <=>(b) @@ -110,7 +122,6 @@ def hidden? def line_number index + 1 end - alias :number :line_number def not_empty? diff --git a/lib/syntax_search/code_search.rb b/lib/syntax_search/code_search.rb index 6370230..485e887 100644 --- a/lib/syntax_search/code_search.rb +++ b/lib/syntax_search/code_search.rb @@ -35,9 +35,12 @@ def initialize(source, record_dir: ENV["SYNTAX_SEARCH_RECORD_DIR"] || ENV["DEBUG @record_dir = Pathname(record_dir).join(@time).tap {|p| p.mkpath } @write_count = 0 end - @code_lines = source.lines.map.with_index do |line, i| + code_lines = source.lines.map.with_index do |line, i| CodeLine.new(line: line, index: i) end + + @code_lines = TrailingSlashJoin.new(code_lines: code_lines).call + @frontier = CodeFrontier.new(code_lines: @code_lines) @invalid_blocks = [] @name_tick = Hash.new {|hash, k| hash[k] = 0 } diff --git a/lib/syntax_search/display_code_with_line_numbers.rb b/lib/syntax_search/display_code_with_line_numbers.rb index 998c57f..ca3015a 100644 --- a/lib/syntax_search/display_code_with_line_numbers.rb +++ b/lib/syntax_search/display_code_with_line_numbers.rb @@ -24,33 +24,48 @@ class DisplayCodeWithLineNumbers TERMINAL_END = "\e[0m" def initialize(lines: , highlight_lines: [], terminal: false) - @lines = lines.sort + @lines = Array(lines).sort @terminal = terminal - @highlight_line_hash = highlight_lines.each_with_object({}) {|line, h| h[line] = true } + @highlight_line_hash = Array(highlight_lines).each_with_object({}) {|line, h| h[line] = true } @digit_count = @lines.last&.line_number.to_s.length end def call @lines.map do |line| - string = String.new("") - if @highlight_line_hash[line] - string << "❯ " - else - string << " " - end + format_line(line) + end.join + end - number = line.line_number.to_s.rjust(@digit_count) - string << number.to_s - if line.empty? - string << line.original_line - else - string << " " - string << TERMINAL_HIGHLIGHT if @terminal && @highlight_line_hash[line] # Bold, italics - string << line.original_line - string << TERMINAL_END if @terminal - end - string + private def format_line(code_line) + # Handle trailing slash lines + code_line.original.lines.map.with_index do |contents, i| + format( + empty: code_line.empty?, + number: (code_line.number + i).to_s, + contents: contents, + highlight: @highlight_line_hash[code_line] + ) end.join end + + private def format(contents: , number: , highlight: false, empty:) + string = String.new("") + if highlight + string << "❯ " + else + string << " " + end + + string << number.rjust(@digit_count).to_s + if empty + string << contents + else + string << " " + string << TERMINAL_HIGHLIGHT if @terminal && highlight + string << contents + string << TERMINAL_END if @terminal + end + string + end end end diff --git a/lib/syntax_search/trailing_slash_join.rb b/lib/syntax_search/trailing_slash_join.rb new file mode 100644 index 0000000..abe7820 --- /dev/null +++ b/lib/syntax_search/trailing_slash_join.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module SyntaxErrorSearch + # Handles code that contains trailing slashes + # by turning multiple lines with trailing slash(es) into + # a single code line + # + # expect(code_lines.join).to eq(<<~EOM) + # it "trailing \ + # "slash" do + # end + # EOM + # + # lines = TrailngSlashJoin(code_lines: code_lines).call + # expect(lines.first.to_s).to eq(<<~EOM) + # it "trailing \ + # "slash" do + # EOM + # + class TrailingSlashJoin + def initialize(code_lines:) + @code_lines = code_lines + @code_lines_dup = code_lines.dup + end + + def call + @trailing_lines = [] + @code_lines.select(&:trailing_slash?).each do |trailing| + stop_next = false + lines = @code_lines[trailing.index..-1].take_while do |line| + next false if stop_next + + if !line.trailing_slash? + stop_next = true + end + + true + end + + joined_line = CodeLine.new(line: lines.map(&:original_line).join, index: trailing.index) + + @code_lines_dup[trailing.index] = joined_line + + @trailing_lines << joined_line + + lines.shift # Don't hide first trailing slash line + lines.each(&:mark_invisible) + end + + return @code_lines_dup + end + end +end diff --git a/spec/unit/code_line_spec.rb b/spec/unit/code_line_spec.rb index 73664d9..63c449a 100644 --- a/spec/unit/code_line_spec.rb +++ b/spec/unit/code_line_spec.rb @@ -4,6 +4,20 @@ module SyntaxErrorSearch RSpec.describe CodeLine do + it "trailing slash" do + code_lines = code_line_array(<<~'EOM') + it "trailing s" \ + "lash" do + EOM + + expect(code_lines.map(&:trailing_slash?)).to eq([true, false]) + + code_lines = code_line_array(<<~'EOM') + amazing_print: ->(obj) { obj.ai + "\n" }, + EOM + expect(code_lines.map(&:trailing_slash?)).to eq([false]) + end + it "knows it's a comment" do line = CodeLine.new(line: " # iama comment", index: 0) expect(line.is_comment?).to be_truthy diff --git a/spec/unit/code_search_spec.rb b/spec/unit/code_search_spec.rb index 9f9f6c2..a80eeb6 100644 --- a/spec/unit/code_search_spec.rb +++ b/spec/unit/code_search_spec.rb @@ -4,6 +4,33 @@ module SyntaxErrorSearch RSpec.describe CodeSearch do + it "handles no spaces between blocks" do + search = CodeSearch.new(<<~'EOM') + require "rails_helper" + RSpec.describe TelehealthAppointment, type: :model do + describe "#participants_state" do + 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 + end + describe "#past?" do + context "more than 15 min have passed since appointment start time" do + it "returns true" do # <== HERE + end + end + end + EOM + + search.call + + expect(search.invalid_blocks.join.strip).to eq('it "returns true" do # <== HERE') + end + it "handles no spaces between blocks" do search = CodeSearch.new(<<~EOM) context "timezones workaround" do diff --git a/spec/unit/trailing_slash_join_spec.rb b/spec/unit/trailing_slash_join_spec.rb new file mode 100644 index 0000000..bbb4e51 --- /dev/null +++ b/spec/unit/trailing_slash_join_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require_relative "../spec_helper.rb" + +module SyntaxErrorSearch + RSpec.describe TrailingSlashJoin do + + it "formats output" do + code_lines = code_line_array(<<~'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 + + out_code_lines = TrailingSlashJoin.new(code_lines: code_lines).call + expect( + DisplayCodeWithLineNumbers.new( + lines: out_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: out_code_lines.select(&:visible?), + highlight_lines: out_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" do + code_lines = code_line_array(<<~'EOM') + it "trailing s" \ + "lash" do + EOM + + out_code_lines = TrailingSlashJoin.new(code_lines: code_lines).call + + expect(code_lines[0]).to_not be_hidden + expect(code_lines[1]).to be_hidden + + expect( + out_code_lines.join + ).to eq(code_lines.map(&:original).join) + end + + it "doesn't falsely identify trailing slashes" do + code_lines = code_line_array(<<~'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 + + out_code_lines = TrailingSlashJoin.new(code_lines: code_lines).call + expect(out_code_lines.join).to eq(code_lines.join) + end + end +end