syntax_suggest 1.0.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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