syntax_search 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9efb5d44fe2c979f66237173d1bf7b215a1bcc8003b4a96cb595eabbcac606c6
4
- data.tar.gz: 5757f68cdf7dace8980bb31b49a756b858fba8f7260156640621832374b4e635
3
+ metadata.gz: cb2c76d5b7441b5c04e92205407605d6f71be326bfbf240b11763156fc00e188
4
+ data.tar.gz: 728230f8d8694e221149a671d0c44f44d82829407cf5a82b498523015d9df33f
5
5
  SHA512:
6
- metadata.gz: b9fb23706395a520d52af51ea2a441d972115d6cd85a9e4048d17424d0e7077e679cd23d6ec1a9f43f4fde1e6d29ab8670906dc27451b730f2dab4c5c753789f
7
- data.tar.gz: 13209882976145efe8087a5c5d52e1980d5295c4b7ab51628dc92007f6b83900933fb2e08046bac7ccf118056289bba04f3f5597b70400c74c32969468e31409
6
+ metadata.gz: 38ecea1a966f361809d3e13440ee16acffd3bbf79d7629a48d65d3a5f5bc4474c51a8b79c1b8f54f9f2737ef4aa86105394e45caae9bd02f125874b8bc9d4496
7
+ data.tar.gz: 13520d3877d427ce2423576543688c9d91e0813bd12bf9e1e886dc65874cea527b04a88763867b7767d2cad03bfd5fc0745b22cc69c96dc140477e54b80c7b08
@@ -1,5 +1,9 @@
1
1
  ## HEAD (unreleased)
2
2
 
