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.
@@ -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