syntax_search 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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