3
+ ## 0.1.3
4
+
5
+ - Internal refactor (https://github.com/zombocom/syntax_search/pull/13)
6
+
3
7
  ## 0.1.2
4
8
 
5
9
  - Codeblocks in output are now indented with 4 spaces and "code fences" are removed (https://github.com/zombocom/syntax_search/pull/11)
data/Gemfile CHANGED
@@ -7,3 +7,4 @@ gemspec
7
7
 
8
8
  gem "rake", "~> 12.0"
9
9
  gem "rspec", "~> 3.0"
10
+ gem "stackprof"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_search (0.1.2)
4
+ syntax_search (0.1.3)
5
5
  parser
6
6
 
7
7
  GEM
@@ -25,6 +25,7 @@ GEM
25
25
  diff-lcs (>= 1.2.0, < 2.0)
26
26
  rspec-support (~> 3.10.0)
27
27
  rspec-support (3.10.0)
28
+ stackprof (0.2.16)
28
29
 
29
30
  PLATFORMS
30
31
  ruby
@@ -32,6 +33,7 @@ PLATFORMS
32
33
  DEPENDENCIES
33
34
  rake (~> 12.0)
34
35
  rspec (~> 3.0)
36
+ stackprof
35
37
  syntax_search!
36
38
 
37
39
  BUNDLED WITH
@@ -40,6 +40,7 @@ module SyntaxErrorSearch
40
40
  blocks: blocks,
41
41
  filename: filename,
42
42
  terminal: terminal,
43
+ code_lines: search.code_lines,
43
44
  invalid_type: invalid_type(source),
44
45
  io: $stderr
45
46
  ).call
@@ -152,5 +153,9 @@ end
152
153
  require_relative "syntax_search/code_line"
153
154
  require_relative "syntax_search/code_block"
154
155
  require_relative "syntax_search/code_frontier"
155
- require_relative "syntax_search/code_search"
156
156
  require_relative "syntax_search/display_invalid_blocks"
157
+ require_relative "syntax_search/around_block_scan"
158
+ require_relative "syntax_search/block_expand"
159
+ require_relative "syntax_search/parse_blocks_from_indent_line"
160
+
161
+ require_relative "syntax_search/code_search"
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ module SyntaxErrorSearch
4
+ # This class is useful for exploring contents before and after
5
+ # a block
6
+ #
7
+ # It searches above and below the passed in block to match for
8
+ # whatever criteria you give it:
9
+ #
10
+ # Example:
11
+ #
12
+ # def dog
13
+ # puts "bark"
14
+ # puts "bark"
15
+ # end
16
+ #
17
+ # scan = AroundBlockScan.new(
18
+ # code_lines: code_lines
19
+ # block: CodeBlock.new(lines: code_lines[1])
20
+ # )
21
+ #
22
+ # scan.scan_while { true }
23
+ #
24
+ # puts scan.before_index # => 0
25
+ # puts scan.after_index # => 3
26
+ #
27
+ # Contents can also be filtered using AroundBlockScan#skip
28
+ #
29
+ # To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent
30
+ class AroundBlockScan
31
+ def initialize(code_lines: , block:)
32
+ @code_lines = code_lines
33
+ @orig_before_index = block.lines.first.index
34
+ @orig_after_index = block.lines.last.index
35
+ @skip_array = []
36
+ @after_array = []
37
+ @before_array = []
38
+ end
39
+
40
+ def skip(name)
41
+ @skip_array << name
42
+ self
43
+ end
44
+
45
+ def scan_while(&block)
46
+ @before_index = before_lines.reverse_each.take_while do |line|
47
+ next true if @skip_array.detect {|meth| line.send(meth) }
48
+
49
+ block.call(line)
50
+ end.reverse.first&.index
51
+
52
+ @after_index = after_lines.take_while do |line|
53
+ next true if @skip_array.detect {|meth| line.send(meth) }
54
+
55
+ block.call(line)
56
+ end.last&.index
57
+ self
58
+ end
59
+
60
+ def scan_adjacent_indent
61
+ before_indent = @code_lines[@orig_before_index.pred]&.indent || 0
62
+ after_indent = @code_lines[@orig_after_index.next]&.indent || 0
63
+
64
+ indent = [before_indent, after_indent].min
65
+ @before_index = before_index.pred if before_indent >= indent
66
+ @after_index = after_index.next if after_indent >= indent
67
+
68
+ self
69
+ end
70
+
71
+ def code_block
72
+ CodeBlock.new(lines: @code_lines[before_index..after_index])
73
+ end
74
+
75
+ def before_index
76
+ @before_index || @orig_before_index
77
+ end
78
+
79
+ def after_index
80
+ @after_index || @orig_after_index
81
+ end
82
+
83
+ private def before_lines
84
+ @code_lines[0...@orig_before_index]
85
+ end
86
+
87
+ private def after_lines
88
+ @code_lines[@orig_after_index.next..-1]
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ module SyntaxErrorSearch
3
+ # This class is responsible for taking a code block that exists
4
+ # at a far indentaion and then iteratively increasing the block
5
+ # so that it captures everything within the same indentation block.
6
+ #
7
+ # def dog
8
+ # puts "bow"
9
+ # puts "wow"
10
+ # end
11
+ #
12
+ # block = BlockExpand.new(code_lines: code_lines)
13
+ # .call(CodeBlock.new(lines: code_lines[1]))
14
+ #
15
+ # puts block.to_s
16
+ # # => puts "bow"
17
+ # puts "wow"
18
+ #
19
+ #
20
+ # Once a code block has captured everything at a given indentation level
21
+ # then it will expand to capture surrounding indentation.
22
+ #
23
+ # block = BlockExpand.new(code_lines: code_lines)
24
+ # .call(block)
25
+ #
26
+ # block.to_s
27
+ # # => def dog
28
+ # puts "bow"
29
+ # puts "wow"
30
+ # end
31
+ #
32
+ class BlockExpand
33
+ def initialize(code_lines: )
34
+ @code_lines = code_lines
35
+ end
36
+
37
+ def call(block)
38
+ if (next_block = expand_neighbors(block, grab_empty: true))
39
+ return next_block
40
+ end
41
+
42
+ expand_indent(block)
43
+ end
44
+
45
+ def expand_indent(block)
46
+ block = AroundBlockScan.new(code_lines: @code_lines, block: block)
47
+ .scan_adjacent_indent
48
+ .code_block
49
+
50
+ # Handle if/else/end case
51
+ if (next_block = expand_neighbors(block, grab_empty: false))
52
+ return next_block
53
+ else
54
+ return block
55
+ end
56
+ end
57
+
58
+ def expand_neighbors(block, grab_empty: true)
59
+ scan = AroundBlockScan.new(code_lines: @code_lines, block: block)
60
+ .skip(:hidden?)
61
+ .scan_while {|line| line.not_empty? && line.indent >= block.current_indent }
62
+
63
+ # Slurp up empties
64
+ if grab_empty
65
+ scan = AroundBlockScan.new(code_lines: @code_lines, block: scan.code_block)
66
+ .scan_while {|line| line.empty? || line.hidden? }
67
+ end
68
+
69
+ new_block = scan.code_block
70
+
71
+ if block.lines == new_block.lines
72
+ return nil
73
+ else
74
+ return new_block
75
+ end
76
+ end
77
+ end
78
+ end
@@ -3,11 +3,7 @@
3
3
  module SyntaxErrorSearch
4
4
  # Multiple lines form a singular CodeBlock
5
5
  #
6
- # Source code is made of multiple CodeBlocks. A code block
7
- # has a reference to the source code that created itself, this allows
8
- # a code block to "expand" when needed
9
- #
10
- # The most important ability of a CodeBlock is this ability to expand:
6
+ # Source code is made of multiple CodeBlocks.
11
7
  #
12
8
  # Example:
13
9
  #
@@ -16,21 +12,19 @@ module SyntaxErrorSearch
16
12
  # # puts "foo"
17
13
  # # end
18
14
  #
19
- # code_block.expand_until_next_boundry
15
+ # code_block.valid? # => true
16
+ # code_block.in_valid? # => false
20
17
  #
21
- # code_block.to_s # =>
22
- # # class Foo
23
- # # def foo
24
- # # puts "foo"
25
- # # end
26
- # # end
27
18
  #
28
19
  class CodeBlock
29
20
  attr_reader :lines
30
21
 
31
- def initialize(code_lines: nil, lines: [])
22
+ def initialize(lines: [])
32
23
  @lines = Array(lines)
33
- @code_lines = code_lines
24
+ end
25
+
26
+ def mark_invisible
27
+ @lines.map(&:mark_invisible)
34
28
  end
35
29
 
36
30
  def is_end?
@@ -38,11 +32,11 @@ module SyntaxErrorSearch
38
32
  end
39
33
 
40
34
  def starts_at
41
- @lines.first&.line_number
35
+ @starts_at ||= @lines.first&.line_number
42
36
  end
43
37
 
44
- def code_lines
45
- @code_lines
38
+ def ends_at
39
+ @ends_at ||= @lines.last&.line_number
46
40
  end
47
41
 
48
42
  # This is used for frontier ordering, we are searching from
@@ -53,155 +47,8 @@ module SyntaxErrorSearch
53
47
  self.current_indent <=> other.current_indent
54
48
  end
55
49
 
56
- # Only the lines that are not empty and visible
57
- def visible_lines
58
- @lines
59
- .select(&:not_empty?)
60
- .select(&:visible?)
61
- end
62
-
63
- # This method is used to expand a code block to capture it's calling context
64
- def expand_until_next_boundry
65
- expand_to_indent(next_indent)
66
- self
67
- end
68
-
69
- # This method expands the given code block until it captures
70
- # its nearest neighbors. This is used to expand a single line of code
71
- # to its smallest likely block.
72
- #
73
- # code_block.to_s # =>
74
- # # puts "foo"
75
- # code_block.expand_until_neighbors
76
- #
77
- # code_block.to_s # =>
78
- # # puts "foo"
79
- # # puts "bar"
80
- # # puts "baz"
81
- #
82
- def expand_until_neighbors
83
- expand_to_indent(current_indent)
84
-
85
- expand_hidden_parner_line if self.to_s.strip == "end"
86
- self
87
- end
88
-
89
- def expand_hidden_parner_line
90
- index = @lines.first.index
91
- indent = current_indent
92
- partner_line = code_lines.select {|line| line.index < index && line.indent == indent }.last
93
-
94
- if partner_line&.hidden?
95
- partner_line.mark_visible
96
- @lines.prepend(partner_line)
97
- end
98
- end
99
-
100
- # This method expands the existing code block up (before)
101
- # and down (after). It will break on change in indentation
102
- # and empty lines.
103
- #
104
- # code_block.to_s # =>
105
- # # def foo
106
- # # puts "foo"
107
- # # end
108
- #
109
- # code_block.expand_to_indent(0)
110
- # code_block.to_s # =>
111
- # # class Foo
112
- # # def foo
113
- # # puts "foo"
114
- # # end
115
- # # end
116
- #
117
- private def expand_to_indent(indent)
118
- array = []
119
- before_lines(skip_empty: false).each do |line|
120
- if line.empty?
121
- array.prepend(line)
122
- break
123
- end
124
-
125
- if line.indent == indent
126
- array.prepend(line)
127
- else
128
- break
129
- end
130
- end
131
-
132
- array << @lines
133
-
134
- after_lines(skip_empty: false).each do |line|
135
- if line.empty?
136
- array << line
137
- break
138
- end
139
-
140
- if line.indent == indent
141
- array << line
142
- else
143
- break
144
- end
145
- end
146
-
147
- @lines = array.flatten
148
- end
149
-
150
- def next_indent
151
- [
152
- before_line&.indent || 0,
153
- after_line&.indent || 0
154
- ].max
155
- end
156
-
157
50
  def current_indent
158
- lines.detect(&:not_empty?)&.indent || 0
159
- end
160
-
161
- def before_line
162
- before_lines.first
163
- end
164
-
165
- def after_line
166
- after_lines.first
167
- end
168
-
169
- def before_lines(skip_empty: true)
170
- index = @lines.first.index
171
- lines = code_lines.select {|line| line.index < index }
172
- lines.select!(&:not_empty?) if skip_empty
173
- lines.select!(&:visible?)
174
- lines.reverse!
175
-
176
- lines
177
- end
178
-
179
- def after_lines(skip_empty: true)
180
- index = @lines.last.index
181
- lines = code_lines.select {|line| line.index > index }
182
- lines.select!(&:not_empty?) if skip_empty
183
- lines.select!(&:visible?)
184
- lines
185
- end
186
-
187
- # Returns a code block of the source that does not include
188
- # the current lines. This is useful for checking if a source
189
- # with the given lines removed parses successfully. If so
190
- #
191
- # Then it's proof that the current block is invalid
192
- def block_without
193
- @block_without ||= CodeBlock.new(
194
- source: @source,
195
- lines: @source.code_lines - @lines
196
- )
197
- end
198
-
199
- def document_valid_without?
200
- block_without.valid?
201
- end
202
-
203
- def valid_without?
204
- block_without.valid?
51
+ @current_indent ||= lines.select(&:not_empty?).map(&:indent).min || 0
205
52
  end
206
53
 
207
54
  def invalid?
@@ -1,178 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxErrorSearch
4
- # This class is responsible for generating, storing, and sorting code blocks
4
+ # The main function of the frontier is to hold the edges of our search and to
5
+ # evaluate when we can stop searching.
5
6
  #
6
- # The search algorithm for finding our syntax errors isn't in this class, but
7
- # this is class holds the bulk of the logic for generating, storing, detecting
8
- # and filtering invalid code.
7
+ # ## Knowing where we've been
9
8
  #
10
- # This is loosely based on the idea of a "frontier" for searching for a path
11
- # example: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
9
+ # Once a code block is generated it is added onto the frontier where it will be
10
+ # sorted and then the frontier can be filtered. Large blocks that totally contain a
11
+ # smaller block will cause the smaller block to be evicted.
12
12
  #
13
- # In this case our path is going from code with a syntax error to code without a
14
- # syntax error. We're currently doing that by evaluating individual lines
15
- # with respect to indentation and other whitespace (empty lines). As represented
16
- # by individual "code blocks".
13
+ # CodeFrontier#<<
14
+ # CodeFrontier#pop
17
15
  #
18
- # This class does not just store the frontier that we're searching, but is responsible
19
- # for generating new code blocks as well. This is not ideal, but the state of generating
20
- # and evaluating paths i.e. codeblocks is very tightly coupled.
16
+ # ## Knowing where we can go
21
17
  #
22
- # ## Creation
18
+ # Internally it keeps track of an "indent hash" which is exposed via `next_indent_line`
19
+ # when called this will return a line of code with the most indentation.
23
20
  #
24
- # This example code is re-used in the other sections
21
+ # This line of code can be used to build a CodeBlock via and then when that code block
22
+ # is added back to the frontier, then the lines in the code block are removed from the
23
+ # indent hash so we don't double-create the same block.
25
24
  #
26
- # Example:
25
+ # CodeFrontier#next_indent_line
26
+ # CodeFrontier#register_indent_block
27
27
  #
28
- # code_lines = [
29
- # CodeLine.new(line: "def cinco\n", index: 0)
30
- # CodeLine.new(line: " def dog\n", index: 1) # Syntax error 1
31
- # CodeLine.new(line: " def cat\n", index: 2) # Syntax error 2
32
- # CodeLine.new(line: "end\n", index: 3)
33
- # ]
28
+ # ## Knowing when to stop
34
29
  #
35
- # frontier = CodeFrontier.new(code_lines: code_lines)
30
+ # The frontier holds the syntax error when removing all code blocks from the original
31
+ # source document allows it to be parsed as syntatically valid:
36
32
  #
37
- # frontier << frontier.next_block if frontier.next_block?
38
- # frontier << frontier.next_block if frontier.next_block?
33
+ # CodeFrontier#holds_all_syntax_errors?
39
34
  #
40
- # frontier.holds_all_syntax_errors? # => true
41
- # block = frontier.pop
42
- # frontier.holds_all_syntax_errors? # => false
43
- # frontier << block
44
- # frontier.holds_all_syntax_errors? # => true
35
+ # ## Filtering false positives
45
36
  #
46
- # frontier.detect_invalid_blocks.map(&:to_s) # =>
47
- # [
48
- # "def dog\n",
49
- # "def cat\n"
50
- # ]
37
+ # Once the search is completed, the frontier will have many blocks that do not contain
38
+ # the syntax error. To filter to the smallest subset that does call:
51
39
  #
52
- # ## Block Generation
53
- #
54
- # Currently code blocks are generated based off of indentation. With the idea that blocks are,
55
- # well, indented. Once a code block is added to the frontier or it is expanded, or it is generated
56
- # then we also need to remove those lines from our generation code so we don't generate the same block
57
- # twice by accident.
58
- #
59
- # This is block generation is currently done via the "indent_hash" internally by starting at the outer
60
- # most indentation.
61
- #
62
- # Example:
63
- #
64
- # ```
65
- # def river
66
- # puts "lol" # <=== Start looking here and expand outwards
67
- # end
68
- # ```
69
- #
70
- # Generating new code blocks is a little verbose but looks like this:
71
- #
72
- # frontier << frontier.next_block if frontier.next_block?
73
- #
74
- # Once a block is in the frontier, it can be popped off:
75
- #
76
- # frontier.pop
77
- # # => <# CodeBlock >
78
- #
79
- # ## Block (frontier) storage, ordering and retrieval
80
- #
81
- # Once a block is generated it is stored internally in a frontier array. This is very similar to a search algorithm.
82
- # The array is sorted by indentation order, so that when a block is popped off the array, the one with
83
- # the largest current indentation is evaluated first.
84
- #
85
- # For example, if we have these two blocks in the frontier:
86
- #
87
- # ```
88
- # # Block A - 0 spaces for indentation
89
- #
90
- # def cinco
91
- # puts "lol"
92
- # end
93
- # ```
94
- #
95
- # ```
96
- # # Block B - 2 spaces for indentation
97
- #
98
- # def river
99
- # puts "hehe"
100
- # end
101
- # ```
102
- #
103
- # The "Block B" has more current indentation, so it would be evaluated first.
104
- #
105
- # ## Frontier evaluation (Find the syntax error)
106
- #
107
- # Another key difference between this and a normal search "frontier" is that we're not checking if
108
- # an individual code block meets the goal (turning invalid code to valid code) since there can
109
- # be multiple syntax errors and this will require multiple code blocks. To handle this, we're
110
- # evaluating all the contents of the frontier at the same time to see if the solution exists in any
111
- # of our search blocks.
112
- #
113
- # # Using the previously generated frontier
114
- #
115
- # frontier << Block.new(lines: code_lines[1], code_lines: code_lines)
116
- # frontier.holds_all_syntax_errors? # => false
117
- #
118
- # frontier << Block.new(lines: code_lines[2], code_lines: code_lines)
119
- # frontier.holds_all_syntax_errors? # => true
120
- #
121
- # ## Detect invalid blocks (Filter for smallest solution)
122
- #
123
- # After we prove that a solution exists and we've found it to be in our frontier, we can start stop searching.
124
- # Once we've done this, we need to search through the existing frontier code blocks to find the minimum combination
125
- # of blocks that hold the solution. This is done in: `detect_invalid_blocks`.
126
- #
127
- # # Using the previously generated frontier
128
- #
129
- # frontier << CodeBlock.new(lines: code_lines[0], code_lines: code_lines)
130
- # frontier << CodeBlock.new(lines: code_lines[1], code_lines: code_lines)
131
- # frontier << CodeBlock.new(lines: code_lines[2], code_lines: code_lines)
132
- # frontier << CodeBlock.new(lines: code_lines[3], code_lines: code_lines)
133
- #
134
- # frontier.count # => 4
135
- # frontier.detect_invalid_blocks.length => 2
136
- # frontier.detect_invalid_blocks.map(&:to_s) # =>
137
- # [
138
- # "def dog\n",
139
- # "def cat\n"
140
- # ]
141
- #
142
- # Once invalid blocks are found and filtered, then they can be passed to a formatter.
143
- #
144
- #
145
- #
146
-
147
- class IndentScan
148
- attr_reader :code_lines
149
-
150
- def initialize(code_lines: )
151
- @code_lines = code_lines
152
- end
153
-
154
- def neighbors_from_top(top_line)
155
- code_lines
156
- .select {|l| l.index >= top_line.index }
157
- .select {|l| l.not_empty? }
158
- .select {|l| l.visible? }
159
- .take_while {|l| l.indent >= top_line.indent }
160
- end
161
-
162
- def each_neighbor_block(top_line)
163
- neighbors = neighbors_from_top(top_line)
164
-
165
- until neighbors.empty?
166
- lines = [neighbors.pop]
167
- while (block = CodeBlock.new(lines: lines, code_lines: code_lines)) && block.invalid? && neighbors.any?
168
- lines.prepend neighbors.pop
169
- end
170
-
171
- yield block if block
172
- end
173
- end
174
- end
175
-
40
+ # CodeFrontier#detect_invalid_blocks
176
41
  class CodeFrontier
177
42
  def initialize(code_lines: )
178
43
  @code_lines = code_lines
@@ -207,16 +72,9 @@ module SyntaxErrorSearch
207
72
 
208
73
  # Returns a code block with the largest indentation possible
209
74
  def pop
210
- return nil if empty?
211
-
212
75
  return @frontier.pop
213
76
  end
214
77
 
215
- def next_block?
216
- !@indent_hash.empty?
217
- end
218
-
219
-
220
78
  def indent_hash_indent
221
79
  @indent_hash.keys.sort.last
222
80
  end
@@ -226,40 +84,25 @@ module SyntaxErrorSearch
226
84
  @indent_hash[indent]&.first
227
85
  end
228
86
 
229
- def generate_blocks
230
- end
231
-
232
- def next_block
233
- indent = @indent_hash.keys.sort.last
234
- lines = @indent_hash[indent].first
235
-
236
- block = CodeBlock.new(
237
- lines: lines,
238
- code_lines: @code_lines
239
- ).expand_until_neighbors
240
-
241
- register(block)
242
- block
243
- end
244
-
245
87
  def expand?
246
88
  return false if @frontier.empty?
247
89
  return true if @indent_hash.empty?
248
90
 
249
- @frontier.last.current_indent >= @indent_hash.keys.sort.last
250
- end
91
+ frontier_indent = @frontier.last.current_indent
92
+ hash_indent = @indent_hash.keys.sort.last
251
93
 
252
- # This method is responsible for determining if a new code
253
- # block should be generated instead of evaluating an already
254
- # existing block in the frontier
255
- def generate_new_block?
256
- return false if @indent_hash.empty?
257
- return true if @frontier.empty?
94
+ if ENV["DEBUG"]
95
+ puts "```"
96
+ puts @frontier.last.to_s
97
+ puts "```"
98
+ puts " @frontier indent: #{frontier_indent}"
99
+ puts " @hash indent: #{hash_indent}"
100
+ end
258
101
 
259
- @frontier.last.current_indent <= @indent_hash.keys.sort.last
102
+ frontier_indent >= hash_indent
260
103
  end
261
104
 
262
- def register(block)
105
+ def register_indent_block(block)
263
106
  block.lines.each do |line|
264
107
  @indent_hash[line.indent]&.delete(line)
265
108
  end
@@ -273,22 +116,18 @@ module SyntaxErrorSearch
273
116
  # and that each code block's lines are removed from the indentation hash so we
274
117
  # don't re-evaluate the same line multiple times.
275
118
  def <<(block)
276
- register(block)
119
+ register_indent_block(block)
277
120
 
121
+ # Make sure we don't double expand, if a code block fully engulfs another code block, keep the bigger one
122
+ @frontier.reject! {|b|
123
+ b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
124
+ }
278
125
  @frontier << block
279
126
  @frontier.sort!
280
127
 
281
128
  self
282
129
  end
283
130
 
284
- def any?
285
- !empty?
286
- end
287
-
288
- def empty?
289
- @frontier.empty? && @indent_hash.empty?
290
- end
291
-
292
131
  # Example:
293
132
  #
294
133
  # combination([:a, :b, :c, :d])
@@ -3,15 +3,16 @@
3
3
  module SyntaxErrorSearch
4
4
  # Searches code for a syntax error
5
5
  #
6
- # The bulk of the heavy lifting is done by the CodeFrontier
6
+ # The bulk of the heavy lifting is done in:
7
7
  #
8
- # The flow looks like this:
8
+ # - CodeFrontier (Holds information for generating blocks and determining if we can stop searching)
9
+ # - ParseBlocksFromLine (Creates blocks into the frontier)
10
+ # - BlockExpand (Expands existing blocks to search more code
9
11
  #
10
12
  # ## Syntax error detection
11
13
  #
12
14
  # When the frontier holds the syntax error, we can stop searching
13
15
  #
14
- #
15
16
  # search = CodeSearch.new(<<~EOM)
16
17
  # def dog
17
18
  # def lol
@@ -23,7 +24,6 @@ module SyntaxErrorSearch
23
24
  # search.invalid_blocks.map(&:to_s) # =>
24
25
  # # => ["def lol\n"]
25
26
  #
26
- #
27
27
  class CodeSearch
28
28
  private; attr_reader :frontier; public
29
29
  public; attr_reader :invalid_blocks, :record_dir, :code_lines
@@ -41,24 +41,33 @@ module SyntaxErrorSearch
41
41
  @invalid_blocks = []
42
42
  @name_tick = Hash.new {|hash, k| hash[k] = 0 }
43
43
  @tick = 0
44
- @scan = IndentScan.new(code_lines: @code_lines)
44
+ @block_expand = BlockExpand.new(code_lines: code_lines)
45
+ @parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
45
46
  end
46
47
 
48
+ # Used for debugging
47
49
  def record(block:, name: "record")
48
50
  return if !@record_dir
49
51
  @name_tick[name] += 1
50
52
  filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt"
53
+ if ENV["DEBUG"]
54
+ puts "\n\n==== #{filename} ===="
55
+ puts "\n```#{block.starts_at}:#{block.ends_at}"
56
+ puts "#{block.to_s}"
57
+ puts "```"
58
+ puts " block indent: #{block.current_indent}"
59
+ end
51
60
  @record_dir.join(filename).open(mode: "a") do |f|
52
61
  display = DisplayInvalidBlocks.new(
53
62
  blocks: block,
54
- terminal: false
63
+ terminal: false,
64
+ code_lines: @code_lines,
55
65
  )
56
66
  f.write(display.indent display.code_with_lines)
57
67
  end
58
68
  end
59
69
 
60
- def push_if_invalid(block, name: )
61
- frontier.register(block)
70
+ def push(block, name: )
62
71
  record(block: block, name: name)
63
72
 
64
73
  if block.valid?
@@ -69,32 +78,36 @@ module SyntaxErrorSearch
69
78
  end
70
79
  end
71
80
 
81
+ # Parses the most indented lines into blocks that are marked
82
+ # and added to the frontier
72
83
  def add_invalid_blocks
73
84
  max_indent = frontier.next_indent_line&.indent
74
85
 
75
86
  while (line = frontier.next_indent_line) && (line.indent == max_indent)
76
- neighbors = @scan.neighbors_from_top(frontier.next_indent_line)
77
87
 
78
- @scan.each_neighbor_block(frontier.next_indent_line) do |block|
88
+ @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|
79
89
  record(block: block, name: "add")
80
- if block.valid?
81
- block.lines.each(&:mark_invisible)
82
- end
83
- end
84
90
 
85
- block = CodeBlock.new(lines: neighbors, code_lines: @code_lines)
86
- push_if_invalid(block, name: "add")
91
+ block.mark_invisible if block.valid?
92
+ push(block, name: "add")
93
+ end
87
94
  end
88
95
  end
89
96
 
97
+ # Given an already existing block in the frontier, expand it to see
98
+ # if it contains our invalid syntax
90
99
  def expand_invalid_block
91
100
  block = frontier.pop
92
101
  return unless block
93
102
 
94
- block.expand_until_next_boundry
95
- push_if_invalid(block, name: "expand")
103
+ record(block: block, name: "pop")
104
+
105
+ # block = block.expand_until_next_boundry
106
+ block = @block_expand.call(block)
107
+ push(block, name: "expand")
96
108
  end
97
109
 
110
+ # Main search loop
98
111
  def call
99
112
  until frontier.holds_all_syntax_errors?
100
113
  @tick += 1
@@ -5,14 +5,14 @@ module SyntaxErrorSearch
5
5
  class DisplayInvalidBlocks
6
6
  attr_reader :filename
7
7
 
8
- def initialize(blocks:, io: $stderr, filename: nil, terminal: false, invalid_type: :unmatched_end)
8
+ def initialize(code_lines: ,blocks:, io: $stderr, filename: nil, terminal: false, invalid_type: :unmatched_end)
9
9
  @terminal = terminal
10
10
  @filename = filename
11
11
  @io = io
12
12
 
13
13
  @blocks = Array(blocks)
14
14
  @lines = @blocks.map(&:lines).flatten
15
- @code_lines = @blocks.first&.code_lines || []
15
+ @code_lines = code_lines
16
16
  @digit_count = @code_lines.last&.line_number.to_s.length
17
17
 
18
18
  @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true }
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxErrorSearch
4
+ # This class is responsible for generating initial code blocks
5
+ # that will then later be expanded.
6
+ #
7
+ # The biggest concern when guessing about code blocks, is accidentally
8
+ # grabbing one that contains only an "end". In this example:
9
+ #
10
+ # def dog
11
+ # begonn # mispelled `begin`
12
+ # puts "bark"
13
+ # end
14
+ # end
15
+ #
16
+ # The following lines would be matched (from bottom to top):
17
+ #
18
+ # 1) end
19
+ #
20
+ # 2) puts "bark"
21
+ # end
22
+ #
23
+ # 3) begonn
24
+ # puts "bark"
25
+ # end
26
+ #
27
+ # At this point it has no where else to expand, and it will yield this inner
28
+ # code as a block
29
+ class ParseBlocksFromIndentLine
30
+ attr_reader :code_lines
31
+
32
+ def initialize(code_lines: )
33
+ @code_lines = code_lines
34
+ end
35
+
36
+ # Builds blocks from bottom up
37
+ def each_neighbor_block(target_line)
38
+ scan = AroundBlockScan.new(code_lines: code_lines, block: CodeBlock.new(lines: target_line))
39
+ .skip(:empty?)
40
+ .skip(:hidden?)
41
+ .scan_while {|line| line.indent >= target_line.indent }
42
+
43
+ neighbors = @code_lines[scan.before_index..scan.after_index]
44
+
45
+ until neighbors.empty?
46
+ lines = [neighbors.pop]
47
+ while (block = CodeBlock.new(lines: lines)) && block.invalid? && neighbors.any?
48
+ lines.prepend neighbors.pop
49
+ end
50
+
51
+ yield block if block
52
+ end
53
+ end
54
+ end
55
+ end
56
+
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxErrorSearch
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntax_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - schneems
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-12 00:00:00.000000000 Z
11
+ date: 2020-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -49,13 +49,16 @@ files:
49
49
  - bin/setup
50
50
  - exe/syntax_search
51
51
  - lib/syntax_search.rb
52
+ - lib/syntax_search/around_block_scan.rb
52
53
  - lib/syntax_search/auto.rb
54
+ - lib/syntax_search/block_expand.rb
53
55
  - lib/syntax_search/code_block.rb
54
56
  - lib/syntax_search/code_frontier.rb
55
57
  - lib/syntax_search/code_line.rb
56
58
  - lib/syntax_search/code_search.rb
57
59
  - lib/syntax_search/display_invalid_blocks.rb
58
60
  - lib/syntax_search/fyi.rb
61
+ - lib/syntax_search/parse_blocks_from_indent_line.rb
59
62
  - lib/syntax_search/version.rb
60
63
  - syntax_search.gemspec
61
64
  homepage: https://github.com/zombocom/syntax_search.git