syntax_tree 5.2.0 → 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c5651bab4283bf0f1c451619e5c3b54c9b229d1fb5050a3732918bccb7be177
4
- data.tar.gz: 2cda5e4b62a3beac16dddc8ff9298624c161bc99c3b82ba2ecc10d8d643aac4b
3
+ metadata.gz: 3261679bf963ba7263bb3ae21e826ed1fa7a28c6afbf2a2b65b42101be16b7ac
4
+ data.tar.gz: 653c6c79a34db7686ceeadf83f0d8adb47093d1ab1d946636008ee4c31144258
5
5
  SHA512:
6
- metadata.gz: 81d331f10356569b23ada917cea2ecf5a2dd1073bc927e39ba0b6036ea4b79813ce7b1e4826f39d919559cdbb5925da8a5680d72653d64d0eb05979729356173
7
- data.tar.gz: 53b320f073ef5e15efcd2e7d706e959e3ade48f18ab0f131a588a183276aeea3352cf6da0ef2ad842dc0c9bf517ac4c441ee4fbe98fa73dd156e845d5d4007d7
6
+ metadata.gz: 9cefe2a22c594efc74aa0fbe300c986a414c890efb7204633481345ceb9b0bd3578d836046ca64e289621ce31ea343f0da40db1f9b3d2d7dbd7f0a11b020c725
7
+ data.tar.gz: 628ed098064cb9e6dc720666d59b5e9d8653f7e06cb7e0774887dfeb4ed4a5bdb217ea644b93f52a3730e2dfdc14334c0690018d145728a89fa99cbd041e4301
@@ -12,7 +12,7 @@ jobs:
12
12
  steps:
13
13
  - name: Dependabot metadata
14
14
  id: metadata
15
- uses: dependabot/fetch-metadata@v1.3.5
15
+ uses: dependabot/fetch-metadata@v1.3.6
16
16
  with:
17
17
  github-token: "${{ secrets.GITHUB_TOKEN }}"
18
18
  - name: Enable auto-merge for Dependabot PRs
@@ -27,7 +27,7 @@ jobs:
27
27
  - name: Checkout
28
28
  uses: actions/checkout@v3
29
29
  - name: Setup Pages
30
- uses: actions/configure-pages@v2
30
+ uses: actions/configure-pages@v3
31
31
  - name: Set up Ruby
32
32
  uses: ruby/setup-ruby@v1
33
33
  with:
data/.gitmodules CHANGED
@@ -4,3 +4,6 @@
4
4
  [submodule "spec"]
5
5
  path = spec/ruby
6
6
  url = git@github.com:ruby/spec.git
7
+ [submodule "test/ruby-syntax-fixtures"]
8
+ path = test/ruby-syntax-fixtures
9
+ url = https://github.com/ruby-syntax-tree/ruby-syntax-fixtures
data/.rubocop.yml CHANGED
@@ -8,8 +8,12 @@ AllCops:
8
8
  TargetRubyVersion: 2.7
9
9
  Exclude:
10
10
  - '{.git,.github,bin,coverage,pkg,spec,test/fixtures,vendor,tmp}/**/*'
11
+ - test/ruby-syntax-fixtures/**/*
11
12
  - test.rb
12
13
 
14
+ Gemspec/DevelopmentDependencies:
15
+ Enabled: false
16
+
13
17
  Layout/LineLength:
14
18
  Max: 80
15
19
 
data/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [5.3.0] - 2023-01-26
10
+
11
+ ### Added
12
+
13
+ - `#arity` has been added to `DefNode`, `BlockNode`, and `Params`. The method returns a range where the lower bound is the minimum and the upper bound is the maximum number of arguments that can be used to invoke that block/method definition.
14
+ - `#arity` has been added to `CallNode`, `Command`, `CommandCall`, and `VCall` nodes. The method returns the number of arguments included in the invocation. For splats, double splats, or argument forwards, this method returns `Float::INFINITY`.
15
+ - `SyntaxTree::index` and `SyntaxTree::index_file` APIs have been added to collect a list of classes, modules, and methods defined in a given source string or file, respectively. These APIs are experimental and subject to change.
16
+ - A `plugin/disable_auto_ternary` plugin has been added the disables the formatted that automatically changes permissable `if/else` clauses into ternaries.
17
+
18
+ ### Changed
19
+
20
+ - Files are now only written from the CLI if the content of them changes, which should match watching files less chaotic.
21
+ - In the case that `rb_iseq_load` cannot be found, `Fiddle::DLError` is now rescued.
22
+ - Previously if there were invalid UTF-8 byte sequences after the `__END__` keyword the parser could potentially have crashed when parsing comments. This has been fixed.
23
+ - Previously there was special formatting for array literals that contained only variable references (either locals, method calls, or constants). For consistency, this has been removed and all array literals are now formatted the same way.
24
+
9
25
  ## [5.2.0] - 2023-01-04
