syntax_search 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|