ffast 0.2.2 → 0.2.3

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