ffast 0.2.2 → 0.2.4

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/fast-pattern-expert/SKILL.md +71 -0
  3. data/.github/workflows/release.yml +27 -0
  4. data/.github/workflows/ruby.yml +34 -0
  5. data/.gitignore +2 -0
  6. data/Fastfile +105 -18
  7. data/README.md +21 -7
  8. data/bin/console +1 -1
  9. data/bin/fast-experiment +3 -0
  10. data/bin/fast-mcp +7 -0
  11. data/fast.gemspec +1 -3
  12. data/ideia_blog_post.md +36 -0
  13. data/lib/fast/cli.rb +74 -23
  14. data/lib/fast/experiment.rb +19 -2
  15. data/lib/fast/git.rb +1 -1
  16. data/lib/fast/mcp_server.rb +341 -0
  17. data/lib/fast/node.rb +258 -0
  18. data/lib/fast/prism_adapter.rb +327 -0
  19. data/lib/fast/rewriter.rb +64 -10
  20. data/lib/fast/scan.rb +207 -0
  21. data/lib/fast/shortcut.rb +16 -4
  22. data/lib/fast/source.rb +116 -0
  23. data/lib/fast/source_rewriter.rb +153 -0
  24. data/lib/fast/sql/rewriter.rb +36 -7
  25. data/lib/fast/sql.rb +15 -17
  26. data/lib/fast/summary.rb +440 -0
  27. data/lib/fast/version.rb +1 -1
  28. data/lib/fast.rb +218 -101
  29. data/mkdocs.yml +19 -4
  30. data/requirements-docs.txt +3 -0
  31. metadata +18 -59
  32. data/docs/command_line.md +0 -238
  33. data/docs/editors-integration.md +0 -46
  34. data/docs/experiments.md +0 -155
  35. data/docs/git.md +0 -115
  36. data/docs/ideas.md +0 -70
  37. data/docs/index.md +0 -404
  38. data/docs/pry-integration.md +0 -27
  39. data/docs/research.md +0 -93
  40. data/docs/shortcuts.md +0 -323
  41. data/docs/similarity_tutorial.md +0 -176
  42. data/docs/sql-support.md +0 -253
  43. data/docs/syntax.md +0 -395
  44. data/docs/videos.md +0 -16
  45. data/docs/walkthrough.md +0 -135
  46. data/examples/build_stubbed_and_let_it_be_experiment.rb +0 -51
  47. data/examples/experimental_replacement.rb +0 -46
  48. data/examples/find_usage.rb +0 -26
  49. data/examples/let_it_be_experiment.rb +0 -11
  50. data/examples/method_complexity.rb +0 -37
  51. data/examples/search_duplicated.rb +0 -15
  52. data/examples/similarity_research.rb +0 -58
  53. data/examples/simple_rewriter.rb +0 -6
  54. data/experiments/let_it_be_experiment.rb +0 -9
  55. data/experiments/remove_useless_hook.rb +0 -9
  56. data/experiments/replace_create_with_build_stubbed.rb +0 -10
@@ -4,12 +4,12 @@ module Fast
4
4
  # @see Fast::SQLRewriter
5
5
  # @return string with the content updated in case the pattern matches.
6
6
  def replace(pattern, ast, &replacement)
7
- sql_rewriter_for(pattern, ast, &replacement).rewrite!
7
+ rewriter_for(pattern, ast, &replacement).rewrite!
8
8
  end
9
9
 
10
10
  # @return [Fast::SQL::Rewriter]
11
11
  # @see Fast::Rewriter
12
- def sql_rewriter_for(pattern, ast, &replacement)
12
+ def rewriter_for(pattern, ast, &replacement)
13
13
  rewriter = Rewriter.new
14
14
  rewriter.ast = ast
15
15
  rewriter.search = pattern
@@ -41,24 +41,53 @@ module Fast
41
41
  # @see Fast::Rewriter
42
42
  class Rewriter < Fast::Rewriter
43
43
 
44
+ def rewrite!
45
+ replace_on(*types)
46
+ case ast
47
+ when Array
48
+ rewrite(buffer, ast.first)
49
+ else
50
+ rewrite(buffer, ast)
51
+ end
52
+ end
53
+
54
+ def source
55
+ super ||
56
+ begin
57
+ case ast
58
+ when Array
59
+ ast.first
60
+ else
61
+ ast
62
+ end.location.expression.source_buffer.source
63
+ end
64
+ end
44
65
  # @return [Array<Symbol>] with all types that matches
