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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
require 'fast/source'
|
|
5
|
+
|
|
6
|
+
module Fast
|
|
7
|
+
module PrismAdapter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
class Location
|
|
11
|
+
attr_accessor :node
|
|
12
|
+
attr_reader :expression
|
|
13
|
+
|
|
14
|
+
def initialize(buffer_name, source, start_offset, end_offset, prism_node: nil)
|
|
15
|
+
@buffer_name = buffer_name
|
|
16
|
+
@source = source
|
|
17
|
+
@prism_node = prism_node
|
|
18
|
+
buffer = Fast::Source.buffer(buffer_name, source: source)
|
|
19
|
+
@expression = Fast::Source.range(
|
|
20
|
+
buffer,
|
|
21
|
+
character_offset(source, start_offset),
|
|
22
|
+
character_offset(source, end_offset)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def name
|
|
27
|
+
return unless @prism_node&.respond_to?(:name_loc) && @prism_node.name_loc
|
|
28
|
+
|
|
29
|
+
range_for(@prism_node.name_loc)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def selector
|
|
33
|
+
return unless @prism_node&.respond_to?(:message_loc) && @prism_node.message_loc
|
|
34
|
+
|
|
35
|
+
range_for(@prism_node.message_loc)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def operator
|
|
39
|
+
return unless @prism_node&.respond_to?(:operator_loc) && @prism_node.operator_loc
|
|
40
|
+
|
|
41
|
+
range_for(@prism_node.operator_loc)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def character_offset(source, byte_offset)
|
|
47
|
+
source.byteslice(0, byte_offset).to_s.length
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def range_for(prism_location)
|
|
51
|
+
buffer = Fast::Source.buffer(@buffer_name, source: @source)
|
|
52
|
+
Fast::Source.range(
|
|
53
|
+
buffer,
|
|
54
|
+
character_offset(@source, prism_location.start_offset),
|
|
55
|
+
character_offset(@source, prism_location.end_offset)
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class Node < Fast::Node
|
|
61
|
+
def initialize(type, children:, loc:)
|
|
62
|
+
super(type, Array(children), location: loc)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def updated(type = nil, children = nil, properties = nil)
|
|
66
|
+
self.class.new(
|
|
67
|
+
type || self.type,
|
|
68
|
+
children: children || self.children,
|
|
69
|
+
loc: properties&.fetch(:location, loc) || loc
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse(source, buffer_name: '(string)')
|
|
75
|
+
result = Prism.parse(source)
|
|
76
|
+
return unless result.success?
|
|
77
|
+
|
|
78
|
+
adapt(result.value, source, buffer_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def adapt(node, source, buffer_name)
|
|
82
|
+
return if node.nil?
|
|
83
|
+
|
|
84
|
+
case node
|
|
85
|
+
when Prism::ProgramNode
|
|
86
|
+
statements = adapt_statements(node.statements, source, buffer_name)
|
|
87
|
+
statements.is_a?(Node) ? statements : build_node(:begin, statements, node, source, buffer_name)
|
|
88
|
+
when Prism::StatementsNode
|
|
89
|
+
adapt_statements(node, source, buffer_name)
|
|
90
|
+
when Prism::ModuleNode
|
|
91
|
+
build_node(:module, [adapt(node.constant_path, source, buffer_name), adapt(node.body, source, buffer_name)], node, source, buffer_name)
|
|
92
|
+
when Prism::ClassNode
|
|
93
|
+
build_node(:class, [adapt(node.constant_path, source, buffer_name), adapt(node.superclass, source, buffer_name), adapt(node.body, source, buffer_name)], node, source, buffer_name)
|
|
94
|
+
when Prism::SingletonClassNode
|
|
95
|
+
build_node(:sclass, [adapt(node.expression, source, buffer_name), adapt(node.body, source, buffer_name)], node, source, buffer_name)
|
|
96
|
+
when Prism::DefNode
|
|
97
|
+
if node.receiver
|
|
98
|
+
build_node(:defs, [adapt(node.receiver, source, buffer_name), node.name, adapt_parameters(node.parameters, source, buffer_name), adapt(node.body, source, buffer_name)], node, source, buffer_name)
|
|
99
|
+
else
|
|
100
|
+
build_node(:def, [node.name, adapt_parameters(node.parameters, source, buffer_name), adapt(node.body, source, buffer_name)], node, source, buffer_name)
|
|
101
|
+
end
|
|
102
|
+
when Prism::BlockNode
|
|
103
|
+
return nil unless node.respond_to?(:call)
|
|
104
|
+
|
|
105
|
+
build_node(:block, [adapt_call_node(node.call, source, buffer_name), adapt_block_parameters(node.parameters, source, buffer_name), adapt(node.body, source, buffer_name)], node, source, buffer_name)
|
|
106
|
+
when Prism::CallNode
|
|
107
|
+
if node.respond_to?(:block) && node.block.is_a?(Prism::BlockNode)
|
|
108
|
+
return build_node(
|
|
109
|
+
:block,
|
|
110
|
+
[
|
|
111
|
+
adapt_call_node(node, source, buffer_name),
|
|
112
|
+
adapt_block_parameters(node.block.parameters, source, buffer_name),
|
|
113
|
+
adapt(node.block.body, source, buffer_name)
|
|
114
|
+
],
|
|
115
|
+
node,
|
|
116
|
+
source,
|
|
117
|
+
buffer_name
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
adapt_call_node(node, source, buffer_name)
|
|
122
|
+
when Prism::ParenthesesNode
|
|
123
|
+
adapt(node.body, source, buffer_name)
|
|
124
|
+
when Prism::RangeNode
|
|
125
|
+
build_node(node.exclude_end? ? :erange : :irange, [adapt(node.left, source, buffer_name), adapt(node.right, source, buffer_name)], node, source, buffer_name)
|
|
126
|
+
when Prism::BlockArgumentNode
|
|
127
|
+
build_node(:block_pass, [adapt(node.expression, source, buffer_name)], node, source, buffer_name)
|
|
128
|
+
when Prism::ConstantPathNode
|
|
129
|
+
build_const_path(node, source, buffer_name)
|
|
130
|
+
when Prism::ConstantReadNode
|
|
131
|
+
build_node(:const, [nil, node.name], node, source, buffer_name)
|
|
132
|
+
when Prism::ConstantWriteNode
|
|
133
|
+
build_node(:casgn, [nil, node.name, adapt(node.value, source, buffer_name)], node, source, buffer_name)
|
|
134
|
+
when Prism::SymbolNode
|
|
135
|
+
build_node(:sym, [node.unescaped], node, source, buffer_name)
|
|
136
|
+
when Prism::StringNode
|
|
137
|
+
build_node(:str, [node.unescaped], node, source, buffer_name)
|
|
138
|
+
when Prism::InterpolatedStringNode
|
|
139
|
+
build_node(:dstr, node.parts.filter_map { |part| adapt(part, source, buffer_name) }, node, source, buffer_name)
|
|
140
|
+
when Prism::InterpolatedSymbolNode
|
|
141
|
+
build_node(:dsym, node.parts.filter_map { |part| adapt(part, source, buffer_name) }, node, source, buffer_name)
|
|
142
|
+
when Prism::ArrayNode
|
|
143
|
+
build_node(:array, node.elements.map { |child| adapt(child, source, buffer_name) }, node, source, buffer_name)
|
|
144
|
+
when Prism::HashNode
|
|
145
|
+
build_node(:hash, node.elements.map { |child| adapt(child, source, buffer_name) }, node, source, buffer_name)
|
|
146
|
+
when Prism::KeywordHashNode
|
|
147
|
+
build_node(:hash, node.elements.map { |child| adapt(child, source, buffer_name) }, node, source, buffer_name)
|
|
148
|
+
when Prism::AssocNode
|
|
149
|
+
build_node(:pair, [adapt(node.key, source, buffer_name), adapt(node.value, source, buffer_name)], node, source, buffer_name)
|
|
150
|
+
when Prism::SelfNode
|
|
151
|
+
build_node(:self, [], node, source, buffer_name)
|
|
152
|
+
when Prism::LocalVariableReadNode
|
|
153
|
+
build_node(:lvar, [node.name], node, source, buffer_name)
|
|
154
|
+
when Prism::InstanceVariableReadNode
|
|
155
|
+
build_node(:ivar, [node.name], node, source, buffer_name)
|
|
156
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode
|
|
157
|
+
build_node(:ivasgn, [node.name, adapt(node.value, source, buffer_name)], node, source, buffer_name)
|
|
158
|
+
when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode
|
|
159
|
+
build_node(:lvasgn, [node.name, adapt(node.value, source, buffer_name)], node, source, buffer_name)
|
|
160
|
+
when Prism::LocalVariableOperatorWriteNode
|
|
161
|
+
build_node(
|
|
162
|
+
:op_asgn,
|
|
163
|
+
[
|
|
164
|
+
build_node(:lvasgn, [node.name], node, source, buffer_name),
|
|
165
|
+
(node.respond_to?(:binary_operator) ? node.binary_operator : node.operator),
|
|
166
|
+
adapt(node.value, source, buffer_name)
|
|
167
|
+
],
|
|
168
|
+
node,
|
|
169
|
+
source,
|
|
170
|
+
buffer_name
|
|
171
|
+
)
|
|
172
|
+
when Prism::IntegerNode
|
|
173
|
+
build_node(:int, [node.value], node, source, buffer_name)
|
|
174
|
+
when Prism::FloatNode
|
|
175
|
+
build_node(:float, [node.value], node, source, buffer_name)
|
|
176
|
+
when Prism::TrueNode
|
|
177
|
+
build_node(:true, [], node, source, buffer_name)
|
|
178
|
+
when Prism::FalseNode
|
|
179
|
+
build_node(:false, [], node, source, buffer_name)
|
|
180
|
+
when Prism::NilNode
|
|
181
|
+
build_node(:nil, [], node, source, buffer_name)
|
|
182
|
+
when Prism::IfNode
|
|
183
|
+
build_node(:if, [adapt(node.predicate, source, buffer_name), adapt(node.statements, source, buffer_name), adapt(node.consequent, source, buffer_name)], node, source, buffer_name)
|
|
184
|
+
when Prism::UnlessNode
|
|
185
|
+
build_node(:if, [adapt(node.predicate, source, buffer_name), adapt(node.consequent, source, buffer_name), adapt(node.statements, source, buffer_name)], node, source, buffer_name)
|
|
186
|
+
when Prism::CaseNode
|
|
187
|
+
children = [adapt(node.predicate, source, buffer_name)]
|
|
188
|
+
children.concat(node.conditions.map { |condition| adapt(condition, source, buffer_name) })
|
|
189
|
+
else_clause =
|
|
190
|
+
if node.respond_to?(:else_clause)
|
|
191
|
+
node.else_clause
|
|
192
|
+
elsif node.respond_to?(:consequent)
|
|
193
|
+
node.consequent
|
|
194
|
+
end
|
|
195
|
+
children << adapt_else_clause(else_clause, source, buffer_name) if else_clause
|
|
196
|
+
build_node(:case, children, node, source, buffer_name)
|
|
197
|
+
when Prism::WhenNode
|
|
198
|
+
condition =
|
|
199
|
+
if node.conditions.length == 1
|
|
200
|
+
adapt(node.conditions.first, source, buffer_name)
|
|
201
|
+
else
|
|
202
|
+
build_node(:array, node.conditions.map { |child| adapt(child, source, buffer_name) }, node, source, buffer_name)
|
|
203
|
+
end
|
|
204
|
+
build_node(:when, [condition, adapt(node.statements, source, buffer_name)].compact, node, source, buffer_name)
|
|
205
|
+
when Prism::ElseNode
|
|
206
|
+
adapt_else_clause(node, source, buffer_name)
|
|
207
|
+
when Prism::BeginNode, Prism::EmbeddedStatementsNode
|
|
208
|
+
statements = adapt_statements(node.statements, source, buffer_name)
|
|
209
|
+
children = statements.is_a?(Node) && statements.type == :begin ? statements.children : Array(statements)
|
|
210
|
+
build_node(:begin, children, node, source, buffer_name)
|
|
211
|
+
when Prism::EmbeddedVariableNode
|
|
212
|
+
build_node(:begin, [adapt(node.variable, source, buffer_name)].compact, node, source, buffer_name)
|
|
213
|
+
when Prism::LambdaNode
|
|
214
|
+
build_node(:lambda, [adapt_block_parameters(node.parameters, source, buffer_name), adapt(node.body, source, buffer_name)], node, source, buffer_name)
|
|
215
|
+
else
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def adapt_statements(node, source, buffer_name)
|
|
221
|
+
return nil unless node
|
|
222
|
+
|
|
223
|
+
children = node.body.filter_map { |child| adapt(child, source, buffer_name) }
|
|
224
|
+
return nil if children.empty?
|
|
225
|
+
return children.first if children.one?
|
|
226
|
+
|
|
227
|
+
build_node(:begin, children, node, source, buffer_name)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def adapt_parameters(node, source, buffer_name)
|
|
231
|
+
return build_node(:args, [], nil, source, buffer_name) unless node
|
|
232
|
+
|
|
233
|
+
children = []
|
|
234
|
+
children.concat(node.requireds.map { |child| build_node(:arg, [child.name], child, source, buffer_name) }) if node.respond_to?(:requireds)
|
|
235
|
+
children.concat(node.optionals.map { |child| build_node(:optarg, [child.name, adapt(child.value, source, buffer_name)], child, source, buffer_name) }) if node.respond_to?(:optionals)
|
|
236
|
+
children << build_node(:restarg, [parameter_name(node.rest)], node.rest, source, buffer_name) if node.respond_to?(:rest) && node.rest
|
|
237
|
+
children.concat(node.posts.map { |child| build_node(:arg, [child.name], child, source, buffer_name) }) if node.respond_to?(:posts)
|
|
238
|
+
children.concat(node.keywords.map { |child| adapt_keyword_parameter(child, source, buffer_name) }) if node.respond_to?(:keywords)
|
|
239
|
+
children << build_node(:kwrestarg, [parameter_name(node.keyword_rest)], node.keyword_rest, source, buffer_name) if node.respond_to?(:keyword_rest) && node.keyword_rest
|
|
240
|
+
children << build_node(:blockarg, [parameter_name(node.block)], node.block, source, buffer_name) if node.respond_to?(:block) && node.block
|
|
241
|
+
build_node(:args, children, node, source, buffer_name)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def adapt_block_parameters(node, source, buffer_name)
|
|
245
|
+
return build_node(:args, [], nil, source, buffer_name) unless node
|
|
246
|
+
|
|
247
|
+
params = node.respond_to?(:parameters) ? node.parameters : node
|
|
248
|
+
adapt_parameters(params, source, buffer_name)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def adapt_keyword_parameter(node, source, buffer_name)
|
|
252
|
+
case node
|
|
253
|
+
when Prism::RequiredKeywordParameterNode
|
|
254
|
+
build_node(:kwarg, [node.name], node, source, buffer_name)
|
|
255
|
+
when Prism::OptionalKeywordParameterNode
|
|
256
|
+
build_node(:kwoptarg, [node.name, adapt(node.value, source, buffer_name)], node, source, buffer_name)
|
|
257
|
+
else
|
|
258
|
+
build_node(:arg, [node.name], node, source, buffer_name)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def adapt_call_node(node, source, buffer_name)
|
|
263
|
+
children = [adapt(node.receiver, source, buffer_name), node.name]
|
|
264
|
+
children.concat(node.arguments&.arguments.to_a.map { |arg| adapt(arg, source, buffer_name) } || [])
|
|
265
|
+
children << adapt(node.block, source, buffer_name) if node.respond_to?(:block) && node.block && !node.block.is_a?(Prism::BlockNode)
|
|
266
|
+
return build_node(:send, children, node, source, buffer_name) unless node.respond_to?(:block) && node.block.is_a?(Prism::BlockNode)
|
|
267
|
+
|
|
268
|
+
end_offset = node.block.location.start_offset
|
|
269
|
+
while end_offset > node.location.start_offset && source.byteslice(end_offset - 1, 1)&.match?(/\s/)
|
|
270
|
+
end_offset -= 1
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
loc = Location.new(
|
|
274
|
+
buffer_name,
|
|
275
|
+
source,
|
|
276
|
+
node.location.start_offset,
|
|
277
|
+
end_offset,
|
|
278
|
+
prism_node: node
|
|
279
|
+
)
|
|
280
|
+
send_node = Node.new(:send, children: children, loc: loc)
|
|
281
|
+
loc.node = send_node
|
|
282
|
+
send_node
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def adapt_else_clause(node, source, buffer_name)
|
|
286
|
+
adapt(node.statements, source, buffer_name)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def parameter_name(node)
|
|
290
|
+
node.respond_to?(:name) ? node.name : nil
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def build_const_path(node, source, buffer_name)
|
|
294
|
+
parent = node.parent ? adapt(node.parent, source, buffer_name) : nil
|
|
295
|
+
build_node(:const, [parent, node.child.name], node, source, buffer_name)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def build_node(type, children, prism_node, source, buffer_name)
|
|
299
|
+
loc =
|
|
300
|
+
if prism_node
|
|
301
|
+
Location.new(buffer_name, source, prism_node.location.start_offset, prism_node.location.end_offset, prism_node: prism_node)
|
|
302
|
+
else
|
|
303
|
+
Location.new(buffer_name, source, 0, 0)
|
|
304
|
+
end
|
|
305
|
+
node = Node.new(type, children: children, loc: loc)
|
|
306
|
+
loc.node = node
|
|
307
|
+
node
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
data/lib/fast/rewriter.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fast/source'
|
|
4
|
+
require_relative 'source_rewriter'
|
|
5
|
+
|
|
3
6
|
# Rewriter loads a set of methods related to automated replacement using
|
|
4
7
|
# expressions and custom blocks of code.
|
|
5
8
|
module Fast
|
|
6
9
|
class << self
|
|
7
10
|
# Replaces content based on a pattern.
|
|
8
|
-
# @param [
|
|
11
|
+
# @param [Fast::Node] ast with the current AST to search.
|
|
9
12
|
# @param [String] pattern with the expression to be targeting nodes.
|
|
10
13
|
# @param [Proc] replacement gives the [Rewriter] context in the block.
|
|
11
14
|
# @example
|
|
@@ -15,7 +18,9 @@ module Fast
|
|
|
15
18
|
# @return [String] with the new source code after apply the replacement
|
|
16
19
|
# @see Fast::Rewriter
|
|
17
20
|
def replace(pattern, ast, source = nil, &replacement)
|
|
18
|
-
rewriter_for(pattern, ast, source, &replacement).rewrite!
|
|
21
|
+
rewritten = rewriter_for(pattern, ast, source, &replacement).rewrite!
|
|
22
|
+
Fast.validate_ruby!(rewritten, buffer_name: ast.buffer_name) if rewritten
|
|
23
|
+
rewritten
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
# @return [Fast::Rewriter]
|
|
@@ -54,12 +59,11 @@ module Fast
|
|
|
54
59
|
# rewriter.search ='(lvasgn _ ...)'
|
|
55
60
|
# rewriter.replacement = -> (node) { replace(node.location.name, 'variable_renamed') }
|
|
56
61
|
# rewriter.rewrite! # => "variable_renamed = 1"
|
|
57
|
-
class Rewriter
|
|
62
|
+
class Rewriter
|
|
58
63
|
# @return [Integer] with occurrence index
|
|
59
64
|
attr_reader :match_index
|
|
60
65
|
attr_accessor :search, :replacement, :source, :ast
|
|
61
66
|
def initialize(*_args)
|
|
62
|
-
super()
|
|
63
67
|
@match_index = 0
|
|
64
68
|
end
|
|
65
69
|
|
|
@@ -69,14 +73,18 @@ module Fast
|
|
|
69
73
|
end
|
|
70
74
|
|
|
71
75
|
def buffer
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
Fast::Source.buffer('replacement', source: source || ast.loc.expression.source)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def rewrite(source_buffer, root)
|
|
80
|
+
@source_rewriter = Fast::SourceRewriter.new(source_buffer)
|
|
81
|
+
traverse(root)
|
|
82
|
+
@source_rewriter.process
|
|
75
83
|
end
|
|
76
84
|
|
|
77
85
|
# @return [Array<Symbol>] with all types that matches
|
|
78
86
|
def types
|
|
79
|
-
Fast.search(search, ast).
|
|
87
|
+
Fast.search(search, ast).select { |node| Fast.ast_node?(node) }.map(&:type).uniq
|
|
80
88
|
end
|
|
81
89
|
|
|
82
90
|
def match?(node)
|
|
@@ -92,13 +100,33 @@ module Fast
|
|
|
92
100
|
@match_index += 1
|
|
93
101
|
execute_replacement(node, captures)
|
|
94
102
|
end
|
|
95
|
-
|
|
103
|
+
traverse_children(node)
|
|
96
104
|
end
|
|
97
105
|
end
|
|
98
106
|
end
|
|
99
107
|
|
|
108
|
+
def remove(range)
|
|
109
|
+
@source_rewriter.remove(range)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def wrap(range, before, after)
|
|
113
|
+
@source_rewriter.wrap(range, before, after)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def insert_before(range, content)
|
|
117
|
+
@source_rewriter.insert_before(range, content)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def insert_after(range, content)
|
|
121
|
+
@source_rewriter.insert_after(range, content)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def replace(range, content)
|
|
125
|
+
@source_rewriter.replace(range, content)
|
|
126
|
+
end
|
|
127
|
+
|
|
100
128
|
# Execute {#replacement} block
|
|
101
|
-
# @param [
|
|
129
|
+
# @param [Fast::Node] node that will be yield in the replacement block
|
|
102
130
|
# @param [Array<Object>, nil] captures are yield if {#replacement} take second argument.
|
|
103
131
|
def execute_replacement(node, captures)
|
|
104
132
|
if replacement.parameters.length == 1
|
|
@@ -107,5 +135,31 @@ module Fast
|
|
|
107
135
|
instance_exec node, captures, &replacement
|
|
108
136
|
end
|
|
109
137
|
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def traverse(node)
|
|
142
|
+
return if node.nil?
|
|
143
|
+
|
|
144
|
+
if node.is_a?(Array)
|
|
145
|
+
node.each { |child| traverse(child) }
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return unless Fast.ast_node?(node)
|
|
150
|
+
|
|
151
|
+
handler = :"on_#{node.type}"
|
|
152
|
+
if respond_to?(handler, true)
|
|
153
|
+
public_send(handler, node)
|
|
154
|
+
else
|
|
155
|
+
traverse_children(node)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def traverse_children(node)
|
|
160
|
+
node.children.each do |child|
|
|
161
|
+
traverse(child) if Fast.ast_node?(child) || child.is_a?(Array)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
110
164
|
end
|
|
111
165
|
end
|
data/lib/fast/scan.rb
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fast
|
|
4
|
+
class Scan
|
|
5
|
+
GROUPS = {
|
|
6
|
+
models: 'Models',
|
|
7
|
+
controllers: 'Controllers',
|
|
8
|
+
services: 'Services',
|
|
9
|
+
jobs: 'Jobs',
|
|
10
|
+
mailers: 'Mailers',
|
|
11
|
+
libraries: 'Libraries',
|
|
12
|
+
other: 'Other'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
MAX_METHODS = 5
|
|
16
|
+
MAX_SIGNALS = 4
|
|
17
|
+
MAX_MACROS = 3
|
|
18
|
+
|
|
19
|
+
def initialize(locations, command_name: '.scan', level: nil)
|
|
20
|
+
@locations = Array(locations)
|
|
21
|
+
@command_name = command_name
|
|
22
|
+
@level = normalize_level(level)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def scan
|
|
26
|
+
files = Fast.ruby_files_from(*@locations)
|
|
27
|
+
grouped = files.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |file, memo|
|
|
28
|
+
entries = flatten_entries(Fast.summary(IO.read(file), file: file, command_name: @command_name).outline)
|
|
29
|
+
next if entries.empty?
|
|
30
|
+
|
|
31
|
+
memo[classify(file, entries)] << [file, entries]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
print_grouped(grouped)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def classify(file, entries)
|
|
40
|
+
entries = structural_entries(entries)
|
|
41
|
+
|
|
42
|
+
return :models if file.include?('/models/') || model_like?(entries)
|
|
43
|
+
return :controllers if file.include?('/controllers/') || controller_like?(entries)
|
|
44
|
+
return :services if file.include?('/services/') || name_like?(entries, /Service\z/)
|
|
45
|
+
return :jobs if file.include?('/jobs/') || name_like?(entries, /Job\z/)
|
|
46
|
+
return :mailers if file.include?('/mailers/') || name_like?(entries, /Mailer\z/)
|
|
47
|
+
return :libraries if file.start_with?('lib/')
|
|
48
|
+
|
|
49
|
+
:other
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def model_like?(entries)
|
|
53
|
+
entries.any? do |entry|
|
|
54
|
+
superclass = entry[:superclass].to_s
|
|
55
|
+
superclass.end_with?('ApplicationRecord', 'ActiveRecord::Base') ||
|
|
56
|
+
entry[:relationships].any? ||
|
|
57
|
+
entry[:validations].any? ||
|
|
58
|
+
entry[:scopes].any?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def controller_like?(entries)
|
|
63
|
+
entries.any? do |entry|
|
|
64
|
+
superclass = entry[:superclass].to_s
|
|
65
|
+
superclass.end_with?('Controller', 'BaseController', 'ApplicationController') ||
|
|
66
|
+
entry[:hooks].any? { |hook| hook.include?('_action') }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def name_like?(entries, pattern)
|
|
71
|
+
entries.any? { |entry| entry[:name].to_s.match?(pattern) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def print_grouped(grouped)
|
|
75
|
+
GROUPS.each do |key, label|
|
|
76
|
+
files = grouped[key]
|
|
77
|
+
next if files.empty?
|
|
78
|
+
|
|
79
|
+
puts "#{label}:"
|
|
80
|
+
files.sort_by(&:first).each do |file, entries|
|
|
81
|
+
print_file(file, entries)
|
|
82
|
+
end
|
|
83
|
+
puts
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def print_file(file, entries)
|
|
88
|
+
entries = structural_entries(entries)
|
|
89
|
+
return if entries.empty?
|
|
90
|
+
|
|
91
|
+
puts "- #{file}"
|
|
92
|
+
entries.each do |entry|
|
|
93
|
+
puts " #{object_signature(entry)}"
|
|
94
|
+
|
|
95
|
+
signals = build_signals(entry)
|
|
96
|
+
puts " signals: #{signals.join(' | ')}" if show_signals? && signals.any?
|
|
97
|
+
|
|
98
|
+
methods = build_methods(entry)
|
|
99
|
+
puts " methods: #{methods.join(', ')}" if show_methods? && methods.any?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def structural_entries(entries)
|
|
104
|
+
filtered = entries.select do |entry|
|
|
105
|
+
%i[module class].include?(entry[:kind]) && interesting_entry?(entry)
|
|
106
|
+
end
|
|
107
|
+
filtered.empty? ? entries.reject { |entry| entry[:kind] == :send } : filtered
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def flatten_entries(entries, namespace = nil)
|
|
111
|
+
entries.flat_map do |entry|
|
|
112
|
+
qualified_name = qualify_name(namespace, entry[:name])
|
|
113
|
+
flattened_entry = entry.merge(
|
|
114
|
+
name: qualified_name,
|
|
115
|
+
nested: []
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
[flattened_entry] + flatten_entries(entry[:nested], qualified_name)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def qualify_name(namespace, name)
|
|
123
|
+
return name unless namespace && name
|
|
124
|
+
return name if name.include?('::')
|
|
125
|
+
|
|
126
|
+
"#{namespace}::#{name}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def interesting_entry?(entry)
|
|
130
|
+
entry[:methods].values.any?(&:any?) ||
|
|
131
|
+
entry[:relationships].any? ||
|
|
132
|
+
entry[:hooks].any? ||
|
|
133
|
+
entry[:validations].any? ||
|
|
134
|
+
entry[:scopes].any? ||
|
|
135
|
+
entry[:macros].any? ||
|
|
136
|
+
entry[:mixins].any?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def object_signature(entry)
|
|
140
|
+
signature = entry[:name].to_s
|
|
141
|
+
return signature unless entry[:kind] == :class && entry[:superclass]
|
|
142
|
+
|
|
143
|
+
"#{signature} < #{entry[:superclass]}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_signals(entry)
|
|
147
|
+
signals = []
|
|
148
|
+
signals << summarize_section('relationships', entry[:relationships]) if entry[:relationships].any?
|
|
149
|
+
signals << summarize_section('hooks', entry[:hooks]) if entry[:hooks].any?
|
|
150
|
+
signals << summarize_section('validations', entry[:validations]) if entry[:validations].any?
|
|
151
|
+
signals << summarize_section('scopes', entry[:scopes]) if entry[:scopes].any?
|
|
152
|
+
signals << summarize_section('macros', entry[:macros], limit: MAX_MACROS) if entry[:macros].any?
|
|
153
|
+
signals << summarize_section('mixins', entry[:mixins]) if entry[:mixins].any?
|
|
154
|
+
signals.first(MAX_SIGNALS)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def summarize_section(name, values, limit: 2)
|
|
158
|
+
preview = values.first(limit).join(', ')
|
|
159
|
+
suffix = values.length > limit ? ", +#{values.length - limit}" : ''
|
|
160
|
+
"#{name}=#{preview}#{suffix}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def build_methods(entry)
|
|
164
|
+
public_methods = entry[:methods][:public].first(MAX_METHODS)
|
|
165
|
+
protected_methods = entry[:methods][:protected].first(2)
|
|
166
|
+
private_methods = entry[:methods][:private].first(2)
|
|
167
|
+
|
|
168
|
+
methods = public_methods.map { |method| qualify_method(entry, method) }
|
|
169
|
+
methods.concat(protected_methods.map { |method| "protected #{qualify_method(entry, method)}" })
|
|
170
|
+
methods.concat(private_methods.map { |method| "private #{qualify_method(entry, method)}" })
|
|
171
|
+
methods
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def qualify_method(entry, signature)
|
|
175
|
+
method = signature.delete_prefix('def ')
|
|
176
|
+
separator = singleton_method?(method) || module_function_entry?(entry) ? '.' : '#'
|
|
177
|
+
method = method.delete_prefix('self.')
|
|
178
|
+
"#{entry[:name]}#{separator}#{method}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def singleton_method?(method)
|
|
182
|
+
method.start_with?('self.')
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def module_function_entry?(entry)
|
|
186
|
+
entry[:kind] == :module && entry[:macros].include?('module_function')
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def normalize_level(level)
|
|
190
|
+
return 3 if level.nil?
|
|
191
|
+
|
|
192
|
+
[[level.to_i, 1].max, 3].min
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def show_signals?
|
|
196
|
+
@level >= 2
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def show_methods?
|
|
200
|
+
@level >= 3
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|