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
data/lib/fast/node.rb ADDED
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fast
4
+ class Node
5
+ attr_reader :type, :children, :loc
6
+
7
+ def initialize(type, children = [], properties = {})
8
+ @type = type.to_sym
9
+ @children = Array(children).freeze
10
+ @loc = properties[:location]
11
+ assign_parents!
12
+ end
13
+
14
+ class << self
15
+ def set_parent(node, parent)
16
+ NODE_PARENTS[node] = parent
17
+ end
18
+
19
+ def parent_for(node)
20
+ NODE_PARENTS[node]
21
+ end
22
+ end
23
+
24
+ # @return [String] with path of the file or simply buffer name.
25
+ def buffer_name
26
+ expression.source_buffer.name
27
+ end
28
+
29
+ # @return [Fast::Source::Range] from the expression
30
+ def expression
31
+ loc.expression
32
+ end
33
+
34
+ # Backward-compatible alias for callers that still use `location`.
35
+ def location
36
+ loc
37
+ end
38
+
39
+ # @return [String] with the content of the #expression
40
+ def source
41
+ expression.source
42
+ end
43
+
44
+ # @return [Boolean] true if a file exists with the #buffer_name
45
+ def from_file?
46
+ File.exist?(buffer_name)
47
+ end
48
+
49
+ def each_child_node
50
+ return enum_for(:each_child_node) unless block_given?
51
+
52
+ children.select { |child| Fast.ast_node?(child) }.each { |child| yield child }
53
+ end
54
+
55
+ def each_descendant(*types, &block)
56
+ return enum_for(:each_descendant, *types) unless block_given?
57
+
58
+ each_child_node do |child|
59
+ yield child if types.empty? || types.include?(child.type)
60
+ child.each_descendant(*types, &block) if child.respond_to?(:each_descendant)
61
+ end
62
+ end
63
+
64
+ def root?
65
+ parent.nil?
66
+ end
67
+
68
+ def parent
69
+ self.class.parent_for(self)
70
+ end
71
+
72
+ def updated(type = nil, children = nil, properties = nil)
73
+ updated_node = self.class.new(
74
+ type || self.type,
75
+ children || self.children,
76
+ { location: properties&.fetch(:location, loc) || loc }
77
+ )
78
+ updated_node.send(:assign_parents!)
79
+ updated_node
80
+ end
81
+
82
+ def ==(other)
83
+ Fast.ast_node?(other) && type == other.type && children == other.children
84
+ end
85
+ alias eql? ==
86
+
87
+ def hash
88
+ [type, children].hash
89
+ end
90
+
91
+ def to_a
92
+ children.dup
93
+ end
94
+
95
+ def <<(child)
96
+ updated(nil, children + [child])
97
+ end
98
+
99
+ def deconstruct
100
+ to_a
101
+ end
102
+
103
+ def to_ast
104
+ self
105
+ end
106
+
107
+ def to_sexp
108
+ format_node(:sexp)
109
+ end
110
+
111
+ def to_s
112
+ to_sexp
113
+ end
114
+
115
+ def inspect
116
+ format_node(:inspect)
117
+ end
118
+
119
+ def respond_to_missing?(method_name, include_private = false)
120
+ type_query_method?(method_name) || super
121
+ end
122
+
123
+ def method_missing(method_name, *args, &block)
124
+ return type == type_query_name(method_name) if type_query_method?(method_name) && args.empty? && !block
125
+
126
+ super
127
+ end
128
+
129
+ # @return [Array<String>] with authors from the current expression range
130
+ def blame_authors
131
+ `git blame -L #{expression.first_line},#{expression.last_line} #{buffer_name}`.lines.map do |line|
132
+ line.split('(')[1].split(/\d+/).first.strip
133
+ end
134
+ end
135
+
136
+ # @return [String] with the first element from #blame_authors
137
+ def author
138
+ blame_authors.first
139
+ end
140
+
141
+ # Search recursively into a node and its children using a pattern.
142
+ # @param [String] pattern
143
+ # @param [Array] *args extra arguments to interpolate in the pattern.
144
+ # @return [Array<Fast::Node>>] with files and results
145
+ def search(pattern, *args)
146
+ Fast.search(pattern, self, *args)
147
+ end
148
+
149
+ # Captures elements from search recursively
150
+ # @param [String] pattern
151
+ # @param [Array] *args extra arguments to interpolate in the pattern.
152
+ # @return [Array<Fast::Node>>] with files and results
153
+ def capture(pattern, *args)
154
+ Fast.capture(pattern, self, *args)
155
+ end
156
+
157
+ private
158
+
159
+ def assign_parents!
160
+ each_child_node do |child|
161
+ self.class.set_parent(child, self)
162
+ child.send(:assign_parents!) if child.respond_to?(:assign_parents!, true)
163
+ end
164
+ end
165
+
166
+ def type_query_method?(method_name)
167
+ method_name.to_s.end_with?('_type?')
168
+ end
169
+
170
+ def type_query_name(method_name)
171
+ method_name.to_s.delete_suffix('_type?').to_sym
172
+ end
173
+
174
+ def format_node(style)
175
+ opener = style == :inspect ? "s(:#{type}" : "(#{type}"
176
+ separator = style == :inspect ? ', ' : ' '
177
+ inline_children = children.map { |child| format_atom(child, style, inline: true) }
178
+ return "#{opener})" if inline_children.empty?
179
+
180
+ if children.none? { |child| Fast.ast_node?(child) } && inline_children.all? { |child| !child.include?("\n") }
181
+ return "#{opener}#{separator}#{inline_children.join(separator)})"
182
+ end
183
+
184
+ lines = [opener]
185
+ current_line = +''
186
+
187
+ children.each do |child|
188
+ formatted = format_atom(child, style, inline: false)
189
+ if formatted.include?("\n")
190
+ flush_current_line!(lines, current_line, style)
191
+ current_line.clear
192
+ lines << indent_multiline(formatted)
193
+ elsif Fast.ast_node?(child)
194
+ flush_current_line!(lines, current_line, style)
195
+ current_line = formatted.dup
196
+ else
197
+ current_line << separator unless current_line.empty?
198
+ current_line << formatted
199
+ end
200
+ end
201
+
202
+ flush_current_line!(lines, current_line, style)
203
+ if lines.length > 2 && scalar_line?(lines[1], style)
204
+ lines[0] = "#{lines[0]}#{separator}#{lines[1].strip}"
205
+ lines.delete_at(1)
206
+ end
207
+ lines[-1] = "#{lines[-1]})"
208
+ lines.join("\n")
209
+ end
210
+
211
+ def flush_current_line!(lines, current_line, style)
212
+ return if current_line.empty?
213
+
214
+ line = style == :inspect ? " #{current_line}," : " #{current_line}"
215
+ lines << line
216
+ end
217
+
218
+ def indent_multiline(text)
219
+ text.lines.map { |line| " #{line}" }.join
220
+ end
221
+
222
+ def format_atom(atom, style, inline:)
223
+ if Fast.ast_node?(atom)
224
+ text = style == :inspect ? atom.inspect : atom.to_sexp
225
+ return text if inline || !text.include?("\n")
226
+
227
+ style == :inspect ? trim_trailing_comma(text) : text
228
+ else
229
+ format_scalar(atom, style)
230
+ end
231
+ end
232
+
233
+ def trim_trailing_comma(text)
234
+ lines = text.lines
235
+ lines[-1] = lines[-1].sub(/,\z/, '')
236
+ lines.join
237
+ end
238
+
239
+ def scalar_line?(line, style)
240
+ stripped = line.strip
241
+ return false if stripped.empty?
242
+
243
+ opener = style == :inspect ? 's(' : '('
244
+ !stripped.start_with?(opener)
245
+ end
246
+
247
+ def format_scalar(value, _style)
248
+ case value
249
+ when Symbol, String
250
+ value.inspect
251
+ when Array
252
+ "[#{value.map { |item| format_atom(item, :sexp, inline: true) }.join(', ')}]"
253
+ else
254
+ value.inspect
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,327 @@
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::XStringNode
139
+ build_node(:xstr, [node.unescaped], node, source, buffer_name)
140
+ when Prism::InterpolatedStringNode
141
+ build_node(:dstr, node.parts.filter_map { |part| adapt(part, source, buffer_name) }, node, source, buffer_name)
142
+ when Prism::InterpolatedXStringNode
143
+ build_node(:dxstr, node.parts.filter_map { |part| adapt(part, source, buffer_name) }, node, source, buffer_name)
144
+ when Prism::InterpolatedSymbolNode
145
+ build_node(:dsym, node.parts.filter_map { |part| adapt(part, source, buffer_name) }, node, source, buffer_name)
146
+ when Prism::ArrayNode
147
+ build_node(:array, node.elements.map { |child| adapt(child, source, buffer_name) }, node, source, buffer_name)
148
+ when Prism::HashNode
149
+ build_node(:hash, node.elements.map { |child| adapt(child, source, buffer_name) }, node, source, buffer_name)
150
+ when Prism::KeywordHashNode
151
+ build_node(:hash, node.elements.map { |child| adapt(child, source, buffer_name) }, node, source, buffer_name)
152
+ when Prism::AssocNode
153
+ build_node(:pair, [adapt(node.key, source, buffer_name), adapt(node.value, source, buffer_name)], node, source, buffer_name)
154
+ when Prism::SelfNode
155
+ build_node(:self, [], node, source, buffer_name)
156
+ when Prism::LocalVariableReadNode
157
+ build_node(:lvar, [node.name], node, source, buffer_name)
158
+ when Prism::InstanceVariableReadNode
159
+ build_node(:ivar, [node.name], node, source, buffer_name)
160
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode
161
+ build_node(:ivasgn, [node.name, adapt(node.value, source, buffer_name)], node, source, buffer_name)
162
+ when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode
163
+ build_node(:lvasgn, [node.name, adapt(node.value, source, buffer_name)], node, source, buffer_name)
164
+ when Prism::LocalVariableOperatorWriteNode
165
+ build_node(
166
+ :op_asgn,
167
+ [
168
+ build_node(:lvasgn, [node.name], node, source, buffer_name),
169
+ (node.respond_to?(:binary_operator) ? node.binary_operator : node.operator),
170
+ adapt(node.value, source, buffer_name)
171
+ ],
172
+ node,
173
+ source,
174
+ buffer_name
175
+ )
176
+ when Prism::IntegerNode
177
+ build_node(:int, [node.value], node, source, buffer_name)
178
+ when Prism::FloatNode
179
+ build_node(:float, [node.value], node, source, buffer_name)
180
+ when Prism::TrueNode
181
+ build_node(:true, [], node, source, buffer_name)
182
+ when Prism::FalseNode
183
+ build_node(:false, [], node, source, buffer_name)
184
+ when Prism::NilNode
185
+ build_node(:nil, [], node, source, buffer_name)
186
+ when Prism::IfNode
187
+ 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)
188
+ when Prism::UnlessNode
189
+ 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)
190
+ when Prism::RescueModifierNode
191
+ build_node(:rescue, [adapt(node.expression, source, buffer_name), build_node(:resbody, [nil, nil, adapt(node.rescue_expression, source, buffer_name)], node, source, buffer_name), nil], node, source, buffer_name)
192
+ when Prism::CaseNode
193
+ children = [adapt(node.predicate, source, buffer_name)]
194
+ children.concat(node.conditions.map { |condition| adapt(condition, source, buffer_name) })
195
+ else_clause =
196
+ if node.respond_to?(:else_clause)
197
+ node.else_clause
198
+ elsif node.respond_to?(:consequent)
199
+ node.consequent
200
+ end
201
+ children << adapt_else_clause(else_clause, source, buffer_name) if else_clause
202
+ build_node(:case, children, node, source, buffer_name)
203
+ when Prism::WhenNode
204
+ condition =
205
+ if node.conditions.length == 1
206
+ adapt(node.conditions.first, source, buffer_name)
207
+ else
208
+ build_node(:array, node.conditions.map { |child| adapt(child, source, buffer_name) }, node, source, buffer_name)
209
+ end
210
+ build_node(:when, [condition, adapt(node.statements, source, buffer_name)].compact, node, source, buffer_name)
211
+ when Prism::ElseNode
212
+ adapt_else_clause(node, source, buffer_name)
213
+ when Prism::BeginNode, Prism::EmbeddedStatementsNode
214
+ statements = adapt_statements(node.statements, source, buffer_name)
215
+ children = statements.is_a?(Node) && statements.type == :begin ? statements.children : Array(statements)
216
+ build_node(:begin, children, node, source, buffer_name)
217
+ when Prism::EmbeddedVariableNode
218
+ build_node(:begin, [adapt(node.variable, source, buffer_name)].compact, node, source, buffer_name)
219
+ when Prism::LambdaNode
220
+ build_node(:lambda, [adapt_block_parameters(node.parameters, source, buffer_name), adapt(node.body, source, buffer_name)], node, source, buffer_name)
221
+ else
222
+ nil
223
+ end
224
+ end
225
+
226
+ def adapt_statements(node, source, buffer_name)
227
+ return nil unless node
228
+
229
+ children = node.body.filter_map { |child| adapt(child, source, buffer_name) }
230
+ return nil if children.empty?
231
+ return children.first if children.one?
232
+
233
+ build_node(:begin, children, node, source, buffer_name)
234
+ end
235
+
236
+ def adapt_parameters(node, source, buffer_name)
237
+ return build_node(:args, [], nil, source, buffer_name) unless node
238
+
239
+ children = []
240
+ children.concat(node.requireds.map { |child| adapt_required_parameter(child, source, buffer_name) }) if node.respond_to?(:requireds)
241
+ children.concat(node.optionals.map { |child| build_node(:optarg, [parameter_name(child), adapt(child.value, source, buffer_name)], child, source, buffer_name) }) if node.respond_to?(:optionals)
242
+ children << build_node(:restarg, [parameter_name(node.rest)], node.rest, source, buffer_name) if node.respond_to?(:rest) && node.rest
243
+ children.concat(node.posts.map { |child| adapt_required_parameter(child, source, buffer_name) }) if node.respond_to?(:posts)
244
+ children.concat(node.keywords.map { |child| adapt_keyword_parameter(child, source, buffer_name) }) if node.respond_to?(:keywords)
245
+ children << build_node(:kwrestarg, [parameter_name(node.keyword_rest)], node.keyword_rest, source, buffer_name) if node.respond_to?(:keyword_rest) && node.keyword_rest
246
+ children << build_node(:blockarg, [parameter_name(node.block)], node.block, source, buffer_name) if node.respond_to?(:block) && node.block
247
+ build_node(:args, children, node, source, buffer_name)
248
+ end
249
+
250
+ def adapt_block_parameters(node, source, buffer_name)
251
+ return build_node(:args, [], nil, source, buffer_name) unless node
252
+
253
+ params = node.respond_to?(:parameters) ? node.parameters : node
254
+ adapt_parameters(params, source, buffer_name)
255
+ end
256
+
257
+ def adapt_required_parameter(child, source, buffer_name)
258
+ if child.is_a?(Prism::MultiTargetNode)
259
+ mlhs_children = child.lefts.map { |c| adapt_required_parameter(c, source, buffer_name) }
260
+ mlhs_children << build_node(:restarg, [parameter_name(child.rest)], child.rest, source, buffer_name) if child.respond_to?(:rest) && child.rest
261
+ mlhs_children.concat(child.rights.map { |c| adapt_required_parameter(c, source, buffer_name) }) if child.respond_to?(:rights)
262
+ build_node(:mlhs, mlhs_children, child, source, buffer_name)
263
+ else
264
+ build_node(:arg, [parameter_name(child)], child, source, buffer_name)
265
+ end
266
+ end
267
+
268
+ def adapt_keyword_parameter(node, source, buffer_name)
269
+ case node
270
+ when Prism::RequiredKeywordParameterNode
271
+ build_node(:kwarg, [parameter_name(node)], node, source, buffer_name)
272
+ when Prism::OptionalKeywordParameterNode
273
+ build_node(:kwoptarg, [parameter_name(node), adapt(node.value, source, buffer_name)], node, source, buffer_name)
274
+ else
275
+ build_node(:arg, [parameter_name(node)], node, source, buffer_name)
276
+ end
277
+ end
278
+
279
+ def adapt_call_node(node, source, buffer_name)
280
+ children = [adapt(node.receiver, source, buffer_name), node.name]
281
+ children.concat(node.arguments&.arguments.to_a.map { |arg| adapt(arg, source, buffer_name) } || [])
282
+ children << adapt(node.block, source, buffer_name) if node.respond_to?(:block) && node.block && !node.block.is_a?(Prism::BlockNode)
283
+ return build_node(:send, children, node, source, buffer_name) unless node.respond_to?(:block) && node.block.is_a?(Prism::BlockNode)
284
+
285
+ end_offset = node.block.location.start_offset
286
+ while end_offset > node.location.start_offset && source.byteslice(end_offset - 1, 1)&.match?(/\s/)
287
+ end_offset -= 1
288
+ end
289
+
290
+ loc = Location.new(
291
+ buffer_name,
292
+ source,
293
+ node.location.start_offset,
294
+ end_offset,
295
+ prism_node: node
296
+ )
297
+ send_node = Node.new(:send, children: children, loc: loc)
298
+ loc.node = send_node
299
+ send_node
300
+ end
301
+
302
+ def adapt_else_clause(node, source, buffer_name)
303
+ adapt(node.statements, source, buffer_name)
304
+ end
305
+
306
+ def parameter_name(node)
307
+ node.respond_to?(:name) ? node.name : nil
308
+ end
309
+
310
+ def build_const_path(node, source, buffer_name)
311
+ parent = node.parent ? adapt(node.parent, source, buffer_name) : nil
312
+ build_node(:const, [parent, node.child.name], node, source, buffer_name)
313
+ end
314
+
315
+ def build_node(type, children, prism_node, source, buffer_name)
316
+ loc =
317
+ if prism_node
318
+ Location.new(buffer_name, source, prism_node.location.start_offset, prism_node.location.end_offset, prism_node: prism_node)
319
+ else
320
+ Location.new(buffer_name, source, 0, 0)
321
+ end
322
+ node = Node.new(type, children: children, loc: loc)
323
+ loc.node = node
324
+ node
325
+ end
326
+ end
327
+ end