45
66
  def types
46
- ast.type
67
+ case ast
68
+ when Array
69
+ ast.map(&:type)
70
+ when NilClass
71
+ []
72
+ else
73
+ ast.type
74
+ end
47
75
  end
48
76
 
49
77
  # Generate methods for all affected types.
50
- # Note the strategy is different from parent class, it if matches the root node, it executes otherwise it search pattern on
78
+ # Note the strategy is different from parent class,
79
+ # it will not stop on first match, but will execute the replacement on
51
80
  # all matching elements.
52
81
  # @see Fast.replace
53
82
  def replace_on(*types)
54
83
  types.map do |type|
55
84
  self.instance_exec do
56
- self.class.define_method :"on_#{ast.type}" do |node|
85
+ self.class.define_method :"on_#{type}" do |node|
57
86
  # SQL nodes are not being automatically invoked by the rewriter,
58
87
  # so we need to match the root node and invoke on matching inner elements.
59
- node.search(search).each_with_index do |node, i|
88
+ Fast.search(search, ast).each_with_index do |node, i|
60
89
  @match_index += 1
61
- execute_replacement(node, nil)
90
+ execute_replacement(node, i)
62
91
  end
63
92
  end
64
93
  end
data/lib/fast/sql.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'pg_query'
2
+ require_relative '../fast/source'
2
3
  require_relative 'sql/rewriter'
3
4
 
4
5
  module Fast
@@ -42,10 +43,10 @@ module Fast
42
43
  # s(:val,
43
44
  # s(:string,
44
45
  # s(:str, "hello AST"))))))))
45
- # `s` represents a Fast::Node which is a subclass of Parser::AST::Node and
46
- # has additional methods to access the tokens and location of the node.
46
+ # `s` represents a Fast::Node with additional methods to access the tokens
47
+ # and location of the node.
47
48
  # ast.search(:string).first.location.expression
48
- # => #<Parser::Source::Range (sql) 7...18>
49
+ # => #<Fast::Source::Range (sql) 7...18>
49
50
  def parse_sql(statement, buffer_name: "(sql)")
50
51
  SQL.parse(statement, buffer_name: buffer_name)
51
52
  end
@@ -53,26 +54,26 @@ module Fast
53
54
  # This module contains methods to parse SQL statements and rewrite them.
54
55
  # It uses PGQuery to parse the SQL statements.
55
56
  # It uses Parser to rewrite the SQL statements.
56
- # It uses Parser::Source::Map to map the AST nodes to the SQL tokens.
57
+ # It uses Fast::Source::Map to map the AST nodes to the SQL tokens.
57
58
  #
58
59
  # @example
59
60
  # Fast::SQL.parse("select 1")
60
61
  # => s(:select_stmt, s(:target_list, ...
61
62
  # @see Fast::SQL::Node
62
63
  module SQL
63
- # The SQL source buffer is a subclass of Parser::Source::Buffer
64
+ # The SQL source buffer is a subclass of Fast::Source::Buffer
64
65
  # which contains the tokens of the SQL statement.
65
66
  # When you call `ast.location.expression` it will return a range
66
67
  # which is mapped to the tokens.
67
68
  # @example
68
69
  # ast = Fast::SQL.parse("select 1")
69
- # ast.location.expression # => #<Parser::Source::Range (sql) 0...9>
70
+ # ast.location.expression # => #<Fast::Source::Range (sql) 0...9>
70
71
  # ast.location.expression.source_buffer.tokens
71
72
  # => [
72
73
  # <PgQuery::ScanToken: start: 0, end: 6, token: :SELECT, keyword_kind: :RESERVED_KEYWORD>,
73
74
  # <PgQuery::ScanToken: start: 7, end: 8, token: :ICONST, keyword_kind: :NO_KEYWORD>]
74
75
  # @see Fast::SQL::Node
75
- class SourceBuffer < Parser::Source::Buffer
76
+ class SourceBuffer < Fast::Source::Buffer
76
77
  def tokens
77
78
  @tokens ||= PgQuery.scan(source).first.tokens
78
79
  end
@@ -111,15 +112,13 @@ module Fast
111
112
  return [] if statement.nil?
112
113
  source_buffer = SQL::SourceBuffer.new(buffer_name, source: statement)
113
114
  tree = PgQuery.parse(statement).tree
115
+ first, *, last = source_buffer.tokens
114
116
  stmts = tree.stmts.map do |stmt|
