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,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
-