syntax_suggest 1.0.4 → 2.0.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.
@@ -61,11 +61,14 @@ module SyntaxSuggest
61
61
  # they can expand to capture more code up and down). It does this conservatively
62
62
  # as there's no undo (currently).
63
63
  def expand_indent(block)
64
- AroundBlockScan.new(code_lines: @code_lines, block: block)
64
+ now = AroundBlockScan.new(code_lines: @code_lines, block: block)
65
65
  .force_add_hidden
66
66
  .stop_after_kw
67
67
  .scan_adjacent_indent
68
- .code_block
68
+
69
+ now.lookahead_balance_one_line
70
+
71
+ now.code_block
69
72
  end
70
73
 
71
74
  # A neighbor is code that is at or above the current indent line.
@@ -125,17 +128,20 @@ module SyntaxSuggest
125
128
  #
126
129
  # We try to resolve this edge case with `lookahead_balance_one_line` below.
127
130
  def expand_neighbors(block)
128
- neighbors = AroundBlockScan.new(code_lines: @code_lines, block: block)
131
+ now = AroundBlockScan.new(code_lines: @code_lines, block: block)
132
+
133
+ # Initial scan
134
+ now
129
135
  .force_add_hidden
130
136
  .stop_after_kw
131
137
  .scan_neighbors_not_empty
132
138
 
133
139
  # Slurp up empties
134
- with_empties = neighbors
140
+ now
135
141
  .scan_while { |line| line.empty? }
136
142
 
137
143
  # If next line is kw and it will balance us, take it
138
- expanded_lines = with_empties
144
+ expanded_lines = now
139
145
  .lookahead_balance_one_line
140
146
  .lines
141
147
 
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxSuggest
4
+ module Capture
5
+ # Shows surrounding kw/end pairs
6
+ #
7
+ # The purpose of showing these extra pairs is due to cases
8
+ # of ambiguity when only one visible line is matched.
9
+ #
10
+ # For example:
11
+ #
12
+ # 1 class Dog
13
+ # 2 def bark
14
+ # 4 def eat
15
+ # 5 end
16
+ # 6 end
17
+ #
18
+ # In this case either line 2 could be missing an `end` or
19
+ # line 4 was an extra line added by mistake (it happens).
20
+ #
21
+ # When we detect the above problem it shows the issue
22
+ # as only being on line 2
23
+ #
24
+ # 2 def bark
25
+ #
26
+ # Showing "neighbor" keyword pairs gives extra context:
27
+ #
28
+ # 2 def bark
29
+ # 4 def eat
30
+ # 5 end
31
+ #
32
+ #
33
+ # Example:
34
+ #
35
+ # lines = BeforeAfterKeywordEnds.new(
36
+ # block: block,
37
+ # code_lines: code_lines
38
+ # ).call()
39
+ #
40
+ class BeforeAfterKeywordEnds
41
+ def initialize(code_lines:, block:)
42
+ @scanner = ScanHistory.new(code_lines: code_lines, block: block)
43
+ @original_indent = block.current_indent
44
+ end
45
+
46
+ def call
47
+ lines = []
48
+
49
+ @scanner.scan(
50
+ up: ->(line, kw_count, end_count) {
51
+ next true if line.empty?
52
+ break if line.indent < @original_indent
53
+ next true if line.indent != @original_indent
54
+
55
+ # If we're going up and have one complete kw/end pair, stop
56
+ if kw_count != 0 && kw_count == end_count
57
+ lines << line
58
+ break
59
+ end
60
+
61
+ lines << line if line.is_kw? || line.is_end?
62
+ true
63
+ },
64
+ down: ->(line, kw_count, end_count) {
65
+ next true if line.empty?
66
+ break if line.indent < @original_indent
67
+ next true if line.indent != @original_indent
68
+
69
+ # if we're going down and have one complete kw/end pair,stop
70
+ if kw_count != 0 && kw_count == end_count
71
+ lines << line
72
+ break
73
+ end
74
+
75
+ lines << line if line.is_kw? || line.is_end?
76
+ true
77
+ }
78
+ )
79
+ @scanner.stash_changes
80
+
81
+ lines
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxSuggest
4
+ module Capture
5
+ # Shows the context around code provided by "falling" indentation
6
+ #
7
+ # If this is the original code lines:
8
+ #
9
+ # class OH
10
+ # def hello
11
+ # it "foo" do
12
+ # end
13
+ # end
14
+ #
15
+ # And this is the line that is captured
16
+ #
17
+ # it "foo" do
18
+ #
19
+ # It will yield its surrounding context:
20
+ #
21
+ # class OH
22
+ # def hello
23
+ # end
24
+ # end
25
+ #
26
+ # Example:
27
+ #
28
+ # FallingIndentLines.new(
29
+ # block: block,
30
+ # code_lines: @code_lines
31
+ # ).call do |line|
32
+ # @lines_to_output << line
33
+ # end
34
+ #
35
+ class FallingIndentLines
36
+ def initialize(code_lines:, block:)
37
+ @lines = nil
38
+ @scanner = ScanHistory.new(code_lines: code_lines, block: block)
39
+ @original_indent = block.current_indent
40
+ end
41
+
42
+ def call(&yieldable)
43
+ last_indent_up = @original_indent
44
+ last_indent_down = @original_indent
45
+
46
+ @scanner.commit_if_changed
47
+ @scanner.scan(
48
+ up: ->(line, _, _) {
49
+ next true if line.empty?
50
+
51
+ if line.indent < last_indent_up
52
+ yieldable.call(line)
53
+ last_indent_up = line.indent
54
+ end
55
+ true
56
+ },
57
+ down: ->(line, _, _) {
58
+ next true if line.empty?
59
+
60
+ if line.indent < last_indent_down
61
+ yieldable.call(line)
62
+ last_indent_down = line.indent
63
+ end
64
+ true
65
+ }
66
+ )
67
+ @scanner.stash_changes
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module SyntaxSuggest
4
+ module Capture
5
+ end
6
+ end
7
+
8
+ require_relative "capture/falling_indent_lines"
9
+ require_relative "capture/before_after_keyword_ends"
10
+
3
11
  module SyntaxSuggest
