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