syntax_search 0.1.1 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/check_changelog.yml +13 -0
- data/CHANGELOG.md +24 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -5
- data/README.md +28 -15
- data/assets/syntax_search.gif +0 -0
- data/exe/syntax_search +1 -0
- data/lib/syntax_search.rb +23 -14
- data/lib/syntax_search/around_block_scan.rb +193 -0
- data/lib/syntax_search/block_expand.rb +74 -0
- data/lib/syntax_search/capture_code_context.rb +62 -0
- data/lib/syntax_search/code_block.rb +24 -165
- data/lib/syntax_search/code_frontier.rb +40 -201
- data/lib/syntax_search/code_line.rb +42 -1
- data/lib/syntax_search/code_search.rb +60 -20
- data/lib/syntax_search/display_code_with_line_numbers.rb +56 -0
- data/lib/syntax_search/display_invalid_blocks.rb +46 -45
- data/lib/syntax_search/heredoc_block_parse.rb +30 -0
- data/lib/syntax_search/lex_all.rb +58 -0
- data/lib/syntax_search/parse_blocks_from_indent_line.rb +56 -0
- data/lib/syntax_search/version.rb +1 -1
- data/lib/syntax_search/who_dis_syntax_error.rb +32 -0
- data/syntax_search.gemspec +0 -2
- metadata +13 -17
@@ -29,7 +29,7 @@ module SyntaxErrorSearch
|
|
29
29
|
# Marking a line as invisible also lets the overall program know
|
30
30
|
# that it should not check that area for syntax errors.
|
31
31
|
class CodeLine
|
32
|
-
attr_reader :line, :index, :indent
|
32
|
+
attr_reader :line, :index, :indent, :original_line
|
33
33
|
|
34
34
|
def initialize(line: , index:)
|
35
35
|
@original_line = line.freeze
|
@@ -39,6 +39,45 @@ module SyntaxErrorSearch
|
|
39
39
|
@indent = SpaceCount.indent(line)
|
40
40
|
@status = nil # valid, invalid, unknown
|
41
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
|
42
81
|
end
|
43
82
|
|
44
83
|
def mark_invalid
|
@@ -72,6 +111,8 @@ module SyntaxErrorSearch
|
|
72
111
|
index + 1
|
73
112
|
end
|
74
113
|
|
114
|
+
alias :number :line_number
|
115
|
+
|
75
116
|
def not_empty?
|
76
117
|
!empty?
|
77
118
|
end
|
@@ -3,15 +3,16 @@
|
|
3
3
|
module SyntaxErrorSearch
|
4
4
|
# Searches code for a syntax error
|
5
5
|
#
|
6
|
-
# The bulk of the heavy lifting is done
|
6
|
+
# The bulk of the heavy lifting is done in:
|
7
7
|
#
|
8
|
-
#
|
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
|
9
11
|
#
|
10
12
|
# ## Syntax error detection
|
11
13
|
#
|
12
14
|
# When the frontier holds the syntax error, we can stop searching
|
13
15
|
#
|
14
|
-
#
|
15
16
|
# search = CodeSearch.new(<<~EOM)
|
16
17
|
# def dog
|
17
18
|
# def lol
|
@@ -23,42 +24,51 @@ module SyntaxErrorSearch
|
|
23
24
|
# search.invalid_blocks.map(&:to_s) # =>
|
24
25
|
# # => ["def lol\n"]
|
25
26
|
#
|
26
|
-
#
|
27
27
|
class CodeSearch
|
28
28
|
private; attr_reader :frontier; public
|
29
29
|
public; attr_reader :invalid_blocks, :record_dir, :code_lines
|
30
30
|
|
31
|
-
def initialize(
|
31
|
+
def initialize(source, record_dir: ENV["SYNTAX_SEARCH_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
|
32
|
+
@source = source
|
32
33
|
if record_dir
|
33
34
|
@time = Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')
|
34
35
|
@record_dir = Pathname(record_dir).join(@time).tap {|p| p.mkpath }
|
35
36
|
@write_count = 0
|
36
37
|
end
|
37
|
-
@code_lines =
|
38
|
+
@code_lines = source.lines.map.with_index do |line, i|
|
38
39
|
CodeLine.new(line: line, index: i)
|
39
40
|
end
|
40
41
|
@frontier = CodeFrontier.new(code_lines: @code_lines)
|
41
42
|
@invalid_blocks = []
|
42
43
|
@name_tick = Hash.new {|hash, k| hash[k] = 0 }
|
43
44
|
@tick = 0
|
44
|
-
@
|
45
|
+
@block_expand = BlockExpand.new(code_lines: code_lines)
|
46
|
+
@parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
|
45
47
|
end
|
46
48
|
|
49
|
+
# Used for debugging
|
47
50
|
def record(block:, name: "record")
|
48
51
|
return if !@record_dir
|
49
52
|
@name_tick[name] += 1
|
50
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
|
51
61
|
@record_dir.join(filename).open(mode: "a") do |f|
|
52
62
|
display = DisplayInvalidBlocks.new(
|
53
63
|
blocks: block,
|
54
|
-
terminal: false
|
64
|
+
terminal: false,
|
65
|
+
code_lines: @code_lines,
|
55
66
|
)
|
56
67
|
f.write(display.indent display.code_with_lines)
|
57
68
|
end
|
58
69
|
end
|
59
70
|
|
60
|
-
def
|
61
|
-
frontier.register(block)
|
71
|
+
def push(block, name: )
|
62
72
|
record(block: block, name: name)
|
63
73
|
|
64
74
|
if block.valid?
|
@@ -69,33 +79,63 @@ module SyntaxErrorSearch
|
|
69
79
|
end
|
70
80
|
end
|
71
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
|
72
92
|
def add_invalid_blocks
|
73
93
|
max_indent = frontier.next_indent_line&.indent
|
74
94
|
|
75
95
|
while (line = frontier.next_indent_line) && (line.indent == max_indent)
|
76
|
-
neighbors = @scan.neighbors_from_top(frontier.next_indent_line)
|
77
96
|
|
78
|
-
@
|
97
|
+
@parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|
|
79
98
|
record(block: block, name: "add")
|
80
|
-
if block.valid?
|
81
|
-
block.lines.each(&:mark_invisible)
|
82
|
-
end
|
83
|
-
end
|
84
99
|
|
85
|
-
|
86
|
-
|
100
|
+
block.mark_invisible if block.valid?
|
101
|
+
push(block, name: "add")
|
102
|
+
end
|
87
103
|
end
|
88
104
|
end
|
89
105
|
|
106
|
+
# Given an already existing block in the frontier, expand it to see
|
107
|
+
# if it contains our invalid syntax
|
90
108
|
def expand_invalid_block
|
91
109
|
block = frontier.pop
|
92
110
|
return unless block
|
93
111
|
|
94
|
-
block
|
95
|
-
|
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")
|
96
133
|
end
|
97
134
|
|
135
|
+
# Main search loop
|
98
136
|
def call
|
137
|
+
sweep_heredocs
|
138
|
+
sweep_comments
|
99
139
|
until frontier.holds_all_syntax_errors?
|
100
140
|
@tick += 1
|
101
141
|
|
@@ -0,0 +1,56 @@
|
|
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,25 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "capture_code_context"
|
4
|
+
require_relative "display_code_with_line_numbers"
|
5
|
+
|
3
6
|
module SyntaxErrorSearch
|
4
7
|
# Used for formatting invalid blocks
|
5
8
|
class DisplayInvalidBlocks
|
6
9
|
attr_reader :filename
|
7
10
|
|
8
|
-
def initialize(blocks:, io: $stderr, filename: nil, terminal: false)
|
11
|
+
def initialize(code_lines: ,blocks:, io: $stderr, filename: nil, terminal: false, invalid_type: :unmatched_end)
|
9
12
|
@terminal = terminal
|
10
13
|
@filename = filename
|
11
14
|
@io = io
|
12
15
|
|
13
16
|
@blocks = Array(blocks)
|
14
|
-
@lines = @blocks.map(&:lines).flatten
|
15
|
-
@code_lines = @blocks.first&.code_lines || []
|
16
|
-
@digit_count = @code_lines.last&.line_number.to_s.length
|
17
17
|
|
18
|
-
@
|
18
|
+
@invalid_lines = @blocks.map(&:lines).flatten
|
19
|
+
@code_lines = code_lines
|
20
|
+
|
21
|
+
@invalid_type = invalid_type
|
19
22
|
end
|
20
23
|
|
21
24
|
def call
|
22
|
-
if @blocks.any?
|
25
|
+
if @blocks.any? { |b| !b.hidden? }
|
23
26
|
found_invalid_blocks
|
24
27
|
else
|
25
28
|
@io.puts "Syntax OK"
|
@@ -33,15 +36,28 @@ module SyntaxErrorSearch
|
|
33
36
|
end
|
34
37
|
|
35
38
|
private def found_invalid_blocks
|
36
|
-
@
|
39
|
+
case @invalid_type
|
40
|
+
when :missing_end
|
41
|
+
@io.puts <<~EOM
|
37
42
|
|
38
|
-
|
43
|
+
SyntaxSearch: Missing `end` detected
|
39
44
|
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
43
60
|
|
44
|
-
EOM
|
45
61
|
@io.puts("file: #{filename}") if filename
|
46
62
|
@io.puts <<~EOM
|
47
63
|
simplified:
|
@@ -50,50 +66,35 @@ module SyntaxErrorSearch
|
|
50
66
|
EOM
|
51
67
|
end
|
52
68
|
|
53
|
-
def indent(string, with: "
|
69
|
+
def indent(string, with: " ")
|
54
70
|
string.each_line.map {|l| with + l }.join
|
55
71
|
end
|
56
72
|
|
57
73
|
def code_block
|
58
74
|
string = String.new("")
|
59
|
-
string <<
|
60
|
-
# string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename
|
61
|
-
string << code_with_lines
|
62
|
-
string << "```\n"
|
75
|
+
string << code_with_context
|
63
76
|
string
|
64
77
|
end
|
65
78
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
72
90
|
end
|
73
91
|
|
74
92
|
def code_with_lines
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
string << "❯ "
|
81
|
-
else
|
82
|
-
string << " "
|
83
|
-
end
|
84
|
-
|
85
|
-
number = line.line_number.to_s.rjust(@digit_count)
|
86
|
-
string << number.to_s
|
87
|
-
if line.empty?
|
88
|
-
string << line.to_s
|
89
|
-
else
|
90
|
-
string << " "
|
91
|
-
string << terminal_highlight if @terminal && @invalid_line_hash[line] # Bold, italics
|
92
|
-
string << line.to_s
|
93
|
-
string << terminal_end if @terminal
|
94
|
-
end
|
95
|
-
string
|
96
|
-
end.join
|
93
|
+
DisplayCodeWithLineNumbers.new(
|
94
|
+
lines: @code_lines.select(&:visible?),
|
95
|
+
terminal: @terminal,
|
96
|
+
highlight_lines: @invalid_lines,
|
97
|
+
).call
|
97
98
|
end
|
98
99
|
end
|
99
100
|
end
|
@@ -0,0 +1,30 @@
|
|
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
|
@@ -0,0 +1,58 @@
|
|
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
|