4
12
  # Turns a "invalid block(s)" into useful context
5
13
  #
@@ -55,6 +63,10 @@ module SyntaxSuggest
55
63
  capture_falling_indent(block)
56
64
  end
57
65
 
66
+ sorted_lines
67
+ end
68
+
69
+ def sorted_lines
58
70
  @lines_to_output.select!(&:not_empty?)
59
71
  @lines_to_output.uniq!
60
72
  @lines_to_output.sort!
@@ -77,10 +89,10 @@ module SyntaxSuggest
77
89
  # end
78
90
  #
79
91
  def capture_falling_indent(block)
80
- AroundBlockScan.new(
92
+ Capture::FallingIndentLines.new(
81
93
  block: block,
82
94
  code_lines: @code_lines
83
- ).on_falling_indent do |line|
95
+ ).call do |line|
84
96
  @lines_to_output << line
85
97
  end
86
98
  end
@@ -115,9 +127,10 @@ module SyntaxSuggest
115
127
  def capture_before_after_kws(block)
116
128
  return unless block.visible_lines.count == 1
117
129
 
118
- around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
119
- .start_at_next_line
120
- .capture_neighbor_context
130
+ around_lines = Capture::BeforeAfterKeywordEnds.new(
131
+ code_lines: @code_lines,
132
+ block: block
133
+ ).call
121
134
 
122
135
  around_lines -= block.lines
123
136
 
@@ -47,9 +47,9 @@ module SyntaxSuggest
47
47
  # ## Heredocs
48
48
  #
49
49
  # A heredoc is an way of defining a multi-line string. They can cause many
50
- # problems. If left as a single line, Ripper would try to parse the contents
50
+ # problems. If left as a single line, the parser would try to parse the contents
51
51
  # as ruby code rather than as a string. Even without this problem, we still
52
- # hit an issue with indentation
52
+ # hit an issue with indentation:
53
53
  #
54
54
  # 1 foo = <<~HEREDOC
55
55
  # 2 "Be yourself; everyone else is already taken.""
@@ -224,7 +224,7 @@ module SyntaxSuggest
224
224
  #
225
225
  def join_consecutive!
226
226
  consecutive_groups = @document.select(&:ignore_newline_not_beg?).map do |code_line|
227
- take_while_including(code_line.index..-1) do |line|
227
+ take_while_including(code_line.index..) do |line|
228
228
  line.ignore_newline_not_beg?
229
229
  end
230
230
  end
@@ -245,7 +245,7 @@ module SyntaxSuggest
245
245
  # expect(lines[1].to_s).to eq("")
246
246
  def join_trailing_slash!
247
247
  trailing_groups = @document.select(&:trailing_slash?).map do |code_line|
248
- take_while_including(code_line.index..-1) { |x| x.trailing_slash? }
248
+ take_while_including(code_line.index..) { |x| x.trailing_slash? }
249
249
  end
250
250
  join_groups(trailing_groups)
251
251
  self
@@ -279,7 +279,7 @@ module SyntaxSuggest
279
279
  )
