syntax_search 0.2.0 → 0.2.1
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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +2 -29
- data/Gemfile.lock +3 -1
- data/exe/syntax_search +5 -71
- data/lib/syntax_search/auto.rb +5 -49
- data/syntax_search.gemspec +3 -3
- metadata +17 -19
- data/lib/syntax_search.rb +0 -145
- data/lib/syntax_search/around_block_scan.rb +0 -193
- data/lib/syntax_search/block_expand.rb +0 -74
- data/lib/syntax_search/capture_code_context.rb +0 -62
- data/lib/syntax_search/code_block.rb +0 -78
- data/lib/syntax_search/code_frontier.rb +0 -151
- data/lib/syntax_search/code_line.rb +0 -128
- data/lib/syntax_search/code_search.rb +0 -154
- data/lib/syntax_search/display_code_with_line_numbers.rb +0 -56
- data/lib/syntax_search/display_invalid_blocks.rb +0 -100
- data/lib/syntax_search/fyi.rb +0 -7
- data/lib/syntax_search/heredoc_block_parse.rb +0 -30
- data/lib/syntax_search/lex_all.rb +0 -58
- data/lib/syntax_search/parse_blocks_from_indent_line.rb +0 -56
- data/lib/syntax_search/version.rb +0 -5
- data/lib/syntax_search/who_dis_syntax_error.rb +0 -32
@@ -1,74 +0,0 @@
|
|
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
|
-
.skip(:hidden?)
|
48
|
-
.stop_after_kw
|
49
|
-
.scan_adjacent_indent
|
50
|
-
.code_block
|
51
|
-
end
|
52
|
-
|
53
|
-
def expand_neighbors(block, grab_empty: true)
|
54
|
-
scan = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
55
|
-
.skip(:hidden?)
|
56
|
-
.stop_after_kw
|
57
|
-
.scan_neighbors
|
58
|
-
|
59
|
-
# Slurp up empties
|
60
|
-
if grab_empty
|
61
|
-
scan = AroundBlockScan.new(code_lines: @code_lines, block: scan.code_block)
|
62
|
-
.scan_while {|line| line.empty? || line.hidden? }
|
63
|
-
end
|
64
|
-
|
65
|
-
new_block = scan.code_block
|
66
|
-
|
67
|
-
if block.lines == new_block.lines
|
68
|
-
return nil
|
69
|
-
else
|
70
|
-
return new_block
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SyntaxErrorSearch
|
4
|
-
|
5
|
-
# Given a block, this method will capture surrounding
|
6
|
-
# code to give the user more context for the location of
|
7
|
-
# the problem.
|
8
|
-
#
|
9
|
-
# Return is an array of CodeLines to be rendered.
|
10
|
-
#
|
11
|
-
# Surrounding code is captured regardless of visible state
|
12
|
-
#
|
13
|
-
# puts block.to_s # => "def bark"
|
14
|
-
#
|
15
|
-
# context = CaptureCodeContext.new(
|
16
|
-
# blocks: block,
|
17
|
-
# code_lines: code_lines
|
18
|
-
# )
|
19
|
-
#
|
20
|
-
# puts context.call.join
|
21
|
-
# # =>
|
22
|
-
# class Dog
|
23
|
-
# def bark
|
24
|
-
# end
|
25
|
-
#
|
26
|
-
class CaptureCodeContext
|
27
|
-
attr_reader :code_lines
|
28
|
-
|
29
|
-
def initialize(blocks: , code_lines:)
|
30
|
-
@blocks = Array(blocks)
|
31
|
-
@code_lines = code_lines
|
32
|
-
@visible_lines = @blocks.map(&:visible_lines).flatten
|
33
|
-
@lines_to_output = @visible_lines.dup
|
34
|
-
end
|
35
|
-
|
36
|
-
def call
|
37
|
-
@blocks.each do |block|
|
38
|
-
around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
39
|
-
.start_at_next_line
|
40
|
-
.capture_neighbor_context
|
41
|
-
|
42
|
-
around_lines -= block.lines
|
43
|
-
|
44
|
-
@lines_to_output.concat(around_lines)
|
45
|
-
|
46
|
-
AroundBlockScan.new(
|
47
|
-
block: block,
|
48
|
-
code_lines: @code_lines,
|
49
|
-
).on_falling_indent do |line|
|
50
|
-
@lines_to_output << line
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
@lines_to_output.select!(&:not_empty?)
|
55
|
-
@lines_to_output.select!(&:not_comment?)
|
56
|
-
@lines_to_output.uniq!
|
57
|
-
@lines_to_output.sort!
|
58
|
-
|
59
|
-
return @lines_to_output
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SyntaxErrorSearch
|
4
|
-
# Multiple lines form a singular CodeBlock
|
5
|
-
#
|
6
|
-
# Source code is made of multiple CodeBlocks.
|
7
|
-
#
|
8
|
-
# Example:
|
9
|
-
#
|
10
|
-
# code_block.to_s # =>
|
11
|
-
# # def foo
|
12
|
-
# # puts "foo"
|
13
|
-
# # end
|
14
|
-
#
|
15
|
-
# code_block.valid? # => true
|
16
|
-
# code_block.in_valid? # => false
|
17
|
-
#
|
18
|
-
#
|
19
|
-
class CodeBlock
|
20
|
-
attr_reader :lines
|
21
|
-
|
22
|
-
def initialize(lines: [])
|
23
|
-
@lines = Array(lines)
|
24
|
-
end
|
25
|
-
|
26
|
-
def visible_lines
|
27
|
-
@lines.select(&:visible?).select(&:not_empty?)
|
28
|
-
end
|
29
|
-
|
30
|
-
def mark_invisible
|
31
|
-
@lines.map(&:mark_invisible)
|
32
|
-
end
|
33
|
-
|
34
|
-
def is_end?
|
35
|
-
to_s.strip == "end"
|
36
|
-
end
|
37
|
-
|
38
|
-
def hidden?
|
39
|
-
@lines.all?(&:hidden?)
|
40
|
-
end
|
41
|
-
|
42
|
-
def starts_at
|
43
|
-
@starts_at ||= @lines.first&.line_number
|
44
|
-
end
|
45
|
-
|
46
|
-
def ends_at
|
47
|
-
@ends_at ||= @lines.last&.line_number
|
48
|
-
end
|
49
|
-
|
50
|
-
# This is used for frontier ordering, we are searching from
|
51
|
-
# the largest indentation to the smallest. This allows us to
|
52
|
-
# populate an array with multiple code blocks then call `sort!`
|
53
|
-
# on it without having to specify the sorting criteria
|
54
|
-
def <=>(other)
|
55
|
-
out = self.current_indent <=> other.current_indent
|
56
|
-
return out if out != 0
|
57
|
-
|
58
|
-
# Stable sort
|
59
|
-
self.starts_at <=> other.starts_at
|
60
|
-
end
|
61
|
-
|
62
|
-
def current_indent
|
63
|
-
@current_indent ||= lines.select(&:not_empty?).map(&:indent).min || 0
|
64
|
-
end
|
65
|
-
|
66
|
-
def invalid?
|
67
|
-
!valid?
|
68
|
-
end
|
69
|
-
|
70
|
-
def valid?
|
71
|
-
SyntaxErrorSearch.valid?(self.to_s)
|
72
|
-
end
|
73
|
-
|
74
|
-
def to_s
|
75
|
-
@lines.join
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
@@ -1,151 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SyntaxErrorSearch
|
4
|
-
# The main function of the frontier is to hold the edges of our search and to
|
5
|
-
# evaluate when we can stop searching.
|
6
|
-
#
|
7
|
-
# ## Knowing where we've been
|
8
|
-
#
|
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
|
-
#
|
13
|
-
# CodeFrontier#<<
|
14
|
-
# CodeFrontier#pop
|
15
|
-
#
|
16
|
-
# ## Knowing where we can go
|
17
|
-
#
|
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.
|
20
|
-
#
|
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.
|
24
|
-
#
|
25
|
-
# CodeFrontier#next_indent_line
|
26
|
-
# CodeFrontier#register_indent_block
|
27
|
-
#
|
28
|
-
# ## Knowing when to stop
|
29
|
-
#
|
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:
|
32
|
-
#
|
33
|
-
# CodeFrontier#holds_all_syntax_errors?
|
34
|
-
#
|
35
|
-
# ## Filtering false positives
|
36
|
-
#
|
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:
|
39
|
-
#
|
40
|
-
# CodeFrontier#detect_invalid_blocks
|
41
|
-
class CodeFrontier
|
42
|
-
def initialize(code_lines: )
|
43
|
-
@code_lines = code_lines
|
44
|
-
@frontier = []
|
45
|
-
@indent_hash = {}
|
46
|
-
code_lines.each do |line|
|
47
|
-
next if line.empty?
|
48
|
-
|
49
|
-
@indent_hash[line.indent] ||= []
|
50
|
-
@indent_hash[line.indent] << line
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def count
|
55
|
-
@frontier.count
|
56
|
-
end
|
57
|
-
|
58
|
-
# Returns true if the document is valid with all lines
|
59
|
-
# removed. By default it checks all blocks in present in
|
60
|
-
# the frontier array, but can be used for arbitrary arrays
|
61
|
-
# of codeblocks as well
|
62
|
-
def holds_all_syntax_errors?(block_array = @frontier)
|
63
|
-
without_lines = block_array.map do |block|
|
64
|
-
block.lines
|
65
|
-
end
|
66
|
-
|
67
|
-
SyntaxErrorSearch.valid_without?(
|
68
|
-
without_lines: without_lines,
|
69
|
-
code_lines: @code_lines
|
70
|
-
)
|
71
|
-
end
|
72
|
-
|
73
|
-
# Returns a code block with the largest indentation possible
|
74
|
-
def pop
|
75
|
-
return @frontier.pop
|
76
|
-
end
|
77
|
-
|
78
|
-
def indent_hash_indent
|
79
|
-
@indent_hash.keys.sort.last
|
80
|
-
end
|
81
|
-
|
82
|
-
def next_indent_line
|
83
|
-
indent = @indent_hash.keys.sort.last
|
84
|
-
@indent_hash[indent]&.first
|
85
|
-
end
|
86
|
-
|
87
|
-
def expand?
|
88
|
-
return false if @frontier.empty?
|
89
|
-
return true if @indent_hash.empty?
|
90
|
-
|
91
|
-
frontier_indent = @frontier.last.current_indent
|
92
|
-
hash_indent = @indent_hash.keys.sort.last
|
93
|
-
|
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
|
101
|
-
|
102
|
-
frontier_indent >= hash_indent
|
103
|
-
end
|
104
|
-
|
105
|
-
def register_indent_block(block)
|
106
|
-
block.lines.each do |line|
|
107
|
-
@indent_hash[line.indent]&.delete(line)
|
108
|
-
end
|
109
|
-
@indent_hash.select! {|k, v| !v.empty?}
|
110
|
-
self
|
111
|
-
end
|
112
|
-
|
113
|
-
# Add a block to the frontier
|
114
|
-
#
|
115
|
-
# This method ensures the frontier always remains sorted (in indentation order)
|
116
|
-
# and that each code block's lines are removed from the indentation hash so we
|
117
|
-
# don't re-evaluate the same line multiple times.
|
118
|
-
def <<(block)
|
119
|
-
register_indent_block(block)
|
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
|
-
}
|
125
|
-
@frontier << block
|
126
|
-
@frontier.sort!
|
127
|
-
|
128
|
-
self
|
129
|
-
end
|
130
|
-
|
131
|
-
# Example:
|
132
|
-
#
|
133
|
-
# combination([:a, :b, :c, :d])
|
134
|
-
# # => [[: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]]
|
135
|
-
def self.combination(array)
|
136
|
-
guesses = []
|
137
|
-
1.upto(array.length).each do |size|
|
138
|
-
guesses.concat(array.combination(size).to_a)
|
139
|
-
end
|
140
|
-
guesses
|
141
|
-
end
|
142
|
-
|
143
|
-
# Given that we know our syntax error exists somewhere in our frontier, we want to find
|
144
|
-
# the smallest possible set of blocks that contain all the syntax errors
|
145
|
-
def detect_invalid_blocks
|
146
|
-
self.class.combination(@frontier).detect do |block_array|
|
147
|
-
holds_all_syntax_errors?(block_array)
|
148
|
-
end || []
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
@@ -1,128 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SyntaxErrorSearch
|
4
|
-
# Represents a single line of code of a given source file
|
5
|
-
#
|
6
|
-
# This object contains metadata about the line such as
|
7
|
-
# amount of indentation. An if it is empty or not.
|
8
|
-
#
|
9
|
-
# While a given search for syntax errors is being performed
|
10
|
-
# state about the search can be stored in individual lines such
|
11
|
-
# as :valid or :invalid.
|
12
|
-
#
|
13
|
-
# Visibility of lines can be toggled on and off.
|
14
|
-
#
|
15
|
-
# Example:
|
16
|
-
#
|
17
|
-
# line = CodeLine.new(line: "def foo\n", index: 0)
|
18
|
-
# line.line_number => 1
|
19
|
-
# line.empty? # => false
|
20
|
-
# line.visible? # => true
|
21
|
-
# line.mark_invisible
|
22
|
-
# line.visible? # => false
|
23
|
-
#
|
24
|
-
# A CodeBlock is made of multiple CodeLines
|
25
|
-
#
|
26
|
-
# Marking a line as invisible indicates that it should not be used
|
27
|
-
# for syntax checks. It's essentially the same as commenting it out
|
28
|
-
#
|
29
|
-
# Marking a line as invisible also lets the overall program know
|
30
|
-
# that it should not check that area for syntax errors.
|
31
|
-
class CodeLine
|
32
|
-
attr_reader :line, :index, :indent, :original_line
|
33
|
-
|
34
|
-
def initialize(line: , index:)
|
35
|
-
@original_line = line.freeze
|
36
|
-
@line = @original_line
|
37
|
-
@empty = line.strip.empty?
|
38
|
-
@index = index
|
39
|
-
@indent = SpaceCount.indent(line)
|
40
|
-
@status = nil # valid, invalid, unknown
|
41
|
-
@invalid = false
|
42
|
-
|
43
|
-
@kw_count = 0
|
44
|
-
@end_count = 0
|
45
|
-
@lex = LexAll.new(source: line)
|
46
|
-
@lex.each do |lex|
|
47
|
-
next unless lex.type == :on_kw
|
48
|
-
|
49
|
-
case lex.token
|
50
|
-
when 'def', 'case', 'for', 'begin', 'class', 'module', 'if', 'unless', 'while', 'until' , 'do'
|
51
|
-
@kw_count += 1
|
52
|
-
when 'end'
|
53
|
-
@end_count += 1
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
@is_comment = true if @lex.detect {|lex| lex.type != :on_sp}&.type == :on_comment
|
58
|
-
|
59
|
-
@is_kw = (@kw_count - @end_count) > 0
|
60
|
-
@is_end = (@end_count - @kw_count) > 0
|
61
|
-
end
|
62
|
-
|
63
|
-
def <=>(b)
|
64
|
-
self.index <=> b.index
|
65
|
-
end
|
66
|
-
|
67
|
-
def is_comment?
|
68
|
-
@is_comment
|
69
|
-
end
|
70
|
-
|
71
|
-
def not_comment?
|
72
|
-
!is_comment?
|
73
|
-
end
|
74
|
-
|
75
|
-
def is_kw?
|
76
|
-
@is_kw
|
77
|
-
end
|
78
|
-
|
79
|
-
def is_end?
|
80
|
-
@is_end
|
81
|
-
end
|
82
|
-
|
83
|
-
def mark_invalid
|
84
|
-
@invalid = true
|
85
|
-
self
|
86
|
-
end
|
87
|
-
|
88
|
-
def marked_invalid?
|
89
|
-
@invalid
|
90
|
-
end
|
91
|
-
|
92
|
-
def mark_invisible
|
93
|
-
@line = ""
|
94
|
-
self
|
95
|
-
end
|
96
|
-
|
97
|
-
def mark_visible
|
98
|
-
@line = @original_line
|
99
|
-
self
|
100
|
-
end
|
101
|
-
|
102
|
-
def visible?
|
103
|
-
!line.empty?
|
104
|
-
end
|
105
|
-
|
106
|
-
def hidden?
|
107
|
-
!visible?
|
108
|
-
end
|
109
|
-
|
110
|
-
def line_number
|
111
|
-
index + 1
|
112
|
-
end
|
113
|
-
|
114
|
-
alias :number :line_number
|
115
|
-
|
116
|
-
def not_empty?
|
117
|
-
!empty?
|
118
|
-
end
|
119
|
-
|
120
|
-
def empty?
|
121
|
-
@empty
|
122
|
-
end
|
123
|
-
|
124
|
-
def to_s
|
125
|
-
self.line
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|