115
- v = clean_structure(stmt.stmt.to_h)
116
- inner_stmt = statement[stmt.stmt_location, stmt.stmt_len]
117
- first, *, last = source_buffer.tokens
118
117
  from = stmt.stmt_location
119
- to = from.zero? ? last.end : from + stmt.stmt_len
120
- expression = Parser::Source::Range.new(source_buffer, from, to)
121
- source_map = Parser::Source::Map.new(expression)
122
- sql_tree_to_ast(v, source_buffer: source_buffer, source_map: source_map)
118
+ to = stmt.stmt_len.zero? ? last.end : from + stmt.stmt_len
119
+ expression = Fast::Source.range(source_buffer, from, to)
120
+ source_map = Fast::Source.map(expression)
121
+ sql_tree_to_ast(clean_structure(stmt.stmt.to_h), source_buffer: source_buffer, source_map: source_map)
123
122
  end.flatten
124
123
  stmts.one? ? stmts.first : stmts
125
124
  end
@@ -150,8 +149,8 @@ module Fast
150
149
  when Hash
151
150
  if (start = obj.delete(:location))
152
151
  if (token = source_buffer.tokens.find{|e|e.start == start})
153
- expression = Parser::Source::Range.new(source_buffer, token.start, token.end)
154
- source_map = Parser::Source::Map.new(expression)
152
+ expression = Fast::Source.range(source_buffer, token.start, token.end)
153
+ source_map = Fast::Source.map(expression)
155
154
  end
156
155
  end
157
156
  obj.map do |key, value|
@@ -164,4 +163,3 @@ module Fast
164
163
  end
165
164
  end
166
165
  end