10
26
 
11
27
  ### Added
@@ -481,7 +497,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
481
497
 
482
498
  - 🎉 Initial release! 🎉
483
499
 
484
- [unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.2.0...HEAD
500
+ [unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.3.0...HEAD
501
+ [5.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.2.0...v5.3.0
485
502
  [5.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.1.0...v5.2.0
486
503
  [5.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.0.1...v5.1.0
487
504
  [5.0.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.0.0...v5.0.1
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_tree (5.2.0)
4
+ syntax_tree (5.3.0)
5
5
  prettier_print (>= 1.2.0)
6
6
 
7
7
  GEM
@@ -17,18 +17,18 @@ GEM
17
17
  prettier_print (1.2.0)
18
18
  rainbow (3.1.1)
19
19
  rake (13.0.6)
20
- regexp_parser (2.6.1)
20
+ regexp_parser (2.6.2)
21
21
  rexml (3.2.5)
22
- rubocop (1.42.0)
22
+ rubocop (1.44.1)
23
23
  json (~> 2.3)
24
24
  parallel (~> 1.10)
25
- parser (>= 3.1.2.1)
25
+ parser (>= 3.2.0.0)
26
26
  rainbow (>= 2.2.2, < 4.0)
27
27
  regexp_parser (>= 1.8, < 3.0)
28
28
  rexml (>= 3.2.5, < 4.0)
29
29
  rubocop-ast (>= 1.24.1, < 2.0)
30
30
  ruby-progressbar (~> 1.7)
31
- unicode-display_width (>= 1.4.0, < 3.0)
31
+ unicode-display_width (>= 2.4.0, < 3.0)
32
32
  rubocop-ast (1.24.1)
33
33
  parser (>= 3.1.1.0)
34
34
  ruby-progressbar (1.11.0)
@@ -38,7 +38,7 @@ GEM
38
38
  simplecov_json_formatter (~> 0.1)
39
39
  simplecov-html (0.12.3)
40
40
  simplecov_json_formatter (0.1.4)
41
- unicode-display_width (2.4.1)
41
+ unicode-display_width (2.4.2)
42
42
 
43
43
  PLATFORMS
44
44
  arm64-darwin-21
data/README.md CHANGED
@@ -658,6 +658,7 @@ To register plugins, define a file somewhere in your load path named `syntax_tre
658
658
 
659
659
  * `plugin/single_quotes` - This will change all of your string literals to use single quotes instead of the default double quotes.
660
660
  * `plugin/trailing_comma` - This will put trailing commas into multiline array literals, hash literals, and method calls that can support trailing commas.
661
+ * `plugin/disable_auto_ternary` - This will prevent the automatic conversion of `if ... else` to ternary expressions.
661
662
 
662
663
  If you're using Syntax Tree as a library, you can require those files directly or manually pass those options to the formatter initializer through the `SyntaxTree::Formatter::Options` class.
663
664
 
@@ -303,10 +303,11 @@ module SyntaxTree
303
303
  options.print_width,
304
304
  options: options.formatter_options
305
305
  )
306
+ changed = source != formatted
306
307
 
307
- File.write(filepath, formatted) if item.writable?
308
+ File.write(filepath, formatted) if item.writable? && changed
308
309
 
309
- color = source == formatted ? Color.gray(filepath) : filepath
310
+ color = changed ? filepath : Color.gray(filepath)
310
311
  delta = ((Time.now - start) * 1000).round
311
312
 
312
313
  puts "#{color} #{delta}ms"
@@ -21,11 +21,15 @@ module SyntaxTree
21
21
  # that folks have become entrenched in their ways, we decided to provide a
22
22
  # small amount of configurability.
23
23
  class Options
24
- attr_reader :quote, :trailing_comma, :target_ruby_version
24
+ attr_reader :quote,
25
+ :trailing_comma,
26
+ :disable_auto_ternary,
27
+ :target_ruby_version
25
28
 
26
29
  def initialize(
27
30
  quote: :default,
28
31
  trailing_comma: :default,
32
+ disable_auto_ternary: :default,
29
33
  target_ruby_version: :default
30
34
  )
31
35
  @quote =
@@ -50,6 +54,17 @@ module SyntaxTree
50
54
  trailing_comma
51
55
  end
52
56
 
57
+ @disable_auto_ternary =
58
+ if disable_auto_ternary == :default
59
+ # We ship with a disable ternary plugin that will define this
60
+ # constant. That constant is responsible for determining the default
61
+ # disable ternary value. If it's defined, then we default to true.
62
+ # Otherwise we default to false.
63
+ defined?(DISABLE_TERNARY)
64
+ else
65
+ disable_auto_ternary
66
+ end
67
+
53
68
  @target_ruby_version =
54
69
  if target_ruby_version == :default
55
70
  # The default target Ruby version is the current version of Ruby.
@@ -69,8 +84,13 @@ module SyntaxTree
69
84
 
70
85
  # These options are overridden in plugins to we need to make sure they are
71
86
  # available here.
72
- attr_reader :quote, :trailing_comma, :target_ruby_version
87
+ attr_reader :quote,
88
+ :trailing_comma,
89
+ :disable_auto_ternary,
90
+ :target_ruby_version
91
+
73
92
  alias trailing_comma? trailing_comma
93
+ alias disable_auto_ternary? disable_auto_ternary
74
94
 
75
95
  def initialize(source, *args, options: Options.new)
76
96
  super(*args)
@@ -81,6 +101,7 @@ module SyntaxTree
81
101
  # Memoizing these values to make access faster.
82
102
  @quote = options.quote
83
103
  @trailing_comma = options.trailing_comma
104
+ @disable_auto_ternary = options.disable_auto_ternary
84
105
  @target_ruby_version = options.target_ruby_version
85
106
  end
86
107
 
@@ -0,0 +1,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ # This class can be used to build an index of the structure of Ruby files. We
5
+ # define an index as the list of constants and methods defined within a file.
6
+ #
7
+ # This index strives to be as fast as possible to better support tools like
8
+ # IDEs. Because of that, it has different backends depending on what
9
+ # functionality is available.
10
+ module Index
11
+ # This is a location for an index entry.
12
+ class Location
13
+ attr_reader :line, :column
14
+
15
+ def initialize(line, column)
16
+ @line = line
17
+ @column = column
18
+ end
19
+ end
20
+
21
+ # This entry represents a class definition using the class keyword.
22
+ class ClassDefinition
23
+ attr_reader :nesting, :name, :location, :comments
24
+
25
+ def initialize(nesting, name, location, comments)
26
+ @nesting = nesting
27
+ @name = name
28
+ @location = location
29
+ @comments = comments
30
+ end
31
+ end
32
+
33
+ # This entry represents a module definition using the module keyword.
34
+ class ModuleDefinition
35
+ attr_reader :nesting, :name, :location, :comments
36
+
37
+ def initialize(nesting, name, location, comments)
38
+ @nesting = nesting
39
+ @name = name
40
+ @location = location
41
+ @comments = comments
42
+ end
43
+ end
44
+
45
+ # This entry represents a method definition using the def keyword.
46
+ class MethodDefinition
47
+ attr_reader :nesting, :name, :location, :comments
48
+
49
+ def initialize(nesting, name, location, comments)
50
+ @nesting = nesting
51
+ @name = name
52
+ @location = location
53
+ @comments = comments
54
+ end
55
+ end
56
+
57
+ # This entry represents a singleton method definition using the def keyword
58
+ # with a specified target.
59
+ class SingletonMethodDefinition
60
+ attr_reader :nesting, :name, :location, :comments
61
+
62
+ def initialize(nesting, name, location, comments)
63
+ @nesting = nesting
64
+ @name = name
65
+ @location = location
66
+ @comments = comments
67
+ end
68
+ end
69
+
70
+ # When you're using the instruction sequence backend, this class is used to
71
+ # lazily parse comments out of the source code.
72
+ class FileComments
73
+ # We use the ripper library to pull out source comments.
74
+ class Parser < Ripper
75
+ attr_reader :comments
76
+
77
+ def initialize(*)
78
+ super
79
+ @comments = {}
80
+ end
81
+
82
+ def on_comment(value)
83
+ comments[lineno] = value.chomp
84
+ end
85
+ end
86
+
87
+ # This represents the Ruby source in the form of a file. When it needs to
88
+ # be read we'll read the file.
89
+ class FileSource
90
+ attr_reader :filepath
91
+
92
+ def initialize(filepath)
93
+ @filepath = filepath
94
+ end
95
+
96
+ def source
97
+ File.read(filepath)
98
+ end
99
+ end
100
+
101
+ # This represents the Ruby source in the form of a string. When it needs
102
+ # to be read the string is returned.
103
+ class StringSource
104
+ attr_reader :source
105
+
106
+ def initialize(source)
107
+ @source = source
108
+ end
109
+ end
110
+
111
+ attr_reader :source
112
+
113
+ def initialize(source)
114
+ @source = source
115
+ end
116
+
117
+ def comments
118
+ @comments ||= Parser.new(source.source).tap(&:parse).comments
119
+ end
120
+ end
121
+
122
+ # This class handles parsing comments from Ruby source code in the case that
123
+ # we use the instruction sequence backend. Because the instruction sequence
124
+ # backend doesn't provide comments (since they are dropped) we provide this
125
+ # interface to lazily parse them out.
126
+ class EntryComments
127
+ include Enumerable
128
+ attr_reader :file_comments, :location
129
+
130
+ def initialize(file_comments, location)
131
+ @file_comments = file_comments
132
+ @location = location
133
+ end
134
+
135
+ def each(&block)
136
+ line = location.line - 1
137
+ result = []
138
+
139
+ while line >= 0 && (comment = file_comments.comments[line])
140
+ result.unshift(comment)
141
+ line -= 1
142
+ end
143
+
144
+ result.each(&block)
145
+ end
146
+ end
147
+
148
+ # This backend creates the index using RubyVM::InstructionSequence, which is
149
+ # faster than using the Syntax Tree parser, but is not available on all
150
+ # runtimes.
151
+ class ISeqBackend
152
+ VM_DEFINECLASS_TYPE_CLASS = 0x00
153
+ VM_DEFINECLASS_TYPE_SINGLETON_CLASS = 0x01
154
+ VM_DEFINECLASS_TYPE_MODULE = 0x02
155
+ VM_DEFINECLASS_FLAG_SCOPED = 0x08
156
+ VM_DEFINECLASS_FLAG_HAS_SUPERCLASS = 0x10
157
+
158
+ def index(source)
159
+ index_iseq(
160
+ RubyVM::InstructionSequence.compile(source).to_a,
161
+ FileComments.new(FileComments::StringSource.new(source))
162
+ )
163
+ end
164
+
165
+ def index_file(filepath)
166
+ index_iseq(
167
+ RubyVM::InstructionSequence.compile_file(filepath).to_a,
168
+ FileComments.new(FileComments::FileSource.new(filepath))
169
+ )
170
+ end
171
+
172
+ private
173
+
174
+ def location_for(iseq)
175
+ code_location = iseq[4][:code_location]
176
+ Location.new(code_location[0], code_location[1])
177
+ end
178
+
179
+ def index_iseq(iseq, file_comments)
180
+ results = []
181
+ queue = [[iseq, []]]
182
+
183
+ while (current_iseq, current_nesting = queue.shift)
184
+ current_iseq[13].each_with_index do |insn, index|
185
+ next unless insn.is_a?(Array)
186
+
187
+ case insn[0]
188
+ when :defineclass
189
+ _, name, class_iseq, flags = insn
190
+
191
+ if flags == VM_DEFINECLASS_TYPE_SINGLETON_CLASS
192
+ # At the moment, we don't support singletons that aren't
193
+ # defined on self. We could, but it would require more
194
+ # emulation.
195
+ if current_iseq[13][index - 2] != [:putself]
196
+ raise NotImplementedError,
197
+ "singleton class with non-self receiver"
198
+ end
199
+ elsif flags & VM_DEFINECLASS_TYPE_MODULE > 0
200
+ location = location_for(class_iseq)
201
+ results << ModuleDefinition.new(
202
+ current_nesting,
203
+ name,
204
+ location,
205
+ EntryComments.new(file_comments, location)
206
+ )
207
+ else
208
+ location = location_for(class_iseq)
209
+ results << ClassDefinition.new(
210
+ current_nesting,
211
+ name,
212
+ location,
213
+ EntryComments.new(file_comments, location)
214
+ )
215
+ end
216
+
217
+ queue << [class_iseq, current_nesting + [name]]
218
+ when :definemethod
219
+ location = location_for(insn[2])
220
+ results << MethodDefinition.new(
221
+ current_nesting,
222
+ insn[1],
223
+ location,
224
+ EntryComments.new(file_comments, location)
225
+ )
226
+ when :definesmethod
227
+ if current_iseq[13][index - 1] != [:putself]
228
+ raise NotImplementedError,
229
+ "singleton method with non-self receiver"
230
+ end
231
+
232
+ location = location_for(insn[2])
233
+ results << SingletonMethodDefinition.new(
234
+ current_nesting,
235
+ insn[1],
236
+ location,
237
+ EntryComments.new(file_comments, location)
238
+ )
239
+ end
240
+ end
241
+ end
242
+
243
+ results
244
+ end
245
+ end
246
+
247
+ # This backend creates the index using the Syntax Tree parser and a visitor.
248
+ # It is not as fast as using the instruction sequences directly, but is
249
+ # supported on all runtimes.
250
+ class ParserBackend
251
+ class IndexVisitor < Visitor
252
+ attr_reader :results, :nesting, :statements
253
+
254
+ def initialize
255
+ @results = []
256
+ @nesting = []
257
+ @statements = nil
258
+ end
259
+
260
+ def visit_class(node)
261
+ name = visit(node.constant).to_sym
262
+ location =
263
+ Location.new(node.location.start_line, node.location.start_column)
264
+
265
+ results << ClassDefinition.new(
266
+ nesting.dup,
267
+ name,
268
+ location,
269
+ comments_for(node)
270
+ )
271
+
272
+ nesting << name
273
+ super
274
+ nesting.pop
275
+ end
276
+
277
+ def visit_const_ref(node)
278
+ node.constant.value
279
+ end
280
+
281
+ def visit_def(node)
282
+ name = node.name.value.to_sym
283
+ location =
284
+ Location.new(node.location.start_line, node.location.start_column)
285
+
286
+ results << if node.target.nil?
287
+ MethodDefinition.new(
288
+ nesting.dup,
289
+ name,
290
+ location,
291
+ comments_for(node)
292
+ )
293
+ else
294
+ SingletonMethodDefinition.new(
295
+ nesting.dup,
296
+ name,
297
+ location,
298
+ comments_for(node)
299
+ )
300
+ end
301
+ end
302
+
303
+ def visit_module(node)
304
+ name = visit(node.constant).to_sym
305
+ location =
306
+ Location.new(node.location.start_line, node.location.start_column)
307
+
308
+ results << ModuleDefinition.new(
309
+ nesting.dup,
310
+ name,
311
+ location,
312
+ comments_for(node)
313
+ )
314
+
315
+ nesting << name
316
+ super
317
+ nesting.pop
318
+ end
319
+
320
+ def visit_program(node)
321
+ super
322
+ results
323
+ end
324
+
325
+ def visit_statements(node)
326
+ @statements = node
327
+ super
328
+ end
329
+
330
+ private
331
+
332
+ def comments_for(node)
333
+ comments = []
334
+
335
+ body = statements.body
336
+ line = node.location.start_line - 1
337
+ index = body.index(node) - 1
338
+
339
+ while index >= 0 && body[index].is_a?(Comment) &&
340
+ (line - body[index].location.start_line < 2)
341
+ comments.unshift(body[index].value)
342
+ line = body[index].location.start_line
343
+ index -= 1
344
+ end
345
+
346
+ comments
347
+ end
348
+ end
349
+
350
+ def index(source)
351
+ SyntaxTree.parse(source).accept(IndexVisitor.new)
352
+ end
353
+
354
+ def index_file(filepath)
355
+ index(SyntaxTree.read(filepath))
356
+ end
357
+ end
358
+
359
+ # The class defined here is used to perform the indexing, depending on what
360
+ # functionality is available from the runtime.
361
+ INDEX_BACKEND =
362
+ defined?(RubyVM::InstructionSequence) ? ISeqBackend : ParserBackend
363
+
364
+ # This method accepts source code and then indexes it.
365
+ def self.index(source, backend: INDEX_BACKEND.new)
366
+ backend.index(source)
367
+ end
368
+
369
+ # This method accepts a filepath and then indexes it.
370
+ def self.index_file(filepath, backend: INDEX_BACKEND.new)
371
+ backend.index_file(filepath)
372
+ end
373
+ end
374
+ end