280
280
 
281
281
  # Hide the rest of the lines
282
- lines[1..-1].each do |line|
282
+ lines[1..].each do |line|
283
283
  # The above lines already have newlines in them, if add more
284
284
  # then there will be double newline, use an empty line instead
285
285
  @document[line.index] = CodeLine.new(line: "", index: line.index, lex: [])
@@ -293,7 +293,7 @@ module SyntaxSuggest
293
293
  # Like `take_while` except when it stops
294
294
  # iterating, it also returns the line
295
295
  # that caused it to stop
296
- def take_while_including(range = 0..-1)
296
+ def take_while_including(range = 0..)
297
297
  take_next_and_stop = false
298
298
  @document[range].take_while do |line|
299
299
  next if take_next_and_stop
@@ -81,7 +81,7 @@ module SyntaxSuggest
81
81
  # lines then the result cannot be invalid
82
82
  #
83
83
  # That means there's no reason to re-check all
84
- # lines with ripper (which is expensive).
84
+ # lines with the parser (which is expensive).
85
85
  # Benchmark in commit message
86
86
  @valid = if lines.all? { |l| l.hidden? || l.empty? }
87
87
  true
@@ -117,7 +117,7 @@ module SyntaxSuggest
117
117
 
118
118
  if ENV["SYNTAX_SUGGEST_DEBUG"]
119
119
  puts "```"
120
- puts @queue.peek.to_s
120
+ puts @queue.peek
121
121
  puts "```"
122
122
  puts " @frontier indent: #{frontier_indent}"
123
123
  puts " @unvisited indent: #{unvisited_indent}"
@@ -180,12 +180,19 @@ module SyntaxSuggest
180
180
  # EOM
181
181
  # expect(lines.first.trailing_slash?).to eq(true)
182
182
  #
183
- def trailing_slash?
184
- last = @lex.last
185
- return false unless last
186
- return false unless last.type == :on_sp
183
+ if SyntaxSuggest.use_prism_parser?
184
+ def trailing_slash?
185
+ last = @lex.last
186
+ last&.type == :on_tstring_end
187
+ end
188
+ else
189
+ def trailing_slash?
190
+ last = @lex.last
191
+ return false unless last
192
+ return false unless last.type == :on_sp
187
193
 
188
- last.token == TRAILING_SLASH
194
+ last.token == TRAILING_SLASH
195
+ end
189
196
  end
190
197
 
191
198
  # Endless method detection
@@ -43,7 +43,7 @@ module SyntaxSuggest
43
43
 
44
44
  def initialize(source, record_dir: DEFAULT_VALUE)
45
45
  record_dir = if record_dir == DEFAULT_VALUE
46
- ENV["SYNTAX_SUGGEST_RECORD_DIR"] || ENV["SYNTAX_SUGGEST_DEBUG"] ? "tmp" : nil
46
+ (ENV["SYNTAX_SUGGEST_RECORD_DIR"] || ENV["SYNTAX_SUGGEST_DEBUG"]) ? "tmp" : nil
47
47
  else
48
48
  record_dir
49
49
  end
@@ -73,7 +73,7 @@ module SyntaxSuggest
73
73
  if ENV["SYNTAX_SUGGEST_DEBUG"]
74
74
  puts "\n\n==== #{filename} ===="
75
75
  puts "\n```#{block.starts_at}..#{block.ends_at}"
76
- puts block.to_s
76
+ puts block
77
77
  puts "```"
78
78
  puts " block indent: #{block.current_indent}"
79
79
  end
@@ -21,7 +21,7 @@ if SyntaxError.method_defined?(:detailed_message)
21
21
  attr_reader :string
22
22
  end
23
23
 
24
- # SyntaxSuggest.record_dir [Private]
24
+ # SyntaxSuggest.module_for_detailed_message [Private]
25
25
  #
26
26
  # Used to monkeypatch SyntaxError via Module.prepend
27
27
  def self.module_for_detailed_message
@@ -14,7 +14,7 @@ module SyntaxSuggest
14
14
  @filename = filename
15
15
  @code_lines = code_lines
16
16
 
17
- @terminal = terminal == DEFAULT_VALUE ? io.isatty : terminal
17
+ @terminal = (terminal == DEFAULT_VALUE) ? io.isatty : terminal
18
18
  end
19
19
 
20
20
  def document_ok?
@@ -2,7 +2,21 @@
2
2
 
3
3
  require_relative "left_right_lex_count"
4
4
 
5
+ if !SyntaxSuggest.use_prism_parser?
6
+ require_relative "ripper_errors"
7
+ end
8
+
5
9
  module SyntaxSuggest
