syntax_suggest 2.0.3 → 3.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +4 -20
- data/.github/workflows/sync-ruby.yml +1 -1
- data/.standard.yml +1 -1
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -0
- data/lib/syntax_suggest/api.rb +7 -40
- data/lib/syntax_suggest/clean_document.rb +11 -94
- data/lib/syntax_suggest/code_line.rb +55 -76
- data/lib/syntax_suggest/core_ext.rb +39 -88
- data/lib/syntax_suggest/explain_syntax.rb +5 -13
- data/lib/syntax_suggest/{left_right_lex_count.rb → left_right_token_count.rb} +21 -27
- data/lib/syntax_suggest/token.rb +49 -0
- data/lib/syntax_suggest/version.rb +1 -1
- data/lib/syntax_suggest/visitor.rb +80 -0
- data/syntax_suggest.gemspec +1 -1
- metadata +5 -6
- data/lib/syntax_suggest/lex_all.rb +0 -74
- data/lib/syntax_suggest/lex_value.rb +0 -70
- data/lib/syntax_suggest/ripper_errors.rb +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af5147af49a48cc1639c03e5b4f89d9bd2d841981548fef41477786fc1955fa4
|
|
4
|
+
data.tar.gz: 21bbc4796ad0444e965258c645637f1faf853392d27a54a4a28096db92f44257
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a48e4e6788b73a404d67cafd519713cce379ad1e84dc270e81f7047ea523d84b75d91420f3915614a68233eb9c4c07ab237e090741d2d7ffd4326001d6e4fcd7
|
|
7
|
+
data.tar.gz: e29a44a49a2185bec367895efda2ee6bc978adc24930711e9ddff7a9e4881519519ed2890d4ba9828608e30bf9b8e196f4b45bac42d78f1be0c8b3f745b87f02
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -24,6 +24,7 @@ jobs:
|
|
|
24
24
|
uses: ruby/actions/.github/workflows/ruby_versions.yml@master
|
|
25
25
|
with:
|
|
26
26
|
engine: cruby
|
|
27
|
+
min_version: 3.3
|
|
27
28
|
|
|
28
29
|
test:
|
|
29
30
|
needs: ruby-versions
|
|
@@ -33,7 +34,9 @@ jobs:
|
|
|
33
34
|
matrix:
|
|
34
35
|
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
|
|
35
36
|
prism_version:
|
|
36
|
-
|
|
37
|
+
# See https://stdgems.org/prism for which ruby version shipped with which prism version
|
|
38
|
+
- 0.19.0
|
|
39
|
+
- 1.2.0
|
|
37
40
|
- 1.8.0
|
|
38
41
|
- head
|
|
39
42
|
env:
|
|
@@ -52,22 +55,3 @@ jobs:
|
|
|
52
55
|
- name: test
|
|
53
56
|
run: bin/rake test
|
|
54
57
|
continue-on-error: ${{ matrix.ruby == 'head' }}
|
|
55
|
-
|
|
56
|
-
test-disable-prism:
|
|
57
|
-
needs: ruby-versions
|
|
58
|
-
runs-on: ubuntu-latest
|
|
59
|
-
strategy:
|
|
60
|
-
fail-fast: false
|
|
61
|
-
matrix:
|
|
62
|
-
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
|
|
63
|
-
steps:
|
|
64
|
-
- name: Checkout code
|
|
65
|
-
uses: actions/checkout@v6
|
|
66
|
-
- name: Set up Ruby
|
|
67
|
-
uses: ruby/setup-ruby@v1
|
|
68
|
-
with:
|
|
69
|
-
ruby-version: ${{ matrix.ruby }}
|
|
70
|
-
bundler-cache: true
|
|
71
|
-
- name: test
|
|
72
|
-
run: SYNTAX_SUGGEST_DISABLE_PRISM=1 bin/rake test
|
|
73
|
-
continue-on-error: ${{ matrix.ruby == 'head' }}
|
data/.standard.yml
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby_version: 3.
|
|
1
|
+
ruby_version: 3.3.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
## HEAD (unreleased)
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
- Changed: Minimum supported Ruby version is now 3.3. (https://github.com/ruby/syntax_suggest/pull/246)
|
|
6
|
+
- Changed: Use native prism to analyse. (https://github.com/ruby/syntax_suggest/pull/251)
|
|
7
|
+
|
|
3
8
|
## 2.0.3
|
|
4
9
|
|
|
5
10
|
- Fix: Correctly identify trailing slashes when using Prism > 1.8.0. (https://github.com/ruby/syntax_suggest/pull/243)
|
|
11
|
+
- Fix: Correctly handle `%I` delimiters. (https://github.com/ruby/syntax_suggest/pull/249)
|
|
6
12
|
- Internal: Add tests to multiple versions of prism
|
|
7
13
|
|
|
8
14
|
## 2.0.2
|
data/Gemfile
CHANGED
data/lib/syntax_suggest/api.rb
CHANGED
|
@@ -7,25 +7,8 @@ require "stringio"
|
|
|
7
7
|
require "pathname"
|
|
8
8
|
require "timeout"
|
|
9
9
|
|
|
10
|
-
# We need Ripper loaded for `Prism.lex_compat` even if we're using Prism
|
|
11
|
-
# for lexing and parsing
|
|
12
|
-
require "ripper"
|
|
13
|
-
|
|
14
10
|
# Prism is the new parser, replacing Ripper
|
|
15
|
-
|
|
16
|
-
# We need to "dual boot" both for now because syntax_suggest
|
|
17
|
-
# supports older rubies that do not ship with syntax suggest.
|
|
18
|
-
#
|
|
19
|
-
# We also need the ability to control loading of this library
|
|
20
|
-
# so we can test that both modes work correctly in CI.
|
|
21
|
-
if (value = ENV["SYNTAX_SUGGEST_DISABLE_PRISM"])
|
|
22
|
-
warn "Skipping loading prism due to SYNTAX_SUGGEST_DISABLE_PRISM=#{value}"
|
|
23
|
-
else
|
|
24
|
-
begin
|
|
25
|
-
require "prism"
|
|
26
|
-
rescue LoadError
|
|
27
|
-
end
|
|
28
|
-
end
|
|
11
|
+
require "prism"
|
|
29
12
|
|
|
30
13
|
module SyntaxSuggest
|
|
31
14
|
# Used to indicate a default value that cannot
|
|
@@ -35,14 +18,6 @@ module SyntaxSuggest
|
|
|
35
18
|
class Error < StandardError; end
|
|
36
19
|
TIMEOUT_DEFAULT = ENV.fetch("SYNTAX_SUGGEST_TIMEOUT", 1).to_i
|
|
37
20
|
|
|
38
|
-
# SyntaxSuggest.use_prism_parser? [Private]
|
|
39
|
-
#
|
|
40
|
-
# Tells us if the prism parser is available for use
|
|
41
|
-
# or if we should fallback to `Ripper`
|
|
42
|
-
def self.use_prism_parser?
|
|
43
|
-
defined?(Prism)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
21
|
# SyntaxSuggest.handle_error [Public]
|
|
47
22
|
#
|
|
48
23
|
# Takes a `SyntaxError` exception, uses the
|
|
@@ -152,20 +127,11 @@ module SyntaxSuggest
|
|
|
152
127
|
# SyntaxSuggest.invalid? [Private]
|
|
153
128
|
#
|
|
154
129
|
# Opposite of `SyntaxSuggest.valid?`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
source = source.to_s
|
|
159
|
-
|
|
160
|
-
Prism.parse(source).failure?
|
|
161
|
-
end
|
|
162
|
-
else
|
|
163
|
-
def self.invalid?(source)
|
|
164
|
-
source = source.join if source.is_a?(Array)
|
|
165
|
-
source = source.to_s
|
|
130
|
+
def self.invalid?(source)
|
|
131
|
+
source = source.join if source.is_a?(Array)
|
|
132
|
+
source = source.to_s
|
|
166
133
|
|
|
167
|
-
|
|
168
|
-
end
|
|
134
|
+
Prism.parse(source).failure?
|
|
169
135
|
end
|
|
170
136
|
|
|
171
137
|
# SyntaxSuggest.valid? [Private]
|
|
@@ -219,7 +185,6 @@ require_relative "explain_syntax"
|
|
|
219
185
|
require_relative "clean_document"
|
|
220
186
|
|
|
221
187
|
# Helpers
|
|
222
|
-
require_relative "lex_all"
|
|
223
188
|
require_relative "code_line"
|
|
224
189
|
require_relative "code_block"
|
|
225
190
|
require_relative "block_expand"
|
|
@@ -231,3 +196,5 @@ require_relative "priority_engulf_queue"
|
|
|
231
196
|
require_relative "pathname_from_message"
|
|
232
197
|
require_relative "display_invalid_blocks"
|
|
233
198
|
require_relative "parse_blocks_from_indent_line"
|
|
199
|
+
require_relative "visitor"
|
|
200
|
+
require_relative "token"
|
|
@@ -66,27 +66,9 @@ module SyntaxSuggest
|
|
|
66
66
|
#
|
|
67
67
|
# All of these problems are fixed by joining the whole heredoc into a single
|
|
68
68
|
# line.
|
|
69
|
-
#
|
|
70
|
-
# ## Comments and whitespace
|
|
71
|
-
#
|
|
72
|
-
# Comments can throw off the way the lexer tells us that the line
|
|
73
|
-
# logically belongs with the next line. This is valid ruby but
|
|
74
|
-
# results in a different lex output than before:
|
|
75
|
-
#
|
|
76
|
-
# 1 User.
|
|
77
|
-
# 2 where(name: "schneems").
|
|
78
|
-
# 3 # Comment here
|
|
79
|
-
# 4 first
|
|
80
|
-
#
|
|
81
|
-
# To handle this we can replace comment lines with empty lines
|
|
82
|
-
# and then re-lex the source. This removal and re-lexing preserves
|
|
83
|
-
# line index and document size, but generates an easier to work with
|
|
84
|
-
# document.
|
|
85
|
-
#
|
|
86
69
|
class CleanDocument
|
|
87
70
|
def initialize(source:)
|
|
88
|
-
|
|
89
|
-
@document = CodeLine.from_source(lines.join, lines: lines)
|
|
71
|
+
@document = CodeLine.from_source(source)
|
|
90
72
|
end
|
|
91
73
|
|
|
92
74
|
# Call all of the document "cleaners"
|
|
@@ -110,62 +92,6 @@ module SyntaxSuggest
|
|
|
110
92
|
@document.join
|
|
111
93
|
end
|
|
112
94
|
|
|
113
|
-
# Remove comments
|
|
114
|
-
#
|
|
115
|
-
# replace with empty newlines
|
|
116
|
-
#
|
|
117
|
-
# source = <<~'EOM'
|
|
118
|
-
# # Comment 1
|
|
119
|
-
# puts "hello"
|
|
120
|
-
# # Comment 2
|
|
121
|
-
# puts "world"
|
|
122
|
-
# EOM
|
|
123
|
-
#
|
|
124
|
-
# lines = CleanDocument.new(source: source).lines
|
|
125
|
-
# expect(lines[0].to_s).to eq("\n")
|
|
126
|
-
# expect(lines[1].to_s).to eq("puts "hello")
|
|
127
|
-
# expect(lines[2].to_s).to eq("\n")
|
|
128
|
-
# expect(lines[3].to_s).to eq("puts "world")
|
|
129
|
-
#
|
|
130
|
-
# Important: This must be done before lexing.
|
|
131
|
-
#
|
|
132
|
-
# After this change is made, we lex the document because
|
|
133
|
-
# removing comments can change how the doc is parsed.
|
|
134
|
-
#
|
|
135
|
-
# For example:
|
|
136
|
-
#
|
|
137
|
-
# values = LexAll.new(source: <<~EOM))
|
|
138
|
-
# User.
|
|
139
|
-
# # comment
|
|
140
|
-
# where(name: 'schneems')
|
|
141
|
-
# EOM
|
|
142
|
-
# expect(
|
|
143
|
-
# values.count {|v| v.type == :on_ignored_nl}
|
|
144
|
-
# ).to eq(1)
|
|
145
|
-
#
|
|
146
|
-
# After the comment is removed:
|
|
147
|
-
#
|
|
148
|
-
# values = LexAll.new(source: <<~EOM))
|
|
149
|
-
# User.
|
|
150
|
-
#
|
|
151
|
-
# where(name: 'schneems')
|
|
152
|
-
# EOM
|
|
153
|
-
# expect(
|
|
154
|
-
# values.count {|v| v.type == :on_ignored_nl}
|
|
155
|
-
# ).to eq(2)
|
|
156
|
-
#
|
|
157
|
-
def clean_sweep(source:)
|
|
158
|
-
# Match comments, but not HEREDOC strings with #{variable} interpolation
|
|
159
|
-
# https://rubular.com/r/HPwtW9OYxKUHXQ
|
|
160
|
-
source.lines.map do |line|
|
|
161
|
-
if line.match?(/^\s*#([^{].*|)$/)
|
|
162
|
-
$/
|
|
163
|
-
else
|
|
164
|
-
line
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
95
|
# Smushes all heredoc lines into one line
|
|
170
96
|
#
|
|
171
97
|
# source = <<~'EOM'
|
|
@@ -182,11 +108,11 @@ module SyntaxSuggest
|
|
|
182
108
|
start_index_stack = []
|
|
183
109
|
heredoc_beg_end_index = []
|
|
184
110
|
lines.each do |line|
|
|
185
|
-
line.
|
|
186
|
-
case
|
|
187
|
-
when :
|
|
111
|
+
line.tokens.each do |token|
|
|
112
|
+
case token.type
|
|
113
|
+
when :HEREDOC_START
|
|
188
114
|
start_index_stack << line.index
|
|
189
|
-
when :
|
|
115
|
+
when :HEREDOC_END
|
|
190
116
|
start_index = start_index_stack.pop
|
|
191
117
|
end_index = line.index
|
|
192
118
|
heredoc_beg_end_index << [start_index, end_index]
|
|
@@ -212,20 +138,10 @@ module SyntaxSuggest
|
|
|
212
138
|
# expect(lines[0].to_s).to eq(source)
|
|
213
139
|
# expect(lines[1].to_s).to eq("")
|
|
214
140
|
#
|
|
215
|
-
# The one known case this doesn't handle is:
|
|
216
|
-
#
|
|
217
|
-
# Ripper.lex <<~EOM
|
|
218
|
-
# a &&
|
|
219
|
-
# b ||
|
|
220
|
-
# c
|
|
221
|
-
# EOM
|
|
222
|
-
#
|
|
223
|
-
# For some reason this introduces `on_ignore_newline` but with BEG type
|
|
224
|
-
#
|
|
225
141
|
def join_consecutive!
|
|
226
|
-
consecutive_groups = @document.select(&:
|
|
142
|
+
consecutive_groups = @document.select(&:consecutive?).map do |code_line|
|
|
227
143
|
take_while_including(code_line.index..) do |line|
|
|
228
|
-
line.
|
|
144
|
+
line.consecutive?
|
|
229
145
|
end
|
|
230
146
|
end
|
|
231
147
|
|
|
@@ -273,16 +189,17 @@ module SyntaxSuggest
|
|
|
273
189
|
|
|
274
190
|
# Join group into the first line
|
|
275
191
|
@document[line.index] = CodeLine.new(
|
|
276
|
-
|
|
192
|
+
tokens: lines.map(&:tokens).flatten,
|
|
277
193
|
line: lines.join,
|
|
278
|
-
index: line.index
|
|
194
|
+
index: line.index,
|
|
195
|
+
consecutive: false
|
|
279
196
|
)
|
|
280
197
|
|
|
281
198
|
# Hide the rest of the lines
|
|
282
199
|
lines[1..].each do |line|
|
|
283
200
|
# The above lines already have newlines in them, if add more
|
|
284
201
|
# then there will be double newline, use an empty line instead
|
|
285
|
-
@document[line.index] = CodeLine.new(line: "", index: line.index,
|
|
202
|
+
@document[line.index] = CodeLine.new(line: "", index: line.index, tokens: [], consecutive: false)
|
|
286
203
|
end
|
|
287
204
|
end
|
|
288
205
|
self
|
|
@@ -26,23 +26,57 @@ module SyntaxSuggest
|
|
|
26
26
|
|
|
27
27
|
# Returns an array of CodeLine objects
|
|
28
28
|
# from the source string
|
|
29
|
-
def self.from_source(source
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
def self.from_source(source)
|
|
30
|
+
source = +source
|
|
31
|
+
parse_result = Prism.parse_lex(source)
|
|
32
|
+
ast, tokens = parse_result.value
|
|
33
|
+
|
|
34
|
+
clean_comments!(source, parse_result.comments)
|
|
35
|
+
|
|
36
|
+
visitor = Visitor.new
|
|
37
|
+
visitor.visit(ast)
|
|
38
|
+
tokens.sort_by! { |token, _state| token.location.start_line }
|
|
39
|
+
|
|
40
|
+
prev_token = nil
|
|
41
|
+
tokens.map! do |token, _state|
|
|
42
|
+
prev_token = Token.new(token, prev_token, visitor)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
tokens_for_line = tokens.each_with_object(Hash.new { |h, k| h[k] = [] }) { |token, hash| hash[token.line] << token }
|
|
46
|
+
source.lines.map.with_index do |line, index|
|
|
33
47
|
CodeLine.new(
|
|
34
48
|
line: line,
|
|
35
49
|
index: index,
|
|
36
|
-
|
|
50
|
+
tokens: tokens_for_line[index + 1],
|
|
51
|
+
consecutive: visitor.consecutive_lines.include?(index + 1)
|
|
37
52
|
)
|
|
38
53
|
end
|
|
39
54
|
end
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
# Remove comments that apear on their own in source. They will never be the cause
|
|
57
|
+
# of syntax errors and are just visual noise. Example:
|
|
58
|
+
#
|
|
59
|
+
# source = +<<~RUBY
|
|
60
|
+
# # Comment-only line
|
|
61
|
+
# foo # Inline comment
|
|
62
|
+
# RUBY
|
|
63
|
+
# CodeLine.clean_comments!(source, Prism.parse(source).comments)
|
|
64
|
+
# source # => "\nfoo # Inline comment\n"
|
|
65
|
+
def self.clean_comments!(source, comments)
|
|
66
|
+
# Iterate backwards since we are modifying the source in place and must preserve
|
|
67
|
+
# the offsets. Prism comments are sorted by their location in the source.
|
|
68
|
+
comments.reverse_each do |comment|
|
|
69
|
+
next if comment.trailing?
|
|
70
|
+
source.bytesplice(comment.location.start_offset, comment.location.length, "")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
attr_reader :line, :index, :tokens, :line_number, :indent
|
|
75
|
+
def initialize(line:, index:, tokens:, consecutive:)
|
|
76
|
+
@tokens = tokens
|
|
44
77
|
@line = line
|
|
45
78
|
@index = index
|
|
79
|
+
@consecutive = consecutive
|
|
46
80
|
@original = line
|
|
47
81
|
@line_number = @index + 1
|
|
48
82
|
strip_line = line.dup
|
|
@@ -151,29 +185,16 @@ module SyntaxSuggest
|
|
|
151
185
|
index <=> other.index
|
|
152
186
|
end
|
|
153
187
|
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
#
|
|
160
|
-
# This predicate method is used to determine when those
|
|
161
|
-
# two criteria have been met.
|
|
162
|
-
#
|
|
163
|
-
# The one known case this doesn't handle is:
|
|
164
|
-
#
|
|
165
|
-
# Ripper.lex <<~EOM
|
|
166
|
-
# a &&
|
|
167
|
-
# b ||
|
|
168
|
-
# c
|
|
169
|
-
# EOM
|
|
170
|
-
#
|
|
171
|
-
# For some reason this introduces `on_ignore_newline` but with BEG type
|
|
172
|
-
def ignore_newline_not_beg?
|
|
173
|
-
@ignore_newline_not_beg
|
|
188
|
+
# Can this line be logically joined together
|
|
189
|
+
# with the following line? Determined by walking
|
|
190
|
+
# the AST
|
|
191
|
+
def consecutive?
|
|
192
|
+
@consecutive
|
|
174
193
|
end
|
|
175
194
|
|
|
176
|
-
# Determines if the given line has a trailing slash
|
|
195
|
+
# Determines if the given line has a trailing slash.
|
|
196
|
+
# Simply check if the line contains a backslash after
|
|
197
|
+
# the content of the last token.
|
|
177
198
|
#
|
|
178
199
|
# lines = CodeLine.from_source(<<~EOM)
|
|
179
200
|
# it "foo" \
|
|
@@ -181,61 +202,19 @@ module SyntaxSuggest
|
|
|
181
202
|
# expect(lines.first.trailing_slash?).to eq(true)
|
|
182
203
|
#
|
|
183
204
|
def trailing_slash?
|
|
184
|
-
last = @
|
|
185
|
-
|
|
186
|
-
# Older versions of prism diverged slightly from Ripper in compatibility mode
|
|
187
|
-
case last&.type
|
|
188
|
-
when :on_sp
|
|
189
|
-
last.token == TRAILING_SLASH
|
|
190
|
-
when :on_tstring_end
|
|
191
|
-
true
|
|
192
|
-
else
|
|
193
|
-
false
|
|
194
|
-
end
|
|
205
|
+
return unless (last = @tokens.last)
|
|
206
|
+
@line.byteindex(TRAILING_SLASH, last.location.end_column) != nil
|
|
195
207
|
end
|
|
196
208
|
|
|
197
|
-
# Endless method detection
|
|
198
|
-
#
|
|
199
|
-
# From https://github.com/ruby/irb/commit/826ae909c9c93a2ddca6f9cfcd9c94dbf53d44ab
|
|
200
|
-
# Detecting a "oneliner" seems to need a state machine.
|
|
201
|
-
# This can be done by looking mostly at the "state" (last value):
|
|
202
|
-
#
|
|
203
|
-
# ENDFN -> BEG (token = '=' ) -> END
|
|
204
|
-
#
|
|
205
209
|
private def set_kw_end
|
|
206
|
-
oneliner_count = 0
|
|
207
|
-
in_oneliner_def = nil
|
|
208
|
-
|
|
209
210
|
kw_count = 0
|
|
210
211
|
end_count = 0
|
|
211
212
|
|
|
212
|
-
@
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
end_count += 1 if lex.is_end?
|
|
216
|
-
|
|
217
|
-
if lex.type == :on_ignored_nl
|
|
218
|
-
@ignore_newline_not_beg = !lex.expr_beg?
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
if in_oneliner_def.nil?
|
|
222
|
-
in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN)
|
|
223
|
-
elsif lex.state.allbits?(Ripper::EXPR_ENDFN)
|
|
224
|
-
# Continue
|
|
225
|
-
elsif lex.state.allbits?(Ripper::EXPR_BEG)
|
|
226
|
-
in_oneliner_def = :BODY if lex.token == "="
|
|
227
|
-
elsif lex.state.allbits?(Ripper::EXPR_END)
|
|
228
|
-
# We found an endless method, count it
|
|
229
|
-
oneliner_count += 1 if in_oneliner_def == :BODY
|
|
230
|
-
|
|
231
|
-
in_oneliner_def = nil
|
|
232
|
-
else
|
|
233
|
-
in_oneliner_def = nil
|
|
234
|
-
end
|
|
213
|
+
@tokens.each do |token|
|
|
214
|
+
kw_count += 1 if token.is_kw?
|
|
215
|
+
end_count += 1 if token.is_end?
|
|
235
216
|
end
|
|
236
217
|
|
|
237
|
-
kw_count -= oneliner_count
|
|
238
|
-
|
|
239
218
|
@is_kw = (kw_count - end_count) > 0
|
|
240
219
|
@is_end = (end_count - kw_count) > 0
|
|
241
220
|
end
|
|
@@ -1,96 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
annotation + message
|
|
33
|
-
else
|
|
34
|
-
message
|
|
35
|
-
end
|
|
36
|
-
rescue => e
|
|
37
|
-
if ENV["SYNTAX_SUGGEST_DEBUG"]
|
|
38
|
-
$stderr.warn(e.message)
|
|
39
|
-
$stderr.warn(e.backtrace)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Ignore internal errors
|
|
3
|
+
module SyntaxSuggest
|
|
4
|
+
# SyntaxSuggest.module_for_detailed_message [Private]
|
|
5
|
+
#
|
|
6
|
+
# Used to monkeypatch SyntaxError via Module.prepend
|
|
7
|
+
def self.module_for_detailed_message
|
|
8
|
+
Module.new {
|
|
9
|
+
def detailed_message(highlight: true, syntax_suggest: true, **kwargs)
|
|
10
|
+
return super unless syntax_suggest
|
|
11
|
+
|
|
12
|
+
require "syntax_suggest/api" unless defined?(SyntaxSuggest::DEFAULT_VALUE)
|
|
13
|
+
|
|
14
|
+
message = super
|
|
15
|
+
|
|
16
|
+
if path
|
|
17
|
+
file = Pathname.new(path)
|
|
18
|
+
io = SyntaxSuggest::MiniStringIO.new
|
|
19
|
+
|
|
20
|
+
SyntaxSuggest.call(
|
|
21
|
+
io: io,
|
|
22
|
+
source: file.read,
|
|
23
|
+
filename: file,
|
|
24
|
+
terminal: highlight
|
|
25
|
+
)
|
|
26
|
+
annotation = io.string
|
|
27
|
+
|
|
28
|
+
annotation += "\n" unless annotation.end_with?("\n")
|
|
29
|
+
|
|
30
|
+
annotation + message
|
|
31
|
+
else
|
|
43
32
|
message
|
|
44
33
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else
|
|
51
|
-
autoload :Pathname, "pathname"
|
|
52
|
-
|
|
53
|
-
#--
|
|
54
|
-
# Monkey patch kernel to ensure that all `require` calls call the same
|
|
55
|
-
# method
|
|
56
|
-
#++
|
|
57
|
-
module Kernel
|
|
58
|
-
# :stopdoc:
|
|
59
|
-
|
|
60
|
-
module_function
|
|
61
|
-
|
|
62
|
-
alias_method :syntax_suggest_original_require, :require
|
|
63
|
-
alias_method :syntax_suggest_original_require_relative, :require_relative
|
|
64
|
-
alias_method :syntax_suggest_original_load, :load
|
|
65
|
-
|
|
66
|
-
def load(file, wrap = false)
|
|
67
|
-
syntax_suggest_original_load(file)
|
|
68
|
-
rescue SyntaxError => e
|
|
69
|
-
require "syntax_suggest/api" unless defined?(SyntaxSuggest::DEFAULT_VALUE)
|
|
70
|
-
|
|
71
|
-
SyntaxSuggest.handle_error(e)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def require(file)
|
|
75
|
-
syntax_suggest_original_require(file)
|
|
76
|
-
rescue SyntaxError => e
|
|
77
|
-
require "syntax_suggest/api" unless defined?(SyntaxSuggest::DEFAULT_VALUE)
|
|
78
|
-
|
|
79
|
-
SyntaxSuggest.handle_error(e)
|
|
80
|
-
end
|
|
34
|
+
rescue => e
|
|
35
|
+
if ENV["SYNTAX_SUGGEST_DEBUG"]
|
|
36
|
+
$stderr.warn(e.message)
|
|
37
|
+
$stderr.warn(e.backtrace)
|
|
38
|
+
end
|
|
81
39
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
syntax_suggest_original_require file
|
|
85
|
-
else
|
|
86
|
-
relative_from = caller_locations(1..1).first
|
|
87
|
-
relative_from_path = relative_from.absolute_path || relative_from.path
|
|
88
|
-
syntax_suggest_original_require File.expand_path("../#{file}", relative_from_path)
|
|
40
|
+
# Ignore internal errors
|
|
41
|
+
message
|
|
89
42
|
end
|
|
90
|
-
|
|
91
|
-
require "syntax_suggest/api" unless defined?(SyntaxSuggest::DEFAULT_VALUE)
|
|
92
|
-
|
|
93
|
-
SyntaxSuggest.handle_error(e)
|
|
94
|
-
end
|
|
43
|
+
}
|
|
95
44
|
end
|
|
96
45
|
end
|
|
46
|
+
|
|
47
|
+
SyntaxError.prepend(SyntaxSuggest.module_for_detailed_message)
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
4
|
-
|
|
5
|
-
if !SyntaxSuggest.use_prism_parser?
|
|
6
|
-
require_relative "ripper_errors"
|
|
7
|
-
end
|
|
3
|
+
require_relative "left_right_token_count"
|
|
8
4
|
|
|
9
5
|
module SyntaxSuggest
|
|
10
6
|
class GetParseErrors
|
|
11
7
|
def self.errors(source)
|
|
12
|
-
|
|
13
|
-
Prism.parse(source).errors.map(&:message)
|
|
14
|
-
else
|
|
15
|
-
RipperErrors.new(source).call.errors
|
|
16
|
-
end
|
|
8
|
+
Prism.parse(source).errors.map(&:message)
|
|
17
9
|
end
|
|
18
10
|
end
|
|
19
11
|
|
|
@@ -53,14 +45,14 @@ module SyntaxSuggest
|
|
|
53
45
|
|
|
54
46
|
def initialize(code_lines:)
|
|
55
47
|
@code_lines = code_lines
|
|
56
|
-
@left_right =
|
|
48
|
+
@left_right = LeftRightTokenCount.new
|
|
57
49
|
@missing = nil
|
|
58
50
|
end
|
|
59
51
|
|
|
60
52
|
def call
|
|
61
53
|
@code_lines.each do |line|
|
|
62
|
-
line.
|
|
63
|
-
@left_right.
|
|
54
|
+
line.tokens.each do |token|
|
|
55
|
+
@left_right.count_token(token)
|
|
64
56
|
end
|
|
65
57
|
end
|
|
66
58
|
|
|
@@ -9,19 +9,19 @@ module SyntaxSuggest
|
|
|
9
9
|
#
|
|
10
10
|
# Example:
|
|
11
11
|
#
|
|
12
|
-
# left_right =
|
|
12
|
+
# left_right = LeftRightTokenCount.new
|
|
13
13
|
# left_right.count_kw
|
|
14
14
|
# left_right.missing.first
|
|
15
15
|
# # => "end"
|
|
16
16
|
#
|
|
17
|
-
# left_right =
|
|
17
|
+
# left_right = LeftRightTokenCount.new
|
|
18
18
|
# source = "{ a: b, c: d" # Note missing '}'
|
|
19
|
-
# LexAll.new(source: source).each do |
|
|
20
|
-
# left_right.
|
|
19
|
+
# LexAll.new(source: source).each do |token|
|
|
20
|
+
# left_right.count_token(token)
|
|
21
21
|
# end
|
|
22
22
|
# left_right.missing.first
|
|
23
23
|
# # => "}"
|
|
24
|
-
class
|
|
24
|
+
class LeftRightTokenCount
|
|
25
25
|
def initialize
|
|
26
26
|
@kw_count = 0
|
|
27
27
|
@end_count = 0
|
|
@@ -49,52 +49,46 @@ module SyntaxSuggest
|
|
|
49
49
|
#
|
|
50
50
|
# Example:
|
|
51
51
|
#
|
|
52
|
-
#
|
|
53
|
-
# left_right.
|
|
52
|
+
# token = CodeLine.from_source("{").first.tokens.first
|
|
53
|
+
# left_right = LeftRightTokenCount.new
|
|
54
|
+
# left_right.count_token(Token.new(token)
|
|
54
55
|
# left_right.count_for_char("{")
|
|
55
56
|
# # => 1
|
|
56
57
|
# left_right.count_for_char("}")
|
|
57
58
|
# # => 0
|
|
58
|
-
def
|
|
59
|
-
case
|
|
60
|
-
when :
|
|
59
|
+
def count_token(token)
|
|
60
|
+
case token.type
|
|
61
|
+
when :STRING_CONTENT
|
|
61
62
|
# ^^^
|
|
62
63
|
# Means it's a string or a symbol `"{"` rather than being
|
|
63
64
|
# part of a data structure (like a hash) `{ a: b }`
|
|
64
65
|
# ignore it.
|
|
65
|
-
when :
|
|
66
|
-
:
|
|
66
|
+
when :PERCENT_UPPER_W, :PERCENT_UPPER_I, :PERCENT_LOWER_W,
|
|
67
|
+
:PERCENT_LOWER_I, :REGEXP_BEGIN, :STRING_BEGIN
|
|
67
68
|
# ^^^
|
|
68
69
|
# Handle shorthand syntaxes like `%Q{ i am a string }`
|
|
69
70
|
#
|
|
70
71
|
# The start token will be the full thing `%Q{` but we
|
|
71
72
|
# need to count it as if it's a `{`. Any token
|
|
72
73
|
# can be used
|
|
73
|
-
char =
|
|
74
|
+
char = token.value[-1]
|
|
74
75
|
@count_for_char[char] += 1 if @count_for_char.key?(char)
|
|
75
|
-
when :
|
|
76
|
+
when :EMBEXPR_BEGIN
|
|
76
77
|
# ^^^
|
|
77
78
|
# Embedded string expressions like `"#{foo} <-embed"`
|
|
78
79
|
# are parsed with chars:
|
|
79
80
|
#
|
|
80
|
-
# `#{` as :
|
|
81
|
-
# `}` as :
|
|
82
|
-
#
|
|
83
|
-
# We cannot ignore both :on_emb_expr_beg and :on_embexpr_end
|
|
84
|
-
# because sometimes the lexer thinks something is an embed
|
|
85
|
-
# string end, when it is not like `lol = }` (no clue why).
|
|
81
|
+
# `#{` as :EMBEXPR_BEGIN
|
|
82
|
+
# `}` as :EMBEXPR_END
|
|
86
83
|
#
|
|
87
84
|
# When we see `#{` count it as a `{` or we will
|
|
88
85
|
# have a mis-match count.
|
|
89
86
|
#
|
|
90
|
-
|
|
91
|
-
when "\#{"
|
|
92
|
-
@count_for_char["{"] += 1
|
|
93
|
-
end
|
|
87
|
+
@count_for_char["{"] += 1
|
|
94
88
|
else
|
|
95
|
-
@end_count += 1 if
|
|
96
|
-
@kw_count += 1 if
|
|
97
|
-
@count_for_char[
|
|
89
|
+
@end_count += 1 if token.is_end?
|
|
90
|
+
@kw_count += 1 if token.is_kw?
|
|
91
|
+
@count_for_char[token.value] += 1 if @count_for_char.key?(token.value)
|
|
98
92
|
end
|
|
99
93
|
end
|
|
100
94
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SyntaxSuggest
|
|
4
|
+
# Value object for accessing lex values
|
|
5
|
+
#
|
|
6
|
+
# This lex:
|
|
7
|
+
#
|
|
8
|
+
# [IDENTIFIER(1,0)-(1,8)("describe"), 32]
|
|
9
|
+
#
|
|
10
|
+
# Would translate into:
|
|
11
|
+
#
|
|
12
|
+
# lex.location # => (1,0)-(1,8)
|
|
13
|
+
# lex.type # => :IDENTIFIER
|
|
14
|
+
# lex.token # => "describe"
|
|
15
|
+
class Token
|
|
16
|
+
attr_reader :location, :type, :value
|
|
17
|
+
|
|
18
|
+
KW_TYPES = %i[
|
|
19
|
+
KEYWORD_IF KEYWORD_UNLESS KEYWORD_WHILE KEYWORD_UNTIL
|
|
20
|
+
KEYWORD_DEF KEYWORD_CASE KEYWORD_FOR KEYWORD_BEGIN KEYWORD_CLASS KEYWORD_MODULE KEYWORD_DO KEYWORD_DO_LOOP
|
|
21
|
+
].to_set.freeze
|
|
22
|
+
private_constant :KW_TYPES
|
|
23
|
+
|
|
24
|
+
def initialize(prism_token, previous_prism_token, visitor)
|
|
25
|
+
@location = prism_token.location
|
|
26
|
+
@type = prism_token.type
|
|
27
|
+
@value = prism_token.value
|
|
28
|
+
|
|
29
|
+
# Prism lexes `:module` as SYMBOL_BEGIN, KEYWORD_MODULE
|
|
30
|
+
# https://github.com/ruby/prism/issues/3940
|
|
31
|
+
symbol_content = previous_prism_token&.type == :SYMBOL_BEGIN
|
|
32
|
+
@is_kw = KW_TYPES.include?(@type)
|
|
33
|
+
@is_kw = false if symbol_content || visitor.endless_def_keyword_offsets.include?(@location.start_offset)
|
|
34
|
+
@is_end = @type == :KEYWORD_END
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def line
|
|
38
|
+
@location.start_line
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def is_end?
|
|
42
|
+
@is_end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def is_kw?
|
|
46
|
+
@is_kw
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SyntaxSuggest
|
|
4
|
+
# Walks the Prism AST to extract structural info that cannot be reliably determined from tokens
|
|
5
|
+
# alone.
|
|
6
|
+
#
|
|
7
|
+
# Such as the location of lines that must be logically joined so the search algorithm will
|
|
8
|
+
# treat them as one. Example:
|
|
9
|
+
#
|
|
10
|
+
# source = <<~RUBY
|
|
11
|
+
# User # 1
|
|
12
|
+
# .where(name: "Earlopain") # 2
|
|
13
|
+
# .first # 3
|
|
14
|
+
# RUBY
|
|
15
|
+
# ast, _tokens = Prism.parse_lex(source).value
|
|
16
|
+
# visitor = Visitor.new
|
|
17
|
+
# visitor.visit(ast)
|
|
18
|
+
# visitor.consecutive_lines # => Set[2, 1]
|
|
19
|
+
#
|
|
20
|
+
# This output means that line 1 and line 2 need to be joined with their next line.
|
|
21
|
+
#
|
|
22
|
+
# And determining the location of "endless" method definitions. For example:
|
|
23
|
+
#
|
|
24
|
+
# source = <<~RUBY
|
|
25
|
+
# def cube(x)
|
|
26
|
+
# x * x * x
|
|
27
|
+
# end
|
|
28
|
+
# def square(x) = x * x # 1
|
|
29
|
+
# RUBY
|
|
30
|
+
#
|
|
31
|
+
# ast, _tokens = Prism.parse_lex(source).value
|
|
32
|
+
# visitor = Visitor.new
|
|
33
|
+
# visitor.visit(ast)
|
|
34
|
+
# visitor.endless_def_keyword_offsets # => Set[28]
|
|
35
|
+
class Visitor < Prism::Visitor
|
|
36
|
+
attr_reader :endless_def_keyword_offsets, :consecutive_lines
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@endless_def_keyword_offsets = Set.new
|
|
40
|
+
@consecutive_lines = Set.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Called by Prism::Visitor for every method-call node in the AST
|
|
44
|
+
# (e.g. `foo.bar`, `foo.bar.baz`).
|
|
45
|
+
def visit_call_node(node)
|
|
46
|
+
receiver_loc = node.receiver&.location
|
|
47
|
+
call_operator_loc = node.call_operator_loc
|
|
48
|
+
message_loc = node.message_loc
|
|
49
|
+
if receiver_loc && call_operator_loc && message_loc
|
|
50
|
+
# dot-leading (dot on the next line)
|
|
51
|
+
# foo # line 1 - consecutive
|
|
52
|
+
# .bar # line 2
|
|
53
|
+
if receiver_loc.end_line != call_operator_loc.start_line && call_operator_loc.start_line == message_loc.start_line
|
|
54
|
+
(receiver_loc.end_line..call_operator_loc.start_line - 1).each do |line|
|
|
55
|
+
@consecutive_lines << line
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# dot-trailing (dot on the same line as the receiver)
|
|
60
|
+
# foo. # line 1 - consecutive
|
|
61
|
+
# bar # line 2
|
|
62
|
+
if receiver_loc.end_line == call_operator_loc.start_line && call_operator_loc.start_line != message_loc.start_line
|
|
63
|
+
(call_operator_loc.start_line..message_loc.start_line - 1).each do |line|
|
|
64
|
+
@consecutive_lines << line
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Called by Prism::Visitor for every `def` node in the AST.
|
|
72
|
+
# Records the keyword start location for endless method definitions
|
|
73
|
+
# like `def foo = 123`. These are valid without a matching `end`,
|
|
74
|
+
# so Token must exclude them when deciding if a line is a keyword.
|
|
75
|
+
def visit_def_node(node)
|
|
76
|
+
@endless_def_keyword_offsets << node.def_keyword_loc.start_offset if node.equal_loc
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/syntax_suggest.gemspec
CHANGED
|
@@ -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(">= 3.
|
|
19
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.3.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"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: syntax_suggest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- schneems
|
|
@@ -54,18 +54,17 @@ files:
|
|
|
54
54
|
- lib/syntax_suggest/display_code_with_line_numbers.rb
|
|
55
55
|
- lib/syntax_suggest/display_invalid_blocks.rb
|
|
56
56
|
- lib/syntax_suggest/explain_syntax.rb
|
|
57
|
-
- lib/syntax_suggest/
|
|
58
|
-
- lib/syntax_suggest/lex_all.rb
|
|
59
|
-
- lib/syntax_suggest/lex_value.rb
|
|
57
|
+
- lib/syntax_suggest/left_right_token_count.rb
|
|
60
58
|
- lib/syntax_suggest/mini_stringio.rb
|
|
61
59
|
- lib/syntax_suggest/parse_blocks_from_indent_line.rb
|
|
62
60
|
- lib/syntax_suggest/pathname_from_message.rb
|
|
63
61
|
- lib/syntax_suggest/priority_engulf_queue.rb
|
|
64
62
|
- lib/syntax_suggest/priority_queue.rb
|
|
65
|
-
- lib/syntax_suggest/ripper_errors.rb
|
|
66
63
|
- lib/syntax_suggest/scan_history.rb
|
|
64
|
+
- lib/syntax_suggest/token.rb
|
|
67
65
|
- lib/syntax_suggest/unvisited_lines.rb
|
|
68
66
|
- lib/syntax_suggest/version.rb
|
|
67
|
+
- lib/syntax_suggest/visitor.rb
|
|
69
68
|
- syntax_suggest.gemspec
|
|
70
69
|
homepage: https://github.com/ruby/syntax_suggest.git
|
|
71
70
|
licenses:
|
|
@@ -80,7 +79,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
80
79
|
requirements:
|
|
81
80
|
- - ">="
|
|
82
81
|
- !ruby/object:Gem::Version
|
|
83
|
-
version: 3.
|
|
82
|
+
version: 3.3.0
|
|
84
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
84
|
requirements:
|
|
86
85
|
- - ">="
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SyntaxSuggest
|
|
4
|
-
# Ripper.lex is not guaranteed to lex the entire source document
|
|
5
|
-
#
|
|
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
|
|
18
|
-
class LexAll
|
|
19
|
-
include Enumerable
|
|
20
|
-
|
|
21
|
-
def initialize(source:, source_lines: nil)
|
|
22
|
-
@lex = self.class.lex(source, 1)
|
|
23
|
-
lineno = @lex.last[0][0] + 1
|
|
24
|
-
source_lines ||= source.lines
|
|
25
|
-
last_lineno = source_lines.length
|
|
26
|
-
|
|
27
|
-
until lineno >= last_lineno
|
|
28
|
-
lines = source_lines[lineno..]
|
|
29
|
-
|
|
30
|
-
@lex.concat(
|
|
31
|
-
self.class.lex(lines.join, lineno + 1)
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
lineno = @lex.last[0].first + 1
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
last_lex = nil
|
|
38
|
-
@lex.map! { |elem|
|
|
39
|
-
last_lex = LexValue.new(elem[0].first, elem[1], elem[2], elem[3], last_lex)
|
|
40
|
-
}
|
|
41
|
-
end
|
|
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
|
-
|
|
53
|
-
def to_a
|
|
54
|
-
@lex
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def each
|
|
58
|
-
return @lex.each unless block_given?
|
|
59
|
-
@lex.each do |x|
|
|
60
|
-
yield x
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def [](index)
|
|
65
|
-
@lex[index]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def last
|
|
69
|
-
@lex.last
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
require_relative "lex_value"
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SyntaxSuggest
|
|
4
|
-
# Value object for accessing lex values
|
|
5
|
-
#
|
|
6
|
-
# This lex:
|
|
7
|
-
#
|
|
8
|
-
# [1, 0], :on_ident, "describe", CMDARG
|
|
9
|
-
#
|
|
10
|
-
# Would translate into:
|
|
11
|
-
#
|
|
12
|
-
# lex.line # => 1
|
|
13
|
-
# lex.type # => :on_indent
|
|
14
|
-
# lex.token # => "describe"
|
|
15
|
-
class LexValue
|
|
16
|
-
attr_reader :line, :type, :token, :state
|
|
17
|
-
|
|
18
|
-
def initialize(line, type, token, state, last_lex = nil)
|
|
19
|
-
@line = line
|
|
20
|
-
@type = type
|
|
21
|
-
@token = token
|
|
22
|
-
@state = state
|
|
23
|
-
|
|
24
|
-
set_kw_end(last_lex)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private def set_kw_end(last_lex)
|
|
28
|
-
@is_end = false
|
|
29
|
-
@is_kw = false
|
|
30
|
-
return if type != :on_kw
|
|
31
|
-
|
|
32
|
-
return if last_lex && last_lex.fname? # https://github.com/ruby/ruby/commit/776759e300e4659bb7468e2b97c8c2d4359a2953
|
|
33
|
-
|
|
34
|
-
case token
|
|
35
|
-
when "if", "unless", "while", "until"
|
|
36
|
-
# Only count if/unless when it's not a "trailing" if/unless
|
|
37
|
-
# https://github.com/ruby/ruby/blob/06b44f819eb7b5ede1ff69cecb25682b56a1d60c/lib/irb/ruby-lex.rb#L374-L375
|
|
38
|
-
@is_kw = true unless expr_label?
|
|
39
|
-
when "def", "case", "for", "begin", "class", "module", "do"
|
|
40
|
-
@is_kw = true
|
|
41
|
-
when "end"
|
|
42
|
-
@is_end = true
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def fname?
|
|
47
|
-
state.allbits?(Ripper::EXPR_FNAME)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def ignore_newline?
|
|
51
|
-
type == :on_ignored_nl
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def is_end?
|
|
55
|
-
@is_end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def is_kw?
|
|
59
|
-
@is_kw
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def expr_beg?
|
|
63
|
-
state.anybits?(Ripper::EXPR_BEG)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def expr_label?
|
|
67
|
-
state.allbits?(Ripper::EXPR_LABEL)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SyntaxSuggest
|
|
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.
|
|
8
|
-
#
|
|
9
|
-
# Example:
|
|
10
|
-
#
|
|
11
|
-
# puts RipperErrors.new(" def foo").call.errors
|
|
12
|
-
# # => ["syntax error, unexpected end-of-input, expecting ';' or '\\n'"]
|
|
13
|
-
class RipperErrors < Ripper
|
|
14
|
-
attr_reader :errors
|
|
15
|
-
|
|
16
|
-
# Comes from ripper, called
|
|
17
|
-
# on every parse error, msg
|
|
18
|
-
# is a string
|
|
19
|
-
def on_parse_error(msg)
|
|
20
|
-
@errors ||= []
|
|
21
|
-
@errors << msg
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
alias_method :on_alias_error, :on_parse_error
|
|
25
|
-
alias_method :on_assign_error, :on_parse_error
|
|
26
|
-
alias_method :on_class_name_error, :on_parse_error
|
|
27
|
-
alias_method :on_param_error, :on_parse_error
|
|
28
|
-
alias_method :compile_error, :on_parse_error
|
|
29
|
-
|
|
30
|
-
def call
|
|
31
|
-
@run_once ||= begin
|
|
32
|
-
@errors = []
|
|
33
|
-
parse
|
|
34
|
-
true
|
|
35
|
-
end
|
|
36
|
-
self
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|