Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Kaigi] Beware the Dead End

Richard Schneeman
September 20, 2021
94

[Kaigi] Beware the Dead End

Nothing stops a program from executing quite as fast as a syntax error. After years of “unexpected end” in my dev life, I decided to “do” something about it. In this talk we'll cover lexing, parsing, and indentation informed syntax tree search that power that dead_end Ruby library.

PS I also gave this talk at RubyConf, but with a whole bunch of new content. I recommend that one instead.

Richard Schneeman

September 20, 2021
Tweet

Transcript

  1. > # <= routes.rb:121: syntax error, unexpected end-of-input, expecting `end’

    Rails.application.routes.draw do constraints -> { Rails.application.config.non_production } do namespace :foo do resource :bar end end constraints -> { Rails.application.config.non_production } do namespace :bar do resource :baz end end namespace :admin do resource :session match "/foobar(*path)", via: :all, to: redirect { |_params, req| uri = URI(req.path.gsub("foobar", "foobaz")) uri.query = req.query_string.presence uri.to_s } end
  2. > Rails.application.routes.draw do constraints -> { Rails.application.config.non_production } do namespace

    :foo do resource :bar end end constraints -> { Rails.application.config.non_production } do namespace :bar do resource :baz end end namespace :admin do resource :session match "/foobar(*path)", via: :all, to: redirect { |_params, req| uri = URI(req.path.gsub("foobar", "foobaz")) uri.query = req.query_string.presence uri.to_s } end # <= routes.rb:121: syntax error, unexpected end-of-input, expecting `end’
  3. > RSpec.describe Cutlass::BashResult do it "preserves stdout, stderr, and status"

    stdout = SecureRandom.hex(16) stderr = SecureRandom.hex(16) status = 0 result = BashResult.new( stdout: stdout, stderr: stderr, status: status ) expect(result.stdout).to eq(stdout) expect(result.stderr).to eq(stderr) expect(result.status).to eq(status) end end # <= result_spec:19 syntax error, unexpected end-of-input, expecting `end’
  4. > 3 module Cutlass 4 RSpec.describe Cutlass::BashResult do ❯ 5

    it "preserves stdout, stderr, and status" ❯ 19 end 21 it "success?" do 52 end 53 end 54 end This code has an unmatched `end`. Ensure that all `end` lines in your code have a matching syntax keyword (`def`, `do`, etc.)
  5. >

  6. > # frozen_string_literal: true module Cutlass RSpec.describe Cutlass::BashResult do it

    "preserves stdout, stderr, and status" stdout = SecureRandom.hex(16) stderr = SecureRandom.hex(16) status = 0 result = BashResult.new( stdout: stdout, stderr: stderr, status: status ) expect(result.stdout).to eq(stdout) expect(result.stderr).to eq(stderr) expect(result.status).to eq(status) end end end # <= result_spec:19 syntax error, unexpected end-of-input, expecting `end’
  7. > 3 module Cutlass 4 RSpec.describe Cutlass::BashResult do ❯ 5

    it "preserves stdout, stderr, and status" ❯ 19 end 21 it "success?" do 52 end 53 end 54 end
  8. (This is a lie) -30 -15 0 15 30 45

    60 April May June July August
  9. > 3 module Cutlass 4 RSpec.describe Cutlass::BashResult do ❯ 5

    it "preserves stdout, stderr, and status" ❯ 19 end 21 it "success?" do 52 end 53 end 54 end DeadEnd: Unmatched `end` detected
  10. > 1 module Cutlass 2 RSpec.describe Cutlass::BashResult do ❯ 3

    it "preserves stdout, stderr, and status" do 5 it "success?" do 6 end 7 end 8 end DeadEnd: Missing `end` detected
  11. > 1 class Cat 2 def to_json ❯ 3 hash

    = { ❯ 4 name: name, ❯ 5 fur_color: fur_color, ❯ 6 type: 'Cat', ❯ 7 better_than_dog: false ❯ 8 hash.to_json 9 end 10 end DeadEnd: Unmatched `}` character detected
  12. > 1 class Animal 2 def self.ones_my_three_year_old_likes ❯ 3 array

    = ❯ 4 Dog.new, ❯ 5 Lion.new, ❯ 6 Tiger.new, ❯ 7 Aligator.new, ❯ 8 ] 10 return array 11 end 12 end DeadEnd: Unmatched `[` detected
  13. > 1 class Cat 2 def meow ❯ 3 Animal.call

    do |a ❯ 5 end 6 end 7 end DeadEnd: Unmatched `|` character detected
  14. > 1 class Cat 2 def meow ❯ 3 Animal.call

    do |a ❯ 5 end 6 end 7 end DeadEnd: Unmatched `|` character detected ? ? ?
  15. AI

  16. > Dir.chhhdir("/tmp") do puts `ls` end # unde fi ned

    method `chhhdir' for # Dir:Class (NoMethodError ) # Did you mean? chdir
  17. >

  18. require 'bundler' require 'date' require 'optparse' require 'ostruct' require 'shellwords'

    class Rexe VERSION = '1.5.1' PROJECT_URL = 'https://github.com/keithrbennett/rexe' module Helpers # Try executing code. If error raised, print message (but not stack trace) & exit -1. def try begin
  19. end def bundler_run(&block) # This used to be an unconditional

    call to with_clean_env but that method is now deprecated: # [DEPRECATED] `Bundler.with_clean_env` has been deprecated in favor of `Bundler.with_unbundled_env`. # If you instead want the environment before bundler was originally loaded, # use `Bundler.with_original_env` if Bundler.respond_to?(:with_unbundled_env) Bundler.with_unbundled_env { block.call } else Bundler.with_clean_env { block.call } end end bundler_run { Rexe::Main.new.call }
  20. if / end unless / end while / end until

    / end def / end for / end begin / end class / end module / end do / end
  21. > def bark puts "woof" end bark do puts "not

    a syntax error" end # => "woof"
  22. "hello world" Ripper.lex [ [[1, 0], :on_tstring_beg, "\"", BEG], [[1,

    1], :on_tstring_content, "hello world", BEG], [[1, 12], :on_tstring_end, "\"", END], [[1, 13], :on_nl, "\n", BEG] ] >
  23. [ [[1, 0], :on_tstring_beg, "\"", BEG], [[1, 1], :on_tstring_content, "hello

    world", BEG], [[1, 12], :on_tstring_end, "\"", END], [[1, 13], :on_nl, "\n", BEG] ]
  24. [ [[1, 0], :on_tstring_beg, "\"", BEG], [[1, 1], :on_tstring_content, "hello

    world", BEG], [[1, 12], :on_tstring_end, "\"", END], [[1, 13], :on_nl, "\n", BEG] ]
  25. [ [[1, 0], :on_tstring_beg, "\"", BEG], [[1, 1], :on_tstring_content, "hello

    world", BEG], [[1, 12], :on_tstring_end, "\"", END], [[1, 13], :on_nl, "\n", BEG] ]
  26. while b != 0 if a > b a =

    a - b else b = b - a end end return a Ripper.parse
  27. while b != 0 # if a > b a

    = a - b else b = b - a end end return a Ripper.parse :(
  28. while b != 0 # if a > b a

    = a - b else b = b - a end end return a Ripper.parse :(
  29. while b != 0 # if a > b #

    a = a - b # else # b = b - a # end end return a Ripper.parse :)
  30. while b != 0 # if a > b #

    a = a - b # else # b = b - a # end end return a Ripper.parse :)
  31. AI

  32. while b != 0 if a > b a =

    a - b else b = b - a end end return a Ripper.parse
  33. while b != 0 # if a > b a

    = a - b else b = b - a end end return a Ripper.parse
  34. while b != 0 # if a > b #

    a = a - b # else # b = b - a # end end return a Ripper.parse
  35. module SyntaxErrorSearch # Used for formatting invalid blocks class DisplayInvalidBlocks

    attr_reader :filename def initialize(block_array, io: $stderr, filename: nil) @filename = filename @io = io @blocks = block_array @lines = @blocks.map(&:lines).flatten @digit_count = @lines.last.line_number.to_s.length @code_lines = @blocks.first.code_lines @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} end def call 64: syntax error, unexpected end-of-input, expecting `end' 📕 ❌
  36. 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else 54 string =

    String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics 56 string << "#{number.to_s} " 57 string << line.to_s 58 string << "\e[0m" 59 string 60 end 61 end.join 62 end 63 end 64 end
  37. 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53

    else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics 56 string << "#{number.to_s} " 57 string << line.to_s 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  38. 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53

    else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics 56 string << "#{number.to_s} " 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  39. 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53

    else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  40. 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53

    else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics ❯ 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  41. 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53

    else 54 string = String.new ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics ❯ 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  42. 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53

    else ❯ 54 string = String.new ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics ❯ 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  43. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? ❯ 52 "#{number.to_s}#{line}" 53 else 60 end 61 end.join 62 end 63 end 64 end
  44. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 53 else 60 end 61 end.join 62 end 63 end 64 end 📕 ❌
  45. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) ❯ 51 if line.empty? ❯ 53 else ❯ 60 end 61 end.join 62 end 63 end 64 end
  46. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 61 end.join 62 end 63 end 64 end 📕 ❌
  47. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 47 def code_with_lines 48 @code_lines.map do |line| ❯ 49 next if line.hidden? ❯ 50 number = line.line_number.to_s.rjust(@digit_count) 61 end.join 62 end 63 end 64 end
  48. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 47 def code_with_lines 48 @code_lines.map do |line| 61 end.join 62 end 63 end 64 end 📕 ❌
  49. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 47 def code_with_lines ❯ 48 @code_lines.map do |line| ❯ 61 end.join 62 end 63 end 64 end
  50. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 47 def code_with_lines 62 end 63 end 64 end 📕 ❌
  51. 42 string << code_with_lines 43 string << "```\n" 44 string

    45 end 46 ❯ 47 def code_with_lines ❯ 62 end 63 end 64 end
  52. 40 string << "```\n" 41 string << "#".rjust(@digit_count) + "

    filename: #{filename}\n\n" if filename 42 string << code_with_lines 43 string << "```\n" 44 string 45 end 46 63 end 64 end 📕 ❌
  53. 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 35 36 def filename 37 38 def code_with_filename 39 string = String.new("") 40 string << "```\n" 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename 42 string << code_with_lines 43 string << "```\n" 44 string 45 end 46 63 end 64 end
  54. 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 35 36 def filename 37 38 def code_with_filename 39 string = String.new("") 40 string << "```\n" 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename 42 string << code_with_lines 43 string << "```\n" ❯ 44 string 45 end 46 63 end 64 end
  55. 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 35 36 def filename 37 38 def code_with_filename 39 string = String.new("") 40 string << "```\n" 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename 42 string << code_with_lines ❯ 43 string << "```\n" ❯ 44 string 45 end 46 63 end 64 end
  56. 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 35 36 def filename 37 38 def code_with_filename 39 string = String.new("") 40 string << "```\n" 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename ❯ 42 string << code_with_lines ❯ 43 string << "```\n" ❯ 44 string 45 end 46 63 end 64 end
  57. 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 35 36 def filename 37 38 def code_with_filename 39 string = String.new("") 40 string << "```\n" ❯ 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename ❯ 42 string << code_with_lines ❯ 43 string << "```\n" ❯ 44 string 45 end 46 63 end 64 end
  58. 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 35 36 def filename 37 38 def code_with_filename 39 string = String.new("") ❯ 40 string << "```\n" ❯ 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename ❯ 42 string << code_with_lines ❯ 43 string << "```\n" ❯ 44 string 45 end 46 63 end 64 end
  59. 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 35 36 def filename 37 38 def code_with_filename ❯ 39 string = String.new("") ❯ 40 string << "```\n" ❯ 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename ❯ 42 string << code_with_lines ❯ 43 string << "```\n" ❯ 44 string 45 end 46 63 end 64 end
  60. 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 35 36 def filename 37 38 def code_with_filename 45 end 46 63 end 64 end 📕 ❌
  61. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 7 @filename = filename 8 @io = io 9 @blocks = block_array 10 @lines = @blocks.map(&:lines).flatten 11 @digit_count = @lines.last.line_number.to_s.length 12 @code_lines = @blocks.first.code_lines 13 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} 15 end 16 35 36 def filename 37
  62. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 7 @filename = filename 8 @io = io 9 @blocks = block_array 10 @lines = @blocks.map(&:lines).flatten 11 @digit_count = @lines.last.line_number.to_s.length 12 @code_lines = @blocks.first.code_lines 13 ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} 15 end 16 35 36 def filename 37
  63. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 7 @filename = filename 8 @io = io 9 @blocks = block_array 10 @lines = @blocks.map(&:lines).flatten 11 @digit_count = @lines.last.line_number.to_s.length ❯ 12 @code_lines = @blocks.first.code_lines ❯ 13 ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} 15 end 16 35 36 def filename
  64. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 7 @filename = filename 8 @io = io 9 @blocks = block_array 10 @lines = @blocks.map(&:lines).flatten ❯ 11 @digit_count = @lines.last.line_number.to_s.length ❯ 12 @code_lines = @blocks.first.code_lines ❯ 13 ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} 15 end 16 35 36 def filename
  65. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 7 @filename = filename 8 @io = io 9 @blocks = block_array ❯ 10 @lines = @blocks.map(&:lines).flatten ❯ 11 @digit_count = @lines.last.line_number.to_s.length ❯ 12 @code_lines = @blocks.first.code_lines ❯ 13 ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} 15 end 16 35 36 def filename
  66. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 7 @filename = filename 8 @io = io ❯ 9 @blocks = block_array ❯ 10 @lines = @blocks.map(&:lines).flatten ❯ 11 @digit_count = @lines.last.line_number.to_s.length ❯ 12 @code_lines = @blocks.first.code_lines ❯ 13 ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} 15 end 16 35 36 def filename
  67. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 7 @filename = filename ❯ 8 @io = io ❯ 9 @blocks = block_array ❯ 10 @lines = @blocks.map(&:lines).flatten ❯ 11 @digit_count = @lines.last.line_number.to_s.length ❯ 12 @code_lines = @blocks.first.code_lines ❯ 13 ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} 15 end 16 35 36 def filename
  68. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) ❯ 7 @filename = filename ❯ 8 @io = io ❯ 9 @blocks = block_array ❯ 10 @lines = @blocks.map(&:lines).flatten ❯ 11 @digit_count = @lines.last.line_number.to_s.length ❯ 12 @code_lines = @blocks.first.code_lines ❯ 13 ❯ 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true} 15 end 16 35 36 def filename
  69. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 15 end 16 35 36 def filename 37 38 def code_with_filename 45 end 46 63 end 64 end 📕 ❌
  70. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 15 end 16 35 36 def filename 37 ❯ 38 def code_with_filename ❯ 45 end 46 63 end 64 end
  71. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    6 def initialize(block_array, io: $stderr, filename: nil) 15 end 16 35 36 def filename 37 46 63 end 64 end 📕 ❌
  72. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename 5

    ❯ 6 def initialize(block_array, io: $stderr, filename: nil) ❯ 15 end 16 35 36 def filename 37 46 63 end 64 end
  73. 📕 ❌ 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader

    :filename 5 16 35 36 def filename 37 46 63 end 64 end
  74. 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename ❯

    5 ❯ 16 ❯ 35 ❯ 36 def filename ❯ 37 ❯ 46 63 end 64 end
  75. DeadEnd: Missing `end` detected This code has a missing `end`.

    Ensure that all syntax keywords (`def`, `do`, etc.) have a matching `end`. file: /Users/rschneeman/Documents/projects/dead_end/spec/fixtures/this_project_extra_def.rb.txt simplified: 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename ❯ 36 def filename ❯ 38 def code_with_filename ❯ 45 end 63 end 64 end
  76. # ... Timeout.timeout(timeout) do record_dir ||= ENV["DEBUG"] ? "tmp" :

    nil search = CodeSearch.new(source, record_dir: record_dir).call end # ... dead_end/internals.rb
  77. # ... Timeout.timeout(timeout) do record_dir ||= ENV["DEBUG"] ? "tmp" :

    nil search = CodeSearch.new(source, record_dir: record_dir).call end # ...
  78. # # ## Syntax error detection # # When the

    frontier holds the syntax error, we can stop searching # # search = CodeSearch.new(<<~EOM) # def dog # def lol # end # EOM # # search.call # # search.invalid_blocks.map(&:to_s) # => # # => ["def lol\n"] # code_search.rb
  79. end # Main search loop def call sweep_heredocs sweep_comments until

    frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end @invalid_blocks.concat(frontier.detect_invalid_blocks ) code_search.rb
  80. end # Main search loop def call sweep_heredocs sweep_comments until

    frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  81. end # Main search loop def call sweep_heredocs sweep_comments until

    frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb 📕 ✅ 📕 ❌
  82. > 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename

    ❯ 5 ❯ 16 ❯ 35 ❯ 36 def filename ❯ 37 ❯ 46 63 end 64 end 📕 ❌
  83. # ... end module DeadEnd # ... def self.invalid?(source) source

    = source.join if source.is_a?(Array) source = source.to_s Ripper.new(source).tap(&:parse).error? end # ... end dead_end.rb
  84. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics 56 string << "#{number.to_s} " 57 string << line.to_s 58 string << "\e[0m" 59 string 60 end 61 end.join 62 end 63 end 64 end
  85. end # Main search loop def call sweep_heredocs sweep_comments until

    frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  86. end # Main search loop def call sweep_heredocs sweep_comments until

    frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end @invalid_blocks.concat(frontier.detect_invalid_blocks ) code_search.rb
  87. def expand? return false if @frontier.empty? # ... # Expand

    all blocks before moving to unvisited lines frontier_indent >= unvisited_indent end code_frontier.rb
  88. def expand? return false if @frontier.empty? # ... # Expand

    all blocks before moving to unvisited lines frontier_indent >= unvisited_indent end code_frontier.rb
  89. end # Main search loop def call sweep_heredocs sweep_comments until

    frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end @invalid_blocks.concat(frontier.detect_invalid_blocks ) code_search.rb
  90. end # Main search loop def call sweep_heredocs sweep_comments until

    frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end @invalid_blocks.concat(frontier.detect_invalid_blocks ) code_search.rb
  91. # Parses the most indented lines into blocks that are

    marked # and added to the frontier def visit_new_blocks max_indent = frontier.next_indent_line&.indent while (line = frontier.next_indent_line) && (line.indent == max_indent) @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block| record(block: block, name: "add") block.mark_invisible if block.valid? push(block, name: "add") end end end code_search.rb
  92. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics 56 string << "#{number.to_s} " 57 string << line.to_s 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  93. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics 56 string << "#{number.to_s} " 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  94. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  95. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else 54 string = String.new 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics ❯ 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  96. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else 54 string = String.new ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics ❯ 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  97. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else ❯ 54 string = String.new ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics ❯ 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  98. # Parses the most indented lines into blocks that are

    marked # and added to the frontier def visit_new_blocks max_indent = frontier.next_indent_line&.indent while (line = frontier.next_indent_line) && (line.indent == max_indent) @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block| record(block: block, name: "add") block.mark_invisible if block.valid? push(block, name: "add") end end end code_search.rb
  99. # Parses the most indented lines into blocks that are

    marked # and added to the frontier def visit_new_blocks max_indent = frontier.next_indent_line&.indent while (line = frontier.next_indent_line) && (line.indent == max_indent) @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block| record(block: block, name: "add") block.mark_invisible if block.valid? push(block, name: "add") end end end code_search.rb
  100. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else ❯ 54 string = String.new ❯ 55 string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics ❯ 56 string << "#{number.to_s} " ❯ 57 string << line.to_s ❯ 58 string << "\e[0m" ❯ 59 string 60 end 61 end.join 62 end 63 end 64 end
  101. > 47 def code_with_lines 48 @code_lines.map do |line| 49 next

    if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 52 "#{number.to_s}#{line}" 53 else 60 end 61 end.join 62 end 63 end 64 end
  102. # Parses the most indented lines into blocks that are

    marked # and added to the frontier def visit_new_blocks max_indent = frontier.next_indent_line&.indent while (line = frontier.next_indent_line) && (line.indent == max_indent) @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block| record(block: block, name: "add") block.mark_invisible if block.valid? push(block, name: "add") end end end code_search.rb
  103. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  104. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  105. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  106. > 43 string << "```\n" 44 string 45 end 46

    47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? ❯ 52 "#{number.to_s}#{line}" 53 else 60 end 61 end.join 62 end 63 end 64 end
  107. > 43 string << "```\n" 44 string 45 end 46

    47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? 53 else 60 end 61 end.join 62 end 63 end 64 end
  108. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  109. code_search.rb sweep(block: block, name: "comments") end # Main search loop

    def call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end
  110. end end # Given an already existing block in the

    frontier, expand it to see # if it contains our invalid syntax def expand_invalid_block block = frontier.pop return unless block record(block: block, name: "pop") block = @block_expand.call(block) push(block, name: "expand") end def sweep_heredocs code_search.rb
  111. end end # Given an already existing block in the

    frontier, expand it to see # if it contains our invalid syntax def expand_invalid_block block = frontier.pop return unless block record(block: block, name: "pop") block = @block_expand.call(block) push(block, name: "expand") end def sweep_heredocs code_search.rb
  112. end end # Given an already existing block in the

    frontier, expand it to see # if it contains our invalid syntax def expand_invalid_block block = frontier.pop return unless block record(block: block, name: "pop") block = @block_expand.call(block) push(block, name: "expand") end def sweep_heredocs code_search.rb
  113. # frozen_string_literal: true module DeadEnd # This class is responsible

    for taking a code block that exists # at a far indentaion and then iteratively increasing the block # so that it captures everything within the same indentation block. # # def dog # puts "bow" # puts "wow" # end # # block = BlockExpand.new(code_lines: code_lines) # .call(CodeBlock.new(lines: code_lines[1])) # # puts block.to_s # # => puts "bow" block_expand.rb
  114. # puts "wow" # end # class BlockExpand def initialize(code_lines:

    ) @code_lines = code_lines end def call(block) if (next_block = expand_neighbors(block, grab_empty: true)) return next_block end expand_indent(block) end def expand_indent(block) block_expand.rb
  115. def expand_indent(block) block = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw

    .scan_adjacent_indent .code_block end def expand_neighbors(block, grab_empty: true) scan = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw .scan_neighbors # Slurp up empties block_expand.rb
  116. def expand_indent(block) block = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw

    .scan_adjacent_indent .code_block end def expand_neighbors(block, grab_empty: true) scan = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw .scan_neighbors # Slurp up empties block_expand.rb
  117. def expand_indent(block) block = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw

    .scan_adjacent_indent .code_block end def expand_neighbors(block, grab_empty: true) scan = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw .scan_neighbors # Slurp up empties block_expand.rb
  118. def expand_indent(block) block = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw

    .scan_adjacent_indent .code_block end def expand_neighbors(block, grab_empty: true) scan = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw .scan_neighbors # Slurp up empties block_expand.rb
  119. def expand_indent(block) block = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw

    .scan_adjacent_indent .code_block end def expand_neighbors(block, grab_empty: true) scan = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw .scan_neighbors # Slurp up empties block_expand.rb
  120. > 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if

    filename 42 string << code_with_lines 43 string << "```\n" 44 string 45 end 46 47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 51 if line.empty? ❯ 52 "#{number.to_s}#{line}" 53 else 60 end 61 end.join 62 end 63 end 64 end
  121. > 42 string << code_with_lines 43 string << "```\n" 44

    string 45 end 46 47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) ❯ 51 if line.empty? ❯ 53 else ❯ 60 end 61 end.join 62 end 63 end 64 end
  122. > 42 string << code_with_lines 43 string << "```\n" 44

    string 45 end 46 47 def code_with_lines 48 @code_lines.map do |line| 49 next if line.hidden? 50 number = line.line_number.to_s.rjust(@digit_count) 61 end.join 62 end 63 end 64 end
  123. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  124. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  125. def expand_indent(block) block = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw

    .scan_adjacent_indent .code_block end def expand_neighbors(block, grab_empty: true) scan = AroundBlockScan.new(code_lines: @code_lines, block: block) .skip(:hidden?) .stop_after_kw .scan_neighbors # Slurp up empties block_expand.rb
  126. code_line.rb class CodeLine TRAILING_SLASH = ("\\" + $/).freeze attr_reader :line,

    :index, :indent, :original_line def initialize(line: , index:) @original_line = line.freeze @line = @original_line if line.strip.empty? @empty = true @indent = 0 else @empty = false @indent = SpaceCount.indent(line) end @index = index @status = nil # valid, invalid, unknown
  127. private def lex_detect! lex_array = LexAll.new(source: line) kw_count = 0

    end_count = 0 lex_array.each_with_index do |lex, index| next unless lex.type == :on_kw case lex.token when 'if', 'unless', 'while', 'until' # Only count if/unless when it's not a "trailing" if/unless kw_count += 1 if !lex.expr_label? when 'def', 'case', 'for', 'begin', 'class', 'module', 'do' kw_count += 1 when 'end' end_count += 1 end code_line.rb
  128. > source_code = %q{ string = <<~EOM "I am a

    here doc" if true EOM } Ripper.lex(source_code)
  129. > source_code = %q{ string = <<~EOM "I am a

    here doc" if true EOM } Ripper.lex(source_code) [[[1, 0], :on_ignored_nl, "\n", BEG], [[2, 0], :on_ident, "string", CMDARG], [[2, 6], :on_sp, " ", CMDARG], [[2, 7], :on_op, "=", BEG], [[2, 8], :on_sp, " ", BEG], [[2, 9], :on_heredoc_beg, "<<~EOM", BEG], [[2, 15], :on_nl, "\n", BEG], [[3, 0], :on_tstring_content, " \"I am a here doc\" if true\n", BEG], [[4, 0], :on_tstring_content, "EOM \n", BEG]]
  130. > source_code = %q{ string = <<~EOM "I am a

    here doc" if true EOM } Ripper.lex(source_code) [[[1, 0], :on_ignored_nl, "\n", BEG], [[2, 0], :on_ident, "string", CMDARG], [[2, 6], :on_sp, " ", CMDARG], [[2, 7], :on_op, "=", BEG], [[2, 8], :on_sp, " ", BEG], [[2, 9], :on_heredoc_beg, "<<~EOM", BEG], [[2, 15], :on_nl, "\n", BEG], [[3, 0], :on_tstring_content, " \"I am a here doc\" if true\n", BEG], [[4, 0], :on_tstring_content, "EOM \n", BEG]]
  131. > source_code = %q{ string = <<~EOM "I am a

    here doc" if true EOM } Ripper.lex(source_code) [[[1, 0], :on_ignored_nl, "\n", BEG], [[2, 0], :on_ident, "string", CMDARG], [[2, 6], :on_sp, " ", CMDARG], [[2, 7], :on_op, "=", BEG], [[2, 8], :on_sp, " ", BEG], [[2, 9], :on_heredoc_beg, "<<~EOM", BEG], [[2, 15], :on_nl, "\n", BEG], [[3, 0], :on_tstring_content, " \"I am a here doc\" if true\n", BEG], [[4, 0], :on_tstring_content, "EOM \n", BEG]]
  132. > source_code = %q{ string = <<~EOM "I am a

    here doc" if true EOM } Ripper.lex(source_code.lines[2])
  133. > [[[1, 0], :on_sp, " ", BEG], [[1, 4], :on_tstring_beg,

    "\"", BEG], [[1, 5], :on_tstring_content, "I am a here doc", BEG], [[1, 20], :on_tstring_end, "\"", END], [[1, 21], :on_sp, " ", END], [[1, 22], :on_kw, "if", BEG|LABEL], [[1, 24], :on_sp, " ", BEG|LABEL], [[1, 25], :on_kw, "true", END], [[1, 29], :on_nl, "\n", BEG]] source_code = %q{ string = <<~EOM "I am a here doc" if true EOM } Ripper.lex(source_code.lines[2])
  134. private def lex_detect! lex_array = LexAll.new(source: line) kw_count = 0

    end_count = 0 lex_array.each_with_index do |lex, index| next unless lex.type == :on_kw case lex.token when 'if', 'unless', 'while', 'until' # Only count if/unless when it's not a "trailing" if/unless kw_count += 1 if !lex.expr_label? when 'def', 'case', 'for', 'begin', 'class', 'module', 'do' kw_count += 1 when 'end' end_count += 1 end code_line.rb
  135. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  136. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  137. > 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 17 def call ❯ 18 @io.puts <<~EOM ❯ 19 ❯ 20 DeadEnd: A syntax error was detected ❯ 21 ❯ 22 This code has an unmatched `end` this is caused by either ❯ 23 missing a syntax keyword (`def`, `do`, etc.) or inclusion ❯ 24 of an extra `end` line: ❯ 25 EOM 26 27 @io.puts(<<~EOM) if filename 28 file: #{filename} 29 EOM 30 31 @io.puts <<~EOM
  138. > 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 17 def call 26 27 @io.puts(<<~EOM) if filename 28 file: #{filename} 29 EOM 30 31 @io.puts <<~EOM 32 #{code_with_filename} 33 EOM 34 end 35 36 def filename 37 38 def code_with_filename 39 string = String.new("")
  139. > 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 17 def call 26 ❯ 27 @io.puts(<<~EOM) if filename ❯ 28 file: #{filename} ❯ 29 EOM 30 31 @io.puts <<~EOM 32 #{code_with_filename} 33 EOM 34 end 35 36 def filename 37 38 def code_with_filename 39 string = String.new("")
  140. > 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 17 def call 26 30 ❯ 31 @io.puts <<~EOM ❯ 32 #{code_with_filename} ❯ 33 EOM 34 end 35 36 def filename 37 38 def code_with_filename 39 string = String.new("") 40 string << "```\n" 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename 42 string << code_with_lines
  141. > 14 @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}

    15 end 16 17 def call 26 30 34 end 35 36 def filename 37 38 def code_with_filename 39 string = String.new("") 40 string << "```\n" 41 string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename 42 string << code_with_lines 43 string << "```\n" 44 string 45 end
  142. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  143. sweep(block: block, name: "comments") end # Main search loop def

    call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if frontier.expand? expand_invalid_block else visit_new_blocks end end code_search.rb
  144. def call sweep_heredocs sweep_comments until frontier.holds_all_syntax_errors? @tick += 1 if

    frontier.expand? expand_invalid_block else visit_new_blocks end end @invalid_blocks.concat(frontier.detect_invalid_blocks ) @invalid_blocks.sort_by! {|block| block.starts_at } self end code_search.rb
  145. # 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 code_frontier.rb
  146. # 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 code_frontier.rb
  147. # 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.select(&:invalid?)).detect do |block_array| holds_all_syntax_errors?(block_array) end || [] end end end code_frontier.rb
  148. 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.select(&:invalid?)).detect do |block_array| holds_all_syntax_errors?(block_array) end || [] end end end code_frontier.rb
  149. Timeout.timeout(timeout) do record_dir ||= ENV["DEBUG"] ? "tmp" : nil search

    = CodeSearch.new(source, record_dir: record_dir).call end blocks = search.invalid_blocks DisplayInvalidBlocks.new( blocks: blocks, filename: filename, terminal: terminal, code_lines: search.code_lines, invalid_obj: invalid_type(source), io: io ).call dead_end/internals.rb
  150. Timeout.timeout(timeout) do record_dir ||= ENV["DEBUG"] ? "tmp" : nil search

    = CodeSearch.new(source, record_dir: record_dir).call end blocks = search.invalid_blocks DisplayInvalidBlocks.new( blocks: blocks, filename: filename, terminal: terminal, code_lines: search.code_lines, invalid_obj: invalid_type(source), io: io ).call dead_end/internals.rb
  151. Timeout.timeout(timeout) do record_dir ||= ENV["DEBUG"] ? "tmp" : nil search

    = CodeSearch.new(source, record_dir: record_dir).call end blocks = search.invalid_blocks DisplayInvalidBlocks.new( blocks: blocks, filename: filename, terminal: terminal, code_lines: search.code_lines, invalid_obj: invalid_type(source), io: io ).call dead_end/internals.rb
  152. > DeadEnd: Missing `end` detected This code has a missing

    `end`. Ensure that all syntax keywords (`def`, `do`, etc.) have a matching `end`. file: /Users/rschneeman/Documents/projects/dead_end/spec/fixtures/this_project_extr simplified: 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename ❯ 36 def filename ❯ 38 def code_with_filename ❯ 45 end 63 end 64 end