10
+ class GetParseErrors
11
+ def self.errors(source)
12
+ if SyntaxSuggest.use_prism_parser?
13
+ Prism.parse(source).errors.map(&:message)
14
+ else
15
+ RipperErrors.new(source).call.errors
16
+ end
17
+ end
18
+ end
19
+
6
20
  # Explains syntax errors based on their source
7
21
  #
8
22
  # example:
@@ -15,8 +29,8 @@ module SyntaxSuggest
15
29
  # # => "Unmatched keyword, missing `end' ?"
16
30
  #
17
31
  # When the error cannot be determined by lexical counting
18
- # then ripper is run against the input and the raw ripper
19
- # errors returned.
32
+ # then the parser is run against the input and the raw
33
+ # errors are returned.
20
34
  #
21
35
  # Example:
22
36
  #
@@ -91,10 +105,10 @@ module SyntaxSuggest
91
105
  # Returns an array of syntax error messages
92
106
  #
93
107
  # If no missing pairs are found it falls back
94
- # on the original ripper error messages
108
+ # on the original error messages
95
109
  def errors
96
110
  if missing.empty?
97
- return RipperErrors.new(@code_lines.map(&:original).join).call.errors
111
+ return GetParseErrors.errors(@code_lines.map(&:original).join).uniq
98
112
  end
99
113
 
100
114
  missing.map { |miss| why(miss) }
@@ -3,34 +3,53 @@
3
3
  module SyntaxSuggest
4
4
  # Ripper.lex is not guaranteed to lex the entire source document
5
5
  #
6
- # lex = LexAll.new(source: source)
7
- # lex.each do |value|
8
- # puts value.line
9
- # end
6
+ # This class guarantees the whole document is lex-ed by iteratively
7
+ # lexing the document where ripper stopped.
8
+ #
9
+ # Prism likely doesn't have the same problem. Once ripper support is removed
10
+ # we can likely reduce the complexity here if not remove the whole concept.
11
+ #
12
+ # Example usage:
13
+ #
14
+ # lex = LexAll.new(source: source)
15
+ # lex.each do |value|
16
+ # puts value.line
17
+ # end
10
18
  class LexAll
11
19
  include Enumerable
12
20
 
13
21
  def initialize(source:, source_lines: nil)
14
- @lex = Ripper::Lexer.new(source, "-", 1).parse.sort_by(&:pos)
15
- lineno = @lex.last.pos.first + 1
22
+ @lex = self.class.lex(source, 1)
23
+ lineno = @lex.last[0][0] + 1
16
24
  source_lines ||= source.lines
17
25
  last_lineno = source_lines.length
18
26
 
19
27
  until lineno >= last_lineno
20
- lines = source_lines[lineno..-1]
28
+ lines = source_lines[lineno..]
21
29
 
22
30
  @lex.concat(
23
- Ripper::Lexer.new(lines.join, "-", lineno + 1).parse.sort_by(&:pos)
31
+ self.class.lex(lines.join, lineno + 1)
24
32
  )
25
- lineno = @lex.last.pos.first + 1
33
+
34
+ lineno = @lex.last[0].first + 1
26
35
  end
27
36
 
28
37
  last_lex = nil
29
38
  @lex.map! { |elem|
30
- last_lex = LexValue.new(elem.pos.first, elem.event, elem.tok, elem.state, last_lex)
39
+ last_lex = LexValue.new(elem[0].first, elem[1], elem[2], elem[3], last_lex)
31
40
  }
32
41
  end
33
42
 
43
+ if SyntaxSuggest.use_prism_parser?
44
+ def self.lex(source, line_number)
45
+ Prism.lex_compat(source, line: line_number).value.sort_by { |values| values[0] }
46
+ end
47
+ else
48
+ def self.lex(source, line_number)
49
+ Ripper::Lexer.new(source, "-", line_number).parse.sort_by(&:pos)
50
+ end
51
+ end
52
+
34
53
  def to_a
35
54
  @lex
36
55
  end
@@ -13,7 +13,7 @@ module SyntaxSuggest
13
13
  # # => "/tmp/scratch.rb"
14
14
  #
15
15
  class PathnameFromMessage
16
- EVAL_RE = /^\(eval\):\d+/
16
+ EVAL_RE = /^\(eval.*\):\d+/
17
17
  STREAMING_RE = /^-:\d+/
18
18
  attr_reader :name
19
19
 
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxSuggest
4
- # Capture parse errors from ripper
4
+ # Capture parse errors from Ripper
5
+ #
6
+ # Prism returns the errors with their messages, but Ripper
7
+ # does not. To get them we must make a custom subclass.
5
8
  #
