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

Beware the Dead End

Db953d125f5cc49756edb6149f1b813e?s=47 Richard Schneeman
September 20, 2021
15

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.

Db953d125f5cc49756edb6149f1b813e?s=128

Richard Schneeman

September 20, 2021
Tweet

Transcript

  1. Beware the Dreaded Dead End!!! By @schneems

  2. None
  3. None
  4. Beware the Dreaded Dead End!!! By @schneems

  5. > # <= 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
  6. > 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’
  7. > 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’
  8. 🤔

  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 This code has an unmatched `end`. Ensure that all `end` lines in your code have a matching syntax keyword (`def`, `do`, etc.)
  10. >

  11. > # 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’
  12. > 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
  13. 0 25 50 75 100 April May June July (I

    did not do a study)
  14. (This is a lie) -30 -15 0 15 30 45

    60 April May June July August
  15. (Also a lie) 7% 8% 11% 11% 31% 32%

  16. 🥰

  17. $ gem install dead_end

  18. > 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
  19. > 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
  20. > 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
  21. > 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
  22. > 1 class Cat 2 def meow ❯ 3 Animal.call

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

    do |a ❯ 5 end 6 end 7 end DeadEnd: Unmatched `|` character detected ? ? ?
  24. $ gem install dead_end

  25. Syntax errors

  26. Lexing & Parsing

  27. AI

  28. dead_end

  29. Internals

  30. 🤚

  31. @schneems

  32. @schneems

  33. None
  34. 📕 How to Open Source

  35. Heroku

  36. Heroku

  37. NoMemoryError ScriptError LoadError SyntaxError SecurityError SignalException KeyError StandardError ArgumentError EncodingError

    IOError EOFError I am Exceptional
  38. > Dir.chhhdir("/tmp") do puts `ls` end

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

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

  41. 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
  42. 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 }
  43. if / end unless / end while / end until

    / end def / end for / end begin / end class / end module / end do / end
  44. > Dir.chdir("lol") puts `ls` end ☠☠☠

  45. > Dir.chdir("lol") puts `ls` end ??

  46. > Dir.chdir("/tmp") Dir.chdir("lol") do puts `ls` end

  47. > def bark puts "woof" end bark() # => "woof"

  48. > def bark puts "woof" end bark do puts "not

    a syntax error" end # => "woof"
  49. > expect(WebMock).to have_requested( :post, "localhost:#{port}" ). end $ ruby -wc

    scratch.rb Syntax OK
  50. 📓

  51. Lexing & Parsing

  52. "hello world" Ripper.lex >

  53. None
  54. "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] ] >
  55. [ [[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] ]
  56. [ [[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] ]
  57. [ [[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] ]
  58. while b != 0 if a > b a =

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

    = a - b else b = b - a end end return a Ripper.parse :(
  60. 🤔

  61. while b != 0 # if a > b a

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

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

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

  65. Artificial Intelligence Algorithm

  66. Artificial Intelligence Algorithm

  67. Pathfinding Credit: Factorio 'New pathfinding algorithm'

  68. Credit @redblobgames: Introduction to the A-star Algorithm

  69. Pathfinding Credit: Factorio 'New pathfinding algorithm'

  70. Tree
 Search
 "Reti Draw" Endgame Chess problem by Richard Reti

    (1921)
  71. Tree
 Search
 BOTVINNIK, M.M. (1984). "Computers in Chess Solving Inexact

    Search Problems"
  72. Non-optimal Play

  73. None
  74. None
  75. None
  76. None
  77. None
  78. None
  79. None
  80. None
  81. None
  82. None
  83. None
  84. None
  85. None
  86. None
  87. Checkmate Black

  88. Optimal Draw Play

  89. None
  90. None
  91. None
  92. None
  93. None
  94. None
  95. None
  96. None
  97. Draw

  98. Tree
 Search
 BOTVINNIK, M.M. (1984). "Computers in Chess Solving Inexact

    Search Problems"
  99. AI ~ Search

  100. None
  101. while b != 0 if a > b a =

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

    = a - b else b = b - a end end return a Ripper.parse
  103. ❌🌳💔

  104. > Dir.chdir("/tmp") Dir.chdir("lol") end do??? do??? puts `ls`

  105. > Dir.chdir("/tmp") Dir.chdir("lol") end do puts `ls`🥰

  106. while b != 0 # if a > b #

    a = a - b # else # b = b - a # end end return a Ripper.parse
  107. 🌳 ❌ 💔

  108. demo

  109. 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' 📕 ❌
  110. 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
  111. 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
  112. 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
  113. 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
  114. 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
  115. 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
  116. 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
  117. 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 📕 ❌
  118. 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
  119. 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 📕 ❌
  120. 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
  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) 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. 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 📕 ❌
  124. 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
  125. 42 string << code_with_lines 43 string << "```\n" 44 string

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

    45 end 46 ❯ 47 def code_with_lines ❯ 62 end 63 end 64 end
  127. 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 📕 ❌
  128. 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
  129. 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
  130. 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
  131. 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
  132. 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
  133. 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
  134. 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
  135. 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 📕 ❌
  136. 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
  137. 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
  138. 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
  139. 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
  140. 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
  141. 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
  142. 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
  143. 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
  144. 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 📕 ❌
  145. 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
  146. 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 📕 ❌
  147. 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
  148. 📕 ❌ 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader

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

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

    end 64 end 📕 ✅
  151. Syntax O K 🥰🥰🥰 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks

    4 attr_reader :filename 63 end 64 end 📕 ✅
  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_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
  153. Internals

  154. module Kernel module_function def require(file) dead_end_original_require(file) rescue SyntaxError => e

    DeadEnd.handle_error(e) end end dead_end/auto.rb
  155. # ... Timeout.timeout(timeout) do record_dir ||= ENV["DEBUG"] ? "tmp" :

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

    nil search = CodeSearch.new(source, record_dir: record_dir).call end # ...
  157. # # ## 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
  158. 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
  159. 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
  160. 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 📕 ✅ 📕 ❌
  161. > 1 module SyntaxErrorSearch 3 class DisplayInvalidBlocks 4 attr_reader :filename

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

    63 end 64 end 📕 ✅
  163. # ... 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
  164. > 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
  165. 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
  166. 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
  167. def expand? return false if @frontier.empty? # ... # Expand

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

    all blocks before moving to unvisited lines frontier_indent >= unvisited_indent end code_frontier.rb
  169. 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
  170. 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
  171. # 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
  172. > 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
  173. > 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
  174. > 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
  175. > 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
  176. > 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
  177. > 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
  178. # 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
  179. # 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
  180. > 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
  181. > 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
  182. # 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
  183. 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
  184. 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
  185. 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
  186. > 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
  187. > 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
  188. 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
  189. 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
  190. 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
  191. 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
  192. 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
  193. # 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
  194. # 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
  195. 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
  196. 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
  197. 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
  198. 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
  199. 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
  200. > 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
  201. > 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
  202. > 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
  203. 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
  204. 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
  205. 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
  206. 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
  207. 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
  208. > source_code = %q{ string = <<~EOM "I am a

    here doc" if true EOM } Ripper.lex(source_code)
  209. > 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]]
  210. > 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]]
  211. > 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]]
  212. > source_code = %q{ string = <<~EOM "I am a

    here doc" if true EOM } Ripper.lex(source_code.lines[2])
  213. > [[[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])
  214. 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
  215. 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
  216. 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
  217. > 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
  218. > 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("")
  219. > 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("")
  220. > 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
  221. > 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
  222. 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
  223. $ dead_end bad.rb --record tmp/

  224. None
  225. None
  226. 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
  227. 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
  228. # 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
  229. # 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
  230. # 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
  231. 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
  232. 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
  233. 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
  234. 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
  235. > 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
  236. dead_end

  237. Are you a dev?

  238. None
  239. None
  240. Are you an open source dev?

  241. None
  242. Are you a Ruby core dev?

  243. ❤ 🙏 🙏

  244. Lexing Parsing Syntax SyntaxErrors AI Pathfinding and goal seeking

  245. None
  246. None
  247. None
  248. None
  249. @schneems

  250. None
  251. Questions?