ffast 0.2.0 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +27 -0
- data/.github/workflows/ruby.yml +34 -0
- data/.gitignore +2 -0
- data/Fastfile +146 -3
- data/README.md +244 -132
- data/bin/console +6 -1
- data/bin/fast-experiment +3 -0
- data/bin/fast-mcp +7 -0
- data/fast.gemspec +24 -7
- data/lib/fast/cli.rb +129 -38
- data/lib/fast/experiment.rb +19 -2
- data/lib/fast/git.rb +1 -1
- data/lib/fast/mcp_server.rb +317 -0
- data/lib/fast/node.rb +258 -0
- data/lib/fast/prism_adapter.rb +310 -0
- data/lib/fast/rewriter.rb +64 -10
- data/lib/fast/scan.rb +203 -0
- data/lib/fast/shortcut.rb +23 -6
- data/lib/fast/source.rb +116 -0
- data/lib/fast/source_rewriter.rb +153 -0
- data/lib/fast/sql/rewriter.rb +98 -0
- data/lib/fast/sql.rb +165 -0
- data/lib/fast/summary.rb +435 -0
- data/lib/fast/version.rb +1 -1
- data/lib/fast.rb +165 -79
- data/mkdocs.yml +27 -3
- data/requirements-docs.txt +3 -0
- metadata +48 -62
- data/docs/command_line.md +0 -238
- data/docs/editors-integration.md +0 -46
- data/docs/experiments.md +0 -153
- data/docs/ideas.md +0 -80
- data/docs/index.md +0 -402
- data/docs/pry-integration.md +0 -27
- data/docs/research.md +0 -93
- data/docs/shortcuts.md +0 -323
- data/docs/similarity_tutorial.md +0 -176
- data/docs/syntax.md +0 -395
- data/docs/videos.md +0 -16
- data/examples/build_stubbed_and_let_it_be_experiment.rb +0 -51
- data/examples/experimental_replacement.rb +0 -46
- data/examples/find_usage.rb +0 -26
- data/examples/let_it_be_experiment.rb +0 -11
- data/examples/method_complexity.rb +0 -37
- data/examples/search_duplicated.rb +0 -15
- data/examples/similarity_research.rb +0 -58
- data/examples/simple_rewriter.rb +0 -6
- data/experiments/let_it_be_experiment.rb +0 -9
- data/experiments/remove_useless_hook.rb +0 -9
- data/experiments/replace_create_with_build_stubbed.rb +0 -10
data/lib/fast/summary.rb
ADDED
|
@@ -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