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,154 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SyntaxErrorSearch
4
- # Searches code for a syntax error
5
- #
6
- # The bulk of the heavy lifting is done in:
7
- #
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
11
- #
12
- # ## Syntax error detection
13
- #
14
- # When the frontier holds the syntax error, we can stop searching
15
- #
16
- # search = CodeSearch.new(<<~EOM)
17
- # def dog
18
- # def lol
19
- # end
20
- # EOM
21
- #
22
- # search.call
23
- #
24
- # search.invalid_blocks.map(&:to_s) # =>
25
- # # => ["def lol\n"]
26
- #
27
- class CodeSearch
28
- private; attr_reader :frontier; public
29
- public; attr_reader :invalid_blocks, :record_dir, :code_lines
30
-
31
- def initialize(source, record_dir: ENV["SYNTAX_SEARCH_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
32
- @source = source
33
- if record_dir
34
- @time = Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')
35
- @record_dir = Pathname(record_dir).join(@time).tap {|p| p.mkpath }
36
- @write_count = 0
37
- end
38
- @code_lines = source.lines.map.with_index do |line, i|
39
- CodeLine.new(line: line, index: i)
40
- end
41
- @frontier = CodeFrontier.new(code_lines: @code_lines)
42
- @invalid_blocks = []
43
- @name_tick = Hash.new {|hash, k| hash[k] = 0 }
44
- @tick = 0
45
- @block_expand = BlockExpand.new(code_lines: code_lines)
46
- @parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
47
- end
48
-
49
- # Used for debugging
50
- def record(block:, name: "record")
51
- return if !@record_dir
52
- @name_tick[name] += 1
53
- filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt"
54
- if ENV["DEBUG"]
55
- puts "\n\n==== #{filename} ===="
56
- puts "\n```#{block.starts_at}:#{block.ends_at}"
57
- puts "#{block.to_s}"
58
- puts "```"
59
- puts " block indent: #{block.current_indent}"
60
- end
61
- @record_dir.join(filename).open(mode: "a") do |f|
62
- display = DisplayInvalidBlocks.new(
63
- blocks: block,
64
- terminal: false,
65
- code_lines: @code_lines,
66
- )
67
- f.write(display.indent display.code_with_lines)
68
- end
69
- end
70
-
71
- def push(block, name: )
72
- record(block: block, name: name)
73
-
74
- if block.valid?
75
- block.lines.each(&:mark_invisible)
76
- frontier << block
77
- else
78
- frontier << block
79
- end
80
- end
81
-
82
- # Removes the block without putting it back in the frontier
83
- def sweep(block:, name: )
84
- record(block: block, name: name)
85
-
86
- block.lines.each(&:mark_invisible)
87
- frontier.register_indent_block(block)
88
- end
89
-
90
- # Parses the most indented lines into blocks that are marked
91
- # and added to the frontier
92
- def add_invalid_blocks
93
- max_indent = frontier.next_indent_line&.indent
94
-
95
- while (line = frontier.next_indent_line) && (line.indent == max_indent)
96
-
97
- @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|
98
- record(block: block, name: "add")
99
-
100
- block.mark_invisible if block.valid?
101
- push(block, name: "add")
102
- end
103
- end
104
- end
105
-
106
- # Given an already existing block in the frontier, expand it to see
107
- # if it contains our invalid syntax
108
- def expand_invalid_block
109
- block = frontier.pop
110
- return unless block
111
-
112
- record(block: block, name: "pop")
113
-
114
- # block = block.expand_until_next_boundry
115
- block = @block_expand.call(block)
116
- push(block, name: "expand")
117
- end
118
-
119
- def sweep_heredocs
120
- HeredocBlockParse.new(
121
- source: @source,
122
- code_lines: @code_lines
123
- ).call.each do |block|
124
- push(block, name: "heredoc")
125
- end
126
- end
127
-
128
- def sweep_comments
129
- lines = @code_lines.select(&:is_comment?)
130
- return if lines.empty?
131
- block = CodeBlock.new(lines: lines)
132
- sweep(block: block, name: "comments")
133
- end
134
-
135
- # Main search loop
136
- def call
137
- sweep_heredocs
138
- sweep_comments
139
- until frontier.holds_all_syntax_errors?
140
- @tick += 1
141
-
142
- if frontier.expand?
143
- expand_invalid_block
144
- else
145
- add_invalid_blocks
146
- end
147
- end
148
-
149
- @invalid_blocks.concat(frontier.detect_invalid_blocks )
150
- @invalid_blocks.sort_by! {|block| block.starts_at }
151
- self
152
- end
153
- end
154
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SyntaxErrorSearch
4
- # Outputs code with highlighted lines
5
- #
6
- # Whatever is passed to this class will be rendered
7
- # even if it is "marked invisible" any filtering of
8
- # output should be done before calling this class.
9
- #
10
- #
11
- # DisplayCodeWithLineNumbers.new(
12
- # lines: lines,
13
- # highlight_lines: [lines[2], lines[3]]
14
- # ).call
15
- # # =>
16
- # 1
17
- # 2 def cat
18
- # ❯ 3 Dir.chdir
19
- # ❯ 4 end
20
- # 5 end
21
- # 6
22
- class DisplayCodeWithLineNumbers
23
- TERMINAL_HIGHLIGHT = "\e[1;3m" # Bold, italics
24
- TERMINAL_END = "\e[0m"
25
-
26
- def initialize(lines: , highlight_lines: [], terminal: false)
27
- @lines = lines.sort
28
- @terminal = terminal
29
- @highlight_line_hash = highlight_lines.each_with_object({}) {|line, h| h[line] = true }
30
- @digit_count = @lines.last&.line_number.to_s.length
31
- end
32
-
33
- def call
34
- @lines.map do |line|
35
- string = String.new("")
36
- if @highlight_line_hash[line]
37
- string << "❯ "
38
- else
39
- string << " "
40
- end
41
-
42
- number = line.line_number.to_s.rjust(@digit_count)
43
- string << number.to_s
44
- if line.empty?
45
- string << line.original_line
46
- else
47
- string << " "
48
- string << TERMINAL_HIGHLIGHT if @terminal && @highlight_line_hash[line] # Bold, italics
49
- string << line.original_line
50
- string << TERMINAL_END if @terminal
51
- end
52
- string
53
- end.join
54
- end
55
- end
56
- end
@@ -1,100 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "capture_code_context"
4
- require_relative "display_code_with_line_numbers"
5
-
6
- module SyntaxErrorSearch
7
- # Used for formatting invalid blocks
8
- class DisplayInvalidBlocks
9
- attr_reader :filename
10
-
11
- def initialize(code_lines: ,blocks:, io: $stderr, filename: nil, terminal: false, invalid_type: :unmatched_end)
12
- @terminal = terminal
13
- @filename = filename
14
- @io = io
15
-
16
- @blocks = Array(blocks)
17
-
18
- @invalid_lines = @blocks.map(&:lines).flatten
19
- @code_lines = code_lines
20
-
21
- @invalid_type = invalid_type
22
- end
23
-
24
- def call
25
- if @blocks.any? { |b| !b.hidden? }
26
- found_invalid_blocks
27
- else
28
- @io.puts "Syntax OK"
29
- end
30
- self
31
- end
32
-
33
- private def no_invalid_blocks
34
- @io.puts <<~EOM
35
- EOM
36
- end
37
-
38
- private def found_invalid_blocks
39
- case @invalid_type
40
- when :missing_end
41
- @io.puts <<~EOM
42
-
43
- SyntaxSearch: Missing `end` detected
44
-
45
- This code has a missing `end`. Ensure that all
46
- syntax keywords (`def`, `do`, etc.) have a matching `end`.
47
-
48
- EOM
49
- when :unmatched_end
50
- @io.puts <<~EOM
51
-
52
- SyntaxSearch: Unmatched `end` detected
53
-
54
- This code has an unmatched `end`. Ensure that all `end` lines
55
- in your code have a matching syntax keyword (`def`, `do`, etc.)
56
- and that you don't have any extra `end` lines.
57
-
58
- EOM
59
- end
60
-
61
- @io.puts("file: #{filename}") if filename
62
- @io.puts <<~EOM
63
- simplified:
64
-
65
- #{indent(code_block)}
66
- EOM
67
- end
68
-
69
- def indent(string, with: " ")
70
- string.each_line.map {|l| with + l }.join
71
- end
72
-
73
- def code_block
74
- string = String.new("")
75
- string << code_with_context
76
- string
77
- end
78
-
79
- def code_with_context
80
- lines = CaptureCodeContext.new(
81
- blocks: @blocks,
82
- code_lines: @code_lines
83
- ).call
84
-
85
- DisplayCodeWithLineNumbers.new(
86
- lines: lines,
87
- terminal: @terminal,
88
- highlight_lines: @invalid_lines,
89
- ).call
90
- end
91
-
92
- def code_with_lines
93
- DisplayCodeWithLineNumbers.new(
94
- lines: @code_lines.select(&:visible?),
95
- terminal: @terminal,
96
- highlight_lines: @invalid_lines,
97
- ).call
98
- end
99
- end
100
- end
@@ -1,7 +0,0 @@
1
- require_relative "../syntax_search"
2
-
3
- require_relative "auto.rb"
4
-
5
- SyntaxErrorSearch.send(:remove_const, :SEARCH_SOURCE_ON_ERROR_DEFAULT)
6
- SyntaxErrorSearch::SEARCH_SOURCE_ON_ERROR_DEFAULT = false
7
-
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SyntaxErrorSearch
4
- # Takes in a source, and returns blocks containing each heredoc
5
- class HeredocBlockParse
6
- private; attr_reader :code_lines, :lex; public
7
-
8
- def initialize(source:, code_lines: )
9
- @code_lines = code_lines
10
- @lex = LexAll.new(source: source)
11
- end
12
-
13
- def call
14
- blocks = []
15
- beginning = []
16
- @lex.each do |lex|
17
- case lex.type
18
- when :on_heredoc_beg
19
- beginning << lex.line
20
- when :on_heredoc_end
21
- start_index = beginning.pop - 1
22
- end_index = lex.line - 1
23
- blocks << CodeBlock.new(lines: code_lines[start_index..end_index])
24
- end
25
- end
26
-
27
- blocks
28
- end
29
- end
30
- end
@@ -1,58 +0,0 @@
1
- module SyntaxErrorSearch
2
- # Ripper.lex is not guaranteed to lex the entire source document
3
- #
4
- # lex = LexAll.new(source: source)
5
- # lex.each do |value|
6
- # puts value.line
7
- # end
8
- class LexAll
9
- include Enumerable
10
-
11
- def initialize(source: )
12
- @lex = Ripper.lex(source)
13
- lineno = @lex.last&.first&.first + 1
14
- source_lines = source.lines
15
- last_lineno = source_lines.count
16
-
17
- until lineno >= last_lineno
18
- lines = source_lines[lineno..-1]
19
-
20
- @lex.concat(Ripper.lex(lines.join, '-', lineno + 1))
21
- lineno = @lex.last&.first&.first + 1
22
- end
23
-
24
- @lex.map! {|(line, _), type, token| LexValue.new(line, _, type, token) }
25
- end
26
-
27
- def each
28
- return @lex.each unless block_given?
29
- @lex.each do |x|
30
- yield x
31
- end
32
- end
33
-
34
- def last
35
- @lex.last
36
- end
37
-
38
- # Value object for accessing lex values
39
- #
40
- # This lex:
41
- #
42
- # [1, 0], :on_ident, "describe", CMDARG
43
- #
44
- # Would translate into:
45
- #
46
- # lex.line # => 1
47
- # lex.type # => :on_indent
48
- # lex.token # => "describe"
49
- class LexValue
50
- attr_reader :line, :type, :token
51
- def initialize(line, _, type, token)
52
- @line = line
53
- @type = type
54
- @token = token
55
- end
56
- end
57
- end
58
- end
@@ -1,56 +0,0 @@
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
-