6
9
  # Example:
7
10
  #
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxSuggest
4
+ # Scans up/down from the given block
5
+ #
6
+ # You can try out a change, stash it, or commit it to save for later
7
+ #
8
+ # Example:
9
+ #
10
+ # scanner = ScanHistory.new(code_lines: code_lines, block: block)
11
+ # scanner.scan(
12
+ # up: ->(_, _, _) { true },
13
+ # down: ->(_, _, _) { true }
14
+ # )
15
+ # scanner.changed? # => true
16
+ # expect(scanner.lines).to eq(code_lines)
17
+ #
18
+ # scanner.stash_changes
19
+ #
20
+ # expect(scanner.lines).to_not eq(code_lines)
21
+ class ScanHistory
22
+ attr_reader :before_index, :after_index
23
+
24
+ def initialize(code_lines:, block:)
25
+ @code_lines = code_lines
26
+ @history = [block]
27
+ refresh_index
28
+ end
29
+
30
+ def commit_if_changed
31
+ if changed?
32
+ @history << CodeBlock.new(lines: @code_lines[before_index..after_index])
33
+ end
34
+
35
+ self
36
+ end
37
+
38
+ # Discards any changes that have not been committed
39
+ def stash_changes
40
+ refresh_index
41
+ self
42
+ end
43
+
44
+ # Discard changes that have not been committed and revert the last commit
45
+ #
46
+ # Cannot revert the first commit
47
+ def revert_last_commit
48
+ if @history.length > 1
49
+ @history.pop
50
+ refresh_index
51
+ end
52
+
53
+ self
54
+ end
55
+
56
+ def changed?
57
+ @before_index != current.lines.first.index ||
58
+ @after_index != current.lines.last.index
59
+ end
60
+
61
+ # Iterates up and down
62
+ #
63
+ # Returns line, kw_count, end_count for each iteration
64
+ def scan(up:, down:)
65
+ kw_count = 0
66
+ end_count = 0
67
+
68
+ up_index = before_lines.reverse_each.take_while do |line|
69
+ kw_count += 1 if line.is_kw?
70
+ end_count += 1 if line.is_end?
71
+ up.call(line, kw_count, end_count)
72
+ end.last&.index
73
+
74
+ kw_count = 0
75
+ end_count = 0
76
+
77
+ down_index = after_lines.each.take_while do |line|
78
+ kw_count += 1 if line.is_kw?
79
+ end_count += 1 if line.is_end?
80
+ down.call(line, kw_count, end_count)
81
+ end.last&.index
82
+
83
+ @before_index = if up_index && up_index < @before_index
84
+ up_index
85
+ else
86
+ @before_index
87
+ end
88
+
89
+ @after_index = if down_index && down_index > @after_index
90
+ down_index
91
+ else
92
+ @after_index
93
+ end
94
+
95
+ self
96
+ end
97
+
98
+ def next_up
99
+ return nil if @before_index <= 0
100
+
101
+ @code_lines[@before_index - 1]
102
+ end
103
+
104
+ def next_down
105
+ return nil if @after_index >= @code_lines.length
106
+
107
+ @code_lines[@after_index + 1]
108
+ end
109
+
110
+ def lines
111
+ @code_lines[@before_index..@after_index]
112
+ end
113
+
114
+ private def before_lines
115
+ @code_lines[0...@before_index] || []
116
+ end
117
+
118
+ # Returns an array of all the CodeLines that exist after
119
+ # the currently scanned block
120
+ private def after_lines
121
+ @code_lines[@after_index.next..] || []
122
+ end
123
+
124
+ private def current
125
+ @history.last
126
+ end
127
+
128
+ private def refresh_index
129
+ @before_index = current.lines.first.index
130
+ @after_index = current.lines.last.index
131
+ self
132
+ end
133
+ end
134
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxSuggest
4
- VERSION = "1.0.4"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.description = 'When you get an "unexpected end" in your syntax this gem helps you find it'
17
17
  spec.homepage = "https://github.com/ruby/syntax_suggest.git"
18
18
  spec.license = "MIT"
19
- spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
19
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
20
20
 
21
21
  spec.metadata["homepage_uri"] = spec.homepage
22
22
  spec.metadata["source_code_uri"] = "https://github.com/ruby/syntax_suggest.git"
@@ -27,6 +27,6 @@ Gem::Specification.new do |spec|
27
27
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|assets)/}) }
28
28
  end
29
29
  spec.bindir = "exe"
30
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.executables = ["syntax_suggest"]
31
31
  spec.require_paths = ["lib"]
32
32
  end