167
-
@@ -0,0 +1,440 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fast/prism_adapter'
4
+
5
+ module Fast
6
+ class Summary
7
+ VISIBILITIES = %i[public protected private].freeze
8
+
9
+ def initialize(code_or_ast, file: nil, command_name: '.summary', level: nil)
10
+ @file = file
11
+ @command_name = command_name
12
+ @level = normalize_level(level)
13
+ @source =
14
+ if code_or_ast.is_a?(String)
15
+ code_or_ast
16
+ elsif code_or_ast.respond_to?(:loc) && code_or_ast.loc.respond_to?(:expression)
17
+ code_or_ast.loc.expression.source
18
+ end
19
+
20
+ @ast =
21
+ if unsupported_template?
22
+ nil
23
+ elsif code_or_ast.is_a?(String)
24
+ begin
25
+ Fast.parse_ruby(code_or_ast, buffer_name: file || '(string)')
26
+ rescue StandardError => e
27
+ warn "Error parsing #{file || 'source'}: #{e.message}" if Fast.debugging
28
+ nil
29
+ end
30
+ else
31
+ code_or_ast
32
+ end
33
+ end
34
+
35
+ def summarize
36
+ if @ast
37
+ print_node(@ast)
38
+ elsif unsupported_template?
39
+ puts "Unsupported template format for #{@command_name}: #{File.extname(@file)}"
40
+ else
41
+ puts "Unable to parse #{@file || 'source'} for #{@command_name}"
42
+ end
43
+ end
44
+
45
+ def outline
46
+ return [] unless @ast
47
+
48
+ top_level_nodes(@ast).filter_map { |node| outline_for(node) }
49
+ end
50
+
51
+ private
52
+
53
+ def unsupported_template?
54
+ @file && !File.extname(@file).empty? && File.extname(@file) != '.rb'
55
+ end
56
+
57
+ def print_node(node, indent = '')
58
+ return unless Fast.ast_node?(node)
59
+
60
+ case node.type
61
+ when :module
62
+ puts "#{indent}module #{node_source(node.children[0])}"
63
+ summarize_body(node.children[1], indent + ' ')
64
+ puts "#{indent}end"
65
+ when :class
66
+ name = node_source(node.children[0])
67
+ superclass = node.children[1] ? " < #{node_source(node.children[1])}" : ''
68
+ puts "#{indent}class #{name}#{superclass}"
69
+ summarize_body(node.children[2], indent + ' ')
70
+ puts "#{indent}end"
71
+ when :begin
72
+ summarize_body(node, indent)
73
+ else
74
+ summarize_body(node, indent)
75
+ end
76
+ end
77
+
78
+ def top_level_nodes(node)
79
+ return [] unless Fast.ast_node?(node)
80
+
81
+ case node.type
82
+ when :begin
83
+ node.children.select { |child| Fast.ast_node?(child) }
84
+ else
85
+ [node]
86
+ end
87
+ end
88
+
89
+ def outline_for(node)
90
+ return unless Fast.ast_node?(node)
91
+
92
+ case node.type
93
+ when :module
94
+ summary = build_summary(node.children[1])
95
+ build_outline_entry(node, summary, kind: :module, name: node_source(node.children[0]))
96
+ when :class
97
+ summary = build_summary(node.children[2])
98
+ build_outline_entry(node, summary,
99
+ kind: :class,
100
+ name: node_source(node.children[0]),
101
+ superclass: node.children[1] && node_source(node.children[1]))
102
+ else
103
+ summary = build_summary(node)
104
+ build_outline_entry(node, summary, kind: node.type, name: node.type.to_s)
105
+ end
106
+ end
107
+
108
+ def build_outline_entry(node, summary, kind:, name:, superclass: nil)
109
+ {
110
+ file: @file,
111
+ kind: kind,
112
+ name: name,
113
+ superclass: superclass,
114
+ headline: outline_headline(kind, name, superclass),
115
+ constants: summary[:constants],
116
+ mixins: summary[:mixins],
117
+ relationships: summary[:relationships],
118
+ attributes: summary[:attributes],
119
+ scopes: summary[:scopes],
120
+ hooks: summary[:hooks],
121
+ validations: summary[:validations],
122
+ macros: summary[:macros],
123
+ requires: summary[:requires],
124
+ methods: summary[:methods],
125
+ nested: summary[:nested].filter_map { |child| outline_for(child) },
126
+ line: node.loc&.expression&.line
127
+ }
128
+ end
129
+
130
+ def outline_headline(kind, name, superclass)
131
+ case kind
132
+ when :module
133
+ "module #{name}"
134
+ when :class
135
+ superclass ? "class #{name} < #{superclass}" : "class #{name}"
136
+ else
137
+ name.to_s
138
+ end
139
+ end
140
+
141
+ def summarize_body(body, indent)
142
+ return unless Fast.ast_node?(body)
143
+
144
+ summary = build_summary(body)
145
+
146
+ if show_signals?
147
+ print_requires(summary[:requires], indent)
148
+ print_lines(summary[:constants], indent)
149
+ print_lines(summary[:mixins], indent)
150
+ print_lines(summary[:relationships], indent)
151
+ print_lines(summary[:attributes], indent)
152
+ print_section('Scopes', summary[:scopes], indent)
153
+ print_section('Hooks', summary[:hooks], indent)
154
+ print_section('Validations', summary[:validations], indent)
155
+ print_section('Macros', summary[:macros], indent)
156
+ end
157
+ print_methods(summary[:methods], indent) if show_methods?
158
+ summary[:nested].each { |child| print_node(child, indent) }
159
+ end
160
+
161
+ def build_summary(body)
162
+ summary = {
163
+ constants: [],
164
+ mixins: [],
165
+ relationships: [],
166
+ attributes: [],
167
+ scopes: [],
168
+ hooks: [],
169
+ validations: [],
170
+ macros: [],
171
+ requires: [],
172
+ methods: VISIBILITIES.to_h { |visibility| [visibility, []] },
173
+ nested: []
174
+ }
175
+
176
+ visibility = :public
177
+ body_nodes(body).each do |node|
178
+ next unless Fast.ast_node?(node)
179
+
180
+ case node.type
181
+ when :class, :module
182
+ summary[:nested] << node
183
+ when :casgn
184
+ summary[:constants] << constant_line(node)
185
+ when :def
186
+ summary[:methods][visibility] << method_signature(node)
187
+ when :defs
188
+ summary[:methods][visibility] << singleton_method_signature(node)
189
+ when :sclass
190
+ summarize_singleton_class(node, summary, visibility)
191
+ when :send
192
+ visibility = visibility_change(node) || visibility
193
+ categorize_send(node, summary)
194
+ end
195
+ end
196
+
197
+ summary
198
+ end
199
+
200
+ def summarize_singleton_class(node, summary, default_visibility)
201
+ visibility = default_visibility
202
+ body_nodes(node.children[1]).each do |child|
203
+ next unless Fast.ast_node?(child)
204
+
205
+ case child.type
206
+ when :def
207
+ summary[:methods][visibility] << "def self.#{method_signature(child).delete_prefix('def ')}"
208
+ when :send
209
+ visibility = visibility_change(child) || visibility
210
+ categorize_send(child, summary)
211
+ when :class, :module
212
+ summary[:nested] << child
213
+ when :casgn
214
+ summary[:constants] << constant_line(child)
215
+ end
216
+ end
217
+ end
218
+
219
+ def categorize_send(node, summary)
220
+ return unless node.type == :send && node.children[0].nil?
221
+
222
+ method_name = node.children[1]
223
+ if Fast.match?('(send nil {has_many belongs_to has_one has_and_belongs_to_many} ...)', node)
224
+ summary[:relationships] << compact_node_source(node)
225
+ elsif Fast.match?('(send nil {attr_accessor attr_reader attr_writer} ...)', node)
226
+ summary[:attributes] << attribute_line(node)
227
+ elsif Fast.match?('(send nil {include extend prepend} ...)', node)
228
+ summary[:mixins] << compact_node_source(node)
229
+ elsif Fast.match?('(send nil scope ...)', node)
230
+ summary[:scopes] << scope_line(node)
231
+ elsif Fast.match?('(send nil validates ...)', node)
232
+ summary[:validations] << node_source(node).delete_prefix('validates ')
233
+ elsif Fast.match?('(send nil validate ...)', node)
234
+ summary[:validations] << node_source(node).delete_prefix('validate ')
235
+ elsif Fast.match?('(send nil {require require_relative} (str _))', node)
236
+ summary[:requires] << required_path(node)
237
+ elsif Fast.match?('(send nil {private protected public})', node)
238
+ nil
239
+ else
240
+ summary[:hooks] << compact_node_source(node) if callback_macro?(method_name)
241
+ summary[:macros] << compact_node_source(node) if macro_candidate?(node, summary)
242
+ end
243
+ end
244
+
245
+ def macro_candidate?(node, summary)
246
+ return false unless node.type == :send && node.children[0].nil?
247
+
248
+ name = node.children[1]
249
+ return false if callback_macro?(name)
250
+ return false if Fast.match?('(send nil {has_many belongs_to has_one has_and_belongs_to_many} ...)', node)
251
+ return false if Fast.match?('(send nil {attr_accessor attr_reader attr_writer} ...)', node)
252
+ return false if Fast.match?('(send nil {include extend prepend} ...)', node)
253
+ return false if Fast.match?('(send nil {require require_relative} (str _))', node)
254
+ return false if Fast.match?('(send nil {scope validates validate private protected public} ...)', node)
255
+
256
+ !summary[:macros].include?(compact_node_source(node))
257
+ end
258
+
259
+ def callback_macro?(method_name)
260
+ method_name.to_s.start_with?('before_', 'after_', 'around_')
261
+ end
262
+
263
+ def visibility_change(node)
264
+ return unless node.type == :send && node.children[0].nil?
265
+ return unless VISIBILITIES.include?(node.children[1])
266
+ return unless node.children.length == 2
267
+
268
+ node.children[1]
269
+ end
270
+
271
+ def body_nodes(node)
272
+ return [] unless node
273
+ return node.children if node.type == :begin
274
+
275
+ [node]
276
+ end
277
+
278
+ def normalize_level(level)
279
+ return 3 if level.nil?
280
+
281
+ [[level.to_i, 1].max, 3].min
282
+ end
283
+
284
+ def show_signals?
285
+ @level >= 2
286
+ end
287
+
288
+ def show_methods?
289
+ @level >= 3
290
+ end
291
+
292
+ def constant_line(node)
293
+ lhs = node_source(node.children[0])
294
+ name = node.children[1]
295
+ rhs = node.children[2]
296
+ target = lhs.nil? || lhs.empty? || lhs == 'nil' ? name.to_s : "#{lhs}::#{name}"
297
+ rhs ? "#{target} = #{compact_value(rhs)}" : target
298
+ end
299
+
300
+ def attribute_line(node)
301
+ method_name, = captures_for('(send nil $_ ...)', node)
302
+ args = direct_symbol_arguments(node).map { |symbol| ":#{symbol}" }
303
+ "#{method_name} #{args.join(', ')}"
304
+ end
305
+
306
+ def required_path(node)
307
+ path_node = captures_for('(send nil $_ $(str _))', node).last
308
+ path_node.children.first.inspect
309
+ end
310
+
311
+ def scope_line(node)
312
+ name = captures_for('(send nil :scope (sym $_) ...)', node).first
313
+ lambda_node = captures_for('(send nil :scope (sym _) $({lambda block} ... ...))', node).first
314
+ args = lambda_args(lambda_node)
315
+ [name, args].join
316
+ end
317
+
318
+ def captures_for(pattern, node)
319
+ Fast.match?(pattern, node) || []
320
+ end
321
+
322
+ def lambda_args(node)
323
+ return '' unless Fast.ast_node?(node)
324
+ return '' unless node.type == :lambda || node.type == :block
325
+
326
+ args_node =
327
+ if node.type == :block
328
+ node.children.find { |child| Fast.match?('(args ...)', child) }
329
+ else
330
+ node.children[0]
331
+ end
332
+ return '' unless Fast.match?('(args ...)', args_node) || Fast.match?('(args)', args_node)
333
+ return '' if args_node.children.empty?
334
+
335
+ "(#{args_node.children.map { |arg| node_source(arg) }.join(', ')})"
336
+ end
337
+
338
+ def direct_symbol_arguments(node)
339
+ node.children.drop(2).filter_map do |child|
340
+ captures = captures_for('(sym $_)', child)
341
+ captures.first if captures.any?
342
+ end
343
+ end
344
+
345
+ def method_signature(node)
346
+ args = args_signature(node.children[1])
347
+ "def #{node.children[0]}#{args}"
348
+ end
349
+
350
+ def singleton_method_signature(node)
351
+ receiver = node_source(node.children[0])
352
+ args = args_signature(node.children[2])
353
+ "def #{receiver}.#{node.children[1]}#{args}"
354
+ end
355
+
356
+ def args_signature(args_node)
357
+ return '' unless Fast.match?('(args ...)', args_node) || Fast.match?('(args)', args_node)
358
+ return '' if args_node.children.empty?
359
+
360
+ "(#{args_node.children.map { |arg| node_source(arg) }.join(', ')})"
361
+ end
362
+
363
+ def compact_value(node)
364
+ return node_source(node) unless Fast.ast_node?(node)
365
+
366
+ case node.type
367
+ when :array
368
+ '[...]'
369
+ when :hash
370
+ '{...}'
371
+ when :block, :lambda
372
+ '{ ... }'
373
+ when :send
374
+ return compact_value(node.children[0]) if node.children[1] == :freeze && node.children.length == 2
375
+
376
+ node_source(node)
377
+ else
378
+ node_source(node)
379
+ end
380
+ end
381
+
382
+ def node_source(node)
383
+ return node.to_s unless Fast.ast_node?(node)
384
+
385
+ node.loc.expression.source
386
+ rescue StandardError
387
+ node.to_s
388
+ end
389
+
390
+ def compact_node_source(node)
391
+ source = node_source(node)
392
+ return source unless source.include?("\n")
393
+
394
+ head = source.lines.first.strip
395
+ if head.end_with?('do') || head.include?(' do')
396
+ "#{head} ... end"
397
+ else
398
+ "#{head} ..."
399
+ end
400
+ end
401
+
402
+ def print_requires(requires, indent)
403
+ return if requires.empty?
404
+
405
+ formatted = requires.map { |entry| entry.split(' ', 2).last }.join(', ')
406
+ puts "#{indent}requires: #{formatted}"
407
+ puts
408
+ end
409
+
410
+ def print_lines(lines, indent)
411
+ return if lines.empty?
412
+
413
+ puts
414
+ lines.each { |line| puts "#{indent}#{line}" }
415
+ end
416
+
417
+ def print_section(title, lines, indent)
418
+ return if lines.empty?
419
+
420
+ puts
421
+ joined = lines.join(', ')
422
+ if lines.one? || joined.length <= 100
423
+ puts "#{indent}#{title}: #{joined}"
424
+ else
425
+ puts "#{indent}#{title}:"
426
+ lines.each { |line| puts "#{indent} #{line}" }
427
+ end
428
+ end
429
+
430
+ def print_methods(methods, indent)
431
+ VISIBILITIES.each do |visibility|
432
+ next if methods[visibility].empty?
433
+
434
+ puts
435
+ puts "#{indent}#{visibility}" unless visibility == :public
436
+ methods[visibility].each { |signature| puts "#{indent}#{' ' unless visibility == :public}#{signature}" }
437
+ end
438
+ end
439
+ end
440
+ end
data/lib/fast/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fast
4
- VERSION = '0.2.2'
4
+ VERSION = '0.2.4'
5
5
  end