Skip to content

Commit 5b206c1

Browse files
authored
Merge pull request #1 from zombocom/schneems/wip
Initial working implementation
2 parents 12f767e + 17390ab commit 5b206c1

File tree

11 files changed

+1096
-784
lines changed

11 files changed

+1096
-784
lines changed

lib/syntax_error_search.rb

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,34 @@
66

77
module SyntaxErrorSearch
88
class Error < StandardError; end
9-
# Your code goes here...
9+
10+
# Used for counting spaces
11+
module SpaceCount
12+
def self.indent(string)
13+
string.split(/\w/).first&.length || 0
14+
end
15+
end
16+
17+
18+
def self.valid?(source)
19+
source = source.join if source.is_a?(Array)
20+
source = source.to_s
21+
22+
# Parser writes to stderr even if you catch the error
23+
#
24+
stderr = $stderr
25+
$stderr = StringIO.new
26+
27+
Parser::CurrentRuby.parse(source)
28+
true
29+
rescue Parser::SyntaxError
30+
false
31+
ensure
32+
$stderr = stderr if stderr
33+
end
1034
end
35+
36+
require_relative "syntax_error_search/code_line"
37+
require_relative "syntax_error_search/code_block"
38+
require_relative "syntax_error_search/code_frontier"
39+
require_relative "syntax_error_search/code_search"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
module SyntaxErrorSearch
2+
# Multiple lines form a singular CodeBlock
3+
#
4+
# Source code is made of multiple CodeBlocks. A code block
5+
# has a reference to the source code that created itself, this allows
6+
# a code block to "expand" when needed
7+
#
8+
# The most important ability of a CodeBlock is this ability to expand:
9+
#
10+
# Example:
11+
#
12+
# code_block.to_s # =>
13+
# # def foo
14+
# # puts "foo"
15+
# # end
16+
#
17+
# code_block.expand_until_next_boundry
18+
#
19+
# code_block.to_s # =>
20+
# # class Foo
21+
# # def foo
22+
# # puts "foo"
23+
# # end
24+
# # end
25+
#
26+
class CodeBlock
27+
attr_reader :lines
28+
29+
def initialize(code_lines:, lines: [])
30+
@lines = Array(lines)
31+
@code_lines = code_lines
32+
end
33+
34+
def is_end?
35+
to_s.strip == "end"
36+
end
37+
38+
def starts_at
39+
@lines.first&.line_number
40+
end
41+
42+
def code_lines
43+
@code_lines
44+
end
45+
46+
# This is used for frontier ordering, we are searching from
47+
# the largest indentation to the smallest. This allows us to
48+
# populate an array with multiple code blocks then call `sort!`
49+
# on it without having to specify the sorting criteria
50+
def <=>(other)
51+
self.current_indent <=> other.current_indent
52+
end
53+
54+
# Only the lines that are not empty and visible
55+
def visible_lines
56+
@lines
57+
.select(&:not_empty?)
58+
.select(&:visible?)
59+
end
60+
61+
# This method is used to expand a code block to capture it's calling context
62+
def expand_until_next_boundry
63+
expand_to_indent(next_indent)
64+
self
65+
end
66+
67+
# This method expands the given code block until it captures
68+
# its nearest neighbors. This is used to expand a single line of code
69+
# to its smallest likely block.
70+
#
71+
# code_block.to_s # =>
72+
# # puts "foo"
73+
# code_block.expand_until_neighbors
74+
#
75+
# code_block.to_s # =>
76+
# # puts "foo"
77+
# # puts "bar"
78+
# # puts "baz"
79+
#
80+
def expand_until_neighbors
81+
expand_to_indent(current_indent)
82+
83+
expand_hidden_parner_line if self.to_s.strip == "end"
84+
self
85+
end
86+
87+
def expand_hidden_parner_line
88+
index = @lines.first.index
89+
indent = current_indent
90+
partner_line = code_lines.select {|line| line.index < index && line.indent == indent }.last
91+
92+
if partner_line&.hidden?
93+
partner_line.mark_visible
94+
@lines.prepend(partner_line)
95+
end
96+
end
97+
98+
# This method expands the existing code block up (before)
99+
# and down (after). It will break on change in indentation
100+
# and empty lines.
101+
#
102+
# code_block.to_s # =>
103+
# # def foo
104+
# # puts "foo"
105+
# # end
106+
#
107+
# code_block.expand_to_indent(0)
108+
# code_block.to_s # =>
109+
# # class Foo
110+
# # def foo
111+
# # puts "foo"
112+
# # end
113+
# # end
114+
#
115+
private def expand_to_indent(indent)
116+
array = []
117+
before_lines(skip_empty: false).each do |line|
118+
if line.empty?
119+
array.prepend(line)
120+
break
121+
end
122+
123+
if line.indent == indent
124+
array.prepend(line)
125+
else
126+
break
127+
end
128+
end
129+
130+
array << @lines
131+
132+
after_lines(skip_empty: false).each do |line|
133+
if line.empty?
134+
array << line
135+
break
136+
end
137+
138+
if line.indent == indent
139+
array << line
140+
else
141+
break
142+
end
143+
end
144+
145+
@lines = array.flatten
146+
end
147+
148+
def next_indent
149+
[
150+
before_line&.indent || 0,
151+
after_line&.indent || 0
152+
].max
153+
end
154+
155+
def current_indent
156+
lines.detect(&:not_empty?)&.indent || 0
157+
end
158+
159+
def before_line
160+
before_lines.first
161+
end
162+
163+
def after_line
164+
after_lines.first
165+
end
166+
167+
def before_lines(skip_empty: true)
168+
index = @lines.first.index
169+
lines = code_lines.select {|line| line.index < index }
170+
lines.select!(&:not_empty?) if skip_empty
171+
lines.select!(&:visible?)
172+
lines.reverse!
173+
174+
lines
175+
end
176+
177+
def after_lines(skip_empty: true)
178+
index = @lines.last.index
179+
lines = code_lines.select {|line| line.index > index }
180+
lines.select!(&:not_empty?) if skip_empty
181+
lines.select!(&:visible?)
182+
lines
183+
end
184+
185+
# Returns a code block of the source that does not include
186+
# the current lines. This is useful for checking if a source
187+
# with the given lines removed parses successfully. If so
188+
#
189+
# Then it's proof that the current block is invalid
190+
def block_without
191+
@block_without ||= CodeBlock.new(
192+
source: @source,
193+
lines: @source.code_lines - @lines
194+
)
195+
end
196+
197+
def document_valid_without?
198+
block_without.valid?
199+
end
200+
201+
def valid?
202+
SyntaxErrorSearch.valid?(self.to_s)
203+
end
204+
205+
def to_s
206+
@lines.join
207+
end
208+
end
209+
end
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
module SyntaxErrorSearch
2+
# This class is responsible for generating, storing, and sorting code blocks
3+
class CodeFrontier
4+
def initialize(code_lines: )
5+
@code_lines = code_lines
6+
@frontier = []
7+
@indent_hash = {}
8+
code_lines.each do |line|
9+
next if line.empty?
10+
11+
@indent_hash[line.indent] ||= []
12+
@indent_hash[line.indent] << line
13+
end
14+
end
15+
16+
# Returns true if the document is valid with all lines
17+
# removed. By default it checks all blocks in present in
18+
# the frontier array, but can be used for arbitrary arrays
19+
# of codeblocks as well
20+
def holds_all_syntax_errors?(block_array = @frontier)
21+
lines = @code_lines
22+
block_array.each do |block|
23+
lines -= block.lines
24+
end
25+
26+
return true if lines.empty?
27+
28+
CodeBlock.new(
29+
code_lines: @code_lines,
30+
lines: lines
31+
).valid?
32+
end
33+
34+
# Returns a code block with the largest indentation possible
35+
def pop
36+
return nil if empty?
37+
38+
if generate_new_block?
39+
self << next_block
40+
end
41+
42+
return @frontier.pop
43+
end
44+
45+
def next_block
46+
indent = @indent_hash.keys.sort.last
47+
lines = @indent_hash[indent].first
48+
49+
CodeBlock.new(
50+
lines: lines,
51+
code_lines: @code_lines
52+
).expand_until_neighbors
53+
end
54+
55+
# This method is responsible for determining if a new code
56+
# block should be generated instead of evaluating an already
57+
# existing block in the frontier
58+
def generate_new_block?
59+
return false if @indent_hash.empty?
60+
return true if @frontier.empty?
61+
62+
@frontier.last.current_indent <= @indent_hash.keys.sort.last
63+
end
64+
65+
# Add a block to the frontier
66+
#
67+
# This method ensures the frontier always remains sorted (in indentation order)
68+
# and that each code block's lines are removed from the indentation hash so we
69+
# don't re-evaluate the same line multiple times.
70+
def <<(block)
71+
block.lines.each do |line|
72+
@indent_hash[line.indent]&.delete(line)
73+
end
74+
@indent_hash.select! {|k, v| !v.empty?}
75+
76+
@frontier << block
77+
@frontier.sort!
78+
79+
self
80+
end
81+
82+
def any?
83+
!empty?
84+
end
85+
86+
def empty?
87+
@frontier.empty? && @indent_hash.empty?
88+
end
89+
90+
# Example:
91+
#
92+
# combination([:a, :b, :c, :d])
93+
# # => [[: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]]
94+
def self.combination(array)
95+
guesses = []
96+
1.upto(array.length).each do |size|
97+
guesses.concat(array.combination(size).to_a)
98+
end
99+
guesses
100+
end
101+
102+
# Given that we know our syntax error exists somewhere in our frontier, we want to find
103+
# the smallest possible set of blocks that contain all the syntax errors
104+
def detect_invalid_blocks
105+
self.class.combination(@frontier).detect do |block_array|
106+
holds_all_syntax_errors?(block_array)
107+
end || []
108+
end
109+
end
110+
end

0 commit comments

Comments
 (0)